Merge pull request #558 from anselmov/dev

add support for 2ch.hk
master
Florens 7 years ago committed by GitHub
commit 5fd9904ed8
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
  1. 3
      Clover/app/src/main/java/org/floens/chan/core/site/SiteRegistry.java
  2. 213
      Clover/app/src/main/java/org/floens/chan/core/site/sites/dvach/Dvach.java
  3. 234
      Clover/app/src/main/java/org/floens/chan/core/site/sites/dvach/DvachApi.java
  4. 108
      Clover/app/src/main/java/org/floens/chan/core/site/sites/dvach/DvachBoardsRequest.java
  5. 101
      Clover/app/src/main/java/org/floens/chan/core/site/sites/dvach/DvachReplyCall.java

@ -24,6 +24,7 @@ import org.floens.chan.core.site.sites.lainchan.Lainchan;
import org.floens.chan.core.site.sites.chan8.Chan8;
import org.floens.chan.core.site.sites.arisuchan.Arisuchan;
import org.floens.chan.core.site.sites.sushichan.Sushichan;
import org.floens.chan.core.site.sites.dvach.Dvach;
import java.util.ArrayList;
import java.util.List;
@ -41,6 +42,7 @@ public class SiteRegistry {
URL_HANDLERS.add(Lainchan.URL_HANDLER);
URL_HANDLERS.add(Arisuchan.URL_HANDLER);
URL_HANDLERS.add(Sushichan.URL_HANDLER);
URL_HANDLERS.add(Dvach.URL_HANDLER);
}
static {
@ -53,5 +55,6 @@ public class SiteRegistry {
SITE_CLASSES.put(2, Lainchan.class);
SITE_CLASSES.put(3, Arisuchan.class);
SITE_CLASSES.put(4, Sushichan.class);
SITE_CLASSES.put(5, Dvach.class);
}
}

