nya.miku.wishmaster.chans.makaba.MakabaModule.java Source code

Java tutorial

Introduction

Here is the source code for nya.miku.wishmaster.chans.makaba.MakabaModule.java

Source

/*
 * Overchan Android (Meta Imageboard Client)
 * Copyright (C) 2014-2016  miku-nyan <https://github.com/miku-nyan>
 *     
 * 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 nya.miku.wishmaster.chans.makaba;

import static nya.miku.wishmaster.chans.makaba.MakabaConstants.*;
import static nya.miku.wishmaster.chans.makaba.MakabaJsonMapper.*;

import java.net.URLDecoder;
import java.net.URLEncoder;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.HashMap;
import java.util.Iterator;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

import nya.miku.wishmaster.R;
import nya.miku.wishmaster.api.CloudflareChanModule;
import nya.miku.wishmaster.api.interfaces.CancellableTask;
import nya.miku.wishmaster.api.interfaces.ProgressListener;
import nya.miku.wishmaster.api.models.BoardModel;
import nya.miku.wishmaster.api.models.CaptchaModel;
import nya.miku.wishmaster.api.models.DeletePostModel;
import nya.miku.wishmaster.api.models.PostModel;
import nya.miku.wishmaster.api.models.SendPostModel;
import nya.miku.wishmaster.api.models.SimpleBoardModel;
import nya.miku.wishmaster.api.models.ThreadModel;
import nya.miku.wishmaster.api.models.UrlPageModel;
import nya.miku.wishmaster.api.util.ChanModels;
import nya.miku.wishmaster.api.util.LazyPreferences;
import nya.miku.wishmaster.api.util.UrlPathUtils;
import nya.miku.wishmaster.common.Logger;
import nya.miku.wishmaster.http.ExtendedMultipartBuilder;
import nya.miku.wishmaster.http.streamer.HttpRequestModel;
import nya.miku.wishmaster.http.streamer.HttpStreamer;
import nya.miku.wishmaster.http.streamer.HttpWrongStatusCodeException;
import nya.miku.wishmaster.lib.org_json.JSONArray;
import nya.miku.wishmaster.lib.org_json.JSONException;
import nya.miku.wishmaster.lib.org_json.JSONObject;

import cz.msebera.android.httpclient.HttpEntity;
import cz.msebera.android.httpclient.cookie.Cookie;
import cz.msebera.android.httpclient.impl.cookie.BasicClientCookie;
import android.content.Context;
import android.content.SharedPreferences;
import android.content.res.Resources;
import android.graphics.drawable.Drawable;
import android.preference.CheckBoxPreference;
import android.preference.EditTextPreference;
import android.preference.Preference;
import android.preference.PreferenceCategory;
import android.preference.PreferenceGroup;
import android.support.v4.content.res.ResourcesCompat;
import android.text.InputType;

/**
 * ??, ??? ?? ? ? 2ch.hk ( makaba)
 * @author miku-nyan
 *
 */

public class MakabaModule extends CloudflareChanModule {
    private static final String TAG = "MakabaModule";

    /** -  '2ch.hk' */
    private String domain;
    /** -  'https://2ch.hk/' */
    private String domainUrl;

    private static final String HASHTAG_PREFIX = "\u00A0#";

    /** id  */
    private String captchaId;

    /**  ?  ?? mobile.fcgi */
    private Map<String, BoardModel> boardsMap = null;
    /** ?  ? (? ?,  ??    mobile.fcgi) */
    private Map<String, BoardModel> customBoardsMap = new HashMap<String, BoardModel>();

    public MakabaModule(SharedPreferences preferences, Resources resources) {
        super(preferences, resources);
        updateDomain(preferences.getString(getSharedKey(PREF_KEY_DOMAIN), DEFAULT_DOMAIN),
                preferences.getBoolean(getSharedKey(PREF_KEY_USE_HTTPS_MAKABA), true));
    }

