/* * 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.allchan; import java.text.DateFormat; import java.text.SimpleDateFormat; import java.util.Arrays; import java.util.HashMap; import java.util.List; import java.util.Locale; import java.util.TimeZone; import java.util.regex.Pattern; import android.content.Context; import android.content.SharedPreferences; import android.content.res.Resources; import android.graphics.Color; import android.graphics.drawable.Drawable; import android.preference.CheckBoxPreference; import android.preference.ListPreference; import android.preference.Preference; import android.preference.PreferenceGroup; import android.support.v4.content.res.ResourcesCompat; 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.AttachmentModel; 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.RegexUtils; import nya.miku.wishmaster.api.util.UrlPathUtils; import nya.miku.wishmaster.api.util.WakabaUtils; import nya.miku.wishmaster.common.Logger; import nya.miku.wishmaster.http.ExtendedMultipartBuilder; import nya.miku.wishmaster.http.recaptcha.Recaptcha; import nya.miku.wishmaster.http.recaptcha.Recaptcha2; import nya.miku.wishmaster.http.recaptcha.Recaptcha2solved; import nya.miku.wishmaster.http.streamer.HttpRequestModel; import nya.miku.wishmaster.http.streamer.HttpStreamer; import nya.miku.wishmaster.lib.org_json.JSONArray; import nya.miku.wishmaster.lib.org_json.JSONException; import nya.miku.wishmaster.lib.org_json.JSONObject; public class AllchanModule extends CloudflareChanModule { private static final String TAG = "AllchanModule"; private static final String CHAN_NAME = "allchan.su"; private static final String DOMAIN = "allchan.su"; private static final String[] CAPTCHA_TYPES = new String[] { "Node captcha", "Google Recaptcha 2", "Google Recaptcha 2 (fallback)", "Google Recaptcha" }; private static final String[] CAPTCHA_TYPES_KEYS = new String[] { "node-captcha", "recaptcha", "recaptcha-fallback", "recaptchav1" }; private static final String CAPTCHA_TYPE_DEFAULT = "recaptcha"; private static final int CAPTCHA_NODE = 1; private static final int CAPTCHA_RECAPTCHA = 2; private static final int CAPTCHA_RECAPTCHA_FALLBACK = 3; private static final int CAPTCHA_RECAPTCHA_V1 = 4; private static final String RECAPTCHA_PUBLIC_KEY = "6LfKRgcTAAAAAIe-bmV_pCbMzvKvBZGbZNRsfmED"; private static final String PREF_KEY_CAPTCHA_TYPE = "PREF_KEY_CAPTCHA_TYPE"; public static final String[] CATALOG_TYPES = new String[] { "date", "recent", "bumps" }; public static final String[] CATALOG_DESCRIPTIONS = new String[] { "Сортировать по дате создания", "Сортировать по дате последнего поста", "Сортировать по количеству бампов" }; private static final List<String> SFW_BOARDS = Arrays.asList("a", "cg", "d", "echo", "int", "mlp", "po", "pr", "rf", "rpg", "soc", "vg"); private static final String[] RATINGS = new String[] { "SFW", "R-15", "R-18", "R-18G" }; private static final DateFormat DATE_FORMAT = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss.SSS'Z'", Locale.US); static { DATE_FORMAT.setTimeZone(TimeZone.getTimeZone("GMT")); } private static final Pattern COMMENT_QUOTE = Pattern.compile("<span class=\"quotation\">", Pattern.LITERAL); private HashMap<String, BoardModel> boardsMap; private int captchaType; private String nodeCaptchaKey; private Recaptcha recaptchaV1; public AllchanModule(SharedPreferences preferences, Resources resources) { super(preferences, resources); } @Override public String getChanName() { return CHAN_NAME; } @Override public String getDisplayingName() { return "AllChan"; } @Override public Drawable getChanFavicon() { return ResourcesCompat.getDrawable(resources, R.drawable.favicon_allchan, null); } private boolean useHttps() { return useHttps(true); } private String getUsingUrl() { return (useHttps() ? "https://" : "http://") + DOMAIN + "/"; } @Override protected String getCloudflareCookieDomain() { return DOMAIN; } @Override public void addPreferencesOnScreen(PreferenceGroup preferenceGroup) { Context context = preferenceGroup.getContext(); addOnlyNewPostsPreference(preferenceGroup, true); final ListPreference captchaPreference = new LazyPreferences.ListPreference(context); //captcha_type captchaPreference.setTitle(R.string.pref_captcha_type); captchaPreference.setDialogTitle(R.string.pref_captcha_type); captchaPreference.setKey(getSharedKey(PREF_KEY_CAPTCHA_TYPE)); captchaPreference.setEntryValues(CAPTCHA_TYPES_KEYS); captchaPreference.setEntries(CAPTCHA_TYPES); captchaPreference.setDefaultValue(CAPTCHA_TYPE_DEFAULT); int i = Arrays.asList(CAPTCHA_TYPES_KEYS).indexOf(preferences.getString(getSharedKey(PREF_KEY_CAPTCHA_TYPE), CAPTCHA_TYPE_DEFAULT)); if (i >= 0) captchaPreference.setSummary(CAPTCHA_TYPES[i]); preferenceGroup.addPreference(captchaPreference); addHttpsPreference(preferenceGroup, true); addCloudflareRecaptchaFallbackPreference(preferenceGroup); addProxyPreferences(preferenceGroup); final CheckBoxPreference proxyPreference = (CheckBoxPreference) preferenceGroup.findPreference(getSharedKey(PREF_KEY_USE_PROXY)); proxyPreference.setOnPreferenceClickListener(new Preference.OnPreferenceClickListener() { @Override public boolean onPreferenceClick(Preference preference) { if (proxyPreference.isChecked() && captchaPreference.getValue().equals("recaptcha")) { captchaPreference.setValue("recaptcha-fallback"); } return false; } }); captchaPreference.setOnPreferenceChangeListener(new Preference.OnPreferenceChangeListener() { @Override public boolean onPreferenceChange(Preference preference, Object newValue) { if (proxyPreference.isChecked() && newValue.equals("recaptcha")) { captchaPreference.setValue("recaptcha-fallback"); return false; } return true; } }); } private boolean loadOnlyNewPosts() { return loadOnlyNewPosts(true); } private int getUsingCaptchaType() { String key = preferences.getString(getSharedKey(PREF_KEY_CAPTCHA_TYPE), CAPTCHA_TYPE_DEFAULT); if (Arrays.asList(CAPTCHA_TYPES_KEYS).indexOf(key) == -1) key = CAPTCHA_TYPE_DEFAULT; switch (key) { case "node-captcha": return CAPTCHA_NODE; case "recaptcha": return preferences.getBoolean(getSharedKey(PREF_KEY_USE_PROXY), false) ? CAPTCHA_RECAPTCHA_FALLBACK : CAPTCHA_RECAPTCHA; case "recaptcha-fallback": return CAPTCHA_RECAPTCHA_FALLBACK; case "recaptchav1": return CAPTCHA_RECAPTCHA_V1; } throw new IllegalStateException(); } @Override public SimpleBoardModel[] getBoardsList(ProgressListener listener, CancellableTask task, SimpleBoardModel[] oldBoardsList) throws Exception { String url = getUsingUrl() + "misc/boards.json"; JSONObject json = downloadJSONObject(url, oldBoardsList != null, listener, task); if (json == null) return oldBoardsList; try { JSONArray boards = json.getJSONArray("boards"); SimpleBoardModel[] list = new SimpleBoardModel[boards.length()]; for (int i=0; i<boards.length(); ++i) { BoardModel board = mapBoardModel(boards.getJSONObject(i)); list[i] = new SimpleBoardModel(board); } return list; } catch (JSONException e) { throw new Exception(json.getString("errorDescription")); } } private void addToMap(BoardModel model) { if (boardsMap == null) boardsMap = new HashMap<>(); BoardModel previous = boardsMap.put(model.boardName, model); if (model.lastPage == BoardModel.LAST_PAGE_UNDEFINED && previous != null) model.lastPage = previous.lastPage; } private BoardModel mapBoardModel(JSONObject json) { BoardModel model = new BoardModel(); model.chan = CHAN_NAME; model.boardName = json.getString("name"); model.boardDescription = json.optString("title", model.boardName); model.nsfw = SFW_BOARDS.indexOf(model.boardName) == -1; model.uniqueAttachmentNames = true; model.timeZoneId = "GMT+3"; model.defaultUserName = json.optString("defaultUserName", "Аноним"); model.bumpLimit = json.optInt("bumpLimit", 500); model.readonlyBoard = !json.optBoolean("postingEnabled", true); int attachmentsCount = json.optInt("maxFileCount", 1); model.requiredFileForNewThread = attachmentsCount > 0; model.allowDeletePosts = true; model.allowDeleteFiles = true; model.allowReport = BoardModel.REPORT_NOT_ALLOWED; model.allowNames = true; model.allowSubjects = true; model.allowSage = true; model.allowEmails = true; model.ignoreEmailIfSage = true; model.allowCustomMark = true; model.allowRandomHash = true; model.allowIcons = true; model.iconDescriptions = RATINGS; model.attachmentsMaxCount = attachmentsCount; model.attachmentsFormatFilters = null; model.markType = BoardModel.MARK_BBCODE; model.firstPage = 0; model.lastPage = BoardModel.LAST_PAGE_UNDEFINED; model.searchAllowed = false; model.catalogAllowed = true; model.catalogTypeDescriptions = CATALOG_DESCRIPTIONS; addToMap(model); return model; } @Override public BoardModel getBoard(String shortName, ProgressListener listener, CancellableTask task) throws Exception { if (boardsMap != null && boardsMap.containsKey(shortName)) return boardsMap.get(shortName); String url = getUsingUrl() + "misc/board/b.json"; JSONObject json = downloadJSONObject(url, false, listener, task); try { BoardModel board = mapBoardModel(json.getJSONObject("board")); addToMap(board); return board; } catch (JSONException e) { throw new Exception(json.getString("errorDescription")); } } @Override public ThreadModel[] getThreadsList(String boardName, int page, ProgressListener listener, CancellableTask task, ThreadModel[] oldList) throws Exception { String url = getUsingUrl() + boardName + "/" + Integer.toString(page) + ".json"; JSONObject json = downloadJSONObject(url, oldList != null, listener, task); if (json == null) return oldList; try { try { BoardModel board = mapBoardModel(json.optJSONObject("board")); addToMap(board); board.lastPage = Math.max(json.getInt("pageCount") - 1, 0); } catch (Exception e) { Logger.e(TAG, e); } JSONArray threads = json.getJSONArray("threads"); ThreadModel[] list = new ThreadModel[threads.length()]; for (int i=0; i<threads.length(); ++i) { list[i] = mapThreadModel(threads.getJSONObject(i)); } return list; } catch (JSONException e) { Logger.e(TAG, e); throw new Exception(json.getString("errorDescription")); } } @Override public ThreadModel[] getCatalog(String boardName, int catalogType, ProgressListener listener, CancellableTask task, ThreadModel[] oldList) throws Exception { String url = getUsingUrl() + boardName + "/catalog.json"; if (catalogType > 0) url += "?sort=" + CATALOG_TYPES[catalogType]; JSONObject json = downloadJSONObject(url, oldList != null, listener, task); if (json == null) return oldList; try { try { BoardModel board = mapBoardModel(json.getJSONObject("board")); addToMap(board); } catch (Exception e) { Logger.e(TAG, e); } JSONArray threads = json.getJSONArray("threads"); ThreadModel[] list = new ThreadModel[threads.length()]; for (int i=0; i<threads.length(); ++i) { list[i] = mapThreadModel(threads.getJSONObject(i)); } return list; } catch (JSONException e) { throw new Exception(json.getString("errorDescription")); } } private ThreadModel mapThreadModel(JSONObject json) { ThreadModel model = new ThreadModel(); model.postsCount = json.optInt("postCount", -1); model.attachmentsCount = -1; model.isSticky = json.optBoolean("fixed"); model.isClosed = json.optBoolean("closed"); JSONArray postsJson = json.optJSONArray("lastPosts"); PostModel[] posts = new PostModel[postsJson == null ? 1 : 1 + postsJson.length()]; posts[0] = mapPostModel(json.getJSONObject("opPost"), null); for (int i=1; i<posts.length; ++i) { posts[i] = mapPostModel(postsJson.getJSONObject(i-1), posts[0].number); } model.posts = posts; model.threadNumber = posts[0].number; return model; } @Override public PostModel[] getPostsList(String boardName, String threadNumber, ProgressListener listener, CancellableTask task, PostModel[] oldList) throws Exception { if (oldList != null && oldList.length > 0 && loadOnlyNewPosts()) { String lastPostNumberUrl = getUsingUrl() + "api/threadLastPostNumber.json?boardName=" + boardName + "&threadNumber=" + threadNumber; try { JSONObject lastPostNumber = downloadJSONObject(lastPostNumberUrl, false, listener, task); if (lastPostNumber.optString("lastPostNumber").equals(oldList[oldList.length-1].number)) { return oldList; } } catch (Exception e) { Logger.e(TAG, e); } } String url = getUsingUrl() + boardName + "/res/" + threadNumber + ".json"; JSONObject json = downloadJSONObject(url, oldList != null, listener, task); if (json == null) return oldList; try { try { BoardModel board = mapBoardModel(json.getJSONObject("board")); addToMap(board); } catch (Exception e) { Logger.e(TAG, e); } PostModel[] newList = mapThreadModel(json.getJSONObject("thread")).posts; if (oldList == null) return newList; return ChanModels.mergePostsLists(Arrays.asList(oldList), Arrays.asList(newList)); } catch (JSONException e) { throw new Exception(json.getString("errorDescription")); } } private PostModel mapPostModel(JSONObject json, String parentThread) { PostModel model = new PostModel(); model.number = json.optString("number", null); if (model.number == null) throw new RuntimeException(); String name = json.optString("name"); int index = name.indexOf("<font color=\""); if (index >= 0) { String color = name.substring(index + 13); int endIndex = color.indexOf('"'); if (endIndex >= 0) { try { color = color.substring(0, endIndex); model.color = Color.parseColor(color); } catch (Exception e) { Logger.e(TAG, "couldn't parse color: " + color, e); } } } model.name = RegexUtils.removeHtmlTags(name); String subject = json.optString("subject"); if (!subject.startsWith("<a href")) { model.subject = RegexUtils.removeHtmlTags(subject); } else { model.comment = subject + "<br/>"; model.subject = ""; } String text = RegexUtils.replaceAll(json.optString("text"), COMMENT_QUOTE, "<span class=\"unkfunc\">"); model.comment = model.comment != null ? (model.comment + text) : text; model.email = json.optString("email"); model.trip = json.optString("tripCode"); model.icons = null; model.op = json.optBoolean("isOp"); model.sage = model.email.toLowerCase(Locale.US).contains("sage"); try { model.timestamp = DATE_FORMAT.parse(json.optString("createdAt")).getTime(); } catch (Exception e) { Logger.e(TAG, e); } model.parentThread = json.optString("threadNumber", parentThread != null ? parentThread : model.number); JSONArray fileInfos = json.optJSONArray("fileInfos"); if (fileInfos != null) { AttachmentModel[] attachments = new AttachmentModel[fileInfos.length()]; for (int i=0; i<fileInfos.length(); ++i) { AttachmentModel attachment = new AttachmentModel(); JSONObject attachmentJson = fileInfos.getJSONObject(i); String boardName = attachmentJson.optString("boardName"); attachment.path = boardName + "/src/" + attachmentJson.optString("name"); int size = attachmentJson.optInt("size", -1); if (size > 0) size /= 1024; attachment.size = size; JSONObject dimensions = attachmentJson.optJSONObject("dimensions"); if (dimensions != null) { int width = dimensions.optInt("width", -1); int height = dimensions.optInt("height", -1); if (Math.min(width, height) > 0) { attachment.width = width; attachment.height = height; } else { attachment.width = attachment.height = -1; } } else { attachment.width = attachment.height = -1; } JSONObject thumbJson = attachmentJson.optJSONObject("thumb"); if (thumbJson != null) { String thumbnail = thumbJson.optString("name", null); if (thumbnail != null) thumbnail = boardName + "/thumb/" + thumbnail; attachment.thumbnail = thumbnail; } String mimeType = attachmentJson.optString("mimeType"); if (mimeType.equals("image/gif")) { attachment.type = AttachmentModel.TYPE_IMAGE_GIF; } else if (mimeType.startsWith("image/")) { attachment.type = AttachmentModel.TYPE_IMAGE_STATIC; } else if (mimeType.startsWith("video/")) { attachment.type = AttachmentModel.TYPE_VIDEO; } else if (mimeType.startsWith("audio/")) { attachment.type = AttachmentModel.TYPE_AUDIO; } else { attachment.type = AttachmentModel.TYPE_OTHER_FILE; } attachment.isSpoiler = attachmentJson.optString("rating").toLowerCase(Locale.US).startsWith("r-18"); attachments[i] = attachment; } model.attachments = attachments; } return model; } @Override public CaptchaModel getNewCaptcha(String boardName, String threadNumber, ProgressListener listener, CancellableTask task) throws Exception { try { String quotaUrl = getUsingUrl() + "api/captchaQuota.json?boardName=" + boardName; int quota = downloadJSONObject(quotaUrl, false, null, task).optInt("quota"); if (quota > 0) return null; } catch (Exception e) { Logger.e(TAG, e); } CaptchaModel captchaModel; int captchaType = getUsingCaptchaType(); switch (captchaType) { case CAPTCHA_NODE: String url = getUsingUrl() + "api/nodeCaptchaImage.json"; JSONObject json = downloadJSONObject(url, false, listener, task); String challenge = json.getString("challenge"); String captchaUrl = getUsingUrl() + "node-captcha/" + json.getString("fileName"); captchaModel = downloadCaptcha(captchaUrl, listener, task); captchaModel.type = CaptchaModel.TYPE_NORMAL_DIGITS; this.captchaType = captchaType; this.nodeCaptchaKey = challenge; return captchaModel; case CAPTCHA_RECAPTCHA: case CAPTCHA_RECAPTCHA_FALLBACK: this.captchaType = captchaType; this.nodeCaptchaKey = null; this.recaptchaV1 = null; return null; case CAPTCHA_RECAPTCHA_V1: this.captchaType = captchaType; this.nodeCaptchaKey = null; this.recaptchaV1 = Recaptcha.obtain(RECAPTCHA_PUBLIC_KEY, task, httpClient, useHttps() ? "https" : "http"); captchaModel = new CaptchaModel(); captchaModel.type = CaptchaModel.TYPE_NORMAL; captchaModel.bitmap = recaptchaV1.bitmap; return captchaModel; default: throw new IllegalStateException(); } } @Override public String sendPost(SendPostModel model, ProgressListener listener, CancellableTask task) throws Exception { String url = getUsingUrl() + "action/create" + (model.threadNumber == null ? "Thread" : "Post"); ExtendedMultipartBuilder postEntityBuilder = ExtendedMultipartBuilder.create().setDelegates(listener, task). addString("boardName", model.boardName); if (model.threadNumber != null) { postEntityBuilder.addString("threadNumber", model.threadNumber); } switch (captchaType) { case CAPTCHA_NODE: postEntityBuilder.addString("nodeCaptchaChallenge", nodeCaptchaKey).addString("nodeCaptchaResponse", model.captchaAnswer); break; case CAPTCHA_RECAPTCHA: case CAPTCHA_RECAPTCHA_FALLBACK: postEntityBuilder.addString("captchaEngine", "google-recaptcha"); String response = Recaptcha2solved.pop(RECAPTCHA_PUBLIC_KEY); if (response == null) { boolean fallback = getUsingCaptchaType() == CAPTCHA_RECAPTCHA_FALLBACK; throw Recaptcha2.obtain(getUsingUrl(), RECAPTCHA_PUBLIC_KEY, null, CHAN_NAME, fallback); } postEntityBuilder.addString("g-recaptcha-response", response); break; case CAPTCHA_RECAPTCHA_V1: postEntityBuilder. addString("captchaEngine", "google-recaptcha-v1"). addString("recaptcha_challenge_field", recaptchaV1.challenge). addString("recaptcha_response_field", model.captchaAnswer); break; } postEntityBuilder. addString("email", model.sage ? "sage" : model.email). addString("name", model.name). addString("subject", model.subject). addString("text", model.comment). addString("signAsOp", model.custommark ? "true" : "false"). addString("password", model.password). addString("markupMode", "EXTENDED_WAKABA_MARK,BB_CODE"); String rating = (model.icon >= 0 && model.icon < RATINGS.length) ? RATINGS[model.icon] : "SFW"; if (model.attachments != null && model.attachments.length > 0) { for (int i=0; i<model.attachments.length; ++i) { postEntityBuilder. addFile("file_" + Integer.toString(i+1), model.attachments[i], model.randomHash). addString("file_" + Integer.toString(i+1) + "_rating", rating); } } HttpRequestModel request = HttpRequestModel.builder().setPOST(postEntityBuilder.build()).build(); JSONObject result = HttpStreamer.getInstance().getJSONObjectFromUrl(url, request, httpClient, null, task, false); try { if (model.threadNumber != null) { return getUsingUrl() + model.boardName + "/res/" + model.threadNumber + ".html#" + result.getInt("postNumber"); } else { return getUsingUrl() + model.boardName + "/res/" + result.getInt("threadNumber") + ".html"; } } catch (JSONException e) { throw new Exception(result.getString("errorDescription")); } } @Override public String deletePost(DeletePostModel model, ProgressListener listener, CancellableTask task) throws Exception { if (model.onlyFiles) { String url = getUsingUrl() + "action/deleteFile"; String postUrl = getUsingUrl() + "api/post.json?boardName=" + model.boardName + "&postNumber=" + model.postNumber; PostModel post = mapPostModel(downloadJSONObject(postUrl, false, null, task), null); if (post.attachments != null) { for (AttachmentModel attachment : post.attachments) { ExtendedMultipartBuilder postEntityBuilder = ExtendedMultipartBuilder.create().setDelegates(listener, task). addString("fileName", attachment.path.substring(attachment.path.lastIndexOf('/') + 1)). addString("password", model.password); HttpRequestModel request = HttpRequestModel.builder().setPOST(postEntityBuilder.build()).build(); JSONObject result = HttpStreamer.getInstance().getJSONObjectFromUrl(url, request, httpClient, null, task, false); String error = result.optString("errorDescription"); if (error.length() > 0) throw new Exception(error); } } } else { String url = getUsingUrl() + "action/deletePost"; ExtendedMultipartBuilder postEntityBuilder = ExtendedMultipartBuilder.create().setDelegates(listener, task). addString("boardName", model.boardName). addString("postNumber", model.postNumber). addString("password", model.password); HttpRequestModel request = HttpRequestModel.builder().setPOST(postEntityBuilder.build()).build(); JSONObject result = HttpStreamer.getInstance().getJSONObjectFromUrl(url, request, httpClient, null, task, false); String error = result.optString("errorDescription"); if (error.length() > 0) throw new Exception(error); } return null; } @Override public String buildUrl(UrlPageModel model) throws IllegalArgumentException { if (!model.chanName.equals(CHAN_NAME)) throw new IllegalArgumentException("wrong chan"); if (model.type == UrlPageModel.TYPE_CATALOGPAGE) return getUsingUrl() + model.boardName + "/catalog.html"; return WakabaUtils.buildUrl(model, getUsingUrl()); } @Override public UrlPageModel parseUrl(String url) throws IllegalArgumentException { String urlPath = UrlPathUtils.getUrlPath(url, DOMAIN); if (urlPath == null) throw new IllegalArgumentException("wrong domain"); if (url.contains("/catalog.html")) { try { int index = url.indexOf("/catalog.html"); String left = url.substring(0, index); UrlPageModel model = new UrlPageModel(); model.chanName = getChanName(); model.type = UrlPageModel.TYPE_CATALOGPAGE; model.boardName = left.substring(left.lastIndexOf('/') + 1); model.catalogType = 0; return model; } catch (Exception e) {} } return WakabaUtils.parseUrlPath(urlPath, CHAN_NAME); } }