Make http calls abstract, call through Site

multisite
Floens 9 years ago
parent aa4f96cacf
commit f8c182bba5
  1. 12
      Clover/app/src/main/java/org/floens/chan/chan/ChanUrls.java
  2. 13
      Clover/app/src/main/java/org/floens/chan/core/di/ChanGraph.java
  3. 43
      Clover/app/src/main/java/org/floens/chan/core/manager/ReplyManager.java
  4. 28
      Clover/app/src/main/java/org/floens/chan/core/model/BoardReference.java
  5. 7
      Clover/app/src/main/java/org/floens/chan/core/model/Loadable.java
  6. 44
      Clover/app/src/main/java/org/floens/chan/core/presenter/ReplyPresenter.java
  7. 27
      Clover/app/src/main/java/org/floens/chan/core/presenter/ThreadPresenter.java
  8. 70
      Clover/app/src/main/java/org/floens/chan/core/site/Site.java
  9. 25
      Clover/app/src/main/java/org/floens/chan/core/site/SiteEndpoints.java
  10. 37
      Clover/app/src/main/java/org/floens/chan/core/site/http/DeleteRequest.java
  11. 24
      Clover/app/src/main/java/org/floens/chan/core/site/http/DeleteResponse.java
  12. 66
      Clover/app/src/main/java/org/floens/chan/core/site/http/HttpCall.java
  13. 63
      Clover/app/src/main/java/org/floens/chan/core/site/http/HttpCallManager.java
  14. 33
      Clover/app/src/main/java/org/floens/chan/core/site/http/LoginRequest.java
  15. 27
      Clover/app/src/main/java/org/floens/chan/core/site/http/LoginResponse.java
  16. 16
      Clover/app/src/main/java/org/floens/chan/core/site/http/Reply.java
  17. 42
      Clover/app/src/main/java/org/floens/chan/core/site/http/ReplyResponse.java
  18. 102
      Clover/app/src/main/java/org/floens/chan/core/site/sites/chan4/Chan4.java
  19. 33
      Clover/app/src/main/java/org/floens/chan/core/site/sites/chan4/Chan4DeleteHttpCall.java
  20. 37
      Clover/app/src/main/java/org/floens/chan/core/site/sites/chan4/Chan4PassHttpCall.java
  21. 2
      Clover/app/src/main/java/org/floens/chan/core/site/sites/chan4/Chan4ReaderRequest.java
  22. 55
      Clover/app/src/main/java/org/floens/chan/core/site/sites/chan4/Chan4ReplyHttpCall.java
  23. 2
      Clover/app/src/main/java/org/floens/chan/core/site/sites/chan4/PostParseCallable.java
  24. 46
      Clover/app/src/main/java/org/floens/chan/ui/controller/PassSettingsController.java
  25. 29
      Clover/app/src/main/java/org/floens/chan/ui/controller/ReportController.java
  26. 4
      Clover/app/src/main/java/org/floens/chan/ui/helper/ImagePickDelegate.java
  27. 2
      Clover/app/src/main/java/org/floens/chan/ui/layout/ReplyLayout.java

@ -24,14 +24,6 @@ public class ChanUrls {
return "6Ldp2bsSAAAAAAJ5uyx_lx34lJeEpTLVkP5k04qc"; return "6Ldp2bsSAAAAAAJ5uyx_lx34lJeEpTLVkP5k04qc";
} }
public static String getReplyUrl(String board) {
return "https://sys.4chan.org/" + board + "/post";
}
public static String getDeleteUrl(String board) {
return "https://sys.4chan.org/" + board + "/imgboard.php";
}
public static String getBoardUrlDesktop(String board) { public static String getBoardUrlDesktop(String board) {
return scheme() + "://boards.4chan.org/" + board + "/"; return scheme() + "://boards.4chan.org/" + board + "/";
} }
@ -52,10 +44,6 @@ public class ChanUrls {
return "https://sys.4chan.org/auth"; return "https://sys.4chan.org/auth";
} }
public static String getReportDomain() {
return "https://sys.4chan.org/";
}
public static String[] getReportCookies(String passId) { public static String[] getReportCookies(String passId) {
return new String[]{"pass_enabled=1;", "pass_id=" + passId + ";"}; return new String[]{"pass_enabled=1;", "pass_id=" + passId + ";"};
} }