    @Override
    public String getChanName() {
        return CHAN_NAME;
    }

    @Override
    public String getDisplayingName() {
        return ". (2ch.hk)";
    }

    @Override
    public Drawable getChanFavicon() {
        return ResourcesCompat.getDrawable(resources, R.drawable.favicon_makaba, null);
    }

    @Override
    protected void initHttpClient() {
        super.initHttpClient();
        setCookie(preferences.getString(getSharedKey(PREF_KEY_USERCODE_COOKIE_DOMAIN), null), USERCODE_COOKIE_NAME,
                preferences.getString(getSharedKey(PREF_KEY_USERCODE_COOKIE_VALUE), null));
    }

    private void updateDomain(String domain, boolean useHttps) {
        if (domain.endsWith("/"))
            domain = domain.substring(0, domain.length() - 1);
        if (domain.contains("//"))
            domain = domain.substring(domain.indexOf("//") + 2);
        if (domain.equals(""))
            domain = DEFAULT_DOMAIN;
        this.domain = domain;
        this.domainUrl = (useHttps ? "https://" : "http://") + domain + "/";
    }

    /** ? cookie    */
    private void setCookie(String domain, String name, String value) {
        if (value == null || value.equals(""))
            return;
        BasicClientCookie c = new BasicClientCookie(name, value);
        c.setDomain(domain == null || domain.equals("") ? ("." + this.domain) : domain);
        c.setPath("/");
        httpClient.getCookieStore().addCookie(c);
    }

    @Override
    public void saveCookie(Cookie cookie) {
        super.saveCookie(cookie);
        if (cookie != null && cookie.getName().equals(USERCODE_COOKIE_NAME)) {
            preferences.edit().putString(getSharedKey(PREF_KEY_USERCODE_COOKIE_DOMAIN), cookie.getDomain())
                    .putString(getSharedKey(PREF_KEY_USERCODE_COOKIE_VALUE), cookie.getValue()).commit();
        }
    }

    private void saveUsercodeCookie() {
        for (Cookie cookie : httpClient.getCookieStore().getCookies()) {
            if (cookie.getName().equals(USERCODE_COOKIE_NAME) && cookie.getDomain().contains(domain))
                saveCookie(cookie);
        }
    }

    private void addMobileAPIPreference(PreferenceGroup group) {
        final Context context = group.getContext();
        CheckBoxPreference mobileAPIPref = new LazyPreferences.CheckBoxPreference(context);
        mobileAPIPref.setTitle(R.string.makaba_prefs_mobile_api);
        mobileAPIPref.setSummary(R.string.pref_only_new_posts_summary);
        mobileAPIPref.setKey(getSharedKey(PREF_KEY_MOBILE_API));
        mobileAPIPref.setDefaultValue(true);
        group.addPreference(mobileAPIPref);
    }

