org.floens.chan.core.manager.ReplyManager.java Source code

Java tutorial

Introduction

Here is the source code for org.floens.chan.core.manager.ReplyManager.java

Source

/*
 * Clover - 4chan browser https://github.com/Floens/Clover/
 * Copyright (C) 2014  Floens
 *
 * This program is free software: you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation, either version 3 of the License, or
 * (at your option) any later version.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with this program.  If not, see <http://www.gnu.org/licenses/>.
 */
package org.floens.chan.core.manager;

import android.content.Context;
import android.content.Intent;
import android.text.TextUtils;

import com.squareup.okhttp.Callback;
import com.squareup.okhttp.FormEncodingBuilder;
import com.squareup.okhttp.MediaType;
import com.squareup.okhttp.MultipartBuilder;
import com.squareup.okhttp.OkHttpClient;
import com.squareup.okhttp.Request;
import com.squareup.okhttp.RequestBody;
import com.squareup.okhttp.Response;

import org.floens.chan.ChanApplication;
import org.floens.chan.R;
import org.floens.chan.chan.ChanUrls;
import org.floens.chan.core.model.Reply;
import org.floens.chan.core.model.SavedReply;
import org.floens.chan.ui.activity.ImagePickActivity;
import org.floens.chan.utils.Logger;
import org.floens.chan.utils.Utils;

import java.io.File;
import java.io.IOException;
import java.net.HttpCookie;
import java.util.List;
import java.util.Locale;
import java.util.Random;
import java.util.concurrent.TimeUnit;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

/**
 * To send an reply to 4chan.
 */
public class ReplyManager {
    private static final String TAG = "ReplyManager";

    private static final Pattern POST_THREAD_NO_PATTERN = Pattern.compile("<!-- thread:([0-9]+),no:([0-9]+) -->");
    private static final int TIMEOUT = 10000;

    private final Context context;
    private Reply draft;
    private FileListener fileListener;
    private final Random random = new Random();
    OkHttpClient client;

    public ReplyManager(Context context) {
        this.context = context;
        draft = new Reply();

        client = new OkHttpClient();
        client.setConnectTimeout(TIMEOUT, TimeUnit.MILLISECONDS);
        client.setReadTimeout(TIMEOUT, TimeUnit.MILLISECONDS);
        client.setWriteTimeout(TIMEOUT, TimeUnit.MILLISECONDS);
    }

    /**
     * Clear the draft
     */
    public void removeReplyDraft() {
        draft = new Reply();
    }

    /**
     * Set an reply draft.
     *
     * @param value the draft to save.
     */
    public void setReplyDraft(Reply value) {
        draft = value;
    }

    /**
     * Gets the saved reply draft.
     *
     * @return the saved draft or an empty draft.
     */
    public Reply getReplyDraft() {
        return draft;
    }

    /**
     * Add an quote to the comment field. Looks like >>123456789\n
     *
     * @param no the raw no to quote to.
     */
    public void quote(int no) {
        String textToInsert = ">>" + no + "\n";
        draft.comment = new StringBuilder(draft.comment).insert(draft.cursorPosition, textToInsert).toString();
        draft.cursorPosition += textToInsert.length();
    }

    public void quoteInline(int no, String text) {
        String textToInsert = ">>" + no + "\n";
        String[] lines = text.split("\n+");
        for (String line : lines) {
            textToInsert += ">" + line + "\n";
        }

        draft.comment = new StringBuilder(draft.comment).insert(draft.cursorPosition, textToInsert).toString();
        draft.cursorPosition += textToInsert.length();
    }

    /**
     * Pick an file. Starts up the ImagePickActivity.
     *
     * @param listener FileListener to listen on.
     */
    public void pickFile(FileListener listener) {
        fileListener = listener;

        Intent intent = new Intent(context, ImagePickActivity.class);
        intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
        context.startActivity(intent);
    }

    /**
     * Called from ImagePickActivity, sends onFileLoading to the fileListener.
     */
    public void _onPickedFileLoading() {
        if (fileListener != null) {
            fileListener.onFileLoading();
        }
    }

    /**
     * Called from ImagePickActivity. Sends the file to the listening
     * fileListener, and deletes the fileListener.
     */
    public void _onPickedFile(String name, File file) {
        if (fileListener != null) {
            fileListener.onFile(name, file);
        }
        fileListener = null;
    }

    /**
     * Delete the fileListener.
     */
    public void removeFileListener() {
        fileListener = null;
    }

    public static abstract class FileListener {
        public abstract void onFile(String name, File file);

        public abstract void onFileLoading();
    }

