/* * 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.anonfm; import java.io.ByteArrayOutputStream; import java.io.InputStream; import java.text.DateFormat; import java.text.SimpleDateFormat; import java.util.ArrayList; import java.util.Arrays; import java.util.List; import java.util.Locale; import java.util.TimeZone; import java.util.regex.Matcher; import java.util.regex.Pattern; import android.content.SharedPreferences; import android.content.res.Resources; import android.graphics.Bitmap; import android.graphics.BitmapFactory; import android.graphics.drawable.Drawable; import android.preference.PreferenceGroup; import android.support.v4.content.res.ResourcesCompat; 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.BasicHeader; import cz.msebera.android.httpclient.message.BasicNameValuePair; import nya.miku.wishmaster.R; import nya.miku.wishmaster.api.AbstractChanModule; import nya.miku.wishmaster.api.interfaces.CancellableTask; import nya.miku.wishmaster.api.interfaces.ProgressListener; import nya.miku.wishmaster.api.models.BoardModel; import nya.miku.wishmaster.api.models.CaptchaModel; import nya.miku.wishmaster.api.models.PostModel; import nya.miku.wishmaster.api.models.SendPostModel; import nya.miku.wishmaster.api.models.SimpleBoardModel; import nya.miku.wishmaster.api.models.ThreadModel; import nya.miku.wishmaster.api.models.UrlPageModel; import nya.miku.wishmaster.api.util.ChanModels; import nya.miku.wishmaster.api.util.RegexUtils; import nya.miku.wishmaster.api.util.UrlPathUtils; import nya.miku.wishmaster.common.IOUtils; 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; public class AnonFmModule extends AbstractChanModule { private static final String CHAN_NAME = "anon.fm"; private static final String DOMAIN = "anon.fm"; private static final Pattern CID_PATTERN = Pattern.compile("name=\"cid\" value=\"(\\d+)\""); private static final Header[] HTTP_HEADER_FEEDBACK = new Header[] { new BasicHeader(HttpHeaders.REFERER, "http://" + DOMAIN + "/feedback/") }; private static final DateFormat TIMEFORMAT; private static final BoardModel BOARD; static { TIMEFORMAT = new SimpleDateFormat("HH:mm:ss.SSS", Locale.US); TIMEFORMAT.setTimeZone(TimeZone.getTimeZone("GMT")); BOARD = new BoardModel(); BOARD.chan = CHAN_NAME; BOARD.boardName = "anon.fm"; BOARD.boardDescription = "anon.fm"; BOARD.nsfw = false; BOARD.uniqueAttachmentNames = true; BOARD.timeZoneId = "GMT+3"; BOARD.defaultUserName = ""; BOARD.bumpLimit = Integer.MAX_VALUE; BOARD.readonlyBoard = false; BOARD.requiredFileForNewThread = false; BOARD.allowDeletePosts = false; BOARD.allowDeleteFiles = false; BOARD.allowReport = BoardModel.REPORT_NOT_ALLOWED; BOARD.allowNames = false; BOARD.allowSubjects = false; BOARD.allowSage = false; BOARD.allowEmails = false; BOARD.allowCustomMark = false; BOARD.allowRandomHash = false; BOARD.allowIcons = false; BOARD.attachmentsMaxCount = 0; BOARD.markType = BoardModel.MARK_NOMARK; BOARD.firstPage = 0; BOARD.lastPage = 0; BOARD.searchAllowed = false; BOARD.catalogAllowed = false; } private String cid = null; public AnonFmModule(SharedPreferences preferences, Resources resources) { super(preferences, resources); } @Override public String getChanName() { return CHAN_NAME; } @Override public String getDisplayingName() { return "Радио Анонимус (feedback)"; } @Override public Drawable getChanFavicon() { return ResourcesCompat.getDrawable(resources, R.drawable.favicon_anonfm, null); } @Override public void addPreferencesOnScreen(PreferenceGroup preferenceGroup) { addProxyPreferences(preferenceGroup); } @Override public SimpleBoardModel[] getBoardsList(ProgressListener listener, CancellableTask task, SimpleBoardModel[] oldBoardsList) throws Exception { throw new UnsupportedOperationException(); } @Override public BoardModel getBoard(String shortName, ProgressListener listener, CancellableTask task) throws Exception { if (shortName.equals(BOARD.boardName)) return BOARD; throw new IllegalArgumentException(); } @Override public ThreadModel[] getThreadsList(String boardName, int page, ProgressListener listener, CancellableTask task, ThreadModel[] oldList) { throw new UnsupportedOperationException(); } private UrlPageModel getPageModel(String threadNumber) { UrlPageModel pageModel = new UrlPageModel(); pageModel.type = UrlPageModel.TYPE_THREADPAGE; pageModel.chanName = CHAN_NAME; pageModel.boardName = BOARD.boardName; pageModel.threadNumber = threadNumber; return pageModel; } private PostModel getPostHeader(String threadNumber, boolean newThreadLink) { PostModel postHeader = new PostModel(); postHeader.number = threadNumber; postHeader.subject = "Кукареканье со стороны диджейки"; postHeader.name = ""; postHeader.comment = !newThreadLink ? "" : ("<a href=\"http://" + DOMAIN + "/#_" + Integer.toString(Integer.parseInt(getCurrentThreadNumber().substring(1)) + 1) + "\">Перейти на новую страницу</a>"); return postHeader; } private String getCurrentThreadNumber() { return "_" + preferences.getInt(getSharedKey("curthread"), 1); } private void setCurrentThreadNumber(String threadNumber) { if (!threadNumber.startsWith("_")) throw new IllegalArgumentException(); int i = Integer.parseInt(threadNumber.substring(1)); int currentThread = preferences.getInt(getSharedKey("curthread"), 1); if (i == currentThread) return; if (i < currentThread) throw new IllegalArgumentException("Перейдите на новую страницу"); preferences.edit().putInt(getSharedKey("curthread"), i).commit(); } @Override public String buildUrl(UrlPageModel model) throws IllegalArgumentException { String url = "http://" + DOMAIN + "/"; if (model.threadNumber != null && model.threadNumber.length() > 0) url += "#" + model.threadNumber; return url; } @Override public UrlPageModel parseUrl(String url) throws IllegalArgumentException { if (UrlPathUtils.getUrlPath(url, DOMAIN) == null) throw new IllegalArgumentException("wrong domain"); String threadNumber = url.substring(url.indexOf('#') + 1); if (threadNumber.length() == url.length()) threadNumber = getCurrentThreadNumber(); return getPageModel(threadNumber); } @Override public PostModel[] getPostsList(String boardName, String threadNumber, ProgressListener listener, CancellableTask task, PostModel[] oldList) throws Exception { if (!boardName.equals(BOARD.boardName)) throw new IllegalArgumentException(); setCurrentThreadNumber(threadNumber); String url = "http://" + DOMAIN + "/answers.js"; HttpRequestModel request = HttpRequestModel.builder().setGET().setCheckIfModified(oldList != null).build(); JSONArray json = HttpStreamer.getInstance().getJSONArrayFromUrl(url, request, httpClient, listener, task, false); if (json == null) return oldList; long oneday = 86400000; long now = System.currentTimeMillis(); long startday = (now / oneday) * oneday - (3 * 3600000) + oneday; //today or tomorrow PostModel[] posts = new PostModel[json.length()+1]; posts[0] = oldList != null && oldList.length > 0 ? oldList[0] : getPostHeader(threadNumber, false); for (int i=json.length(); i>=1; --i) { JSONArray current = json.getJSONArray(json.length()-i); long time = TIMEFORMAT.parse(RegexUtils.removeHtmlTags(current.getString(3))).getTime(); posts[i] = new PostModel(); posts[i].number = Long.toString(time); //current.getString(4) ? posts[i].name = current.getString(1).equals("!") ? "Объявление" : current.getString(1); posts[i].subject = ""; posts[i].comment = "<blockquote class=\"unkfunc\">" + current.getString(2) + "</blockquote>" + current.getString(5); posts[i].timestamp = startday + time; while (posts[i].timestamp > (i<json.length() ? posts[i+1].timestamp : now)) posts[i].timestamp -= oneday; posts[i].parentThread = threadNumber; } if (oldList != null) { posts = ChanModels.mergePostsLists(Arrays.asList(oldList), Arrays.asList(posts)); posts[0] = getPostHeader(threadNumber, posts.length > 100); for (PostModel post : posts) post.deleted = false; } return posts; } @Override public CaptchaModel getNewCaptcha(String boardName, String threadNumber, ProgressListener listener, CancellableTask task) throws Exception { HttpRequestModel request = HttpRequestModel.builder().setGET().setCustomHeaders(HTTP_HEADER_FEEDBACK).build(); String postFormHtml = HttpStreamer.getInstance().getStringFromUrl("http://" + DOMAIN + "/feedback", request, httpClient, null, task, false); Matcher matcher = CID_PATTERN.matcher(postFormHtml); if (!matcher.find()) throw new Exception("Couldn't get captcha"); String cid = matcher.group(1); this.cid = cid; String captchaUrl = "http://" + DOMAIN + "/feedback/" + cid + ".gif"; Bitmap captchaBitmap = null; HttpResponseModel responseModel = HttpStreamer.getInstance().getFromUrl(captchaUrl, request, httpClient, listener, task); try { InputStream imageStream = responseModel.stream; captchaBitmap = BitmapFactory.decodeStream(imageStream); } finally { responseModel.release(); } CaptchaModel captchaModel = new CaptchaModel(); captchaModel.type = CaptchaModel.TYPE_NORMAL_DIGITS; captchaModel.bitmap = captchaBitmap; return captchaModel; } @Override public String sendPost(SendPostModel model, ProgressListener listener, CancellableTask task) throws Exception { if (model.comment.length() > 500) throw new Exception("Максимальная длина сообщения - 500 символов"); List<NameValuePair> pairs = new ArrayList<NameValuePair>(); pairs.add(new BasicNameValuePair("cid", cid)); pairs.add(new BasicNameValuePair("left", Integer.toString(500 - model.comment.length()))); pairs.add(new BasicNameValuePair("msg", model.comment)); pairs.add(new BasicNameValuePair("check", model.captchaAnswer)); HttpRequestModel request = HttpRequestModel.builder().setPOST(new UrlEncodedFormEntity(pairs, "UTF-8")).setCustomHeaders(HTTP_HEADER_FEEDBACK).build(); HttpResponseModel response = null; try { response = HttpStreamer.getInstance().getFromUrl("http://" + DOMAIN + "/feedback", 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("window.close,10000")) return "http://" + DOMAIN + "/#" + getCurrentThreadNumber(); if (htmlResponse.contains("Неверный код подтверждения")) throw new Exception("Неверный код подтверждения"); throw new Exception("Ошибка отправки"); } else { throw new Exception(response.statusCode + " - " + response.statusReason); } } finally { if (response != null) response.release(); } } }