    /**   ?  ( .. https) */
    private void addDomainPreferences(PreferenceGroup group) {
        Context context = group.getContext();
        Preference.OnPreferenceChangeListener updateDomainListener = new Preference.OnPreferenceChangeListener() {
            @Override
            public boolean onPreferenceChange(Preference preference, Object newValue) {
                if (preference.getKey().equals(getSharedKey(PREF_KEY_DOMAIN))) {
                    updateDomain((String) newValue,
                            preferences.getBoolean(getSharedKey(PREF_KEY_USE_HTTPS_MAKABA), true));
                    return true;
                } else if (preference.getKey().equals(getSharedKey(PREF_KEY_USE_HTTPS_MAKABA))) {
                    updateDomain(preferences.getString(getSharedKey(PREF_KEY_DOMAIN), DEFAULT_DOMAIN),
                            (boolean) newValue);
                    return true;
                }
                return false;
            }
        };
        PreferenceCategory domainCat = new PreferenceCategory(context);
        domainCat.setTitle(R.string.makaba_prefs_domain_category);
        group.addPreference(domainCat);
        EditTextPreference domainPref = new EditTextPreference(context); //  
        domainPref.setTitle(R.string.pref_domain);
        domainPref.setDialogTitle(R.string.pref_domain);
        domainPref.setSummary(resources.getString(R.string.pref_domain_summary, DOMAINS_HINT));
        domainPref.setKey(getSharedKey(PREF_KEY_DOMAIN));
        domainPref.getEditText().setHint(DEFAULT_DOMAIN);
        domainPref.getEditText().setSingleLine();
        domainPref.getEditText().setInputType(InputType.TYPE_CLASS_TEXT | InputType.TYPE_TEXT_VARIATION_URI);
        domainPref.setOnPreferenceChangeListener(updateDomainListener);
        domainCat.addPreference(domainPref);
        CheckBoxPreference httpsPref = new LazyPreferences.CheckBoxPreference(context); //? "? https"
        httpsPref.setTitle(R.string.pref_use_https);
        httpsPref.setSummary(R.string.pref_use_https_summary);
        httpsPref.setKey(getSharedKey(PREF_KEY_USE_HTTPS_MAKABA));
        httpsPref.setDefaultValue(true);
        httpsPref.setOnPreferenceChangeListener(updateDomainListener);
        domainCat.addPreference(httpsPref);
    }

    @Override
    public void addPreferencesOnScreen(final PreferenceGroup preferenceScreen) {
        addMobileAPIPreference(preferenceScreen);
        addCloudflareRecaptchaFallbackPreference(preferenceScreen);
        addDomainPreferences(preferenceScreen);
        addProxyPreferences(preferenceScreen);
    }

    @Override
    public SimpleBoardModel[] getBoardsList(ProgressListener listener, CancellableTask task,
            SimpleBoardModel[] oldBoardsList) throws Exception {
        List<SimpleBoardModel> list = new ArrayList<SimpleBoardModel>();
        Map<String, BoardModel> newMap = new HashMap<String, BoardModel>();

        String url = domainUrl + "makaba/mobile.fcgi?task=get_boards";

        JSONObject mobileBoardsList = downloadJSONObject(url, (oldBoardsList != null && this.boardsMap != null),
                listener, task);
        if (mobileBoardsList == null)
            return oldBoardsList;

        Iterator<String> it = mobileBoardsList.keys();
        while (it.hasNext()) {
            JSONArray category = mobileBoardsList.getJSONArray(it.next());
            for (int i = 0; i < category.length(); ++i) {
                JSONObject currentBoard = category.getJSONObject(i);
                BoardModel model = mapBoardModel(currentBoard, true, resources);
                newMap.put(model.boardName, model);
                list.add(new SimpleBoardModel(model));
            }
        }

        this.boardsMap = newMap;

        SimpleBoardModel[] result = new SimpleBoardModel[list.size()];
        boolean[] copied = new boolean[list.size()];
        int curIndex = 0;
        for (String category : CATEGORIES) {
            for (int i = 0; i < list.size(); ++i) {
                if (list.get(i).boardCategory.equals(category)) {
                    result[curIndex++] = list.get(i);
                    copied[i] = true;
                }
            }
        }
        for (int i = 0; i < list.size(); ++i) {
            if (!copied[i]) {
                result[curIndex++] = list.get(i);
            }
        }

        return result;
    }

