/*
* 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.fourchan;
import java.net.URLDecoder;
import java.net.URLEncoder;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.HashMap;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import cz.msebera.android.httpclient.NameValuePair;
import cz.msebera.android.httpclient.client.entity.UrlEncodedFormEntity;
import cz.msebera.android.httpclient.cookie.Cookie;
import cz.msebera.android.httpclient.impl.cookie.BasicClientCookie;
import cz.msebera.android.httpclient.message.BasicNameValuePair;
import android.app.Activity;
import android.app.AlertDialog;
import android.app.ProgressDialog;
import android.content.Context;
import android.content.DialogInterface;
import android.content.SharedPreferences;
import android.content.res.Resources;
import android.graphics.Bitmap;
import android.graphics.drawable.Drawable;
import android.os.Build;
import android.preference.CheckBoxPreference;
import android.preference.EditTextPreference;
import android.preference.Preference;
import android.preference.PreferenceGroup;
import android.preference.PreferenceScreen;
import android.support.v4.content.res.ResourcesCompat;
import android.text.Html;
import android.text.InputType;
import android.webkit.WebView;
import android.widget.Toast;
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.common.Async;
import nya.miku.wishmaster.http.ExtendedMultipartBuilder;
import nya.miku.wishmaster.http.interactive.SimpleCaptchaException;
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.http.streamer.HttpWrongStatusCodeException;
import nya.miku.wishmaster.lib.org_json.JSONArray;
import nya.miku.wishmaster.lib.org_json.JSONObject;
public class FourchanModule extends CloudflareChanModule {
static final String CHAN_NAME = "4chan.org";
private static final boolean NEW_RECAPTCHA_DEFAULT = Build.VERSION.SDK_INT >= Build.VERSION_CODES.GINGERBREAD_MR1;
private static final String PREF_KEY_NEW_RECAPTCHA = "PREF_KEY_NEW_RECAPTCHA1";
private static final String PREF_KEY_NEW_RECAPTCHA_FALLBACK = "PREF_KEY_NEW_RECAPTCHA_FALLBACK";
private static final String PREF_KEY_PASS_TOKEN = "PREF_KEY_PASS_TOKEN";
private static final String PREF_KEY_PASS_PIN = "PREF_KEY_PASS_PIN";
private static final String PREF_KEY_PASS_COOKIE = "PREF_KEY_PASS_COOKIE";
static final String RECAPTCHA_KEY = "6Ldp2bsSAAAAAAJ5uyx_lx34lJeEpTLVkP5k04qc";
private boolean usingPasscode = false;
private Map<String, BoardModel> boardsMap = null;
private Recaptcha recaptcha = null;
private Recaptcha reportRecaptcha = null;
private String reportCaptchaAnswer = null;
private static final Pattern ERROR_POSTING = Pattern.compile("<span id=\"errmsg\"(?:[^>]*)>(.*?)(?:</span>|<br)");
private static final Pattern SUCCESS_POSTING = Pattern.compile("<!-- thread:(\\d+),no:(\\d+) -->");
public FourchanModule(SharedPreferences preferences, Resources resources) {
super(preferences, resources);
}
@Override
public String getChanName() {
return CHAN_NAME;
}
@Override
public String getDisplayingName() {
return "4chan";
}
@Override
public Drawable getChanFavicon() {
return ResourcesCompat.getDrawable(resources, R.drawable.favicon_4chan, null);
}
@Override
protected void initHttpClient() {
super.initHttpClient();
setPasscodeCookie(preferences.getString(getSharedKey(PREF_KEY_PASS_COOKIE), ""), false);
}
private void addPasscodePreference(PreferenceGroup preferenceGroup) {
final Context context = preferenceGroup.getContext();
PreferenceScreen passScreen = preferenceGroup.getPreferenceManager().createPreferenceScreen(context);
passScreen.setTitle("4chan pass");
EditTextPreference passTokenPreference = new EditTextPreference(context);
EditTextPreference passPINPreference = new EditTextPreference(context);
Preference passLoginPreference = new Preference(context);
Preference passClearPreference = new Preference(context);
passTokenPreference.setTitle("Token");
passTokenPreference.setDialogTitle("Token");
passTokenPreference.setKey(getSharedKey(PREF_KEY_PASS_TOKEN));
passTokenPreference.getEditText().setSingleLine();
passTokenPreference.getEditText().setInputType(InputType.TYPE_CLASS_TEXT | InputType.TYPE_TEXT_VARIATION_VISIBLE_PASSWORD);
passPINPreference.setTitle("PIN");
passPINPreference.setDialogTitle("PIN");
passPINPreference.setKey(getSharedKey(PREF_KEY_PASS_PIN));
passPINPreference.getEditText().setSingleLine();
passPINPreference.getEditText().setInputType(InputType.TYPE_CLASS_NUMBER);
passLoginPreference.setTitle("Log In");
passLoginPreference.setOnPreferenceClickListener(new Preference.OnPreferenceClickListener() {
@Override
public boolean onPreferenceClick(Preference preference) {
if (!useHttps()) Toast.makeText(context, "Using HTTPS even if HTTP is selected", Toast.LENGTH_SHORT).show();
final String token = preferences.getString(getSharedKey(PREF_KEY_PASS_TOKEN), "");
final String pin = preferences.getString(getSharedKey(PREF_KEY_PASS_PIN), "");
final String authUrl = "https://sys.4chan.org/auth"; //only https
final CancellableTask passAuthTask = new CancellableTask.BaseCancellableTask();
final ProgressDialog passAuthProgressDialog = new ProgressDialog(context);
passAuthProgressDialog.setMessage("Logging in");
passAuthProgressDialog.setOnCancelListener(new DialogInterface.OnCancelListener() {
@Override
public void onCancel(DialogInterface dialog) {
passAuthTask.cancel();
}
});
passAuthProgressDialog.setCanceledOnTouchOutside(false);
passAuthProgressDialog.show();
Async.runAsync(new Runnable() {
@Override
public void run() {
try {
if (passAuthTask.isCancelled()) return;
setPasscodeCookie(null, true);
List<NameValuePair> pairs = new ArrayList<NameValuePair>();
pairs.add(new BasicNameValuePair("act", "do_login"));
pairs.add(new BasicNameValuePair("id", token));
pairs.add(new BasicNameValuePair("pin", pin));
HttpRequestModel request = HttpRequestModel.builder().setPOST(new UrlEncodedFormEntity(pairs, "UTF-8")).build();
String response = HttpStreamer.getInstance().getStringFromUrl(authUrl, request, httpClient, null, passAuthTask, false);
if (passAuthTask.isCancelled()) return;
if (response.contains("Your device is now authorized")) {
String passId = null;
for (Cookie cookie : httpClient.getCookieStore().getCookies()) {
if (cookie.getName().equals("pass_id")) {
String value = cookie.getValue();
if (!value.equals("0")) {
passId = value;
break;
}
}
}
if (passId == null) {
showToast("Could not get pass id");
} else {
setPasscodeCookie(passId, true);
showToast("Success! Your device is now authorized.");
}
} else if (response.contains("Your Token must be exactly 10 characters")) {
showToast("Incorrect token");
} else if (response.contains("You have left one or more fields blank")) {
showToast("You have left one or more fields blank");
} else if (response.contains("Incorrect Token or PIN")) {
showToast("Incorrect Token or PIN");
} else {
Matcher m = Pattern.compile("<strong style=\"color: red; font-size: larger;\">(.*?)</strong>").matcher(response);
if (m.find()) {
showToast(m.group(1));
} else {
showWebView(response);
}
}
} catch (Exception e) {
showToast(e.getMessage() == null ? resources.getString(R.string.error_unknown) : e.getMessage());
} finally {
passAuthProgressDialog.dismiss();
}
}
private void showToast(final String message) {
if (context instanceof Activity) {
((Activity) context).runOnUiThread(new Runnable() {
@Override
public void run() {
Toast.makeText(context, message, Toast.LENGTH_LONG).show();
}
});
}
}
private void showWebView(final String html) {
if (context instanceof Activity) {
((Activity) context).runOnUiThread(new Runnable() {
@Override
public void run() {
WebView webView = new WebView(context);
webView.getSettings().setSupportZoom(true);
webView.loadData(html, "text/html", null);
new AlertDialog.Builder(context).setView(webView).setNeutralButton(android.R.string.ok, null).show();
}
});
}
}
});
return true;
}
});
passClearPreference.setTitle("Reset pass cookie");
passClearPreference.setOnPreferenceClickListener(new Preference.OnPreferenceClickListener() {
@Override
public boolean onPreferenceClick(Preference preference) {
setPasscodeCookie(null, true);
Toast.makeText(context, "Cookie is reset", Toast.LENGTH_LONG).show();
return true;
}
});
passScreen.addPreference(passTokenPreference);
passScreen.addPreference(passPINPreference);
passScreen.addPreference(passLoginPreference);
passScreen.addPreference(passClearPreference);
preferenceGroup.addPreference(passScreen);
}
private void setPasscodeCookie(String cookie, boolean saveToPreferences) {
if (cookie == null || cookie.equals("0")) cookie = "";
if (saveToPreferences) preferences.edit().putString(getSharedKey(PREF_KEY_PASS_COOKIE), cookie).commit();
if (cookie.length() > 0) {
usingPasscode = true;
BasicClientCookie c1 = new BasicClientCookie("pass_id", cookie);
c1.setDomain(".4chan.org");
c1.setPath("/");
httpClient.getCookieStore().addCookie(c1);
BasicClientCookie c2 = new BasicClientCookie("pass_enabled", "1");
c2.setDomain(".4chan.org");
c2.setPath("/");
httpClient.getCookieStore().addCookie(c2);
} else {
usingPasscode = false;
BasicClientCookie c = new BasicClientCookie("pass_id", "0");
c.setDomain(".4chan.org");
c.setPath("/");
httpClient.getCookieStore().addCookie(c);
BasicClientCookie c2 = new BasicClientCookie("pass_enabled", "0");
c2.setDomain(".4chan.org");
c2.setPath("/");
httpClient.getCookieStore().addCookie(c2);
}
}
@Override
public void addPreferencesOnScreen(PreferenceGroup preferenceGroup) {
Context context = preferenceGroup.getContext();
addPasscodePreference(preferenceGroup);
CheckBoxPreference newRecaptchaPref = new LazyPreferences.CheckBoxPreference(context);
newRecaptchaPref.setTitle(R.string.fourchan_prefs_new_recaptcha);
newRecaptchaPref.setSummary(R.string.fourchan_prefs_new_recaptcha_summary);
newRecaptchaPref.setKey(getSharedKey(PREF_KEY_NEW_RECAPTCHA));
newRecaptchaPref.setDefaultValue(NEW_RECAPTCHA_DEFAULT);
preferenceGroup.addPreference(newRecaptchaPref);
final CheckBoxPreference fallbackRecaptchaPref = new LazyPreferences.CheckBoxPreference(context);
fallbackRecaptchaPref.setTitle(R.string.fourchan_prefs_new_recaptcha_fallback);
fallbackRecaptchaPref.setSummary(R.string.fourchan_prefs_new_recaptcha_fallback_summary);
fallbackRecaptchaPref.setKey(getSharedKey(PREF_KEY_NEW_RECAPTCHA_FALLBACK));
fallbackRecaptchaPref.setDefaultValue(false);
preferenceGroup.addPreference(fallbackRecaptchaPref);
fallbackRecaptchaPref.setDependency(getSharedKey(PREF_KEY_NEW_RECAPTCHA));
addPasswordPreference(preferenceGroup);
addHttpsPreference(preferenceGroup, true);
addProxyPreferences(preferenceGroup);
final CheckBoxPreference proxyPreference = (CheckBoxPreference) preferenceGroup.findPreference(getSharedKey(PREF_KEY_USE_PROXY));
fallbackRecaptchaPref.setEnabled(!proxyPreference.isChecked());
proxyPreference.setOnPreferenceClickListener(new Preference.OnPreferenceClickListener() {
@Override
public boolean onPreferenceClick(Preference preference) {
fallbackRecaptchaPref.setEnabled(!proxyPreference.isChecked());
if (proxyPreference.isChecked() && !fallbackRecaptchaPref.isChecked()) fallbackRecaptchaPref.setChecked(true);
return false;
}
});
}
private boolean useHttps() {
return useHttps(true);
}
private boolean useNewRecaptcha() {
return preferences.getBoolean(getSharedKey(PREF_KEY_NEW_RECAPTCHA), NEW_RECAPTCHA_DEFAULT);
}
private boolean newRecaptchaFallback() {
return preferences.getBoolean(getSharedKey(PREF_KEY_USE_PROXY), false) ||
preferences.getBoolean(getSharedKey(PREF_KEY_NEW_RECAPTCHA_FALLBACK), false);
}
@Override
protected boolean cloudflareRecaptchaFallback() {
return newRecaptchaFallback();
}
@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 = (useHttps() ? "https://" : "http://") + "a.4cdn.org/boards.json";
JSONObject boardsJson = downloadJSONObject(url, (oldBoardsList != null && boardsMap != null), listener, task);
if (boardsJson == null) return oldBoardsList;
JSONArray boards = boardsJson.getJSONArray("boards");
for (int i=0, len=boards.length(); i<len; ++i) {
BoardModel model = FourchanJsonMapper.mapBoardModel(boards.getJSONObject(i));
newMap.put(model.boardName, model);
list.add(new SimpleBoardModel(model));
}
boardsMap = newMap;
return list.toArray(new SimpleBoardModel[list.size()]);
}
@Override
public BoardModel getBoard(String shortName, ProgressListener listener, CancellableTask task) throws Exception {
if (boardsMap == null) {
try {
getBoardsList(listener, task, null);
} catch (Exception e) {}
}
if (boardsMap != null && boardsMap.containsKey(shortName)) return boardsMap.get(shortName);
return FourchanJsonMapper.getDefaultBoardModel(shortName);
}
@Override
public ThreadModel[] getCatalog(String boardName, int catalogType, ProgressListener listener, CancellableTask task, ThreadModel[] oldList)
throws Exception {
String url = (useHttps() ? "https://" : "http://") + "a.4cdn.org/" + boardName + "/catalog.json";
JSONArray response = downloadJSONArray(url, oldList != null, listener, task);
if (response == null) return oldList; //if not modified
List<ThreadModel> threads = new ArrayList<>();
for (int i=0, len=response.length(); i<len; ++i) {
JSONArray curArray = response.getJSONObject(i).getJSONArray("threads");
for (int j=0, clen=curArray.length(); j<clen; ++j) {
JSONObject curThreadJson = curArray.getJSONObject(j);
ThreadModel curThread = new ThreadModel();
curThread.threadNumber = Long.toString(curThreadJson.getLong("no"));
curThread.postsCount = curThreadJson.optInt("replies", -2) + 1;
curThread.attachmentsCount = curThreadJson.optInt("images", -2) + 1;
curThread.isSticky = curThreadJson.optInt("sticky") == 1;
curThread.isClosed = curThreadJson.optInt("closed") == 1;
curThread.posts = new PostModel[] { FourchanJsonMapper.mapPostModel(curThreadJson, boardName) };
threads.add(curThread);
}
}
return threads.toArray(new ThreadModel[threads.size()]);
}
@Override
public ThreadModel[] getThreadsList(String boardName, int page, ProgressListener listener, CancellableTask task, ThreadModel[] oldList)
throws Exception {
String url = (useHttps() ? "https://" : "http://") + "a.4cdn.org/" + boardName + "/" + Integer.toString(page) + ".json";
JSONObject response = downloadJSONObject(url, oldList != null, listener, task);
if (response == null) return oldList; //if not modified
JSONArray threads = response.getJSONArray("threads");
ThreadModel[] result = new ThreadModel[threads.length()];
for (int i=0, len=threads.length(); i<len; ++i) {
JSONArray posts = threads.getJSONObject(i).getJSONArray("posts");
JSONObject op = posts.getJSONObject(0);
ThreadModel curThread = new ThreadModel();
curThread.threadNumber = Long.toString(op.getLong("no"));
curThread.postsCount = op.optInt("replies", -2) + 1;
curThread.attachmentsCount = op.optInt("images", -2) + 1;
curThread.isSticky = op.optInt("sticky") == 1;
curThread.isClosed = op.optInt("closed") == 1;
curThread.posts = new PostModel[posts.length()];
for (int j=0, plen=posts.length(); j<plen; ++j) {
curThread.posts[j] = FourchanJsonMapper.mapPostModel(posts.getJSONObject(j), boardName);
}
result[i] = curThread;
}
return result;
}
@Override
public PostModel[] getPostsList(String boardName, String threadNumber, ProgressListener listener, CancellableTask task, PostModel[] oldList)
throws Exception {
String url = (useHttps() ? "https://" : "http://") + "a.4cdn.org/" + boardName + "/thread/" + threadNumber + ".json";
JSONObject response = downloadJSONObject(url, oldList != null, listener, task);
if (response == null) return oldList; //if not modified
JSONArray posts = response.getJSONArray("posts");
PostModel[] result = new PostModel[posts.length()];
for (int i=0, len=posts.length(); i<len; ++i) {
result[i] = FourchanJsonMapper.mapPostModel(posts.getJSONObject(i), boardName);
}
if (oldList != null) {
result = ChanModels.mergePostsLists(Arrays.asList(oldList), Arrays.asList(result));
}
return result;
}
@Override
public PostModel[] search(String boardName, String searchRequest, ProgressListener listener, CancellableTask task) throws Exception {
throw new Exception("Open this page in the browser");
}
@Override
public CaptchaModel getNewCaptcha(String boardName, String threadNumber, ProgressListener listener, CancellableTask task) throws Exception {
if (usingPasscode) return null;
if (useNewRecaptcha()) {
recaptcha = null;
return null;
} else {
recaptcha = Recaptcha.obtain(RECAPTCHA_KEY, task, httpClient, useHttps() ? "https" : "http");
CaptchaModel result = new CaptchaModel();
result.type = CaptchaModel.TYPE_NORMAL;
result.bitmap = recaptcha.bitmap;
return result;
}
}
@Override
public String sendPost(SendPostModel model, ProgressListener listener, CancellableTask task) throws Exception {
String recaptcha2 = Recaptcha2solved.pop(RECAPTCHA_KEY);
if (!usingPasscode) {
if (useNewRecaptcha()) {
if (recaptcha2 == null) throw Recaptcha2.obtain(
(useHttps() ? "https://" : "http://") + "4chan.org/", RECAPTCHA_KEY, null, CHAN_NAME, newRecaptchaFallback());
} else if (recaptcha == null) throw new Exception("Invalid captcha");
}
String url = "https://sys.4chan.org/" + model.boardName + "/post";
ExtendedMultipartBuilder postEntityBuilder = ExtendedMultipartBuilder.create().setDelegates(listener, task).
addString("name", model.name).
addString("email", model.sage ? "sage" : "").
addString("sub", model.subject).
addString("com", model.comment).
addString("mode", "regist").
addString("pwd", model.password);
if (model.threadNumber != null) postEntityBuilder.addString("resto", model.threadNumber);
if (!usingPasscode) {
if (useNewRecaptcha()) {
postEntityBuilder.addString("g-recaptcha-response", recaptcha2);
} else {
postEntityBuilder.addString("recaptcha_challenge_field", recaptcha.challenge).
addString("recaptcha_response_field", model.captchaAnswer);
}
}
if (model.attachments != null && model.attachments.length != 0) postEntityBuilder.addFile("upfile", model.attachments[0]);
if (model.custommark) postEntityBuilder.addString("spoiler", "on");
HttpRequestModel request = HttpRequestModel.builder().setPOST(postEntityBuilder.build()).build();
String response;
try {
response = HttpStreamer.getInstance().getStringFromUrl(url, request, httpClient, listener, task, true);
} catch (HttpWrongStatusCodeException e) {
try {
checkCloudflareError(e, "https://4chan.org");
} catch (Exception cf) {
if (recaptcha2 != null) Recaptcha2solved.push(RECAPTCHA_KEY, recaptcha2);
throw cf;
}
throw e;
}
Matcher errorMatcher = ERROR_POSTING.matcher(response);
if (errorMatcher.find()) {
throw new Exception(Html.fromHtml(errorMatcher.group(1)).toString());
}
Matcher successMatcher = SUCCESS_POSTING.matcher(response);
if (successMatcher.find()) {
UrlPageModel redirect = new UrlPageModel();
redirect.chanName = CHAN_NAME;
redirect.type = UrlPageModel.TYPE_THREADPAGE;
redirect.boardName = model.boardName;
redirect.threadNumber = successMatcher.group(1);
redirect.postNumber = successMatcher.group(2);
if (redirect.threadNumber.equals("0")) redirect.threadNumber = redirect.postNumber;
return buildUrl(redirect);
}
return null;
}
@Override
public String deletePost(DeletePostModel model, ProgressListener listener, CancellableTask task) throws Exception {
String url = "https://sys.4chan.org/" + model.boardName + "/imgboard.php";
ExtendedMultipartBuilder postEntityBuilder = ExtendedMultipartBuilder.create().setDelegates(listener, task).
addString(model.postNumber, "delete");
if (model.onlyFiles) postEntityBuilder.addString("onlyimgdel", "on");
postEntityBuilder.addString("mode", "usrdel").addString("pwd", model.password);
HttpRequestModel request = HttpRequestModel.builder().setPOST(postEntityBuilder.build()).build();
String response = HttpStreamer.getInstance().getStringFromUrl(url, request, httpClient, listener, task, false);
Matcher errorMatcher = ERROR_POSTING.matcher(response);
if (errorMatcher.find()) {
throw new Exception(Html.fromHtml(errorMatcher.group(1)).toString());
}
return null;
}
@Override
public String reportPost(final DeletePostModel model, ProgressListener listener, final CancellableTask task) throws Exception {
if (reportCaptchaAnswer == null) {
throw new SimpleCaptchaException() {
private static final long serialVersionUID = 1L;
@Override
public String getServiceName() {
return "Recaptcha";
}
@Override
protected Bitmap getNewCaptcha() throws Exception {
reportRecaptcha = Recaptcha.obtain(RECAPTCHA_KEY, task, httpClient, "https");
return reportRecaptcha.bitmap;
}
@Override
protected void storeResponse(String response) {
reportCaptchaAnswer = response;
}
};
} else {
String url = "https://sys.4chan.org/" + model.boardName + "/imgboard.php?mode=report&no=" + model.postNumber;
ExtendedMultipartBuilder postEntityBuilder = ExtendedMultipartBuilder.create().setDelegates(listener, task).
addString("cat", "vio").
addString("recaptcha_challenge_field", reportRecaptcha.challenge).
addString("recaptcha_response_field", reportCaptchaAnswer).
addString("board", model.boardName).
addString("no", model.postNumber);
reportCaptchaAnswer = null;
reportRecaptcha = null;
HttpRequestModel request = HttpRequestModel.builder().setPOST(postEntityBuilder.build()).build();
String response = HttpStreamer.getInstance().getStringFromUrl(url, request, httpClient, listener, task, false);
if (response.contains("https://www.4chan.org/banned")) throw new Exception("You can't report posts because you are banned");
if (response.contains("You seem to have mistyped the CAPTCHA")) throw new Exception("You seem to have mistyped the CAPTCHA");
if (response.contains("That post doesn't exist anymore")) throw new Exception("That post doesn't exist anymore");
if (response.contains("You forgot to solve the CAPTCHA")) throw new Exception("You forgot to solve the CAPTCHA");
return null;
}
}
@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(useHttps() ? "https://" : "http://");
try {
switch (model.type) {
case UrlPageModel.TYPE_INDEXPAGE:
return url.append("www.4chan.org").toString();
case UrlPageModel.TYPE_BOARDPAGE:
if (model.boardPage == UrlPageModel.DEFAULT_FIRST_PAGE || model.boardPage == 1)
return url.append("boards.4chan.org/").append(model.boardName).append('/').toString();
return url.append("boards.4chan.org/").append(model.boardName).append('/').append(model.boardPage).toString();
case UrlPageModel.TYPE_CATALOGPAGE:
return url.append("boards.4chan.org/").append(model.boardName).append("/catalog").toString();
case UrlPageModel.TYPE_THREADPAGE:
return url.append("boards.4chan.org/").append(model.boardName).append("/thread/").append(model.threadNumber).
append(model.postNumber == null || model.postNumber.length() == 0 ? "" : ("#p" + model.postNumber)).toString();
case UrlPageModel.TYPE_SEARCHPAGE:
return url.append("boards.4chan.org/").append(model.boardName).append("/catalog#s=").
append(URLEncoder.encode(model.searchRequest, "UTF-8")).toString();
case UrlPageModel.TYPE_OTHERPAGE:
return url.append(model.otherPath.startsWith("/") ? "boards.4chan.org" : "").append(model.otherPath).toString();
}
} catch (Exception e) {}
throw new IllegalArgumentException("wrong page type");
}
@Override
public UrlPageModel parseUrl(String url) throws IllegalArgumentException {
String domain;
String path = "";
Matcher parseUrl = Pattern.compile("https?://(?:www\\.)?(.+)", Pattern.CASE_INSENSITIVE).matcher(url);
if (!parseUrl.find()) throw new IllegalArgumentException("incorrect url");
String urlPath = parseUrl.group(1);
Matcher parsePath = Pattern.compile("(.+?)(?:/(.*))").matcher(urlPath);
if (parsePath.find()) {
domain = parsePath.group(1).toLowerCase(Locale.US);
path = parsePath.group(2);
} else {
domain = parseUrl.group(1).toLowerCase(Locale.US);
}
if (domain.equals("4cdn.org") || domain.endsWith(".4cdn.org")) {
UrlPageModel model = new UrlPageModel();
model.chanName = CHAN_NAME;
model.type = UrlPageModel.TYPE_OTHERPAGE;
model.otherPath = urlPath;
return model;
}
if (!domain.equals("4chan.org") && !domain.endsWith(".4chan.org")) throw new IllegalArgumentException("wrong chan");
UrlPageModel model = new UrlPageModel();
model.chanName = CHAN_NAME;
if (path.length() == 0) {
model.type = UrlPageModel.TYPE_INDEXPAGE;
return model;
}
Matcher threadPage = Pattern.compile("([^/]+)/thread/(\\d+)[^#]*(?:#p(\\d+))?").matcher(path);
if (threadPage.find()) {
model.type = UrlPageModel.TYPE_THREADPAGE;
model.boardName = threadPage.group(1);
model.threadNumber = threadPage.group(2);
model.postNumber = threadPage.group(3);
return model;
}
Matcher pageCatalogSearch = Pattern.compile("([^/]+)/catalog(?:#s=(.+))?").matcher(path);
if (pageCatalogSearch.find()) {
model.boardName = pageCatalogSearch.group(1);
String search = pageCatalogSearch.group(2);
if (search != null) {
model.type = UrlPageModel.TYPE_SEARCHPAGE;
model.searchRequest = search;
try {
model.searchRequest = URLDecoder.decode(model.searchRequest, "UTF-8");
} catch (Exception e) {}
} else {
model.type = UrlPageModel.TYPE_CATALOGPAGE;
model.catalogType = 0;
}
return model;
}
Matcher boardPage = Pattern.compile("([^/]+)(?:/(\\d+)?)?").matcher(path);
if (boardPage.find()) {
model.type = UrlPageModel.TYPE_BOARDPAGE;
model.boardName = boardPage.group(1);
String page = boardPage.group(2);
model.boardPage = page == null ? 1 : Integer.parseInt(page);
return model;
}
throw new IllegalArgumentException("fail to parse");
}
}