mirror of https://github.com/kurisufriend/Clover
parent
26683237e5
commit
31678e104e
@ -0,0 +1,51 @@ |
|||||||
|
/* |
||||||
|
* 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; |
||||||
|
|
||||||
|
import android.util.SparseArray; |
||||||
|
|
||||||
|
import org.floens.chan.core.site.sites.chan4.Chan4; |
||||||
|
import org.floens.chan.core.site.sites.lainchan.Lainchan; |
||||||
|
import org.floens.chan.core.site.sites.vichan.ViChan; |
||||||
|
|
||||||
|
import java.util.ArrayList; |
||||||
|
import java.util.List; |
||||||
|
|
||||||
|
/** |
||||||
|
* Registry of all sites and url handler we have. |
||||||
|
*/ |
||||||
|
public class SiteRegistry { |
||||||
|
public static final List<SiteUrlHandler> URL_HANDLERS = new ArrayList<>(); |
||||||
|
public static final SparseArray<Class<? extends Site>> SITE_CLASSES = new SparseArray<>(); |
||||||
|
|
||||||
|
static { |
||||||
|
URL_HANDLERS.add(Chan4.URL_HANDLER); |
||||||
|
URL_HANDLERS.add(ViChan.URL_HANDLER); |
||||||
|
URL_HANDLERS.add(Lainchan.URL_HANDLER); |
||||||
|
} |
||||||
|
|
||||||
|
static { |
||||||
|
// This id-siteclass mapping is used to look up the correct site class at deserialization.
|
||||||
|
// This differs from the Site.id() id, that id is used for site instance linking, this is just to
|
||||||
|
// find the correct class to use.
|
||||||
|
SITE_CLASSES.put(0, Chan4.class); |
||||||
|
|
||||||
|
SITE_CLASSES.put(1, ViChan.class); |
||||||
|
SITE_CLASSES.put(2, Lainchan.class); |
||||||
|
} |
||||||
|
} |
@ -0,0 +1,106 @@ |
|||||||
|
/* |
||||||
|
* 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.common.vichan; |
||||||
|
|
||||||
|
import org.floens.chan.utils.Logger; |
||||||
|
import org.jsoup.Jsoup; |
||||||
|
import org.jsoup.nodes.Document; |
||||||
|
import org.jsoup.nodes.Element; |
||||||
|
import org.jsoup.select.Elements; |
||||||
|
|
||||||
|
import java.io.IOException; |
||||||
|
import java.util.ArrayList; |
||||||
|
import java.util.Arrays; |
||||||
|
import java.util.HashMap; |
||||||
|
import java.util.List; |
||||||
|
import java.util.Map; |
||||||
|
|
||||||
|
import okhttp3.HttpUrl; |
||||||
|
import okhttp3.OkHttpClient; |
||||||
|
import okhttp3.Request; |
||||||
|
import okhttp3.Response; |
||||||
|
import okhttp3.ResponseBody; |
||||||
|
|
||||||
|
/** |
||||||
|
* Vichan applies garbage looking fields to the post form, to combat bots. |
||||||
|
* Load up the normal html, parse the form, and get these fields for our post. |
||||||
|
* <p> |
||||||
|
* {@link #get()} blocks, run it off the main thread. |
||||||
|
*/ |
||||||
|
public class VichanAntispam { |
||||||
|
private static final String TAG = "Antispam"; |
||||||
|
private HttpUrl url; |
||||||
|
|
||||||
|
private OkHttpClient okHttpClient = new OkHttpClient(); |
||||||
|
|
||||||
|
private List<String> fieldsToIgnore = new ArrayList<>(); |
||||||
|
|
||||||
|
public VichanAntispam(HttpUrl url) { |
||||||
|
this.url = url; |
||||||
|
} |
||||||
|
|
||||||
|
public void addDefaultIgnoreFields() { |
||||||
|
fieldsToIgnore.addAll(Arrays.asList("board", "thread", "name", "email", |
||||||
|
"subject", "body", "password", "file", "spoiler", "json_response", |
||||||
|
"file_url1", "file_url2", "file_url3")); |
||||||
|
} |
||||||
|
|
||||||
|
public void ignoreField(String name) { |
||||||
|
fieldsToIgnore.add(name); |
||||||
|
} |
||||||
|
|
||||||
|
public Map<String, String> get() { |
||||||
|
Map<String, String> res = new HashMap<>(); |
||||||
|
|
||||||
|
Request request = new Request.Builder() |
||||||
|
.url(url) |
||||||
|
.build(); |
||||||
|
try { |
||||||
|
Response response = okHttpClient.newCall(request).execute(); |
||||||
|
ResponseBody body = response.body(); |
||||||
|
if (body != null) { |
||||||
|
Document document = Jsoup.parse(body.string()); |
||||||
|
Elements form = document.body().getElementsByTag("form"); |
||||||
|
for (Element element : form) { |
||||||
|
if (element.attr("name").equals("post")) { |
||||||
|
// Add all <input> and <textarea> elements.
|
||||||
|
Elements inputs = element.getElementsByTag("input"); |
||||||
|
inputs.addAll(element.getElementsByTag("textarea")); |
||||||
|
|
||||||
|
for (Element input : inputs) { |
||||||
|
String name = input.attr("name"); |
||||||
|
String value = input.val(); |
||||||
|
|
||||||
|
if (!fieldsToIgnore.contains(name)) { |
||||||
|
res.put(name, value); |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
break; |
||||||
|
} |
||||||
|
} |
||||||
|
} |
||||||
|
} catch (IOException e) { |
||||||
|
Logger.e(TAG, "IOException parsing vichan bot fields", e); |
||||||
|
} catch (NullPointerException e) { |
||||||
|
Logger.e(TAG, "NullPointerException parsing vichan bot fields", e); |
||||||
|
} |
||||||
|
|
||||||
|
return res; |
||||||
|
} |
||||||
|
} |
@ -0,0 +1,318 @@ |
|||||||
|
package org.floens.chan.core.site.common.vichan; |
||||||
|
|
||||||
|
import android.util.JsonReader; |
||||||
|
|
||||||
|
import org.floens.chan.core.model.Post; |
||||||
|
import org.floens.chan.core.model.PostHttpIcon; |
||||||
|
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 okhttp3.HttpUrl; |
||||||
|
|
||||||
|
import static org.floens.chan.core.site.SiteEndpoints.makeArgument; |
||||||
|
|
||||||
|
public class VichanApi extends CommonSite.CommonApi { |
||||||
|
public VichanApi(CommonSite commonSite) { |
||||||
|
super(commonSite); |
||||||
|
} |
||||||
|
|
||||||
|
@Override |
||||||
|
public void loadThread(JsonReader reader, ChanReaderProcessingQueue queue) throws Exception { |
||||||
|
reader.beginObject(); |
||||||
|
// Page object
|
||||||
|
while (reader.hasNext()) { |
||||||
|
String key = reader.nextName(); |
||||||
|
if (key.equals("posts")) { |
||||||
|
reader.beginArray(); |
||||||
|
// Thread array
|
||||||
|
while (reader.hasNext()) { |
||||||
|
// Thread object
|
||||||
|
readPostObject(reader, queue); |
||||||
|
} |
||||||
|
reader.endArray(); |
||||||
|
} else { |
||||||
|
reader.skipValue(); |
||||||
|
} |
||||||
|
} |
||||||
|
reader.endObject(); |
||||||
|
} |
||||||
|
|
||||||
|
@Override |
||||||
|
public void loadCatalog(JsonReader reader, ChanReaderProcessingQueue queue) throws Exception { |
||||||
|
reader.beginArray(); // Array of pages
|
||||||
|
|
||||||
|
while (reader.hasNext()) { |
||||||
|
reader.beginObject(); // Page 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(); |
||||||
|
} |
||||||
|
|
||||||
|
reader.endArray(); |
||||||
|
} |
||||||
|
|
||||||
|
@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(); |
||||||
|
|
||||||
|
// File
|
||||||
|
String fileId = null; |
||||||
|
String fileExt = null; |
||||||
|
int fileWidth = 0; |
||||||
|
int fileHeight = 0; |
||||||
|
long fileSize = 0; |
||||||
|
boolean fileSpoiler = false; |
||||||
|
String fileName = null; |
||||||
|
|
||||||
|
List<PostImage> files = new ArrayList<>(); |
||||||
|
|
||||||
|
// Country flag
|
||||||
|
String countryCode = null; |
||||||
|
String trollCountryCode = null; |
||||||
|
String countryName = null; |
||||||
|
|
||||||
|
reader.beginObject(); |
||||||
|
while (reader.hasNext()) { |
||||||
|
String key = reader.nextName(); |
||||||
|
|
||||||
|
switch (key) { |
||||||
|
case "no": |
||||||
|
builder.id(reader.nextInt()); |
||||||
|
break; |
||||||
|
case "sub": |
||||||
|
builder.subject(reader.nextString()); |
||||||
|
break; |
||||||
|
case "name": |
||||||
|
builder.name(reader.nextString()); |
||||||
|
break; |
||||||
|
case "com": |
||||||
|
builder.comment(reader.nextString()); |
||||||
|
break; |
||||||
|
case "tim": |
||||||
|
fileId = reader.nextString(); |
||||||
|
break; |
||||||
|
case "time": |
||||||
|
builder.setUnixTimestampSeconds(reader.nextLong()); |
||||||
|
break; |
||||||
|
case "ext": |
||||||
|
fileExt = reader.nextString().replace(".", ""); |
||||||
|
break; |
||||||
|
case "w": |
||||||
|
fileWidth = reader.nextInt(); |
||||||
|
break; |
||||||
|
case "h": |
||||||
|
fileHeight = reader.nextInt(); |
||||||
|
break; |
||||||
|
case "fsize": |
||||||
|
fileSize = reader.nextLong(); |
||||||
|
break; |
||||||
|
case "filename": |
||||||
|
fileName = reader.nextString(); |
||||||
|
break; |
||||||
|
case "trip": |
||||||
|
builder.tripcode(reader.nextString()); |
||||||
|
break; |
||||||
|
case "country": |
||||||
|
countryCode = reader.nextString(); |
||||||
|
break; |
||||||
|
case "troll_country": |
||||||
|
trollCountryCode = reader.nextString(); |
||||||
|
break; |
||||||
|
case "country_name": |
||||||
|
countryName = reader.nextString(); |
||||||
|
break; |
||||||
|
case "spoiler": |
||||||
|
fileSpoiler = reader.nextInt() == 1; |
||||||
|
break; |
||||||
|
case "resto": |
||||||
|
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 "replies": |
||||||
|
builder.replies(reader.nextInt()); |
||||||
|
break; |
||||||
|
case "images": |
||||||
|
builder.images(reader.nextInt()); |
||||||
|
break; |
||||||
|
case "unique_ips": |
||||||
|
builder.uniqueIps(reader.nextInt()); |
||||||
|
break; |
||||||
|
case "id": |
||||||
|
builder.posterId(reader.nextString()); |
||||||
|
break; |
||||||
|
case "capcode": |
||||||
|
builder.moderatorCapcode(reader.nextString()); |
||||||
|
break; |
||||||
|
case "extra_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(); |
||||||
|
|
||||||
|
// The file from between the other values.
|
||||||
|
if (fileId != null && fileName != null && fileExt != null) { |
||||||
|
Map<String, String> args = makeArgument("tim", fileId, |
||||||
|
"ext", fileExt); |
||||||
|
PostImage image = new PostImage.Builder() |
||||||
|
.originalName(String.valueOf(fileId)) |
||||||
|
.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) |
||||||
|
.spoiler(fileSpoiler) |
||||||
|
.size(fileSize) |
||||||
|
.build(); |
||||||
|
// Insert it at the beginning.
|
||||||
|
files.add(0, image); |
||||||
|
} |
||||||
|
|
||||||
|
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); |
||||||
|
queue.setOp(op); |
||||||
|
} |
||||||
|
|
||||||
|
Post cached = queue.getCachedPost(builder.id); |
||||||
|
if (cached != null) { |
||||||
|
// Id is known, use the cached post object.
|
||||||
|
queue.addForReuse(cached); |
||||||
|
return; |
||||||
|
} |
||||||
|
|
||||||
|
if (countryCode != null && countryName != null) { |
||||||
|
HttpUrl countryUrl = endpoints.icon(builder, "country", |
||||||
|
makeArgument("country_code", countryCode)); |
||||||
|
builder.addHttpIcon(new PostHttpIcon(countryUrl, countryName)); |
||||||
|
} |
||||||
|
|
||||||
|
if (trollCountryCode != null && countryName != null) { |
||||||
|
HttpUrl countryUrl = endpoints.icon(builder, "troll_country", |
||||||
|
makeArgument("troll_country_code", trollCountryCode)); |
||||||
|
builder.addHttpIcon(new PostHttpIcon(countryUrl, countryName)); |
||||||
|
} |
||||||
|
|
||||||
|
queue.addForParse(builder); |
||||||
|
} |
||||||
|
|
||||||
|
private PostImage readPostImage(JsonReader reader, Post.Builder builder, |
||||||
|
SiteEndpoints endpoints) throws IOException { |
||||||
|
reader.beginObject(); |
||||||
|
|
||||||
|
String fileId = null; |
||||||
|
long fileSize = 0; |
||||||
|
|
||||||
|
String fileExt = null; |
||||||
|
int fileWidth = 0; |
||||||
|
int fileHeight = 0; |
||||||
|
boolean fileSpoiler = false; |
||||||
|
String fileName = null; |
||||||
|
|
||||||
|
while (reader.hasNext()) { |
||||||
|
switch (reader.nextName()) { |
||||||
|
case "tim": |
||||||
|
fileId = reader.nextString(); |
||||||
|
break; |
||||||
|
case "fsize": |
||||||
|
fileSize = reader.nextLong(); |
||||||
|
break; |
||||||
|
case "w": |
||||||
|
fileWidth = reader.nextInt(); |
||||||
|
break; |
||||||
|
case "h": |
||||||
|
fileHeight = reader.nextInt(); |
||||||
|
break; |
||||||
|
case "spoiler": |
||||||
|
fileSpoiler = reader.nextInt() == 1; |
||||||
|
break; |
||||||
|
case "ext": |
||||||
|
fileExt = reader.nextString().replace(".", ""); |
||||||
|
break; |
||||||
|
case "filename": |
||||||
|
fileName = reader.nextString(); |
||||||
|
break; |
||||||
|
default: |
||||||
|
reader.skipValue(); |
||||||
|
break; |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
reader.endObject(); |
||||||
|
|
||||||
|
if (fileId != null && fileName != null && fileExt != null) { |
||||||
|
Map<String, String> args = makeArgument("tim", fileId, |
||||||
|
"ext", fileExt); |
||||||
|
return new PostImage.Builder() |
||||||
|
.originalName(String.valueOf(fileId)) |
||||||
|
.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) |
||||||
|
.spoiler(fileSpoiler) |
||||||
|
.size(fileSize) |
||||||
|
.build(); |
||||||
|
} |
||||||
|
return null; |
||||||
|
} |
||||||
|
} |
@ -0,0 +1,205 @@ |
|||||||
|
package org.floens.chan.core.site.sites.vichan; |
||||||
|
|
||||||
|
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.site.Site; |
||||||
|
import org.floens.chan.core.site.SiteAuthentication; |
||||||
|
import org.floens.chan.core.site.SiteIcon; |
||||||
|
import org.floens.chan.core.site.common.CommonSite; |
||||||
|
import org.floens.chan.core.site.common.MultipartHttpCall; |
||||||
|
import org.floens.chan.core.site.common.vichan.VichanApi; |
||||||
|
import org.floens.chan.core.site.http.Reply; |
||||||
|
import org.floens.chan.core.site.http.ReplyResponse; |
||||||
|
import org.floens.chan.core.site.parser.CommentParser; |
||||||
|
import org.floens.chan.core.site.parser.StyleRule; |
||||||
|
import org.jsoup.Jsoup; |
||||||
|
|
||||||
|
import java.util.Locale; |
||||||
|
import java.util.Map; |
||||||
|
import java.util.regex.Matcher; |
||||||
|
import java.util.regex.Pattern; |
||||||
|
|
||||||
|
import okhttp3.HttpUrl; |
||||||
|
import okhttp3.Response; |
||||||
|
|
||||||
|
import static android.text.TextUtils.isEmpty; |
||||||
|
|
||||||
|
public class ViChan extends CommonSite { |
||||||
|
public static final CommonSiteUrlHandler URL_HANDLER = new CommonSiteUrlHandler() { |
||||||
|
@Override |
||||||
|
public Class<? extends Site> getSiteClass() { |
||||||
|
return ViChan.class; |
||||||
|
} |
||||||
|
|
||||||
|
@Override |
||||||
|
public HttpUrl getUrl() { |
||||||
|
return HttpUrl.parse("https://8ch.net/"); |
||||||
|
} |
||||||
|
|
||||||
|
@Override |
||||||
|
public String[] getNames() { |
||||||
|
return new String[]{"8chan", "8ch"}; |
||||||
|
} |
||||||
|
|
||||||
|
@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(); |
||||||
|
} |
||||||
|
} |
||||||
|
}; |
||||||
|
|
||||||
|
@Override |
||||||
|
public void setup() { |
||||||
|
setName("8chan"); |
||||||
|
setIcon(SiteIcon.fromAssets("icons/8chan.png")); |
||||||
|
setBoardsType(BoardsType.INFINITE); |
||||||
|
|
||||||
|
setResolvable(URL_HANDLER); |
||||||
|
|
||||||
|
setConfig(new CommonConfig() { |
||||||
|
@Override |
||||||
|
public boolean feature(Feature feature) { |
||||||
|
return feature == Feature.POSTING; |
||||||
|
} |
||||||
|
}); |
||||||
|
|
||||||
|
setEndpoints(new CommonEndpoints() { |
||||||
|
private final SimpleHttpUrl root = from("https://8ch.net"); |
||||||
|
private final SimpleHttpUrl sys = from("https://sys.8ch.net"); |
||||||
|
|
||||||
|
@Override |
||||||
|
public HttpUrl catalog(Board board) { |
||||||
|
return root.builder().s(board.code).s("catalog.json").url(); |
||||||
|
} |
||||||
|
|
||||||
|
@Override |
||||||
|
public HttpUrl thread(Board board, Loadable loadable) { |
||||||
|
return root.builder().s(board.code).s("res").s(loadable.no + ".json").url(); |
||||||
|
} |
||||||
|
|
||||||
|
@Override |
||||||
|
public HttpUrl imageUrl(Post.Builder post, Map<String, String> arg) { |
||||||
|
return root.builder().s("file_store").s(arg.get("tim") + "." + arg.get("ext")).url(); |
||||||
|
} |
||||||
|
|
||||||
|
@Override |
||||||
|
public HttpUrl thumbnailUrl(Post.Builder post, boolean spoiler, Map<String, String> arg) { |
||||||
|
String ext; |
||||||
|
switch (arg.get("ext")) { |
||||||
|
case "jpeg": |
||||||
|
case "jpg": |
||||||
|
case "png": |
||||||
|
case "gif": |
||||||
|
ext = arg.get("ext"); |
||||||
|
break; |
||||||
|
default: |
||||||
|
ext = "jpg"; |
||||||
|
break; |
||||||
|
} |
||||||
|
|
||||||
|
return root.builder().s("file_store").s("thumb").s(arg.get("tim") + "." + ext).url(); |
||||||
|
} |
||||||
|
|
||||||
|
@Override |
||||||
|
public HttpUrl icon(Post.Builder post, String icon, Map<String, String> arg) { |
||||||
|
SimpleHttpUrl stat = root.builder().s("static"); |
||||||
|
|
||||||
|
if (icon.equals("country")) { |
||||||
|
stat.s("flags").s(arg.get("country_code").toLowerCase(Locale.ENGLISH) + ".png"); |
||||||
|
} |
||||||
|
|
||||||
|
return stat.url(); |
||||||
|
} |
||||||
|
|
||||||
|
@Override |
||||||
|
public HttpUrl reply(Loadable loadable) { |
||||||
|
return sys.builder().s("post.php").url(); |
||||||
|
} |
||||||
|
}); |
||||||
|
|
||||||
|
setActions(new CommonActions() { |
||||||
|
@Override |
||||||
|
public void setupPost(Reply reply, MultipartHttpCall call) { |
||||||
|
call.parameter("board", reply.loadable.board.code); |
||||||
|
|
||||||
|
if (reply.loadable.isThreadMode()) { |
||||||
|
call.parameter("post", "New Reply"); |
||||||
|
call.parameter("thread", String.valueOf(reply.loadable.no)); |
||||||
|
} else { |
||||||
|
call.parameter("post", "New Thread"); |
||||||
|
call.parameter("page", "1"); |
||||||
|
} |
||||||
|
|
||||||
|
call.parameter("pwd", reply.password); |
||||||
|
call.parameter("name", reply.name); |
||||||
|
call.parameter("email", reply.options); |
||||||
|
|
||||||
|
if (!reply.loadable.isThreadMode() && !isEmpty(reply.subject)) { |
||||||
|
call.parameter("subject", reply.subject); |
||||||
|
} |
||||||
|
|
||||||
|
call.parameter("body", reply.comment); |
||||||
|
|
||||||
|
if (reply.file != null) { |
||||||
|
call.fileParameter("file", reply.fileName, reply.file); |
||||||
|
} |
||||||
|
|
||||||
|
if (reply.spoilerImage) { |
||||||
|
call.parameter("spoiler", "on"); |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
@Override |
||||||
|
public void handlePost(ReplyResponse replyResponse, Response response, String result) { |
||||||
|
Matcher auth = Pattern.compile(".*\"captcha\": ?true.*").matcher(result); |
||||||
|
Matcher err = Pattern.compile(".*<h1>Error</h1>.*<h2[^>]*>(.*?)</h2>.*").matcher(result); |
||||||
|
if (auth.find()) { |
||||||
|
replyResponse.requireAuthentication = true; |
||||||
|
replyResponse.errorMessage = result; |
||||||
|
} else if (err.find()) { |
||||||
|
replyResponse.errorMessage = Jsoup.parse(err.group(1)).body().text(); |
||||||
|
} else { |
||||||
|
HttpUrl url = response.request().url(); |
||||||
|
Matcher m = Pattern.compile("/\\w+/\\w+/(\\d+).html").matcher(url.encodedPath()); |
||||||
|
try { |
||||||
|
if (m.find()) { |
||||||
|
replyResponse.threadNo = Integer.parseInt(m.group(1)); |
||||||
|
replyResponse.postNo = Integer.parseInt(url.encodedFragment()); |
||||||
|
replyResponse.posted = true; |
||||||
|
} |
||||||
|
} catch (NumberFormatException ignored) { |
||||||
|
replyResponse.errorMessage = "Error posting: could not find posted thread."; |
||||||
|
} |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
@Override |
||||||
|
public SiteAuthentication postAuthenticate() { |
||||||
|
return SiteAuthentication.fromUrl("https://8ch.net/dnsbls_bypass.php", |
||||||
|
"You failed the CAPTCHA", |
||||||
|
"You may now go back and make your post"); |
||||||
|
} |
||||||
|
}); |
||||||
|
|
||||||
|
setApi(new VichanApi(this)); |
||||||
|
|
||||||
|
CommentParser commentParser = new CommentParser(); |
||||||
|
commentParser.addDefaultRules(); |
||||||
|
commentParser.setQuotePattern(Pattern.compile(".*#(\\d+)")); |
||||||
|
commentParser.setFullQuotePattern(Pattern.compile("/(\\w+)/\\w+/(\\d+)\\.html#(\\d+)")); |
||||||
|
commentParser.rule(StyleRule.tagRule("p").cssClass("quote").color(StyleRule.Color.INLINE_QUOTE).linkify()); |
||||||
|
|
||||||
|
setParser(commentParser); |
||||||
|
} |
||||||
|
} |
@ -0,0 +1,204 @@ |
|||||||
|
package org.floens.chan.core.site.sites.lainchan; |
||||||
|
|
||||||
|
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.site.Site; |
||||||
|
import org.floens.chan.core.site.SiteAuthentication; |
||||||
|
import org.floens.chan.core.site.SiteIcon; |
||||||
|
import org.floens.chan.core.site.common.CommonSite; |
||||||
|
import org.floens.chan.core.site.common.MultipartHttpCall; |
||||||
|
import org.floens.chan.core.site.common.vichan.VichanAntispam; |
||||||
|
import org.floens.chan.core.site.common.vichan.VichanApi; |
||||||
|
import org.floens.chan.core.site.http.Reply; |
||||||
|
import org.floens.chan.core.site.http.ReplyResponse; |
||||||
|
import org.floens.chan.core.site.parser.CommentParser; |
||||||
|
import org.floens.chan.core.site.parser.StyleRule; |
||||||
|
import org.jsoup.Jsoup; |
||||||
|
|
||||||
|
import java.util.Locale; |
||||||
|
import java.util.Map; |
||||||
|
import java.util.regex.Matcher; |
||||||
|
import java.util.regex.Pattern; |
||||||
|
|
||||||
|
import okhttp3.HttpUrl; |
||||||
|
import okhttp3.Response; |
||||||
|
|
||||||
|
import static android.text.TextUtils.isEmpty; |
||||||
|
|
||||||
|
public class Lainchan extends CommonSite { |
||||||
|
public static final CommonSiteUrlHandler URL_HANDLER = new CommonSiteUrlHandler() { |
||||||
|
@Override |
||||||
|
public Class<? extends Site> getSiteClass() { |
||||||
|
return Lainchan.class; |
||||||
|
} |
||||||
|
|
||||||
|
@Override |
||||||
|
public HttpUrl getUrl() { |
||||||
|
return HttpUrl.parse("https://lainchan.org/"); |
||||||
|
} |
||||||
|
|
||||||
|
@Override |
||||||
|
public String[] getNames() { |
||||||
|
return new String[]{"lainchan"}; |
||||||
|
} |
||||||
|
|
||||||
|
@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(); |
||||||
|
} |
||||||
|
} |
||||||
|
}; |
||||||
|
|
||||||
|
@Override |
||||||
|
public void setup() { |
||||||
|
setName("Lainchan"); |
||||||
|
setIcon(SiteIcon.fromFavicon(HttpUrl.parse("https://lainchan.org/favicon.ico"))); |
||||||
|
setBoardsType(BoardsType.INFINITE); |
||||||
|
|
||||||
|
setResolvable(URL_HANDLER); |
||||||
|
|
||||||
|
setConfig(new CommonConfig() { |
||||||
|
@Override |
||||||
|
public boolean feature(Feature feature) { |
||||||
|
return feature == Feature.POSTING; |
||||||
|
} |
||||||
|
}); |
||||||
|
|
||||||
|
setEndpoints(new CommonEndpoints() { |
||||||
|
private final SimpleHttpUrl root = from("https://lainchan.org"); |
||||||
|
private final SimpleHttpUrl sys = from("https://lainchan.org"); |
||||||
|
|
||||||
|
@Override |
||||||
|
public HttpUrl catalog(Board board) { |
||||||
|
return root.builder().s(board.code).s("catalog.json").url(); |
||||||
|
} |
||||||
|
|
||||||
|
@Override |
||||||
|
public HttpUrl thread(Board board, Loadable loadable) { |
||||||
|
return root.builder().s(board.code).s("res").s(loadable.no + ".json").url(); |
||||||
|
} |
||||||
|
|
||||||
|
@Override |
||||||
|
public HttpUrl thumbnailUrl(Post.Builder post, boolean spoiler, Map<String, String> arg) { |
||||||
|
return root.builder().s(post.board.code).s("thumb").s(arg.get("tim") + ".png").url(); |
||||||
|
} |
||||||
|
|
||||||
|
@Override |
||||||
|
public HttpUrl imageUrl(Post.Builder post, Map<String, String> arg) { |
||||||
|
return root.builder().s(post.board.code).s("src").s(arg.get("tim") + "." + arg.get("ext")).url(); |
||||||
|
} |
||||||
|
|
||||||
|
@Override |
||||||
|
public HttpUrl icon(Post.Builder post, String icon, Map<String, String> arg) { |
||||||
|
SimpleHttpUrl stat = root.builder().s("static"); |
||||||
|
|
||||||
|
if (icon.equals("country")) { |
||||||
|
stat.s("flags").s(arg.get("country_code").toLowerCase(Locale.ENGLISH) + ".png"); |
||||||
|
} |
||||||
|
|
||||||
|
return stat.url(); |
||||||
|
} |
||||||
|
|
||||||
|
@Override |
||||||
|
public HttpUrl reply(Loadable loadable) { |
||||||
|
return sys.builder().s("post.php").url(); |
||||||
|
} |
||||||
|
}); |
||||||
|
|
||||||
|
setActions(new CommonActions() { |
||||||
|
@Override |
||||||
|
public void setupPost(Reply reply, MultipartHttpCall call) { |
||||||
|
call.parameter("board", reply.loadable.board.code); |
||||||
|
|
||||||
|
if (reply.loadable.isThreadMode()) { |
||||||
|
call.parameter("thread", String.valueOf(reply.loadable.no)); |
||||||
|
} else { |
||||||
|
// call.parameter("page", "1");
|
||||||
|
} |
||||||
|
|
||||||
|
call.parameter("password", reply.password); |
||||||
|
call.parameter("name", reply.name); |
||||||
|
call.parameter("email", reply.options); |
||||||
|
|
||||||
|
if (!reply.loadable.isThreadMode() && !isEmpty(reply.subject)) { |
||||||
|
call.parameter("subject", reply.subject); |
||||||
|
} |
||||||
|
|
||||||
|
call.parameter("body", reply.comment); |
||||||
|
|
||||||
|
if (reply.file != null) { |
||||||
|
call.fileParameter("file", reply.fileName, reply.file); |
||||||
|
} |
||||||
|
|
||||||
|
if (reply.spoilerImage) { |
||||||
|
call.parameter("spoiler", "on"); |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
@Override |
||||||
|
public boolean requirePrepare() { |
||||||
|
return true; |
||||||
|
} |
||||||
|
|
||||||
|
@Override |
||||||
|
public void prepare(MultipartHttpCall call, Reply reply, ReplyResponse replyResponse) { |
||||||
|
VichanAntispam antispam = new VichanAntispam( |
||||||
|
HttpUrl.parse(resolvable().desktopUrl(reply.loadable, null))); |
||||||
|
antispam.addDefaultIgnoreFields(); |
||||||
|
for (Map.Entry<String, String> e : antispam.get().entrySet()) { |
||||||
|
call.parameter(e.getKey(), e.getValue()); |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
@Override |
||||||
|
public void handlePost(ReplyResponse replyResponse, Response response, String result) { |
||||||
|
Matcher auth = Pattern.compile(".*\"captcha\": ?true.*").matcher(result); |
||||||
|
Matcher err = Pattern.compile(".*<h1[^>]*>Error</h1>.*<h2[^>]*>(.*?)</h2>.*").matcher(result); |
||||||
|
if (auth.find()) { |
||||||
|
replyResponse.requireAuthentication = true; |
||||||
|
replyResponse.errorMessage = result; |
||||||
|
} else if (err.find()) { |
||||||
|
replyResponse.errorMessage = Jsoup.parse(err.group(1)).body().text(); |
||||||
|
} else { |
||||||
|
HttpUrl url = response.request().url(); |
||||||
|
Matcher m = Pattern.compile("/\\w+/\\w+/(\\d+).html").matcher(url.encodedPath()); |
||||||
|
try { |
||||||
|
if (m.find()) { |
||||||
|
replyResponse.threadNo = Integer.parseInt(m.group(1)); |
||||||
|
replyResponse.postNo = Integer.parseInt(url.encodedFragment()); |
||||||
|
replyResponse.posted = true; |
||||||
|
} |
||||||
|
} catch (NumberFormatException ignored) { |
||||||
|
replyResponse.errorMessage = "Error posting: could not find posted thread."; |
||||||
|
} |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
@Override |
||||||
|
public SiteAuthentication postAuthenticate() { |
||||||
|
return SiteAuthentication.fromNone(); |
||||||
|
} |
||||||
|
}); |
||||||
|
|
||||||
|
setApi(new VichanApi(this)); |
||||||
|
|
||||||
|
CommentParser commentParser = new CommentParser(); |
||||||
|
commentParser.addDefaultRules(); |
||||||
|
commentParser.setQuotePattern(Pattern.compile(".*#(\\d+)")); |
||||||
|
commentParser.setFullQuotePattern(Pattern.compile("/(\\w+)/\\w+/(\\d+)\\.html#(\\d+)")); |
||||||
|
commentParser.rule(StyleRule.tagRule("p").cssClass("quote").color(StyleRule.Color.INLINE_QUOTE).linkify()); |
||||||
|
|
||||||
|
setParser(commentParser); |
||||||
|
} |
||||||
|
} |
@ -1,541 +0,0 @@ |
|||||||
package org.floens.chan.core.site.sites.vichan; |
|
||||||
|
|
||||||
import android.support.annotation.Nullable; |
|
||||||
import android.util.JsonReader; |
|
||||||
|
|
||||||
import org.floens.chan.core.model.Post; |
|
||||||
import org.floens.chan.core.model.PostHttpIcon; |
|
||||||
import org.floens.chan.core.model.PostImage; |
|
||||||
import org.floens.chan.core.model.orm.Board; |
|
||||||
import org.floens.chan.core.model.orm.Loadable; |
|
||||||
import org.floens.chan.core.site.Site; |
|
||||||
import org.floens.chan.core.site.SiteAuthentication; |
|
||||||
import org.floens.chan.core.site.SiteEndpoints; |
|
||||||
import org.floens.chan.core.site.SiteIcon; |
|
||||||
import org.floens.chan.core.site.common.CommonSite; |
|
||||||
import org.floens.chan.core.site.common.MultipartHttpCall; |
|
||||||
import org.floens.chan.core.site.http.Reply; |
|
||||||
import org.floens.chan.core.site.http.ReplyResponse; |
|
||||||
import org.floens.chan.core.site.parser.ChanReaderProcessingQueue; |
|
||||||
import org.floens.chan.core.site.parser.CommentParser; |
|
||||||
import org.floens.chan.core.site.parser.StyleRule; |
|
||||||
import org.jsoup.Jsoup; |
|
||||||
import org.jsoup.parser.Parser; |
|
||||||
|
|
||||||
import java.io.IOException; |
|
||||||
import java.util.ArrayList; |
|
||||||
import java.util.List; |
|
||||||
import java.util.Locale; |
|
||||||
import java.util.Map; |
|
||||||
import java.util.regex.Matcher; |
|
||||||
import java.util.regex.Pattern; |
|
||||||
|
|
||||||
import okhttp3.HttpUrl; |
|
||||||
import okhttp3.Response; |
|
||||||
|
|
||||||
import static android.text.TextUtils.isEmpty; |
|
||||||
import static org.floens.chan.core.site.SiteEndpoints.makeArgument; |
|
||||||
|
|
||||||
public class ViChan extends CommonSite { |
|
||||||
public static final CommonSiteUrlHandler RESOLVABLE = new CommonSiteUrlHandler() { |
|
||||||
@Override |
|
||||||
public Class<? extends Site> getSiteClass() { |
|
||||||
return ViChan.class; |
|
||||||
} |
|
||||||
|
|
||||||
@Override |
|
||||||
public boolean matchesName(String value) { |
|
||||||
return value.equals("8chan") || value.equals("8ch"); |
|
||||||
} |
|
||||||
|
|
||||||
@Override |
|
||||||
public boolean respondsTo(HttpUrl url) { |
|
||||||
return url.host().equals("8ch.net"); |
|
||||||
} |
|
||||||
|
|
||||||
@Override |
|
||||||
public Loadable resolveLoadable(Site site, HttpUrl url) { |
|
||||||
Matcher board = Pattern.compile("/(\\w+)") |
|
||||||
.matcher(url.encodedPath()); |
|
||||||
Matcher thread = Pattern.compile("/(\\w+)/res/(\\d+).html") |
|
||||||
.matcher(url.encodedPath()); |
|
||||||
|
|
||||||
try { |
|
||||||
if (thread.find()) { |
|
||||||
Board b = site.board(thread.group(1)); |
|
||||||
if (b == null) { |
|
||||||
return null; |
|
||||||
} |
|
||||||
Loadable l = Loadable.forThread(site, b, Integer.parseInt(thread.group(3))); |
|
||||||
|
|
||||||
if (isEmpty(url.fragment())) { |
|
||||||
l.markedNo = Integer.parseInt(url.fragment()); |
|
||||||
} |
|
||||||
|
|
||||||
return l; |
|
||||||
} else if (board.find()) { |
|
||||||
Board b = site.board(board.group(1)); |
|
||||||
if (b == null) { |
|
||||||
return null; |
|
||||||
} |
|
||||||
|
|
||||||
return Loadable.forCatalog(b); |
|
||||||
} |
|
||||||
} catch (NumberFormatException ignored) { |
|
||||||
} |
|
||||||
|
|
||||||
return null; |
|
||||||
} |
|
||||||
|
|
||||||
@Override |
|
||||||
public String desktopUrl(Loadable loadable, @Nullable Post post) { |
|
||||||
if (loadable.isCatalogMode()) { |
|
||||||
return "https://8ch.net/" + loadable.boardCode; |
|
||||||
} else if (loadable.isThreadMode()) { |
|
||||||
return "https://8ch.net/" + loadable.boardCode + "/res/" + loadable.no + ".html"; |
|
||||||
} else { |
|
||||||
return "https://8ch.net/"; |
|
||||||
} |
|
||||||
} |
|
||||||
}; |
|
||||||
|
|
||||||
private static final String TAG = "ViChan"; |
|
||||||
|
|
||||||
@Override |
|
||||||
public void setup() { |
|
||||||
setName("8chan"); |
|
||||||
setIcon(SiteIcon.fromAssets("icons/8chan.png")); |
|
||||||
setBoardsType(BoardsType.INFINITE); |
|
||||||
|
|
||||||
setResolvable(RESOLVABLE); |
|
||||||
|
|
||||||
setConfig(new CommonConfig() { |
|
||||||
@Override |
|
||||||
public boolean feature(Feature feature) { |
|
||||||
return feature == Feature.POSTING; |
|
||||||
} |
|
||||||
}); |
|
||||||
|
|
||||||
setEndpoints(new CommonEndpoints() { |
|
||||||
private final SimpleHttpUrl root = from("https://8ch.net"); |
|
||||||
private final SimpleHttpUrl sys = from("https://sys.8ch.net"); |
|
||||||
|
|
||||||
@Override |
|
||||||
public HttpUrl catalog(Board board) { |
|
||||||
return root.builder().s(board.code).s("catalog.json").url(); |
|
||||||
} |
|
||||||
|
|
||||||
@Override |
|
||||||
public HttpUrl thread(Board board, Loadable loadable) { |
|
||||||
return root.builder().s(board.code).s("res").s(loadable.no + ".json").url(); |
|
||||||
} |
|
||||||
|
|
||||||
@Override |
|
||||||
public HttpUrl imageUrl(Post.Builder post, Map<String, String> arg) { |
|
||||||
return root.builder().s("file_store").s(arg.get("tim") + "." + arg.get("ext")).url(); |
|
||||||
} |
|
||||||
|
|
||||||
@Override |
|
||||||
public HttpUrl thumbnailUrl(Post.Builder post, boolean spoiler, Map<String, String> arg) { |
|
||||||
String ext; |
|
||||||
switch (arg.get("ext")) { |
|
||||||
case "jpeg": |
|
||||||
case "jpg": |
|
||||||
case "png": |
|
||||||
case "gif": |
|
||||||
ext = arg.get("ext"); |
|
||||||
break; |
|
||||||
default: |
|
||||||
ext = "jpg"; |
|
||||||
break; |
|
||||||
} |
|
||||||
|
|
||||||
return root.builder().s("file_store").s("thumb").s(arg.get("tim") + "." + ext).url(); |
|
||||||
} |
|
||||||
|
|
||||||
@Override |
|
||||||
public HttpUrl icon(Post.Builder post, String icon, Map<String, String> arg) { |
|
||||||
SimpleHttpUrl stat = root.builder().s("static"); |
|
||||||
|
|
||||||
if (icon.equals("country")) { |
|
||||||
stat.s("flags").s(arg.get("country_code").toLowerCase(Locale.ENGLISH) + ".png"); |
|
||||||
} |
|
||||||
|
|
||||||
return stat.url(); |
|
||||||
} |
|
||||||
|
|
||||||
@Override |
|
||||||
public HttpUrl reply(Loadable loadable) { |
|
||||||
return sys.builder().s("post.php").url(); |
|
||||||
} |
|
||||||
}); |
|
||||||
|
|
||||||
setActions(new CommonActions() { |
|
||||||
@Override |
|
||||||
public void setupPost(Reply reply, MultipartHttpCall call) { |
|
||||||
call.parameter("board", reply.loadable.board.code); |
|
||||||
|
|
||||||
if (reply.loadable.isThreadMode()) { |
|
||||||
call.parameter("post", "New Reply"); |
|
||||||
call.parameter("thread", String.valueOf(reply.loadable.no)); |
|
||||||
} else { |
|
||||||
call.parameter("post", "New Thread"); |
|
||||||
call.parameter("page", "1"); |
|
||||||
} |
|
||||||
|
|
||||||
call.parameter("pwd", reply.password); |
|
||||||
call.parameter("name", reply.name); |
|
||||||
call.parameter("email", reply.options); |
|
||||||
|
|
||||||
if (!reply.loadable.isThreadMode() && !isEmpty(reply.subject)) { |
|
||||||
call.parameter("subject", reply.subject); |
|
||||||
} |
|
||||||
|
|
||||||
call.parameter("body", reply.comment); |
|
||||||
|
|
||||||
if (reply.file != null) { |
|
||||||
call.fileParameter("file", reply.fileName, reply.file); |
|
||||||
} |
|
||||||
|
|
||||||
if (reply.spoilerImage) { |
|
||||||
call.parameter("spoiler", "on"); |
|
||||||
} |
|
||||||
} |
|
||||||
|
|
||||||
@Override |
|
||||||
public void handlePost(ReplyResponse replyResponse, Response response, String result) { |
|
||||||
Matcher auth = Pattern.compile(".*\"captcha\": ?true.*").matcher(result); |
|
||||||
Matcher err = Pattern.compile(".*<h1>Error</h1>.*<h2[^>]*>(.*?)</h2>.*").matcher(result); |
|
||||||
if (auth.find()) { |
|
||||||
replyResponse.requireAuthentication = true; |
|
||||||
replyResponse.errorMessage = result; |
|
||||||
} else if (err.find()) { |
|
||||||
replyResponse.errorMessage = Jsoup.parse(err.group(1)).body().text(); |
|
||||||
} else { |
|
||||||
HttpUrl url = response.request().url(); |
|
||||||
Matcher m = Pattern.compile("/\\w+/\\w+/(\\d+).html").matcher(url.encodedPath()); |
|
||||||
try { |
|
||||||
if (m.find()) { |
|
||||||
replyResponse.threadNo = Integer.parseInt(m.group(1)); |
|
||||||
replyResponse.postNo = Integer.parseInt(url.encodedFragment()); |
|
||||||
replyResponse.posted = true; |
|
||||||
} |
|
||||||
} catch (NumberFormatException ignored) { |
|
||||||
replyResponse.errorMessage = "Error posting: could not find posted thread."; |
|
||||||
} |
|
||||||
} |
|
||||||
} |
|
||||||
|
|
||||||
@Override |
|
||||||
public SiteAuthentication postAuthenticate() { |
|
||||||
return SiteAuthentication.fromUrl("https://8ch.net/dnsbls_bypass.php", |
|
||||||
"You failed the CAPTCHA", |
|
||||||
"You may now go back and make your post"); |
|
||||||
} |
|
||||||
}); |
|
||||||
|
|
||||||
setApi(new ViChanApi()); |
|
||||||
|
|
||||||
CommentParser commentParser = new CommentParser(); |
|
||||||
commentParser.addDefaultRules(); |
|
||||||
commentParser.setQuotePattern(Pattern.compile(".*#(\\d+)")); |
|
||||||
commentParser.setFullQuotePattern(Pattern.compile("/(\\w+)/\\w+/(\\d+)\\.html#(\\d+)")); |
|
||||||
commentParser.rule(StyleRule.tagRule("p").cssClass("quote").color(StyleRule.Color.INLINE_QUOTE).linkify()); |
|
||||||
|
|
||||||
setParser(commentParser); |
|
||||||
} |
|
||||||
|
|
||||||
private class ViChanApi extends CommonApi { |
|
||||||
@Override |
|
||||||
public void loadThread(JsonReader reader, ChanReaderProcessingQueue queue) throws Exception { |
|
||||||
reader.beginObject(); |
|
||||||
// Page object
|
|
||||||
while (reader.hasNext()) { |
|
||||||
String key = reader.nextName(); |
|
||||||
if (key.equals("posts")) { |
|
||||||
reader.beginArray(); |
|
||||||
// Thread array
|
|
||||||
while (reader.hasNext()) { |
|
||||||
// Thread object
|
|
||||||
readPostObject(reader, queue); |
|
||||||
} |
|
||||||
reader.endArray(); |
|
||||||
} else { |
|
||||||
reader.skipValue(); |
|
||||||
} |
|
||||||
} |
|
||||||
reader.endObject(); |
|
||||||
} |
|
||||||
|
|
||||||
@Override |
|
||||||
public void loadCatalog(JsonReader reader, ChanReaderProcessingQueue queue) throws Exception { |
|
||||||
reader.beginArray(); // Array of pages
|
|
||||||
|
|
||||||
while (reader.hasNext()) { |
|
||||||
reader.beginObject(); // Page 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(); |
|
||||||
} |
|
||||||
|
|
||||||
reader.endArray(); |
|
||||||
} |
|
||||||
|
|
||||||
@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(); |
|
||||||
|
|
||||||
// File
|
|
||||||
String fileId = null; |
|
||||||
String fileExt = null; |
|
||||||
int fileWidth = 0; |
|
||||||
int fileHeight = 0; |
|
||||||
long fileSize = 0; |
|
||||||
boolean fileSpoiler = false; |
|
||||||
String fileName = null; |
|
||||||
|
|
||||||
List<PostImage> files = new ArrayList<>(); |
|
||||||
|
|
||||||
// Country flag
|
|
||||||
String countryCode = null; |
|
||||||
String trollCountryCode = null; |
|
||||||
String countryName = null; |
|
||||||
|
|
||||||
reader.beginObject(); |
|
||||||
while (reader.hasNext()) { |
|
||||||
String key = reader.nextName(); |
|
||||||
|
|
||||||
switch (key) { |
|
||||||
case "no": |
|
||||||
builder.id(reader.nextInt()); |
|
||||||
break; |
|
||||||
case "sub": |
|
||||||
builder.subject(reader.nextString()); |
|
||||||
break; |
|
||||||
case "name": |
|
||||||
builder.name(reader.nextString()); |
|
||||||
break; |
|
||||||
case "com": |
|
||||||
builder.comment(reader.nextString()); |
|
||||||
break; |
|
||||||
case "tim": |
|
||||||
fileId = reader.nextString(); |
|
||||||
break; |
|
||||||
case "time": |
|
||||||
builder.setUnixTimestampSeconds(reader.nextLong()); |
|
||||||
break; |
|
||||||
case "ext": |
|
||||||
fileExt = reader.nextString().replace(".", ""); |
|
||||||
break; |
|
||||||
case "w": |
|
||||||
fileWidth = reader.nextInt(); |
|
||||||
break; |
|
||||||
case "h": |
|
||||||
fileHeight = reader.nextInt(); |
|
||||||
break; |
|
||||||
case "fsize": |
|
||||||
fileSize = reader.nextLong(); |
|
||||||
break; |
|
||||||
case "filename": |
|
||||||
fileName = reader.nextString(); |
|
||||||
break; |
|
||||||
case "trip": |
|
||||||
builder.tripcode(reader.nextString()); |
|
||||||
break; |
|
||||||
case "country": |
|
||||||
countryCode = reader.nextString(); |
|
||||||
break; |
|
||||||
case "troll_country": |
|
||||||
trollCountryCode = reader.nextString(); |
|
||||||
break; |
|
||||||
case "country_name": |
|
||||||
countryName = reader.nextString(); |
|
||||||
break; |
|
||||||
case "spoiler": |
|
||||||
fileSpoiler = reader.nextInt() == 1; |
|
||||||
break; |
|
||||||
case "resto": |
|
||||||
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 "replies": |
|
||||||
builder.replies(reader.nextInt()); |
|
||||||
break; |
|
||||||
case "images": |
|
||||||
builder.images(reader.nextInt()); |
|
||||||
break; |
|
||||||
case "unique_ips": |
|
||||||
builder.uniqueIps(reader.nextInt()); |
|
||||||
break; |
|
||||||
case "id": |
|
||||||
builder.posterId(reader.nextString()); |
|
||||||
break; |
|
||||||
case "capcode": |
|
||||||
builder.moderatorCapcode(reader.nextString()); |
|
||||||
break; |
|
||||||
case "extra_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(); |
|
||||||
|
|
||||||
// The file from between the other values.
|
|
||||||
if (fileId != null && fileName != null && fileExt != null) { |
|
||||||
Map<String, String> args = makeArgument("tim", fileId, |
|
||||||
"ext", fileExt); |
|
||||||
PostImage image = new PostImage.Builder() |
|
||||||
.originalName(String.valueOf(fileId)) |
|
||||||
.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) |
|
||||||
.spoiler(fileSpoiler) |
|
||||||
.size(fileSize) |
|
||||||
.build(); |
|
||||||
// Insert it at the beginning.
|
|
||||||
files.add(0, image); |
|
||||||
} |
|
||||||
|
|
||||||
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); |
|
||||||
queue.setOp(op); |
|
||||||
} |
|
||||||
|
|
||||||
Post cached = queue.getCachedPost(builder.id); |
|
||||||
if (cached != null) { |
|
||||||
// Id is known, use the cached post object.
|
|
||||||
queue.addForReuse(cached); |
|
||||||
return; |
|
||||||
} |
|
||||||
|
|
||||||
if (countryCode != null && countryName != null) { |
|
||||||
HttpUrl countryUrl = endpoints.icon(builder, "country", |
|
||||||
makeArgument("country_code", countryCode)); |
|
||||||
builder.addHttpIcon(new PostHttpIcon(countryUrl, countryName)); |
|
||||||
} |
|
||||||
|
|
||||||
if (trollCountryCode != null && countryName != null) { |
|
||||||
HttpUrl countryUrl = endpoints.icon(builder, "troll_country", |
|
||||||
makeArgument("troll_country_code", trollCountryCode)); |
|
||||||
builder.addHttpIcon(new PostHttpIcon(countryUrl, countryName)); |
|
||||||
} |
|
||||||
|
|
||||||
queue.addForParse(builder); |
|
||||||
} |
|
||||||
|
|
||||||
private PostImage readPostImage(JsonReader reader, Post.Builder builder, |
|
||||||
SiteEndpoints endpoints) throws IOException { |
|
||||||
reader.beginObject(); |
|
||||||
|
|
||||||
String fileId = null; |
|
||||||
long fileSize = 0; |
|
||||||
|
|
||||||
String fileExt = null; |
|
||||||
int fileWidth = 0; |
|
||||||
int fileHeight = 0; |
|
||||||
boolean fileSpoiler = false; |
|
||||||
String fileName = null; |
|
||||||
|
|
||||||
while (reader.hasNext()) { |
|
||||||
switch (reader.nextName()) { |
|
||||||
case "tim": |
|
||||||
fileId = reader.nextString(); |
|
||||||
break; |
|
||||||
case "fsize": |
|
||||||
fileSize = reader.nextLong(); |
|
||||||
break; |
|
||||||
case "w": |
|
||||||
fileWidth = reader.nextInt(); |
|
||||||
break; |
|
||||||
case "h": |
|
||||||
fileHeight = reader.nextInt(); |
|
||||||
break; |
|
||||||
case "spoiler": |
|
||||||
fileSpoiler = reader.nextInt() == 1; |
|
||||||
break; |
|
||||||
case "ext": |
|
||||||
fileExt = reader.nextString().replace(".", ""); |
|
||||||
break; |
|
||||||
case "filename": |
|
||||||
fileName = reader.nextString(); |
|
||||||
break; |
|
||||||
default: |
|
||||||
reader.skipValue(); |
|
||||||
break; |
|
||||||
} |
|
||||||
} |
|
||||||
|
|
||||||
reader.endObject(); |
|
||||||
|
|
||||||
if (fileId != null && fileName != null && fileExt != null) { |
|
||||||
Map<String, String> args = makeArgument("tim", fileId, |
|
||||||
"ext", fileExt); |
|
||||||
return new PostImage.Builder() |
|
||||||
.originalName(String.valueOf(fileId)) |
|
||||||
.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) |
|
||||||
.spoiler(fileSpoiler) |
|
||||||
.size(fileSize) |
|
||||||
.build(); |
|
||||||
} |
|
||||||
return null; |
|
||||||
} |
|
||||||
} |
|
||||||
} |
|
Loading…
Reference in new issue