    @Override
    public BoardModel getBoard(String shortName, ProgressListener listener, CancellableTask task) throws Exception {
        try {
            if (this.boardsMap == null) {
                try {
                    getBoardsList(listener, task, null);
                } catch (Exception e) {
                    Logger.d(TAG, "cannot update boards list from mobile.fcgi");
                }
            }
            if (this.boardsMap != null) {
                if (this.boardsMap.containsKey(shortName)) {
                    return this.boardsMap.get(shortName);
                }
            }

            if (this.customBoardsMap.containsKey(shortName)) {
                return this.customBoardsMap.get(shortName);
            }

            String url = domainUrl + shortName + "/index.json";
            JSONObject json;
            try {
                json = downloadJSONObject(url, false, listener, task);
            } finally {
                HttpStreamer.getInstance().removeFromModifiedMap(url);
            }
            BoardModel result = mapBoardModel(json, false, resources);
            if (!shortName.equals(result.boardName))
                throw new Exception();
            this.customBoardsMap.put(result.boardName, result);
            return result;
        } catch (Exception e) {
            Logger.e(TAG, e);
            return defaultBoardModel(shortName, resources);
        }
    }

    @Override
    public ThreadModel[] getThreadsList(String boardName, int page, ProgressListener listener, CancellableTask task,
            ThreadModel[] oldList) throws Exception {
        String url = domainUrl + boardName + "/" + (page == 0 ? "index" : Integer.toString(page)) + ".json";
        JSONObject index = downloadJSONObject(url, (oldList != null), listener, task);
        if (index == null)
            return oldList;

        try { // ?  BoardModel  ?  ?? 
            BoardModel boardModel = mapBoardModel(index, false, resources);
            if (boardName.equals(boardModel.boardName)) {
                this.customBoardsMap.put(boardModel.boardName, boardModel);
            }
        } catch (Exception e) {
            /* ?  ? ??   ?,   ?  */ }

        JSONArray threads = index.getJSONArray("threads");
        ThreadModel[] result = new ThreadModel[threads.length()];
        for (int i = 0; i < threads.length(); ++i) {
            result[i] = mapThreadModel(threads.getJSONObject(i), boardName);
        }
        return result;
    }

    @Override
    public ThreadModel[] getCatalog(String boardName, int catalogType, ProgressListener listener,
            CancellableTask task, ThreadModel[] oldList) throws Exception {
        Exception last = null;
        for (String url : new String[] { domainUrl + "makaba/makaba.fcgi?task=catalog&board=" + boardName
                + "&filter=" + CATALOG_TYPES[catalogType] + "&json=1", domainUrl + boardName + "/catalog.json" }) {
            try {
                JSONObject json = downloadJSONObject(url, (oldList != null), listener, task);
                if (json == null)
                    return oldList;
                JSONArray threads = json.getJSONArray("threads");
                ThreadModel[] result = new ThreadModel[threads.length()];
                for (int i = 0; i < threads.length(); ++i) {
                    JSONObject curThread = threads.getJSONObject(i);
                    ThreadModel model = new ThreadModel();
                    model.threadNumber = curThread.getString("num");
                    try {
                        model.postsCount = curThread.getInt("posts_count") + 1;
                        model.attachmentsCount = curThread.getInt("files_count");
                        model.attachmentsCount += curThread.getJSONArray("files").length();
                        model.isSticky = curThread.getInt("sticky") != 0;
                        model.isClosed = curThread.getInt("closed") != 0;
                    } catch (Exception e) {
                        Logger.e(TAG, e);
                    }
                    model.posts = new PostModel[] { mapPostModel(curThread, boardName) };
                    result[i] = model;
                }
                return result;
            } catch (Exception e) {
                last = e;
            }
        }
        throw last == null ? new Exception() : last;
    }