@ -0,0 +1,213 @@
package org.floens.chan.core.site.sites.dvach;
import android.support.annotation.Nullable;
import org.floens.chan.core.model.Post;
import org.floens.chan.core.model.orm.Board;
import org.floens.chan.core.model.orm.Loadable;
import org.floens.chan.core.settings.OptionsSetting;
import org.floens.chan.core.site.Boards;
import org.floens.chan.core.site.Site;
import org.floens.chan.core.site.SiteAuthentication;
import org.floens.chan.core.site.SiteIcon;
import org.floens.chan.core.site.SiteSetting;
import org.floens.chan.core.site.common.CommonReplyHttpCall;
import org.floens.chan.core.site.common.CommonSite;
import org.floens.chan.core.site.common.MultipartHttpCall;
import org.floens.chan.core.site.common.vichan.VichanActions;
import org.floens.chan.core.site.common.vichan.VichanCommentParser;
import org.floens.chan.core.site.common.vichan.VichanEndpoints;
import org.floens.chan.core.site.http.DeleteRequest;
import org.floens.chan.core.site.http.HttpCall;
import org.floens.chan.core.site.http.Reply;
import org.floens.chan.core.site.sites.chan4.Chan4;
import org.floens.chan.utils.Logger;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.List;
import java.util.Map;
import okhttp3.HttpUrl;
import static android.support.constraint.Constraints.TAG;
public class Dvach extends CommonSite {
public static final CommonSiteUrlHandler URL_HANDLER = new CommonSiteUrlHandler() {
@Override
public Class<? extends Site> getSiteClass() {
return Dvach.class;
}
@Override
public HttpUrl getUrl() {
return HttpUrl.parse("https:/2ch.hk");
}
@Override
public String[] getNames() {
return new String[]{"dvach", "2ch"};
}
@Override
public String desktopUrl(Loadable loadable, @Nullable Post post) {
if (loadable.isCatalogMode()) {
return getUrl().newBuilder().addPathSegment(loadable.boardCode).toString();
} else if (loadable.isThreadMode()) {
return getUrl().newBuilder()
.addPathSegment(loadable.boardCode).addPathSegment("res")
.addPathSegment(String.valueOf(loadable.no) + ".html")
.toString();
} else {
return getUrl().toString();
}
}
};
static final String CAPTCHA_KEY = "6LeQYz4UAAAAAL8JCk35wHSv6cuEV5PyLhI6IxsM";
private OptionsSetting<Chan4.CaptchaType> captchaType;
@Override
public void initializeSettings() {
super.initializeSettings();
captchaType = new OptionsSetting<>(settingsProvider,
"preference_captcha_type",
Chan4.CaptchaType.class, Chan4.CaptchaType.V2JS);
}
@Override
public List<SiteSetting> settings() {
return Arrays.asList(
SiteSetting.forOption(
captchaType,
"Captcha type",
Arrays.asList("Javascript", "Noscript"))
);
}
@Override
public void setup() {
setName("2ch.hk");
setIcon(SiteIcon.fromFavicon(HttpUrl.parse("https://2ch.hk/favicon.ico")));
setBoardsType(BoardsType.DYNAMIC);
setResolvable(URL_HANDLER);
setConfig(new CommonConfig() {
@Override
public boolean feature(Feature feature) {
return feature == Feature.POSTING;
}
});
setEndpoints(new VichanEndpoints(this,
"https://2ch.hk",
"https://2ch.hk") {
@Override
public HttpUrl imageUrl(Post.Builder post, Map<String, String> arg) {
return root.builder().s(arg.get("path")).url();
}
@Override
public HttpUrl thumbnailUrl(Post.Builder post, boolean spoiler, Map<String, String> arg) {
return root.builder().s(arg.get("thumbnail")).url();
}
@Override
public HttpUrl boards() {
return new HttpUrl.Builder().scheme("https").host("2ch.hk").addPathSegment("boards.json").build();
}
@Override
public HttpUrl reply(Loadable loadable) {
return new HttpUrl.Builder().scheme("https").host("2ch.hk").addPathSegment("makaba").addPathSegment("posting.fcgi").addQueryParameter("json", "1").build();
}
});
setActions(new VichanActions(this) {
@Override
public void setupPost(Reply reply, MultipartHttpCall call) {
super.setupPost(reply, call);
if (reply.loadable.isThreadMode()) {
// "thread" is already added in VichanActions.
call.parameter("post", "New Reply");
} else {
call.parameter("post", "New Thread");
call.parameter("page", "1");
}
}
@Override
public boolean requirePrepare() {
return false;
}
@Override
public void post(Reply reply, final PostListener postListener) {
httpCallManager.makeHttpCall(new DvachReplyCall(Dvach.this, reply), new HttpCall.HttpCallback<CommonReplyHttpCall>() {
@Override
public void onHttpSuccess(CommonReplyHttpCall httpPost) {
postListener.onPostComplete(httpPost, httpPost.replyResponse);
}
@Override
public void onHttpFail(CommonReplyHttpCall httpPost, Exception e) {
postListener.onPostError(httpPost, e);
}
});
}
@Override
public boolean postRequiresAuthentication() {
return !isLoggedIn();
}
@Override
public SiteAuthentication postAuthenticate() {
if (isLoggedIn()) {
return SiteAuthentication.fromNone();
} else {
switch (captchaType.get()) {
case V2JS:
return SiteAuthentication.fromCaptcha2(CAPTCHA_KEY, "https://2ch.hk/api/captcha/recaptcha/mobile");
case V2NOJS:
return SiteAuthentication.fromCaptcha2nojs(CAPTCHA_KEY, "https://2ch.hk/api/captcha/recaptcha/mobile");
default:
throw new IllegalArgumentException();
}
}
}
@Override
public void delete(DeleteRequest deleteRequest, DeleteListener deleteListener) {
super.delete(deleteRequest, deleteListener);
}
@Override
public void boards(final BoardsListener listener) {
requestQueue.add(new DvachBoardsRequest(Dvach.this, response -> {
listener.onBoardsReceived(new Boards(response));
}, (error) -> {
Logger.e(TAG, "Failed to get boards from server", error);
// API fail, provide some default boards
List<Board> list = new ArrayList<>();
list.add(Board.fromSiteNameCode(Dvach.this, "бред", "b"));
list.add(Board.fromSiteNameCode(Dvach.this, "Видеоигры, general, официальные треды", "vg"));
list.add(Board.fromSiteNameCode(Dvach.this, "новости", "news"));
list.add(Board.fromSiteNameCode(Dvach.this, "политика, новости, ольгинцы, хохлы, либерахи, рептилоиды.. oh shi", "po"));
Collections.shuffle(list);
listener.onBoardsReceived(new Boards(list));
}));
}
});
setApi(new DvachApi(this));
setParser(new VichanCommentParser());
}
}

