Java tutorial
/* * Overchan Android (Meta Imageboard Client) * Copyright (C) 2014-2015 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.nullchancc; import java.io.ByteArrayInputStream; import java.io.ByteArrayOutputStream; import java.io.InputStream; import java.io.SequenceInputStream; import java.text.DateFormat; import java.text.DateFormatSymbols; import java.text.SimpleDateFormat; import java.util.ArrayList; import java.util.List; import java.util.Locale; import java.util.TimeZone; import java.util.regex.Matcher; import java.util.regex.Pattern; import org.apache.http.Header; import org.apache.http.HttpHeaders; import org.apache.http.NameValuePair; import org.apache.http.client.entity.UrlEncodedFormEntityHC4; import org.apache.http.message.BasicNameValuePair; import android.annotation.SuppressLint; import android.content.Context; 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.CheckBoxPreference; import android.preference.PreferenceGroup; import android.support.v4.content.res.ResourcesCompat; import nya.miku.wishmaster.R; 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.ThreadModel; 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.chans.AbstractWakabaModule; 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; @SuppressWarnings("deprecation") //https://issues.apache.org/jira/browse/HTTPCLIENT-1632 public class NullchanccModule extends AbstractWakabaModule { private static final String TAG = "NullchanccModule"; private static final String CHAN_NAME = "0chan.cc"; private static final String DOMAIN = "0chan.cc"; private static final SimpleBoardModel[] BOARDS = new SimpleBoardModel[] { ChanModels.obtainSimpleBoardModel(CHAN_NAME, "b", "", "all", true), ChanModels.obtainSimpleBoardModel(CHAN_NAME, "d", "?", "all", false), ChanModels.obtainSimpleBoardModel(CHAN_NAME, "r", "?", "all", false), ChanModels.obtainSimpleBoardModel(CHAN_NAME, "0", " ?", "all", false), ChanModels.obtainSimpleBoardModel(CHAN_NAME, "e", "?", "geek", false), ChanModels.obtainSimpleBoardModel(CHAN_NAME, "t", "", "geek", false), ChanModels.obtainSimpleBoardModel(CHAN_NAME, "hw", "", "geek", false), ChanModels.obtainSimpleBoardModel(CHAN_NAME, "s", "", "geek", false), ChanModels.obtainSimpleBoardModel(CHAN_NAME, "c", "", "geek", false), ChanModels.obtainSimpleBoardModel(CHAN_NAME, "vg", "", "geek", false), ChanModels.obtainSimpleBoardModel(CHAN_NAME, "8", "8-bit pixel art", "geek", false), ChanModels.obtainSimpleBoardModel(CHAN_NAME, "bg", "?? ", "geek", false), ChanModels.obtainSimpleBoardModel(CHAN_NAME, "wh", "Warhammer", "geek", false), ChanModels.obtainSimpleBoardModel(CHAN_NAME, "a", "?", "other", false), ChanModels.obtainSimpleBoardModel(CHAN_NAME, "au", "?", "other", false), ChanModels.obtainSimpleBoardModel(CHAN_NAME, "bo", "", "other", false), ChanModels.obtainSimpleBoardModel(CHAN_NAME, "co", "?", "other", false), ChanModels.obtainSimpleBoardModel(CHAN_NAME, "cook", " ?", "other", false), ChanModels.obtainSimpleBoardModel(CHAN_NAME, "f", "Flash", "other", false), ChanModels.obtainSimpleBoardModel(CHAN_NAME, "fa", " ?", "other", false), ChanModels.obtainSimpleBoardModel(CHAN_NAME, "fl", "? ?", "other", false), ChanModels.obtainSimpleBoardModel(CHAN_NAME, "m", "", "other", false), ChanModels.obtainSimpleBoardModel(CHAN_NAME, "med", "", "other", false), ChanModels.obtainSimpleBoardModel(CHAN_NAME, "ne", "", "other", false), ChanModels.obtainSimpleBoardModel(CHAN_NAME, "ph", "", "other", false), ChanModels.obtainSimpleBoardModel(CHAN_NAME, "tv", " ?", "other", false), ChanModels.obtainSimpleBoardModel(CHAN_NAME, "wp", "", "other", false), ChanModels.obtainSimpleBoardModel(CHAN_NAME, "war", "", "other", false), ChanModels.obtainSimpleBoardModel(CHAN_NAME, "h", "", "adult", true), ChanModels.obtainSimpleBoardModel(CHAN_NAME, "g", "", "adult", true), ChanModels.obtainSimpleBoardModel(CHAN_NAME, "fur", "", "adult", true) }; private static final String PREF_KEY_ONLY_NEW_POSTS = "PREF_KEY_ONLY_NEW_POSTS"; private static final Pattern PATTERN_EMBEDDED = Pattern.compile("<div (?:[^>]*)data-id=\"([^\"]*)\"(?:[^>]*)>", Pattern.DOTALL); private static final Pattern ERROR_POSTING = Pattern.compile("<h2(?:[^>]*)>(.*?)</h2>", Pattern.DOTALL); public NullchanccModule(SharedPreferences preferences, Resources resources) { super(preferences, resources); } @Override public String getChanName() { return CHAN_NAME; } @Override public String getDisplayingName() { return " (.cc)"; } @Override public Drawable getChanFavicon() { return ResourcesCompat.getDrawable(resources, R.drawable.favicon_0chan, null); } @Override protected String getUsingDomain() { return DOMAIN; } @Override protected boolean canHttps() { return true; } private void addOnlyNewPostsPreference(PreferenceGroup group) { Context context = group.getContext(); CheckBoxPreference onlyNewPostsPreference = new CheckBoxPreference(context); onlyNewPostsPreference.setTitle(R.string.nullchancc_prefs_only_new_posts); onlyNewPostsPreference.setSummary(R.string.nullchancc_prefs_only_new_posts_summary); onlyNewPostsPreference.setKey(getSharedKey(PREF_KEY_ONLY_NEW_POSTS)); onlyNewPostsPreference.setDefaultValue(true); group.addItemFromInflater(onlyNewPostsPreference); } private boolean loadOnlyNewPosts() { return preferences.getBoolean(getSharedKey(PREF_KEY_ONLY_NEW_POSTS), true); } @Override public void addPreferencesOnScreen(PreferenceGroup preferenceGroup) { addOnlyNewPostsPreference(preferenceGroup); super.addPreferencesOnScreen(preferenceGroup); } @Override protected SimpleBoardModel[] getBoardsList() { return BOARDS; } @Override public BoardModel getBoard(String shortName, ProgressListener listener, CancellableTask task) throws Exception { BoardModel model = super.getBoard(shortName, listener, task); model.timeZoneId = "GMT+3"; model.defaultUserName = "?"; model.readonlyBoard = false; model.requiredFileForNewThread = !shortName.equals("0"); model.allowDeletePosts = true; model.allowDeleteFiles = true; model.allowNames = !shortName.equals("b"); model.allowSubjects = true; model.allowSage = true; model.allowEmails = false; model.allowWatermark = false; model.allowOpMark = false; model.allowRandomHash = true; model.allowIcons = false; model.attachmentsMaxCount = 1; model.attachmentsFormatFilters = null; model.markType = BoardModel.MARK_WAKABAMARK; return model; } @SuppressLint("SimpleDateFormat") @Override protected WakabaReader getWakabaReader(InputStream stream, UrlPageModel urlModel) { if (urlModel != null && urlModel.chanName != null && urlModel.chanName.equals("expand")) { stream = new SequenceInputStream(new ByteArrayInputStream("<form id=\"delform\">".getBytes()), stream); } return new WakabaReader(stream) { private final DateFormat dateFormat; { DateFormatSymbols symbols = new DateFormatSymbols(); symbols.setShortMonths(new String[] { "", "", "", "?", "", "", "", "?", "", "", "??", "" }); dateFormat = new SimpleDateFormat("yyyy MMM dd HH:mm:ss", symbols); dateFormat.setTimeZone(TimeZone.getTimeZone("GMT+3")); } @Override protected void parseDate(String date) { if (date.length() > 0) { date = date.replaceAll("(?:[^\\d]*)(\\d(?:.*))", "$1"); try { currentPost.timestamp = dateFormat.parse(date).getTime(); } catch (Exception e) { Logger.e(TAG, "cannot parse date", e); } } } @Override protected void parseOmittedString(String omitted) { if (omitted.indexOf('>') != -1) omitted = omitted.substring(omitted.indexOf('>')); super.parseOmittedString(omitted); } @Override protected void postprocessPost(PostModel post) { Matcher matcher = PATTERN_EMBEDDED.matcher(post.comment); while (matcher.find()) { String id = matcher.group(1); String div = matcher.group(0).toLowerCase(Locale.US); String url = null; if (div.contains("youtube")) { url = "http://www.youtube.com/watch?v=" + id; } else if (div.contains("vimeo")) { url = "http://vimeo.com/" + id; } else if (div.contains("coub")) { url = "http://coub.com/view/" + id; } if (url != null) { AttachmentModel attachment = new AttachmentModel(); attachment.type = AttachmentModel.TYPE_OTHER_NOTFILE; attachment.path = url; attachment.thumbnail = div.contains("youtube") ? ("http://img.youtube.com/vi/" + id + "/default.jpg") : null; int oldCount = post.attachments != null ? post.attachments.length : 0; AttachmentModel[] attachments = new AttachmentModel[oldCount + 1]; for (int i = 0; i < oldCount; ++i) attachments[i] = post.attachments[i]; attachments[oldCount] = attachment; post.attachments = attachments; } } } }; } @Override public PostModel[] getPostsList(String boardName, String threadNumber, ProgressListener listener, CancellableTask task, PostModel[] oldList) throws Exception { if (loadOnlyNewPosts() && oldList != null && oldList.length > 0) { String url = getUsingUrl() + "expand.php?after=" + oldList[oldList.length - 1].number + "&board=" + boardName + "&threadid=" + threadNumber; UrlPageModel object = new UrlPageModel(); object.chanName = "expand"; ThreadModel[] page = readWakabaPage(url, listener, task, true, object); if (page != null && page.length > 0) { PostModel[] posts = new PostModel[oldList.length + page[0].posts.length]; for (int i = 0; i < oldList.length; ++i) posts[i] = oldList[i]; for (int i = 0; i < page[0].posts.length; ++i) posts[oldList.length + i] = page[0].posts[i]; return posts; } else { return oldList; } } return super.getPostsList(boardName, threadNumber, listener, task, oldList); } @Override public CaptchaModel getNewCaptcha(String boardName, String threadNumber, ProgressListener listener, CancellableTask task) throws Exception { String captchaUrl = getUsingUrl() + "captcha.php?" + Math.random(); Bitmap captchaBitmap = null; HttpRequestModel requestModel = HttpRequestModel.builder().setGET().build(); HttpResponseModel responseModel = HttpStreamer.getInstance().getFromUrl(captchaUrl, requestModel, httpClient, listener, task); try { InputStream imageStream = responseModel.stream; captchaBitmap = BitmapFactory.decodeStream(imageStream); } finally { responseModel.release(); } CaptchaModel captchaModel = new CaptchaModel(); captchaModel.type = CaptchaModel.TYPE_NORMAL; captchaModel.bitmap = captchaBitmap; return captchaModel; } @Override public String sendPost(SendPostModel model, ProgressListener listener, CancellableTask task) throws Exception { String url = getUsingUrl() + "board.php"; ExtendedMultipartBuilder postEntityBuilder = ExtendedMultipartBuilder.create().setDelegates(listener, task) .addString("board", model.boardName) .addString("replythread", model.threadNumber == null ? "0" : model.threadNumber) .addString("name", model.name); if (model.sage) postEntityBuilder.addString("sage", "on"); postEntityBuilder.addString("captcha", model.captchaAnswer).addString("subject", model.subject) .addString("message", model.comment).addString("postpassword", model.password); if (model.attachments != null && model.attachments.length > 0) postEntityBuilder.addFile("imagefile", model.attachments[0], model.randomHash); else postEntityBuilder.addString("nofile", "on"); postEntityBuilder.addString("redirecttothread", "1"); 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 == 302) { 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"); Matcher errorMatcher = ERROR_POSTING.matcher(htmlResponse); if (errorMatcher.find()) throw new Exception(errorMatcher.group(1).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() + "board.php"; List<NameValuePair> pairs = new ArrayList<NameValuePair>(); pairs.add(new BasicNameValuePair("board", model.boardName)); pairs.add(new BasicNameValuePair("post[]", model.postNumber)); if (model.onlyFiles) pairs.add(new BasicNameValuePair("fileonly", "on")); pairs.add(new BasicNameValuePair("postpassword", model.password)); pairs.add(new BasicNameValuePair("deletepost", "")); HttpRequestModel request = HttpRequestModel.builder().setPOST(new UrlEncodedFormEntityHC4(pairs, "UTF-8")) .setNoRedirect(true).build(); String result = HttpStreamer.getInstance().getStringFromUrl(url, request, httpClient, listener, task, false); System.out.println(result); if (result.contains("? ")) throw new Exception("? "); return null; } @Override public String fixRelativeUrl(String url) { if (useHttps()) url = url.replace("http://0chan.cc", "https://0chan.cc"); return super.fixRelativeUrl(url); } }