    @Override
    public PostModel[] getPostsList(String boardName, String threadNumber, ProgressListener listener,
            CancellableTask task, PostModel[] oldList) throws Exception {
        boolean mobileAPI = preferences.getBoolean(getSharedKey(PREF_KEY_MOBILE_API), true);
        if (!mobileAPI) {
            String url = domainUrl + boardName + "/res/" + threadNumber + ".json";
            JSONObject object = downloadJSONObject(url, (oldList != null), listener, task);
            if (object == null)
                return oldList;
            JSONArray postsArray = object.getJSONArray("threads").getJSONObject(0).getJSONArray("posts");
            PostModel[] posts = new PostModel[postsArray.length()];
            for (int i = 0; i < postsArray.length(); ++i) {
                posts[i] = mapPostModel(postsArray.getJSONObject(i), boardName);
            }
            if (oldList != null) {
                posts = ChanModels.mergePostsLists(Arrays.asList(oldList), Arrays.asList(posts));
            }
            return posts;
        }
        try {
            String lastPost = threadNumber;
            if (oldList != null && oldList.length > 0) {
                lastPost = oldList[oldList.length - 1].number;
            }
            String url = domainUrl + "makaba/mobile.fcgi?task=get_thread&board=" + boardName + "&thread="
                    + threadNumber + "&num=" + lastPost;
            JSONArray newPostsArray = downloadJSONArray(url, (oldList != null), listener, task);
            if (newPostsArray == null)
                return oldList;
            PostModel[] newPosts = new PostModel[newPostsArray.length()];
            for (int i = 0; i < newPostsArray.length(); ++i) {
                newPosts[i] = mapPostModel(newPostsArray.getJSONObject(i), boardName);
            }
            if (oldList == null || oldList.length == 0) {
                return newPosts;
            } else {
                long lastNum = Long.parseLong(lastPost);
                ArrayList<PostModel> list = new ArrayList<PostModel>(Arrays.asList(oldList));
                for (int i = 0; i < newPosts.length; ++i) {
                    if (Long.parseLong(newPosts[i].number) > lastNum) {
                        list.add(newPosts[i]);
                    }
                }
                return list.toArray(new PostModel[list.size()]);
            }
        } catch (JSONException e) {
            String lastPost = threadNumber;
            if (oldList != null && oldList.length > 0) {
                lastPost = oldList[oldList.length - 1].number;
            }
            String url = domainUrl + "makaba/mobile.fcgi?task=get_thread&board=" + boardName + "&thread="
                    + threadNumber + "&num=" + lastPost;
            JSONObject makabaError = downloadJSONObject(url, (oldList != null), listener, task);
            Integer code = makabaError.has("Code") ? makabaError.getInt("Code") : null;
            if (code != null && code.equals(Integer.valueOf(-404)))
                code = 404;
            String error = code != null ? code.toString() : null;
            String reason = makabaError.has("Error") ? makabaError.getString("Error") : null;
            if (reason != null) {
                if (error != null) {
                    error += ": " + reason;
                } else {
                    error = reason;
                }
            }
            throw error == null ? e : new Exception(error);
        }
    }

    @Override
    public PostModel[] search(String boardName, String searchRequest, ProgressListener listener,
            CancellableTask task) throws Exception {
        String url;
        HttpRequestModel request;
        if (searchRequest.startsWith(HASHTAG_PREFIX)) {
            url = domainUrl + "makaba/makaba.fcgi?task=hashtags&board=" + boardName + "&tag="
                    + URLEncoder.encode(searchRequest.substring(HASHTAG_PREFIX.length()), "UTF-8") + "&json=1";
            request = HttpRequestModel.DEFAULT_GET;
        } else {
            url = domainUrl + "makaba/makaba.fcgi";
            HttpEntity postEntity = ExtendedMultipartBuilder.create().addString("task", "search")
                    .addString("board", boardName).addString("find", searchRequest).addString("json", "1").build();
            request = HttpRequestModel.builder().setPOST(postEntity).build();
        }
        JSONObject response = null;
        try {
            response = HttpStreamer.getInstance().getJSONObjectFromUrl(url, request, httpClient, listener, task,
                    true);
        } catch (HttpWrongStatusCodeException e) {
            checkCloudflareError(e, url);
            throw e;
        }
        if (listener != null)
            listener.setIndeterminate();
        JSONArray posts = response.getJSONArray("posts");
        PostModel[] result = new PostModel[posts.length()];
        for (int i = 0; i < posts.length(); ++i) {
            result[i] = mapPostModel(posts.getJSONObject(i), boardName);
        }
        return result;
    }