    public void postPass(String token, String pin, final PassListener passListener) {
        FormEncodingBuilder formBuilder = new FormEncodingBuilder();

        formBuilder.add("act", "do_login");

        formBuilder.add("id", token);
        formBuilder.add("pin", pin);

        Request.Builder request = new Request.Builder().url(ChanUrls.getPassUrl()).post(formBuilder.build());

        makeOkHttpCall(request, new Callback() {
            @Override
            public void onFailure(Request request, IOException e) {
                final PassResponse res = new PassResponse();
                res.isError = true;
                res.message = context.getString(R.string.pass_error);
                runUI(new Runnable() {
                    public void run() {
                        passListener.onResponse(res);
                    }
                });
            }

            @Override
            public void onResponse(Response response) throws IOException {
                if (!response.isSuccessful()) {
                    onFailure(response.request(), null);
                    return;
                }
                String responseString = response.body().string();
                response.body().close();

                final PassResponse res = new PassResponse();
                if (responseString.contains("Your device is now authorized")) {
                    List<String> cookies = response.headers("Set-Cookie");
                    String passId = null;
                    for (String cookie : cookies) {
                        try {
                            List<HttpCookie> parsedList = HttpCookie.parse(cookie);
                            for (HttpCookie parsed : parsedList) {
                                if (parsed.getName().equals("pass_id") && !parsed.getValue().equals("0")) {
                                    passId = parsed.getValue();
                                }
                            }
                        } catch (IllegalArgumentException ignored) {
                        }
                    }
                    if (passId != null) {
                        res.passId = passId;
                        res.message = "Success! Your device is now authorized.";
                    } else {
                        res.isError = true;
                        res.message = "Could not get pass id";
                    }
                } else {
                    res.isError = true;
                    if (responseString.contains("Your Token must be exactly 10 characters")) {
                        res.message = "Incorrect token";
                    } else if (responseString.contains("You have left one or more fields blank")) {
                        res.message = "You have left one or more fields blank";
                    } else if (responseString.contains("Incorrect Token or PIN")) {
                        res.message = "Incorrect Token or PIN";
                    } else {
                        res.unknownError = true;
                    }
                }

                runUI(new Runnable() {
                    public void run() {
                        passListener.onResponse(res);
                    }
                });
            }
        });
    }

    public static interface PassListener {
        public void onResponse(PassResponse response);
    }

    public static class PassResponse {
        public boolean isError = false;
        public boolean unknownError = false;
        public String responseData = "";
        public String message = "";
        public String passId;
    }

    public void postDelete(final SavedReply reply, boolean onlyImageDelete, final DeleteListener listener) {
        FormEncodingBuilder formBuilder = new FormEncodingBuilder();
        formBuilder.add(Integer.toString(reply.no), "delete");
        if (onlyImageDelete) {
            formBuilder.add("onlyimgdel", "on");
        }
        formBuilder.add("mode", "usrdel");
        formBuilder.add("pwd", reply.password);

        Request.Builder request = new Request.Builder().url(ChanUrls.getDeleteUrl(reply.board))
                .post(formBuilder.build());

        makeOkHttpCall(request, new Callback() {
            @Override
            public void onFailure(Request request, IOException e) {
                final DeleteResponse res = new DeleteResponse();
                res.isNetworkError = true;
                runUI(new Runnable() {
                    @Override
                    public void run() {
                        listener.onResponse(res);
                    }
                });
            }

            @Override
            public void onResponse(Response response) throws IOException {
                if (!response.isSuccessful()) {
                    onFailure(response.request(), null);
                    return;
                }
                String responseString = response.body().string();
                response.body().close();

                final DeleteResponse res = new DeleteResponse();
                res.responseData = responseString;

                if (responseString.contains("You must wait longer before deleting this post")) {
                    res.isUserError = true;
                    res.isTooSoonError = true;
                } else if (responseString.contains("Password incorrect")) {
                    res.isUserError = true;
                    res.isInvalidPassword = true;
                } else if (responseString.contains("You cannot delete a post this old")) {
                    res.isUserError = true;
                    res.isTooOldError = true;
                } else if (responseString.contains("Updating index")) {
                    res.isSuccessful = true;
                }

                runUI(new Runnable() {
                    @Override
                    public void run() {
                        listener.onResponse(res);
                    }
                });
            }
        });
    }

    public interface DeleteListener {
        public void onResponse(DeleteResponse response);
    }

    public static class DeleteResponse {
        public boolean isNetworkError = false;
        public boolean isUserError = false;
        public boolean isInvalidPassword = false;
        public boolean isTooSoonError = false;
        public boolean isTooOldError = false;
        public boolean isSuccessful = false;
        public String responseData = "";
    }

    public void postReply(Reply reply, ReplyListener replyListener) {
        if (reply.usePass) {
            postReplyInternal(reply, replyListener, null);
        } else {
            postReplyInternal(reply, replyListener, reply.captchaResponse);
        }
    }

