/* * Overchan Android (Meta Imageboard Client) * Copyright (C) 2014-2016 miku-nyan <https://github.com/miku-nyan> * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see <http://www.gnu.org/licenses/>. */ package nya.miku.wishmaster.chans.inach; import java.io.ByteArrayOutputStream; import java.io.InputStream; import java.util.ArrayList; import java.util.Arrays; import java.util.Comparator; import java.util.List; import java.util.Locale; import cz.msebera.android.httpclient.Header; import cz.msebera.android.httpclient.HttpHeaders; import cz.msebera.android.httpclient.NameValuePair; import cz.msebera.android.httpclient.client.entity.UrlEncodedFormEntity; import cz.msebera.android.httpclient.message.BasicNameValuePair; import android.content.SharedPreferences; import android.content.res.Resources; import android.graphics.drawable.Drawable; import android.preference.PreferenceGroup; import android.support.v4.content.res.ResourcesCompat; import nya.miku.wishmaster.R; import nya.miku.wishmaster.api.AbstractWakabaModule; import nya.miku.wishmaster.api.interfaces.CancellableTask; import nya.miku.wishmaster.api.interfaces.ProgressListener; import nya.miku.wishmaster.api.models.AttachmentModel; import nya.miku.wishmaster.api.models.BoardModel; import nya.miku.wishmaster.api.models.CaptchaModel; import nya.miku.wishmaster.api.models.DeletePostModel; import nya.miku.wishmaster.api.models.PostModel; import nya.miku.wishmaster.api.models.SendPostModel; import nya.miku.wishmaster.api.models.SimpleBoardModel; import nya.miku.wishmaster.api.models.UrlPageModel; import nya.miku.wishmaster.api.util.ChanModels; import nya.miku.wishmaster.api.util.WakabaReader; import nya.miku.wishmaster.common.IOUtils; import nya.miku.wishmaster.common.Logger; import nya.miku.wishmaster.http.ExtendedMultipartBuilder; import nya.miku.wishmaster.http.streamer.HttpRequestModel; import nya.miku.wishmaster.http.streamer.HttpResponseModel; import nya.miku.wishmaster.http.streamer.HttpStreamer; import nya.miku.wishmaster.lib.org_json.JSONArray; import nya.miku.wishmaster.lib.org_json.JSONObject; public class InachModule extends AbstractWakabaModule { private static final String TAG = "InachReader"; private static final String CHAN_NAME = "inach.org"; private static final String DOMAIN_NAME = "inach.org"; private static final String PREF_AJAX_UPDATE = "PREF_AJAX_UPDATE"; private static final SimpleBoardModel[] BOARDS; private static final String[] ATTACHMENT_FORMATS = new String[] { "gif", "jpg", "png", "pdf", "odf", "zip", "rar", "tar", "bz2", "7z", "doc", "odt", "mp3", "mp4", "mpeg", "flv", "swf", "avi" }; static { BOARDS = new SimpleBoardModel[] { ChanModels.obtainSimpleBoardModel(CHAN_NAME, "b", "Random", null, true), ChanModels.obtainSimpleBoardModel(CHAN_NAME, "2d", "Animation", null, false), ChanModels.obtainSimpleBoardModel(CHAN_NAME, "cu", "Culture", null, false), ChanModels.obtainSimpleBoardModel(CHAN_NAME, "kn", "Knowledge", null, false), ChanModels.obtainSimpleBoardModel(CHAN_NAME, "ad", "Adult", null, true), ChanModels.obtainSimpleBoardModel(CHAN_NAME, "in", "Inach", null, true), ChanModels.obtainSimpleBoardModel(CHAN_NAME, "pl", "Playing Games", null, false), ChanModels.obtainSimpleBoardModel(CHAN_NAME, "int", "Inach", null, true), ChanModels.obtainSimpleBoardModel(CHAN_NAME, "mu", "Music", null, false), ChanModels.obtainSimpleBoardModel(CHAN_NAME, "bl", "Blogs", null, true) }; } private StringBuilder buffer = null; public InachModule(SharedPreferences preferences, Resources resources) { super(preferences, resources); } @Override public String getChanName() { return CHAN_NAME; } @Override public String getDisplayingName() { return "Inach.org"; } @Override public Drawable getChanFavicon() { return ResourcesCompat.getDrawable(resources, R.drawable.favicon_inach, null); } @Override protected String getUsingDomain() { return DOMAIN_NAME; } @Override public void addPreferencesOnScreen(PreferenceGroup preferenceGroup) { addOnlyNewPostsPreference(preferenceGroup, true); super.addPreferencesOnScreen(preferenceGroup); } private boolean useAjax() { return preferences.getBoolean(getSharedKey(PREF_AJAX_UPDATE), true); } @Override protected WakabaReader getWakabaReader(InputStream stream, UrlPageModel urlModel) { return new InachReader(stream); } @Override protected SimpleBoardModel[] getBoardsList() { return BOARDS; } @Override public BoardModel getBoard(String shortName, ProgressListener listener, CancellableTask task) throws Exception { BoardModel board = super.getBoard(shortName, listener, task); board.defaultUserName = "Аноним"; board.timeZoneId = "GMT+3"; board.readonlyBoard = false; board.requiredFileForNewThread = true; board.allowDeletePosts = true; board.allowDeleteFiles = true; board.allowNames = false; board.allowSubjects = true; board.allowSage = true; board.allowEmails = true; board.ignoreEmailIfSage = true; board.allowCustomMark = false; board.allowRandomHash = true; board.allowIcons = false; board.attachmentsMaxCount = 1; board.attachmentsFormatFilters = ATTACHMENT_FORMATS; board.markType = BoardModel.MARK_WAKABAMARK; return board; } @Override public PostModel[] getPostsList(String boardName, String threadNumber, ProgressListener listener, CancellableTask task, PostModel[] oldList) throws Exception { if (useAjax() && oldList != null) { int postsCount = 0; for (PostModel post : oldList) if (!post.deleted) ++postsCount; if (postsCount > 1) { String ajaxUrl = getUsingUrl() + "cached.pl?task=updatethread&board=" + boardName + "&thread=" + threadNumber + "&posts=" + Integer.toString(postsCount - 2); try { JSONObject json = downloadJSONObject(ajaxUrl, true, listener, task); if (json == null) return oldList; JSONArray array = json.getJSONArray("newposts"); if (array.length() == 0) throw new Exception(); PostModel[] newPosts = new PostModel[array.length()]; for (int i=0, len=array.length(); i<len; ++i) newPosts[i] = mapAjaxModel(array.getJSONObject(i).getJSONObject("data"), boardName, threadNumber); Arrays.sort(newPosts, new Comparator<PostModel>() { @Override public int compare(PostModel lhs, PostModel rhs) { return Long.valueOf(Long.parseLong(lhs.number)).compareTo(Long.parseLong(rhs.number)); } }); long lastPostNum = Long.parseLong(oldList[oldList.length-1].number); ArrayList<PostModel> list = new ArrayList<PostModel>(Arrays.asList(oldList)); for (int i=0; i<newPosts.length; ++i) { if (Long.parseLong(newPosts[i].number) > lastPostNum) { list.add(newPosts[i]); } } return list.toArray(new PostModel[list.size()]); } catch (Exception e) { Logger.e(TAG, "no ajax", e); } finally { buffer = null; } } } return super.getPostsList(boardName, threadNumber, listener, task, oldList); } private PostModel mapAjaxModel(JSONObject json, String boardName, String threadNum) { PostModel model = new PostModel(); model.number = Long.toString(json.getLong("num")); model.name = json.optString("name", ""); model.subject = json.optString("subject", ""); model.comment = fixString(json.optString("comment", "")); model.email = json.optString("email", ""); if (model.email.startsWith("mailto:")) model.email = model.email.substring(7); model.trip = ""; model.sage = model.email.toLowerCase(Locale.US).contains("sage"); try { model.timestamp = InachReader.DATE_FORMAT.parse(json.getString("date")).getTime(); } catch (Exception e) { Logger.e(TAG, "cannot parse date; make sure you choose the right DateFormat for this chan", e); } model.parentThread = threadNum; String imagePath = json.optString("image", ""); if (imagePath.length() != 0) { AttachmentModel attachment = new AttachmentModel(); attachment.path = "/" + boardName + "/" + imagePath; attachment.thumbnail = json.optString("thumbnail", null); if (attachment.thumbnail != null) attachment.thumbnail = "/" + boardName + "/" + attachment.thumbnail; try { String size = json.optString("size"); if (size.length() == 0) throw new Exception(); attachment.size = Math.round(Math.round(Integer.parseInt(size) * 100 / 1024.0f) / 100.0f); } catch (Exception e) { attachment.size = -1; } try { String width = json.optString("width", ""); String height = json.optString("height", ""); if (width.length() == 0 || height.length() == 0) throw new Exception(); attachment.width = Integer.parseInt(width); attachment.height = Integer.parseInt(height); } catch (Exception e) { attachment.width = -1; attachment.height = -1; } String pathLower = attachment.path.toLowerCase(Locale.US); if (pathLower.endsWith(".jpg") || pathLower.endsWith(".jpeg") || pathLower.endsWith(".png")) attachment.type = AttachmentModel.TYPE_IMAGE_STATIC; else if (pathLower.endsWith(".gif")) attachment.type = AttachmentModel.TYPE_IMAGE_GIF; else if (pathLower.endsWith(".webm")) attachment.type = AttachmentModel.TYPE_VIDEO; else if (pathLower.endsWith(".mp3") || pathLower.endsWith(".ogg")) attachment.type = AttachmentModel.TYPE_AUDIO; else attachment.type = AttachmentModel.TYPE_OTHER_FILE; model.attachments = new AttachmentModel[] { attachment }; } String youtubeId = json.optString("youtube", ""); if (youtubeId.length() != 0) { AttachmentModel attachment = new AttachmentModel(); attachment.size = -1; attachment.type = AttachmentModel.TYPE_OTHER_NOTFILE; attachment.path = "http://youtube.com/watch?v=" + youtubeId; attachment.thumbnail = "http://img.youtube.com/vi/" + youtubeId + "/default.jpg"; if (model.attachments == null || model.attachments.length == 0) { model.attachments = new AttachmentModel[] { attachment }; } else { model.attachments = new AttachmentModel[] { model.attachments[0], attachment }; } } return model; } private String fixString(String comment) { boolean inTag = false; if (buffer == null) buffer = new StringBuilder(); else buffer.setLength(0); comment = comment.replace("' style='display: table-cell; vertical-align: middle;'", "'"). replace("<span style='display: table-cell; vertical-align: middle;' ", "<span "); for (int i=0; i<comment.length(); ++i) { char ch = comment.charAt(i); switch (ch) { case '<': inTag = true; break; case '>': inTag = false; break; case '\'': if (inTag) { buffer.append('\"'); continue; } } buffer.append(ch); } return buffer.toString(); } @Override public CaptchaModel getNewCaptcha(String boardName, String threadNumber, ProgressListener listener, CancellableTask task) throws Exception { String captchaUrl = getUsingUrl() + "captcha.pl?key=" + (threadNumber == null ? "mainpage" : ("res" + threadNumber)) + "&dummy=" + Long.toString(Math.round(Math.random()*1000000)) + "&update=1"; return downloadCaptcha(captchaUrl, listener, task); } @Override public String sendPost(SendPostModel model, ProgressListener listener, CancellableTask task) throws Exception { String url = getUsingUrl() + model.boardName + "/wakaba.pl"; ExtendedMultipartBuilder postEntityBuilder = ExtendedMultipartBuilder.create().setDelegates(listener, task). addString("task", "post"); if (model.threadNumber != null) postEntityBuilder.addString("parent", model.threadNumber); if (model.sage) postEntityBuilder.addString("fieldsage", "on"); postEntityBuilder. addString("fieldnoko", "on"). addString("field2", model.sage ? "sage" : model.email). addString("field3", model.subject). addString("field4", model.comment). addString("captcha", model.captchaAnswer). addString("password", model.password); if (model.attachments != null && model.attachments.length > 0) postEntityBuilder.addFile("file", model.attachments[0], model.randomHash); HttpRequestModel request = HttpRequestModel.builder().setPOST(postEntityBuilder.build()).setNoRedirect(true).build(); HttpResponseModel response = null; try { response = HttpStreamer.getInstance().getFromUrl(url, request, httpClient, null, task); if (response.statusCode == 303) { for (Header header : response.headers) { if (header != null && HttpHeaders.LOCATION.equalsIgnoreCase(header.getName())) { return fixRelativeUrl(header.getValue()); } } } else if (response.statusCode == 200) { ByteArrayOutputStream output = new ByteArrayOutputStream(1024); IOUtils.copyStream(response.stream, output); String htmlResponse = output.toString("UTF-8"); if (!htmlResponse.contains("<blockquote")) { int start = htmlResponse.indexOf("<h1 style='text-align: center'>"); if (start != -1) { int end = htmlResponse.indexOf("<br><br>", start + 31); if (end != -1) { throw new Exception(htmlResponse.substring(start + 31, end).trim()); } } } } else throw new Exception(response.statusCode + " - " + response.statusReason); } finally { if (response != null) response.release(); } return null; } @Override public String deletePost(DeletePostModel model, ProgressListener listener, CancellableTask task) throws Exception { String url = getUsingUrl() + model.boardName + "/wakaba.pl"; List<NameValuePair> pairs = new ArrayList<NameValuePair>(); pairs.add(new BasicNameValuePair("delete", model.postNumber)); pairs.add(new BasicNameValuePair("parent", model.threadNumber)); pairs.add(new BasicNameValuePair("task", "delete")); if (model.onlyFiles) pairs.add(new BasicNameValuePair("fileonly", "on")); pairs.add(new BasicNameValuePair("password", model.password)); HttpRequestModel request = HttpRequestModel.builder().setPOST(new UrlEncodedFormEntity(pairs, "UTF-8")).setNoRedirect(true).build(); HttpResponseModel response = null; try { response = HttpStreamer.getInstance().getFromUrl(url, request, httpClient, null, task); if (response.statusCode == 200) { ByteArrayOutputStream output = new ByteArrayOutputStream(1024); IOUtils.copyStream(response.stream, output); String htmlResponse = output.toString("UTF-8"); if (!htmlResponse.contains("<blockquote")) { int start = htmlResponse.indexOf("<h1 style='text-align: center'>"); if (start != -1) { int end = htmlResponse.indexOf("<br><br>", start + 31); if (end != -1) { throw new Exception(htmlResponse.substring(start + 31, end).trim()); } } } } } finally { if (response != null) response.release(); } return null; } }