    @Override
    public CaptchaModel getNewCaptcha(String boardName, String threadNumber, ProgressListener listener,
            CancellableTask task) throws Exception {
        String response;
        String url = domainUrl + "makaba/captcha.fcgi?type=2chaptcha"
                + (threadNumber != null ? "&action=thread" : "") + ("&board=" + boardName);
        try {
            response = HttpStreamer.getInstance().getStringFromUrl(url, HttpRequestModel.DEFAULT_GET, httpClient,
                    null, task, true);
            if (task != null && task.isCancelled())
                throw new Exception("interrupted");
            if (response.startsWith("DISABLED") || response.startsWith("VIP")) {
                captchaId = null;
                return null;
            } else if (!response.startsWith("CHECK")) {
                throw new Exception("Invalid captcha response");
            }
        } catch (HttpWrongStatusCodeException e) {
            checkCloudflareError(e, url);
            throw e;
        }

        String id = response.substring(response.indexOf('\n') + 1);
        url = domainUrl + "makaba/captcha.fcgi?type=2chaptcha&action=image&id=" + id;
        CaptchaModel captchaModel = downloadCaptcha(url, listener, task);
        captchaModel.type = CaptchaModel.TYPE_NORMAL_DIGITS;
        captchaId = id;
        return captchaModel;
    }

    @Override
    public String sendPost(SendPostModel model, ProgressListener listener, CancellableTask task) throws Exception {
        String url = domainUrl + "makaba/posting.fcgi?json=1";
        ExtendedMultipartBuilder postEntityBuilder = ExtendedMultipartBuilder.create().setDelegates(listener, task)
                .addString("task", "post").addString("board", model.boardName)
                .addString("thread", model.threadNumber == null ? "0" : model.threadNumber);

        postEntityBuilder.addString("comment", model.comment);

        if (captchaId != null) {
            postEntityBuilder.addString("captcha_type", "2chaptcha").addString("2chaptcha_id", captchaId)
                    .addString("2chaptcha_value", model.captchaAnswer);
        }
        if (task != null && task.isCancelled())
            throw new InterruptedException("interrupted");

        if (model.subject != null)
            postEntityBuilder.addString("subject", model.subject);
        if (model.name != null)
            postEntityBuilder.addString("name", model.name);
        if (model.sage)
            postEntityBuilder.addString("email", "sage");
        else if (model.email != null)
            postEntityBuilder.addString("email", model.email);

        if (model.attachments != null) {
            String[] images = new String[] { "image1", "image2", "image3", "image4" };
            for (int i = 0; i < model.attachments.length; ++i) {
                postEntityBuilder.addFile(images[i], model.attachments[i], model.randomHash);
            }
        }

        if (model.icon != -1)
            postEntityBuilder.addString("icon", Integer.toString(model.icon));

        //if (model.watermark) postEntityBuilder.addString("water_mark", "on");
        if (model.custommark)
            postEntityBuilder.addString("op_mark", "1");

        HttpRequestModel request = HttpRequestModel.builder().setPOST(postEntityBuilder.build()).build();
        String response = null;
        try {
            response = HttpStreamer.getInstance().getStringFromUrl(url, request, httpClient, null, task, true);
        } catch (HttpWrongStatusCodeException e) {
            checkCloudflareError(e, url);
            throw e;
        }
        saveUsercodeCookie();
        JSONObject makabaResult = new JSONObject(response);
        try {
            String statusResult = makabaResult.getString("Status");
            if (statusResult.equals("OK")) {
                try {
                    if (model.threadNumber != null) {
                        UrlPageModel redirect = new UrlPageModel();
                        redirect.type = UrlPageModel.TYPE_THREADPAGE;
                        redirect.chanName = CHAN_NAME;
                        redirect.boardName = model.boardName;
                        redirect.threadNumber = model.threadNumber;
                        redirect.postNumber = Long.toString(makabaResult.getLong("Num"));
                        return buildUrl(redirect);
                    }
                } catch (Exception e) {
                    Logger.e(TAG, e);
                }
                return null;
            } else if (statusResult.equals("Redirect")) {
                UrlPageModel redirect = new UrlPageModel();
                redirect.type = UrlPageModel.TYPE_THREADPAGE;
                redirect.chanName = CHAN_NAME;
                redirect.boardName = model.boardName;
                redirect.threadNumber = Long.toString(makabaResult.getLong("Target"));
                return buildUrl(redirect);
            }
        } catch (Exception e) {
        }
        throw new Exception(makabaResult.getString("Reason"));
    }