    private void postReplyInternal(final Reply reply, final ReplyListener replyListener, String captchaHash) {
        reply.password = Long.toHexString(random.nextLong());

        MultipartBuilder formBuilder = new MultipartBuilder();
        formBuilder.type(MultipartBuilder.FORM);

        formBuilder.addFormDataPart("mode", "regist");
        formBuilder.addFormDataPart("pwd", reply.password);

        if (reply.resto >= 0) {
            formBuilder.addFormDataPart("resto", String.valueOf(reply.resto));
        }

        formBuilder.addFormDataPart("name", reply.name);
        formBuilder.addFormDataPart("email", reply.email);

        if (!TextUtils.isEmpty(reply.subject)) {
            formBuilder.addFormDataPart("sub", reply.subject);
        }

        formBuilder.addFormDataPart("com", reply.comment);

        if (captchaHash != null) {
            formBuilder.addFormDataPart("g-recaptcha-response", captchaHash);
        }

        if (reply.file != null) {
            formBuilder.addFormDataPart("upfile", reply.fileName,
                    RequestBody.create(MediaType.parse("application/octet-stream"), reply.file));
        }

        if (reply.spoilerImage) {
            formBuilder.addFormDataPart("spoiler", "on");
        }

        Request.Builder request = new Request.Builder().url(ChanUrls.getReplyUrl(reply.board))
                .post(formBuilder.build());

        if (reply.usePass) {
            request.addHeader("Cookie", "pass_id=" + reply.passId);
        }

        makeOkHttpCall(request, new Callback() {
            @Override
            public void onFailure(Request request, IOException e) {
                final ReplyResponse res = new ReplyResponse();
                res.isNetworkError = true;

                runUI(new Runnable() {
                    public void run() {
                        replyListener.onResponse(res);
                    }
                });
            }

            @Override
            public void onResponse(Response response) throws IOException {
                final ReplyResponse res = new ReplyResponse();
                if (response.isSuccessful()) {
                    onReplyPosted(response.body().string(), reply, res);
                    response.body().close();
                } else {
                    res.isNetworkError = true;
                }

                runUI(new Runnable() {
                    public void run() {
                        replyListener.onResponse(res);
                    }
                });
            }
        });
    }

    private ReplyResponse onReplyPosted(String responseString, Reply reply, ReplyResponse res) {
        res.responseData = responseString;

        if (res.responseData.contains("No file selected")) {
            res.isUserError = true;
            res.isFileError = true;
        } else if (res.responseData.contains("You forgot to solve the CAPTCHA")
                || res.responseData.contains("You seem to have mistyped the CAPTCHA")) {
            res.isUserError = true;
            res.isCaptchaError = true;
        } else if (res.responseData.toLowerCase(Locale.ENGLISH).contains("post successful")) {
            res.isSuccessful = true;

            Matcher matcher = POST_THREAD_NO_PATTERN.matcher(res.responseData);
            int threadNo = -1;
            int no = -1;
            if (matcher.find()) {
                try {
                    threadNo = Integer.parseInt(matcher.group(1));
                    no = Integer.parseInt(matcher.group(2));
                } catch (NumberFormatException err) {
                    err.printStackTrace();
                }
            }

            if (threadNo >= 0 && no >= 0) {
                SavedReply savedReply = new SavedReply();
                savedReply.board = reply.board;
                savedReply.no = no;
                savedReply.password = reply.password;

                ChanApplication.getDatabaseManager().saveReply(savedReply);

                res.threadNo = threadNo;
                res.no = no;
            } else {
                Logger.w(TAG, "No thread & no in the response");
            }
        }
        return res;
    }

    private void makeOkHttpCall(Request.Builder requestBuilder, Callback callback) {
        requestBuilder.header("User-Agent", ChanApplication.getInstance().getUserAgent());
        Request request = requestBuilder.build();
        client.newCall(request).enqueue(callback);
    }

    private void runUI(Runnable runnable) {
        Utils.runOnUiThread(runnable);
    }

    public static interface ReplyListener {
        public void onResponse(ReplyResponse response);
    }

    public static class ReplyResponse {
        /**
         * No response from server.
         */
        public boolean isNetworkError = false;

        /**
         * Some user error, like no file or captcha wrong.
         */
        public boolean isUserError = false;

        /**
         * The userError was an fileError
         */
        public boolean isFileError = false;

        /**
         * The userError was an captchaError
         */
        public boolean isCaptchaError = false;

        /**
         * Received 'post successful'
         */
        public boolean isSuccessful = false;

        /**
         * Raw html from the response. Used to set html in an WebView to the
         * client, when the error was not recognized by Clover.
         */
        public String responseData = "";

        /**
         * The no the post has
         */
        public int no = -1;

        /**
         * The thread no the post has
         */
        public int threadNo = -1;
    }
}