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()
+ }
+ }
+}