From e2e396fa3ded7c29207eb7f52b73539d70344b7b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andr=C3=A9=20Gasser?= Date: Tue, 18 Jul 2023 06:59:23 +0200 Subject: [PATCH] Add conversations API (#235) --- .../bigbone/rx/RxConversationMethods.kt | 53 +++++++ .../kotlin/social/bigbone/MastodonClient.kt | 8 + .../social/bigbone/api/entity/Conversation.kt | 33 +++++ .../bigbone/api/method/ConversationMethods.kt | 51 +++++++ bigbone/src/test/assets/conversations.json | 140 ++++++++++++++++++ .../assets/conversations_after_deleting.json | 1 + .../test/assets/conversations_after_edit.json | 69 +++++++++ .../api/method/ConversationMethodsTest.kt | 56 +++++++ .../java/social/bigbone/GetConversations.java | 24 +++ .../social/bigbone/sample/GetConversations.kt | 23 +++ 10 files changed, 458 insertions(+) create mode 100644 bigbone-rx/src/main/kotlin/social/bigbone/rx/RxConversationMethods.kt create mode 100644 bigbone/src/main/kotlin/social/bigbone/api/entity/Conversation.kt create mode 100644 bigbone/src/main/kotlin/social/bigbone/api/method/ConversationMethods.kt create mode 100644 bigbone/src/test/assets/conversations.json create mode 100644 bigbone/src/test/assets/conversations_after_deleting.json create mode 100644 bigbone/src/test/assets/conversations_after_edit.json create mode 100644 bigbone/src/test/kotlin/social/bigbone/api/method/ConversationMethodsTest.kt create mode 100644 sample-java/src/main/java/social/bigbone/GetConversations.java create mode 100644 sample-kotlin/src/main/kotlin/social/bigbone/sample/GetConversations.kt diff --git a/bigbone-rx/src/main/kotlin/social/bigbone/rx/RxConversationMethods.kt b/bigbone-rx/src/main/kotlin/social/bigbone/rx/RxConversationMethods.kt new file mode 100644 index 000000000..d9b7cb33d --- /dev/null +++ b/bigbone-rx/src/main/kotlin/social/bigbone/rx/RxConversationMethods.kt @@ -0,0 +1,53 @@ +package social.bigbone.rx + +import io.reactivex.rxjava3.core.Completable +import io.reactivex.rxjava3.core.Single +import social.bigbone.MastodonClient +import social.bigbone.api.Pageable +import social.bigbone.api.Range +import social.bigbone.api.entity.Conversation +import social.bigbone.api.method.ConversationMethods +import social.bigbone.rx.extensions.onErrorIfNotDisposed + +/** + * Reactive implementation of [ConversationMethods]. + * Direct conversations with other participants. (Currently, just threads containing a post with "direct" visibility.) + * @see Mastodon conversations API methods + */ +class RxConversationMethods(client: MastodonClient) { + private val conversationMethods = ConversationMethods(client) + + @JvmOverloads + fun getConversations(range: Range = Range()): Single> { + return Single.create { + try { + val conversations = conversationMethods.getConversations(range) + it.onSuccess(conversations.execute()) + } catch (throwable: Throwable) { + it.onErrorIfNotDisposed(throwable) + } + } + } + + fun deleteConversation(conversationId: String): Completable { + return Completable.create { + try { + conversationMethods.deleteConversation(conversationId) + it.onComplete() + } catch (throwable: Throwable) { + it.onErrorIfNotDisposed(throwable) + } + } + } + + fun markConversationAsRead(conversationId: String): Single { + return Single.create { + try { + val conversation = conversationMethods.markConversationAsRead(conversationId) + it.onSuccess(conversation.execute()) + } catch (throwable: Throwable) { + it.onError(throwable) + } + } + } +} diff --git a/bigbone/src/main/kotlin/social/bigbone/MastodonClient.kt b/bigbone/src/main/kotlin/social/bigbone/MastodonClient.kt index f5593a11e..72875918a 100644 --- a/bigbone/src/main/kotlin/social/bigbone/MastodonClient.kt +++ b/bigbone/src/main/kotlin/social/bigbone/MastodonClient.kt @@ -15,6 +15,7 @@ import social.bigbone.api.exception.BigBoneRequestException import social.bigbone.api.method.AccountMethods import social.bigbone.api.method.AppMethods import social.bigbone.api.method.BlockMethods +import social.bigbone.api.method.ConversationMethods import social.bigbone.api.method.DirectoryMethods import social.bigbone.api.method.FavouriteMethods import social.bigbone.api.method.FilterMethods @@ -79,6 +80,13 @@ private constructor( @get:JvmName("blocks") val blocks: BlockMethods by lazy { BlockMethods(this) } + /** + * Access API methods under "api/vX/conversations" endpoint. + */ + @Suppress("unused") // public API + @get:JvmName("conversations") + val conversations: ConversationMethods by lazy { ConversationMethods(this) } + /** * Access API methods under "api/vX/directory" endpoint. */ diff --git a/bigbone/src/main/kotlin/social/bigbone/api/entity/Conversation.kt b/bigbone/src/main/kotlin/social/bigbone/api/entity/Conversation.kt new file mode 100644 index 000000000..a45b14ca0 --- /dev/null +++ b/bigbone/src/main/kotlin/social/bigbone/api/entity/Conversation.kt @@ -0,0 +1,33 @@ +package social.bigbone.api.entity + +import com.google.gson.annotations.SerializedName + +/** + * Represents a conversation with "direct message" visibility. + * @see Mastodon API Conversation + */ +data class Conversation( + /** + * The ID of the conversation in the database. + */ + @SerializedName("id") + val id: String = "0", + + /** + * Is the conversation currently marked as unread? + */ + @SerializedName("unread") + val unread: Boolean = true, + + /** + * Participants in the conversation. + */ + @SerializedName("accounts") + val accounts: List? = emptyList(), + + /** + * The last status in the conversation. + */ + @SerializedName("last_status") + val lastStatus: Status? = null, +) diff --git a/bigbone/src/main/kotlin/social/bigbone/api/method/ConversationMethods.kt b/bigbone/src/main/kotlin/social/bigbone/api/method/ConversationMethods.kt new file mode 100644 index 000000000..2338fcc4f --- /dev/null +++ b/bigbone/src/main/kotlin/social/bigbone/api/method/ConversationMethods.kt @@ -0,0 +1,51 @@ +package social.bigbone.api.method + +import social.bigbone.MastodonClient +import social.bigbone.MastodonRequest +import social.bigbone.api.Pageable +import social.bigbone.api.Range +import social.bigbone.api.entity.Conversation + +/** + * Direct conversations with other participants. (Currently, just threads containing a post with "direct" visibility.) + * @see Mastodon conversations API methods + */ +class ConversationMethods(private val client: MastodonClient) { + /** + * View your direct conversations with other participants. + * @param range optional Range for the pageable return value + * @see Mastodon API documentation: methods/conversations/#get + */ + @JvmOverloads + fun getConversations(range: Range = Range()): MastodonRequest> { + return client.getPageableMastodonRequest( + endpoint = "api/v1/conversations", + method = MastodonClient.Method.GET, + parameters = range.toParameters() + ) + } + + /** + * Removes a conversation from your list of conversations. + * @param conversationId ID of the conversation. + * @see Mastodon API documentation: methods/conversations/#delete + */ + fun deleteConversation(conversationId: String) { + client.performAction( + endpoint = "api/v1/conversations/$conversationId", + method = MastodonClient.Method.DELETE + ) + } + + /** + * Marks a conversation as read. + * @param conversationId ID of the conversation. + * @see Mastodon API documentation: methods/conversations/#read + */ + fun markConversationAsRead(conversationId: String): MastodonRequest { + return client.getMastodonRequest( + endpoint = "api/v1/conversations/$conversationId/read", + method = MastodonClient.Method.POST + ) + } +} diff --git a/bigbone/src/test/assets/conversations.json b/bigbone/src/test/assets/conversations.json new file mode 100644 index 000000000..777ba0606 --- /dev/null +++ b/bigbone/src/test/assets/conversations.json @@ -0,0 +1,140 @@ +[ + { + "id": "418450", + "unread": true, + "accounts": [ + { + "id": 11111, + "username": "test", + "acct": "test@test.com", + "display_name": "test", + "locked": false, + "created_at": "2017-04-27T11:12:20.039Z", + "followers_count": 371, + "following_count": 14547, + "statuses_count": 195, + "note": "

test

", + "url": "https://test.com/test", + "avatar": "test", + "avatar_static": "test", + "header": "test", + "header_static": "test", + "emojis": [ + { + "shortcode": "test", + "url": "https://test.jp/images/custom_emojis/images/000/001/576/original/test.png", + "static_url": "https://test.jp/images/custom_emojis/images/000/001/576/static/test.png", + "visible_in_picker": true + } + ] + } + ], + "last_status": { + "account": { + "acct": "test", + "avatar": "test", + "avatar_static": "test", + "created_at": "2017-04-13T13:09:20.869Z", + "display_name": "test", + "followers_count": 1, + "following_count": 0, + "header": "https://mstdn.jp/headers/original/missing.png", + "header_static": "https://mstdn.jp/headers/original/missing.png", + "id": "14476", + "locked": false, + "note": "test", + "statuses_count": 10, + "url": "https://mstdn.jp/test", + "username": "test" + }, + "application": null, + "content": "Test Status", + "created_at": "2017-04-14T06:11:41.893Z", + "favourited": null, + "favourites_count": 0, + "id": "11111", + "in_reply_to_account_id": null, + "in_reply_to_id": null, + "media_attachments": [], + "mentions": [], + "reblog": null, + "reblogged": null, + "reblogs_count": 0, + "sensitive": false, + "spoiler_text": "", + "tags": [], + "uri": "tag:mstdn.jp,2017-04-14:objectId=172429:objectType=Status", + "url": "https://mstdn.jp/test", + "visibility": "public" + } + }, + { + "id": "418374", + "unread": false, + "accounts": [ + { + "id": 11111, + "username": "test", + "acct": "test@test.com", + "display_name": "test", + "locked": false, + "created_at": "2017-04-27T11:12:20.039Z", + "followers_count": 371, + "following_count": 14547, + "statuses_count": 195, + "note": "

test

", + "url": "https://test.com/test", + "avatar": "test", + "avatar_static": "test", + "header": "test", + "header_static": "test", + "emojis": [ + { + "shortcode": "test", + "url": "https://test.jp/images/custom_emojis/images/000/001/576/original/test.png", + "static_url": "https://test.jp/images/custom_emojis/images/000/001/576/static/test.png", + "visible_in_picker": true + } + ] + } + ], + "last_status": { + "account": { + "acct": "test", + "avatar": "test", + "avatar_static": "test", + "created_at": "2017-04-13T13:09:20.869Z", + "display_name": "test", + "followers_count": 1, + "following_count": 0, + "header": "https://mstdn.jp/headers/original/missing.png", + "header_static": "https://mstdn.jp/headers/original/missing.png", + "id": "14476", + "locked": false, + "note": "test", + "statuses_count": 10, + "url": "https://mstdn.jp/test", + "username": "test" + }, + "application": null, + "content": "Test Status", + "created_at": "2017-04-14T06:11:41.893Z", + "favourited": null, + "favourites_count": 0, + "id": "11111", + "in_reply_to_account_id": null, + "in_reply_to_id": null, + "media_attachments": [], + "mentions": [], + "reblog": null, + "reblogged": null, + "reblogs_count": 0, + "sensitive": false, + "spoiler_text": "", + "tags": [], + "uri": "tag:mstdn.jp,2017-04-14:objectId=172429:objectType=Status", + "url": "https://mstdn.jp/test", + "visibility": "public" + } + } +] diff --git a/bigbone/src/test/assets/conversations_after_deleting.json b/bigbone/src/test/assets/conversations_after_deleting.json new file mode 100644 index 000000000..0967ef424 --- /dev/null +++ b/bigbone/src/test/assets/conversations_after_deleting.json @@ -0,0 +1 @@ +{} diff --git a/bigbone/src/test/assets/conversations_after_edit.json b/bigbone/src/test/assets/conversations_after_edit.json new file mode 100644 index 000000000..4729e37b7 --- /dev/null +++ b/bigbone/src/test/assets/conversations_after_edit.json @@ -0,0 +1,69 @@ +{ + "id": "418450", + "unread": false, + "accounts": [ + { + "id": 11111, + "username": "test", + "acct": "test@test.com", + "display_name": "test", + "locked": false, + "created_at": "2017-04-27T11:12:20.039Z", + "followers_count": 371, + "following_count": 14547, + "statuses_count": 195, + "note": "

test

", + "url": "https://test.com/test", + "avatar": "test", + "avatar_static": "test", + "header": "test", + "header_static": "test", + "emojis": [ + { + "shortcode": "test", + "url": "https://test.jp/images/custom_emojis/images/000/001/576/original/test.png", + "static_url": "https://test.jp/images/custom_emojis/images/000/001/576/static/test.png", + "visible_in_picker": true + } + ] + } + ], + "last_status": { + "account": { + "acct": "test", + "avatar": "test", + "avatar_static": "test", + "created_at": "2017-04-13T13:09:20.869Z", + "display_name": "test", + "followers_count": 1, + "following_count": 0, + "header": "https://mstdn.jp/headers/original/missing.png", + "header_static": "https://mstdn.jp/headers/original/missing.png", + "id": "14476", + "locked": false, + "note": "test", + "statuses_count": 10, + "url": "https://mstdn.jp/test", + "username": "test" + }, + "application": null, + "content": "Test Status", + "created_at": "2017-04-14T06:11:41.893Z", + "favourited": null, + "favourites_count": 0, + "id": "11111", + "in_reply_to_account_id": null, + "in_reply_to_id": null, + "media_attachments": [], + "mentions": [], + "reblog": null, + "reblogged": null, + "reblogs_count": 0, + "sensitive": false, + "spoiler_text": "", + "tags": [], + "uri": "tag:mstdn.jp,2017-04-14:objectId=172429:objectType=Status", + "url": "https://mstdn.jp/test", + "visibility": "public" + } +} diff --git a/bigbone/src/test/kotlin/social/bigbone/api/method/ConversationMethodsTest.kt b/bigbone/src/test/kotlin/social/bigbone/api/method/ConversationMethodsTest.kt new file mode 100644 index 000000000..4c0942ca9 --- /dev/null +++ b/bigbone/src/test/kotlin/social/bigbone/api/method/ConversationMethodsTest.kt @@ -0,0 +1,56 @@ +package social.bigbone.api.method + +import org.amshove.kluent.shouldBeEqualTo +import org.junit.jupiter.api.Assertions +import org.junit.jupiter.api.Test +import social.bigbone.api.exception.BigBoneRequestException +import social.bigbone.testtool.MockClient + +class ConversationMethodsTest { + @Test + fun getConversations() { + val client = MockClient.mock("conversations.json") + val conversationMethods = ConversationMethods(client) + val pageable = conversationMethods.getConversations().execute() + val conversation = pageable.part.first() + conversation.id shouldBeEqualTo "418450" + conversation.unread shouldBeEqualTo true + conversation.lastStatus?.id shouldBeEqualTo "11111" + } + + @Test + fun getConversationsWithException() { + Assertions.assertThrows(BigBoneRequestException::class.java) { + val client = MockClient.ioException() + val conversationMethods = ConversationMethods(client) + conversationMethods.getConversations().execute() + } + } + + @Test + fun deleteConversationWithException() { + Assertions.assertThrows(BigBoneRequestException::class.java) { + val client = MockClient.ioException() + val conversationMethods = ConversationMethods(client) + conversationMethods.deleteConversation("12345") + } + } + + @Test + fun markConversationAsRead() { + val client = MockClient.mock("conversations_after_edit.json") + val conversationMethods = ConversationMethods(client) + val conversation = conversationMethods.markConversationAsRead("418450").execute() + conversation.id shouldBeEqualTo "418450" + conversation.unread shouldBeEqualTo false + } + + @Test + fun markConversationAsReadWithException() { + Assertions.assertThrows(BigBoneRequestException::class.java) { + val client = MockClient.ioException() + val conversationMethods = ConversationMethods(client) + conversationMethods.markConversationAsRead("12345").execute() + } + } +} diff --git a/sample-java/src/main/java/social/bigbone/GetConversations.java b/sample-java/src/main/java/social/bigbone/GetConversations.java new file mode 100644 index 000000000..6b03ee197 --- /dev/null +++ b/sample-java/src/main/java/social/bigbone/GetConversations.java @@ -0,0 +1,24 @@ +package social.bigbone; + +import social.bigbone.api.Pageable; +import social.bigbone.api.entity.Conversation; +import social.bigbone.api.exception.BigBoneRequestException; + +public class GetConversations { + public static void main(final String[] args) throws BigBoneRequestException { + final String instance = args[0]; + final String accessToken = args[1]; + + // Instantiate client + final MastodonClient client = new MastodonClient.Builder(instance) + .accessToken(accessToken) + .build(); + + // Get conversations + final Pageable conversations = client.conversations().getConversations().execute(); + conversations.getPart().forEach(conversation -> { + String lastStatusText = conversation.getLastStatus() != null ? conversation.getLastStatus().getContent() + "\n" : "n/a\n"; + System.out.print(conversation.getId() + " " + lastStatusText); + }); + } +} diff --git a/sample-kotlin/src/main/kotlin/social/bigbone/sample/GetConversations.kt b/sample-kotlin/src/main/kotlin/social/bigbone/sample/GetConversations.kt new file mode 100644 index 000000000..82bcef8d8 --- /dev/null +++ b/sample-kotlin/src/main/kotlin/social/bigbone/sample/GetConversations.kt @@ -0,0 +1,23 @@ +package social.bigbone.sample + +import social.bigbone.MastodonClient + +object GetConversations { + @JvmStatic + fun main(args: Array) { + val instance = args[0] + val accessToken = args[1] + + // instantiate client + val client = MastodonClient.Builder(instance) + .accessToken(accessToken) + .build() + + // Get conversations + val conversations = client.conversations.getConversations().execute() + conversations.part.forEach { conversation -> + val lastStatusText = conversation.lastStatus?.content + print("$conversation.id $lastStatusText") + } + } +}