    @Override
    public String reportPost(DeletePostModel model, ProgressListener listener, CancellableTask task)
            throws Exception {
        String url = domainUrl + "makaba/makaba.fcgi?json=1";
        ExtendedMultipartBuilder postEntityBuilder = ExtendedMultipartBuilder.create().setDelegates(listener, task)
                .addString("task", "report").addString("posts", "").addString("board", model.boardName)
                .addString("thread", model.threadNumber)
                .addString("comment", ">>" + model.postNumber + " " + model.reportReason);

        HttpRequestModel request = HttpRequestModel.builder().setPOST(postEntityBuilder.build()).setNoRedirect(true)
                .build();
        String response = null;
        try {
            response = HttpStreamer.getInstance().getStringFromUrl(url, request, httpClient, null, task, true);
        } catch (HttpWrongStatusCodeException e) {
            checkCloudflareError(e, url);
            throw e;
        }
        try {
            JSONObject json = new JSONObject(response);
            if (json.getString("message_title").equals(" "))
                return null;
            throw new Exception(json.getString("message_title") + " " + json.getString("message"));
        } catch (Exception e) {
            Logger.e(TAG, e);
            throw new Exception(response);
        }
    }

    @Override
    public String buildUrl(UrlPageModel model) throws IllegalArgumentException {
        if (!model.chanName.equals(CHAN_NAME))
            throw new IllegalArgumentException("wrong chan");
        if (model.boardName != null && !model.boardName.matches("\\w*"))
            throw new IllegalArgumentException("wrong board name");
        StringBuilder url = new StringBuilder(this.domainUrl);
        switch (model.type) {
        case UrlPageModel.TYPE_INDEXPAGE:
            break;
        case UrlPageModel.TYPE_BOARDPAGE:
            url.append(model.boardName).append("/");
            if (model.boardPage == UrlPageModel.DEFAULT_FIRST_PAGE)
                model.boardPage = 0;
            if (model.boardPage != 0)
                url.append(model.boardPage).append(".html");
            break;
        case UrlPageModel.TYPE_CATALOGPAGE:
            url.append("makaba.fcgi?task=catalog&board=").append(model.boardName).append("&filter=")
                    .append(CATALOG_TYPES[model.catalogType]);
            break;
        case UrlPageModel.TYPE_SEARCHPAGE:
            if (model.searchRequest.startsWith(HASHTAG_PREFIX)) {
                url.append("makaba/makaba.fcgi?task=hashtags&board=").append(model.boardName).append("&tag=");
                try {
                    url.append(URLEncoder.encode(model.searchRequest.substring(HASHTAG_PREFIX.length()), "UTF-8"));
                } catch (Exception e) {
                    throw new RuntimeException(e);
                }
                break;
            }
            throw new IllegalArgumentException("can't build url for search page");
        case UrlPageModel.TYPE_THREADPAGE:
            url.append(model.boardName).append("/res/").append(model.threadNumber).append(".html");
            if (model.postNumber != null && model.postNumber.length() != 0)
                url.append("#").append(model.postNumber);
            break;
        case UrlPageModel.TYPE_OTHERPAGE:
            url.append(model.otherPath.startsWith("/") ? model.otherPath.substring(1) : model.otherPath);
        }
        return url.toString();
    }

