From 4b47d907cd7704a2daa54926bc4ffb98145c1020 Mon Sep 17 00:00:00 2001 From: adb Date: Wed, 20 Jan 2021 16:32:41 +0100 Subject: [PATCH] add com.github.jreddit:1.0.4 package --- build.gradle | 9 +- .../jreddit/oauth/RedditOAuthAgent.java | 370 ++++++++++++ .../com/github/jreddit/oauth/RedditToken.java | 187 ++++++ .../github/jreddit/oauth/app/RedditApp.java | 52 ++ .../jreddit/oauth/app/RedditInstalledApp.java | 19 + .../jreddit/oauth/app/RedditScriptApp.java | 19 + .../jreddit/oauth/app/RedditWebApp.java | 19 + .../jreddit/oauth/client/RedditClient.java | 47 ++ .../oauth/client/RedditHttpClient.java | 141 +++++ .../oauth/client/RedditPoliteClient.java | 101 ++++ .../oauth/exception/RedditOAuthException.java | 18 + .../jreddit/oauth/param/RedditDuration.java | 27 + .../jreddit/oauth/param/RedditScope.java | 48 ++ .../oauth/param/RedditScopeBuilder.java | 100 ++++ .../oauth/param/RedditTokenCompleteScope.java | 48 ++ .../github/jreddit/parser/entity/Comment.java | 269 +++++++++ .../github/jreddit/parser/entity/Kind.java | 44 ++ .../github/jreddit/parser/entity/Message.java | 167 ++++++ .../jreddit/parser/entity/MessageType.java | 25 + .../github/jreddit/parser/entity/More.java | 106 ++++ .../jreddit/parser/entity/Submission.java | 546 ++++++++++++++++++ .../jreddit/parser/entity/Subreddit.java | 223 +++++++ .../github/jreddit/parser/entity/Thing.java | 77 +++ .../jreddit/parser/entity/UserInfo.java | 212 +++++++ .../entity/imaginary/CommentTreeElement.java | 16 + .../entity/imaginary/FullSubmission.java | 35 ++ .../entity/imaginary/MixedListingElement.java | 16 + .../exception/RedditParseException.java | 29 + .../parser/listing/CommentsListingParser.java | 55 ++ .../parser/listing/CommentsMoreParser.java | 84 +++ .../parser/listing/MixedListingParser.java | 62 ++ .../parser/listing/RedditListingParser.java | 193 +++++++ .../listing/SubmissionsListingParser.java | 51 ++ .../listing/SubredditsListingParser.java | 51 ++ .../parser/single/FullSubmissionParser.java | 156 +++++ .../jreddit/parser/util/CommentTreeUtils.java | 96 +++ .../github/jreddit/parser/util/JsonUtils.java | 98 ++++ .../jreddit/request/RedditGetRequest.java | 48 ++ .../jreddit/request/RedditPostRequest.java | 86 +++ .../request/action/MarkActionRequest.java | 16 + .../action/flair/DeleteFlairRequest.java | 19 + .../request/action/mark/HideRequest.java | 20 + .../request/action/mark/MarkNsfwRequest.java | 20 + .../request/action/mark/ReportRequest.java | 20 + .../request/action/mark/SaveRequest.java | 20 + .../request/action/mark/UnhideRequest.java | 20 + .../action/mark/UnmarkNsfwRequest.java | 20 + .../request/action/mark/UnsaveRequest.java | 20 + .../request/action/mark/VoteRequest.java | 27 + .../request/retrieval/ListingRequest.java | 62 ++ .../comments/CommentsOfUserRequest.java | 40 ++ .../comments/MoreCommentsRequest.java | 37 ++ .../mixed/FullSubmissionRequest.java | 110 ++++ .../retrieval/mixed/MixedOfUserRequest.java | 63 ++ .../request/retrieval/param/CommentSort.java | 29 + .../request/retrieval/param/QuerySyntax.java | 22 + .../request/retrieval/param/SearchSort.java | 25 + .../retrieval/param/SubmissionSort.java | 27 + .../retrieval/param/SubredditsView.java | 25 + .../request/retrieval/param/TimeSpan.java | 26 + .../retrieval/param/UserMixedCategory.java | 20 + .../retrieval/param/UserOverviewSort.java | 24 + .../param/UserSubmissionsCategory.java | 20 + .../SubmissionsOfSubredditRequest.java | 32 + .../submissions/SubmissionsOfUserRequest.java | 44 ++ .../submissions/SubmissionsSearchRequest.java | 44 ++ .../subreddits/SubredditsOfUserRequest.java | 29 + .../subreddits/SubredditsSearchRequest.java | 21 + .../request/util/KeyValueFormatter.java | 86 +++ .../sh/adb/RandomRedditMemesAPI/Main.java | 2 - .../adb/RandomRedditMemesAPI/RedditAPI.java | 11 +- 71 files changed, 4946 insertions(+), 5 deletions(-) create mode 100644 src/main/com/github/jreddit/oauth/RedditOAuthAgent.java create mode 100644 src/main/com/github/jreddit/oauth/RedditToken.java create mode 100644 src/main/com/github/jreddit/oauth/app/RedditApp.java create mode 100644 src/main/com/github/jreddit/oauth/app/RedditInstalledApp.java create mode 100644 src/main/com/github/jreddit/oauth/app/RedditScriptApp.java create mode 100644 src/main/com/github/jreddit/oauth/app/RedditWebApp.java create mode 100644 src/main/com/github/jreddit/oauth/client/RedditClient.java create mode 100644 src/main/com/github/jreddit/oauth/client/RedditHttpClient.java create mode 100644 src/main/com/github/jreddit/oauth/client/RedditPoliteClient.java create mode 100644 src/main/com/github/jreddit/oauth/exception/RedditOAuthException.java create mode 100644 src/main/com/github/jreddit/oauth/param/RedditDuration.java create mode 100644 src/main/com/github/jreddit/oauth/param/RedditScope.java create mode 100644 src/main/com/github/jreddit/oauth/param/RedditScopeBuilder.java create mode 100644 src/main/com/github/jreddit/oauth/param/RedditTokenCompleteScope.java create mode 100644 src/main/com/github/jreddit/parser/entity/Comment.java create mode 100644 src/main/com/github/jreddit/parser/entity/Kind.java create mode 100644 src/main/com/github/jreddit/parser/entity/Message.java create mode 100644 src/main/com/github/jreddit/parser/entity/MessageType.java create mode 100644 src/main/com/github/jreddit/parser/entity/More.java create mode 100644 src/main/com/github/jreddit/parser/entity/Submission.java create mode 100644 src/main/com/github/jreddit/parser/entity/Subreddit.java create mode 100644 src/main/com/github/jreddit/parser/entity/Thing.java create mode 100644 src/main/com/github/jreddit/parser/entity/UserInfo.java create mode 100644 src/main/com/github/jreddit/parser/entity/imaginary/CommentTreeElement.java create mode 100644 src/main/com/github/jreddit/parser/entity/imaginary/FullSubmission.java create mode 100644 src/main/com/github/jreddit/parser/entity/imaginary/MixedListingElement.java create mode 100644 src/main/com/github/jreddit/parser/exception/RedditParseException.java create mode 100644 src/main/com/github/jreddit/parser/listing/CommentsListingParser.java create mode 100644 src/main/com/github/jreddit/parser/listing/CommentsMoreParser.java create mode 100644 src/main/com/github/jreddit/parser/listing/MixedListingParser.java create mode 100644 src/main/com/github/jreddit/parser/listing/RedditListingParser.java create mode 100644 src/main/com/github/jreddit/parser/listing/SubmissionsListingParser.java create mode 100644 src/main/com/github/jreddit/parser/listing/SubredditsListingParser.java create mode 100644 src/main/com/github/jreddit/parser/single/FullSubmissionParser.java create mode 100644 src/main/com/github/jreddit/parser/util/CommentTreeUtils.java create mode 100644 src/main/com/github/jreddit/parser/util/JsonUtils.java create mode 100644 src/main/com/github/jreddit/request/RedditGetRequest.java create mode 100644 src/main/com/github/jreddit/request/RedditPostRequest.java create mode 100644 src/main/com/github/jreddit/request/action/MarkActionRequest.java create mode 100644 src/main/com/github/jreddit/request/action/flair/DeleteFlairRequest.java create mode 100644 src/main/com/github/jreddit/request/action/mark/HideRequest.java create mode 100644 src/main/com/github/jreddit/request/action/mark/MarkNsfwRequest.java create mode 100644 src/main/com/github/jreddit/request/action/mark/ReportRequest.java create mode 100644 src/main/com/github/jreddit/request/action/mark/SaveRequest.java create mode 100644 src/main/com/github/jreddit/request/action/mark/UnhideRequest.java create mode 100644 src/main/com/github/jreddit/request/action/mark/UnmarkNsfwRequest.java create mode 100644 src/main/com/github/jreddit/request/action/mark/UnsaveRequest.java create mode 100644 src/main/com/github/jreddit/request/action/mark/VoteRequest.java create mode 100644 src/main/com/github/jreddit/request/retrieval/ListingRequest.java create mode 100644 src/main/com/github/jreddit/request/retrieval/comments/CommentsOfUserRequest.java create mode 100644 src/main/com/github/jreddit/request/retrieval/comments/MoreCommentsRequest.java create mode 100644 src/main/com/github/jreddit/request/retrieval/mixed/FullSubmissionRequest.java create mode 100644 src/main/com/github/jreddit/request/retrieval/mixed/MixedOfUserRequest.java create mode 100644 src/main/com/github/jreddit/request/retrieval/param/CommentSort.java create mode 100644 src/main/com/github/jreddit/request/retrieval/param/QuerySyntax.java create mode 100644 src/main/com/github/jreddit/request/retrieval/param/SearchSort.java create mode 100644 src/main/com/github/jreddit/request/retrieval/param/SubmissionSort.java create mode 100644 src/main/com/github/jreddit/request/retrieval/param/SubredditsView.java create mode 100644 src/main/com/github/jreddit/request/retrieval/param/TimeSpan.java create mode 100644 src/main/com/github/jreddit/request/retrieval/param/UserMixedCategory.java create mode 100644 src/main/com/github/jreddit/request/retrieval/param/UserOverviewSort.java create mode 100644 src/main/com/github/jreddit/request/retrieval/param/UserSubmissionsCategory.java create mode 100644 src/main/com/github/jreddit/request/retrieval/submissions/SubmissionsOfSubredditRequest.java create mode 100644 src/main/com/github/jreddit/request/retrieval/submissions/SubmissionsOfUserRequest.java create mode 100644 src/main/com/github/jreddit/request/retrieval/submissions/SubmissionsSearchRequest.java create mode 100644 src/main/com/github/jreddit/request/retrieval/subreddits/SubredditsOfUserRequest.java create mode 100644 src/main/com/github/jreddit/request/retrieval/subreddits/SubredditsSearchRequest.java create mode 100644 src/main/com/github/jreddit/request/util/KeyValueFormatter.java diff --git a/build.gradle b/build.gradle index 936e275..833fef7 100644 --- a/build.gradle +++ b/build.gradle @@ -4,7 +4,14 @@ repositories { apply plugin: "java" dependencies { - compile "com.github.jreddit:jreddit:1.0.3" + //compile "com.github.jreddit:jreddit:1.0.3" //not supported by reddit api anymore + //jreddit 1.0.4 dependencies + compile group: 'com.googlecode.json-simple', name: 'json-simple', version: '1.1.1' + compile group: 'org.apache.httpcomponents', name: 'httpclient', version: '4.3.3' + compile group: 'org.apache.oltu.oauth2', name: 'org.apache.oltu.oauth2.client', version: '1.0.0' + testCompile group: 'commons-validator', name: 'commons-validator', version: '1.4.1' + testCompile group: 'junit', name: 'junit', version: '4.8.1' + testCompile group: 'org.mockito', name: 'mockito-core', version: '1.9.5' } sourceSets { diff --git a/src/main/com/github/jreddit/oauth/RedditOAuthAgent.java b/src/main/com/github/jreddit/oauth/RedditOAuthAgent.java new file mode 100644 index 0000000..2730230 --- /dev/null +++ b/src/main/com/github/jreddit/oauth/RedditOAuthAgent.java @@ -0,0 +1,370 @@ +package com.github.jreddit.oauth; + +import java.util.HashMap; +import java.util.Map; +import java.util.UUID; + +import javax.xml.bind.DatatypeConverter; + +import org.apache.oltu.oauth2.client.OAuthClient; +import org.apache.oltu.oauth2.client.URLConnectionClient; +import org.apache.oltu.oauth2.client.request.OAuthClientRequest; +import org.apache.oltu.oauth2.client.request.OAuthClientRequest.TokenRequestBuilder; +import org.apache.oltu.oauth2.common.OAuthProviderType; +import org.apache.oltu.oauth2.common.exception.OAuthProblemException; +import org.apache.oltu.oauth2.common.exception.OAuthSystemException; +import org.apache.oltu.oauth2.common.message.types.GrantType; + +import com.github.jreddit.oauth.app.RedditApp; +import com.github.jreddit.oauth.exception.RedditOAuthException; +import com.github.jreddit.oauth.param.RedditDuration; +import com.github.jreddit.oauth.param.RedditScopeBuilder; +import com.github.jreddit.request.util.KeyValueFormatter; + +/** + * Thread-safe reddit OAuth agent.
+ *
+ * Communicates with reddit to retrieve tokens, and converts them + * into {@link RedditToken}s, which are used internally by jReddit. This class + * supports both the code grant flow and implicit grant flow. + * + * @author Simon Kassing + */ +public class RedditOAuthAgent { + + /** Reddit authorization endpoint. */ + private static final String REDDIT_AUTHORIZE = "https://www.reddit.com/api/v1/authorize?"; + + /** Grant type for an installed client (weirdly enough a URI). */ + private static final String GRANT_TYPE_INSTALLED_CLIENT = "https://oauth.reddit.com/grants/installed_client"; + + /** Grant type for client credentials (described in OAuth2 standard). */ + private static final String GRANT_TYPE_CLIENT_CREDENTIALS = "client_credentials"; + + /* Parameter keys */ + private static final String PARAM_CLIENT_ID = "client_id"; + private static final String PARAM_RESPONSE_TYPE = "response_type"; + private static final String PARAM_STATE = "state"; + private static final String PARAM_REDIRECT_URI = "redirect_uri"; + private static final String PARAM_DURATION = "duration"; + private static final String PARAM_SCOPE = "scope"; + private static final String PARAM_GRANT_TYPE = "grant_type"; + private static final String PARAM_CODE = "code"; + private static final String PARAM_DEVICE_ID = "device_id"; + + /* Header keys */ + private static final String HEADER_USER_AGENT = "User-Agent"; + private static final String HEADER_AUTHORIZATION = "Authorization"; + + /** User agent. */ + private final String userAgent; + + /** OAuth2 client for OAuth related requests. */ + private OAuthClient oAuthClient; + + /** Reddit application. */ + private RedditApp redditApp; + + /** + * Constructor for a Reddit OAuth agent.
+ *
+ * A default Apache OAuthClient will be made to perform the OAuth communication. + * + * @param userAgent User agent for your application (e.g. "jReddit: Reddit API Wrapper for Java") + * @param redditApp Reddit application + */ + public RedditOAuthAgent(String userAgent, RedditApp redditApp) { + this(userAgent, redditApp, new OAuthClient(new URLConnectionClient())); + } + + /** + * Constructor for a Reddit OAuth agent. + * + * @param userAgent User agent for your application (e.g. "jReddit: Reddit API Wrapper for Java") + * @param redditApp Reddit application + * @param oAuthClient Apache OAuth2 client + */ + public RedditOAuthAgent(String userAgent, RedditApp redditApp, OAuthClient oAuthClient) { + this.userAgent = userAgent; + this.redditApp = redditApp; + this.oAuthClient = oAuthClient; + } + + /** + * Generate the code flow Uniform Resource Locator (URI) for a + * reddit user to authorize your application.
+ *
+ * The user will, after authorization, receive a code. This can be turned into + * a RedditToken using {@link #token(String)}. + * + * @param scopeBuilder Authorization scope builder (must not be null) + * @param duration Duration that the token can last + * + * @return The URI users need to visit and retrieve the code from + * + * @see {@link #token(String)} for converting the code into a usable RedditToken + */ + public synchronized String generateCodeFlowURI(RedditScopeBuilder scopeBuilder, RedditDuration duration) { + + // Set parameters + Map params = new HashMap(); + params.put(PARAM_CLIENT_ID, redditApp.getClientID()); + params.put(PARAM_RESPONSE_TYPE, "code"); + params.put(PARAM_STATE, UUID.randomUUID().toString()); + params.put(PARAM_REDIRECT_URI, redditApp.getRedirectURI()); + params.put(PARAM_DURATION, duration.value()); + params.put(PARAM_SCOPE, scopeBuilder.build()); + + // Create URI + return REDDIT_AUTHORIZE + KeyValueFormatter.format(params, true); + + } + + /** + * Generate the implicit flow Uniform Resource Locator (URI) for a + * reddit user to authorize your application.
+ *
+ * The user will, after authorization, receive token information. This can be turned into + * a RedditToken using {@link #tokenFromInfo(String, String, long, String)}. + * + * @param scopeBuilder Authorization scope builder (must not be null) + * + * @return The URI users need to visit and retrieve the token information from + * + * @see {@link #tokenFromInfo(String, String, long, String)} for converting the + * token information into RedditToken + */ + public synchronized String generateImplicitFlowURI(RedditScopeBuilder scopeBuilder) { + + // Set parameters + Map params = new HashMap(); + params.put(PARAM_CLIENT_ID, redditApp.getClientID()); + params.put(PARAM_RESPONSE_TYPE, "token"); + params.put(PARAM_STATE, UUID.randomUUID().toString()); + params.put(PARAM_REDIRECT_URI, redditApp.getRedirectURI()); + params.put(PARAM_SCOPE, scopeBuilder.build()); + + // Create URI + return REDDIT_AUTHORIZE + KeyValueFormatter.format(params, true); + + } + + /** + * Add a user agent to the OAuth request. + * + * @param request OAuth request + */ + private void addUserAgent(OAuthClientRequest request) { + request.addHeader(HEADER_USER_AGENT, userAgent); + } + + /** + * Add the basic authentication protocol to the OAuth request using + * the credentials of the Reddit application provided. + * + * @param request OAuth request + * @param app Reddit application + */ + private void addBasicAuthentication(OAuthClientRequest request, RedditApp app) { + String authString = app.getClientID() + ":" + app.getClientSecret(); + String authStringEnc = DatatypeConverter.printBase64Binary(authString.getBytes()); + request.addHeader(HEADER_AUTHORIZATION, "Basic " + authStringEnc); + } + + /** + * Token retrieval (code flow).
+ *
+ * Retrieve a token for a specific user, meaning that the token is coupled to a user. + * After it has expired, the token will no longer work. You must either request a new + * token, or refresh it using {@link #refreshToken(RedditToken)}. + * + * @param code One-time code received from the user, after manual authorization by that user + * + * @return Token (associated with a user) + * + * @throws RedditOAuthException + */ + public synchronized RedditToken token(String code) throws RedditOAuthException { + + try { + + // Set general values of the request + OAuthClientRequest request = OAuthClientRequest + .tokenProvider(OAuthProviderType.REDDIT) + .setGrantType(GrantType.AUTHORIZATION_CODE) + .setClientId(redditApp.getClientID()) + .setClientSecret(redditApp.getClientSecret()) + .setRedirectURI(redditApp.getRedirectURI()) + .setParameter(PARAM_CODE, code) + .buildBodyMessage(); + + // Add the user agent + addUserAgent(request); + + // Add basic authentication + addBasicAuthentication(request, redditApp); + + // Return a wrapper controlled by jReddit + return new RedditToken(oAuthClient.accessToken(request)); + + } catch (OAuthSystemException oase) { + throw new RedditOAuthException(oase); + } catch (OAuthProblemException oape) { + throw new RedditOAuthException(oape); + } + + } + + /** + * Refresh token.
+ *
+ * This is only possible for tokens retrieved through the code flow + * authorization and had their duration set to permanent. Tokens that do not have + * a refresh_token with them or are expired, will not be able to be refreshed. + * In that case, a new one must be acquired. + * + * @param rToken Reddit token (which needs to be refreshed) + * + * @return Whether the token was successfully refreshed + * + * @throws RedditOAuthException + * + * @see RedditToken#isRefreshable() + */ + public synchronized boolean refreshToken(RedditToken rToken) throws RedditOAuthException { + + try { + + // Check whether the token can be refreshed + if (rToken.isRefreshable()) { + + // Set general values of the request + OAuthClientRequest request = OAuthClientRequest + .tokenProvider(OAuthProviderType.REDDIT) + .setGrantType(GrantType.REFRESH_TOKEN) + .setRefreshToken(rToken.getRefreshToken()) + .buildBodyMessage(); + + // Add the user agent + addUserAgent(request); + + // Add basic authentication + addBasicAuthentication(request, redditApp); + + // Return a wrapper controlled by jReddit + rToken.refresh(oAuthClient.accessToken(request)); + + return true; + + } else { + + // The token cannot be refreshed + return false; + + } + + + } catch (OAuthSystemException oase) { + throw new RedditOAuthException(oase); + } catch (OAuthProblemException oape) { + throw new RedditOAuthException(oape); + } + + } + + /** + * Token retrieval (app-only flow).
+ *
+ * Retrieve a token for the application-only, meaning that the + * token is not coupled to any user. The token is typically only + * valid for a short period of time (at the moment of writing: 1 hour). + * After it has expired, the token will no longer work. You must request a new + * token in that case. Refreshing an application-only token is not possible. + * + * @param confidential True: confidential clients (web apps / scripts) not acting on + * behalf of one or more logged out users. False: installed app types, + * and other apps acting on behalf of one or more logged out users. + * + * @return Token (not associated with a user) + * + * @throws RedditOAuthException + */ + public synchronized RedditToken tokenAppOnly(boolean confidential) throws RedditOAuthException { + + try { + + // Set general values of the request + TokenRequestBuilder builder = OAuthClientRequest + .tokenProvider(OAuthProviderType.REDDIT) + .setParameter(PARAM_GRANT_TYPE, confidential ? GRANT_TYPE_CLIENT_CREDENTIALS : GRANT_TYPE_INSTALLED_CLIENT) + .setClientId(redditApp.getClientID()) + .setClientSecret(redditApp.getClientSecret()); + + // If it is not acting on behalf of a unique client, a unique device identifier must be generated: + if (!confidential) { + builder = builder.setParameter(PARAM_DEVICE_ID, UUID.randomUUID().toString()); + } + + // Build the request + OAuthClientRequest request = builder.buildBodyMessage(); + + // Add the user agent + addUserAgent(request); + + // Add basic authentication + addBasicAuthentication(request, redditApp); + + // Return a wrapper controlled by jReddit + return new RedditToken(oAuthClient.accessToken(request)); + + } catch (OAuthSystemException oase) { + throw new RedditOAuthException(oase); + } catch (OAuthProblemException oape) { + throw new RedditOAuthException(oape); + } + + } + + /** + * Generate a token from information received using the implicit grant flow.
+ *
+ * WARNING: The expiration of the token is no longer very accurate. There is a delay + * between the user receiving the token, and inputting it into this function. Beware that the + * token might expire earlier than that the token reports it to. + * + * @param accessToken Access token + * @param tokenType Token type (commonly "bearer") + * @param expiresIn Expires in (seconds) + * @param scope Scope + * + * @return RedditToken generated using the given information. + */ + public synchronized RedditToken tokenFromInfo(String accessToken, String tokenType, long expiresIn, String scope) { + return new RedditToken(accessToken, tokenType, expiresIn, scope); + } + + /** + * Revocation of a RedditToken.
+ *
+ * Be sure to not use the token after + * calling this function, as its state pertaining its validity (e.g. scope, + * expiration, refreshability) is no longer valid when it is revoked.
+ *
+ * Note: Per RFC 7009, this request will return a success (204) response + * even if the passed in token was never valid. + * + * @param token RedditToken to revoke + * @param revokeAccessTokenOnly Whether to only revoke the access token, or both + * + * @return Whether the token is no longer valid + */ + public boolean revoke(RedditToken token, boolean revokeAccessTokenOnly) { + // TODO: Implement + // https://www.reddit.com/api/v1/revoke_token + // In POST data: token=TOKEN&token_type_hint=TOKEN_TYPE + // TOKEN_TYPE: refresh_token or access_token + // + return true; + } + +} diff --git a/src/main/com/github/jreddit/oauth/RedditToken.java b/src/main/com/github/jreddit/oauth/RedditToken.java new file mode 100644 index 0000000..8f41e55 --- /dev/null +++ b/src/main/com/github/jreddit/oauth/RedditToken.java @@ -0,0 +1,187 @@ +package com.github.jreddit.oauth; + +import org.apache.oltu.oauth2.client.response.OAuthJSONAccessTokenResponse; + +import com.github.jreddit.oauth.param.RedditScope; +import com.github.jreddit.oauth.param.RedditTokenCompleteScope; + +/** + * OAuth2 token wrapper for reddit.
+ *
+ * This class wraps the information received from reddit following + * the request for a token.
+ * A token has three dimensions: + *
    + *
  • scope: what can it be used for? + *
  • expiration: how long can it be used? + *
  • refreshable: can its duration be prolonged? + *
+ * + * @author Simon Kassing + * + */ +public class RedditToken { + + /** Token type parameter. */ + public static final String PARAM_TOKEN_TYPE = "token_type"; + + /** Access token. */ + private String accessToken; + + /** Refresh token. */ + private String refreshToken; + + /** Manager of the scopes that this token applies to. */ + private RedditTokenCompleteScope scopes; + + /** Token type. Only value possible (15-06-2015): bearer */ + private String tokenType; + + /** Time at which the token expires (seconds since UNIX epoch). */ + private long expiration; + + /** How long the token was valid starting from its creation (in seconds). */ + private long expirationSpan; + + /** + * @param token JSON response after an OAuth2 token request + */ + protected RedditToken(OAuthJSONAccessTokenResponse token) { + this.accessToken = token.getAccessToken(); + this.refreshToken = token.getRefreshToken(); + this.expiration = currentTimeSeconds() + token.getExpiresIn(); + this.expirationSpan = token.getExpiresIn(); + this.scopes = new RedditTokenCompleteScope(token.getScope()); + this.tokenType = token.getParam(PARAM_TOKEN_TYPE); + } + + /** + * Reddit token constructor with specific user-provided parameters. + * + * @param accessToken Access token + * @param tokenType Type of token (commonly "bearer") + * @param expiresIn Expires in (seconds) + * @param scope Scope + */ + protected RedditToken(String accessToken, String tokenType, long expiresIn, String scope) { + this.accessToken = accessToken; + this.refreshToken = null; + this.expiration = currentTimeSeconds() + expiresIn; + this.expirationSpan = expiresIn; + this.scopes = new RedditTokenCompleteScope(scope); + this.tokenType = tokenType; + } + + /** + * Refresh this reddit token with data received from the new token. + * + * @param token Token received from a refresh request to reddit + */ + public void refresh(OAuthJSONAccessTokenResponse token) { + this.accessToken = token.getAccessToken(); + this.expiration = currentTimeSeconds() + token.getExpiresIn(); + this.expirationSpan = token.getExpiresIn(); + this.scopes = new RedditTokenCompleteScope(token.getScope()); + this.tokenType = token.getParam(PARAM_TOKEN_TYPE); + } + + /** + * Retrieve the access token. + * + * @return Access token (e.g. "jsdkjfskaj9f8-dnafk") + */ + public String getAccessToken() { + return accessToken; + } + + /** + * Retrieve the refresh token. + * + * @return Refresh token (e.g. "nvkJu9kjdada2-d98afj") + */ + public String getRefreshToken() { + return refreshToken; + } + + /** + * Retrieve whether the token is expired. + * + * @return Is the token expired? + */ + public boolean isExpired() { + return expiration < currentTimeSeconds(); + } + + /** + * Check whether this token possess this particular authorization scope. + * If it does not support the scope, it means that the token does + * not have approval from the user to perform the actions belonging to + * that scope. + * + * @param scope Reddit scope + * + * @return Does this token support this scope? + */ + public boolean hasScope(RedditScope scope) { + return this.scopes.has(scope); + } + + /** + * Retrieve the expiration. + * + * @return Expiration time (seconds since UNIX epoch) + */ + public long getExpiration() { + return expiration; + } + /** + * Retrieve the token type. + * + * @return Token type (e.g. "bearer") + */ + public String getTokenType() { + return tokenType; + } + + /** + * Check whether the token can be refreshed. + * This means that a refresh token exists. + * + * @return Can the token be refreshed? + */ + public boolean isRefreshable() { + return this.getRefreshToken() != null; + } + + /** + * Check whether this token will expire within the given time frame (given in seconds). + * + * @param seconds Amount of seconds + * + * @return Will the token expire within the given time frame? + */ + public boolean willExpireIn(long seconds) { + return currentTimeSeconds() + seconds > expiration; + } + + /** + * Retrieve the original expiration span of the token starting from its creation. + * + * @return Expiration span (in seconds) + */ + public long getExpirationSpan() { + return expirationSpan; + } + + /** + * Retrieve the current time in seconds. + * + * Uses System.currentTimeMillis(). + * + * @return Current time in seconds. + */ + private static long currentTimeSeconds() { + return System.currentTimeMillis() / (long) 1000; + } + +} diff --git a/src/main/com/github/jreddit/oauth/app/RedditApp.java b/src/main/com/github/jreddit/oauth/app/RedditApp.java new file mode 100644 index 0000000..9a09083 --- /dev/null +++ b/src/main/com/github/jreddit/oauth/app/RedditApp.java @@ -0,0 +1,52 @@ +package com.github.jreddit.oauth.app; + +public abstract class RedditApp { + + private final String clientID; + private final String clientSecret; + private final String redirectURI; + + /** + * Reddit Application.
+ *
+ * All information given in this constructor must + * match the information stated on reddit. + * + * @param clientID Client identifier (e.g. "p_jcolKysdMFud") + * @param clientSecret Client secret (e.g. "gko_LXEJKF89djs98fhFJkj9s") + * @param redirectURI Redirect URI (e.g. "http://www.example.com/auth") + */ + public RedditApp(String clientID, String clientSecret, String redirectURI) { + this.clientID = clientID; + this.clientSecret = clientSecret; + this.redirectURI = redirectURI; + } + + /** + * Retrieve client identifier. + * + * @return Client identifier (e.g. "p_jcolKysdMFud") + */ + public String getClientID() { + return clientID; + } + + /** + * Retrieve client secret. + * + * @return Client secret (e.g. "gko_LXEJKF89djs98fhFJkj9s") + */ + public String getClientSecret() { + return clientSecret; + } + + /** + * Retrieve redirect Uniform Resource Identifier. + * + * @return Redirect URI (e.g. "http://www.example.com/auth") + */ + public String getRedirectURI() { + return redirectURI; + } + +} diff --git a/src/main/com/github/jreddit/oauth/app/RedditInstalledApp.java b/src/main/com/github/jreddit/oauth/app/RedditInstalledApp.java new file mode 100644 index 0000000..7f1217a --- /dev/null +++ b/src/main/com/github/jreddit/oauth/app/RedditInstalledApp.java @@ -0,0 +1,19 @@ +package com.github.jreddit.oauth.app; + +public class RedditInstalledApp extends RedditApp { + + /** + * Reddit Installed Application.
+ *
+ * All information given in this constructor must + * match the information stated on reddit. The secret is defaulted + * to an empty string. + * + * @param clientID Client identifier (e.g. "p_jcolKysdMFud") + * @param redirectURI Redirect URI (e.g. "http://www.example.com/auth") + */ + public RedditInstalledApp(String clientID, String redirectURI) { + super(clientID, "", redirectURI); // Empty string is the secret for an installed app + } + +} diff --git a/src/main/com/github/jreddit/oauth/app/RedditScriptApp.java b/src/main/com/github/jreddit/oauth/app/RedditScriptApp.java new file mode 100644 index 0000000..295f792 --- /dev/null +++ b/src/main/com/github/jreddit/oauth/app/RedditScriptApp.java @@ -0,0 +1,19 @@ +package com.github.jreddit.oauth.app; + +public class RedditScriptApp extends RedditApp { + + /** + * Reddit Script Application.
+ *
+ * All information given in this constructor must + * match the information stated on reddit. + * + * @param clientID Client identifier (e.g. "p_jcolKysdMFud") + * @param clientSecret Client secret (e.g. "gko_LXEJKF89djs98fhFJkj9s") + * @param redirectURI Redirect URI (e.g. "http://www.example.com/auth") + */ + public RedditScriptApp(String clientID, String clientSecret, String redirectURI) { + super(clientID, clientSecret, redirectURI); + } + +} diff --git a/src/main/com/github/jreddit/oauth/app/RedditWebApp.java b/src/main/com/github/jreddit/oauth/app/RedditWebApp.java new file mode 100644 index 0000000..e3bbc7f --- /dev/null +++ b/src/main/com/github/jreddit/oauth/app/RedditWebApp.java @@ -0,0 +1,19 @@ +package com.github.jreddit.oauth.app; + +public class RedditWebApp extends RedditApp { + + /** + * Reddit Web Application.
+ *
+ * All information given in this constructor must + * match the information stated on reddit. + * + * @param clientID Client identifier (e.g. "p_jcolKysdMFud") + * @param clientSecret Client secret (e.g. "gko_LXEJKF89djs98fhFJkj9s") + * @param redirectURI Redirect URI (e.g. "http://www.example.com/auth") + */ + public RedditWebApp(String clientID, String clientSecret, String redirectURI) { + super(clientID, clientSecret, redirectURI); + } + +} diff --git a/src/main/com/github/jreddit/oauth/client/RedditClient.java b/src/main/com/github/jreddit/oauth/client/RedditClient.java new file mode 100644 index 0000000..aeede20 --- /dev/null +++ b/src/main/com/github/jreddit/oauth/client/RedditClient.java @@ -0,0 +1,47 @@ +package com.github.jreddit.oauth.client; + +import com.github.jreddit.oauth.RedditToken; +import com.github.jreddit.request.RedditGetRequest; +import com.github.jreddit.request.RedditPostRequest; + +public abstract class RedditClient { + + /** API Domain of OAuth */ + public static final String OAUTH_API_DOMAIN = "https://oauth.reddit.com"; + + /** + * Perform a POST reddit request authenticated with the given reddit token.
+ *
+ * Does the following: (a) generates the URI (including query parameters) and appends + * it to the reddit base, (b) adds the POST body parameters, + * (c) adds the authorization from the token to the request, and + * (d) executes the request.
+ *
+ * Exception handling: if any function raises an exception, + * it will be logged using SLF4J. The result would be null. + * + * @param rToken Reddit token + * @param request Reddit POST request + * + * @return Response from reddit (raw), if failed null + */ + public abstract String post(RedditToken rToken, RedditPostRequest request); + + /** + * Perform a GET reddit request authenticated with the given reddit token.
+ *
+ * Does the following: (a) generates the URI (including query parameters) and appends + * it to the reddit base, (b) adds the authorization from the token to + * the request, and (c) executes the request.
+ *
+ * Exception handling: if any function raises an exception, + * it will be logged using SLF4J. The result would be null. + * + * @param rToken Reddit token + * @param request Reddit GET request + * + * @return Response from reddit (raw), if failed null + */ + public abstract String get(RedditToken rToken, RedditGetRequest request); + +} diff --git a/src/main/com/github/jreddit/oauth/client/RedditHttpClient.java b/src/main/com/github/jreddit/oauth/client/RedditHttpClient.java new file mode 100644 index 0000000..512f059 --- /dev/null +++ b/src/main/com/github/jreddit/oauth/client/RedditHttpClient.java @@ -0,0 +1,141 @@ +package com.github.jreddit.oauth.client; + +import java.io.IOException; +import java.io.UnsupportedEncodingException; + +import org.apache.http.HttpRequest; +import org.apache.http.HttpResponse; +import org.apache.http.client.ClientProtocolException; +import org.apache.http.client.HttpClient; +import org.apache.http.client.methods.HttpGet; +import org.apache.http.client.methods.HttpPost; +import org.apache.http.client.methods.HttpUriRequest; +import org.apache.http.entity.StringEntity; +import org.apache.http.util.EntityUtils; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import com.github.jreddit.oauth.RedditToken; +import com.github.jreddit.request.RedditGetRequest; +import com.github.jreddit.request.RedditPostRequest; + +/** + * HTTP client implementation for a RedditClient. + * + * @author Simon Kassing + * + * @see RedditClient + */ +public class RedditHttpClient extends RedditClient { + + private static final Logger LOGGER = LoggerFactory.getLogger(RedditHttpClient.class); + + private final String userAgent; + + private final HttpClient httpClient; + + /** + * @param userAgent User agent of your application + * @param httpClient HTTP client to use for the requests + */ + public RedditHttpClient(String userAgent, HttpClient httpClient) { + this.userAgent = userAgent; + this.httpClient = httpClient; + } + + @Override + public String post(RedditToken rToken, RedditPostRequest redditRequest) { + + try { + + // Create post request + HttpPost request = new HttpPost(OAUTH_API_DOMAIN + redditRequest.generateRedditURI()); + + // Add parameters to body + request.setEntity(new StringEntity(redditRequest.generateBody())); + + // Add authorization + addAuthorization(request, rToken); + + // Add user agent + addUserAgent(request); + + // Add content type + request.addHeader("Content-Type", "application/x-www-form-urlencoded; charset=UTF-8"); + + return executeHttpRequest(request); + + } catch (UnsupportedEncodingException uee) { + LOGGER.warn("Unsupported Encoding Exception thrown in POST request when encoding body", uee); + } + + return null; + + } + + @Override + public String get(RedditToken rToken, RedditGetRequest redditRequest) { + + // Create get request + HttpGet request = new HttpGet(OAUTH_API_DOMAIN + redditRequest.generateRedditURI()); + + // Add authorization + addAuthorization(request, rToken); + + // Add user agent + addUserAgent(request); + + return executeHttpRequest(request); + + } + + /** + * Execute the given HTTP request. + * + * @param request HTTP request + * + * @return Result, null if failed + */ + private String executeHttpRequest(HttpUriRequest request) { + try { + + // Attempt to do execute request + HttpResponse response = httpClient.execute(request); + + // Return response if successful + if (response != null) { + return EntityUtils.toString(response.getEntity(), "UTF-8"); + } + + } catch (UnsupportedEncodingException uee) { + LOGGER.warn("Unsupported Encoding Exception thrown in request", uee); + } catch (ClientProtocolException cpe) { + LOGGER.warn("Client Protocol Exception thrown in request", cpe); + } catch (IOException ioe) { + LOGGER.warn("I/O Exception thrown in request", ioe); + } + + return null; + + } + + /** + * Add authorization to the HTTP request. + * + * @param request HTTP request + * @param rToken Reddit token (generally of the "bearer" type) + */ + private void addAuthorization(HttpRequest request, RedditToken rToken) { + request.addHeader("Authorization", rToken.getTokenType() + " " + rToken.getAccessToken()); + } + + /** + * Add user agent to the HTTP request. + * + * @param request HTTP request + */ + private void addUserAgent(HttpRequest request) { + request.addHeader("User-Agent", userAgent); + } + +} diff --git a/src/main/com/github/jreddit/oauth/client/RedditPoliteClient.java b/src/main/com/github/jreddit/oauth/client/RedditPoliteClient.java new file mode 100644 index 0000000..5bfeea8 --- /dev/null +++ b/src/main/com/github/jreddit/oauth/client/RedditPoliteClient.java @@ -0,0 +1,101 @@ +package com.github.jreddit.oauth.client; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import com.github.jreddit.oauth.RedditToken; +import com.github.jreddit.request.RedditGetRequest; +import com.github.jreddit.request.RedditPostRequest; + +/** + * Wrapper for any reddit client, which makes it polite. + * Polite means that it will only send requests in an interval, + * and does not overload reddit. It will also mean you will get + * less denial messages from reddit *hint* *hint*. + * + * @author Simon Kassing + */ +public class RedditPoliteClient extends RedditClient { + + /** Logger for this class. */ + private static final Logger LOGGER = LoggerFactory.getLogger(RedditPoliteClient.class); + + /** Wrapped reddit client. */ + private RedditClient redditClient; + + /** Default interval in milliseconds (1 per second). */ + private static final long DEFAULT_INTERVAL = 1000L; + + /** Default interval in milliseconds. */ + private long interval; + + /** Last time a request was made. */ + private long lastReqTime = 0; + + /** + * Polite wrapper around the reddit client. + * + * @param redditClient Reddit client to wrap + */ + public RedditPoliteClient(RedditClient redditClient) { + this(redditClient, DEFAULT_INTERVAL); + } + + /** + * Polite wrapper around the reddit client with configurable time. + * + * @param redditClient Reddit client to wrap + * @param interval Interval in milliseconds + */ + public RedditPoliteClient(RedditClient redditClient, long interval) { + this.redditClient = redditClient; + this.interval = interval; + } + + @Override + public String post(RedditToken rToken, RedditPostRequest request) { + waitIfNeeded(); + String result = redditClient.post(rToken, request); + noteTime(); + return result; + } + + @Override + public String get(RedditToken rToken, RedditGetRequest request) { + waitIfNeeded(); + String result = redditClient.get(rToken, request); + noteTime(); + return result; + } + + /** + * Note the current time. + */ + private void noteTime() { + lastReqTime = System.currentTimeMillis(); + } + + /** + * Wait if required. + */ + private void waitIfNeeded() { + + // Calculate elapsed milliseconds + long elapsed = System.currentTimeMillis() - lastReqTime; + + // If enough time has elapsed, no need to wait + if (elapsed >= interval) { + return; + } + + // If not enough time was elapsed, wait the remainder + long toWait = interval - elapsed; + try { + Thread.sleep(toWait); + } catch (InterruptedException ie) { + LOGGER.warn("Interrupted Exception thrown while politely waiting for remainder of interval", ie); + } + + } + +} diff --git a/src/main/com/github/jreddit/oauth/exception/RedditOAuthException.java b/src/main/com/github/jreddit/oauth/exception/RedditOAuthException.java new file mode 100644 index 0000000..cbde8eb --- /dev/null +++ b/src/main/com/github/jreddit/oauth/exception/RedditOAuthException.java @@ -0,0 +1,18 @@ +package com.github.jreddit.oauth.exception; + +import org.apache.oltu.oauth2.common.exception.OAuthProblemException; +import org.apache.oltu.oauth2.common.exception.OAuthSystemException; + +public class RedditOAuthException extends Exception { + + private static final long serialVersionUID = 2403104136770312353L; + + public RedditOAuthException(OAuthSystemException e) { + super("A OAuth system exception was thrown when authenticating with reddit.", e); + } + + public RedditOAuthException(OAuthProblemException e) { + super("A OAuth problem exception was thrown when authenticating with reddit.", e); + } + +} diff --git a/src/main/com/github/jreddit/oauth/param/RedditDuration.java b/src/main/com/github/jreddit/oauth/param/RedditDuration.java new file mode 100644 index 0000000..fd60cc1 --- /dev/null +++ b/src/main/com/github/jreddit/oauth/param/RedditDuration.java @@ -0,0 +1,27 @@ +package com.github.jreddit.oauth.param; + +/** + * Enumerator for the duration of tokens.
+ *
+ * There are two possible values: + *
    + *
  • permanent: The token can be refreshed as many times as the application desires.
  • + *
  • temporary: The token cannot be refreshed, and will last for one hour.
  • + *
+ */ +public enum RedditDuration { + + PERMANENT("permanent"), + TEMPORARY("temporary"); + + private final String value; + + RedditDuration(String value) { + this.value = value; + } + + public String value() { + return this.value; + } + +} diff --git a/src/main/com/github/jreddit/oauth/param/RedditScope.java b/src/main/com/github/jreddit/oauth/param/RedditScope.java new file mode 100644 index 0000000..c20767c --- /dev/null +++ b/src/main/com/github/jreddit/oauth/param/RedditScope.java @@ -0,0 +1,48 @@ +package com.github.jreddit.oauth.param; + +/** + * Enumerator for the possible authorization scopes for reddit requests.
+ *
+ * These scopes are used to specify what actions an application + * can perform with the generated token. The scope can be combined + * together, so that a single token can have access to multiple scopes. + * + * @see RedditScopeBuilder + * + * @author Simon Kassing + */ +public enum RedditScope { + + IDENTITY("identity"), + EDIT("edit"), + FLAIR("flair"), + HISTORY("history"), + MODCONFIG("modconfig"), + MODFLAIR("modflair"), + MODLOG("modlog"), + MODPOSTS("modposts"), + MODWIKI("modwiki"), + MYSUBREDDITS("mysubreddits"), + PRIVATEMESSAGE("privatemessages"), + READ("read"), + REPORT("report"), + SAVE("save"), + SUBMIT("submit"), + SUBSCRIBE("subscribe"), + VOTE("vote"), + WIKIEDIT("wikiedit"), + WIKIREAD("wikiread"); + + protected static final String SEPARATOR = ","; + + private final String value; + + RedditScope(String value) { + this.value = value; + } + + public String value() { + return this.value; + } + +} diff --git a/src/main/com/github/jreddit/oauth/param/RedditScopeBuilder.java b/src/main/com/github/jreddit/oauth/param/RedditScopeBuilder.java new file mode 100644 index 0000000..cf135e7 --- /dev/null +++ b/src/main/com/github/jreddit/oauth/param/RedditScopeBuilder.java @@ -0,0 +1,100 @@ +package com.github.jreddit.oauth.param; + +import java.util.HashSet; +import java.util.Set; + +/** + * Builder for the scopes of a request. + * + * @author Simon Kassing + * + */ +public class RedditScopeBuilder { + + /** Set of reddit scopes. */ + private Set scopes; + + public RedditScopeBuilder() { + scopes = new HashSet(); + } + + /** + * Build a string for the request. Called upon by + * RedditToken when requesting a token that + * allows scope definition. + * + * @return String list of the scopes + */ + public String build() { + + // Add each scope to the scope list + String s = ""; + for (RedditScope scope : scopes) { + s += scope.value() + RedditScope.SEPARATOR; + } + + // Remove final separator + if (s.length() > 0) { + s = s.substring(0, s.length() - RedditScope.SEPARATOR.length()); + } + + // Return scope list + return s; + + } + + /** + * Add a scope to the builder. If the scope has already + * been added, it will not be added double. + * + * @param scope Reddit scope + * + * @return This builder + */ + public RedditScopeBuilder addScope(RedditScope scope) { + scopes.add(scope); + return this; + } + + /** + * Add multiple scopes to the builder. If a scope has already + * been added, it will not be added double. + * + * @param scopes Multiple scopes + * + * @return This builder + */ + public RedditScopeBuilder addScopes(RedditScope... scopes) { + for (RedditScope scope : scopes) { + addScope(scope); + } + return this; + } + + /** + * Remove a scope from the builder. + * + * @param scope Reddit scope + * + * @return This builder + */ + public RedditScopeBuilder removeScope(RedditScope scope) { + scopes.remove(scope); + return this; + } + + /** + * Remove multiple scopes from the builder. + * + * @param scopes Multiple scopes + * + * @return This builder + */ + public RedditScopeBuilder removeScopes(RedditScope... scopes) { + for (RedditScope scope : scopes) { + removeScope(scope); + } + return this; + } + +} diff --git a/src/main/com/github/jreddit/oauth/param/RedditTokenCompleteScope.java b/src/main/com/github/jreddit/oauth/param/RedditTokenCompleteScope.java new file mode 100644 index 0000000..f12b693 --- /dev/null +++ b/src/main/com/github/jreddit/oauth/param/RedditTokenCompleteScope.java @@ -0,0 +1,48 @@ +package com.github.jreddit.oauth.param; + +import java.util.HashSet; +import java.util.Set; + +/** + * Manager of the scopes a response from reddit returns. + * Used by RedditToken to parse the list of scopes it receives. + * + * @see {@link com.github.jreddit.oauth.RedditToken} + * + * @author Simon Kassing + */ +public class RedditTokenCompleteScope { + + /** Set of scopes. */ + private Set scopes; + + /** + * @param scopes List of scopes (e.g. "flair,edit") + */ + public RedditTokenCompleteScope(String scopes) { + + // Create set + this.scopes = new HashSet(); + + // Split up + String[] split = scopes.split(RedditScope.SEPARATOR); + + // Add each to the set + for (String s : split) { + this.scopes.add(s); + } + + } + + /** + * Check whether it has this scope. + * + * @param scope Reddit scope + * + * @return Does it have this scope? + */ + public boolean has(RedditScope scope) { + return scopes.contains(scope.value()); + } + +} diff --git a/src/main/com/github/jreddit/parser/entity/Comment.java b/src/main/com/github/jreddit/parser/entity/Comment.java new file mode 100644 index 0000000..92c7cdb --- /dev/null +++ b/src/main/com/github/jreddit/parser/entity/Comment.java @@ -0,0 +1,269 @@ +package com.github.jreddit.parser.entity; + +import static com.github.jreddit.parser.util.JsonUtils.safeJsonToBoolean; +import static com.github.jreddit.parser.util.JsonUtils.safeJsonToDouble; +import static com.github.jreddit.parser.util.JsonUtils.safeJsonToInteger; +import static com.github.jreddit.parser.util.JsonUtils.safeJsonToString; + +import java.util.List; + +import org.json.simple.JSONObject; + +import com.github.jreddit.parser.entity.imaginary.CommentTreeElement; +import com.github.jreddit.parser.entity.imaginary.MixedListingElement; + +/** + * A Reddit comment. Contains the edited timestamp, the body + * + * @author Benjamin Jakobus + * @author Raul Rene Lepsa + * @author Simon Kassing + */ +public class Comment extends Thing implements CommentTreeElement, MixedListingElement { + + private String author; // Username of the author + private String parentId; // Parent identifier + private String subreddit; // Subreddit name + private String subredditId; // Subreddit identifier + private String linkId; // Submission (aka. link) identifier + private String bodyHTML; // The body with HTML markup + private Boolean scoreHidden; // Whether the score is hidden + private String body; // The actual body + private Boolean edited; // Edited timestamp + private double created; // Created timestamp + private double createdUTC; // Created UTC timestamp + private List replies; // Replies if retrieved + private Integer gilded; // Amount of times the comment been gilded + private Integer score; // Karma score + private Integer upvotes; // Number of upvotes that this body received + private Integer downvotes; // Number of downvotes that this body received + + // Possible fields to add as well: +// private String bannedBy; +// String likes; +// private String approvedBy; +// private String authorFlairCSSClass; +// private String authorFlairText; +// String num_reports = null; +// String distinguished = null; + + public Comment(JSONObject obj) { + super(safeJsonToString(obj.get("name"))); + + this.setAuthor(safeJsonToString(obj.get("author"))); + this.setParentId(safeJsonToString(obj.get("parent_id"))); + this.setBody(safeJsonToString(obj.get("body"))); + this.setEdited(safeJsonToBoolean(obj.get("edited"))); + this.setCreated(safeJsonToDouble(obj.get("created"))); + this.setCreatedUTC(safeJsonToDouble(obj.get("created_utc"))); + this.setGilded(safeJsonToInteger(obj.get("gilded"))); + this.setScore(safeJsonToInteger(obj.get("score"))); + this.setUpvotes(safeJsonToInteger(obj.get("ups"))); + this.setDownvotes(safeJsonToInteger(obj.get("downs"))); + this.setSubreddit(safeJsonToString(obj.get("subreddit"))); + this.setSubredditId(safeJsonToString(obj.get("subreddit_id"))); + this.setLinkId(safeJsonToString(obj.get("link_id"))); + this.setBodyHTML(safeJsonToString(obj.get("body_html"))); + this.setScoreHidden(safeJsonToBoolean(obj.get("score_hidden"))); + + } + + /** + * Retrieve the replies of this comment. + * + * @return Replies (will always initialized, and empty if there are no replies) + */ + public List getReplies() { + return this.replies; + } + + /** + * Set the replies of this comment. + * + * @param replies Comment tree of replies + */ + public void setReplies(List replies) { + this.replies = replies; + } + + /** + * Retrieve the username of the author. + * + * @return Username of the author + */ + public String getAuthor() { + return author; + } + + /** + * Set the username of the author + * + * @param author Username of the author + */ + public void setAuthor(String author) { + this.author = author; + } + + public String getParentId() { + return parentId; + } + + public void setParentId(String parentId) { + this.parentId = parentId; + } + + public String getBody() { + return body; + } + + public void setBody(String body) { + this.body = body; + } + + public Boolean getEdited() { + return edited; + } + + public void setEdited(Boolean edited) { + this.edited = edited; + } + + public double getCreated() { + return created; + } + + public void setCreated(double created) { + this.created = created; + } + + public Integer getGilded() { + return gilded; + } + + public void setGilded(Integer gilded) { + this.gilded = gilded; + } + + public Integer getUpvotes() { + return upvotes; + } + + public void setUpvotes(Integer upvotes) { + this.upvotes = upvotes; + } + + public Integer getDownvotes() { + return downvotes; + } + + public void setDownvotes(Integer downvotes) { + this.downvotes = downvotes; + } + + public double getCreatedUTC() { + return createdUTC; + } + + public void setCreatedUTC(double createdUTC) { + this.createdUTC = createdUTC; + } + + public Integer getScore() { + return score; + } + + public void setScore(Integer score) { + this.score = score; + } + + /** + * @return the subreddit + */ + public String getSubreddit() { + return subreddit; + } + + /** + * @param subreddit the subreddit to set + */ + public void setSubreddit(String subreddit) { + this.subreddit = subreddit; + } + + /** + * @return the subredditId + */ + public String getSubredditId() { + return subredditId; + } + + /** + * @param subredditId the subredditId to set + */ + public void setSubredditId(String subredditId) { + this.subredditId = subredditId; + } + + /** + * @return the linkId + */ + public String getLinkId() { + return linkId; + } + + /** + * @param linkId the linkId to set + */ + public void setLinkId(String linkId) { + this.linkId = linkId; + } + + /** + * @return the bodyHTML + */ + public String getBodyHTML() { + return bodyHTML; + } + + /** + * @param bodyHTML the bodyHTML to set + */ + public void setBodyHTML(String bodyHTML) { + this.bodyHTML = bodyHTML; + } + + /** + * @return the scoreHidden + */ + public Boolean isScoreHidden() { + return scoreHidden; + } + + /** + * @param scoreHidden the scoreHidden to set + */ + public void setScoreHidden(Boolean scoreHidden) { + this.scoreHidden = scoreHidden; + } + + @Override + public String toString() { + return "Comment(" + identifier + ")<" + ((body.length() > 10) ? body.substring(0, 10) : body) + ">"; + } + + @Override + public boolean equals(Object other) { + return other instanceof Comment && this.getFullName().equals(((Comment) other).getFullName()); + } + + @Override + public int hashCode() { + return this.hashCode() * this.getFullName().hashCode(); + } + + @Override + public int compareTo(Thing o) { + return this.getFullName().compareTo(o.getFullName()); + } + +} diff --git a/src/main/com/github/jreddit/parser/entity/Kind.java b/src/main/com/github/jreddit/parser/entity/Kind.java new file mode 100644 index 0000000..68a64a3 --- /dev/null +++ b/src/main/com/github/jreddit/parser/entity/Kind.java @@ -0,0 +1,44 @@ +package com.github.jreddit.parser.entity; + +/** + * Enumeration to represent the different types of Things. + */ +public enum Kind { + + COMMENT("t1"), ACCOUNT("t2"), LINK("t3"), MESSAGE("t4"), SUBREDDIT("t5"), AWARD("t6"), PROMO_CAMPAIGN("t8"), MORE("more"), LISTING("listing"); + + private String value; + + /** + * Type enumeration constructor. + * @param value String representation + */ + Kind(String value) { + this.value = value; + } + + /** + * Retrieve the value of the type. + * @return Type String representation. + */ + public String value() { + return value; + } + + /** + * Match a string with its respective kind. + * + * @param t String kind (e.g. "t1" or "t5") + * + * @return Match kind (null, it not found) + */ + public static Kind match(String t) { + for (Kind k : Kind.values()) { + if (k.value().equals(t)) { + return k; + } + } + return null; + } + +} diff --git a/src/main/com/github/jreddit/parser/entity/Message.java b/src/main/com/github/jreddit/parser/entity/Message.java new file mode 100644 index 0000000..f31125f --- /dev/null +++ b/src/main/com/github/jreddit/parser/entity/Message.java @@ -0,0 +1,167 @@ +package com.github.jreddit.parser.entity; + +import org.json.simple.JSONObject; + +/** + * Encapsulates the private messages. + * Corresponds to the Kind.MESSAGES, which is has the value t4 for the Reddit API + * + * @author Karan Goel + * @author Raul Rene Lepsa + * @author Simon Kassing + */ +public class Message { + + // The ID of this message + private String id; + + // Name - a combination of the Message Type (t4) and the ID of the message + private String fullName; + + // Name of the author of the message + private String author; + + // Recipient of the message + private String recipient; + + // The body of the message + private String body; + + // HTML version of the Body + private String bodyHtml; + + // If the message was a comment or not + private boolean isComment; + + // If it is a comment, it has a parent + private String parentId; + + // Timestamp of when the message was created + private String created; + + // UTC timestamp of when the message was created + private String createdUTC; + + // The content of the message + private String context; + + // The subject of the message + private String subject; + + public Message(JSONObject jsonObject) { + + this.setBody(jsonObject.get("body").toString()); + this.setComment(Boolean.valueOf(jsonObject.get("was_comment").toString())); + this.setFullName(jsonObject.get("name").toString()); + if (jsonObject.get("author") == null) + { + this.setAuthor("reddit"); + } else { + this.setAuthor(jsonObject.get("author").toString()); + } + this.setCreated(jsonObject.get("created").toString()); + this.setRecipient(jsonObject.get("dest").toString()); + this.setCreatedUTC(jsonObject.get("created_utc").toString()); + this.setBodyHtml(jsonObject.get("body_html").toString()); + this.setSubject(jsonObject.get("subject").toString()); + this.setContext(jsonObject.get("context").toString()); + this.setId(jsonObject.get("id").toString()); + + } + + public String getId() { + return id; + } + + public void setId(String id) { + this.id = id; + } + + public String getAuthor() { + return author; + } + + public void setAuthor(String author) { + this.author = author; + } + + public String getRecipient() { + return recipient; + } + + public void setRecipient(String recipient) { + this.recipient = recipient; + } + + public String getBody() { + return body; + } + + public void setBody(String body) { + this.body = body; + } + + public String getBodyHtml() { + return bodyHtml; + } + + public void setBodyHtml(String bodyHtml) { + this.bodyHtml = bodyHtml; + } + + public boolean isComment() { + return isComment; + } + + public void setComment(boolean isComment) { + this.isComment = isComment; + } + + public String getFullName() { + return fullName; + } + + public void setFullName(String fullName) { + this.fullName = fullName; + } + + public String getCreated() { + return created; + } + + public void setCreated(String created) { + this.created = created; + } + + public String getCreatedUTC() { + return createdUTC; + } + + public void setCreatedUTC(String createdUTC) { + this.createdUTC = createdUTC; + } + + public String getContext() { + return context; + } + + public void setContext(String context) { + this.context = context; + } + + public String getSubject() { + return subject; + } + + public void setSubject(String subject) { + this.subject = subject; + } + + public String getParentId() { + return parentId; + } + + public void setParentId(String parentId) { + this.parentId = parentId; + } +} diff --git a/src/main/com/github/jreddit/parser/entity/MessageType.java b/src/main/com/github/jreddit/parser/entity/MessageType.java new file mode 100644 index 0000000..15965fe --- /dev/null +++ b/src/main/com/github/jreddit/parser/entity/MessageType.java @@ -0,0 +1,25 @@ +package com.github.jreddit.parser.entity; + +/** + * Deal with different types of messages + * + * @author Raul Rene Lepsa + */ +public enum MessageType { + + INBOX("inbox"), SENT("sent"), UNREAD("unread"); + + private String value; + + MessageType(String value) { + this.value = value; + } + + public String getValue() { + return value; + } + + public void setValue(String value) { + this.value = value; + } +} diff --git a/src/main/com/github/jreddit/parser/entity/More.java b/src/main/com/github/jreddit/parser/entity/More.java new file mode 100644 index 0000000..20452e1 --- /dev/null +++ b/src/main/com/github/jreddit/parser/entity/More.java @@ -0,0 +1,106 @@ +package com.github.jreddit.parser.entity; + +import java.util.ArrayList; +import java.util.List; + +import org.json.simple.JSONArray; +import org.json.simple.JSONObject; + +import com.github.jreddit.parser.entity.imaginary.CommentTreeElement; +import com.github.jreddit.parser.util.JsonUtils; + +/** + * MORE entity. Can only exist in a comment tree, and thus + * implements the CommenTreeElement interface. + * + * @author Simon Kassing + */ +public class More extends Thing implements CommentTreeElement { + + /** List of comment identifiers (ID36) that are his children. */ + private List children; + + /** Counting number assigned by reddit (does not tell much in a comment tree). */ + private Long count; + + /** Parent comment fullname. */ + private String parentId; + + /** + * Construct a "More" thing. + * + * @param obj JSON object + */ + public More(JSONObject obj) { + super(Kind.MORE.value() + "_NONE"); + + // The obj.get("name") and obj.get("id") are neglected, as these + // are already implicitly included in the children array. + + // Retrieve count from JSON + this.count = JsonUtils.safeJsonToLong(obj.get("count")); + + // Retrieve parent identifier from JSON + this.parentId = JsonUtils.safeJsonToString(obj.get("parent_id")); + + // Iterate over children + this.children = new ArrayList(); + JSONArray jsonChildren = (JSONArray) obj.get("children"); + for (Object child : jsonChildren) { + this.children.add(JsonUtils.safeJsonToString(child)); + } + + } + + /** + * Retrieve the children ID36 comment identifiers. + * + * @return The children (e.g. ["dja241", "dsfjak24"]) + */ + public List getChildren() { + return children; + } + + /** + * Retrieve the counting number assigned by reddit (does not tell much in this case). + * + * @return The counting number + */ + public Long getCount() { + return count; + } + + /** + * Retrieve how many children (comments) the MORE hides. + * + * @return How many comments the more hides ({@link #getChildren()}'s size) + */ + public int getChildrenSize() { + return children.size(); + } + + /** + * Retrieve the parent fullname comment identifier. + * + * @return The parent identifier (e.g. "t1_38942f") + */ + public String getParentId() { + return parentId; + } + + @Override + public int compareTo(Thing o) { + if (!(o instanceof More)) { + return 1; + } else { + return ((More) o).getChildren().equals(this.getChildren()) ? 0 : -1; + } + + } + + @Override + public String toString() { + return "More()<" + this.getChildrenSize() + " more directly underneath>"; + } + +} diff --git a/src/main/com/github/jreddit/parser/entity/Submission.java b/src/main/com/github/jreddit/parser/entity/Submission.java new file mode 100644 index 0000000..7c02e00 --- /dev/null +++ b/src/main/com/github/jreddit/parser/entity/Submission.java @@ -0,0 +1,546 @@ +package com.github.jreddit.parser.entity; + +import static com.github.jreddit.parser.util.JsonUtils.safeJsonToBoolean; +import static com.github.jreddit.parser.util.JsonUtils.safeJsonToDouble; +import static com.github.jreddit.parser.util.JsonUtils.safeJsonToLong; +import static com.github.jreddit.parser.util.JsonUtils.safeJsonToString; + +import org.json.simple.JSONObject; + +import com.github.jreddit.parser.entity.imaginary.MixedListingElement; + + +/** + * This class represents a vote on a link submission on Reddit. + * + * @author Omer Elnour + * @author Andrei Sfat + * @author Raul Rene Lepsa + * @author Jonny Krysh + * @author Danny Tsegai + * @author Simon Kassing + */ +public class Submission extends Thing implements MixedListingElement { + + /* All String values */ + private String url; + private String permalink; + private String author; + private String title; + private String subreddit; + private String subredditId; + private String thumbnail; + private String selftext; + private String selftextHTML; + private String domain; + private String bannedBy; + private String approvedBy; + private String authorFlairCSSClass; + private String linkFlairCSSClass; + private String authorFlairText; + private String linkFlairText; + private String distinguished; + private String from; + private String fromId; + private String removalReason; + private String fromKind; + + /* All Long values */ + private Long gilded; + private Long commentCount; + private Long reportCount; + private Long score; + private Long upVotes; + private Long downVotes; + + /* All Double values */ + private Double created; + private Double createdUTC; + private Double upvoteRatio; + + /* All Boolean values */ + private Boolean visited; + private Boolean self; + private Boolean saved; + private Boolean edited; + private Boolean stickied; + private Boolean nsfw; + private Boolean hidden; + private Boolean clicked; + private Boolean likes; + + /** + * Create a Submission from a JSONObject + * + * @param obj The JSONObject to load Submission data from + */ + public Submission(JSONObject obj) { + super(safeJsonToString(obj.get("name"))); + + setURL(safeJsonToString(obj.get("url"))); + setPermalink(safeJsonToString(obj.get("permalink"))); + setAuthor(safeJsonToString(obj.get("author"))); + setTitle(safeJsonToString(obj.get("title"))); + setSubreddit(safeJsonToString(obj.get("subreddit"))); + setSubredditId(safeJsonToString(obj.get("subreddit_id"))); + setThumbnail(safeJsonToString(obj.get("thumbnail"))); + setSelftext(safeJsonToString(obj.get("selftext"))); + setSelftextHTML(safeJsonToString(obj.get("selftext_html"))); + setDomain(safeJsonToString(obj.get("domain"))); + setBannedBy(safeJsonToString(obj.get("banned_by"))); + setApprovedBy(safeJsonToString(obj.get("approved_by"))); + setAuthorFlairCSSClass(safeJsonToString(obj.get("author_flair_css_class"))); + setLinkFlairCSSClass(safeJsonToString(obj.get("link_flair_css_class"))); + setDistinguished(safeJsonToString(obj.get("distinguished"))); + setAuthorFlairText(safeJsonToString(obj.get("author_flair_text"))); + setLinkFlairText(safeJsonToString(obj.get("link_flair_text"))); + setFrom(safeJsonToString(obj.get("from"))); + setFromId(safeJsonToString(obj.get("from_id"))); + setRemovalReason(safeJsonToString(obj.get("removal_reason"))); + setFromKind(safeJsonToString(obj.get("from_kind"))); + + setGilded(safeJsonToLong(obj.get("gilded"))); + setCommentCount(safeJsonToLong(obj.get("num_comments"))); + setReportCount(safeJsonToLong(obj.get("num_reports"))); + setScore(safeJsonToLong(obj.get("score"))); + setUpVotes(safeJsonToLong(obj.get("ups"))); + setDownVotes(safeJsonToLong(obj.get("downs"))); + + setCreated(safeJsonToDouble(obj.get("created"))); + setCreatedUTC(safeJsonToDouble(obj.get("created_utc"))); + setUpvoteRatio(safeJsonToDouble(obj.get("upvote_ratio"))); + + setVisited(safeJsonToBoolean(obj.get("visited"))); + setSelf(safeJsonToBoolean(obj.get("is_self"))); + setSaved(safeJsonToBoolean(obj.get("saved"))); + setEdited(safeJsonToBoolean(obj.get("edited"))); + setStickied(safeJsonToBoolean(obj.get("stickied"))); + setNSFW(safeJsonToBoolean(obj.get("over_18"))); + setHidden(safeJsonToBoolean(obj.get("hidden"))); + setClicked(safeJsonToBoolean(obj.get("clicked"))); + setLikes(safeJsonToBoolean(obj.get("likes"))); + + } + + public String getFromKind() { + return fromKind; + } + + private void setFromKind(String fromKind) { + this.fromKind = fromKind; + } + + public String getRemovalReason() { + return removalReason; + } + + private void setRemovalReason(String removalReason) { + this.removalReason = removalReason; + } + + public String getFrom() { + return from; + } + + private void setFrom(String from) { + this.from = from; + } + + public String getFromId() { + return fromId; + } + + private void setFromId(String fromId) { + this.fromId = fromId; + } + + /** + * @return Up-vote ratio (total number of up-votes divided by total number of votes) + */ + public Double getUpvoteRatio() { + return upvoteRatio; + } + + private void setUpvoteRatio(Double upvoteRatio) { + this.upvoteRatio = upvoteRatio; + } + + + /** + * @return The CSS class of the author + */ + public String getAuthorFlairCSSClass() { + return authorFlairCSSClass; + } + + private void setAuthorFlairCSSClass(String authorFlairCSSClass) { + this.authorFlairCSSClass = authorFlairCSSClass; + } + + /** + * @return The text of the flair of the author + */ + public String getAuthorFlairText() { + return authorFlairText; + } + + private void setAuthorFlairText(String authorFlairText) { + this.authorFlairText = authorFlairText; + } + + + /** + * @return Flair text of the submission ('link') + */ + public String getLinkFlairText() { + return linkFlairText; + } + + private void setLinkFlairText(String linkFlairText) { + this.linkFlairText = linkFlairText; + } + + + /** + * @return The CSS class of the submission ('link') flair + */ + public String getLinkFlairCSSClass() { + return linkFlairCSSClass; + } + + private void setLinkFlairCSSClass(String linkFlairCSSClass) { + this.linkFlairCSSClass = linkFlairCSSClass; + } + + /** + * @return Whether the post is distinguished (e.g. by a "moderator") + */ + public String getDistinguished() { + return distinguished; + } + + private void setDistinguished(String distinguished) { + this.distinguished = distinguished; + } + + /** + * @return Whether the user that authenticated the retrieval of this submission upvoted it + */ + public Boolean getLikes() { + return likes; + } + + private void setLikes(Boolean likes) { + this.likes = likes; + } + + /** + * @return The username of the one that approved this submission to the subreddit it belongs to + */ + public String getApprovedBy() { + return approvedBy; + } + + private void setApprovedBy(String approvedBy) { + this.approvedBy = approvedBy; + } + + /** + * @return Whether the user that authenticated the retrieval of this submission has hidden it + */ + public Boolean isHidden() { + return hidden; + } + + private void setHidden(Boolean hidden) { + this.hidden = hidden; + } + + /** + * @return Whether the user that authenticated the retrieval of this submission has already clicked on it before + */ + public Boolean isClicked() { + return clicked; + } + + private void setClicked(Boolean clicked) { + this.clicked = clicked; + } + + public void setUpVotes(Long upVotes) { + this.upVotes = upVotes; + } + + /** + * @return The score (number of upvotes minus number of downvotes) + */ + public Long getScore() { + return score; + } + + private void setScore(Long score) { + this.score = score; + } + + public void setAuthor(String author) { + this.author = author; + } + + public void setCreatedUTC(Double createdUTC) { + this.createdUTC = createdUTC; + } + + public void setDownVotes(Long downVotes) { + this.downVotes = downVotes; + } + + public void setCommentCount(Long commentCount) { + this.commentCount = commentCount; + } + + public void setSubreddit(String subreddit) { + this.subreddit = subreddit; + } + + public void setTitle(String title) { + this.title = title; + } + + public void setURL(String url) { + this.url = url; + } + + public String getURL() { + return url; + } + + public String getPermalink() { + return permalink; + } + + public void setPermalink(String permalink) { + this.permalink = permalink; + } + + public Long getCommentCount() { + return commentCount; + } + + public Long getUpVotes() { + return upVotes; + } + + public Long getDownVotes() { + return downVotes; + } + + + + public Double getCreatedUTC() { + return createdUTC; + } + + public String getAuthor() { + return author; + } + + /** + * @return Title of the submission + */ + public String getTitle() { + return title; + } + + /** + * @return Subreddit name (e.g. "programming') + */ + public String getSubreddit() { + return subreddit; + } + + /** + * @return Subreddit ID36 identifier + */ + public String getSubredditId() { + return subredditId; + } + + public void setSubredditId(String subredditId) { + this.subredditId = subredditId; + } + + /** + * @return Thumbnail image URL + */ + public String getThumbnail() { + return thumbnail; + } + + private void setThumbnail(String thumbnail) { + this.thumbnail = thumbnail; + } + + /** + * @return The self text of the submission (written by the author) + */ + public String getSelftext() { + return selftext; + } + + private void setSelftext(String selftext) { + this.selftext = selftext; + } + + /** + * @return The self text of the submission in HTML (written by the author) + */ + public String getSelftextHTML() { + return selftextHTML; + } + + private void setSelftextHTML(String selftextHTML) { + this.selftextHTML = selftextHTML; + } + + /** + * @return The domain of the URL this submission links to + */ + public String getDomain() { + return domain; + } + + private void setDomain(String domain) { + this.domain = domain; + } + + /** + * @return The username of the moderator that banned this submission + */ + public String getBannedBy() { + return bannedBy; + } + + private void setBannedBy(String bannedBy) { + this.bannedBy = bannedBy; + } + + /** + * @return Count of how many times this submission received gold ('was gilded') + */ + public Long getGilded() { + return gilded; + } + + private void setGilded(Long gilded) { + this.gilded = gilded; + } + + /** + * @return Count of how many times this submission was reported + */ + public Long getReportCount() { + return reportCount; + } + + private void setReportCount(Long reportCount) { + this.reportCount = reportCount; + } + + /** + * @return The created time-stamp (ms since Unix epoch) + */ + public Double getCreated() { + return created; + } + + private void setCreated(Double created) { + this.created = created; + } + + /** + * @return Whether the user that authenticated the retrieval of this submission has already visited it before + */ + public Boolean isVisited() { + return visited; + } + + private void setVisited(Boolean visited) { + this.visited = visited; + } + + /** + * @return Whether it is a self post + */ + public Boolean isSelf() { + return self; + } + + private void setSelf(Boolean self) { + this.self = self; + } + + /** + * @return Whether the user that authenticated the retrieval of this submission has saved it + */ + public Boolean isSaved() { + return saved; + } + + private void setSaved(Boolean saved) { + this.saved = saved; + } + + /** + * @return Whether the submission has been edited + */ + public Boolean isEdited() { + return edited; + } + + private void setEdited(Boolean edited) { + this.edited = edited; + } + + /** + * @return Whether the submission is sticked to the top of the subreddit + */ + public Boolean isStickied() { + return stickied; + } + + private void setStickied(Boolean stickied) { + this.stickied = stickied; + } + + /** + * @return Whether the post is Not Suited For Work (contains adult content) + */ + public Boolean isNSFW() { + return nsfw; + } + + private void setNSFW(Boolean nsfw) { + this.nsfw = nsfw; + } + + @Override + public String toString() { + return "Submission(" + this.getFullName() + ")<" + title + ">"; + } + + @Override + public boolean equals(Object other) { + return other instanceof Submission && this.getFullName().equals(((Submission) other).getFullName()); + } + + @Override + public int hashCode() { + return this.hashCode() * this.getFullName().hashCode(); + } + + @Override + public int compareTo(Thing o) { + return this.getFullName().compareTo(o.getFullName()); + } + +} \ No newline at end of file diff --git a/src/main/com/github/jreddit/parser/entity/Subreddit.java b/src/main/com/github/jreddit/parser/entity/Subreddit.java new file mode 100644 index 0000000..56622be --- /dev/null +++ b/src/main/com/github/jreddit/parser/entity/Subreddit.java @@ -0,0 +1,223 @@ +package com.github.jreddit.parser.entity; + +import static com.github.jreddit.parser.util.JsonUtils.safeJsonToBoolean; +import static com.github.jreddit.parser.util.JsonUtils.safeJsonToDouble; +import static com.github.jreddit.parser.util.JsonUtils.safeJsonToLong; +import static com.github.jreddit.parser.util.JsonUtils.safeJsonToString; + +import org.json.simple.JSONObject; + +/** + * Encapsulates a subreddit. + * + * @author Benjamin Jakobus + * @author Simon Kassing + */ +public class Subreddit extends Thing { + + private String displayName; + private String title; + private String url; + private String description; + private String subredditType; + + private double created; + private double createdUTC; + + private Boolean nsfw; + + private Long subscribers; + + // Other possible fields + + // Submit text HTML +// String submit_text_html = null; + + // Whether user is banned +// Boolean user_is_banned = null; + + // Submit text +// String submit_text = "submit text for subreddit"; + + // Header image +// String header_img = "http://a.thumbs.redditmedia.com/yyL5sveWcgkCPKbr.png"; + + // Description in HTML markup +// String description_html = "<div>HTML description for subreddit</d>"; + + // Whether user is moderator +// Boolean user_is_moderator = null; + + // Header title +// String header_title = "Header title for subreddit"; + + // Submit link title +// String submit_link_label = "Submit link label"; + + // Accounts active +// String accounts_active = null; + + // Whether it allows public traffic +// Boolean public_traffic = true; + + // Size of header +// JSONArray header_size = JsonHelpers.jsonArrayOf(160, 64); + + // Submit text label +// String submit_text_label = "Submit text label"; + + // Whether user is contributor +// Boolean user_is_contributor = null; + + // Public description +// String public_description = "Public description of subreddit"; + + // Amount of minutes the comment score is hidden +// long comment_score_hide_mins = 0; + + // What types of submissions are allowed +// String submission_type = "any"; + + // Whether the user is contributor +// Boolean user_is_subscriber = null; + + /** + * Create a Submission from a JSONObject + * + * @param obj The JSONObject to load Submission data from + */ + public Subreddit(JSONObject obj) { + super(safeJsonToString(obj.get("name"))); + + setDisplayName(safeJsonToString(obj.get("display_name"))); + setTitle(safeJsonToString(obj.get("title"))); + setURL(safeJsonToString(obj.get("url"))); + setCreated(safeJsonToDouble(obj.get("created"))); + setCreatedUTC(safeJsonToDouble(obj.get("created_utc"))); + setNSFW(safeJsonToBoolean(obj.get("over18"))); + setSubscribers(safeJsonToLong(obj.get("subscribers"))); + setDescription(safeJsonToString(obj.get("description"))); + setSubredditType(safeJsonToString(obj.get("subreddit_type"))); + + } + + private void setCreated(double created) { + this.created = created; + } + + private void setCreatedUTC(double createdUTC) { + this.createdUTC = createdUTC; + } + + private void setDescription(String description) { + this.description = description; + } + + private void setDisplayName(String displayName) { + this.displayName = displayName; + } + + private void setNSFW(Boolean nsfw) { + this.nsfw = nsfw; + } + + private void setSubscribers(long subscribers) { + this.subscribers = subscribers; + } + + private void setTitle(String title) { + this.title = title; + } + + private void setURL(String url) { + this.url = url; + } + + /** + * @return Timestamp of when the subreddit was created. + */ + public double getCreated() { + return created; + } + + /** + * @return UTC timestamp of when the subreddit was created. + */ + public double getCreatedUTC() { + return createdUTC; + } + + /** + * @return Description detailing the subreddit. + */ + public String getDescription() { + return description; + } + + /** + * @return The subreddit's display name. + */ + public String getDisplayName() { + return displayName; + } + + /** + * @return The number of subscribers for this subreddit. + */ + public long getSubscribers() { + return subscribers; + } + + /** + * @return The subreddit's title. + */ + public String getTitle() { + return title; + } + + /** + * @return The subreddit's URL. + */ + public String getURL() { + return url; + } + + /** + * @return True if the subreddit is marked as containing adult content; false if not. + */ + public Boolean isNSFW() { + return nsfw; + } + + /** + * @return The type of subreddit (e.g. "private" or "public") + */ + public String getSubredditType() { + return subredditType; + } + + public void setSubredditType(String type) { + this.subredditType = type; + } + + @Override + public String toString() { + return "Subreddit(" + this.getFullName() + ")<" + this.getDisplayName() + ">"; + } + + @Override + public boolean equals(Object other) { + return (other instanceof Subreddit && this.getFullName().equals(((Subreddit) other).getFullName())); + } + + @Override + public int hashCode() { + return this.hashCode() * this.getFullName().hashCode(); + } + + @Override + public int compareTo(Thing o) { + return this.getFullName().compareTo(o.getFullName()); + } + +} \ No newline at end of file diff --git a/src/main/com/github/jreddit/parser/entity/Thing.java b/src/main/com/github/jreddit/parser/entity/Thing.java new file mode 100644 index 0000000..84086d5 --- /dev/null +++ b/src/main/com/github/jreddit/parser/entity/Thing.java @@ -0,0 +1,77 @@ +package com.github.jreddit.parser.entity; + +/** + * This class represents a reddit "thing" + * + * @author Omer Elnour + * @author Simon Kassing + * @see Reddit API Reference + */ +public abstract class Thing implements Comparable { + + /** + * The kind of this thing. + * + * @see Reddit API Reference for full names (section 'kind prefixes') + */ + protected final Kind kind; + + /** + * The identifier of this thing. + * + * @see Reddit API Reference for full names (section 'identifier') + */ + protected final String identifier; + + /** + * The full name of this thing. + * Combination of its kind ({@link #getKind() getKind}) and its unique ID. + * + * @see Reddit API Reference for full names + */ + protected final String fullName; + + /** + * Constructor. Must be called. + * @param name Full name of the thing + */ + public Thing(String name) { + assert name.contains("_") : "A full name must contain an underscore."; + this.fullName = name; + String[] split = name.split("_"); + this.kind = Kind.match(split[0]); + this.identifier = split[1]; + } + + /** + * Retrieve the kind of this thing. + * Example: t3 indicates a kind 3 (a link). + * + * @see Reddit API Reference for full names (section 'kind prefixes') + */ + public Kind getKind() { + return kind; + } + + /** + * Retrieve the identifier of this thing. + * Example: 15bfi0. + * + * @see Reddit API Reference for full names (section 'identifier') + */ + public String getIdentifier() { + return identifier; + } + + /** + * Retrieve the full name of this thing. + * Combination of its kind (see {@link #getKind() getKind}) and its unique ID, combined with a underscore. + * Example: t3_15bfi0 indicates a kind 3 (a link) and as unique identifier 15bfi0. + * + * @see Reddit API Reference for full names + */ + public String getFullName() { + return fullName; + } + +} \ No newline at end of file diff --git a/src/main/com/github/jreddit/parser/entity/UserInfo.java b/src/main/com/github/jreddit/parser/entity/UserInfo.java new file mode 100644 index 0000000..8ededa7 --- /dev/null +++ b/src/main/com/github/jreddit/parser/entity/UserInfo.java @@ -0,0 +1,212 @@ +package com.github.jreddit.parser.entity; + +import org.json.simple.JSONObject; + +/** + * Encapsulates user information (regarding karma, emails, identifiers, statuses, created time and current modhash) + * + * @author Raul Rene Lepsa + */ +public class UserInfo { + + // User identifier + private String id; + + // The user's name + private String name; + + // Modhash token + private String modhash; + + // Karma points for all the comments + private long commentKarma; + + // Karma points for all the submitted links + private long linkKarma; + + // Whether the user is a moderator + private boolean isMod; + + // Whether or not the user has moderator email + private Boolean hasModMail; + + // Whether the account is associated with an email address + private Boolean hasMail; + + // Indicates whether the user has verified the email address + private Boolean hasVerifiedEmail; + + // Whether the user is a gold member + private boolean isGold; + + // Timestamp of the creation date + private double created; + + // UTC timestamp of creation date + private double createdUTC; + + // Indicates whether this user is friends with the currently connected one. Believe it or not, you can actually be + // friends with yourself. http://www.reddit.com/r/reddit.com/comments/duf7q/random_reddit_protip_you_can_add_yourself_as_a/ + private boolean isFriend; + + // Indicates whether the user is over 18 + private Boolean over18; + + public UserInfo() { + + } + + public UserInfo(JSONObject info) { + + setHasMail((Boolean) info.get("has_mail")); + setHasModMail((Boolean) info.get("has_mod_mail")); + setCommentKarma((Long) info.get("comment_karma")); + setCreatedUTC((Double) info.get("created_utc")); + setGold((Boolean) info.get("is_gold")); + setLinkKarma((Long) info.get("link_karma")); + setMod((Boolean) info.get("is_mod")); + setFriend((Boolean) info.get("is_friend")); + setModhash((String) info.get("modhash")); + setHasVerifiedEmail((Boolean) info.get("has_verified_email")); + setId((String) info.get("id")); + setOver18((Boolean) info.get("over_18")); + setCreated((Double) info.get("created")); + setName((String) info.get("name")); + + } + + public String getId() { + return id; + } + + public void setId(String id) { + this.id = id; + } + + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } + + public String getModhash() { + return modhash; + } + + public void setModhash(String modhash) { + this.modhash = modhash; + } + + public long getCommentKarma() { + return commentKarma; + } + + public void setCommentKarma(long commentKarma) { + this.commentKarma = commentKarma; + } + + public long getLinkKarma() { + return linkKarma; + } + + public void setLinkKarma(long linkKarma) { + this.linkKarma = linkKarma; + } + + public boolean isMod() { + return isMod; + } + + public void setMod(boolean isMod) { + this.isMod = isMod; + } + + public Boolean getHasModMail() { + return hasModMail; + } + + public void setHasModMail(Boolean hasModMail) { + this.hasModMail = hasModMail; + } + + public Boolean getHasMail() { + return hasMail; + } + + public void setHasMail(Boolean hasMail) { + this.hasMail = hasMail; + } + + public Boolean isHasVerifiedEmail() { + return hasVerifiedEmail; + } + + public void setHasVerifiedEmail(Boolean hasVerifiedEmail) { + this.hasVerifiedEmail = hasVerifiedEmail; + } + + public boolean isGold() { + return isGold; + } + + public void setGold(boolean isGold) { + this.isGold = isGold; + } + + public double getCreated() { + return created; + } + + public void setCreated(double created) { + this.created = created; + } + + public double getCreatedUTC() { + return createdUTC; + } + + public void setCreatedUTC(double createdUTC) { + this.createdUTC = createdUTC; + } + + public boolean isFriend() { + return isFriend; + } + + public void setFriend(boolean isFriend) { + this.isFriend = isFriend; + } + + public Boolean getOver18() { + return over18; + } + + public void setOver18(Boolean over18) { + this.over18 = over18; + } + + @Override + public String toString() { + StringBuilder result = new StringBuilder(); + String newLine = System.getProperty("line.separator"); + + result.append("id: ").append(id).append(newLine) + .append("name: ").append(name).append(newLine) + .append("modhash: ").append(modhash).append(newLine) + .append("commentKarma: ").append(commentKarma).append(newLine) + .append("linkKarma: ").append(linkKarma).append(newLine) + .append("isModerator: ").append(isMod).append(newLine) + .append("hasModMail: ").append(hasModMail).append(newLine) + .append("hasMail: ").append(hasMail).append(newLine) + .append("hasVerifiedEmail: ").append(hasVerifiedEmail).append(newLine) + .append("isGold: ").append(isGold).append(newLine) + .append("Created: ").append(created).append(newLine) + .append("CreatedUTC: ").append(createdUTC).append(newLine) + .append("isFriend: ").append(isFriend).append(newLine) + .append("over18: ").append(over18); + + return result.toString(); + } +} diff --git a/src/main/com/github/jreddit/parser/entity/imaginary/CommentTreeElement.java b/src/main/com/github/jreddit/parser/entity/imaginary/CommentTreeElement.java new file mode 100644 index 0000000..5264564 --- /dev/null +++ b/src/main/com/github/jreddit/parser/entity/imaginary/CommentTreeElement.java @@ -0,0 +1,16 @@ +package com.github.jreddit.parser.entity.imaginary; + +/** + * Interface to abstract over the two possible elements in a + * comment tree, namely a {@link Comment} or a {@link More} thing. If an object + * is of the type of this interface, it means that it must + * be either an {@link Comment} or a {@link More} thing. + * + * @author Simon Kassing + * + * @see com.github.jreddit.parser.entity.Comment + * @see com.github.jreddit.parser.entity.More + */ +public interface CommentTreeElement { + // Empty because only an abstraction for either a Comment or a More thing +} diff --git a/src/main/com/github/jreddit/parser/entity/imaginary/FullSubmission.java b/src/main/com/github/jreddit/parser/entity/imaginary/FullSubmission.java new file mode 100644 index 0000000..5910fff --- /dev/null +++ b/src/main/com/github/jreddit/parser/entity/imaginary/FullSubmission.java @@ -0,0 +1,35 @@ +package com.github.jreddit.parser.entity.imaginary; + +import java.util.List; + +import com.github.jreddit.parser.entity.Submission; + +public class FullSubmission { + + private Submission submission; + private List commentTree; + + public FullSubmission(Submission submission, List commentTree) { + this.submission = submission; + this.commentTree = commentTree; + } + + /** + * + * @return the submission + */ + public Submission getSubmission() { + return submission; + } + + /** + * + * @return the commentTree + */ + public List getCommentTree() { + return commentTree; + } + + + +} diff --git a/src/main/com/github/jreddit/parser/entity/imaginary/MixedListingElement.java b/src/main/com/github/jreddit/parser/entity/imaginary/MixedListingElement.java new file mode 100644 index 0000000..b0ee6e6 --- /dev/null +++ b/src/main/com/github/jreddit/parser/entity/imaginary/MixedListingElement.java @@ -0,0 +1,16 @@ +package com.github.jreddit.parser.entity.imaginary; + +/** + * Interface to abstract over the two possible elements in a + * mixed listing, namely a {@link Comment} or a {@link Submission} thing. + * If an object is of the type of this interface, it means that it must + * be either of the respective {@link Comment} or {@link Submission} class. + * + * @author Simon Kassing + * + * @see com.github.jreddit.parser.entity.Comment + * @see com.github.jreddit.parser.entity.Submission + */ +public interface MixedListingElement { + // Empty because only an abstraction for either a Comment or a Submission thing +} diff --git a/src/main/com/github/jreddit/parser/exception/RedditParseException.java b/src/main/com/github/jreddit/parser/exception/RedditParseException.java new file mode 100644 index 0000000..72caa35 --- /dev/null +++ b/src/main/com/github/jreddit/parser/exception/RedditParseException.java @@ -0,0 +1,29 @@ +package com.github.jreddit.parser.exception; + +import org.json.simple.parser.ParseException; + +public class RedditParseException extends Exception { + + private static final long serialVersionUID = -1031803118041533936L; + + public RedditParseException(String custom) { + super("Could not parse response from reddit (" + custom + ")"); + } + + public RedditParseException(String custom, Throwable t) { + super("Could not parse response from reddit (" + custom + ")", t); + } + + public RedditParseException() { + this("undefined (null) response"); + } + + public RedditParseException(int errorCode) { + this("contained HTTP error code: " + errorCode); + } + + public RedditParseException(ParseException pe) { + this("invalid JSON format", pe); + } + +} diff --git a/src/main/com/github/jreddit/parser/listing/CommentsListingParser.java b/src/main/com/github/jreddit/parser/listing/CommentsListingParser.java new file mode 100644 index 0000000..e212502 --- /dev/null +++ b/src/main/com/github/jreddit/parser/listing/CommentsListingParser.java @@ -0,0 +1,55 @@ +package com.github.jreddit.parser.listing; + +import java.util.LinkedList; +import java.util.List; + +import org.json.simple.parser.ParseException; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import com.github.jreddit.parser.entity.Comment; +import com.github.jreddit.parser.entity.Thing; +import com.github.jreddit.parser.exception.RedditParseException; + +public class CommentsListingParser extends RedditListingParser { + + private static final Logger LOGGER = LoggerFactory.getLogger(CommentsListingParser.class); + + /** + * Parse JSON received from reddit into a list of comments. + * This parser expects the JSON to be of a listing of comments.
+ *
+ * Note: this parsing can only be performed on listings of comments, not on + * a comment tree of a submission. + * + * @param jsonText JSON Text + * @return Parsed list of comments + * + * @throws ParseException + * @throws RedditRequestException + */ + public List parse(String jsonText) throws RedditParseException { + + // Parse to a list of things + List things = this.parseGeneric(jsonText); + + // List of comment and submission mixed elements + List comments = new LinkedList(); + + // Iterate over things + for (Thing t : things) { + + if (t instanceof Comment) { + comments.add((Comment) t); + } else { + LOGGER.warn("Encountered an unexpected reddit thing (" + t.getKind().value() + "), skipping it."); + } + + } + + // Return resulting comments list + return comments; + + } + +} diff --git a/src/main/com/github/jreddit/parser/listing/CommentsMoreParser.java b/src/main/com/github/jreddit/parser/listing/CommentsMoreParser.java new file mode 100644 index 0000000..3cbf987 --- /dev/null +++ b/src/main/com/github/jreddit/parser/listing/CommentsMoreParser.java @@ -0,0 +1,84 @@ +package com.github.jreddit.parser.listing; + +import java.util.LinkedList; +import java.util.List; + +import org.json.simple.JSONObject; +import org.json.simple.parser.JSONParser; +import org.json.simple.parser.ParseException; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import com.github.jreddit.parser.entity.Comment; +import com.github.jreddit.parser.entity.More; +import com.github.jreddit.parser.entity.Thing; +import com.github.jreddit.parser.entity.imaginary.CommentTreeElement; +import com.github.jreddit.parser.exception.RedditParseException; + +public class CommentsMoreParser extends RedditListingParser { + + private static final Logger LOGGER = LoggerFactory.getLogger(CommentsMoreParser.class); + + private static final JSONParser JSON_PARSER = new JSONParser(); + + + /** + * Parse JSON received from reddit into a list of new additional comment tree elements. + * This parser expects the JSON to be of a listing of comments and more's.
+ *
+ * Note: this parsing can only be performed on listings of comments and more's, not on + * a comment tree of a submission. + * + * @param jsonText JSON Text + * @return Parsed list of comments + * + * @throws ParseException + * @throws RedditRequestException + */ + public List parse(String jsonText) throws RedditParseException { + + try { + + // Parse JSON response + Object response = JSON_PARSER.parse(jsonText); + + // Validate main + this.validate(response); + + // Move to the main object + JSONObject main = (JSONObject) ((JSONObject) response).get("json"); + + // List of comment and more mixed elements + List elements = new LinkedList(); + + // If the main has data (it can happen that it does not, when no comments identifiers were passed along) + if (main.get("data") != null) { + + // Parse to a list of things + List things = this.parseGeneric(main.toJSONString(), "things"); + + // Iterate over things + for (Thing t : things) { + + if (t instanceof Comment) { + elements.add((Comment) t); + } else if (t instanceof More) { + elements.add((More) t); + } else { + LOGGER.warn("Encountered an unexpected reddit thing (" + t.getKind().value() + "), skipping it."); + } + + } + + } + + // Return resulting element list + return elements; + + } catch (ParseException pe) { + throw new RedditParseException(pe); + } + + } + +} diff --git a/src/main/com/github/jreddit/parser/listing/MixedListingParser.java b/src/main/com/github/jreddit/parser/listing/MixedListingParser.java new file mode 100644 index 0000000..bb36a27 --- /dev/null +++ b/src/main/com/github/jreddit/parser/listing/MixedListingParser.java @@ -0,0 +1,62 @@ +package com.github.jreddit.parser.listing; + +import java.util.LinkedList; +import java.util.List; + +import org.json.simple.parser.ParseException; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import com.github.jreddit.parser.entity.Comment; +import com.github.jreddit.parser.entity.Submission; +import com.github.jreddit.parser.entity.Thing; +import com.github.jreddit.parser.entity.imaginary.MixedListingElement; +import com.github.jreddit.parser.exception.RedditParseException; + +/** + * Parser for a listing that has both submissions and comments mixed together. + * + * @author Simon Kassing + * + * @see MixedListingElement + */ +public class MixedListingParser extends RedditListingParser { + + private static final Logger LOGGER = LoggerFactory.getLogger(MixedListingParser.class); + + /** + * Parse JSON received from reddit into a list of submissions and comments. + * This parser expects the JSON to be of a listing of submissions and comments. + * + * @param jsonText JSON Text + * @return Parsed list of submissions + * + * @throws ParseException + */ + public List parse(String jsonText) throws RedditParseException { + + // Parse to a list of things + List things = this.parseGeneric(jsonText); + + // List of comment and submission mixed elements + List mixedElements = new LinkedList(); + + // Iterate over things + for (Thing t : things) { + + if (t instanceof Comment) { + mixedElements.add((Comment) t); + } else if (t instanceof Submission) { + mixedElements.add((Submission) t); + } else { + LOGGER.warn("Encountered an unexpected reddit thing (" + t.getKind().value() + "), skipping it."); + } + + } + + // Return result + return mixedElements; + + } + +} diff --git a/src/main/com/github/jreddit/parser/listing/RedditListingParser.java b/src/main/com/github/jreddit/parser/listing/RedditListingParser.java new file mode 100644 index 0000000..73f5853 --- /dev/null +++ b/src/main/com/github/jreddit/parser/listing/RedditListingParser.java @@ -0,0 +1,193 @@ +package com.github.jreddit.parser.listing; + +import static com.github.jreddit.parser.util.JsonUtils.safeJsonToString; + +import java.util.LinkedList; +import java.util.List; + +import org.json.simple.JSONArray; +import org.json.simple.JSONObject; +import org.json.simple.parser.JSONParser; +import org.json.simple.parser.ParseException; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import com.github.jreddit.parser.entity.Comment; +import com.github.jreddit.parser.entity.Kind; +import com.github.jreddit.parser.entity.More; +import com.github.jreddit.parser.entity.Submission; +import com.github.jreddit.parser.entity.Subreddit; +import com.github.jreddit.parser.entity.Thing; +import com.github.jreddit.parser.exception.RedditParseException; +import com.github.jreddit.parser.util.JsonUtils; + +public class RedditListingParser { + + private static final Logger LOGGER = LoggerFactory.getLogger(RedditListingParser.class); + + protected static final JSONParser JSON_PARSER = new JSONParser(); + + /** + * Validate that it is indeed the starting of a listing of reddit things. + * + * @param response Object returned by JSON parser + * + * @throws RedditRequestException If the response is not valid listing of reddit things + */ + public void validate(Object response) throws RedditParseException { + + // Check for null + if (response == null) { + throw new RedditParseException(); + } + + // Check it is a JSON response + if (!(response instanceof JSONObject)) { + throw new RedditParseException("not a JSON response"); + } + + // Cast to JSON object + JSONObject jsonResponse = ((JSONObject) response); + + // Check for error + if (jsonResponse.get("error") != null) { + throw new RedditParseException(JsonUtils.safeJsonToInteger(jsonResponse.get("error"))); + } + + // Check that data exists + if (jsonResponse.get("data") == null && jsonResponse.get("json") == null) { + throw new RedditParseException("data is missing from listing"); + } + + } + + /** + * Parse JSON received from reddit into a list of things. + * This parser expects the JSON to be of a listing of things, and supports + * the following things: More, Comment, Submission, and Subreddit. + * + * @param jsonText JSON Text + * @return Parsed list of things + * + * @throws ParseException + */ + public List parseGeneric(String jsonText) throws RedditParseException { + return parseGeneric(jsonText, "children"); + } + + /** + * Parse JSON received from reddit into a list of things. + * This parser expects the JSON to be of a listing of things, and supports + * the following things: More, Comment, Submission, and Subreddit.
+ *
+ * Note: if it encounters an invalid element (e.g. missing kind or data), it will + * log a warning using SLF4J and would return null. + * + * @param jsonText JSON Text + * @param listingName Name of the listing name within the data + * + * @return Parsed list of things + * + * @throws ParseException + */ + public List parseGeneric(String jsonText, String listingName) throws RedditParseException { + + try { + + // List of submissions + List things = new LinkedList(); + + // Send request to reddit server via REST client + Object response = JSON_PARSER.parse(jsonText); + + // Check for reddit error, can throw a RedditError + validate(response); + + // Cast to a JSON object + JSONObject object = (JSONObject) response; + + // Get the array of children + JSONArray array = (JSONArray) ((JSONObject) object.get("data")).get(listingName); + + // Iterate over array of children + for (Object element : array) { + + // Get the element + JSONObject data = (JSONObject) element; + + // Make sure it is of the correct kind + String kindData = safeJsonToString(data.get("kind")); + Object objData = data.get("data"); + + // If no kind is given + if (kindData == null) { + LOGGER.warn("Kind data missing, skipping it."); + + // If no data is given + } else if (objData == null || !(objData instanceof JSONObject)) { + LOGGER.warn("Object data missing, skipping it."); + + } else { + + // Attempt to match + Kind kind = Kind.match(kindData); + + // Parse the thing + Thing thing = parseThing(kind, ((JSONObject) data.get("data"))); + + // Show warning if failed + if (thing == null) { + LOGGER.warn("Encountered invalid kind for a listing (" + kindData + "), skipping it."); + + } else { + things.add(thing); + } + + } + + } + + // Finally return list of submissions + return things; + + } catch (ParseException pe) { + throw new RedditParseException(pe); + } + + } + + /** + * Parse the data into a thing if possible. + * + * @param kind Kind of data + * @param data Data for the thing + * @return The thing generated from the data, if failed null + */ + private Thing parseThing(Kind kind, JSONObject data) { + + // For a comment + if (kind == Kind.COMMENT) { + return new Comment(data); + + // For a submission + } else if (kind == Kind.LINK) { + return new Submission(data); + + // For a subreddit + } else if (kind == Kind.SUBREDDIT) { + return new Subreddit(data); + + // For a more + } else if (kind == Kind.MORE) { + return new More(data); + + // In all other cases (null, or of a different type) + } else { + return null; + } + + } + + + +} diff --git a/src/main/com/github/jreddit/parser/listing/SubmissionsListingParser.java b/src/main/com/github/jreddit/parser/listing/SubmissionsListingParser.java new file mode 100644 index 0000000..ff2eaa1 --- /dev/null +++ b/src/main/com/github/jreddit/parser/listing/SubmissionsListingParser.java @@ -0,0 +1,51 @@ +package com.github.jreddit.parser.listing; + +import java.util.LinkedList; +import java.util.List; + +import org.json.simple.parser.ParseException; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import com.github.jreddit.parser.entity.Submission; +import com.github.jreddit.parser.entity.Thing; +import com.github.jreddit.parser.exception.RedditParseException; + +public class SubmissionsListingParser extends RedditListingParser { + + private static final Logger LOGGER = LoggerFactory.getLogger(SubmissionsListingParser.class); + + /** + * Parse JSON received from reddit into a list of submissions. + * This parser expects the JSON to be of a listing of submissions ('links'). + * + * @param jsonText JSON Text + * @return Parsed list of submissions + * + * @throws ParseException + */ + public List parse(String jsonText) throws RedditParseException { + + // Parse to a list of things + List things = this.parseGeneric(jsonText); + + // List of comment and submission mixed elements + List submissions = new LinkedList(); + + // Iterate over things + for (Thing t : things) { + + if (t instanceof Submission) { + submissions.add((Submission) t); + } else { + LOGGER.warn("Encountered an unexpected reddit thing (" + t.getKind().value() + "), skipping it."); + } + + } + + // Return resulting comments list + return submissions; + + } + +} diff --git a/src/main/com/github/jreddit/parser/listing/SubredditsListingParser.java b/src/main/com/github/jreddit/parser/listing/SubredditsListingParser.java new file mode 100644 index 0000000..9637dda --- /dev/null +++ b/src/main/com/github/jreddit/parser/listing/SubredditsListingParser.java @@ -0,0 +1,51 @@ +package com.github.jreddit.parser.listing; + +import java.util.LinkedList; +import java.util.List; + +import org.json.simple.parser.ParseException; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import com.github.jreddit.parser.entity.Subreddit; +import com.github.jreddit.parser.entity.Thing; +import com.github.jreddit.parser.exception.RedditParseException; + +public class SubredditsListingParser extends RedditListingParser { + + private static final Logger LOGGER = LoggerFactory.getLogger(SubredditsListingParser.class); + + /** + * Parse JSON received from reddit into a list of subreddits. + * This parser expects the JSON to be of a listing of subreddits. + * + * @param jsonText JSON Text + * @return Parsed list of subreddits + * + * @throws ParseException + */ + public List parse(String jsonText) throws RedditParseException { + + // Parse to a list of things + List things = this.parseGeneric(jsonText); + + // List of comment and submission mixed elements + List subreddits = new LinkedList(); + + // Iterate over things + for (Thing t : things) { + + if (t instanceof Subreddit) { + subreddits.add((Subreddit) t); + } else { + LOGGER.warn("Encountered an unexpected reddit thing (" + t.getKind().value() + "), skipping it."); + } + + } + + // Return resulting comments list + return subreddits; + + } + +} diff --git a/src/main/com/github/jreddit/parser/single/FullSubmissionParser.java b/src/main/com/github/jreddit/parser/single/FullSubmissionParser.java new file mode 100644 index 0000000..b142959 --- /dev/null +++ b/src/main/com/github/jreddit/parser/single/FullSubmissionParser.java @@ -0,0 +1,156 @@ +package com.github.jreddit.parser.single; + +import static com.github.jreddit.parser.util.JsonUtils.safeJsonToString; + +import java.util.ArrayList; +import java.util.List; + +import org.json.simple.JSONArray; +import org.json.simple.JSONObject; +import org.json.simple.parser.JSONParser; +import org.json.simple.parser.ParseException; + +import com.github.jreddit.parser.entity.Comment; +import com.github.jreddit.parser.entity.Kind; +import com.github.jreddit.parser.entity.More; +import com.github.jreddit.parser.entity.Submission; +import com.github.jreddit.parser.entity.imaginary.CommentTreeElement; +import com.github.jreddit.parser.entity.imaginary.FullSubmission; +import com.github.jreddit.parser.exception.RedditParseException; +import com.github.jreddit.parser.util.JsonUtils; + +public class FullSubmissionParser { + + protected static final JSONParser JSON_PARSER = new JSONParser(); + + /** + * Parse JSON received from reddit into a full submission. + * A full submissions means it has both (a) the submission, and (b) the comment tree. + * + * @param jsonText JSON Text + * @return Full submission + * + * @throws ParseException + */ + public FullSubmission parse(String jsonText) throws RedditParseException { + + try { + + // Parse JSON text + Object response = JSON_PARSER.parse(jsonText); + + // Validate response + validate(response); + + // Create submission (casting with JSON is horrible) + JSONObject main = (JSONObject) ((JSONArray) response).get(0); + Submission submission = new Submission((JSONObject) ((JSONObject) ((JSONArray)((JSONObject) main.get("data")).get("children")).get(0)).get("data")); + + // Create comment tree + JSONObject mainTree = (JSONObject) ((JSONArray) response).get(1); + List commentTree = parseRecursive(mainTree); + + // Return the set of submission and its comment tree + return new FullSubmission(submission, commentTree); + + } catch (ParseException pe) { + throw new RedditParseException(pe); + } + + } + + /** + * Parse a JSON object consisting of comments and add them + * to the already existing list of comments. This does NOT create + * a new comment list. + * + * @param comments List of comments + * @param object JSON Object + */ + protected List parseRecursive(JSONObject main) throws RedditParseException { + + List commentTree = new ArrayList(); + + // Iterate over the comment tree results + JSONArray array = (JSONArray) ((JSONObject) main.get("data")).get("children"); + for (Object element : array) { + + // Get the element + JSONObject data = (JSONObject) element; + + // Make sure it is of the correct kind + String kind = safeJsonToString(data.get("kind")); + + // If it is a comment + if (kind != null && kind.equals(Kind.COMMENT.value())) { + + // Create comment + Comment comment = new Comment( (JSONObject) data.get("data") ); + + // Retrieve replies + Object replies = ((JSONObject) data.get("data")).get("replies"); + + // If it is an JSON object + if (replies instanceof JSONObject) { + comment.setReplies(parseRecursive( (JSONObject) replies )); + + // If there are no replies, end with an empty one + } else { + comment.setReplies(new ArrayList()); + } + + // Add comment to the tree + commentTree.add(comment); + } + + // If it is a more + if (kind != null && kind.equals(Kind.MORE.value())) { + + // Add to comment tree + commentTree.add(new More((JSONObject) data.get("data"))); + + } + + } + + return commentTree; + + } + + /** + * Validate that it is in fact a full submission. + * + * @param response Object from the JSON parser + * + * @throws RedditRequestException If the JSON is in incorrect format + */ + public void validate(Object response) throws RedditParseException { + + // Check for null + if (response == null) { + throw new RedditParseException(); + } + + // Check it is a JSON response + if (response instanceof JSONObject) { + + // Cast to JSON object + JSONObject jsonResponse = (JSONObject) response; + + // Check for error + if (jsonResponse.get("error") != null) { + throw new RedditParseException(JsonUtils.safeJsonToInteger(jsonResponse.get("error"))); + } else { + throw new RedditParseException("invalid json format, started with object (should start with array)"); + } + + } + + // It must start with an array + if (!(response instanceof JSONArray)) { + throw new RedditParseException("invalid json format, did not start with array"); + } + + } + +} diff --git a/src/main/com/github/jreddit/parser/util/CommentTreeUtils.java b/src/main/com/github/jreddit/parser/util/CommentTreeUtils.java new file mode 100644 index 0000000..3f43486 --- /dev/null +++ b/src/main/com/github/jreddit/parser/util/CommentTreeUtils.java @@ -0,0 +1,96 @@ +package com.github.jreddit.parser.util; + +import java.util.ArrayList; +import java.util.List; + +import com.github.jreddit.parser.entity.Comment; +import com.github.jreddit.parser.entity.imaginary.CommentTreeElement; + +public class CommentTreeUtils { + + private CommentTreeUtils() { + // Empty to disallow the invocation of the default constructor for this utility class + } + + /** + * Flatten the comment tree. + * The original comment tree is not overwritten. + * + * @param cs List of comments that you get returned from one of the other methods here + * + * @return Flattened comment tree. + */ + public static List flattenCommentTree(List commentTree) { + List target = new ArrayList(); + flattenCommentTree(commentTree, target); + return target; + } + + /** + * Flatten the comment tree. + * The original comment tree is not overwritten. + * + * @param cs List of comments that you get returned from one of the other methods here + * @param target List in which to place the flattened comment tree. + */ + private static void flattenCommentTree(List commentTree, List target) { + for (CommentTreeElement c : commentTree) { + target.add(c); + if (c instanceof Comment) { + flattenCommentTree(((Comment)c).getReplies(), target); + } + } + } + + /** + * Get printable version of the given comment tree. + * + * @param cs List of comment tree elements + * + * @return Printable comment tree + */ + public static String printCommentTree(List cs) { + StringBuilder builder = new StringBuilder(); + for (CommentTreeElement c : cs) { + builder.append(printCommentTree(c, 0)); + } + return builder.toString(); + } + + /** + * Get printable version of a comment at a specific level.
+ *
+ * Note: uses unsafe recursion + * + * @param c Comment + * @param level Level to place at + * + * @return Printable comment tree + */ + private static String printCommentTree(CommentTreeElement c, int level) { + + // Initialize empty buffer + StringBuilder builder = new StringBuilder(); + + // Add tabulation + for (int i = 0; i < level; i++) { + builder.append("\t"); + } + + // Comment string + builder.append(c.toString()); + builder.append("\n"); + + // Iterate over children + if (c instanceof Comment) { + for (CommentTreeElement child : ((Comment) c).getReplies()) { + builder.append(printCommentTree(child, level + 1)); + } + } + + // Return the buffer + return builder.toString(); + + } + +} diff --git a/src/main/com/github/jreddit/parser/util/JsonUtils.java b/src/main/com/github/jreddit/parser/util/JsonUtils.java new file mode 100644 index 0000000..b0c776e --- /dev/null +++ b/src/main/com/github/jreddit/parser/util/JsonUtils.java @@ -0,0 +1,98 @@ +package com.github.jreddit.parser.util; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * Safe utilities (not throwing exceptions) for the conversion of JSON + * data into basic types such as Integer, Boolean, Long, and Double. + */ +public final class JsonUtils { + + private static final Logger LOGGER = LoggerFactory.getLogger(JsonUtils.class); + + private JsonUtils() { + // forbid creating JsonUtils instance + } + + /** + * Safely converts an object into string (used because sometimes JSONObject's get() method returns null). + * + * @param obj The object to convert. + * @return The string. + */ + public static String safeJsonToString(Object obj) { + return obj == null ? null : obj.toString(); + } + + /** + * Safely converts an object into an integer + * + * @param obj The object to convert. + * @return an Integer representing the integer value of the Object (null if the object cannot be converted to an Integer) + */ + public static Integer safeJsonToInteger(Object obj) { + Integer intValue = null; + + try { + String str = safeJsonToString(obj); + intValue = str != null ? Integer.parseInt(str) : null; + } catch (NumberFormatException e) { + LOGGER.warn("Safe JSON conversion to Integer failed", e); + } + + return intValue; + } + + /** + * Safely converts an object into an double + * + * @param obj The object to convert. + * @return a Double representing the double value of the Object (null if the object cannot be converted to Double) + */ + public static Double safeJsonToDouble(Object obj) { + Double doubleValue = null; + + try { + String str = safeJsonToString(obj); + doubleValue = str != null ? Double.parseDouble(str) : null; + } catch (NumberFormatException e) { + LOGGER.warn("Safe JSON conversion to Double failed", e); + } + + return doubleValue; + } + + + /** + * Safely converts an object into an boolean + * + * @param obj The object to convert. + * @return a Boolean representing the boolean value of the Object (null only if the object was also null) + */ + public static Boolean safeJsonToBoolean(Object obj) { + String str = safeJsonToString(obj); + Boolean booleanValue = str != null ? Boolean.parseBoolean(str) : null; + return booleanValue; + } + + /** + * Safely converts an object into an long + * + * @param obj The object to convert. + * @return a Long representing the long value of the Object (null if the object cannot be converted to Long) + */ + public static Long safeJsonToLong(Object obj) { + Long longValue = null; + + try { + String str = safeJsonToString(obj); + longValue = str != null ? Long.parseLong(str) : null; + } catch (NumberFormatException e) { + LOGGER.warn("Safe JSON conversion to Long failed", e); + } + + return longValue; + } + +} diff --git a/src/main/com/github/jreddit/request/RedditGetRequest.java b/src/main/com/github/jreddit/request/RedditGetRequest.java new file mode 100644 index 0000000..a0852ac --- /dev/null +++ b/src/main/com/github/jreddit/request/RedditGetRequest.java @@ -0,0 +1,48 @@ +package com.github.jreddit.request; + +import java.util.HashMap; +import java.util.Map; + +import com.github.jreddit.request.util.KeyValueFormatter; + +public abstract class RedditGetRequest { + + /** Mapping of all request parameters. */ + private Map parameters; + + /** + * Default constructor. + */ + public RedditGetRequest() { + parameters = new HashMap(); + } + + /** + * Add a parameter to the request. + * If the key of the parameter already exists, the previous value will be overwritten. + * + * @param key Key of the parameter (e.g. "limit") + * @param value Value of the parameter (e.g. "100") + */ + protected void addParameter(String key, String value) { + parameters.put(key, value); + } + + /** + * Generate the query parameters to be added to an URL. + * + * @return Parameters (e.g. "limit=100&sort=top") + */ + protected String generateParameters() { + return KeyValueFormatter.format(parameters, true); + } + + /** + * Generate the URI of the request. + * Be sure to call {@link #generateParameters()} to add the parameters to the end of the URL. + * + * @return Reddit Uniform Resource Identifier (e.g. "/usr/endpoint?limit=100&sort=top") + */ + public abstract String generateRedditURI(); + +} diff --git a/src/main/com/github/jreddit/request/RedditPostRequest.java b/src/main/com/github/jreddit/request/RedditPostRequest.java new file mode 100644 index 0000000..5f76922 --- /dev/null +++ b/src/main/com/github/jreddit/request/RedditPostRequest.java @@ -0,0 +1,86 @@ +package com.github.jreddit.request; + +import java.util.HashMap; +import java.util.Map; + +import com.github.jreddit.request.util.KeyValueFormatter; + +public abstract class RedditPostRequest { + + /** Mapping of all request query parameters. */ + private Map queryParameters; + + /** Mapping of all request body parameters. */ + private Map bodyParameters; + + /** + * Default constructor. + */ + public RedditPostRequest() { + queryParameters = new HashMap(); + bodyParameters = new HashMap(); + } + + /** + * Add a parameter to the query of the request. + * If the key of the parameter already exists, the previous value will be overwritten. + * + * @param key Key of the parameter (e.g. "limit") + * @param value Value of the parameter (e.g. "100") + */ + protected void addQueryParameter(String key, String value) { + queryParameters.put(key, value); + } + + /** + * Add a parameter to the body of the request. + * If the key of the parameter already exists, the previous value will be overwritten. + * + * @param key Key of the parameter (e.g. "id") + * @param value Value of the parameter (e.g. "dajkjsf8") + */ + protected void addBodyParameter(String key, String value) { + bodyParameters.put(key, value); + } + + /** + * Generate the query parameters to be added to an URI.
+ *
+ * Note: values are encoded. + * + * @return Query parameters (e.g. "limit=100&sort=top") + */ + protected String generateQueryParameters() { + return KeyValueFormatter.format(queryParameters, true); + } + + /** + * Generate the body parameters to be added.
+ *
+ * Note: values are encoded. + * + * @return Body parameters (e.g. "limit=100&sort=top") + */ + protected String generateBodyParameters() { + return KeyValueFormatter.format(bodyParameters, true); + } + + /** + * Generate the URI of the request. + * Be sure to call {@link #generateQueryParameters()} in your implementation + * to add the parameters to the end of the URL. + * + * @return Reddit Uniform Resource Identifier (e.g. "/usr/endpoint?limit=100&sort=top") + */ + public abstract String generateRedditURI(); + + /** + * Generate the body for a POST request using the POST parameters. + + * @return Body (e.g. "limit=100&sort=top" for parameters limit: 100 and sort: "top") + */ + public String generateBody() { + return generateBodyParameters(); + } + +} diff --git a/src/main/com/github/jreddit/request/action/MarkActionRequest.java b/src/main/com/github/jreddit/request/action/MarkActionRequest.java new file mode 100644 index 0000000..9e2e640 --- /dev/null +++ b/src/main/com/github/jreddit/request/action/MarkActionRequest.java @@ -0,0 +1,16 @@ +package com.github.jreddit.request.action; + +import com.github.jreddit.request.RedditPostRequest; + +public abstract class MarkActionRequest extends RedditPostRequest { + + /** + * Action request. + * + * @param fullname The fullname of the target (e.g. "t3_djkfsjka") + */ + public MarkActionRequest(String fullname) { + this.addBodyParameter("id", fullname); + } + +} diff --git a/src/main/com/github/jreddit/request/action/flair/DeleteFlairRequest.java b/src/main/com/github/jreddit/request/action/flair/DeleteFlairRequest.java new file mode 100644 index 0000000..7e3e050 --- /dev/null +++ b/src/main/com/github/jreddit/request/action/flair/DeleteFlairRequest.java @@ -0,0 +1,19 @@ +package com.github.jreddit.request.action.flair; + +import com.github.jreddit.request.RedditPostRequest; + +public class DeleteFlairRequest extends RedditPostRequest { + + /** Endpoint format. */ + private static final String ENDPOINT_FORMAT = "/api/deleteflair.json?"; + + public DeleteFlairRequest(String username) { + this.addBodyParameter("name", username); + } + + @Override + public String generateRedditURI() { + return ENDPOINT_FORMAT; + } + +} diff --git a/src/main/com/github/jreddit/request/action/mark/HideRequest.java b/src/main/com/github/jreddit/request/action/mark/HideRequest.java new file mode 100644 index 0000000..016189d --- /dev/null +++ b/src/main/com/github/jreddit/request/action/mark/HideRequest.java @@ -0,0 +1,20 @@ +package com.github.jreddit.request.action.mark; + +import com.github.jreddit.request.action.MarkActionRequest; + + +public class HideRequest extends MarkActionRequest { + + /** Endpoint format. */ + private static final String ENDPOINT_FORMAT = "/api/hide.json?"; + + public HideRequest(String fullname) { + super(fullname); + } + + @Override + public String generateRedditURI() { + return ENDPOINT_FORMAT; + } + +} diff --git a/src/main/com/github/jreddit/request/action/mark/MarkNsfwRequest.java b/src/main/com/github/jreddit/request/action/mark/MarkNsfwRequest.java new file mode 100644 index 0000000..2f9efeb --- /dev/null +++ b/src/main/com/github/jreddit/request/action/mark/MarkNsfwRequest.java @@ -0,0 +1,20 @@ +package com.github.jreddit.request.action.mark; + +import com.github.jreddit.request.action.MarkActionRequest; + + +public class MarkNsfwRequest extends MarkActionRequest { + + /** Endpoint format. */ + private static final String ENDPOINT_FORMAT = "/api/marknsfw.json?"; + + public MarkNsfwRequest(String fullname) { + super(fullname); + } + + @Override + public String generateRedditURI() { + return ENDPOINT_FORMAT; + } + +} diff --git a/src/main/com/github/jreddit/request/action/mark/ReportRequest.java b/src/main/com/github/jreddit/request/action/mark/ReportRequest.java new file mode 100644 index 0000000..030ca42 --- /dev/null +++ b/src/main/com/github/jreddit/request/action/mark/ReportRequest.java @@ -0,0 +1,20 @@ +package com.github.jreddit.request.action.mark; + +import com.github.jreddit.request.action.MarkActionRequest; + + +public class ReportRequest extends MarkActionRequest { + + /** Endpoint format. */ + private static final String ENDPOINT_FORMAT = "/api/report.json?"; + + public ReportRequest(String fullname) { + super(fullname); + } + + @Override + public String generateRedditURI() { + return ENDPOINT_FORMAT; + } + +} diff --git a/src/main/com/github/jreddit/request/action/mark/SaveRequest.java b/src/main/com/github/jreddit/request/action/mark/SaveRequest.java new file mode 100644 index 0000000..c743480 --- /dev/null +++ b/src/main/com/github/jreddit/request/action/mark/SaveRequest.java @@ -0,0 +1,20 @@ +package com.github.jreddit.request.action.mark; + +import com.github.jreddit.request.action.MarkActionRequest; + + +public class SaveRequest extends MarkActionRequest { + + /** Endpoint format. */ + private static final String ENDPOINT_FORMAT = "/api/save.json?"; + + public SaveRequest(String fullname) { + super(fullname); + } + + @Override + public String generateRedditURI() { + return ENDPOINT_FORMAT; + } + +} diff --git a/src/main/com/github/jreddit/request/action/mark/UnhideRequest.java b/src/main/com/github/jreddit/request/action/mark/UnhideRequest.java new file mode 100644 index 0000000..782adb1 --- /dev/null +++ b/src/main/com/github/jreddit/request/action/mark/UnhideRequest.java @@ -0,0 +1,20 @@ +package com.github.jreddit.request.action.mark; + +import com.github.jreddit.request.action.MarkActionRequest; + + +public class UnhideRequest extends MarkActionRequest { + + /** Endpoint format. */ + private static final String ENDPOINT_FORMAT = "/api/unhide.json?"; + + public UnhideRequest(String fullname) { + super(fullname); + } + + @Override + public String generateRedditURI() { + return ENDPOINT_FORMAT; + } + +} diff --git a/src/main/com/github/jreddit/request/action/mark/UnmarkNsfwRequest.java b/src/main/com/github/jreddit/request/action/mark/UnmarkNsfwRequest.java new file mode 100644 index 0000000..16c2f14 --- /dev/null +++ b/src/main/com/github/jreddit/request/action/mark/UnmarkNsfwRequest.java @@ -0,0 +1,20 @@ +package com.github.jreddit.request.action.mark; + +import com.github.jreddit.request.action.MarkActionRequest; + + +public class UnmarkNsfwRequest extends MarkActionRequest { + + /** Endpoint format. */ + private static final String ENDPOINT_FORMAT = "/api/unmarknsfw.json?"; + + public UnmarkNsfwRequest(String fullname) { + super(fullname); + } + + @Override + public String generateRedditURI() { + return ENDPOINT_FORMAT; + } + +} diff --git a/src/main/com/github/jreddit/request/action/mark/UnsaveRequest.java b/src/main/com/github/jreddit/request/action/mark/UnsaveRequest.java new file mode 100644 index 0000000..42d363c --- /dev/null +++ b/src/main/com/github/jreddit/request/action/mark/UnsaveRequest.java @@ -0,0 +1,20 @@ +package com.github.jreddit.request.action.mark; + +import com.github.jreddit.request.action.MarkActionRequest; + + +public class UnsaveRequest extends MarkActionRequest { + + /** Endpoint format. */ + private static final String ENDPOINT_FORMAT = "/api/unsave.json?"; + + public UnsaveRequest(String fullname) { + super(fullname); + } + + @Override + public String generateRedditURI() { + return ENDPOINT_FORMAT; + } + +} diff --git a/src/main/com/github/jreddit/request/action/mark/VoteRequest.java b/src/main/com/github/jreddit/request/action/mark/VoteRequest.java new file mode 100644 index 0000000..3306ecf --- /dev/null +++ b/src/main/com/github/jreddit/request/action/mark/VoteRequest.java @@ -0,0 +1,27 @@ +package com.github.jreddit.request.action.mark; + +import com.github.jreddit.request.action.MarkActionRequest; + + +public class VoteRequest extends MarkActionRequest { + + /** Endpoint format. */ + private static final String ENDPOINT_FORMAT = "/api/vote.json?"; + + /** + * Vote request constructor. + * + * @param fullname Fullname of what to vote on + * @param direction Direction (must be -1, 0, or 1) + */ + public VoteRequest(String fullname, int direction) { + super(fullname); + this.addBodyParameter("dir", String.valueOf(direction)); + } + + @Override + public String generateRedditURI() { + return ENDPOINT_FORMAT; + } + +} diff --git a/src/main/com/github/jreddit/request/retrieval/ListingRequest.java b/src/main/com/github/jreddit/request/retrieval/ListingRequest.java new file mode 100644 index 0000000..c36687f --- /dev/null +++ b/src/main/com/github/jreddit/request/retrieval/ListingRequest.java @@ -0,0 +1,62 @@ +package com.github.jreddit.request.retrieval; + +import com.github.jreddit.parser.entity.Thing; +import com.github.jreddit.request.RedditGetRequest; + +public abstract class ListingRequest extends RedditGetRequest { + + /** + * @param count Starting count for first element of listing + * @return This request + */ + public ListingRequest setCount(int count) { + this.addParameter("count", String.valueOf(count)); + return this; + } + + /** + * @param limit Maximum number of listing elements. This does not mean exactly this parameter will be returned. An upper bound (~100) is imposed by reddit. + * @return This request + */ + public ListingRequest setLimit(int limit) { + this.addParameter("limit", String.valueOf(limit)); + return this; + } + + /** + * @param after The thing in a listing after which the newly requested listing should start. + * @return This request + */ + public ListingRequest setAfter(Thing after) { + return setAfter(after.getFullName()); + } + + /** + * @param after The fullname of a thing in a listing after which the newly requested listing should start. + * @return This request + * @see ListingRequest#setAfter(Thing) The usage of setAfter(Thing) is preferred over this method + */ + public ListingRequest setAfter(String after) { + this.addParameter("after", after); + return this; + } + + /** + * @param before The thing in a listing before which the newly requested listing should end. + * @return This request + */ + public ListingRequest setBefore(Thing before) { + return setBefore(before.getFullName()); + } + + /** + * @param before The fullname of a thing in a listing before which the newly requested listing should end. + * @return This request + * @see ListingRequest#setBefore(Thing) The usage of setBefore(Thing) is preferred over this method + */ + public ListingRequest setBefore(String before) { + this.addParameter("before", before); + return this; + } + +} diff --git a/src/main/com/github/jreddit/request/retrieval/comments/CommentsOfUserRequest.java b/src/main/com/github/jreddit/request/retrieval/comments/CommentsOfUserRequest.java new file mode 100644 index 0000000..9aabc4b --- /dev/null +++ b/src/main/com/github/jreddit/request/retrieval/comments/CommentsOfUserRequest.java @@ -0,0 +1,40 @@ +package com.github.jreddit.request.retrieval.comments; + +import com.github.jreddit.request.retrieval.ListingRequest; +import com.github.jreddit.request.retrieval.param.TimeSpan; +import com.github.jreddit.request.retrieval.param.UserOverviewSort; + +public class CommentsOfUserRequest extends ListingRequest { + + private static final String ENDPOINT_FORMAT = "/user/%s/comments.json?%s"; + + String username; + + /** + * @param username Username of a user (e.g. "JohnM") + */ + public CommentsOfUserRequest(String username) { + this.username = username; + } + + public CommentsOfUserRequest setSort(UserOverviewSort sort) { + this.addParameter("sort", sort.value()); + return this; + } + + public CommentsOfUserRequest setShowGiven() { + this.addParameter("show", "given"); + return this; + } + + public CommentsOfUserRequest setTime(TimeSpan time) { + this.addParameter("t", time.value()); + return this; + } + + @Override + public String generateRedditURI() { + return String.format(ENDPOINT_FORMAT, username, this.generateParameters()); + } + +} diff --git a/src/main/com/github/jreddit/request/retrieval/comments/MoreCommentsRequest.java b/src/main/com/github/jreddit/request/retrieval/comments/MoreCommentsRequest.java new file mode 100644 index 0000000..bbe9477 --- /dev/null +++ b/src/main/com/github/jreddit/request/retrieval/comments/MoreCommentsRequest.java @@ -0,0 +1,37 @@ +package com.github.jreddit.request.retrieval.comments; + +import java.util.List; + +import com.github.jreddit.request.RedditGetRequest; +import com.github.jreddit.request.retrieval.param.CommentSort; +import com.github.jreddit.request.util.KeyValueFormatter; + +public class MoreCommentsRequest extends RedditGetRequest { + + /** Endpoint format. */ + private static final String ENDPOINT_FORMAT = "/api/morechildren.json?%s"; + + /** + * @param submissionFullname Fullname of the submission (e.g. "t3_dajdkjf") + * @param commentIdentifiers List of comment ID36 identifiers (e.g. ["jdafid9", "jdafid10"]) + * + * @see {@link com.github.jreddit.parser.entity.More#getChildren()} is typically used to retrieve the 2nd parameter + */ + public MoreCommentsRequest(String submissionFullname, List commentIdentifiers) { + // Neglected optional "id" parameter, as it is only relevant for HTML + this.addParameter("api_type", "json"); + this.addParameter("link_id", submissionFullname); + this.addParameter("children", KeyValueFormatter.formatCommaSeparatedList(commentIdentifiers)); + } + + public MoreCommentsRequest setSort(CommentSort sort) { + this.addParameter("sort", sort.value()); + return this; + } + + @Override + public String generateRedditURI() { + return String.format(ENDPOINT_FORMAT, this.generateParameters()); + } + +} diff --git a/src/main/com/github/jreddit/request/retrieval/mixed/FullSubmissionRequest.java b/src/main/com/github/jreddit/request/retrieval/mixed/FullSubmissionRequest.java new file mode 100644 index 0000000..80da04c --- /dev/null +++ b/src/main/com/github/jreddit/request/retrieval/mixed/FullSubmissionRequest.java @@ -0,0 +1,110 @@ +package com.github.jreddit.request.retrieval.mixed; + +import com.github.jreddit.parser.entity.Comment; +import com.github.jreddit.parser.entity.Submission; +import com.github.jreddit.request.RedditGetRequest; +import com.github.jreddit.request.retrieval.param.CommentSort; + +public class FullSubmissionRequest extends RedditGetRequest { + + private static final String ENDPOINT_FORMAT = "/comments/%s.json?%s"; + + private String submissionIdentifier; + + /** + * @param submissionIdentifier Submission ID36 identifier (e.g. "dfjIuf") + * + * @see {@link FullSubmissionRequest(Submission)} is preferred over this constructor + */ + public FullSubmissionRequest(String submissionIdentifier) { + this.submissionIdentifier = submissionIdentifier; + } + + /** + * @param submission Submission ID36 identifier (e.g. "dfjIuf") + */ + public FullSubmissionRequest(Submission submission) { + this(submission.getIdentifier()); + } + + public FullSubmissionRequest setSort(CommentSort sort) { + this.addParameter("sort", sort.value()); + return this; + } + + public FullSubmissionRequest setLimit(int limit) { + this.addParameter("limit", String.valueOf(limit)); + return this; + } + + /** + * Set the comment that will be the (highlighted) focal point of the + * returned view and context will be the number of parents shown. + * + * @param commentIdentifier Comment ID36 identifier + * + * @return This builder + * + * @see Comment#getIdentifier() + * @see {@link #setContext(int)} + */ + public FullSubmissionRequest setComment(String commentIdentifier) { + this.addParameter("comment", commentIdentifier); + return this; + } + + /** + * Set the number of parents shown. This will only affect the result if + * {@link #setComment(String)} has been set. + * + * @param context Maximum number of parents shown (integer between 0 and 8) + * + * @return This builder + */ + public FullSubmissionRequest setContext(int context) { + this.addParameter("context", String.valueOf(context)); + return this; + } + + /** + * Set the maximum depth of subtrees in the thread. + * + * @param depth An integer indicating maximum depth + * + * @return This builder + */ + public FullSubmissionRequest setDepth(int depth) { + this.addParameter("depth", String.valueOf(depth)); + return this; + } + + /** + * Set whether or not to show the edits in the comments. + * + * @param showEdits Should the edits be shown? + * + * @return This builder + */ + public FullSubmissionRequest setShowEdits(boolean showEdits) { + this.addParameter("showedits", String.valueOf(showEdits)); + return this; + } + + /** + * Set whether or not the "more" buttons should be shown. + * + * @param showMore Should the more buttons be shown? + * + * @return This builder + */ + public FullSubmissionRequest setShowMore(boolean showMore) { + this.addParameter("showmore", String.valueOf(showMore)); + return this; + } + + @Override + public String generateRedditURI() { + return String.format(ENDPOINT_FORMAT, submissionIdentifier, this.generateParameters()); + } + +} diff --git a/src/main/com/github/jreddit/request/retrieval/mixed/MixedOfUserRequest.java b/src/main/com/github/jreddit/request/retrieval/mixed/MixedOfUserRequest.java new file mode 100644 index 0000000..f0acb28 --- /dev/null +++ b/src/main/com/github/jreddit/request/retrieval/mixed/MixedOfUserRequest.java @@ -0,0 +1,63 @@ +package com.github.jreddit.request.retrieval.mixed; + +import com.github.jreddit.request.retrieval.ListingRequest; +import com.github.jreddit.request.retrieval.param.TimeSpan; +import com.github.jreddit.request.retrieval.param.UserMixedCategory; +import com.github.jreddit.request.retrieval.param.UserOverviewSort; + +public class MixedOfUserRequest extends ListingRequest { + + /** Endpoint format. */ + private static final String ENDPOINT_FORMAT = "/user/%s/%s.json?%s"; + + private UserMixedCategory category; + private String username; + + /** + * @param username Username of a user (e.g. "JohnM") + * @param category Category of mixed things + */ + public MixedOfUserRequest(String username, UserMixedCategory category) { + this.username = username; + this.category = category; + } + + /** + * Set the sorting method.
+ *
+ * Note: This only works for Overview + * + * @param sort Sorting method + * + * @return This builder + */ + public MixedOfUserRequest setSort(UserOverviewSort sort) { + this.addParameter("sort", sort.value()); + return this; + } + + /** + * Set the time span.
+ *
+ * Note: This only works for Overview, and then specifically for the top/controversial sorting method. + * + * @param time Time span + * + * @return This builder + */ + public MixedOfUserRequest setTime(TimeSpan time) { + this.addParameter("t", time.value()); + return this; + } + + public MixedOfUserRequest setShowGiven() { + this.addParameter("show", "given"); + return this; + } + + @Override + public String generateRedditURI() { + return String.format(ENDPOINT_FORMAT, username, category.value(), this.generateParameters()); + } + +} diff --git a/src/main/com/github/jreddit/request/retrieval/param/CommentSort.java b/src/main/com/github/jreddit/request/retrieval/param/CommentSort.java new file mode 100644 index 0000000..4d1021c --- /dev/null +++ b/src/main/com/github/jreddit/request/retrieval/param/CommentSort.java @@ -0,0 +1,29 @@ +package com.github.jreddit.request.retrieval.param; + +/** + * Enum to represent comment sorts on Reddit. You see these on a page that lists comments. + * + * @author Evin Ugur + * @author Raul Rene Lepsa + * @author Simon Kassing + */ +public enum CommentSort { + + CONFIDENCE("confidence"), + NEW("new"), + TOP("top"), + CONTROVERSIAL("controversial"), + OLD("old"), + QA("qa"); + + private final String value; + + CommentSort(String value) { + this.value = value; + } + + public String value() { + return this.value; + } + +} diff --git a/src/main/com/github/jreddit/request/retrieval/param/QuerySyntax.java b/src/main/com/github/jreddit/request/retrieval/param/QuerySyntax.java new file mode 100644 index 0000000..7437459 --- /dev/null +++ b/src/main/com/github/jreddit/request/retrieval/param/QuerySyntax.java @@ -0,0 +1,22 @@ +package com.github.jreddit.request.retrieval.param; + +/** + * Enum to represent the possible query syntaxes. + * @author Simon Kassing + */ +public enum QuerySyntax { + + CLOUDSEARCH ("cloudsearch"), + LUCENE ("lucene"), + PLAIN ("plain"); + + private final String value; + + QuerySyntax(String value) { + this.value = value; + } + + public String value() { + return this.value; + } +} diff --git a/src/main/com/github/jreddit/request/retrieval/param/SearchSort.java b/src/main/com/github/jreddit/request/retrieval/param/SearchSort.java new file mode 100644 index 0000000..0f7396d --- /dev/null +++ b/src/main/com/github/jreddit/request/retrieval/param/SearchSort.java @@ -0,0 +1,25 @@ +package com.github.jreddit.request.retrieval.param; + +/** + * Enumeration to represent the different sort methods for submission search. + * @author Simon Kassing + */ +public enum SearchSort { + + HOT("hot"), + RELEVANCE("relevance"), + NEW("new"), + TOP("top"), + COMMENTS("comments"); + + private final String value; + + SearchSort(String value) { + this.value = value; + } + + public String value() { + return this.value; + } + +} diff --git a/src/main/com/github/jreddit/request/retrieval/param/SubmissionSort.java b/src/main/com/github/jreddit/request/retrieval/param/SubmissionSort.java new file mode 100644 index 0000000..ba37c0a --- /dev/null +++ b/src/main/com/github/jreddit/request/retrieval/param/SubmissionSort.java @@ -0,0 +1,27 @@ +package com.github.jreddit.request.retrieval.param; + +/** + * Enum to represent submission sorts on Reddit. You see these on a page that lists Submissions. + * + * @author Evin Ugur + * @author Raul Rene Lepsa + * @author Simon Kassing + */ +public enum SubmissionSort { + + HOT("hot"), + NEW("new"), + RISING("rising"), + CONTROVERSIAL("controversial"), + TOP("top"); + + private final String value; + + SubmissionSort(String value) { + this.value = value; + } + + public String value() { + return this.value; + } +} diff --git a/src/main/com/github/jreddit/request/retrieval/param/SubredditsView.java b/src/main/com/github/jreddit/request/retrieval/param/SubredditsView.java new file mode 100644 index 0000000..e7b97aa --- /dev/null +++ b/src/main/com/github/jreddit/request/retrieval/param/SubredditsView.java @@ -0,0 +1,25 @@ +package com.github.jreddit.request.retrieval.param; + +/** + * Enumeration to represent the different subreddit categories. + * @author Simon Kassing + */ +public enum SubredditsView { + + NEW("new"), + POPULAR("popular"), + MINE_SUBSCRIBER("mine/subscriber"), + MINE_CONTRIBUTOR("mine/contributor"), + MINE_MODERATOR("mine/moderator"); + + private final String value; + + SubredditsView(String value) { + this.value = value; + } + + public String value() { + return this.value; + } + +} diff --git a/src/main/com/github/jreddit/request/retrieval/param/TimeSpan.java b/src/main/com/github/jreddit/request/retrieval/param/TimeSpan.java new file mode 100644 index 0000000..1d9315d --- /dev/null +++ b/src/main/com/github/jreddit/request/retrieval/param/TimeSpan.java @@ -0,0 +1,26 @@ +package com.github.jreddit.request.retrieval.param; + +/** + * Enumeration to represent the different submission search times. + * @author Simon Kassing + */ +public enum TimeSpan { + + HOUR("hour"), + DAY("day"), + WEEK("week"), + MONTH("month"), + YEAR("year"), + ALL("all"); + + private final String value; + + TimeSpan(String value) { + this.value = value; + } + + public String value() { + return this.value; + } + +} diff --git a/src/main/com/github/jreddit/request/retrieval/param/UserMixedCategory.java b/src/main/com/github/jreddit/request/retrieval/param/UserMixedCategory.java new file mode 100644 index 0000000..0d1d520 --- /dev/null +++ b/src/main/com/github/jreddit/request/retrieval/param/UserMixedCategory.java @@ -0,0 +1,20 @@ +package com.github.jreddit.request.retrieval.param; + +public enum UserMixedCategory { + + OVERVIEW("overview"), + GILDED_RECEIVED("gilded"), + GILDED_GIVEN("gilded/given"), + SAVED("saved"); + + private final String value; + + UserMixedCategory(String value) { + this.value = value; + } + + public String value() { + return this.value; + } + +} diff --git a/src/main/com/github/jreddit/request/retrieval/param/UserOverviewSort.java b/src/main/com/github/jreddit/request/retrieval/param/UserOverviewSort.java new file mode 100644 index 0000000..2f0ba28 --- /dev/null +++ b/src/main/com/github/jreddit/request/retrieval/param/UserOverviewSort.java @@ -0,0 +1,24 @@ +package com.github.jreddit.request.retrieval.param; + +/** + * Enumeration to represent the different sort methods for the user overview. + * @author Simon Kassing + */ +public enum UserOverviewSort { + + NEW("new"), + HOT("hot"), + TOP("top"), + COMMENTS("controversial"); + + private final String value; + + UserOverviewSort(String value) { + this.value = value; + } + + public String value() { + return this.value; + } + +} diff --git a/src/main/com/github/jreddit/request/retrieval/param/UserSubmissionsCategory.java b/src/main/com/github/jreddit/request/retrieval/param/UserSubmissionsCategory.java new file mode 100644 index 0000000..dc09074 --- /dev/null +++ b/src/main/com/github/jreddit/request/retrieval/param/UserSubmissionsCategory.java @@ -0,0 +1,20 @@ +package com.github.jreddit.request.retrieval.param; + +public enum UserSubmissionsCategory { + + SUBMITTED("submitted"), + UPVOTED("upvoted"), + DOWNVOTED("downvoted"), + HIDDEN("hidden"); + + private final String value; + + UserSubmissionsCategory(String value) { + this.value = value; + } + + public String value() { + return this.value; + } + +} diff --git a/src/main/com/github/jreddit/request/retrieval/submissions/SubmissionsOfSubredditRequest.java b/src/main/com/github/jreddit/request/retrieval/submissions/SubmissionsOfSubredditRequest.java new file mode 100644 index 0000000..f0f0a53 --- /dev/null +++ b/src/main/com/github/jreddit/request/retrieval/submissions/SubmissionsOfSubredditRequest.java @@ -0,0 +1,32 @@ +package com.github.jreddit.request.retrieval.submissions; + +import com.github.jreddit.request.retrieval.ListingRequest; +import com.github.jreddit.request.retrieval.param.SubmissionSort; + +public class SubmissionsOfSubredditRequest extends ListingRequest { + + private static final String ENDPOINT_FORMAT = "/r/%s/%s.json?%s"; + + private SubmissionSort sort; + private String subreddit; + + /** + * @param subreddit Subreddit (e.g. "funny") + * @param sort Sorting method + */ + public SubmissionsOfSubredditRequest(String subreddit, SubmissionSort sort) { + this.subreddit = subreddit; + this.sort = sort; + } + + public SubmissionsOfSubredditRequest setShowAll() { + this.addParameter("show", "all"); + return this; + } + + @Override + public String generateRedditURI() { + return String.format(ENDPOINT_FORMAT, subreddit, sort.value(), this.generateParameters()); + } + +} diff --git a/src/main/com/github/jreddit/request/retrieval/submissions/SubmissionsOfUserRequest.java b/src/main/com/github/jreddit/request/retrieval/submissions/SubmissionsOfUserRequest.java new file mode 100644 index 0000000..5dd07dc --- /dev/null +++ b/src/main/com/github/jreddit/request/retrieval/submissions/SubmissionsOfUserRequest.java @@ -0,0 +1,44 @@ +package com.github.jreddit.request.retrieval.submissions; + +import com.github.jreddit.request.retrieval.ListingRequest; +import com.github.jreddit.request.retrieval.param.TimeSpan; +import com.github.jreddit.request.retrieval.param.UserOverviewSort; +import com.github.jreddit.request.retrieval.param.UserSubmissionsCategory; + +public class SubmissionsOfUserRequest extends ListingRequest { + + private static final String ENDPOINT_FORMAT = "/user/%s/%s.json?%s"; + + private UserSubmissionsCategory category; + private String username; + + /** + * @param username Username of a user (e.g. "JohnM") + * @param category Category of user submissions + */ + public SubmissionsOfUserRequest(String username, UserSubmissionsCategory category) { + this.username = username; + this.category = category; + } + + public SubmissionsOfUserRequest setSort(UserOverviewSort sort) { + this.addParameter("sort", sort.value()); + return this; + } + + public SubmissionsOfUserRequest setTime(TimeSpan time) { + this.addParameter("t", time.value()); + return this; + } + + public SubmissionsOfUserRequest setShowGiven() { + this.addParameter("show", "given"); + return this; + } + + @Override + public String generateRedditURI() { + return String.format(ENDPOINT_FORMAT, username, category.value(), this.generateParameters()); + } + +} diff --git a/src/main/com/github/jreddit/request/retrieval/submissions/SubmissionsSearchRequest.java b/src/main/com/github/jreddit/request/retrieval/submissions/SubmissionsSearchRequest.java new file mode 100644 index 0000000..1d170a5 --- /dev/null +++ b/src/main/com/github/jreddit/request/retrieval/submissions/SubmissionsSearchRequest.java @@ -0,0 +1,44 @@ +package com.github.jreddit.request.retrieval.submissions; + +import com.github.jreddit.request.retrieval.ListingRequest; +import com.github.jreddit.request.retrieval.param.QuerySyntax; +import com.github.jreddit.request.retrieval.param.SearchSort; +import com.github.jreddit.request.retrieval.param.TimeSpan; + +public class SubmissionsSearchRequest extends ListingRequest { + + private static final String ENDPOINT_FORMAT = "/search.json?%s"; + + /** + * @param query Mandatory search query (e.g. "programming Java"), its syntax depends on what is set using {@link #setSyntax(QuerySyntax)}. + */ + public SubmissionsSearchRequest(String query) { + this.addParameter("q", query); + } + + public SubmissionsSearchRequest setSyntax(QuerySyntax syntax) { + this.addParameter("syntax", syntax.value()); + return this; + } + + public SubmissionsSearchRequest setSort(SearchSort sort) { + this.addParameter("sort", sort.value()); + return this; + } + + public SubmissionsSearchRequest setTimeSpan(TimeSpan time) { + this.addParameter("t", time.value()); + return this; + } + + public SubmissionsSearchRequest setShowAll() { + this.addParameter("show", "all"); + return this; + } + + @Override + public String generateRedditURI() { + return String.format(ENDPOINT_FORMAT, this.generateParameters()); + } + +} diff --git a/src/main/com/github/jreddit/request/retrieval/subreddits/SubredditsOfUserRequest.java b/src/main/com/github/jreddit/request/retrieval/subreddits/SubredditsOfUserRequest.java new file mode 100644 index 0000000..f92b149 --- /dev/null +++ b/src/main/com/github/jreddit/request/retrieval/subreddits/SubredditsOfUserRequest.java @@ -0,0 +1,29 @@ +package com.github.jreddit.request.retrieval.subreddits; + +import com.github.jreddit.request.retrieval.ListingRequest; +import com.github.jreddit.request.retrieval.param.SubredditsView; + +public class SubredditsOfUserRequest extends ListingRequest { + + private static final String ENDPOINT_FORMAT = "/subreddits/%s.json?%s"; // ApiEndpointUtils.SUBREDDITS_GET + + private SubredditsView view; + + /** + * @param view View of the subreddits + */ + public SubredditsOfUserRequest(SubredditsView view) { + this.view = view; + } + + public SubredditsOfUserRequest setShowAll() { + this.addParameter("show", "all"); + return this; + } + + @Override + public String generateRedditURI() { + return String.format(ENDPOINT_FORMAT, view, this.generateParameters()); + } + +} diff --git a/src/main/com/github/jreddit/request/retrieval/subreddits/SubredditsSearchRequest.java b/src/main/com/github/jreddit/request/retrieval/subreddits/SubredditsSearchRequest.java new file mode 100644 index 0000000..8b3c597 --- /dev/null +++ b/src/main/com/github/jreddit/request/retrieval/subreddits/SubredditsSearchRequest.java @@ -0,0 +1,21 @@ +package com.github.jreddit.request.retrieval.subreddits; + +import com.github.jreddit.request.retrieval.ListingRequest; + +public class SubredditsSearchRequest extends ListingRequest { + + private static final String ENDPOINT_FORMAT = "/subreddits/search.json?%s"; + + /** + * @param query Search query (e.g. "programming") + */ + public SubredditsSearchRequest(String query) { + this.addParameter("q", query); + } + + @Override + public String generateRedditURI() { + return String.format(ENDPOINT_FORMAT, this.generateParameters()); + } + +} diff --git a/src/main/com/github/jreddit/request/util/KeyValueFormatter.java b/src/main/com/github/jreddit/request/util/KeyValueFormatter.java new file mode 100644 index 0000000..f6ee955 --- /dev/null +++ b/src/main/com/github/jreddit/request/util/KeyValueFormatter.java @@ -0,0 +1,86 @@ +package com.github.jreddit.request.util; + +import java.io.UnsupportedEncodingException; +import java.net.URLEncoder; +import java.util.List; +import java.util.Map; +import java.util.Set; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +public class KeyValueFormatter { + + private static final Logger LOGGER = LoggerFactory.getLogger(KeyValueFormatter.class); + + private KeyValueFormatter() { + // Left empty to prevent instantiation of this utility class + } + + /** + * Format a mapping of key-value pairs to a string, separating key from value using an + * equals (=) sign, and separating pairs using an ampersand (&) sign. + * + * @param keyValueParameters Mapping of key-value pairs + * @param encodeUTF8 Whether or not the values should be encoded in UTF-8 + * + * @return Formatted string of key-value pairs (e.g. "a=1&b=something") + */ + public static String format(Map keyValueParameters, boolean encodeUTF8) { + + // Key set + Set keys = keyValueParameters.keySet(); + + // Iterate over keys + String paramsString = ""; + boolean start = true; + for (String key : keys) { + + // Add separation ampersand + if (!start) { + paramsString = paramsString.concat("&"); + } else { + start = false; + } + + // Retrieve value + String value = keyValueParameters.get(key); + + // Encode key + if (encodeUTF8) { + try { + value = URLEncoder.encode(value, "UTF-8"); + } catch (UnsupportedEncodingException e) { + LOGGER.warn("Unsupported Encoding Exception thrown when encoding value", e); + } + } + + // Add key-value pair + paramsString = paramsString.concat(key + "=" + value); + + } + + // Return final parameter string + return paramsString; + + } + + /** + * Format a comma separated list of the given list. + * + * @param list List of strings + * + * @return Comma-separated list string (e.g. "a,b,c,d") + */ + public static String formatCommaSeparatedList(List list) { + StringBuilder builder = new StringBuilder(); + for (int i = 0; i < list.size(); i++) { + if (i != 0) { + builder.append(","); + } + builder.append(list.get(i)); + } + return builder.toString(); + } + +} diff --git a/src/main/sh/adb/RandomRedditMemesAPI/Main.java b/src/main/sh/adb/RandomRedditMemesAPI/Main.java index f73a2ae..05e2079 100644 --- a/src/main/sh/adb/RandomRedditMemesAPI/Main.java +++ b/src/main/sh/adb/RandomRedditMemesAPI/Main.java @@ -10,7 +10,6 @@ import java.io.OutputStream; import java.net.InetSocketAddress; public class Main { - public static void main(String[] args) throws IOException { HttpServer server = HttpServer.create(new InetSocketAddress(8000), 0); server.createContext("", new ErrorHandler()); @@ -19,7 +18,6 @@ public class Main { server.setExecutor(null); server.start(); } - } class CommunityHandler implements HttpHandler { @Override diff --git a/src/main/sh/adb/RandomRedditMemesAPI/RedditAPI.java b/src/main/sh/adb/RandomRedditMemesAPI/RedditAPI.java index 1340afb..3740705 100644 --- a/src/main/sh/adb/RandomRedditMemesAPI/RedditAPI.java +++ b/src/main/sh/adb/RandomRedditMemesAPI/RedditAPI.java @@ -1,5 +1,12 @@ package sh.adb.RandomRedditMemesAPI; -public class RedditAPI { +import org.json.simple.parser.ParseException; + +import com.github.jreddit.oauth.RedditOAuthAgent; +import com.github.jreddit.oauth.RedditToken; +import com.github.jreddit.oauth.app.RedditApp; +import com.github.jreddit.oauth.app.RedditInstalledApp; +import com.github.jreddit.oauth.exception.RedditOAuthException; -} \ No newline at end of file +public class RedditAPI { +}