/*******************************************************************************
* This file is part of RedReader.
*
* RedReader 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.
*
* RedReader 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 RedReader. If not, see <http://www.gnu.org/licenses/>.
******************************************************************************/
package org.quantumbadger.redreader.reddit;
import android.content.Context;
import android.support.annotation.IntDef;
import android.support.annotation.NonNull;
import org.quantumbadger.redreader.account.RedditAccount;
import org.quantumbadger.redreader.activities.BugReportActivity;
import org.quantumbadger.redreader.cache.CacheManager;
import org.quantumbadger.redreader.cache.CacheRequest;
import org.quantumbadger.redreader.cache.downloadstrategy.DownloadStrategy;
import org.quantumbadger.redreader.cache.downloadstrategy.DownloadStrategyAlways;
import org.quantumbadger.redreader.common.Constants;
import org.quantumbadger.redreader.common.TimestampBound;
import org.quantumbadger.redreader.io.RequestResponseHandler;
import org.quantumbadger.redreader.jsonwrap.JsonBufferedArray;
import org.quantumbadger.redreader.jsonwrap.JsonValue;
import org.quantumbadger.redreader.reddit.api.SubredditRequestFailure;
import org.quantumbadger.redreader.reddit.things.RedditSubreddit;
import org.quantumbadger.redreader.reddit.things.RedditThing;
import org.quantumbadger.redreader.reddit.things.RedditUser;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.net.URI;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.UUID;
import static org.quantumbadger.redreader.http.HTTPBackend.PostField;
public final class RedditAPI {
public static final int ACTION_UPVOTE = 0;
public static final int ACTION_UNVOTE = 1;
public static final int ACTION_DOWNVOTE = 2;
public static final int ACTION_SAVE = 3;
public static final int ACTION_HIDE = 4;
public static final int ACTION_UNSAVE = 5;
public static final int ACTION_UNHIDE = 6;
public static final int ACTION_REPORT = 7;
public static final int ACTION_DELETE = 8;
public static final int SUBSCRIPTION_ACTION_SUBSCRIBE = 0;
public static final int SUBSCRIPTION_ACTION_UNSUBSCRIBE = 1;
@IntDef({ACTION_UPVOTE, ACTION_UNVOTE, ACTION_DOWNVOTE, ACTION_SAVE, ACTION_HIDE, ACTION_UNSAVE,
ACTION_UNHIDE, ACTION_REPORT, ACTION_DELETE})
@Retention(RetentionPolicy.SOURCE)
public @interface RedditAction {}
@IntDef({SUBSCRIPTION_ACTION_SUBSCRIBE, SUBSCRIPTION_ACTION_UNSUBSCRIBE})
@Retention(RetentionPolicy.SOURCE)
public @interface RedditSubredditAction {}
public static void submit(
final CacheManager cm,
final APIResponseHandler.ActionResponseHandler responseHandler,
final RedditAccount user,
final boolean is_self,
final String subreddit,
final String title,
final String body,
final boolean sendRepliesToInbox,
final Context context)
{
final LinkedList<PostField> postFields = new LinkedList<>();
postFields.add(new PostField("kind", is_self ? "self" : "link"));
postFields.add(new PostField("sendreplies", sendRepliesToInbox ? "true" : "false"));
postFields.add(new PostField("sr", subreddit));
postFields.add(new PostField("title", title));
if(is_self)
postFields.add(new PostField("text", body));
else
postFields.add(new PostField("url", body));
cm.makeRequest(new APIPostRequest(Constants.Reddit.getUri("/api/submit"), user, postFields, context) {
@Override
public void onJsonParseStarted(JsonValue result, long timestamp, UUID session, boolean fromCache) {
System.out.println(result.toString());
try {
final APIResponseHandler.APIFailureType failureType = findFailureType(result);
if(failureType != null) {
responseHandler.notifyFailure(failureType);
return;
}
} catch(Throwable t) {
notifyFailure(CacheRequest.REQUEST_FAILURE_PARSE, t, null, "JSON failed to parse");
}
responseHandler.notifySuccess();
}
@Override
protected void onCallbackException(Throwable t) {
BugReportActivity.handleGlobalError(context, t);
}
@Override
protected void onFailure(@CacheRequest.RequestFailureType int type, Throwable t, Integer status, String readableMessage) {
responseHandler.notifyFailure(type, t, status, readableMessage);
}
});
}
public static void compose(
@NonNull final CacheManager cm,
@NonNull final APIResponseHandler.ActionResponseHandler responseHandler,
@NonNull final RedditAccount user,
@NonNull final String recipient,
@NonNull final String subject,
@NonNull final String body,
@NonNull final Context context) {
final LinkedList<PostField> postFields = new LinkedList<>();
postFields.add(new PostField("api_type", "json"));
postFields.add(new PostField("subject", subject));
postFields.add(new PostField("to", recipient));
postFields.add(new PostField("text", body));
cm.makeRequest(new APIPostRequest(Constants.Reddit.getUri("/api/compose"), user, postFields, context) {
@Override
public void onJsonParseStarted(JsonValue result, long timestamp, UUID session, boolean fromCache) {
System.out.println(result.toString());
try {
final APIResponseHandler.APIFailureType failureType = findFailureType(result);
if(failureType != null) {
responseHandler.notifyFailure(failureType);
return;
}
} catch(Throwable t) {
notifyFailure(CacheRequest.REQUEST_FAILURE_PARSE, t, null, "JSON failed to parse");
}
responseHandler.notifySuccess();
}
@Override
protected void onCallbackException(Throwable t) {
BugReportActivity.handleGlobalError(context, t);
}
@Override
protected void onFailure(@CacheRequest.RequestFailureType int type, Throwable t, Integer status, String readableMessage) {
responseHandler.notifyFailure(type, t, status, readableMessage);
}
});
}
public static void comment(final CacheManager cm,
final APIResponseHandler.ActionResponseHandler responseHandler,
final RedditAccount user,
final String parentIdAndType,
final String markdown,
final Context context) {
final LinkedList<PostField> postFields = new LinkedList<>();
postFields.add(new PostField("thing_id", parentIdAndType));
postFields.add(new PostField("text", markdown));
cm.makeRequest(new APIPostRequest(Constants.Reddit.getUri("/api/comment"), user, postFields, context) {
@Override
public void onJsonParseStarted(JsonValue result, long timestamp, UUID session, boolean fromCache) {
try {
final APIResponseHandler.APIFailureType failureType = findFailureType(result);
if(failureType != null) {
responseHandler.notifyFailure(failureType);
return;
}
} catch(Throwable t) {
notifyFailure(CacheRequest.REQUEST_FAILURE_PARSE, t, null, "JSON failed to parse");
}
responseHandler.notifySuccess();
}
@Override
protected void onCallbackException(Throwable t) {
BugReportActivity.handleGlobalError(context, t);
}
@Override
protected void onFailure(@CacheRequest.RequestFailureType int type, Throwable t, Integer status, String readableMessage) {
responseHandler.notifyFailure(type, t, status, readableMessage);
}
});
}
public static void markAllAsRead(
final CacheManager cm,
final APIResponseHandler.ActionResponseHandler responseHandler,
final RedditAccount user,
final Context context) {
final LinkedList<PostField> postFields = new LinkedList<>();
cm.makeRequest(new APIPostRequest(Constants.Reddit.getUri("/api/read_all_messages"), user, postFields, context) {
@Override
public void onJsonParseStarted(JsonValue result, long timestamp, UUID session, boolean fromCache) {
try {
final APIResponseHandler.APIFailureType failureType = findFailureType(result);
if(failureType != null) {
responseHandler.notifyFailure(failureType);
return;
}
} catch(Throwable t) {
notifyFailure(CacheRequest.REQUEST_FAILURE_PARSE, t, null, "JSON failed to parse");
}
responseHandler.notifySuccess();
}
@Override
protected void onCallbackException(Throwable t) {
BugReportActivity.handleGlobalError(context, t);
}
@Override
protected void onFailure(@CacheRequest.RequestFailureType int type, Throwable t, Integer status, String readableMessage) {
responseHandler.notifyFailure(type, t, status, readableMessage);
}
});
}
public static void editComment(final CacheManager cm,
final APIResponseHandler.ActionResponseHandler responseHandler,
final RedditAccount user,
final String commentIdAndType,
final String markdown,
final Context context) {
final LinkedList<PostField> postFields = new LinkedList<>();
postFields.add(new PostField("thing_id", commentIdAndType));
postFields.add(new PostField("text", markdown));
cm.makeRequest(new APIPostRequest(Constants.Reddit.getUri("/api/editusertext"), user, postFields, context) {
@Override
public void onJsonParseStarted(JsonValue result, long timestamp, UUID session, boolean fromCache) {
try {
final APIResponseHandler.APIFailureType failureType = findFailureType(result);
if(failureType != null) {
responseHandler.notifyFailure(failureType);
return;
}
} catch(Throwable t) {
notifyFailure(CacheRequest.REQUEST_FAILURE_PARSE, t, null, "JSON failed to parse");
}
responseHandler.notifySuccess();
}
@Override
protected void onCallbackException(Throwable t) {
BugReportActivity.handleGlobalError(context, t);
}
@Override
protected void onFailure(@CacheRequest.RequestFailureType int type, Throwable t, Integer status, String readableMessage) {
responseHandler.notifyFailure(type, t, status, readableMessage);
}
});
}
public static void action(final CacheManager cm,
final APIResponseHandler.ActionResponseHandler responseHandler,
final RedditAccount user,
final String idAndType,
final @RedditAction int action,
final Context context) {
final LinkedList<PostField> postFields = new LinkedList<>();
postFields.add(new PostField("id", idAndType));
final URI url = prepareActionUri(action, postFields);
cm.makeRequest(new APIPostRequest(url, user, postFields, context) {
@Override
protected void onCallbackException(final Throwable t) {
BugReportActivity.handleGlobalError(context, t);
}
@Override
protected void onFailure(final @CacheRequest.RequestFailureType int type, final Throwable t, final Integer status, final String readableMessage) {
responseHandler.notifyFailure(type, t, status, readableMessage);
}
@Override
public void onJsonParseStarted(final JsonValue result, final long timestamp, final UUID session, final boolean fromCache) {
try {
final APIResponseHandler.APIFailureType failureType = findFailureType(result);
if(failureType != null) {
responseHandler.notifyFailure(failureType);
return;
}
} catch(Throwable t) {
notifyFailure(CacheRequest.REQUEST_FAILURE_PARSE, t, null, "JSON failed to parse");
}
responseHandler.notifySuccess();
}
});
}
private static URI prepareActionUri(final @RedditAction int action, final LinkedList<PostField> postFields) {
switch(action) {
case ACTION_DOWNVOTE:
postFields.add(new PostField("dir", "-1"));
return Constants.Reddit.getUri(Constants.Reddit.PATH_VOTE);
case ACTION_UNVOTE:
postFields.add(new PostField("dir", "0"));
return Constants.Reddit.getUri(Constants.Reddit.PATH_VOTE);
case ACTION_UPVOTE:
postFields.add(new PostField("dir", "1"));
return Constants.Reddit.getUri(Constants.Reddit.PATH_VOTE);
case ACTION_SAVE: return Constants.Reddit.getUri(Constants.Reddit.PATH_SAVE);
case ACTION_HIDE: return Constants.Reddit.getUri(Constants.Reddit.PATH_HIDE);
case ACTION_UNSAVE: return Constants.Reddit.getUri(Constants.Reddit.PATH_UNSAVE);
case ACTION_UNHIDE: return Constants.Reddit.getUri(Constants.Reddit.PATH_UNHIDE);
case ACTION_REPORT: return Constants.Reddit.getUri(Constants.Reddit.PATH_REPORT);
case ACTION_DELETE: return Constants.Reddit.getUri(Constants.Reddit.PATH_DELETE);
default:
throw new RuntimeException("Unknown post/comment action");
}
}
public static void subscriptionAction(final CacheManager cm,
final APIResponseHandler.ActionResponseHandler responseHandler,
final RedditAccount user,
final String subredditCanonicalName,
final @RedditSubredditAction int action,
final Context context) {
RedditSubredditManager.getInstance(context, user).getSubreddit(
subredditCanonicalName,
TimestampBound.ANY,
new RequestResponseHandler<RedditSubreddit, SubredditRequestFailure>() {
@Override
public void onRequestFailed(SubredditRequestFailure failureReason) {
responseHandler.notifyFailure(
failureReason.requestFailureType,
failureReason.t,
failureReason.statusLine,
failureReason.readableMessage);
}
@Override
public void onRequestSuccess(RedditSubreddit subreddit, long timeCached) {
final LinkedList<PostField> postFields = new LinkedList<>();
postFields.add(new PostField("sr", subreddit.name));
final URI url = subscriptionPrepareActionUri(action, postFields);
cm.makeRequest(new APIPostRequest(url, user, postFields, context) {
@Override
protected void onCallbackException(final Throwable t) {
BugReportActivity.handleGlobalError(context, t);
}
@Override
protected void onFailure(final @CacheRequest.RequestFailureType int type, final Throwable t, final Integer status, final String readableMessage) {
responseHandler.notifyFailure(type, t, status, readableMessage);
}
@Override
public void onJsonParseStarted(final JsonValue result, final long timestamp, final UUID session, final boolean fromCache) {
try {
final APIResponseHandler.APIFailureType failureType = findFailureType(result);
if(failureType != null) {
responseHandler.notifyFailure(failureType);
return;
}
} catch(Throwable t) {
notifyFailure(CacheRequest.REQUEST_FAILURE_PARSE, t, null, "JSON failed to parse");
}
responseHandler.notifySuccess();
}
});
}
},
null
);
}
private static URI subscriptionPrepareActionUri(final @RedditSubredditAction int action,
final LinkedList<PostField> postFields) {
switch(action) {
case SUBSCRIPTION_ACTION_SUBSCRIBE:
postFields.add(new PostField("action", "sub"));
return Constants.Reddit.getUri(Constants.Reddit.PATH_SUBSCRIBE);
case SUBSCRIPTION_ACTION_UNSUBSCRIBE:
postFields.add(new PostField("action", "unsub"));
return Constants.Reddit.getUri(Constants.Reddit.PATH_SUBSCRIBE);
default:
throw new RuntimeException("Unknown subreddit action");
}
}
public static void getUser(final CacheManager cm,
final String usernameToGet,
final APIResponseHandler.UserResponseHandler responseHandler,
final RedditAccount user,
final DownloadStrategy downloadStrategy,
final boolean cancelExisting,
final Context context) {
final URI uri = Constants.Reddit.getUri("/user/" + usernameToGet + "/about.json");
cm.makeRequest(new APIGetRequest(uri, user, Constants.Priority.API_USER_ABOUT, Constants.FileType.USER_ABOUT, downloadStrategy, true, cancelExisting, context) {
@Override
protected void onDownloadNecessary() {}
@Override
protected void onDownloadStarted() {
responseHandler.notifyDownloadStarted();
}
@Override
protected void onCallbackException(final Throwable t) {
BugReportActivity.handleGlobalError(context, t);
}
@Override
protected void onFailure(final @CacheRequest.RequestFailureType int type, final Throwable t, final Integer status, final String readableMessage) {
responseHandler.notifyFailure(type, t, status, readableMessage);
}
@Override
public void onJsonParseStarted(final JsonValue result, final long timestamp, final UUID session, final boolean fromCache) {
try {
final RedditThing userThing = result.asObject(RedditThing.class);
final RedditUser userResult = userThing.asUser();
responseHandler.notifySuccess(userResult, timestamp);
} catch(Throwable t) {
// TODO look for error
notifyFailure(CacheRequest.REQUEST_FAILURE_PARSE, t, null, "JSON parse failed for unknown reason");
}
}
});
}
// lol, reddit api
private static APIResponseHandler.APIFailureType findFailureType(final JsonValue response) {
// TODO handle 403 forbidden
switch(response.getType()) {
case JsonValue.TYPE_OBJECT:
for(final Map.Entry<String, JsonValue> v : response.asObject()) {
final APIResponseHandler.APIFailureType failureType = findFailureType(v.getValue());
if(failureType != null) return failureType;
}
try {
final JsonBufferedArray errors = response.asObject().getObject("json").getArray("errors");
if(errors != null) {
errors.join();
if(errors.getCurrentItemCount() > 0) {
return APIResponseHandler.APIFailureType.UNKNOWN;
}
}
} catch(Exception e) {
// Do nothing
}
break;
case JsonValue.TYPE_ARRAY:
for(final JsonValue v : response.asArray()) {
final APIResponseHandler.APIFailureType failureType = findFailureType(v);
if(failureType != null) return failureType;
}
break;
case JsonValue.TYPE_STRING:
if(Constants.Reddit.isApiErrorUser(response.asString()))
return APIResponseHandler.APIFailureType.INVALID_USER;
if(Constants.Reddit.isApiErrorCaptcha(response.asString()))
return APIResponseHandler.APIFailureType.BAD_CAPTCHA;
if(Constants.Reddit.isApiErrorNotAllowed(response.asString()))
return APIResponseHandler.APIFailureType.NOTALLOWED;
if(Constants.Reddit.isApiErrorSubredditRequired(response.asString()))
return APIResponseHandler.APIFailureType.SUBREDDIT_REQUIRED;
if(Constants.Reddit.isApiErrorURLRequired(response.asString()))
return APIResponseHandler.APIFailureType.URL_REQUIRED;
if(Constants.Reddit.isApiTooFast(response.asString()))
return APIResponseHandler.APIFailureType.TOO_FAST;
if(Constants.Reddit.isApiTooLong(response.asString()))
return APIResponseHandler.APIFailureType.TOO_LONG;
break;
default:
// Ignore
}
return null;
}
private static abstract class APIPostRequest extends CacheRequest {
@Override
protected void onDownloadNecessary() {}
@Override
protected void onDownloadStarted() {}
public APIPostRequest(final URI url, final RedditAccount user, final List<PostField> postFields, final Context context) {
super(url, user, null, Constants.Priority.API_ACTION, 0,
DownloadStrategyAlways.INSTANCE, Constants.FileType.NOCACHE, DOWNLOAD_QUEUE_REDDIT_API, true, postFields, false, false, context);
}
@Override
protected final void onSuccess(final CacheManager.ReadableCacheFile cacheFile, final long timestamp, final UUID session, final boolean fromCache, final String mimetype) {
throw new RuntimeException("onSuccess called for uncached request");
}
@Override
protected final void onProgress(final boolean authorizationInProgress, final long bytesRead, final long totalBytes) {
}
@Override
public abstract void onJsonParseStarted(JsonValue result, long timestamp, UUID session, boolean fromCache);
}
// TODO merge get and post into one?
private static abstract class APIGetRequest extends CacheRequest {
public APIGetRequest(final URI url, final RedditAccount user, final int priority, final int fileType, final DownloadStrategy downloadStrategy, final boolean cache, final boolean cancelExisting, final Context context) {
super(url, user, null, priority, 0, downloadStrategy, fileType, DOWNLOAD_QUEUE_REDDIT_API, true, null, cache, cancelExisting, context);
}
@Override
protected final void onSuccess(final CacheManager.ReadableCacheFile cacheFile, final long timestamp, final UUID session, final boolean fromCache, final String mimetype) {
if(!cache) throw new RuntimeException("onSuccess called for uncached request");
}
@Override
protected final void onProgress(final boolean authorizationInProgress, final long bytesRead, final long totalBytes) {}
@Override
public abstract void onJsonParseStarted(JsonValue result, long timestamp, UUID session, boolean fromCache);
}
}