@ -0,0 +1,234 @@
package org.floens.chan.core.site.sites.dvach;
import android.util.JsonReader;
import org.floens.chan.core.model.Post;
import org.floens.chan.core.model.PostImage;
import org.floens.chan.core.site.SiteEndpoints;
import org.floens.chan.core.site.common.CommonSite;
import org.floens.chan.core.site.parser.ChanReaderProcessingQueue;
import org.jsoup.parser.Parser;
import java.io.IOException;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import static org.floens.chan.core.site.SiteEndpoints.makeArgument;
public class DvachApi extends CommonSite.CommonApi {
DvachApi(CommonSite commonSite) {
super(commonSite);
}
@Override
public void loadThread(JsonReader reader, ChanReaderProcessingQueue queue) throws Exception {
reader.beginObject(); // Main object
while (reader.hasNext()) {
if (reader.nextName().equals("threads")) {
reader.beginArray(); // Threads array
while (reader.hasNext()) {
reader.beginObject(); // Posts object
if (reader.nextName().equals("posts")) {
reader.beginArray(); // Posts array
while (reader.hasNext()) {
readPostObject(reader, queue);
}
reader.endArray();
}
reader.endObject();
}
reader.endArray();
} else {
reader.skipValue();
}
}
reader.endObject();
}
@Override
public void loadCatalog(JsonReader reader, ChanReaderProcessingQueue queue) throws Exception {
reader.beginObject(); // Main object
while (reader.hasNext()) {
if (reader.nextName().equals("threads")) {
reader.beginArray(); // Threads array
while (reader.hasNext()) {
readPostObject(reader, queue);
}
reader.endArray();
} else {
reader.skipValue();
}
}
reader.endObject();
}
@Override
public void readPostObject(JsonReader reader, ChanReaderProcessingQueue queue) throws Exception {
Post.Builder builder = new Post.Builder();
builder.board(queue.getLoadable().board);
SiteEndpoints endpoints = queue.getLoadable().getSite().endpoints();
List<PostImage> files = new ArrayList<>();
reader.beginObject();
while (reader.hasNext()) {
String key = reader.nextName();
switch (key) {
case "name":
builder.name(reader.nextString());
break;
case "comment":
builder.comment(reader.nextString());
break;
case "timestamp":
builder.setUnixTimestampSeconds(reader.nextLong());
break;
case "trip":
builder.tripcode(reader.nextString());
break;
case "op":
int opId = reader.nextInt();
builder.op(opId == 0);
builder.opId(opId);
break;
case "sticky":
builder.sticky(reader.nextInt() == 1);
break;
case "closed":
builder.closed(reader.nextInt() == 1);
break;
case "archived":
builder.archived(reader.nextInt() == 1);
break;
case "posts_count":
builder.replies(reader.nextInt() - 1);
break;
case "files_count":
builder.images(reader.nextInt());
break;
case "lasthit":
builder.lastModified(reader.nextLong());
break;
case "num":
String num = reader.nextString();
builder.id(Integer.parseInt(num));
break;
case "files":
reader.beginArray();
while (reader.hasNext()) {
PostImage postImage = readPostImage(reader, builder, endpoints);
if (postImage != null) {
files.add(postImage);
}
}
reader.endArray();
break;
default:
// Unknown/ignored key
reader.skipValue();
break;
}
}
reader.endObject();
builder.images(files);
if (builder.op) {
// Update OP fields later on the main thread
Post.Builder op = new Post.Builder();
op.closed(builder.closed);
op.archived(builder.archived);
op.sticky(builder.sticky);
op.replies(builder.replies);
op.images(builder.imagesCount);
op.uniqueIps(builder.uniqueIps);
op.lastModified(builder.lastModified);
queue.setOp(op);
}
Post cached = queue.getCachedPost(builder.id);
if (cached != null) {
// Id is known, use the cached post object.
queue.addForReuse(cached);
return;
}
queue.addForParse(builder);
}
private PostImage readPostImage(JsonReader reader, Post.Builder builder,
SiteEndpoints endpoints) throws IOException {
reader.beginObject();
String path = null;
long fileSize = 0;
String fileExt = null;
int fileWidth = 0;
int fileHeight = 0;
String fileName = null;
String thumbnail = null;
while (reader.hasNext()) {
switch (reader.nextName()) {
case "path":
path = reader.nextString();
break;
case "name":
fileName = reader.nextString();
break;
case "size":
fileSize = reader.nextLong();
break;
case "width":
fileWidth = reader.nextInt();
break;
case "height":
fileHeight = reader.nextInt();
break;
case "thumbnail":
thumbnail = reader.nextString();
break;
default:
reader.skipValue();
break;
}
}
reader.endObject();
if (fileName != null) {
fileExt = fileName.substring(fileName.lastIndexOf('.') + 1);
fileName = fileName.substring(0, fileName.lastIndexOf('.'));
}
if (path != null && fileName != null) {
Map<String, String> args = makeArgument("path", path, "thumbnail", thumbnail);
return new PostImage.Builder()
.originalName(fileName)
.thumbnailUrl(endpoints.thumbnailUrl(builder, false, args))
.spoilerThumbnailUrl(endpoints.thumbnailUrl(builder, true, args))
.imageUrl(endpoints.imageUrl(builder, args))
.filename(Parser.unescapeEntities(fileName, false))
.extension(fileExt)
.imageWidth(fileWidth)
.imageHeight(fileHeight)
.size(fileSize)
.build();
}
return null;
}
}

