From 54dda0e19cdb9477868224d592199d19320dd2c5 Mon Sep 17 00:00:00 2001 From: Matthew Dean Date: Sat, 26 May 2018 14:33:55 -0400 Subject: [PATCH] Better support for querying suspended accounts --- .../net/dean/jraw/models/AccountQuery.java | 27 ++++++++++++++++ .../dean/jraw/SuspendedAccountException.kt | 7 +++++ .../net/dean/jraw/models/AccountStatus.kt | 12 +++++++ .../dean/jraw/references/UserReferences.kt | 31 +++++++++++++++++-- .../test/integration/UserReferenceTest.kt | 27 +++++++++++++++- .../test/kotlin/net/dean/jraw/test/util.kt | 2 +- 6 files changed, 101 insertions(+), 5 deletions(-) create mode 100644 lib/src/main/java/net/dean/jraw/models/AccountQuery.java create mode 100644 lib/src/main/kotlin/net/dean/jraw/SuspendedAccountException.kt create mode 100644 lib/src/main/kotlin/net/dean/jraw/models/AccountStatus.kt diff --git a/lib/src/main/java/net/dean/jraw/models/AccountQuery.java b/lib/src/main/java/net/dean/jraw/models/AccountQuery.java new file mode 100644 index 000000000..77dab639b --- /dev/null +++ b/lib/src/main/java/net/dean/jraw/models/AccountQuery.java @@ -0,0 +1,27 @@ +package net.dean.jraw.models; + +import com.google.auto.value.AutoValue; +import org.jetbrains.annotations.Nullable; + +/** + * The main function of an AccountQuery is to store the status of an account. If an account exists and is not suspended, + * then the {@link Account} object can be used normally. + */ +@AutoValue +public abstract class AccountQuery { + /** The reddit username being queried */ + public abstract String getName(); + + public abstract AccountStatus getStatus(); + + /** The account data. Only non-null when the status is {@link AccountStatus#EXISTS}. */ + @Nullable public abstract Account getAccount(); + + public static AccountQuery create(String name, AccountStatus status) { + return create(name, status, null); + } + + public static AccountQuery create(String name, AccountStatus status, @Nullable Account data) { + return new AutoValue_AccountQuery(name, status, data); + } +} diff --git a/lib/src/main/kotlin/net/dean/jraw/SuspendedAccountException.kt b/lib/src/main/kotlin/net/dean/jraw/SuspendedAccountException.kt new file mode 100644 index 000000000..c64591ea4 --- /dev/null +++ b/lib/src/main/kotlin/net/dean/jraw/SuspendedAccountException.kt @@ -0,0 +1,7 @@ +package net.dean.jraw + +/** + * Thrown when directly querying a user who is suspended + */ +class SuspendedAccountException(val name: String, cause: Throwable? = null) : + Exception("Account '$name' is suspended", cause) diff --git a/lib/src/main/kotlin/net/dean/jraw/models/AccountStatus.kt b/lib/src/main/kotlin/net/dean/jraw/models/AccountStatus.kt new file mode 100644 index 000000000..a3d15f317 --- /dev/null +++ b/lib/src/main/kotlin/net/dean/jraw/models/AccountStatus.kt @@ -0,0 +1,12 @@ +package net.dean.jraw.models + +enum class AccountStatus { + /** The account exists and is not suspended */ + EXISTS, + + /** An account by the requested name has never been created */ + NON_EXISTENT, + + /** The account exists and has been suspended reddit-wide */ + SUSPENDED +} diff --git a/lib/src/main/kotlin/net/dean/jraw/references/UserReferences.kt b/lib/src/main/kotlin/net/dean/jraw/references/UserReferences.kt index 2e8d2fb7e..cea29f3e6 100644 --- a/lib/src/main/kotlin/net/dean/jraw/references/UserReferences.kt +++ b/lib/src/main/kotlin/net/dean/jraw/references/UserReferences.kt @@ -5,6 +5,7 @@ import com.squareup.moshi.Types import net.dean.jraw.* import net.dean.jraw.JrawUtils.urlEncode import net.dean.jraw.databind.Enveloped +import net.dean.jraw.http.NetworkException import net.dean.jraw.models.* import net.dean.jraw.models.internal.GenericJsonResponse import net.dean.jraw.models.internal.RedditModelEnvelope @@ -23,8 +24,11 @@ sealed class UserReference(reddit: RedditClient, val /** True if and only if this UserReference is a [SelfUserReference] */ abstract val isSelf: Boolean - /** Fetches basic information about this user */ - @EndpointImplementation(Endpoint.GET_ME, Endpoint.GET_USER_USERNAME_ABOUT) + /** + * Fetches basic information about this user. + */ + @Throws(SuspendedAccountException::class) + @Deprecated("Prefer query() for better handling of non-existent/suspended accounts", ReplaceWith("query()")) fun about(): Account { val body = reddit.request { it.path(if (isSelf) "/api/v1/me" else "/user/$username/about") @@ -33,7 +37,28 @@ sealed class UserReference(reddit: RedditClient, val // /api/v1/me returns an Account that isn't wrapped with the data/kind nodes if (isSelf) return JrawUtils.adapter().fromJson(body)!! - return JrawUtils.adapter(Enveloped::class.java).fromJson(body)!! + try { + return JrawUtils.adapter(Enveloped::class.java).fromJson(body)!! + } catch (npe: NullPointerException) { + throw SuspendedAccountException(username) + } + } + + /** + * Gets information about this account. + */ + @EndpointImplementation(Endpoint.GET_ME, Endpoint.GET_USER_USERNAME_ABOUT) + fun query(): AccountQuery { + return try { + AccountQuery.create(username, AccountStatus.EXISTS, about()) + } catch (e: ApiException) { + if (e.cause is NetworkException && e.cause.res.code != 404) + throw e + else + AccountQuery.create(username, AccountStatus.NON_EXISTENT) + } catch (e: SuspendedAccountException) { + AccountQuery.create(username, AccountStatus.SUSPENDED) + } } /** Fetches any trophies the user has achieved */ diff --git a/lib/src/test/kotlin/net/dean/jraw/test/integration/UserReferenceTest.kt b/lib/src/test/kotlin/net/dean/jraw/test/integration/UserReferenceTest.kt index 7861f711a..c8705374b 100644 --- a/lib/src/test/kotlin/net/dean/jraw/test/integration/UserReferenceTest.kt +++ b/lib/src/test/kotlin/net/dean/jraw/test/integration/UserReferenceTest.kt @@ -2,9 +2,10 @@ package net.dean.jraw.test.integration import com.winterbe.expekt.should import net.dean.jraw.ApiException +import net.dean.jraw.models.AccountStatus import net.dean.jraw.models.MultiredditPatch -import net.dean.jraw.models.UserHistorySort import net.dean.jraw.models.TimePeriod +import net.dean.jraw.models.UserHistorySort import net.dean.jraw.references.UserReference import net.dean.jraw.test.CredentialsUtil import net.dean.jraw.test.TestConfig.reddit @@ -27,6 +28,30 @@ class UserReferenceTest : Spek({ } } + describe("query") { + it("should return a status of EXISTS on an account that exists") { + val query = reddit.me().query() + query.name.should.equal(reddit.requireAuthenticatedUser()) + query.account.should.not.be.`null` + query.status.should.equal(AccountStatus.EXISTS) + } + + it("should return a status of NON_EXISTENT on an account name that has never been used") { + val name = randomName() + val query = reddit.user(name).query() + query.name.should.equal(name) + query.account.should.be.`null` + query.status.should.equal(AccountStatus.NON_EXISTENT) + } + + it("should return a status of SUSPENDED on a suspended account") { + val query = reddit.user("TheFlintASteel").query() + query.name.should.equal("TheFlintASteel") + query.account.should.be.`null` + query.status.should.equal(AccountStatus.SUSPENDED) + } + } + describe("trophies") { it("should return a List of Trophies") { // Just make sure it deserializes diff --git a/lib/src/test/kotlin/net/dean/jraw/test/util.kt b/lib/src/test/kotlin/net/dean/jraw/test/util.kt index efcd3892c..88be01451 100644 --- a/lib/src/test/kotlin/net/dean/jraw/test/util.kt +++ b/lib/src/test/kotlin/net/dean/jraw/test/util.kt @@ -66,7 +66,7 @@ fun OAuthData.withExpiration(d: Date) = OAuthData.create(accessToken, scopes, re val rand = SecureRandom() fun randomName(length: Int = 10): String { - return "jraw_test_" + BigInteger(130, rand).toString(32).substring(0..length - 1) + return "jraw_test_" + BigInteger(130, rand).toString(32).substring(0 until length) } fun expectDescendingScore(objects: List, allowedMistakes: Int = 0) {