diff --git a/bigbone-rx/src/main/kotlin/social/bigbone/rx/RxPushNotificationMethods.kt b/bigbone-rx/src/main/kotlin/social/bigbone/rx/RxPushNotificationMethods.kt new file mode 100644 index 000000000..25d50894d --- /dev/null +++ b/bigbone-rx/src/main/kotlin/social/bigbone/rx/RxPushNotificationMethods.kt @@ -0,0 +1,130 @@ +package social.bigbone.rx + +import io.reactivex.rxjava3.core.Completable +import io.reactivex.rxjava3.core.Single +import social.bigbone.MastodonClient +import social.bigbone.api.entity.WebPushSubscription +import social.bigbone.api.method.PushNotificationMethods + +/** + * Reactive implementation of [PushNotificationMethods]. + * Allows access to API methods with endpoints having an "api/vX/push" prefix. + * @see Mastodon push notification API methods + */ +class RxPushNotificationMethods(client: MastodonClient) { + + private val pushNotificationMethods = PushNotificationMethods(client) + + /** + * Add a Web Push API subscription to receive notifications. + * Each access token can have one push subscription. If you create a new subscription, the old subscription is deleted. + * @param endpoint The endpoint URL that is called when a notification event occurs. + * @param userPublicKey User agent public key. Base64 encoded string of a public key from a ECDH keypair using the prime256v1 curve. + * @param userAuthSecret Auth secret, Base64 encoded string of 16 bytes of random data. + * @param mention Receive mention notifications? + * @param status Receive new subscribed account notifications? + * @param reblog Receive reblog notifications? + * @param follow Receive follow notifications? + * @param followRequest Receive follow request notifications? + * @param favourite Receive favourite notifications? + * @param poll Receive poll notifications? + * @param update Receive status edited notifications? + * @param adminSignUp Receive new user signup notifications? Defaults to false. Must have a role with the appropriate permissions. + * @param adminReport Receive new report notifications? Defaults to false. Must have a role with the appropriate permissions. + * @param policy Specify which to receive push notifications from. + * @see Mastodon API documentation: methods/push/#create + */ + @JvmOverloads + fun subscribePushNotification( + endpoint: String, + userPublicKey: String, + userAuthSecret: String, + mention: Boolean? = false, + status: Boolean? = false, + reblog: Boolean? = false, + follow: Boolean? = false, + followRequest: Boolean? = false, + favourite: Boolean? = false, + poll: Boolean? = false, + update: Boolean? = false, + adminSignUp: Boolean? = false, + adminReport: Boolean? = false, + policy: PushNotificationMethods.PushDataPolicy? = null + ): Single = + Single.fromCallable { + pushNotificationMethods.subscribePushNotification( + endpoint, + userPublicKey, + userAuthSecret, + mention, + status, + reblog, + follow, + followRequest, + favourite, + poll, + update, + adminSignUp, + adminReport, + policy + ).execute() + } + + /** + * Updates the current push subscription. Only the data part can be updated. + * To change fundamentals, a new subscription must be created instead. + * @param mention Receive mention notifications? + * @param status Receive new subscribed account notifications? + * @param reblog Receive reblog notifications? + * @param follow Receive follow notifications? + * @param followRequest Receive follow request notifications? + * @param favourite Receive favourite notifications? + * @param poll Receive poll notifications? + * @param update Receive status edited notifications? + * @param adminSignUp Receive new user signup notifications? Defaults to false. Must have a role with the appropriate permissions. + * @param adminReport Receive new report notifications? Defaults to false. Must have a role with the appropriate permissions. + * @param policy Specify which to receive push notifications from. + * @see Mastodon API documentation: methods/push/#update + */ + @JvmOverloads + fun updatePushSubscription( + mention: Boolean? = false, + status: Boolean? = false, + reblog: Boolean? = false, + follow: Boolean? = false, + followRequest: Boolean? = false, + favourite: Boolean? = false, + poll: Boolean? = false, + update: Boolean? = false, + adminSignUp: Boolean? = false, + adminReport: Boolean? = false, + policy: PushNotificationMethods.PushDataPolicy? = null + ): Single = + Single.fromCallable { + pushNotificationMethods.updatePushSubscription( + mention, + status, + reblog, + follow, + followRequest, + favourite, + poll, + update, + adminSignUp, + adminReport, + policy + ).execute() + } + + /** + * View the PushSubscription currently associated with this access token. + * @see Mastodon API documentation: methods/push/#get + */ + fun getPushNotification(): Single = Single.fromCallable { pushNotificationMethods.getPushNotification().execute() } + + /** + * Removes the current Web Push API subscription. + * @see Mastodon API documentation: methods/push/#delete + */ + fun removePushSubscription(): Completable = Completable.fromAction { pushNotificationMethods.removePushSubscription() } +} diff --git a/bigbone/src/main/kotlin/social/bigbone/MastodonClient.kt b/bigbone/src/main/kotlin/social/bigbone/MastodonClient.kt index 78db60986..e8e17a897 100644 --- a/bigbone/src/main/kotlin/social/bigbone/MastodonClient.kt +++ b/bigbone/src/main/kotlin/social/bigbone/MastodonClient.kt @@ -35,6 +35,7 @@ import social.bigbone.api.method.OAuthMethods import social.bigbone.api.method.OEmbedMethods import social.bigbone.api.method.PollMethods import social.bigbone.api.method.PreferenceMethods +import social.bigbone.api.method.PushNotificationMethods import social.bigbone.api.method.ReportMethods import social.bigbone.api.method.SearchMethods import social.bigbone.api.method.StatusMethods @@ -284,6 +285,13 @@ private constructor( @get:JvmName("timelines") val timelines: TimelineMethods by lazy { TimelineMethods(this) } + /** + * Access API methods under "push" endpoint. + */ + @Suppress("unused") // public API + @get:JvmName("pushNotifications") + val pushNotifications: PushNotificationMethods by lazy { PushNotificationMethods(this) } + /** * Specifies the HTTP methods / HTTP verb that can be used by this class. */ diff --git a/bigbone/src/main/kotlin/social/bigbone/api/entity/WebPushSubscription.kt b/bigbone/src/main/kotlin/social/bigbone/api/entity/WebPushSubscription.kt new file mode 100644 index 000000000..234bd19e0 --- /dev/null +++ b/bigbone/src/main/kotlin/social/bigbone/api/entity/WebPushSubscription.kt @@ -0,0 +1,103 @@ +package social.bigbone.api.entity + +import kotlinx.serialization.SerialName +import kotlinx.serialization.Serializable + +/** + * Represents a subscription to the push streaming server. + * @see Mastodon API Push + */ +@Serializable +data class WebPushSubscription( + + /** + * The ID of the Web Push subscription in the database. + */ + @SerialName("id") + val id: String = "", + + /** + * Where push alerts will be sent to. + */ + @SerialName("endpoint") + val endpoint: String = "", + + /** + * The streaming server’s VAPID key. + */ + @SerialName("server_key") + val serverKey: String = "", + + /** + * Which alerts should be delivered to the endpoint. + */ + @SerialName("alerts") + val alerts: Alerts +) + +/** + * Which alerts should be delivered to the endpoint. + * @see Mastodon API Push + */ +@Serializable +data class Alerts( + /** + * Receive a push notification when someone else has mentioned you in a status? + */ + @SerialName("mention") + val mention: Boolean? = false, + + /** + * Receive a push notification when a subscribed account posts a status? + */ + @SerialName("status") + val status: Boolean? = false, + + /** + * Receive a push notification when a status you created has been boosted by someone else? + */ + @SerialName("reblog") + val reblog: Boolean? = false, + + /** + * Receive a push notification when someone has followed you? + */ + @SerialName("follow") + val follow: Boolean? = false, + + /** + * Receive a push notification when someone has requested to followed you? + */ + @SerialName("follow_request") + val followRequest: Boolean? = false, + + /** + * Receive a push notification when a status you created has been favourited by someone else? + */ + @SerialName("favourite") + val favourite: Boolean? = false, + + /** + * Receive a push notification when a poll you voted in or created has ended? + */ + @SerialName("poll") + val poll: Boolean? = false, + + /** + * Receive a push notification when a status you interacted with has been edited? + */ + @SerialName("update") + val update: Boolean? = false, + + /** + * Receive a push notification when a new user has signed up? + */ + @SerialName("admin.sign_up") + val adminSignUp: Boolean? = false, + + /** + * Receive a push notification when a new report has been filed? + */ + @SerialName("admin.report") + val adminReport: Boolean? = false +) diff --git a/bigbone/src/main/kotlin/social/bigbone/api/method/PushNotificationMethods.kt b/bigbone/src/main/kotlin/social/bigbone/api/method/PushNotificationMethods.kt new file mode 100644 index 000000000..88b57c9c3 --- /dev/null +++ b/bigbone/src/main/kotlin/social/bigbone/api/method/PushNotificationMethods.kt @@ -0,0 +1,156 @@ +package social.bigbone.api.method + +import social.bigbone.MastodonClient +import social.bigbone.MastodonRequest +import social.bigbone.Parameters +import social.bigbone.api.entity.WebPushSubscription +import social.bigbone.api.exception.BigBoneRequestException + +/** + * Allows access to API methods with endpoints having an "api/vX/push" prefix. + * @see Mastodon push notification API methods + */ +class PushNotificationMethods(private val client: MastodonClient) { + + private val pushEndpoint = "/api/v1/push/subscription" + + /** + * Specify whether to receive push notifications from all, followed, follower, or none users. + */ + enum class PushDataPolicy { + ALL, FOLLOWED, FOLLOWER, NONE + } + + /** + * Add a Web Push API subscription to receive notifications. + * Each access token can have one push subscription. If you create a new subscription, the old subscription is deleted. + * @param endpoint The endpoint URL that is called when a notification event occurs. + * @param userPublicKey User agent public key. Base64 encoded string of a public key from a ECDH keypair using the prime256v1 curve. + * @param userAuthSecret Auth secret, Base64 encoded string of 16 bytes of random data. + * @param mention Receive mention notifications? + * @param status Receive new subscribed account notifications? + * @param reblog Receive reblog notifications? + * @param follow Receive follow notifications? + * @param followRequest Receive follow request notifications? + * @param favourite Receive favourite notifications? + * @param poll Receive poll notifications? + * @param update Receive status edited notifications? + * @param adminSignUp Receive new user signup notifications? Defaults to false. Must have a role with the appropriate permissions. + * @param adminReport Receive new report notifications? Defaults to false. Must have a role with the appropriate permissions. + * @param policy Specify which to receive push notifications from. + * @see Mastodon API documentation: methods/push/#create + */ + @Throws(BigBoneRequestException::class) + @JvmOverloads + fun subscribePushNotification( + endpoint: String, + userPublicKey: String, + userAuthSecret: String, + mention: Boolean? = false, + status: Boolean? = false, + reblog: Boolean? = false, + follow: Boolean? = false, + followRequest: Boolean? = false, + favourite: Boolean? = false, + poll: Boolean? = false, + update: Boolean? = false, + adminSignUp: Boolean? = false, + adminReport: Boolean? = false, + policy: PushDataPolicy? = null + ): MastodonRequest { + return client.getMastodonRequest( + endpoint = pushEndpoint, + method = MastodonClient.Method.POST, + parameters = Parameters().apply { + append("subscription[endpoint]", endpoint) + append("subscription[keys][p256dh]", userPublicKey) + append("subscription[keys][auth]", userAuthSecret) + mention?.let { append("data[alerts][mention]", it) } + status?.let { append("data[alerts][status]", it) } + reblog?.let { append("data[alerts][reblog]", it) } + follow?.let { append("data[alerts][follow]", it) } + followRequest?.let { append("data[alerts][follow_request]", it) } + favourite?.let { append("data[alerts][favourite]", it) } + poll?.let { append("data[alerts][poll]", it) } + update?.let { append("data[alerts][update]", it) } + adminSignUp?.let { append("data[alerts][admin.sign_up]", it) } + adminReport?.let { append("data[alerts][admin.report]", it) } + policy?.let { append("data[policy]", it.name.lowercase()) } + } + ) + } + + /** + * Updates the current push subscription. Only the data part can be updated. + * To change fundamentals, a new subscription must be created instead. + * @param mention Receive mention notifications? + * @param status Receive new subscribed account notifications? + * @param reblog Receive reblog notifications? + * @param follow Receive follow notifications? + * @param followRequest Receive follow request notifications? + * @param favourite Receive favourite notifications? + * @param poll Receive poll notifications? + * @param update Receive status edited notifications? + * @param adminSignUp Receive new user signup notifications? Defaults to false. Must have a role with the appropriate permissions. + * @param adminReport Receive new report notifications? Defaults to false. Must have a role with the appropriate permissions. + * @param policy Specify which to receive push notifications from. + * @see Mastodon API documentation: methods/push/#update + */ + @Throws(BigBoneRequestException::class) + @JvmOverloads + fun updatePushSubscription( + mention: Boolean? = false, + status: Boolean? = false, + reblog: Boolean? = false, + follow: Boolean? = false, + followRequest: Boolean? = false, + favourite: Boolean? = false, + poll: Boolean? = false, + update: Boolean? = false, + adminSignUp: Boolean? = false, + adminReport: Boolean? = false, + policy: PushDataPolicy? = null + ): MastodonRequest { + return client.getMastodonRequest( + endpoint = pushEndpoint, + method = MastodonClient.Method.PUT, + parameters = Parameters().apply { + mention?.let { append("data[alerts][mention]", it) } + status?.let { append("data[alerts][status]", it) } + reblog?.let { append("data[alerts][reblog]", it) } + follow?.let { append("data[alerts][follow]", it) } + followRequest?.let { append("data[alerts][follow_request]", it) } + favourite?.let { append("data[alerts][favourite]", it) } + poll?.let { append("data[alerts][poll]", it) } + update?.let { append("data[alerts][update]", it) } + adminSignUp?.let { append("data[alerts][admin.sign_up]", it) } + adminReport?.let { append("data[alerts][admin.report]", it) } + policy?.let { append("policy", it.name.lowercase()) } + } + ) + } + + /** + * View the PushSubscription currently associated with this access token. + * @see Mastodon API documentation: methods/push/#get + */ + @Throws(BigBoneRequestException::class) + fun getPushNotification(): MastodonRequest { + return client.getMastodonRequest( + endpoint = pushEndpoint, + method = MastodonClient.Method.GET + ) + } + + /** + * Removes the current Web Push API subscription. + * @see Mastodon API documentation: methods/push/#delete + */ + @Throws(BigBoneRequestException::class) + fun removePushSubscription() { + client.performAction( + endpoint = pushEndpoint, + method = MastodonClient.Method.DELETE + ) + } +} diff --git a/bigbone/src/main/kotlin/social/bigbone/api/method/StatusMethods.kt b/bigbone/src/main/kotlin/social/bigbone/api/method/StatusMethods.kt index fb0ad5450..b4a415311 100644 --- a/bigbone/src/main/kotlin/social/bigbone/api/method/StatusMethods.kt +++ b/bigbone/src/main/kotlin/social/bigbone/api/method/StatusMethods.kt @@ -183,7 +183,7 @@ class StatusMethods(private val client: MastodonClient) { append("poll[expires_in]", pollData.expiresIn) append("visibility", visibility.name.lowercase()) append("poll[multiple]", pollData.multiple ?: false) - append("poll[hide_totals", pollData.hideTotals ?: false) + append("poll[hide_totals]", pollData.hideTotals ?: false) inReplyToId?.let { append("in_reply_to_id", it) } append("sensitive", sensitive) spoilerText?.let { append("spoiler_text", it) } diff --git a/bigbone/src/test/assets/push_notification_subscription.json b/bigbone/src/test/assets/push_notification_subscription.json new file mode 100644 index 000000000..8b3a5f1b3 --- /dev/null +++ b/bigbone/src/test/assets/push_notification_subscription.json @@ -0,0 +1,12 @@ +{ + "id": "328183", + "endpoint": "https://yourdomain.example/listener", + "alerts": { + "follow": true, + "favourite": true, + "reblog": false, + "mention": true, + "poll": false + }, + "server_key": "BCk-QqERU0q-CfYZjcuB6lnyyOYfJ2AifKqfeGIm7Z-HiTU5T9eTG5GxVA0_OH5mMlI4UkkDTpaZwozy0TzdZ2M=" +} \ No newline at end of file diff --git a/bigbone/src/test/kotlin/social/bigbone/api/method/PushNotificationMethodsTest.kt b/bigbone/src/test/kotlin/social/bigbone/api/method/PushNotificationMethodsTest.kt new file mode 100644 index 000000000..90e1adb96 --- /dev/null +++ b/bigbone/src/test/kotlin/social/bigbone/api/method/PushNotificationMethodsTest.kt @@ -0,0 +1,79 @@ +package social.bigbone.api.method + +import org.amshove.kluent.shouldBeEqualTo +import org.amshove.kluent.shouldNotBeEqualTo +import org.junit.jupiter.api.Assertions +import org.junit.jupiter.api.Test +import social.bigbone.api.exception.BigBoneRequestException +import social.bigbone.testtool.MockClient + +class PushNotificationMethodsTest { + + @Test + fun subscribeToPushNotification() { + val client = MockClient.mock("push_notification_subscription.json") + val pushNotificationMethods = PushNotificationMethods(client) + val subscription = pushNotificationMethods.subscribePushNotification( + endpoint = "endpoint", + userPublicKey = "userPublicKey", + userAuthSecret = "userAuthSecret", + follow = true, + mention = true, + favourite = true + ).execute() + subscription.serverKey shouldNotBeEqualTo "" + subscription.alerts.follow shouldBeEqualTo true + subscription.alerts.mention shouldBeEqualTo true + subscription.alerts.poll shouldBeEqualTo false + subscription.alerts.reblog shouldBeEqualTo false + subscription.alerts.favourite shouldBeEqualTo true + } + + @Test + fun updatePushNotification() { + val client = MockClient.mock("push_notification_subscription.json") + val pushNotificationMethods = PushNotificationMethods(client) + val subscription = pushNotificationMethods.updatePushSubscription( + follow = true, + mention = true, + favourite = true + ).execute() + subscription.serverKey shouldNotBeEqualTo "" + subscription.alerts.follow shouldBeEqualTo true + subscription.alerts.mention shouldBeEqualTo true + subscription.alerts.poll shouldBeEqualTo false + subscription.alerts.reblog shouldBeEqualTo false + subscription.alerts.favourite shouldBeEqualTo true + } + + @Test + fun deletePushSubscriptionWithException() { + Assertions.assertThrows(BigBoneRequestException::class.java) { + val client = MockClient.ioException() + val pushNotificationMethods = PushNotificationMethods(client) + pushNotificationMethods.removePushSubscription() + } + } + + @Test + fun getPushNotification() { + val client = MockClient.mock("push_notification_subscription.json") + val pushNotificationMethods = PushNotificationMethods(client) + val response = pushNotificationMethods.getPushNotification().execute() + response.serverKey shouldNotBeEqualTo "" + response.alerts.follow shouldBeEqualTo true + response.alerts.mention shouldBeEqualTo true + response.alerts.poll shouldBeEqualTo false + response.alerts.reblog shouldBeEqualTo false + response.alerts.favourite shouldBeEqualTo true + } + + @Test + fun updatePushNotificationSubscriptionWithException() { + Assertions.assertThrows(BigBoneRequestException::class.java) { + val client = MockClient.ioException() + val pushNotificationMethods = PushNotificationMethods(client) + pushNotificationMethods.updatePushSubscription().execute() + } + } +}