@ -0,0 +1,108 @@
/*
* 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.dvach;
import android.util.JsonReader;
import com.android.volley.Response.ErrorListener;
import com.android.volley.Response.Listener;
import org.floens.chan.core.model.orm.Board;
import org.floens.chan.core.net.JsonReaderRequest;
import org.floens.chan.core.site.Site;
import java.io.IOException;
import java.util.ArrayList;
import java.util.List;
public class DvachBoardsRequest extends JsonReaderRequest<List<Board>> {
private final Site site;
DvachBoardsRequest(Site site, Listener<List<Board>> listener, ErrorListener errorListener) {
super(site.endpoints().boards().toString(), listener, errorListener);
this.site = site;
}
@Override
public List<Board> readJson(JsonReader reader) throws Exception {
List<Board> list = new ArrayList<>();
reader.beginObject();
while (reader.hasNext()) {
String key = reader.nextName();
if (key.equals("boards")) {
reader.beginArray();
while (reader.hasNext()) {
Board board = readBoardEntry(reader);
if (board != null) {
list.add(board);
}
}
reader.endArray();
} else {
reader.skipValue();
}
}
reader.endObject();
return list;
}
private Board readBoardEntry(JsonReader reader) throws IOException {
reader.beginObject();
Board board = new Board();
board.siteId = site.id();
board.site = site;
while (reader.hasNext()) {
String key = reader.nextName();
switch (key) {
case "name":
board.name = reader.nextString();
break;
case "id":
board.code = reader.nextString();
break;
case "bump_limit":
board.bumpLimit = reader.nextInt();
break;
case "info":
board.description = reader.nextString();
break;
case "category":
board.workSafe = !"Взрослым".equals(reader.nextString());
break;
default:
reader.skipValue();
break;
}
}
reader.endObject();
if (!board.finish()) {
// Invalid data, ignore
return null;
}
return board;
}
}

@ -0,0 +1,101 @@
/*
* 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.dvach;
import android.text.TextUtils;
import org.floens.chan.core.site.Site;
import org.floens.chan.core.site.common.CommonReplyHttpCall;
import org.floens.chan.core.site.http.Reply;
import org.jsoup.Jsoup;
import java.io.IOException;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import okhttp3.MediaType;
import okhttp3.MultipartBody;
import okhttp3.RequestBody;
import okhttp3.Response;
public class DvachReplyCall extends CommonReplyHttpCall {
private static final Pattern ERROR_MESSAGE = Pattern.compile("^\\{\"Error\":-\\d+,\"Reason\":\"(.*)\"");
private static final Pattern POST_MESSAGE = Pattern.compile("^\\{\"Error\":null,\"Status\":\"OK\",\"Num\":(\\d+)");
private static final Pattern THREAD_MESSAGE = Pattern.compile("^\\{\"Error\":null,\"Status\":\"Redirect\",\"Target\":(\\d+)");
private static final String PROBABLY_BANNED_TEXT = "banned";
DvachReplyCall(Site site, Reply reply) {
super(site, reply);
}
@Override
public void addParameters(MultipartBody.Builder formBuilder) {
formBuilder.addFormDataPart("task", "post");
formBuilder.addFormDataPart("board", reply.loadable.boardCode);
formBuilder.addFormDataPart("comment", reply.comment);
formBuilder.addFormDataPart("thread", String.valueOf(reply.loadable.no));
formBuilder.addFormDataPart("name", reply.name);
formBuilder.addFormDataPart("email", reply.options);
if (!reply.loadable.isThreadMode() && !TextUtils.isEmpty(reply.subject)) {
formBuilder.addFormDataPart("subject", reply.subject);
}
if (reply.captchaResponse != null) {
formBuilder.addFormDataPart("captcha_type", "recaptcha");
formBuilder.addFormDataPart("captcha_key", Dvach.CAPTCHA_KEY);
if (reply.captchaChallenge != null) {
formBuilder.addFormDataPart("recaptcha_challenge_field", reply.captchaChallenge);
formBuilder.addFormDataPart("recaptcha_response_field", reply.captchaResponse);
} else {
formBuilder.addFormDataPart("g-recaptcha-response", reply.captchaResponse);
}
}
if (reply.file != null) {
formBuilder.addFormDataPart("image", reply.fileName, RequestBody.create(
MediaType.parse("application/octet-stream"), reply.file
));
}
}
@Override
public void process(Response response, String result) throws IOException {
Matcher errorMessageMatcher = ERROR_MESSAGE.matcher(result);
if (errorMessageMatcher.find()) {
replyResponse.errorMessage = Jsoup.parse(errorMessageMatcher.group(1)).body().text();
replyResponse.probablyBanned = replyResponse.errorMessage.contains(PROBABLY_BANNED_TEXT);
} else {
replyResponse.posted = true;
Matcher postMessageMatcher = POST_MESSAGE.matcher(result);
if (postMessageMatcher.find()) {
replyResponse.postNo = Integer.parseInt(postMessageMatcher.group(1));
} else {
Matcher threadMessageMatcher = THREAD_MESSAGE.matcher(result);
if (threadMessageMatcher.find()) {
int threadNo = Integer.parseInt(threadMessageMatcher.group(1));
replyResponse.threadNo = threadNo;
replyResponse.postNo = threadNo;
}
}
}
}
}
Loading…
Cancel
Save