/*
* 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;
}
}