diff --git a/USAGE.md b/USAGE.md index 750437611..2c15a4635 100644 --- a/USAGE.md +++ b/USAGE.md @@ -29,7 +29,15 @@ scope = Scope() ## Registering an App -To access the API of a Mastodon server, we first need to create client credentials. +To access the API of a Mastodon server, we first need to create client credentials. + + +> [!IMPORTANT] +> When building an instance of the `MastodonClient`, it may throw a `BigBoneClientInstantiationException` if we could +> not +> successfully retrieve information about an instance you provide. The stacktrace of that exception should either help you +> find a solution, or give you necessary information you can provide to us, e.g. via the GitHub issues, to help you find +> one. __Kotlin__ @@ -46,14 +54,19 @@ val appRegistration = client.apps.createApp( __Java__ ```java -MastodonClient client = new MastodonClient.Builder(instanceHostname).build(); try { - AppRegistration appRegistration = client.apps().createApp( - "bigbone-sample-app", - "urn:ietf:wg:oauth:2.0:oob", - new Scope(), - "https://example.org/" - ).execute(); + MastodonClient client=new MastodonClient.Builder(instanceHostname).build(); +} catch (BigBoneClientInstantiationException e){ + // error handling +} + +try { + AppRegistration appRegistration=client.apps().createApp( + "bigbone-sample-app", + "urn:ietf:wg:oauth:2.0:oob", + new Scope(), + "https://example.org/" + ).execute(); } catch (BigBoneRequestException e) { // error handling } diff --git a/bigbone/src/main/kotlin/social/bigbone/MastodonClient.kt b/bigbone/src/main/kotlin/social/bigbone/MastodonClient.kt index 15d51dd9d..c79dc5939 100644 --- a/bigbone/src/main/kotlin/social/bigbone/MastodonClient.kt +++ b/bigbone/src/main/kotlin/social/bigbone/MastodonClient.kt @@ -10,7 +10,10 @@ import okhttp3.RequestBody.Companion.toRequestBody import okhttp3.Response import social.bigbone.api.Pageable import social.bigbone.api.entity.data.InstanceVersion +import social.bigbone.api.exception.BigBoneClientInstantiationException import social.bigbone.api.exception.BigBoneRequestException +import social.bigbone.api.exception.InstanceVersionRetrievalException +import social.bigbone.api.exception.ServerInfoRetrievalException import social.bigbone.api.method.AccountMethods import social.bigbone.api.method.AnnouncementMethods import social.bigbone.api.method.AppMethods @@ -50,6 +53,7 @@ import social.bigbone.api.method.admin.AdminMeasureMethods import social.bigbone.api.method.admin.AdminRetentionMethods import social.bigbone.extension.emptyRequestBody import social.bigbone.nodeinfo.NodeInfoClient +import social.bigbone.nodeinfo.entity.Server import java.io.IOException import java.security.SecureRandom import java.security.cert.X509Certificate @@ -66,12 +70,12 @@ import javax.net.ssl.X509TrustManager class MastodonClient private constructor( private val instanceName: String, - private val client: OkHttpClient -) { - private var debug = false - private var instanceVersion: String? = null - private var scheme: String = "https" + private val client: OkHttpClient, + private var debug: Boolean = false, + private var instanceVersion: String? = null, + private var scheme: String = "https", private var port: Int = 443 +) { /** * Access API methods under the "accounts" endpoint. @@ -436,19 +440,20 @@ private constructor( * @param endpoint the Mastodon API endpoint to call * @param method the HTTP method to use * @param parameters parameters to use in the action; can be null + * @throws BigBoneRequestException in case the action to be performed yielded an unsuccessful response */ @Throws(BigBoneRequestException::class) internal fun performAction(endpoint: String, method: Method, parameters: Parameters? = null) { - val response = when (method) { + when (method) { Method.DELETE -> delete(endpoint, parameters) Method.GET -> get(endpoint, parameters) Method.PATCH -> patch(endpoint, parameters) Method.POST -> post(endpoint, parameters) Method.PUT -> put(endpoint, parameters) - } - response.close() - if (!response.isSuccessful) { - throw BigBoneRequestException(response) + }.use { response: Response -> + if (!response.isSuccessful) { + throw BigBoneRequestException(response) + } } } @@ -722,22 +727,50 @@ private constructor( /** * Get the version string for this Mastodon instance. * @return a string corresponding to the version of this Mastodon instance - * @throws BigBoneRequestException if instance version can not be retrieved using any known method or API version + * @throws BigBoneClientInstantiationException if instance version cannot be retrieved using any known method or API version */ + @Throws(BigBoneClientInstantiationException::class) private fun getInstanceVersion(): String { - try { - val serverInfoVersion = NodeInfoClient - .retrieveServerInfo(instanceName) - ?.software - ?.takeIf { it.name == "mastodon" } - ?.version - if (serverInfoVersion != null) return serverInfoVersion - } catch (_: BigBoneRequestException) { + return try { + getInstanceVersionViaServerInfo() + } catch (error: BigBoneClientInstantiationException) { + // fall back to retrieving from Mastodon API itself + try { + getInstanceVersionViaApi() + } catch (instanceException: InstanceVersionRetrievalException) { + throw BigBoneClientInstantiationException( + message = "Failed to get instance version of $instanceName", + cause = if (instanceException.cause == instanceException) { + instanceException.initCause(error) + } else { + instanceException + } + ) + } } + } + + @Throws(ServerInfoRetrievalException::class) + private fun getInstanceVersionViaServerInfo(): String { + val serverSoftwareInfo: Server.Software? = NodeInfoClient + .retrieveServerInfo(instanceName) + ?.software + ?.takeIf { it.name == "mastodon" } - // fall back to retrieving from Mastodon API itself - val instanceVersion = getInstanceVersionFromApi(2) ?: getInstanceVersionFromApi(1) - return instanceVersion ?: throw BigBoneRequestException("Unable to fetch instance version") + if (serverSoftwareInfo != null) return serverSoftwareInfo.version + + throw ServerInfoRetrievalException( + cause = IllegalArgumentException("Server $instanceName doesn't appear to run Mastodon") + ) + } + + @Throws(InstanceVersionRetrievalException::class) + private fun getInstanceVersionViaApi(): String { + return try { + getInstanceVersionFromApi(2) + } catch (e: InstanceVersionRetrievalException) { + getInstanceVersionFromApi(1) + } } /** @@ -745,21 +778,23 @@ private constructor( * @param apiVersion the version of API call to use in this request * @return a string corresponding to the version of this Mastodon instance, or null if no version string can be * retrieved using the specified API version. + * @throws InstanceVersionRetrievalException in case we got a server response but no version, or an unsucessful response */ - private fun getInstanceVersionFromApi(apiVersion: Int): String? { - return try { - val response = versionedInstanceRequest(apiVersion) + @Throws(InstanceVersionRetrievalException::class) + private fun getInstanceVersionFromApi(apiVersion: Int): String { + return versionedInstanceRequest(apiVersion).use { response: Response -> if (response.isSuccessful) { val instanceVersion: InstanceVersion? = response.body?.string()?.let { responseBody: String -> JSON_SERIALIZER.decodeFromString(responseBody) } - instanceVersion?.version + instanceVersion + ?.version + ?: throw InstanceVersionRetrievalException( + cause = IllegalStateException("Instance version was null unexpectedly") + ) } else { - response.close() - null + throw InstanceVersionRetrievalException(response = response) } - } catch (e: Exception) { - null } } @@ -779,28 +814,36 @@ private constructor( * @return server response for this request */ internal fun versionedInstanceRequest(version: Int): Response { - val versionString = if (version == 2) { - "v2" - } else { - "v1" - } + val versionString = if (version == 2) "v2" else "v1" + val clientBuilder = OkHttpClient.Builder() - if (trustAllCerts) { - configureForTrustAll(clientBuilder) - } - val client = clientBuilder.build() - return client.newCall( - Request.Builder().url( - fullUrl( - scheme, - instanceName, - port, - "api/$versionString/instance" - ) - ).get().build() - ).execute() + if (trustAllCerts) configureForTrustAll(clientBuilder) + + return clientBuilder + .build() + .newCall( + Request.Builder() + .url( + fullUrl( + scheme = scheme, + instanceName = instanceName, + port = port, + path = "api/$versionString/instance" + ) + ) + .get() + .build() + ) + .execute() } + /** + * Builds this MastodonClient. + * + * @throws BigBoneClientInstantiationException if the client could not be instantiated, likely due to an issue + * when getting the instance version of the server in [instanceName]. Other exceptions, e.g. due to no Internet + * connection are _not_ caught by this library. + */ fun build(): MastodonClient { return MastodonClient( instanceName = instanceName, @@ -809,13 +852,12 @@ private constructor( .readTimeout(readTimeoutSeconds, TimeUnit.SECONDS) .writeTimeout(writeTimeoutSeconds, TimeUnit.SECONDS) .connectTimeout(connectTimeoutSeconds, TimeUnit.SECONDS) - .build() - ).also { - it.debug = debug - it.instanceVersion = getInstanceVersion() - it.scheme = scheme - it.port = port - } + .build(), + debug = debug, + instanceVersion = getInstanceVersion(), + scheme = scheme, + port = port + ) } } diff --git a/bigbone/src/main/kotlin/social/bigbone/api/exception/BigBoneClientInstantiationException.kt b/bigbone/src/main/kotlin/social/bigbone/api/exception/BigBoneClientInstantiationException.kt new file mode 100644 index 000000000..2a470e8e0 --- /dev/null +++ b/bigbone/src/main/kotlin/social/bigbone/api/exception/BigBoneClientInstantiationException.kt @@ -0,0 +1,47 @@ +package social.bigbone.api.exception + +import okhttp3.Response +import social.bigbone.MastodonClient +import social.bigbone.nodeinfo.entity.NodeInfo + +/** + * Exception thrown if we could not instantiate a [MastodonClient]. Mostly used to wrap other more specific exceptions. + */ +open class BigBoneClientInstantiationException : Exception { + constructor() : super() + constructor(message: String) : super(message) + constructor(message: String, cause: Throwable?) : super(message, cause) + constructor(cause: Throwable?) : super(cause) +} + +/** + * Exception thrown if we could not retrieve server information during [MastodonClient] instantiation. + */ +class ServerInfoRetrievalException : BigBoneClientInstantiationException { + constructor(cause: Throwable?) : super(cause) + constructor(message: String, cause: Throwable?) : super(message, cause) + constructor(response: Response, message: String? = null) : super( + message = "${message ?: ""}${response.message}" + ) +} + +/** + * Exception thrown if we could not successfully get the [NodeInfo] server URL during [MastodonClient] instantiation. + */ +class ServerInfoUrlRetrievalException( + response: Response, + message: String? = null +) : + BigBoneClientInstantiationException( + message = "${message ?: ""}${response.message}" + ) + +/** + * Exception thrown if we could not retrieve the instance version of a Mastodon server during [MastodonClient] instantiation. + */ +class InstanceVersionRetrievalException : BigBoneClientInstantiationException { + constructor(cause: Throwable?) : super(cause) + constructor(response: Response, message: String? = null) : super( + message = "${response.code} – ${message ?: ""}${response.message}" + ) +} diff --git a/bigbone/src/main/kotlin/social/bigbone/nodeinfo/NodeInfoClient.kt b/bigbone/src/main/kotlin/social/bigbone/nodeinfo/NodeInfoClient.kt index d0d781f02..32989ea5d 100644 --- a/bigbone/src/main/kotlin/social/bigbone/nodeinfo/NodeInfoClient.kt +++ b/bigbone/src/main/kotlin/social/bigbone/nodeinfo/NodeInfoClient.kt @@ -2,8 +2,10 @@ package social.bigbone.nodeinfo import okhttp3.OkHttpClient import okhttp3.Request +import okhttp3.Response import social.bigbone.JSON_SERIALIZER -import social.bigbone.api.exception.BigBoneRequestException +import social.bigbone.api.exception.ServerInfoRetrievalException +import social.bigbone.api.exception.ServerInfoUrlRetrievalException import social.bigbone.nodeinfo.entity.NodeInfo import social.bigbone.nodeinfo.entity.Server @@ -21,26 +23,23 @@ object NodeInfoClient { * Retrieve server information. * @param host hostname of the server to retrieve information from * @return server information, including the name and version of the software running on this server - * @throws BigBoneRequestException if server info can not be retrieved via NodeInfo for any reason + * @throws ServerInfoRetrievalException if the request for a server info to [host] was unsuccessful */ fun retrieveServerInfo(host: String): Server? { - try { - val serverInfoUrl = getServerInfoUrl(host) - val response = CLIENT.newCall( - Request.Builder() - .url(serverInfoUrl) - .get() - .build() - ).execute() - + CLIENT.newCall( + Request.Builder() + .url(getServerInfoUrl(host)) + .get() + .build() + ).execute().use { response: Response -> if (!response.isSuccessful) { - response.close() - throw BigBoneRequestException("request for NodeInfo URL unsuccessful") + throw ServerInfoRetrievalException( + message = "request for NodeInfo URL unsuccessful", + response = response + ) } return response.body?.string()?.let { JSON_SERIALIZER.decodeFromString(it) } - } catch (e: Exception) { - throw BigBoneRequestException("invalid NodeInfo response") } } @@ -48,29 +47,36 @@ object NodeInfoClient { * Get the URL to retrieve server information from. * @param host the hostname of the server to request information from * @return String containing the URL holding server information + * @throws ServerInfoUrlRetrievalException if we could not call the [host] or if the [NodeInfo] was empty */ + @Throws(ServerInfoUrlRetrievalException::class) private fun getServerInfoUrl(host: String): String { - val response = CLIENT.newCall( + CLIENT.newCall( Request.Builder() .url("https://$host/.well-known/nodeinfo") .get() .build() - ).execute() + ).execute().use { response: Response -> + if (!response.isSuccessful) { + throw ServerInfoUrlRetrievalException( + message = "request for well-known NodeInfo URL unsuccessful", + response = response + ) + } - if (!response.isSuccessful) { - response.close() - throw BigBoneRequestException("request for well-known NodeInfo URL unsuccessful") - } + val nodeInfo: NodeInfo? = response.body?.string()?.let { JSON_SERIALIZER.decodeFromString(it) } + if (nodeInfo == null || nodeInfo.links.isEmpty()) { + throw ServerInfoUrlRetrievalException( + message = "empty link list in well-known NodeInfo location", + response = response + ) + } - val nodeInfo: NodeInfo? = response.body?.string()?.let { JSON_SERIALIZER.decodeFromString(it) } - if (nodeInfo == null || nodeInfo.links.isEmpty()) { - throw BigBoneRequestException("empty link list in well-known NodeInfo location") + // attempt returning URL to schema 2.0 information, but fall back to any - software information exists in all schemas + return nodeInfo.links + .firstOrNull { link -> link.rel == "http://nodeinfo.diaspora.software/ns/schema/2.0" } + ?.href + ?: nodeInfo.links.first().href } - - // attempt returning URL to schema 2.0 information, but fall back to any - software information exists in all schemas - return nodeInfo.links - .firstOrNull { link -> link.rel == "http://nodeinfo.diaspora.software/ns/schema/2.0" } - ?.href - ?: nodeInfo.links.first().href } } diff --git a/bigbone/src/test/assets/mastodon_client_v1_instance_response.json b/bigbone/src/test/assets/mastodon_client_v1_instance_response.json new file mode 100644 index 000000000..630a45216 --- /dev/null +++ b/bigbone/src/test/assets/mastodon_client_v1_instance_response.json @@ -0,0 +1,132 @@ +{ + "uri": "mastodon.social", + "title": "Mastodon", + "short_description": "The original server operated by the Mastodon gGmbH non-profit", + "description": "", + "email": "staff@mastodon.social", + "version": "4.0.0rc1", + "urls": { + "streaming_api": "wss://mastodon.social" + }, + "stats": { + "user_count": 921487, + "status_count": 48629230, + "domain_count": 48722 + }, + "thumbnail": "https://files.mastodon.social/site_uploads/files/000/000/001/@1x/57c12f441d083cde.png", + "languages": [ + "en" + ], + "registrations": false, + "approval_required": false, + "invites_enabled": true, + "configuration": { + "accounts": { + "max_featured_tags": 10 + }, + "statuses": { + "max_characters": 500, + "max_media_attachments": 4, + "characters_reserved_per_url": 23 + }, + "media_attachments": { + "supported_mime_types": [ + "image/jpeg", + "image/png", + "image/gif", + "image/heic", + "image/heif", + "image/webp", + "image/avif", + "video/webm", + "video/mp4", + "video/quicktime", + "video/ogg", + "audio/wave", + "audio/wav", + "audio/x-wav", + "audio/x-pn-wave", + "audio/vnd.wave", + "audio/ogg", + "audio/vorbis", + "audio/mpeg", + "audio/mp3", + "audio/webm", + "audio/flac", + "audio/aac", + "audio/m4a", + "audio/x-m4a", + "audio/mp4", + "audio/3gpp", + "video/x-ms-asf" + ], + "image_size_limit": 10485760, + "image_matrix_limit": 16777216, + "video_size_limit": 41943040, + "video_frame_rate_limit": 60, + "video_matrix_limit": 2304000 + }, + "polls": { + "max_options": 4, + "max_characters_per_option": 50, + "min_expiration": 300, + "max_expiration": 2629746 + } + }, + "contact_account": { + "id": "1", + "username": "Gargron", + "acct": "Gargron", + "display_name": "Eugen Rochko", + "locked": false, + "bot": false, + "discoverable": true, + "group": false, + "created_at": "2016-03-16T00:00:00.000Z", + "note": "\u003cp\u003eFounder, CEO and lead developer \u003cspan class=\"h-card\"\u003e\u003ca href=\"https://mastodon.social/@Mastodon\" class=\"u-url mention\"\u003e@\u003cspan\u003eMastodon\u003c/span\u003e\u003c/a\u003e\u003c/span\u003e, Germany.\u003c/p\u003e", + "url": "https://mastodon.social/@Gargron", + "avatar": "https://files.mastodon.social/accounts/avatars/000/000/001/original/dc4286ceb8fab734.jpg", + "avatar_static": "https://files.mastodon.social/accounts/avatars/000/000/001/original/dc4286ceb8fab734.jpg", + "header": "https://files.mastodon.social/accounts/headers/000/000/001/original/3b91c9965d00888b.jpeg", + "header_static": "https://files.mastodon.social/accounts/headers/000/000/001/original/3b91c9965d00888b.jpeg", + "followers_count": 288547, + "following_count": 342, + "statuses_count": 73066, + "last_status_at": "2023-01-17", + "noindex": false, + "emojis": [], + "fields": [ + { + "name": "Patreon", + "value": "\u003ca href=\"https://www.patreon.com/mastodon\" target=\"_blank\" rel=\"nofollow noopener noreferrer me\"\u003e\u003cspan class=\"invisible\"\u003ehttps://www.\u003c/span\u003e\u003cspan class=\"\"\u003epatreon.com/mastodon\u003c/span\u003e\u003cspan class=\"invisible\"\u003e\u003c/span\u003e\u003c/a\u003e", + "verified_at": null + } + ] + }, + "rules": [ + { + "id": "1", + "text": "Sexually explicit or violent media must be marked as sensitive when posting" + }, + { + "id": "2", + "text": "No racism, sexism, homophobia, transphobia, xenophobia, or casteism" + }, + { + "id": "3", + "text": "No incitement of violence or promotion of violent ideologies" + }, + { + "id": "4", + "text": "No harassment, dogpiling or doxxing of other users" + }, + { + "id": "5", + "text": "No content illegal in Germany" + }, + { + "id": "7", + "text": "Do not share intentionally false or misleading information" + } + ] +} diff --git a/bigbone/src/test/assets/mastodon_client_v2_instance_response.json b/bigbone/src/test/assets/mastodon_client_v2_instance_response.json new file mode 100644 index 000000000..ed0af285b --- /dev/null +++ b/bigbone/src/test/assets/mastodon_client_v2_instance_response.json @@ -0,0 +1,146 @@ +{ + "domain": "mastodon.social", + "title": "Mastodon", + "version": "4.0.0rc1", + "source_url": "https://github.com/mastodon/mastodon", + "description": "The original server operated by the Mastodon gGmbH non-profit", + "usage": { + "users": { + "active_month": 148401 + } + }, + "thumbnail": { + "url": "https://files.mastodon.social/site_uploads/files/000/000/001/@1x/57c12f441d083cde.png", + "blurhash": "UeKUpFxuo~R%0nW;WCnhF6RjaJt757oJodS${'$'}", + "versions": { + "@1x": "https://files.mastodon.social/site_uploads/files/000/000/001/@1x/57c12f441d083cde.png", + "@2x": "https://files.mastodon.social/site_uploads/files/000/000/001/@2x/57c12f441d083cde.png" + } + }, + "languages": [ + "en" + ], + "configuration": { + "urls": { + "streaming": "wss://mastodon.social" + }, + "accounts": { + "max_featured_tags": 10 + }, + "statuses": { + "max_characters": 500, + "max_media_attachments": 4, + "characters_reserved_per_url": 23 + }, + "media_attachments": { + "supported_mime_types": [ + "image/jpeg", + "image/png", + "image/gif", + "image/heic", + "image/heif", + "image/webp", + "image/avif", + "video/webm", + "video/mp4", + "video/quicktime", + "video/ogg", + "audio/wave", + "audio/wav", + "audio/x-wav", + "audio/x-pn-wave", + "audio/vnd.wave", + "audio/ogg", + "audio/vorbis", + "audio/mpeg", + "audio/mp3", + "audio/webm", + "audio/flac", + "audio/aac", + "audio/m4a", + "audio/x-m4a", + "audio/mp4", + "audio/3gpp", + "video/x-ms-asf" + ], + "image_size_limit": 10485760, + "image_matrix_limit": 16777216, + "video_size_limit": 41943040, + "video_frame_rate_limit": 60, + "video_matrix_limit": 2304000 + }, + "polls": { + "max_options": 4, + "max_characters_per_option": 50, + "min_expiration": 300, + "max_expiration": 2629746 + }, + "translation": { + "enabled": true + } + }, + "registrations": { + "enabled": false, + "approval_required": false, + "message": null + }, + "contact": { + "email": "staff@mastodon.social", + "account": { + "id": "1", + "username": "Gargron", + "acct": "Gargron", + "display_name": "Eugen Rochko", + "locked": false, + "bot": false, + "discoverable": true, + "group": false, + "created_at": "2016-03-16T00:00:00.000Z", + "note": "\u003cp\u003eFounder, CEO and lead developer \u003cspan class=\"h-card\"\u003e\u003ca href=\"https://mastodon.social/@Mastodon\" class=\"u-url mention\"\u003e@\u003cspan\u003eMastodon\u003c/span\u003e\u003c/a\u003e\u003c/span\u003e, Germany.\u003c/p\u003e", + "url": "https://mastodon.social/@Gargron", + "avatar": "https://files.mastodon.social/accounts/avatars/000/000/001/original/dc4286ceb8fab734.jpg", + "avatar_static": "https://files.mastodon.social/accounts/avatars/000/000/001/original/dc4286ceb8fab734.jpg", + "header": "https://files.mastodon.social/accounts/headers/000/000/001/original/3b91c9965d00888b.jpeg", + "header_static": "https://files.mastodon.social/accounts/headers/000/000/001/original/3b91c9965d00888b.jpeg", + "followers_count": 288544, + "following_count": 342, + "statuses_count": 73066, + "last_status_at": "2023-01-17", + "noindex": false, + "emojis": [], + "fields": [ + { + "name": "Patreon", + "value": "\u003ca href=\"https://www.patreon.com/mastodon\" target=\"_blank\" rel=\"nofollow noopener noreferrer me\"\u003e\u003cspan class=\"invisible\"\u003ehttps://www.\u003c/span\u003e\u003cspan class=\"\"\u003epatreon.com/mastodon\u003c/span\u003e\u003cspan class=\"invisible\"\u003e\u003c/span\u003e\u003c/a\u003e", + "verified_at": null + } + ] + } + }, + "rules": [ + { + "id": "1", + "text": "Sexually explicit or violent media must be marked as sensitive when posting" + }, + { + "id": "2", + "text": "No racism, sexism, homophobia, transphobia, xenophobia, or casteism" + }, + { + "id": "3", + "text": "No incitement of violence or promotion of violent ideologies" + }, + { + "id": "4", + "text": "No harassment, dogpiling or doxxing of other users" + }, + { + "id": "5", + "text": "No content illegal in Germany" + }, + { + "id": "7", + "text": "Do not share intentionally false or misleading information" + } + ] +} diff --git a/bigbone/src/test/kotlin/social/bigbone/MastodonClientTest.kt b/bigbone/src/test/kotlin/social/bigbone/MastodonClientTest.kt index 9ab1a096e..e30a423e5 100644 --- a/bigbone/src/test/kotlin/social/bigbone/MastodonClientTest.kt +++ b/bigbone/src/test/kotlin/social/bigbone/MastodonClientTest.kt @@ -2,315 +2,54 @@ package social.bigbone import io.mockk.every import io.mockk.mockk +import io.mockk.mockkObject import io.mockk.spyk import okhttp3.MediaType.Companion.toMediaType import okhttp3.Response import okhttp3.ResponseBody import okhttp3.ResponseBody.Companion.toResponseBody +import org.amshove.kluent.invoking import org.amshove.kluent.shouldBeEqualTo -import org.junit.jupiter.api.Assertions +import org.amshove.kluent.shouldThrow +import org.amshove.kluent.withCause +import org.amshove.kluent.withMessage import org.junit.jupiter.api.Test -import social.bigbone.api.exception.BigBoneRequestException +import social.bigbone.api.exception.BigBoneClientInstantiationException +import social.bigbone.api.exception.InstanceVersionRetrievalException +import social.bigbone.api.exception.ServerInfoRetrievalException +import social.bigbone.nodeinfo.NodeInfoClient +import social.bigbone.testtool.AssetsUtil +import java.net.UnknownHostException -@SuppressWarnings("FunctionMaxLength") class MastodonClientTest { - private val invalidResponseBody = "{ \"foo\": \"bar\" }" - private val v2InstanceResponseBody = """{ - "domain": "mastodon.social", - "title": "Mastodon", - "version": "4.0.0rc1", - "source_url": "https://github.com/mastodon/mastodon", - "description": "The original server operated by the Mastodon gGmbH non-profit", - "usage": { - "users": { - "active_month": 148401 - } - }, - "thumbnail": { - "url": "https://files.mastodon.social/site_uploads/files/000/000/001/@1x/57c12f441d083cde.png", - "blurhash": "UeKUpFxuo~R%0nW;WCnhF6RjaJt757oJodS${'$'}", - "versions": { - "@1x": "https://files.mastodon.social/site_uploads/files/000/000/001/@1x/57c12f441d083cde.png", - "@2x": "https://files.mastodon.social/site_uploads/files/000/000/001/@2x/57c12f441d083cde.png" - } - }, - "languages": [ - "en" - ], - "configuration": { - "urls": { - "streaming": "wss://mastodon.social" - }, - "accounts": { - "max_featured_tags": 10 - }, - "statuses": { - "max_characters": 500, - "max_media_attachments": 4, - "characters_reserved_per_url": 23 - }, - "media_attachments": { - "supported_mime_types": [ - "image/jpeg", - "image/png", - "image/gif", - "image/heic", - "image/heif", - "image/webp", - "image/avif", - "video/webm", - "video/mp4", - "video/quicktime", - "video/ogg", - "audio/wave", - "audio/wav", - "audio/x-wav", - "audio/x-pn-wave", - "audio/vnd.wave", - "audio/ogg", - "audio/vorbis", - "audio/mpeg", - "audio/mp3", - "audio/webm", - "audio/flac", - "audio/aac", - "audio/m4a", - "audio/x-m4a", - "audio/mp4", - "audio/3gpp", - "video/x-ms-asf" - ], - "image_size_limit": 10485760, - "image_matrix_limit": 16777216, - "video_size_limit": 41943040, - "video_frame_rate_limit": 60, - "video_matrix_limit": 2304000 - }, - "polls": { - "max_options": 4, - "max_characters_per_option": 50, - "min_expiration": 300, - "max_expiration": 2629746 - }, - "translation": { - "enabled": true - } - }, - "registrations": { - "enabled": false, - "approval_required": false, - "message": null - }, - "contact": { - "email": "staff@mastodon.social", - "account": { - "id": "1", - "username": "Gargron", - "acct": "Gargron", - "display_name": "Eugen Rochko", - "locked": false, - "bot": false, - "discoverable": true, - "group": false, - "created_at": "2016-03-16T00:00:00.000Z", - "note": "\u003cp\u003eFounder, CEO and lead developer \u003cspan class=\"h-card\"\u003e\u003ca href=\"https://mastodon.social/@Mastodon\" class=\"u-url mention\"\u003e@\u003cspan\u003eMastodon\u003c/span\u003e\u003c/a\u003e\u003c/span\u003e, Germany.\u003c/p\u003e", - "url": "https://mastodon.social/@Gargron", - "avatar": "https://files.mastodon.social/accounts/avatars/000/000/001/original/dc4286ceb8fab734.jpg", - "avatar_static": "https://files.mastodon.social/accounts/avatars/000/000/001/original/dc4286ceb8fab734.jpg", - "header": "https://files.mastodon.social/accounts/headers/000/000/001/original/3b91c9965d00888b.jpeg", - "header_static": "https://files.mastodon.social/accounts/headers/000/000/001/original/3b91c9965d00888b.jpeg", - "followers_count": 288544, - "following_count": 342, - "statuses_count": 73066, - "last_status_at": "2023-01-17", - "noindex": false, - "emojis": [], - "fields": [ - { - "name": "Patreon", - "value": "\u003ca href=\"https://www.patreon.com/mastodon\" target=\"_blank\" rel=\"nofollow noopener noreferrer me\"\u003e\u003cspan class=\"invisible\"\u003ehttps://www.\u003c/span\u003e\u003cspan class=\"\"\u003epatreon.com/mastodon\u003c/span\u003e\u003cspan class=\"invisible\"\u003e\u003c/span\u003e\u003c/a\u003e", - "verified_at": null - } - ] - } - }, - "rules": [ - { - "id": "1", - "text": "Sexually explicit or violent media must be marked as sensitive when posting" - }, - { - "id": "2", - "text": "No racism, sexism, homophobia, transphobia, xenophobia, or casteism" - }, - { - "id": "3", - "text": "No incitement of violence or promotion of violent ideologies" - }, - { - "id": "4", - "text": "No harassment, dogpiling or doxxing of other users" - }, - { - "id": "5", - "text": "No content illegal in Germany" - }, - { - "id": "7", - "text": "Do not share intentionally false or misleading information" - } - ] -} - """ - private val v1InstanceResponseBody = """{ - "uri": "mastodon.social", - "title": "Mastodon", - "short_description": "The original server operated by the Mastodon gGmbH non-profit", - "description": "", - "email": "staff@mastodon.social", - "version": "4.0.0rc1", - "urls": { - "streaming_api": "wss://mastodon.social" - }, - "stats": { - "user_count": 921487, - "status_count": 48629230, - "domain_count": 48722 - }, - "thumbnail": "https://files.mastodon.social/site_uploads/files/000/000/001/@1x/57c12f441d083cde.png", - "languages": [ - "en" - ], - "registrations": false, - "approval_required": false, - "invites_enabled": true, - "configuration": { - "accounts": { - "max_featured_tags": 10 - }, - "statuses": { - "max_characters": 500, - "max_media_attachments": 4, - "characters_reserved_per_url": 23 - }, - "media_attachments": { - "supported_mime_types": [ - "image/jpeg", - "image/png", - "image/gif", - "image/heic", - "image/heif", - "image/webp", - "image/avif", - "video/webm", - "video/mp4", - "video/quicktime", - "video/ogg", - "audio/wave", - "audio/wav", - "audio/x-wav", - "audio/x-pn-wave", - "audio/vnd.wave", - "audio/ogg", - "audio/vorbis", - "audio/mpeg", - "audio/mp3", - "audio/webm", - "audio/flac", - "audio/aac", - "audio/m4a", - "audio/x-m4a", - "audio/mp4", - "audio/3gpp", - "video/x-ms-asf" - ], - "image_size_limit": 10485760, - "image_matrix_limit": 16777216, - "video_size_limit": 41943040, - "video_frame_rate_limit": 60, - "video_matrix_limit": 2304000 - }, - "polls": { - "max_options": 4, - "max_characters_per_option": 50, - "min_expiration": 300, - "max_expiration": 2629746 - } - }, - "contact_account": { - "id": "1", - "username": "Gargron", - "acct": "Gargron", - "display_name": "Eugen Rochko", - "locked": false, - "bot": false, - "discoverable": true, - "group": false, - "created_at": "2016-03-16T00:00:00.000Z", - "note": "\u003cp\u003eFounder, CEO and lead developer \u003cspan class=\"h-card\"\u003e\u003ca href=\"https://mastodon.social/@Mastodon\" class=\"u-url mention\"\u003e@\u003cspan\u003eMastodon\u003c/span\u003e\u003c/a\u003e\u003c/span\u003e, Germany.\u003c/p\u003e", - "url": "https://mastodon.social/@Gargron", - "avatar": "https://files.mastodon.social/accounts/avatars/000/000/001/original/dc4286ceb8fab734.jpg", - "avatar_static": "https://files.mastodon.social/accounts/avatars/000/000/001/original/dc4286ceb8fab734.jpg", - "header": "https://files.mastodon.social/accounts/headers/000/000/001/original/3b91c9965d00888b.jpeg", - "header_static": "https://files.mastodon.social/accounts/headers/000/000/001/original/3b91c9965d00888b.jpeg", - "followers_count": 288547, - "following_count": 342, - "statuses_count": 73066, - "last_status_at": "2023-01-17", - "noindex": false, - "emojis": [], - "fields": [ - { - "name": "Patreon", - "value": "\u003ca href=\"https://www.patreon.com/mastodon\" target=\"_blank\" rel=\"nofollow noopener noreferrer me\"\u003e\u003cspan class=\"invisible\"\u003ehttps://www.\u003c/span\u003e\u003cspan class=\"\"\u003epatreon.com/mastodon\u003c/span\u003e\u003cspan class=\"invisible\"\u003e\u003c/span\u003e\u003c/a\u003e", - "verified_at": null - } - ] - }, - "rules": [ - { - "id": "1", - "text": "Sexually explicit or violent media must be marked as sensitive when posting" - }, - { - "id": "2", - "text": "No racism, sexism, homophobia, transphobia, xenophobia, or casteism" - }, - { - "id": "3", - "text": "No incitement of violence or promotion of violent ideologies" - }, - { - "id": "4", - "text": "No harassment, dogpiling or doxxing of other users" - }, - { - "id": "5", - "text": "No content illegal in Germany" - }, - { - "id": "7", - "text": "Do not share intentionally false or misleading information" - } - ] -} - """ - @Test - fun `should return version of v2 endpoint if available`() { + fun `Given server response with available v2 endpoint, when building MastodonClient, then return instance version of v2 endpoint`() { // given - val responseBodyV2Mock = mockk() - every { responseBodyV2Mock.string() } answers { v2InstanceResponseBody } - val responseV2Mock = mockk() - every { responseV2Mock.body } answers { responseBodyV2Mock } - every { responseV2Mock.isSuccessful } answers { true } + val clientBuilder = spyk(MastodonClient.Builder("foo.bar")) { + // Mock internal NodeInfoClient so that we don't open the site in unit testing + mockkObject(NodeInfoClient) + every { NodeInfoClient.retrieveServerInfo("foo.bar") } throws ServerInfoRetrievalException( + "just for testing", + null + ) - val responseV1Mock = mockk() + val responseV1Mock = mockk() + every { versionedInstanceRequest(1) } answers { responseV1Mock } - val clientBuilder = spyk(MastodonClient.Builder("foo.bar")) - every { clientBuilder.versionedInstanceRequest(1) } answers { responseV1Mock } - every { clientBuilder.versionedInstanceRequest(2) } answers { responseV2Mock } + val responseBodyV2Mock = mockk { + every { string() } answers { + AssetsUtil.readFromAssets("mastodon_client_v2_instance_response.json") + } + every { close() } returns Unit + } + val responseV2Mock = mockk { + every { body } answers { responseBodyV2Mock } + every { isSuccessful } answers { true } + every { close() } returns Unit + } + every { versionedInstanceRequest(2) } answers { responseV2Mock } + } // when val client = clientBuilder.build() @@ -320,20 +59,36 @@ class MastodonClientTest { } @Test - fun `should return version of v1 endpoint if v2 endpoint is not available`() { + fun `Given server response with only v1 endpoint available, when building MastodonClient, then return instance version of v1 endpoint`() { // given - val responseV2Mock = mockk() - every { responseV2Mock.isSuccessful } answers { false } + val clientBuilder = spyk(MastodonClient.Builder("foo.bar")) { + // Mock internal NodeInfoClient so that we don't open the site in unit testing + mockkObject(NodeInfoClient) + every { NodeInfoClient.retrieveServerInfo("foo.bar") } throws ServerInfoRetrievalException( + "just for testing", + null + ) - val responseBodyV1Mock = mockk() - every { responseBodyV1Mock.string() } answers { v1InstanceResponseBody } - val responseV1Mock = mockk() - every { responseV1Mock.isSuccessful } answers { true } - every { responseV1Mock.body } answers { responseBodyV1Mock } + val responseBodyV1Mock = mockk { + every { string() } answers { + AssetsUtil.readFromAssets("mastodon_client_v1_instance_response.json") + } + } + val responseV1Mock = mockk { + every { isSuccessful } answers { true } + every { body } answers { responseBodyV1Mock } + every { close() } returns Unit + } + every { versionedInstanceRequest(1) } answers { responseV1Mock } - val clientBuilder = spyk(MastodonClient.Builder("foo.bar")) - every { clientBuilder.versionedInstanceRequest(1) } answers { responseV1Mock } - every { clientBuilder.versionedInstanceRequest(2) } answers { responseV2Mock } + val responseV2Mock = mockk { + every { isSuccessful } answers { false } + every { code } returns 404 + every { message } returns "Not Found" + every { close() } returns Unit + } + every { versionedInstanceRequest(2) } answers { responseV2Mock } + } // when val client = clientBuilder.build() @@ -343,17 +98,46 @@ class MastodonClientTest { } @Test - fun `should throw exception when instance version cannot be found in response body`() { - // given - val clientBuilder = spyk(MastodonClient.Builder("foo.bar")) - val responseMock = mockk() - every { responseMock.body } answers { invalidResponseBody.toResponseBody("application/json".toMediaType()) } - every { responseMock.isSuccessful } answers { true } - every { clientBuilder.versionedInstanceRequest(any()) } answers { responseMock } + fun `Given response body without instance version, when building MastodonClient, then fail with InstanceVersionRetrievalException`() { + val serverUrl = "foo.bar" + val clientBuilder = spyk(MastodonClient.Builder(serverUrl)) { + // Mock internal NodeInfoClient so that we don't open the site in unit testing + mockkObject(NodeInfoClient) + every { NodeInfoClient.retrieveServerInfo(serverUrl) } throws ServerInfoRetrievalException( + "just for testing", + null + ) - // when / then - Assertions.assertThrows(BigBoneRequestException::class.java) { - clientBuilder.build() + val responseMock = mockk { + val invalidResponseBody = "{ \"foo\": \"bar\" }" + every { body } answers { invalidResponseBody.toResponseBody("application/json".toMediaType()) } + every { isSuccessful } answers { true } + every { close() } returns Unit + } + every { versionedInstanceRequest(any()) } answers { responseMock } } + + invoking(clientBuilder::build) + .shouldThrow(BigBoneClientInstantiationException::class) + .withCause(InstanceVersionRetrievalException::class) + .withMessage("Failed to get instance version of $serverUrl") + } + + @Test + fun `Given a server that doesn't run Mastodon, when building MastodonClient, then fail with InstanceVersionRetrievalException`() { + val testUrl = "pod.dapor.net" + val clientBuilder: MastodonClient.Builder = spyk(MastodonClient.Builder(testUrl)) + + invoking(clientBuilder::build) + .shouldThrow(BigBoneClientInstantiationException::class) + .withCause(InstanceVersionRetrievalException::class) + .withMessage("Failed to get instance version of $testUrl") + } + + @Test + fun `Given a server that cannot be reached, when building MastodonClient, then propagate UnknownHostException`() { + val clientBuilder: MastodonClient.Builder = spyk(MastodonClient.Builder("unreachabledomain")) + + invoking(clientBuilder::build) shouldThrow UnknownHostException::class } } diff --git a/sample-java/src/main/java/social/bigbone/sample/Authenticator.java b/sample-java/src/main/java/social/bigbone/sample/Authenticator.java index 6f41dcbe8..d1a5b711e 100644 --- a/sample-java/src/main/java/social/bigbone/sample/Authenticator.java +++ b/sample-java/src/main/java/social/bigbone/sample/Authenticator.java @@ -19,7 +19,8 @@ final class Authenticator { private static final String ACCESS_TOKEN = "access_token"; private static final String REDIRECT_URI = "urn:ietf:wg:oauth:2.0:oob"; - private Authenticator() { } + private Authenticator() { + } static MastodonClient appRegistrationIfNeeded(final String instanceName, final String credentialFilePath, final boolean useStreaming) throws IOException, BigBoneRequestException { final File file = new File(credentialFilePath); @@ -54,7 +55,7 @@ static MastodonClient appRegistrationIfNeeded(final String instanceName, final S System.out.println("access token found..."); } final MastodonClient.Builder builder = new MastodonClient.Builder(instanceName) - .accessToken(properties.get(ACCESS_TOKEN).toString()); + .accessToken(properties.get(ACCESS_TOKEN).toString()); if (useStreaming) { builder.useStreamingApi(); } diff --git a/sample-java/src/main/java/social/bigbone/sample/GetAppRegistration.java b/sample-java/src/main/java/social/bigbone/sample/GetAppRegistration.java index 58992e675..e3abe8d12 100644 --- a/sample-java/src/main/java/social/bigbone/sample/GetAppRegistration.java +++ b/sample-java/src/main/java/social/bigbone/sample/GetAppRegistration.java @@ -6,19 +6,16 @@ import social.bigbone.api.exception.BigBoneRequestException; public class GetAppRegistration { - public static void main(final String[] args) { + public static void main(final String[] args) throws BigBoneRequestException { final MastodonClient client = new MastodonClient.Builder("mstdn.jp").build(); - try { - final Application application = client.apps().createApp( - "bigbone-sample-app", - "urn:ietf:wg:oauth:2.0:oob", - "", - new Scope(Scope.Name.ALL) - ).execute(); - System.out.println("client_id=" + application.getClientId()); - System.out.println("client_secret=" + application.getClientSecret()); - } catch (BigBoneRequestException e) { - e.printStackTrace(); - } + + final Application application = client.apps().createApp( + "bigbone-sample-app", + "urn:ietf:wg:oauth:2.0:oob", + "", + new Scope(Scope.Name.ALL) + ).execute(); + System.out.println("client_id=" + application.getClientId()); + System.out.println("client_secret=" + application.getClientSecret()); } } diff --git a/sample-java/src/main/java/social/bigbone/sample/GetBookmarks.java b/sample-java/src/main/java/social/bigbone/sample/GetBookmarks.java index 3022ce3a3..733063c82 100644 --- a/sample-java/src/main/java/social/bigbone/sample/GetBookmarks.java +++ b/sample-java/src/main/java/social/bigbone/sample/GetBookmarks.java @@ -12,8 +12,8 @@ public static void main(final String[] args) throws BigBoneRequestException { // Instantiate client final MastodonClient client = new MastodonClient.Builder(instance) - .accessToken(accessToken) - .build(); + .accessToken(accessToken) + .build(); // Get bookmarks final Pageable bookmarks = client.bookmarks().getBookmarks().execute(); diff --git a/sample-java/src/main/java/social/bigbone/sample/GetConversations.java b/sample-java/src/main/java/social/bigbone/sample/GetConversations.java index df7234ec7..f61a5a58b 100644 --- a/sample-java/src/main/java/social/bigbone/sample/GetConversations.java +++ b/sample-java/src/main/java/social/bigbone/sample/GetConversations.java @@ -12,8 +12,8 @@ public static void main(final String[] args) throws BigBoneRequestException { // Instantiate client final MastodonClient client = new MastodonClient.Builder(instance) - .accessToken(accessToken) - .build(); + .accessToken(accessToken) + .build(); // Get conversations final Pageable conversations = client.conversations().getConversations().execute(); diff --git a/sample-java/src/main/java/social/bigbone/sample/GetMarkers.java b/sample-java/src/main/java/social/bigbone/sample/GetMarkers.java index e2d3a82f6..d8976271f 100644 --- a/sample-java/src/main/java/social/bigbone/sample/GetMarkers.java +++ b/sample-java/src/main/java/social/bigbone/sample/GetMarkers.java @@ -11,8 +11,8 @@ public static void main(final String[] args) throws BigBoneRequestException { // Instantiate client final MastodonClient client = new MastodonClient.Builder(instance) - .accessToken(accessToken) - .build(); + .accessToken(accessToken) + .build(); // Get markers final Markers markers = client.markers().getMarkers().execute(); diff --git a/sample-java/src/main/java/social/bigbone/sample/GetPublicTimeline.java b/sample-java/src/main/java/social/bigbone/sample/GetPublicTimeline.java index 0bde48a87..f9f696341 100644 --- a/sample-java/src/main/java/social/bigbone/sample/GetPublicTimeline.java +++ b/sample-java/src/main/java/social/bigbone/sample/GetPublicTimeline.java @@ -12,8 +12,7 @@ public static void main(final String[] args) throws BigBoneRequestException { final String instance = args[0]; // Instantiate client - final MastodonClient client = new MastodonClient.Builder(instance) - .build(); + final MastodonClient client = new MastodonClient.Builder(instance).build(); // Get statuses from public timeline final Pageable statuses = client.timelines().getPublicTimeline(LOCAL_AND_REMOTE).execute(); diff --git a/sample-java/src/main/java/social/bigbone/sample/GetRawJson.java b/sample-java/src/main/java/social/bigbone/sample/GetRawJson.java index 17df35250..8b94a5c21 100644 --- a/sample-java/src/main/java/social/bigbone/sample/GetRawJson.java +++ b/sample-java/src/main/java/social/bigbone/sample/GetRawJson.java @@ -10,12 +10,11 @@ public static void main(final String[] args) throws BigBoneRequestException { final String instance = args[0]; // Instantiate client - final MastodonClient client = new MastodonClient.Builder(instance) - .build(); + final MastodonClient client = new MastodonClient.Builder(instance).build(); // Print timeline statuses client.timelines().getPublicTimeline(LOCAL_AND_REMOTE).doOnJson( - System.out::println + System.out::println ).execute(); } } diff --git a/sample-java/src/main/java/social/bigbone/sample/GetTagTimeline.java b/sample-java/src/main/java/social/bigbone/sample/GetTagTimeline.java index fb76022b8..bcf803623 100644 --- a/sample-java/src/main/java/social/bigbone/sample/GetTagTimeline.java +++ b/sample-java/src/main/java/social/bigbone/sample/GetTagTimeline.java @@ -13,8 +13,7 @@ public static void main(final String[] args) throws BigBoneRequestException { final String hashtag = args[1]; // Instantiate client - final MastodonClient client = new MastodonClient.Builder(instance) - .build(); + final MastodonClient client = new MastodonClient.Builder(instance).build(); // Get statuses from public timeline final Pageable statuses = client.timelines().getTagTimeline(hashtag, LOCAL_AND_REMOTE).execute(); diff --git a/sample-java/src/main/java/social/bigbone/sample/ManageFilters.java b/sample-java/src/main/java/social/bigbone/sample/ManageFilters.java index 4fcb19e83..5b54b7832 100644 --- a/sample-java/src/main/java/social/bigbone/sample/ManageFilters.java +++ b/sample-java/src/main/java/social/bigbone/sample/ManageFilters.java @@ -10,10 +10,10 @@ /** * The main method of this class accepts the following parameters: - * - <instance> <accessToken> list: list all existing filters for this account - * - <instance> <accessToken> create <keyword> create a new filter for the keyword - * - <instance> <accessToken> delete <filterId> delete the filter with this filterId - * - <instance> <accessToken> addKeyword <filterId> <filterId> add keyword to the filter with ID filterId + * - <instance> <accessToken> list: list all existing filters for this account + * - <instance> <accessToken> create <keyword> create a new filter for the keyword + * - <instance> <accessToken> delete <filterId> delete the filter with this filterId + * - <instance> <accessToken> addKeyword <filterId> <filterId> add keyword to the filter with ID filterId */ @SuppressWarnings("PMD.SystemPrintln") public class ManageFilters { @@ -26,7 +26,7 @@ public static void main(final String[] args) throws BigBoneRequestException { final MastodonClient client = new MastodonClient.Builder(instance) .accessToken(accessToken) .build(); - + switch (action) { case "list": listExistingFilters(client); @@ -53,26 +53,25 @@ public static void main(final String[] args) throws BigBoneRequestException { */ private static void listExistingFilters(final MastodonClient client) throws BigBoneRequestException { final List existingFilters = client.filters().listFilters().execute(); - for (final Filter filter: existingFilters) { + for (final Filter filter : existingFilters) { System.out.println(filter.getTitle() + " (ID " + filter.getId() + "):"); System.out.print(filter.getFilterAction() + " in the following contexts: "); - for (final Filter.FilterContext context: filter.getContext()) { + for (final Filter.FilterContext context : filter.getContext()) { System.out.print(context + " "); } System.out.print("\nkeywords: "); - for (final FilterKeyword filterKeyword: filter.getKeywords()) { + for (final FilterKeyword filterKeyword : filter.getKeywords()) { System.out.print(filterKeyword.getKeyword() + " "); } System.out.println("\n-------------------------------------------------------"); } - } /** * Creates a new filter for the given keyword. This filter will expire automatically after an hour. * Similar functionality exists to update a given filter. * - * @param client a [MastodonClient] with an authenticated user + * @param client a [MastodonClient] with an authenticated user * @param keywordToFilter string that should be filtered by the new filter */ private static void createNewFilter(final MastodonClient client, final String keywordToFilter) throws BigBoneRequestException { @@ -102,7 +101,7 @@ private static void createNewFilter(final MastodonClient client, final String ke * Delete a filter with the given filter ID. * Similar functionality exists to view a given filter. * - * @param client a [MastodonClient] with an authenticated user + * @param client a [MastodonClient] with an authenticated user * @param filterId ID string for the filter that should be deleted */ private static void deleteFilter(final MastodonClient client, final String filterId) throws BigBoneRequestException { @@ -114,8 +113,8 @@ private static void deleteFilter(final MastodonClient client, final String filte * Add a keyword to an existing filter. * Similar functionality exists to view, delete or update individual keywords, or to list all keywords of a given filter. * - * @param client a [MastodonClient] with an authenticated user - * @param filterId ID string for the filter that should be edited + * @param client a [MastodonClient] with an authenticated user + * @param filterId ID string for the filter that should be edited * @param keywordToFilter string for a new keyword that should be filtered by the filter */ private static void addKeywordToFilter(final MastodonClient client, final String filterId, final String keywordToFilter) throws BigBoneRequestException { diff --git a/sample-java/src/main/java/social/bigbone/sample/PerformSimpleSearch.java b/sample-java/src/main/java/social/bigbone/sample/PerformSimpleSearch.java index 399fda92b..4ecde56b2 100644 --- a/sample-java/src/main/java/social/bigbone/sample/PerformSimpleSearch.java +++ b/sample-java/src/main/java/social/bigbone/sample/PerformSimpleSearch.java @@ -12,8 +12,8 @@ public static void main(final String[] args) throws BigBoneRequestException { // Instantiate client final MastodonClient client = new MastodonClient.Builder(instance) - .accessToken(accessToken) - .build(); + .accessToken(accessToken) + .build(); // Perform search and print results final Search searchResult = client.search().searchContent(searchTerm).execute(); diff --git a/sample-java/src/main/java/social/bigbone/sample/PostStatusWithMediaAttached.java b/sample-java/src/main/java/social/bigbone/sample/PostStatusWithMediaAttached.java index 673695083..679e49320 100644 --- a/sample-java/src/main/java/social/bigbone/sample/PostStatusWithMediaAttached.java +++ b/sample-java/src/main/java/social/bigbone/sample/PostStatusWithMediaAttached.java @@ -4,6 +4,7 @@ import social.bigbone.api.entity.MediaAttachment; import social.bigbone.api.entity.data.Visibility; import social.bigbone.api.exception.BigBoneRequestException; + import java.io.File; import java.util.Collections; import java.util.List; @@ -15,8 +16,8 @@ public static void main(final String[] args) throws BigBoneRequestException { // Instantiate client final MastodonClient client = new MastodonClient.Builder(instance) - .accessToken(accessToken) - .build(); + .accessToken(accessToken) + .build(); // Read file from resources folder final ClassLoader classLoader = Thread.currentThread().getContextClassLoader();