    @Override
    public UrlPageModel parseUrl(String url) throws IllegalArgumentException {
        String path = UrlPathUtils.getUrlPath(url, MakabaModule.this.domain, DOMAINS_LIST);
        if (path == null)
            throw new IllegalArgumentException("wrong domain");
        path = path.toLowerCase(Locale.US);

        UrlPageModel model = new UrlPageModel();
        model.chanName = CHAN_NAME;
        try {
            if (path.startsWith("makaba/makaba.fcgi?") && path.contains("task=catalog")
                    && path.contains("board=")) {
                model.type = UrlPageModel.TYPE_CATALOGPAGE;

                String boardName = path.substring(path.indexOf("board=") + 6);
                if (boardName.indexOf("&") != -1)
                    boardName = boardName.substring(0, boardName.indexOf("&"));
                model.boardName = boardName;

                if (path.indexOf("filter=") == -1) {
                    model.catalogType = 0;
                } else {
                    String catalogType = path.substring(path.indexOf("filter=") + 7);
                    if (catalogType.indexOf("&") != -1)
                        catalogType = catalogType.substring(0, catalogType.indexOf("&"));

                    model.catalogType = Arrays.asList(CATALOG_TYPES).indexOf(catalogType);
                    model.catalogType = model.catalogType == -1 ? 0 : model.catalogType;
                }
            } else if (path.startsWith("makaba/makaba.fcgi?") && path.contains("task=hashtags")
                    && path.contains("board=")) {
                model.type = UrlPageModel.TYPE_SEARCHPAGE;
                String boardName = path.substring(path.indexOf("board=") + 6);
                if (boardName.indexOf("&") != -1)
                    boardName = boardName.substring(0, boardName.indexOf("&"));
                model.boardName = boardName;
                if (path.indexOf("tag") == -1)
                    throw new IllegalStateException("cannot parse hashtag");
                String hashtag = path.substring(path.indexOf("tag=") + 4);
                if (hashtag.indexOf("&") != -1)
                    hashtag = hashtag.substring(0, hashtag.indexOf("&"));
                model.searchRequest = HASHTAG_PREFIX + URLDecoder.decode(hashtag, "UTF-8");
            } else if (path.contains("/res/")) {
                model.type = UrlPageModel.TYPE_THREADPAGE;
                Matcher matcher = Pattern.compile("(.+?)/res/([0-9]+?).html(.*)").matcher(path);
                if (!matcher.find())
                    throw new Exception();
                model.boardName = matcher.group(1);
                model.threadNumber = matcher.group(2);
                if (matcher.group(3).startsWith("#")) {
                    String post = matcher.group(3).substring(1);
                    if (!post.equals(""))
                        model.postNumber = post;
                }
            } else {
                model.type = UrlPageModel.TYPE_BOARDPAGE;

                if (path.indexOf("/") == -1) {
                    if (path.equals(""))
                        throw new Exception();
                    model.boardName = path;
                    model.boardPage = 0;
                } else {
                    model.boardName = path.substring(0, path.indexOf("/"));

                    String page = path.substring(path.lastIndexOf("/") + 1);
                    if (!page.equals("")) {
                        String pageNum = page.substring(0, page.indexOf(".html"));
                        model.boardPage = pageNum.equals("index") ? 0 : Integer.parseInt(pageNum);
                    } else
                        model.boardPage = 0;
                }
            }
        } catch (Exception e) {
            if (path == null || path.length() == 0 || path.equals("/")) {
                model.type = UrlPageModel.TYPE_INDEXPAGE;
            } else {
                model.type = UrlPageModel.TYPE_OTHERPAGE;
                model.otherPath = path;
            }
        }
        return model;
    }
}