@ -8,16 +8,18 @@ import org.floens.chan.chan.ChanLoader;
import org.floens.chan.chan.ChanParser; import org.floens.chan.chan.ChanParser;
import org.floens.chan.core.cache.FileCache; import org.floens.chan.core.cache.FileCache;
import org.floens.chan.core.database.DatabaseManager; import org.floens.chan.core.database.DatabaseManager;
import org.floens.chan.core.http.ReplyManager; import org.floens.chan.core.manager.ReplyManager;
import org.floens.chan.core.manager.BoardManager; import org.floens.chan.core.manager.BoardManager;
import org.floens.chan.core.manager.FilterEngine; import org.floens.chan.core.manager.FilterEngine;
import org.floens.chan.core.manager.WatchManager; import org.floens.chan.core.manager.WatchManager;
import org.floens.chan.core.site.loaders.Chan4ReaderRequest; import org.floens.chan.core.site.http.HttpCallManager;
import org.floens.chan.core.site.sites.chan4.Chan4ReaderRequest;
import org.floens.chan.core.presenter.ImageViewerPresenter; import org.floens.chan.core.presenter.ImageViewerPresenter;
import org.floens.chan.core.presenter.ReplyPresenter; import org.floens.chan.core.presenter.ReplyPresenter;
import org.floens.chan.core.presenter.ThreadPresenter; import org.floens.chan.core.presenter.ThreadPresenter;
import org.floens.chan.core.receiver.WatchUpdateReceiver; import org.floens.chan.core.receiver.WatchUpdateReceiver;
import org.floens.chan.core.saver.ImageSaveTask; import org.floens.chan.core.saver.ImageSaveTask;
import org.floens.chan.core.site.sites.chan4.Chan4;
import org.floens.chan.ui.activity.StartActivity; import org.floens.chan.ui.activity.StartActivity;
import org.floens.chan.ui.adapter.DrawerAdapter; import org.floens.chan.ui.adapter.DrawerAdapter;
import org.floens.chan.ui.adapter.PostsFilter; import org.floens.chan.ui.adapter.PostsFilter;
@ -46,6 +48,9 @@ import dagger.Component;
NetModule.class NetModule.class
}) })
@Singleton @Singleton
/**
* Note: please avoid adding inject() statements for Sites.
*/
public interface ChanGraph { public interface ChanGraph {
ChanParser getChanParser(); ChanParser getChanParser();
@ -61,6 +66,8 @@ public interface ChanGraph {
FileCache getFileCache(); FileCache getFileCache();
HttpCallManager getHttpCallManager();
void inject(Chan chan); void inject(Chan chan);
void inject(MainSettingsController mainSettingsController); void inject(MainSettingsController mainSettingsController);
@ -116,4 +123,6 @@ public interface ChanGraph {
void inject(ViewThreadController viewThreadController); void inject(ViewThreadController viewThreadController);
void inject(WatchManager.PinWatcher pinWatcher); void inject(WatchManager.PinWatcher pinWatcher);
void inject(Chan4 chan4);
} }

@ -15,48 +15,32 @@
* You should have received a copy of the GNU General Public License * You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>. * along with this program. If not, see <http://www.gnu.org/licenses/>.
*/ */
package org.floens.chan.core.http; package org.floens.chan.core.manager;
import android.content.Context; import android.content.Context;
import org.floens.chan.core.di.UserAgentProvider;
import org.floens.chan.core.model.Loadable; import org.floens.chan.core.model.Loadable;
import org.floens.chan.core.model.Reply; import org.floens.chan.core.site.http.Reply;
import java.io.File; import java.io.File;
import java.util.HashMap; import java.util.HashMap;
import java.util.Map; import java.util.Map;
import java.util.concurrent.TimeUnit;
import javax.inject.Inject; import javax.inject.Inject;
import javax.inject.Singleton; import javax.inject.Singleton;
import okhttp3.OkHttpClient;
import okhttp3.Request;
/** /**
* To send an reply to 4chan. * Manages replies.
*/ */
@Singleton @Singleton
public class ReplyManager { public class ReplyManager {
private static final int TIMEOUT = 30000;
private final Context context; private final Context context;
private String userAgent;
private OkHttpClient client;
private Map<Loadable, Reply> drafts = new HashMap<>(); private Map<Loadable, Reply> drafts = new HashMap<>();
@Inject @Inject
public ReplyManager(Context context, UserAgentProvider userAgentProvider) { public ReplyManager(Context context) {
this.context = context; this.context = context;
userAgent = userAgentProvider.getUserAgent();
client = new OkHttpClient.Builder()
.connectTimeout(TIMEOUT, TimeUnit.MILLISECONDS)
.readTimeout(TIMEOUT, TimeUnit.MILLISECONDS)
.writeTimeout(TIMEOUT, TimeUnit.MILLISECONDS)
.build();
} }
public Reply getReply(Loadable loadable) { public Reply getReply(Loadable loadable) {
@ -85,23 +69,4 @@ public class ReplyManager {
public File getPickFile() { public File getPickFile() {
return new File(context.getCacheDir(), "picked_file"); return new File(context.getCacheDir(), "picked_file");
} }
public void makeHttpCall(HttpCall httpCall, HttpCallback<? extends HttpCall> callback) {
httpCall.setCallback(callback);
Request.Builder requestBuilder = new Request.Builder();
httpCall.setup(requestBuilder);
requestBuilder.header("User-Agent", userAgent);
Request request = requestBuilder.build();
client.newCall(request).enqueue(httpCall);
}
public interface HttpCallback<T extends HttpCall> {
void onHttpSuccess(T httpPost);
void onHttpFail(T httpPost);
}
} }

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

@ -35,7 +35,7 @@ import org.floens.chan.core.site.Site;
* references the same loadable and that the loadable is properly saved in the database. * references the same loadable and that the loadable is properly saved in the database.
*/ */
@DatabaseTable @DatabaseTable
public class Loadable implements SiteReference { public class Loadable implements SiteReference, BoardReference {
@DatabaseField(generatedId = true) @DatabaseField(generatedId = true)
public int id; public int id;
@ -123,6 +123,11 @@ public class Loadable implements SiteReference {
return site; return site;
} }
@Override
public Board getBoard() {
return board;
}
public void setTitle(String title) { public void setTitle(String title) {
if (!TextUtils.equals(this.title, title)) { if (!TextUtils.equals(this.title, title)) {
this.title = title; this.title = title;

@ -22,17 +22,20 @@ import android.text.TextUtils;
import org.floens.chan.R; import org.floens.chan.R;
import org.floens.chan.chan.ChanUrls; import org.floens.chan.chan.ChanUrls;
import org.floens.chan.core.database.DatabaseManager; import org.floens.chan.core.database.DatabaseManager;
import org.floens.chan.core.http.ReplyHttpCall; import org.floens.chan.core.manager.ReplyManager;
import org.floens.chan.core.http.ReplyManager;
import org.floens.chan.core.manager.BoardManager; import org.floens.chan.core.manager.BoardManager;
import org.floens.chan.core.manager.WatchManager; import org.floens.chan.core.manager.WatchManager;
import org.floens.chan.core.model.Board; import org.floens.chan.core.model.Board;
import org.floens.chan.core.model.ChanThread; import org.floens.chan.core.model.ChanThread;
import org.floens.chan.core.model.Loadable; import org.floens.chan.core.model.Loadable;
import org.floens.chan.core.model.Post; import org.floens.chan.core.model.Post;
import org.floens.chan.core.model.Reply;
import org.floens.chan.core.model.SavedReply; import org.floens.chan.core.model.SavedReply;
import org.floens.chan.core.settings.ChanSettings; import org.floens.chan.core.settings.ChanSettings;
import org.floens.chan.core.site.Site;
import org.floens.chan.core.site.http.HttpCall;
import org.floens.chan.core.site.http.HttpCallManager;
import org.floens.chan.core.site.http.ReplyResponse;
import org.floens.chan.core.site.http.Reply;
import org.floens.chan.ui.helper.ImagePickDelegate; import org.floens.chan.ui.helper.ImagePickDelegate;
import org.floens.chan.ui.layout.CaptchaCallback; import org.floens.chan.ui.layout.CaptchaCallback;
import org.floens.chan.ui.layout.CaptchaLayoutInterface; import org.floens.chan.ui.layout.CaptchaLayoutInterface;
@ -49,7 +52,7 @@ import static org.floens.chan.utils.AndroidUtils.getReadableFileSize;
import static org.floens.chan.utils.AndroidUtils.getRes; import static org.floens.chan.utils.AndroidUtils.getRes;
import static org.floens.chan.utils.AndroidUtils.getString; import static org.floens.chan.utils.AndroidUtils.getString;
public class ReplyPresenter implements ReplyManager.HttpCallback<ReplyHttpCall>, CaptchaCallback, ImagePickDelegate.ImagePickCallback { public class ReplyPresenter implements CaptchaCallback, ImagePickDelegate.ImagePickCallback, Site.PostListener {
public enum Page { public enum Page {
INPUT, INPUT,
CAPTCHA, CAPTCHA,
@ -70,6 +73,9 @@ public class ReplyPresenter implements ReplyManager.HttpCallback<ReplyHttpCall>,
@Inject @Inject
WatchManager watchManager; WatchManager watchManager;
@Inject
HttpCallManager httpCallManager;
@Inject @Inject
DatabaseManager databaseManager; DatabaseManager databaseManager;
@ -189,21 +195,20 @@ public class ReplyPresenter implements ReplyManager.HttpCallback<ReplyHttpCall>,
public void onSubmitClicked() { public void onSubmitClicked() {
callback.loadViewsIntoDraft(draft); callback.loadViewsIntoDraft(draft);
draft.board = loadable.boardCode; draft.loadable = loadable;
draft.resto = loadable.isThreadMode() ? loadable.no : -1;
if (ChanSettings.passLoggedIn()) { if (ChanSettings.passLoggedIn()) {
draft.usePass = true; draft.noVerification = true;
draft.passId = ChanSettings.passId.get(); draft.passId = ChanSettings.passId.get();
} else { } else {
draft.usePass = false; draft.noVerification = false;
draft.passId = null; draft.passId = null;
} }
draft.spoilerImage = draft.spoilerImage && board.spoilers; draft.spoilerImage = draft.spoilerImage && board.spoilers;
draft.captchaResponse = null; draft.captchaResponse = null;
if (draft.usePass) { if (draft.noVerification) {
makeSubmitCall(); makeSubmitCall();
} else { } else {
switchPage(Page.CAPTCHA, true); switchPage(Page.CAPTCHA, true);
@ -211,8 +216,8 @@ public class ReplyPresenter implements ReplyManager.HttpCallback<ReplyHttpCall>,
} }
@Override @Override
public void onHttpSuccess(ReplyHttpCall replyCall) { public void onPostComplete(HttpCall httpCall, ReplyResponse replyResponse) {
if (replyCall.posted) { if (replyResponse.posted) {
if (ChanSettings.postPinThread.get() && loadable.isThreadMode()) { if (ChanSettings.postPinThread.get() && loadable.isThreadMode()) {
ChanThread thread = callback.getThread(); ChanThread thread = callback.getThread();
if (thread != null) { if (thread != null) {
@ -220,7 +225,7 @@ public class ReplyPresenter implements ReplyManager.HttpCallback<ReplyHttpCall>,
} }
} }
SavedReply savedReply = new SavedReply(loadable.boardCode, replyCall.postNo, replyCall.password); SavedReply savedReply = new SavedReply(loadable.boardCode, replyResponse.postNo, replyResponse.password);
databaseManager.runTask(databaseManager.getDatabaseSavedReplyManager().saveReply(savedReply)); databaseManager.runTask(databaseManager.getDatabaseSavedReplyManager().saveReply(savedReply));
switchPage(Page.INPUT, false); switchPage(Page.INPUT, false);
@ -234,23 +239,20 @@ public class ReplyPresenter implements ReplyManager.HttpCallback<ReplyHttpCall>,
callback.onPosted(); callback.onPosted();
if (bound && !loadable.isThreadMode()) { if (bound && !loadable.isThreadMode()) {
callback.showThread(databaseManager.getDatabaseLoadableManager().get(Loadable.forThread(loadable.site, loadable.board, replyCall.postNo))); callback.showThread(databaseManager.getDatabaseLoadableManager().get(Loadable.forThread(loadable.site, loadable.board, replyResponse.postNo)));
} }
} else { } else {
if (replyCall.errorMessage == null) { if (replyResponse.errorMessage == null) {
replyCall.errorMessage = getString(R.string.reply_error); replyResponse.errorMessage = getString(R.string.reply_error);
} }
switchPage(Page.INPUT, true); switchPage(Page.INPUT, true);
callback.openMessage(true, false, replyCall.errorMessage, true); callback.openMessage(true, false, replyResponse.errorMessage, true);
if (replyCall.probablyBanned) {
// callback.openMessageWebview();
}
} }
} }
@Override @Override
public void onHttpFail(ReplyHttpCall httpPost) { public void onPostError(HttpCall httpCall) {
switchPage(Page.INPUT, true); switchPage(Page.INPUT, true);
callback.openMessage(true, false, getString(R.string.reply_error), true); callback.openMessage(true, false, getString(R.string.reply_error), true);
} }
@ -338,7 +340,7 @@ public class ReplyPresenter implements ReplyManager.HttpCallback<ReplyHttpCall>,
} }
private void makeSubmitCall() { private void makeSubmitCall() {
replyManager.makeHttpCall(new ReplyHttpCall(draft), this); loadable.getSite().post(draft, this);
switchPage(Page.LOADING, true); switchPage(Page.LOADING, true);
} }

@ -25,8 +25,7 @@ import org.floens.chan.chan.ChanLoader;
import org.floens.chan.chan.ChanUrls; import org.floens.chan.chan.ChanUrls;
import org.floens.chan.core.database.DatabaseManager; import org.floens.chan.core.database.DatabaseManager;
import org.floens.chan.core.exception.ChanLoaderException; import org.floens.chan.core.exception.ChanLoaderException;
import org.floens.chan.core.http.DeleteHttpCall; import org.floens.chan.core.manager.ReplyManager;
import org.floens.chan.core.http.ReplyManager;
import org.floens.chan.core.manager.WatchManager; import org.floens.chan.core.manager.WatchManager;
import org.floens.chan.core.model.Board; import org.floens.chan.core.model.Board;
import org.floens.chan.core.model.ChanThread; import org.floens.chan.core.model.ChanThread;
@ -40,6 +39,10 @@ import org.floens.chan.core.model.SavedReply;
import org.floens.chan.core.pool.ChanLoaderFactory; import org.floens.chan.core.pool.ChanLoaderFactory;
import org.floens.chan.core.settings.ChanSettings; import org.floens.chan.core.settings.ChanSettings;
import org.floens.chan.core.site.Site; import org.floens.chan.core.site.Site;
import org.floens.chan.core.site.http.DeleteRequest;
import org.floens.chan.core.site.http.DeleteResponse;
import org.floens.chan.core.site.http.HttpCall;
import org.floens.chan.core.site.http.HttpCallManager;
import org.floens.chan.ui.adapter.PostAdapter; import org.floens.chan.ui.adapter.PostAdapter;
import org.floens.chan.ui.adapter.PostsFilter; import org.floens.chan.ui.adapter.PostsFilter;
import org.floens.chan.ui.cell.PostCellInterface; import org.floens.chan.ui.cell.PostCellInterface;
@ -85,6 +88,9 @@ public class ThreadPresenter implements ChanLoader.ChanLoaderCallback, PostAdapt
@Inject @Inject
ReplyManager replyManager; ReplyManager replyManager;
@Inject
HttpCallManager httpCallManager;
@Inject @Inject
ChanLoaderFactory chanLoaderFactory; ChanLoaderFactory chanLoaderFactory;
@ -426,7 +432,10 @@ public class ThreadPresenter implements ChanLoader.ChanLoaderCallback, PostAdapt
menu.add(new FloatingMenuItem(POST_OPTION_OPEN_BROWSER, R.string.action_open_browser)); menu.add(new FloatingMenuItem(POST_OPTION_OPEN_BROWSER, R.string.action_open_browser));
menu.add(new FloatingMenuItem(POST_OPTION_SHARE, R.string.post_share)); menu.add(new FloatingMenuItem(POST_OPTION_SHARE, R.string.post_share));
menu.add(new FloatingMenuItem(POST_OPTION_COPY_TEXT, R.string.post_copy_text)); menu.add(new FloatingMenuItem(POST_OPTION_COPY_TEXT, R.string.post_copy_text));
menu.add(new FloatingMenuItem(POST_OPTION_REPORT, R.string.post_report));
if (loadable.getSite().feature(Site.Feature.POST_REPORT)) {
menu.add(new FloatingMenuItem(POST_OPTION_REPORT, R.string.post_report));
}
if (!loadable.isThreadMode()) { if (!loadable.isThreadMode()) {
menu.add(new FloatingMenuItem(POST_OPTION_HIDE, R.string.post_hide)); menu.add(new FloatingMenuItem(POST_OPTION_HIDE, R.string.post_hide));
@ -603,14 +612,14 @@ public class ThreadPresenter implements ChanLoader.ChanLoaderCallback, PostAdapt
databaseManager.getDatabaseSavedReplyManager().findSavedReply(post.boardId, post.no) databaseManager.getDatabaseSavedReplyManager().findSavedReply(post.boardId, post.no)
); );
if (reply != null) { if (reply != null) {
replyManager.makeHttpCall(new DeleteHttpCall(reply, onlyImageDelete), new ReplyManager.HttpCallback<DeleteHttpCall>() { loadable.getSite().delete(new DeleteRequest(loadable.getSite(), post, reply, onlyImageDelete), new Site.DeleteListener() {
@Override @Override
public void onHttpSuccess(DeleteHttpCall httpPost) { public void onDeleteComplete(HttpCall httpPost, DeleteResponse deleteResponse) {
String message; String message;
if (httpPost.deleted) { if (deleteResponse.deleted) {
message = getString(R.string.delete_success); message = getString(R.string.delete_success);
} else if (!TextUtils.isEmpty(httpPost.errorMessage)) { } else if (!TextUtils.isEmpty(deleteResponse.errorMessage)) {
message = httpPost.errorMessage; message = deleteResponse.errorMessage;
} else { } else {
message = getString(R.string.delete_error); message = getString(R.string.delete_error);
} }
@ -618,7 +627,7 @@ public class ThreadPresenter implements ChanLoader.ChanLoaderCallback, PostAdapt
} }
@Override @Override
public void onHttpFail(DeleteHttpCall httpPost) { public void onDeleteError(HttpCall httpCall) {
threadPresenterCallback.hideDeleting(getString(R.string.delete_error)); threadPresenterCallback.hideDeleting(getString(R.string.delete_error));
} }
}); });

@ -1,23 +1,65 @@
/*
* Clover - 4chan browser https://github.com/Floens/Clover/
* Copyright (C) 2014 Floens
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package org.floens.chan.core.site; package org.floens.chan.core.site;
import org.floens.chan.chan.ChanLoaderRequest; import org.floens.chan.chan.ChanLoaderRequest;
import org.floens.chan.chan.ChanLoaderRequestParams; import org.floens.chan.chan.ChanLoaderRequestParams;
import org.floens.chan.core.model.Board; import org.floens.chan.core.model.Board;
import org.floens.chan.core.model.Loadable;
import org.floens.chan.core.model.Post;
import org.floens.chan.core.site.http.DeleteRequest;
import org.floens.chan.core.site.http.DeleteResponse;
import org.floens.chan.core.site.http.HttpCall;
import org.floens.chan.core.site.http.LoginRequest;
import org.floens.chan.core.site.http.LoginResponse;
import org.floens.chan.core.site.http.Reply;
import org.floens.chan.core.site.http.ReplyResponse;
public interface Site { public interface Site {
enum Feature { enum Feature {
/** /**
* This site supports posting. (Or rather, we've implemented support for it.) * This site supports posting. (Or rather, we've implemented support for it.)
*
* @see #post(Reply, PostListener)
* @see SiteEndpoints#reply(Loadable)
*/ */
POSTING, POSTING,
/** /**
* This site supports deleting posts. * This site supports deleting posts.
*
* @see #delete(DeleteRequest, DeleteListener)
* @see SiteEndpoints#delete(Post)
*/ */
POST_DELETE, POST_DELETE,
/**
* This site supports reporting posts.
*
* @see SiteEndpoints#report(Post)
*/
POST_REPORT,
/** /**
* This site supports some sort of login (like 4pass). * This site supports some sort of login (like 4pass).
*
* @see #login(LoginRequest, LoginListener)
* @see SiteEndpoints#login()
*/ */
LOGIN LOGIN
} }
@ -75,11 +117,35 @@ public interface Site {
void boards(BoardsListener boardsListener); void boards(BoardsListener boardsListener);
interface BoardsListener {
void onBoardsReceived(Boards boards);
}
Board board(String name); Board board(String name);
ChanLoaderRequest loaderRequest(ChanLoaderRequestParams request); ChanLoaderRequest loaderRequest(ChanLoaderRequestParams request);
interface BoardsListener { void post(Reply reply, PostListener postListener);
void onBoardsReceived(Boards boards);
interface PostListener {
void onPostComplete(HttpCall httpCall, ReplyResponse replyResponse);
void onPostError(HttpCall httpCall);
}
void delete(DeleteRequest deleteRequest, DeleteListener deleteListener);
interface DeleteListener {
void onDeleteComplete(HttpCall httpCall, DeleteResponse deleteResponse);
void onDeleteError(HttpCall httpCall);
}
void login(LoginRequest loginRequest, LoginListener loginListener);
interface LoginListener {
void onLoginComplete(HttpCall httpCall, LoginResponse loginResponse);
void onLoginError(HttpCall httpCall);
} }
} }

@ -1,3 +1,20 @@
/*
* Clover - 4chan browser https://github.com/Floens/Clover/
* Copyright (C) 2014 Floens
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package org.floens.chan.core.site; package org.floens.chan.core.site;
import org.floens.chan.core.model.Board; import org.floens.chan.core.model.Board;
@ -22,5 +39,11 @@ public interface SiteEndpoints {
String boards(); String boards();
String reply(Board board, Loadable thread); String reply(Loadable thread);
String delete(Post post);
String report(Post post);
String login();
} }

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

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

@ -15,7 +15,7 @@
* You should have received a copy of the GNU General Public License * You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>. * along with this program. If not, see <http://www.gnu.org/licenses/>.
*/ */
package org.floens.chan.core.http; package org.floens.chan.core.site.http;
import org.floens.chan.utils.AndroidUtils; import org.floens.chan.utils.AndroidUtils;
import org.floens.chan.utils.IOUtils; import org.floens.chan.utils.IOUtils;
@ -28,29 +28,24 @@ import okhttp3.Callback;
import okhttp3.Request; import okhttp3.Request;
import okhttp3.Response; import okhttp3.Response;
/**
* Http calls are an abstraction over a normal OkHttp call.
* <p>These HttpCalls are used for emulating &lt;form&gt; elements used for posting, reporting, deleting, etc.
* <p>Implement {@link #setup(Request.Builder)} and {@link #process(Response, String)}.
* {@code setup()} is called on the main thread, set up up the request builder here. {@code execute()} is
* called on a worker thread after the response was executed, do something with the response here.
*/
public abstract class HttpCall implements Callback { public abstract class HttpCall implements Callback {
private static final String TAG = "HttpCall"; private static final String TAG = "HttpCall";
private boolean successful = false; private boolean successful = false;
private ReplyManager.HttpCallback callback; private HttpCallback callback;
private Exception exception;
public void setSuccessful(boolean successful) {
this.successful = successful;
}
public abstract void setup(Request.Builder requestBuilder); public abstract void setup(Request.Builder requestBuilder);
public abstract void process(Response response, String result) throws IOException; public abstract void process(Response response, String result) throws IOException;
@SuppressWarnings("unchecked")
public void postUI(boolean successful) {
if (successful) {
callback.onHttpSuccess(this);
} else {
callback.onHttpFail(this);
}
}
@Override @Override
public void onResponse(Call call, Response response) { public void onResponse(Call call, Response response) {
try { try {
@ -61,31 +56,60 @@ public abstract class HttpCall implements Callback {
} else { } else {
onFailure(call, null); onFailure(call, null);
} }
} catch (IOException e) { } catch (Exception e) {
exception = e;
Logger.e(TAG, "IOException processing response", e); Logger.e(TAG, "IOException processing response", e);
} finally { } finally {
IOUtils.closeQuietly(response.body()); IOUtils.closeQuietly(response.body());
} }
if (successful) {
callSuccess();
} else {
callFail(exception);
}
}
@Override
public void onFailure(Call call, IOException e) {
callFail(e);
}
public Exception getException() {
return exception;
}
public boolean isSuccessful() {
return successful;
}
private void callSuccess() {
AndroidUtils.runOnUiThread(new Runnable() { AndroidUtils.runOnUiThread(new Runnable() {
@SuppressWarnings("unchecked")
@Override @Override
public void run() { public void run() {
postUI(successful); callback.onHttpSuccess(HttpCall.this);
} }
}); });
} }
@Override private void callFail(final Exception e) {
public void onFailure(Call call, IOException e) {
AndroidUtils.runOnUiThread(new Runnable() { AndroidUtils.runOnUiThread(new Runnable() {
@SuppressWarnings("unchecked")
@Override @Override
public void run() { public void run() {
postUI(false); callback.onHttpFail(HttpCall.this, e);
} }
}); });
} }
void setCallback(ReplyManager.HttpCallback<? extends HttpCall> callback) { public void setCallback(HttpCallback<? extends HttpCall> callback) {
this.callback = callback; this.callback = callback;
} }
public interface HttpCallback<T extends HttpCall> {
void onHttpSuccess(T httpCall);
void onHttpFail(T httpCall, Exception e);
}
} }

@ -0,0 +1,63 @@
/*
* Clover - 4chan browser https://github.com/Floens/Clover/
* Copyright (C) 2014 Floens
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package org.floens.chan.core.site.http;
import org.floens.chan.core.di.UserAgentProvider;
import java.util.concurrent.TimeUnit;
import javax.inject.Inject;
import javax.inject.Singleton;
import okhttp3.OkHttpClient;
import okhttp3.Request;
/**
* Manages the {@link HttpCall} executions.
*/
@Singleton
public class HttpCallManager {
private static final int TIMEOUT = 30000;
private UserAgentProvider userAgentProvider;
private OkHttpClient client;
@Inject
public HttpCallManager(UserAgentProvider userAgentProvider) {
this.userAgentProvider = userAgentProvider;
client = new OkHttpClient.Builder()
.connectTimeout(TIMEOUT, TimeUnit.MILLISECONDS)
.readTimeout(TIMEOUT, TimeUnit.MILLISECONDS)
.writeTimeout(TIMEOUT, TimeUnit.MILLISECONDS)
.build();
}
public void makeHttpCall(HttpCall httpCall, HttpCall.HttpCallback<? extends HttpCall> callback) {
httpCall.setCallback(callback);
Request.Builder requestBuilder = new Request.Builder();
httpCall.setup(requestBuilder);
requestBuilder.header("User-Agent", userAgentProvider.getUserAgent());
Request request = requestBuilder.build();
client.newCall(request).enqueue(httpCall);
}
}

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

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

@ -15,7 +15,9 @@
* You should have received a copy of the GNU General Public License * You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>. * along with this program. If not, see <http://www.gnu.org/licenses/>.
*/ */
package org.floens.chan.core.model; package org.floens.chan.core.site.http;
import org.floens.chan.core.model.Loadable;
import java.io.File; import java.io.File;
@ -24,17 +26,19 @@ import java.io.File;
*/ */
public class Reply { public class Reply {
/** /**
* Optional. Null when ReCaptcha v2 was used or a 4pass * Optional. {@code null} when ReCaptcha v2 was used or a 4pass
*/ */
public String captchaChallenge; public String captchaChallenge;
/** /**
* Optional. Null when a 4pass was used. * Optional. {@code null} when a 4pass was used.
*/ */
public String captchaResponse; public String captchaResponse;
public boolean usePass = false;
public String board; // TODO(multi-site) flip boolean
public int resto; public boolean noVerification = false;
public Loadable loadable;
public String passId; public String passId;
public File file; public File file;

@ -0,0 +1,42 @@
/*
* Clover - 4chan browser https://github.com/Floens/Clover/
* Copyright (C) 2014 Floens
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package org.floens.chan.core.site.http;
import org.floens.chan.core.site.Site;
/**
* Generic response for {@link Site#post(Reply, Site.PostListener)} that the reply layout uses.
*/
public class ReplyResponse {
/**
* {@code true} if the post when through, {@code false} otherwise.
*/
public boolean posted;
/**
* Error message used to show to the user if {@link #posted} is {@code false}.
* <p>Optional
*/
public String errorMessage;
// TODO(multi-site)
public int threadNo;
public int postNo;
public String password;
public boolean probablyBanned;
}

@ -1,5 +1,23 @@
/*
* Clover - 4chan browser https://github.com/Floens/Clover/
* Copyright (C) 2014 Floens
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package org.floens.chan.core.site.sites.chan4; package org.floens.chan.core.site.sites.chan4;
import com.android.volley.RequestQueue;
import com.android.volley.Response; import com.android.volley.Response;
import com.android.volley.VolleyError; import com.android.volley.VolleyError;
@ -11,7 +29,11 @@ import org.floens.chan.core.model.Post;
import org.floens.chan.core.site.Boards; import org.floens.chan.core.site.Boards;
import org.floens.chan.core.site.Site; import org.floens.chan.core.site.Site;
import org.floens.chan.core.site.SiteEndpoints; import org.floens.chan.core.site.SiteEndpoints;
import org.floens.chan.core.site.loaders.Chan4ReaderRequest; import org.floens.chan.core.site.http.DeleteRequest;
import org.floens.chan.core.site.http.HttpCall;
import org.floens.chan.core.site.http.HttpCallManager;
import org.floens.chan.core.site.http.LoginRequest;
import org.floens.chan.core.site.http.Reply;
import org.floens.chan.utils.Logger; import org.floens.chan.utils.Logger;
import java.util.ArrayList; import java.util.ArrayList;
@ -21,6 +43,8 @@ import java.util.Locale;
import java.util.Map; import java.util.Map;
import java.util.Random; import java.util.Random;
import javax.inject.Inject;
import static org.floens.chan.Chan.getGraph; import static org.floens.chan.Chan.getGraph;
public class Chan4 implements Site { public class Chan4 implements Site {
@ -28,6 +52,12 @@ public class Chan4 implements Site {
private static final Random random = new Random(); private static final Random random = new Random();
@Inject
HttpCallManager httpCallManager;
@Inject
RequestQueue requestQueue;
private final SiteEndpoints endpoints = new SiteEndpoints() { private final SiteEndpoints endpoints = new SiteEndpoints() {
@Override @Override
public String catalog(Board board) { public String catalog(Board board) {
@ -69,12 +99,28 @@ public class Chan4 implements Site {
} }
@Override @Override
public String reply(Board board, Loadable thread) { public String reply(Loadable loadable) {
return "https://sys.4chan.org/" + board.code + "/post"; return "https://sys.4chan.org/" + loadable.getBoard().code + "/post";
}
@Override
public String delete(Post post) {
return "https://sys.4chan.org/" + post.board.code + "/imgboard.php";
}
@Override
public String report(Post post) {
return "https://sys.4chan.org/" + post.board.code + "/imgboard.php?mode=report&no=" + post.no;
}
@Override
public String login() {
return "https://sys.4chan.org/auth";
} }
}; };
public Chan4() { public Chan4() {
getGraph().inject(this);
} }
/** /**
@ -100,6 +146,9 @@ public class Chan4 implements Site {
case POST_DELETE: case POST_DELETE:
// yes, with the password saved when posting. // yes, with the password saved when posting.
return true; return true;
case POST_REPORT:
// yes, with a custom url
return true;
default: default:
return false; return false;
} }
@ -144,7 +193,7 @@ public class Chan4 implements Site {
@Override @Override
public void boards(final BoardsListener listener) { public void boards(final BoardsListener listener) {
getGraph().getRequestQueue().add(new Chan4BoardsRequest(this, new Response.Listener<List<Board>>() { requestQueue.add(new Chan4BoardsRequest(this, new Response.Listener<List<Board>>() {
@Override @Override
public void onResponse(List<Board> response) { public void onResponse(List<Board> response) {
listener.onBoardsReceived(new Boards(response)); listener.onBoardsReceived(new Boards(response));
@ -170,4 +219,49 @@ public class Chan4 implements Site {
public ChanLoaderRequest loaderRequest(ChanLoaderRequestParams request) { public ChanLoaderRequest loaderRequest(ChanLoaderRequestParams request) {
return new ChanLoaderRequest(new Chan4ReaderRequest(request)); return new ChanLoaderRequest(new Chan4ReaderRequest(request));
} }
@Override
public void post(Reply reply, final PostListener postListener) {
httpCallManager.makeHttpCall(new Chan4ReplyHttpCall(reply), new HttpCall.HttpCallback<Chan4ReplyHttpCall>() {
@Override
public void onHttpSuccess(Chan4ReplyHttpCall httpPost) {
postListener.onPostComplete(httpPost, httpPost.replyResponse);
}
@Override
public void onHttpFail(Chan4ReplyHttpCall httpPost, Exception e) {
postListener.onPostError(httpPost);
}
});
}
@Override
public void delete(DeleteRequest deleteRequest, final DeleteListener deleteListener) {
httpCallManager.makeHttpCall(new Chan4DeleteHttpCall(deleteRequest), new HttpCall.HttpCallback<Chan4DeleteHttpCall>() {
@Override
public void onHttpSuccess(Chan4DeleteHttpCall httpPost) {
deleteListener.onDeleteComplete(httpPost, httpPost.deleteResponse);
}
@Override
public void onHttpFail(Chan4DeleteHttpCall httpPost, Exception e) {
deleteListener.onDeleteError(httpPost);
}
});
}
@Override
public void login(LoginRequest loginRequest, final LoginListener loginListener) {
httpCallManager.makeHttpCall(new Chan4PassHttpCall(loginRequest), new HttpCall.HttpCallback<Chan4PassHttpCall>() {
@Override
public void onHttpSuccess(Chan4PassHttpCall httpCall) {
loginListener.onLoginComplete(httpCall, httpCall.loginResponse);
}
@Override
public void onHttpFail(Chan4PassHttpCall httpCall, Exception e) {
loginListener.onLoginError(httpCall);
}
});
}
} }

@ -15,10 +15,11 @@
* You should have received a copy of the GNU General Public License * You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>. * along with this program. If not, see <http://www.gnu.org/licenses/>.
*/ */
package org.floens.chan.core.http; package org.floens.chan.core.site.sites.chan4;
import org.floens.chan.chan.ChanUrls; import org.floens.chan.core.site.http.DeleteRequest;
import org.floens.chan.core.model.SavedReply; import org.floens.chan.core.site.http.DeleteResponse;
import org.floens.chan.core.site.http.HttpCall;
import org.jsoup.Jsoup; import org.jsoup.Jsoup;
import java.io.IOException; import java.io.IOException;
@ -29,31 +30,27 @@ import okhttp3.FormBody;
import okhttp3.Request; import okhttp3.Request;
import okhttp3.Response; import okhttp3.Response;
public class DeleteHttpCall extends HttpCall { public class Chan4DeleteHttpCall extends HttpCall {
private static final Pattern ERROR_MESSAGE = Pattern.compile("\"errmsg\"[^>]*>(.*?)<\\/span"); private static final Pattern ERROR_MESSAGE = Pattern.compile("\"errmsg\"[^>]*>(.*?)<\\/span");
public boolean deleted; private final DeleteRequest deleteRequest;
public String errorMessage; public final DeleteResponse deleteResponse = new DeleteResponse();
private final SavedReply reply; public Chan4DeleteHttpCall(DeleteRequest deleteRequest) {
private final boolean onlyImageDelete; this.deleteRequest = deleteRequest;
public DeleteHttpCall(final SavedReply reply, boolean onlyImageDelete) {
this.reply = reply;
this.onlyImageDelete = onlyImageDelete;
} }
@Override @Override
public void setup(Request.Builder requestBuilder) { public void setup(Request.Builder requestBuilder) {
FormBody.Builder formBuilder = new FormBody.Builder(); FormBody.Builder formBuilder = new FormBody.Builder();
formBuilder.add(Integer.toString(reply.no), "delete"); formBuilder.add(Integer.toString(deleteRequest.post.no), "delete");
if (onlyImageDelete) { if (deleteRequest.imageOnly) {
formBuilder.add("onlyimgdel", "on"); formBuilder.add("onlyimgdel", "on");
} }
formBuilder.add("mode", "usrdel"); formBuilder.add("mode", "usrdel");
formBuilder.add("pwd", reply.password); formBuilder.add("pwd", deleteRequest.savedReply.password);
requestBuilder.url(ChanUrls.getDeleteUrl(reply.board)); requestBuilder.url(deleteRequest.site.endpoints().delete(deleteRequest.post));
requestBuilder.post(formBuilder.build()); requestBuilder.post(formBuilder.build());
} }
@ -61,9 +58,9 @@ public class DeleteHttpCall extends HttpCall {
public void process(Response response, String result) throws IOException { public void process(Response response, String result) throws IOException {
Matcher errorMessageMatcher = ERROR_MESSAGE.matcher(result); Matcher errorMessageMatcher = ERROR_MESSAGE.matcher(result);
if (errorMessageMatcher.find()) { if (errorMessageMatcher.find()) {
errorMessage = Jsoup.parse(errorMessageMatcher.group(1)).body().ownText(); deleteResponse.errorMessage = Jsoup.parse(errorMessageMatcher.group(1)).body().ownText();
} else { } else {
deleted = true; deleteResponse.deleted = true;
} }
} }
} }

@ -15,9 +15,11 @@
* You should have received a copy of the GNU General Public License * You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>. * along with this program. If not, see <http://www.gnu.org/licenses/>.
*/ */
package org.floens.chan.core.http; package org.floens.chan.core.site.sites.chan4;
import org.floens.chan.chan.ChanUrls; import org.floens.chan.core.site.http.HttpCall;
import org.floens.chan.core.site.http.LoginRequest;
import org.floens.chan.core.site.http.LoginResponse;
import java.io.IOException; import java.io.IOException;
import java.net.HttpCookie; import java.net.HttpCookie;
@ -27,17 +29,12 @@ import okhttp3.FormBody;
import okhttp3.Request; import okhttp3.Request;
import okhttp3.Response; import okhttp3.Response;
public class PassHttpCall extends HttpCall { public class Chan4PassHttpCall extends HttpCall {
public boolean success; private final LoginRequest loginRequest;
public String message; public final LoginResponse loginResponse = new LoginResponse();
public String passId;
private String token; public Chan4PassHttpCall(LoginRequest loginRequest) {
private String pin; this.loginRequest = loginRequest;
public PassHttpCall(String token, String pin) {
this.token = token;
this.pin = pin;
} }
@Override @Override
@ -46,10 +43,10 @@ public class PassHttpCall extends HttpCall {
formBuilder.add("act", "do_login"); formBuilder.add("act", "do_login");
formBuilder.add("id", token); formBuilder.add("id", loginRequest.user);
formBuilder.add("pin", pin); formBuilder.add("pin", loginRequest.pass);
requestBuilder.url(ChanUrls.getPassUrl()); requestBuilder.url(loginRequest.site.endpoints().login());
requestBuilder.post(formBuilder.build()); requestBuilder.post(formBuilder.build());
} }
@ -59,6 +56,7 @@ public class PassHttpCall extends HttpCall {
if (result.contains("Success! Your device is now authorized")) { if (result.contains("Success! Your device is now authorized")) {
authSuccess = true; authSuccess = true;
} else { } else {
String message;
if (result.contains("Your Token must be exactly 10 characters")) { if (result.contains("Your Token must be exactly 10 characters")) {
message = "Incorrect token"; message = "Incorrect token";
} else if (result.contains("You have left one or more fields blank")) { } else if (result.contains("You have left one or more fields blank")) {
@ -68,6 +66,7 @@ public class PassHttpCall extends HttpCall {
} else { } else {
message = "Unknown error"; message = "Unknown error";
} }
loginResponse.message = message;
} }
if (authSuccess) { if (authSuccess) {
@ -86,11 +85,11 @@ public class PassHttpCall extends HttpCall {
} }
if (passId != null) { if (passId != null) {
this.passId = passId; loginResponse.token = passId;
message = "Success! Your device is now authorized."; loginResponse.message = "Success! Your device is now authorized.";
success = true; loginResponse.success = true;
} else { } else {
message = "Could not get pass id"; loginResponse.message = "Could not get pass id";
} }
} }
} }

@ -15,7 +15,7 @@
* You should have received a copy of the GNU General Public License * You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>. * along with this program. If not, see <http://www.gnu.org/licenses/>.
*/ */
package org.floens.chan.core.site.loaders; package org.floens.chan.core.site.sites.chan4;
import android.util.JsonReader; import android.util.JsonReader;

@ -15,12 +15,14 @@
* You should have received a copy of the GNU General Public License * You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>. * along with this program. If not, see <http://www.gnu.org/licenses/>.
*/ */
package org.floens.chan.core.http; package org.floens.chan.core.site.sites.chan4;
import android.text.TextUtils; import android.text.TextUtils;
import org.floens.chan.chan.ChanUrls; import org.floens.chan.core.site.Site;
import org.floens.chan.core.model.Reply; import org.floens.chan.core.site.http.HttpCall;
import org.floens.chan.core.site.http.ReplyResponse;
import org.floens.chan.core.site.http.Reply;
import org.jsoup.Jsoup; import org.jsoup.Jsoup;
import java.io.IOException; import java.io.IOException;
@ -34,40 +36,34 @@ import okhttp3.Request;
import okhttp3.RequestBody; import okhttp3.RequestBody;
import okhttp3.Response; import okhttp3.Response;
public class ReplyHttpCall extends HttpCall { public class Chan4ReplyHttpCall extends HttpCall {
private static final String TAG = "ReplyHttpCall"; private static final String TAG = "Chan4ReplyHttpCall";
private static final Random RANDOM = new Random(); private static final Random RANDOM = new Random();
private static final Pattern THREAD_NO_PATTERN = Pattern.compile("<!-- thread:([0-9]+),no:([0-9]+) -->"); private static final Pattern THREAD_NO_PATTERN = Pattern.compile("<!-- thread:([0-9]+),no:([0-9]+) -->");
private static final Pattern ERROR_MESSAGE = Pattern.compile("\"errmsg\"[^>]*>(.*?)<\\/span"); private static final Pattern ERROR_MESSAGE = Pattern.compile("\"errmsg\"[^>]*>(.*?)<\\/span");
private static final String PROBABLY_BANNED_TEXT = "banned";
public boolean posted; public final Reply reply;
public String errorMessage; public final ReplyResponse replyResponse = new ReplyResponse();
public String text;
public String password;
public int threadNo = -1;
public int postNo = -1;
public boolean probablyBanned;
private final Reply reply; public Chan4ReplyHttpCall(Reply reply) {
public ReplyHttpCall(Reply reply) {
this.reply = reply; this.reply = reply;
} }
@Override @Override
public void setup(Request.Builder requestBuilder) { public void setup(Request.Builder requestBuilder) {
boolean thread = reply.resto >= 0; boolean thread = reply.loadable.isThreadMode();
password = Long.toHexString(RANDOM.nextLong()); replyResponse.password = Long.toHexString(RANDOM.nextLong());
MultipartBody.Builder formBuilder = new MultipartBody.Builder(); MultipartBody.Builder formBuilder = new MultipartBody.Builder();
formBuilder.setType(MultipartBody.FORM); formBuilder.setType(MultipartBody.FORM);
formBuilder.addFormDataPart("mode", "regist"); formBuilder.addFormDataPart("mode", "regist");
formBuilder.addFormDataPart("pwd", password); formBuilder.addFormDataPart("pwd", replyResponse.password);
if (thread) { if (thread) {
formBuilder.addFormDataPart("resto", String.valueOf(reply.resto)); formBuilder.addFormDataPart("resto", String.valueOf(reply.loadable.no));
} }
formBuilder.addFormDataPart("name", reply.name); formBuilder.addFormDataPart("name", reply.name);
@ -79,7 +75,7 @@ public class ReplyHttpCall extends HttpCall {
formBuilder.addFormDataPart("com", reply.comment); formBuilder.addFormDataPart("com", reply.comment);
if (reply.captchaResponse != null) { if (!reply.noVerification) {
if (reply.captchaChallenge != null) { if (reply.captchaChallenge != null) {
formBuilder.addFormDataPart("recaptcha_challenge_field", reply.captchaChallenge); formBuilder.addFormDataPart("recaptcha_challenge_field", reply.captchaChallenge);
formBuilder.addFormDataPart("recaptcha_response_field", reply.captchaResponse); formBuilder.addFormDataPart("recaptcha_response_field", reply.captchaResponse);
@ -98,33 +94,32 @@ public class ReplyHttpCall extends HttpCall {
formBuilder.addFormDataPart("spoiler", "on"); formBuilder.addFormDataPart("spoiler", "on");
} }
requestBuilder.url(ChanUrls.getReplyUrl(reply.board)); Site site = reply.loadable.getSite();
requestBuilder.url(site.endpoints().reply(reply.loadable));
requestBuilder.post(formBuilder.build()); requestBuilder.post(formBuilder.build());
if (reply.usePass) { if (reply.noVerification) {
requestBuilder.addHeader("Cookie", "pass_id=" + reply.passId); requestBuilder.addHeader("Cookie", "pass_id=" + reply.passId);
} }
} }
@Override @Override
public void process(Response response, String result) throws IOException { public void process(Response response, String result) throws IOException {
text = result;
Matcher errorMessageMatcher = ERROR_MESSAGE.matcher(result); Matcher errorMessageMatcher = ERROR_MESSAGE.matcher(result);
if (errorMessageMatcher.find()) { if (errorMessageMatcher.find()) {
errorMessage = Jsoup.parse(errorMessageMatcher.group(1)).body().text(); replyResponse.errorMessage = Jsoup.parse(errorMessageMatcher.group(1)).body().text();
probablyBanned = errorMessage.contains("banned"); replyResponse.probablyBanned = replyResponse.errorMessage.contains(PROBABLY_BANNED_TEXT);
} else { } else {
Matcher threadNoMatcher = THREAD_NO_PATTERN.matcher(result); Matcher threadNoMatcher = THREAD_NO_PATTERN.matcher(result);
if (threadNoMatcher.find()) { if (threadNoMatcher.find()) {
try { try {
threadNo = Integer.parseInt(threadNoMatcher.group(1)); replyResponse.threadNo = Integer.parseInt(threadNoMatcher.group(1));
postNo = Integer.parseInt(threadNoMatcher.group(2)); replyResponse.postNo = Integer.parseInt(threadNoMatcher.group(2));
} catch (NumberFormatException ignored) { } catch (NumberFormatException ignored) {
} }
if (threadNo >= 0 && postNo >= 0) { if (replyResponse.threadNo >= 0 && replyResponse.postNo >= 0) {
posted = true; replyResponse.posted = true;
} }
} }
} }

@ -15,7 +15,7 @@
* You should have received a copy of the GNU General Public License * You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>. * along with this program. If not, see <http://www.gnu.org/licenses/>.
*/ */
package org.floens.chan.core.site.loaders; package org.floens.chan.core.site.sites.chan4;
import org.floens.chan.chan.ChanParser; import org.floens.chan.chan.ChanParser;
import org.floens.chan.core.database.DatabaseSavedReplyManager; import org.floens.chan.core.database.DatabaseSavedReplyManager;

@ -26,12 +26,16 @@ import android.widget.EditText;
import android.widget.LinearLayout; import android.widget.LinearLayout;
import android.widget.TextView; import android.widget.TextView;
import org.floens.chan.Chan;
import org.floens.chan.R; import org.floens.chan.R;
import org.floens.chan.controller.Controller; import org.floens.chan.controller.Controller;
import org.floens.chan.core.http.PassHttpCall; import org.floens.chan.core.manager.ReplyManager;
import org.floens.chan.core.http.ReplyManager;
import org.floens.chan.core.settings.ChanSettings; import org.floens.chan.core.settings.ChanSettings;
import org.floens.chan.core.site.Site;
import org.floens.chan.core.site.Sites;
import org.floens.chan.core.site.http.HttpCall;
import org.floens.chan.core.site.http.HttpCallManager;
import org.floens.chan.core.site.http.LoginRequest;
import org.floens.chan.core.site.http.LoginResponse;
import org.floens.chan.ui.view.CrossfadeView; import org.floens.chan.ui.view.CrossfadeView;
import org.floens.chan.utils.AndroidUtils; import org.floens.chan.utils.AndroidUtils;
import org.floens.chan.utils.AnimationUtils; import org.floens.chan.utils.AnimationUtils;
@ -41,10 +45,13 @@ import javax.inject.Inject;
import static org.floens.chan.Chan.getGraph; import static org.floens.chan.Chan.getGraph;
import static org.floens.chan.utils.AndroidUtils.getString; import static org.floens.chan.utils.AndroidUtils.getString;
public class PassSettingsController extends Controller implements View.OnClickListener, ReplyManager.HttpCallback<PassHttpCall> { public class PassSettingsController extends Controller implements View.OnClickListener, Site.LoginListener {
@Inject @Inject
ReplyManager replyManager; ReplyManager replyManager;
@Inject
HttpCallManager httpCallManager;
private LinearLayout container; private LinearLayout container;
private CrossfadeView crossfadeView; private CrossfadeView crossfadeView;
private TextView errors; private TextView errors;
@ -120,36 +127,37 @@ public class PassSettingsController extends Controller implements View.OnClickLi
} }
@Override @Override
public void onHttpSuccess(PassHttpCall httpPost) { public void onLoginComplete(HttpCall httpCall, LoginResponse loginResponse) {
if (httpPost.success) { if (loginResponse.success) {
authSuccess(httpPost); authSuccess(loginResponse);
} else { } else {
authFail(httpPost); authFail(loginResponse);
} }
authAfter(); authAfter();
} }
@Override @Override
public void onHttpFail(PassHttpCall httpPost) { public void onLoginError(HttpCall httpCall) {
authFail(httpPost); authFail(null);
authAfter(); authAfter();
} }
private void authSuccess(PassHttpCall httpPost) { private void authSuccess(LoginResponse response) {
crossfadeView.toggle(false, true); crossfadeView.toggle(false, true);
button.setText(R.string.setting_pass_logout); button.setText(R.string.setting_pass_logout);
ChanSettings.passId.set(httpPost.passId); ChanSettings.passId.set(response.token);
authenticated.setText(httpPost.message); authenticated.setText(response.message);
((PassSettingControllerListener) previousSiblingController).onPassEnabledChanged(true); ((PassSettingControllerListener) previousSiblingController).onPassEnabledChanged(true);
} }
private void authFail(PassHttpCall httpPost) { private void authFail(LoginResponse response) {
if (httpPost.message == null) { String message = getString(R.string.setting_pass_error);
httpPost.message = getString(R.string.setting_pass_error); if (response != null && response.message != null) {
message = response.message;
} }
showError(httpPost.message); showError(message);
button.setText(R.string.setting_pass_login); button.setText(R.string.setting_pass_login);
} }
@ -170,7 +178,9 @@ public class PassSettingsController extends Controller implements View.OnClickLi
ChanSettings.passToken.set(inputToken.getText().toString()); ChanSettings.passToken.set(inputToken.getText().toString());
ChanSettings.passPin.set(inputPin.getText().toString()); ChanSettings.passPin.set(inputPin.getText().toString());
replyManager.makeHttpCall(new PassHttpCall(ChanSettings.passToken.get(), ChanSettings.passPin.get()), this); // TODO(multi-site) some selector of some sorts
Site site = Sites.defaultSite();
site.login(new LoginRequest(site, ChanSettings.passToken.get(), ChanSettings.passPin.get()), this);
} }
private void showError(String error) { private void showError(String error) {

@ -20,6 +20,7 @@ package org.floens.chan.ui.controller;
import android.annotation.SuppressLint; import android.annotation.SuppressLint;
import android.content.Context; import android.content.Context;
import android.webkit.CookieManager; import android.webkit.CookieManager;
import android.webkit.WebSettings;
import android.webkit.WebView; import android.webkit.WebView;
import org.floens.chan.R; import org.floens.chan.R;
@ -27,8 +28,12 @@ import org.floens.chan.chan.ChanUrls;
import org.floens.chan.controller.Controller; import org.floens.chan.controller.Controller;
import org.floens.chan.core.model.Post; import org.floens.chan.core.model.Post;
import org.floens.chan.core.settings.ChanSettings; import org.floens.chan.core.settings.ChanSettings;
import org.floens.chan.core.site.Site;
import org.floens.chan.core.site.Sites;
import org.floens.chan.ui.helper.PostHelper; import org.floens.chan.ui.helper.PostHelper;
import okhttp3.HttpUrl;
public class ReportController extends Controller { public class ReportController extends Controller {
private Post post; private Post post;
@ -44,18 +49,26 @@ public class ReportController extends Controller {
super.onCreate(); super.onCreate();
navigationItem.title = context.getString(R.string.report_screen, PostHelper.getTitle(post, null)); navigationItem.title = context.getString(R.string.report_screen, PostHelper.getTitle(post, null));
CookieManager cookieManager = CookieManager.getInstance(); Site site = post.board.getSite();
cookieManager.removeAllCookie(); String url = site.endpoints().report(post);
if (ChanSettings.passLoggedIn()) {
for (String cookie : ChanUrls.getReportCookies(ChanSettings.passId.get())) { if (site == Sites.CHAN4) {
cookieManager.setCookie(ChanUrls.getReportDomain(), cookie); CookieManager cookieManager = CookieManager.getInstance();
cookieManager.removeAllCookie();
if (ChanSettings.passLoggedIn()) {
HttpUrl parsed = HttpUrl.parse(url);
String domain = parsed.scheme() + "://" + parsed.host() + "/";
for (String cookie : ChanUrls.getReportCookies(ChanSettings.passId.get())) {
cookieManager.setCookie(domain, cookie);
}
} }
} }
WebView webView = new WebView(context); WebView webView = new WebView(context);
webView.getSettings().setJavaScriptEnabled(true); WebSettings settings = webView.getSettings();
webView.getSettings().setDomStorageEnabled(true); settings.setJavaScriptEnabled(true);
webView.loadUrl(ChanUrls.getReportUrl(post.boardId, post.no)); settings.setDomStorageEnabled(true);
webView.loadUrl(url);
view = webView; view = webView;
} }
} }

@ -21,12 +21,10 @@ import android.app.Activity;
import android.content.Intent; import android.content.Intent;
import android.database.Cursor; import android.database.Cursor;
import android.net.Uri; import android.net.Uri;
import android.os.Build;
import android.os.ParcelFileDescriptor; import android.os.ParcelFileDescriptor;
import android.provider.OpenableColumns; import android.provider.OpenableColumns;
import org.floens.chan.Chan; import org.floens.chan.core.manager.ReplyManager;
import org.floens.chan.core.http.ReplyManager;
import org.floens.chan.utils.IOUtils; import org.floens.chan.utils.IOUtils;
import org.floens.chan.utils.Logger; import org.floens.chan.utils.Logger;

@ -37,7 +37,7 @@ import android.widget.Toast;
import org.floens.chan.R; import org.floens.chan.R;
import org.floens.chan.core.model.ChanThread; import org.floens.chan.core.model.ChanThread;
import org.floens.chan.core.model.Loadable; import org.floens.chan.core.model.Loadable;
import org.floens.chan.core.model.Reply; import org.floens.chan.core.site.http.Reply;
import org.floens.chan.core.presenter.ReplyPresenter; import org.floens.chan.core.presenter.ReplyPresenter;
import org.floens.chan.core.settings.ChanSettings; import org.floens.chan.core.settings.ChanSettings;
import org.floens.chan.ui.activity.StartActivity; import org.floens.chan.ui.activity.StartActivity;

Loading…
Cancel
Save