Repository: andregasser/bigbone
Pull request #494 (2024-10-18 23:11:31)

Add grouped_notifications API
# Description

This PR adds new [methods for Grouped Notifications](https://docs.joinmastodon.org/methods/grouped_notifications) that were added as part of the most recent [Mastodon 4.3.0 release](https://github.com/mastodon/mastodon/releases).

Sorry, this got a lot larger than initially intended. When starting to add methods, I had to add some new properties and entities as well.

During implementation, I noticed one odd server return that I raised as a [bug ticket](https://github.com/mastodon/mastodon/issues/32594) as the return doesn’t feel right to me. To circumvent the current behaviour, I have added [ed72848](https://github.com/andregasser/bigbone/pull/494/commits/ed7284892184328ae260616f4ae1b2e6bf649e8b) as otherwise, our current approach to pageable data doesn’t allow for the structure returned.

# Type of Change

- New feature
- Documentation

# Breaking Changes

- `Notification.NotificationType` was moved to its own class. Import changes from `import social.bigbone.api.entity.Notification.NotificationType` to `import social.bigbone.api.entity.NotificationType`
- New `NotificationType`s were added and they are still in alphabetical order. So if you relied on `NotificationType#ordinal`, please be aware that ordinals changed.

# How Has This Been Tested?

1. New unit tests
2. New sample class ran on actual server

# Mandatory Checklist

- [x] I ran `gradle check` and there were no errors reported
- [x] I have performed a self-review of my code
- [x] I have added tests that prove my fix is effective or that my feature works
- [x] All tests pass locally with my changes
- [x] I have added KDoc documentation to all public methods

# Optional checks

- [x] In case you worked on a new feature: Did you also implement the reactive endpoint (bigbone-rx)?
- [x] In case you added new `*Methods` classes: Did you also reference it in the `MastodonClient` main class?
- [x] Did you also update the documentation in the `/docs` folder (e.g. API Coverage page)?



Patch:
diff --git a/bigbone-rx/src/main/kotlin/social/bigbone/rx/RxGroupedNotificationMethods.kt b/bigbone-rx/src/main/kotlin/social/bigbone/rx/RxGroupedNotificationMethods.kt
new file mode 100644
index 0000000..817a016
--- /dev/null
+++ b/bigbone-rx/src/main/kotlin/social/bigbone/rx/RxGroupedNotificationMethods.kt
@@ -0,0 +1,161 @@
+package social.bigbone.rx
+
+import io.reactivex.rxjava3.core.Completable
+import io.reactivex.rxjava3.core.Single
+import social.bigbone.MastodonClient
+import social.bigbone.MastodonRequest
+import social.bigbone.api.Pageable
+import social.bigbone.api.Range
+import social.bigbone.api.entity.Account
+import social.bigbone.api.entity.GroupedNotificationsResults
+import social.bigbone.api.entity.NotificationType
+import social.bigbone.api.entity.UnreadNotificationCount
+import social.bigbone.api.exception.BigBoneRequestException
+import social.bigbone.api.method.GroupedNotificationMethods
+import social.bigbone.api.method.GroupedNotificationMethods.ExpandAccounts
+import social.bigbone.api.method.NotificationMethods
+
+/**
+ * Reactive implementation of [GroupedNotificationMethods].
+ * Allows access to API methods with endpoints having an "api/v2/notifications" prefix.
+ *
+ * This differs from [RxNotificationMethods] in that it offers grouping.
+ *
+ * Grouped notifications were implemented server-side so that:
+ * 1. grouping is consistent across clients
+ * 2. clients do not run into the issue of going through entire pages that do not
+ * contribute to any new group; instead, notifications are already deduplicated server-side
+ *
+ * The API shape is a bit different from the non-grouped notifications,
+ * because large notification groups usually tend to involve the same accounts,
+ * and moving accounts to a root key can avoid a lot of duplication,
+ * resulting in less server-side work and smaller network payloads.
+ *
+ * @see <a href="https://docs.joinmastodon.org/methods/grouped_notifications/">Mastodon grouped notifications API methods</a>
+ * @since Mastodon 4.3.0
+ */
+class RxGroupedNotificationMethods(client: MastodonClient) {
+
+    private val groupedNotificationMethods = GroupedNotificationMethods(client)
+
+    /**
+     * Return grouped notifications concerning the user.
+     *
+     * Notifications of type favourite, follow or reblog with the same type and the same target
+     * made in a similar timeframe are given a same group_key by the server, and querying this
+     * endpoint will return aggregated notifications, with only one object per group_key.
+     *
+     * Other notification types may be grouped in the future.
+     * The grouped_types parameter should be used by the client to explicitly list the types it
+     * supports showing grouped notifications for.
+     *
+     * @param includeTypes Types to include in the results.
+     * @param excludeTypes Types to exclude from the results.
+     * @param accountId Return only notifications received from the specified account.
+     * @param expandAccounts One of [ExpandAccounts.FULL] or [ExpandAccounts.PARTIAL_AVATARS].
+     * When set to [ExpandAccounts.PARTIAL_AVATARS], some accounts will not be rendered in full
+     * in the returned accounts list but will be instead returned in stripped-down form in the
+     * partial_accounts list.
+     * The most recent account in a notification group is always rendered in full in the accounts attribute.
+     * @param groupedTypes Restricts which [NotificationType]s can be grouped.
+     * Use this if there are notification types for which your client does not support grouping.
+     * If omitted, the server will group notifications of all types it supports
+     * (4.3.0: [NotificationType.FAVOURITE], [NotificationType.FOLLOW], [NotificationType.REBLOG]).
+     * If you do not want any notification grouping, use [NotificationMethods.getAllNotifications] instead.
+     * Notifications that would be grouped if not for this parameter will instead be returned as individual
+     * single-notification groups with a unique group_key that can be assumed to be of the form "ungrouped-{notification_id}".
+     * @param includeFiltered Whether to include notifications filtered by the user’s NotificationPolicy. Defaults to false.
+     * @param range optional Range for the pageable return value.
+     * @see <a href="https://docs.joinmastodon.org/methods/grouped_notifications/#get-grouped">
+     *     Mastodon API documentation: methods/grouped_notifications/#get-grouped</a>
+     * @since Mastodon 4.3.0
+     */
+    @JvmOverloads
+    fun getAllGroupedNotifications(
+        includeTypes: List<NotificationType>? = null,
+        excludeTypes: List<NotificationType>? = null,
+        accountId: String? = null,
+        expandAccounts: ExpandAccounts? = null,
+        groupedTypes: List<NotificationType>? = null,
+        includeFiltered: Boolean = false,
+        range: Range = Range()
+    ): Single<MastodonRequest<Pageable<GroupedNotificationsResults>>> = Single.fromCallable {
+        groupedNotificationMethods.getAllGroupedNotifications(
+            includeTypes = includeTypes,
+            excludeTypes = excludeTypes,
+            accountId = accountId,
+            expandAccounts = expandAccounts,
+            groupedTypes = groupedTypes,
+            includeFiltered = includeFiltered,
+            range = range
+        )
+    }
+
+    /**
+     * View information about a specific notification group with a given group key.
+     * @param groupKey The group key of the notification group
+     * @see <a href="https://docs.joinmastodon.org/methods/grouped_notifications/#get-notification-group">
+     *     Mastodon API documentation: methods/grouped_notifications/#get-notification-group</a>
+     * @since Mastodon 4.3.0
+     */
+    fun getGroupedNotification(groupKey: String): Single<MastodonRequest<GroupedNotificationsResults>> = Single
+        .fromCallable { groupedNotificationMethods.getGroupedNotification(groupKey = groupKey) }
+
+    /**
+     * Dismiss a single notification group from the server.
+     * @param groupKey The group key of the notifications to discard.
+     * @see <a href="https://docs.joinmastodon.org/methods/grouped_notifications/#dismiss-group">
+     *     Mastodon API documentation: methods/grouped_notifications/#dismiss-group</a>
+     * @since Mastodon 4.3.0
+     */
+    @Throws(BigBoneRequestException::class)
+    fun dismissNotification(groupKey: String) = Completable.fromAction {
+        groupedNotificationMethods.dismissNotification(groupKey = groupKey)
+    }
+
+    /**
+     * Get accounts of all notifications in a notification group.
+     * @param groupKey The group key of the notifications to get accounts from.
+     * @see <a href="https://docs.joinmastodon.org/methods/grouped_notifications/#get-group-accounts">
+     *     Mastodon API documentation: methods/grouped_notifications/#get-group-accounts</a>
+     * @since Mastodon 4.3.0
+     */
+    fun getAccountsOfAllNotificationsInGroup(groupKey: String): Single<MastodonRequest<List<Account>>> = Single
+        .fromCallable { groupedNotificationMethods.getAccountsOfAllNotificationsInGroup(groupKey = groupKey) }
+
+    /**
+     * Get the (capped) number of unread notification groups for the current user.
+     * A notification is considered unread if it is more recent than the notifications read marker.
+     * Because the count is dependent on the parameters, it is computed every time and is thus a relatively slow operation
+     * (although faster than getting the full corresponding notifications), therefore the number of returned notifications is capped.
+     * @param limit Maximum number of results to return. Defaults to 100 notifications. Max 1_000 notifications.
+     * @param types [NotificationType]s that should count towards unread notifications.
+     * @param excludeTypes [NotificationType]s that should not count towards unread notifications.
+     * @param accountId Only count unread notifications received from the specified account.
+     * @param groupedTypes Restrict which [NotificationType]s can be grouped.
+     * Use this if there are [NotificationType]s for which your client does not support grouping.
+     * If omitted, the server will group notifications of all types it supports
+     * (4.3.0: [NotificationType.FAVOURITE], [NotificationType.FOLLOW], [NotificationType.REBLOG]).
+     * If you do not want any notification grouping, use [NotificationMethods.getUnreadCount] instead.
+     * @see <a href="https://docs.joinmastodon.org/methods/grouped_notifications/#unread-group-count">
+     *     Mastodon API documentation: methods/grouped_notifications/#unread-group-count</a>
+     * @since Mastodon 4.3.0
+     * @throws IllegalArgumentException if [limit] is set and higher than 1_000.
+     */
+    @Throws(IllegalArgumentException::class)
+    fun getNumberOfUnreadNotifications(
+        limit: Int? = null,
+        types: List<NotificationType>? = null,
+        excludeTypes: List<NotificationType>? = null,
+        accountId: String? = null,
+        groupedTypes: List<NotificationType>? = null
+    ): Single<MastodonRequest<UnreadNotificationCount>> = Single.fromCallable {
+        groupedNotificationMethods.getNumberOfUnreadNotifications(
+            limit = limit,
+            types = types,
+            excludeTypes = excludeTypes,
+            accountId = accountId,
+            groupedTypes = groupedTypes
+        )
+    }
+}
diff --git a/bigbone-rx/src/main/kotlin/social/bigbone/rx/RxNotificationMethods.kt b/bigbone-rx/src/main/kotlin/social/bigbone/rx/RxNotificationMethods.kt
index 7e15da0..5c1d139 100644
--- a/bigbone-rx/src/main/kotlin/social/bigbone/rx/RxNotificationMethods.kt
+++ b/bigbone-rx/src/main/kotlin/social/bigbone/rx/RxNotificationMethods.kt
@@ -6,6 +6,7 @@ import social.bigbone.MastodonClient
 import social.bigbone.api.Pageable
 import social.bigbone.api.Range
 import social.bigbone.api.entity.Notification
+import social.bigbone.api.entity.NotificationType
 import social.bigbone.api.method.NotificationMethods
 
 /**
@@ -27,8 +28,8 @@ class RxNotificationMethods(client: MastodonClient) {
      */
     @JvmOverloads
     fun getAllNotifications(
-        includeTypes: List<Notification.NotificationType>? = null,
-        excludeTypes: List<Notification.NotificationType>? = null,
+        includeTypes: List<NotificationType>? = null,
+        excludeTypes: List<NotificationType>? = null,
         accountId: String? = null,
         range: Range = Range()
     ): Single<Pageable<Notification>> = Single.fromCallable {
diff --git a/bigbone/src/main/kotlin/social/bigbone/MastodonClient.kt b/bigbone/src/main/kotlin/social/bigbone/MastodonClient.kt
index c61ddad..9dd991b 100644
--- a/bigbone/src/main/kotlin/social/bigbone/MastodonClient.kt
+++ b/bigbone/src/main/kotlin/social/bigbone/MastodonClient.kt
@@ -45,6 +45,7 @@ import social.bigbone.api.method.FeaturedTagMethods
 import social.bigbone.api.method.FilterMethods
 import social.bigbone.api.method.FollowRequestMethods
 import social.bigbone.api.method.FollowedTagMethods
+import social.bigbone.api.method.GroupedNotificationMethods
 import social.bigbone.api.method.InstanceMethods
 import social.bigbone.api.method.ListMethods
 import social.bigbone.api.method.MarkerMethods
@@ -293,6 +294,14 @@ class MastodonClient private constructor(
     @get:JvmName("followedTags")
     val followedTags: FollowedTagMethods by lazy { FollowedTagMethods(this) }
 
+    /**
+     * Access API methods under the "notification" endpoint that are specific to grouped notifications.
+     * See also: [notifications]
+     */
+    @Suppress("unused") // public API
+    @get:JvmName("groupedNotifications")
+    val groupedNotifications: GroupedNotificationMethods by lazy { GroupedNotificationMethods(this) }
+
     /**
      * Access API methods under the "instance" endpoint.
      */
diff --git a/bigbone/src/main/kotlin/social/bigbone/MastodonRequest.kt b/bigbone/src/main/kotlin/social/bigbone/MastodonRequest.kt
index 5753e1a..b6fa00c 100644
--- a/bigbone/src/main/kotlin/social/bigbone/MastodonRequest.kt
+++ b/bigbone/src/main/kotlin/social/bigbone/MastodonRequest.kt
@@ -55,14 +55,17 @@ class MastodonRequest<T>(
         val response = executor()
         if (response.isSuccessful) {
             try {
-                val body: String? = response.body?.string()
-                requireNotNull(body)
+                val body: String = response.body.string()
 
                 val element: JsonElement = JSON_SERIALIZER.parseToJsonElement(body)
                 if (element is JsonObject) {
                     action(body)
 
-                    return mapper(body) as T
+                    return if (isPageable) {
+                        listOf(mapper(body)).toPageable(response) as T
+                    } else {
+                        mapper(body) as T
+                    }
                 }
 
                 val mappedJsonElements: List<Any> = element.jsonArray.map { jsonElement: JsonElement ->
@@ -81,9 +84,7 @@ class MastodonRequest<T>(
                 throw BigBoneRequestException("Successful response could not be parsed", e)
             }
         } else {
-            val error = response.body?.string()?.let {
-                JSON_SERIALIZER.decodeFromString<Error>(it)
-            }
+            val error = JSON_SERIALIZER.decodeFromString<Error>(response.body.string())
             response.close()
             throw BigBoneRequestException(response, error)
         }
diff --git a/bigbone/src/main/kotlin/social/bigbone/api/entity/AccountWarning.kt b/bigbone/src/main/kotlin/social/bigbone/api/entity/AccountWarning.kt
new file mode 100644
index 0000000..268aba6
--- /dev/null
+++ b/bigbone/src/main/kotlin/social/bigbone/api/entity/AccountWarning.kt
@@ -0,0 +1,130 @@
+package social.bigbone.api.entity
+
+import kotlinx.serialization.ExperimentalSerializationApi
+import kotlinx.serialization.SerialName
+import kotlinx.serialization.Serializable
+import social.bigbone.DateTimeSerializer
+import social.bigbone.PrecisionDateTime
+import social.bigbone.PrecisionDateTime.InvalidPrecisionDateTime
+import social.bigbone.api.entity.AccountWarning.Action
+
+/**
+ * Moderation warning against a particular account.
+ * @see <a href="https://docs.joinmastodon.org/entities/AccountWarning/">entities/AccountWarning</a>
+ * @since Mastodon 4.3.0
+ */
+@Serializable
+data class AccountWarning(
+    /**
+     * The ID of the account warning in the database.
+     * Type: String (cast from integer)
+     * @since Mastodon 4.3.0
+     */
+    @SerialName("id")
+    val id: String = "",
+
+    /**
+     * Action taken against the account.
+     * @since Mastodon 4.3.0
+     */
+    @SerialName("action")
+    val action: Action? = null,
+
+    /**
+     * Message from the moderator to the target account.
+     * @since Mastodon 4.3.0
+     */
+    @SerialName("text")
+    val text: String = "",
+
+    /**
+     * List of status IDs that are relevant to the warning.
+     * When [action] is
+     * * [Action.MARK_STATUSES_AS_SENSITIVE] OR
+     * * [Action.DELETE_STATUSES],
+     * those are the affected statuses.
+     * @since Mastodon 4.3.0
+     */
+    @SerialName("status_ids")
+    val statusIds: List<String>? = null,
+
+    /**
+     * Account against which a moderation decision has been taken.
+     * @since Mastodon 4.3.0
+     */
+    @SerialName("target_account")
+    val targetAccount: Account? = null,
+
+    /**
+     * Appeal submitted by the target account, if any.
+     * @since Mastodon 4.3.0
+     */
+    @SerialName("appeal")
+    val appeal: Appeal? = null,
+
+    /**
+     * When the event took place.
+     * @since Mastodon 4.3.0
+     */
+    @Serializable(with = DateTimeSerializer::class)
+    @SerialName("created_at")
+    val createdAt: PrecisionDateTime = InvalidPrecisionDateTime.Unavailable,
+) {
+    /**
+     * Action that can be taken against an account.
+     */
+    @Serializable
+    enum class Action {
+        /**
+         * No action was taken, this is simply a warning.
+         * @since Mastodon 4.3.0
+         */
+        @SerialName("none")
+        NONE,
+
+        /**
+         * The account has been disabled.
+         * @since Mastodon 4.3.0
+         */
+        @SerialName("disable")
+        DISABLE,
+
+        /**
+         * Specific posts from the target account have been marked as sensitive.
+         * @since Mastodon 4.3.0
+         */
+        @SerialName("mark_statuses_as_sensitive")
+        MARK_STATUSES_AS_SENSITIVE,
+
+        /**
+         * Specific statuses from the target account have been deleted.
+         * @since Mastodon 4.3.0
+         */
+        @SerialName("delete_statuses")
+        DELETE_STATUSES,
+
+        /**
+         * All posts from the target account are marked as sensitive.
+         * @since Mastodon 4.3.0
+         */
+        @SerialName("sensitive")
+        SENSITIVE,
+
+        /**
+         * The target account has been limited.
+         * @since Mastodon 4.3.0
+         */
+        @SerialName("silence")
+        SILENCE,
+
+        /**
+         * The target account has been suspended.
+         * @since Mastodon 4.3.0
+         */
+        @SerialName("suspend")
+        SUSPEND;
+
+        @OptIn(ExperimentalSerializationApi::class)
+        val apiName: String get() = serializer().descriptor.getElementName(ordinal)
+    }
+}
diff --git a/bigbone/src/main/kotlin/social/bigbone/api/entity/Appeal.kt b/bigbone/src/main/kotlin/social/bigbone/api/entity/Appeal.kt
new file mode 100644
index 0000000..031ec02
--- /dev/null
+++ b/bigbone/src/main/kotlin/social/bigbone/api/entity/Appeal.kt
@@ -0,0 +1,54 @@
+package social.bigbone.api.entity
+
+import kotlinx.serialization.ExperimentalSerializationApi
+import kotlinx.serialization.SerialName
+import kotlinx.serialization.Serializable
+
+/**
+ * Appeal against a moderation action.
+ * @see <a href="https://docs.joinmastodon.org/entities/Appeal/">entities/Appeal</a>
+ * @since Mastodon 4.3.0
+ */
+@Serializable
+data class Appeal(
+    /**
+     * Text of the appeal from the moderated account to the moderators.
+     * @since Mastodon 4.3.0
+     */
+    @SerialName("text")
+    val text: String = "",
+
+    /**
+     * State of the appeal.
+     * @since Mastodon 4.3.0
+     */
+    @SerialName("state")
+    val state: State = State.PENDING
+) {
+    /**
+     * State an appeal can have.
+     */
+    @Serializable
+    enum class State {
+        /**
+         * The appeal has been approved by a moderator.
+         */
+        @SerialName("APPROVED")
+        APPROVED,
+
+        /**
+         * The appeal has been rejected by a moderator.
+         */
+        @SerialName("REJECTED")
+        REJECTED,
+
+        /**
+         * The appeal has been submitted, but neither approved nor rejected yet.
+         */
+        @SerialName("PENDING")
+        PENDING;
+
+        @OptIn(ExperimentalSerializationApi::class)
+        val apiName: String get() = serializer().descriptor.getElementName(ordinal)
+    }
+}
diff --git a/bigbone/src/main/kotlin/social/bigbone/api/entity/GroupedNotificationsResults.kt b/bigbone/src/main/kotlin/social/bigbone/api/entity/GroupedNotificationsResults.kt
new file mode 100644
index 0000000..5f67c81
--- /dev/null
+++ b/bigbone/src/main/kotlin/social/bigbone/api/entity/GroupedNotificationsResults.kt
@@ -0,0 +1,42 @@
+package social.bigbone.api.entity
+
+import kotlinx.serialization.SerialName
+import kotlinx.serialization.Serializable
+import social.bigbone.api.method.GroupedNotificationMethods
+
+/**
+ * GroupedNotificationsResults entity.
+ * @see <a href="https://docs.joinmastodon.org/methods/grouped_notifications/#GroupedNotificationsResults">GroupedNotificationsResults</a>
+ * @since Mastodon 4.3.0
+ */
+@Serializable
+data class GroupedNotificationsResults(
+    /**
+     * Accounts referenced by grouped notifications.
+     * @since Mastodon 4.3.0
+     */
+    @SerialName("accounts")
+    val accounts: List<Account> = emptyList(),
+
+    /**
+     * Partial accounts referenced by grouped notifications.
+     * Those are only returned when requesting grouped notifications with [GroupedNotificationMethods.ExpandAccounts.PARTIAL_AVATARS].
+     * @since Mastodon 4.3.0
+     */
+    @SerialName("partial_accounts")
+    val partialAccounts: String? = null,
+
+    /**
+     * Statuses referenced by grouped notifications.
+     * @since Mastodon 4.3.0
+     */
+    @SerialName("statuses")
+    val statuses: List<Status> = emptyList(),
+
+    /**
+     * The grouped notifications themselves.
+     * @since Mastodon 4.3.0
+     */
+    @SerialName("notification_groups")
+    val notificationGroups: List<NotificationGroup>
+)
diff --git a/bigbone/src/main/kotlin/social/bigbone/api/entity/Notification.kt b/bigbone/src/main/kotlin/social/bigbone/api/entity/Notification.kt
index 317a95a..bc147a7 100644
--- a/bigbone/src/main/kotlin/social/bigbone/api/entity/Notification.kt
+++ b/bigbone/src/main/kotlin/social/bigbone/api/entity/Notification.kt
@@ -1,6 +1,5 @@
 package social.bigbone.api.entity
 
-import kotlinx.serialization.ExperimentalSerializationApi
 import kotlinx.serialization.SerialName
 import kotlinx.serialization.Serializable
 import social.bigbone.DateTimeSerializer
@@ -48,45 +47,29 @@ data class Notification(
      * Report that was the object of the notification. Attached when type of the notification is admin.report.
      */
     @SerialName("report")
-    val report: Report? = null
-) {
+    val report: Report? = null,
+
     /**
-     * Specifies the notification type.
+     * Group key shared by similar notifications, to be used in the grouped notifications feature.
+     * Should be considered opaque, but ungrouped notifications can be assumed to have a group_key of the form "ungrouped-{notification_id}".
+     * @since Mastodon 4.3.0
      */
-    @Serializable
-    enum class NotificationType {
-
-        @SerialName("admin.report")
-        ADMIN_REPORT,
-
-        @SerialName("admin.sign_up")
-        ADMIN_SIGN_UP,
-
-        @SerialName("favourite")
-        FAVOURITE,
-
-        @SerialName("follow")
-        FOLLOW,
-
-        @SerialName("follow_request")
-        FOLLOW_REQUEST,
-
-        @SerialName("mention")
-        MENTION,
+    @SerialName("group_key")
+    val groupKey: String? = null,
 
-        @SerialName("poll")
-        POLL,
-
-        @SerialName("reblog")
-        REBLOG,
-
-        @SerialName("status")
-        STATUS,
-
-        @SerialName("update")
-        UPDATE;
+    /**
+     * Summary of the event that caused follow relationships to be severed.
+     * Attached when [type] of the notification is [NotificationType.SEVERED_RELATIONSHIPS].
+     * @since Mastodon 4.3.0
+     */
+    @SerialName("relationship_severance_event")
+    val relationshipSeveranceEvent: RelationshipSeveranceEvent? = null,
 
-        @OptIn(ExperimentalSerializationApi::class)
-        val apiName: String get() = serializer().descriptor.getElementName(ordinal)
-    }
-}
+    /**
+     * Moderation warning that caused the notification.
+     * Attached when [type] of the notification is [NotificationType.MODERATION_WARNING].
+     * @since Mastodon 4.3.0
+     */
+    @SerialName("moderation_warning")
+    val moderationWarning: AccountWarning? = null
+)
diff --git a/bigbone/src/main/kotlin/social/bigbone/api/entity/NotificationGroup.kt b/bigbone/src/main/kotlin/social/bigbone/api/entity/NotificationGroup.kt
new file mode 100644
index 0000000..0c95bff
--- /dev/null
+++ b/bigbone/src/main/kotlin/social/bigbone/api/entity/NotificationGroup.kt
@@ -0,0 +1,116 @@
+package social.bigbone.api.entity
+
+import kotlinx.serialization.SerialName
+import kotlinx.serialization.Serializable
+import social.bigbone.DateTimeSerializer
+import social.bigbone.PrecisionDateTime
+import social.bigbone.PrecisionDateTime.InvalidPrecisionDateTime
+
+/**
+ * NotificationGroup entity.
+ * @see <a href="https://docs.joinmastodon.org/methods/grouped_notifications/#NotificationGroup">grouped_notifications/#NotificationGroup</a>
+ * @since Mastodon 4.3.0
+ */
+@Serializable
+data class NotificationGroup(
+    /**
+     * Group key identifying the grouped notifications.
+     * Should be treated as an opaque value.
+     * @since Mastodon 4.3.0
+     */
+    @SerialName("group_key")
+    val groupKey: String = "",
+
+    /**
+     * Total number of individual notifications that are part of this notification group.
+     * @since Mastodon 4.3.0
+     */
+    @SerialName("notifications_count")
+    val notificationsCount: Int = 0,
+
+    /**
+     * The type of event that resulted in the notifications in this group.
+     * @since Mastodon 4.3.0
+     */
+    @SerialName("type")
+    val type: NotificationType? = null,
+
+    /**
+     * ID of the most recent notification in the group.
+     * @since Mastodon 4.3.0
+     */
+    @SerialName("most_recent_notification_id")
+    val mostRecentNotificationId: Int = 0,
+
+    /**
+     *  ID of the oldest notification from this group represented within the current page.
+     *  This is only returned when paginating through notification groups.
+     *  Useful when polling new notifications.
+     * @since Mastodon 4.3.0
+     */
+    @SerialName("page_min_id")
+    val pageMinId: String? = null,
+
+    /**
+     * ID of the newest notification from this group represented within the current page.
+     * This is only returned when paginating through notification groups.
+     * Useful when polling new notifications.
+     * @since Mastodon 4.3.0
+     */
+    @SerialName("page_max_id")
+    val pageMaxId: String? = null,
+
+    /**
+     * Date at which the most recent notification from this group within the current page has been created.
+     * This is only returned when paginating through notification groups.
+     * @since Mastodon 4.3.0
+     */
+    @SerialName("latest_page_notification_at")
+    @Serializable(with = DateTimeSerializer::class)
+    val latestPageNotificationAt: PrecisionDateTime = InvalidPrecisionDateTime.Unavailable,
+
+    /**
+     *  IDs of some of the accounts who most recently triggered notifications in this group.
+     * @since Mastodon 4.3.0
+     */
+    @SerialName("sample_account_ids")
+    val sampleAccountIds: List<String> = emptyList(),
+
+    /**
+     * ID of the [Status] that was the object of the notification.
+     * Attached when [type] of the notification is
+     * * [NotificationType.FAVOURITE],
+     * * [NotificationType.REBLOG],
+     * * [NotificationType.STATUS],
+     * * [NotificationType.MENTION],
+     * * [NotificationType.POLL], or
+     * * [NotificationType.UPDATE].
+     * @since Mastodon 4.3.0
+     */
+    @SerialName("status_id")
+    val statusId: String? = null,
+
+    /**
+     * Report that was the object of the notification.
+     * Attached when type of the notification is [NotificationType.ADMIN_REPORT].
+     * @since Mastodon 4.3.0
+     */
+    @SerialName("report")
+    val report: Report? = null,
+
+    /**
+     * Summary of the event that caused follow relationships to be severed.
+     * Attached when [type] of the notification is [NotificationType.SEVERED_RELATIONSHIPS].
+     * @since Mastodon 4.3.0
+     */
+    @SerialName("event")
+    val event: RelationshipSeveranceEvent? = null,
+
+    /**
+     * Moderation warning that caused the notification.
+     * Attached when [type] of the notification is [NotificationType.MODERATION_WARNING].
+     * @since Mastodon 4.3.0
+     */
+    @SerialName("moderation_warning")
+    val moderationWarning: AccountWarning? = null
+)
diff --git a/bigbone/src/main/kotlin/social/bigbone/api/entity/NotificationType.kt b/bigbone/src/main/kotlin/social/bigbone/api/entity/NotificationType.kt
new file mode 100644
index 0000000..275f000
--- /dev/null
+++ b/bigbone/src/main/kotlin/social/bigbone/api/entity/NotificationType.kt
@@ -0,0 +1,99 @@
+package social.bigbone.api.entity
+
+import kotlinx.serialization.ExperimentalSerializationApi
+import kotlinx.serialization.SerialName
+import kotlinx.serialization.Serializable
+
+/**
+ * Specifies the notification type.
+ */
+@Serializable
+enum class NotificationType {
+
+    /**
+     * A new report has been filed.
+     * @since Mastodon 4.0.0
+     */
+    @SerialName("admin.report")
+    ADMIN_REPORT,
+
+    /**
+     * Someone signed up (optionally sent to admins).
+     * @since Mastodon 3.5.0
+     */
+    @SerialName("admin.sign_up")
+    ADMIN_SIGN_UP,
+
+    /**
+     * Someone favourited one of your statuses.
+     * @since Mastodon 0.9.9
+     */
+    @SerialName("favourite")
+    FAVOURITE,
+
+    /**
+     * Someone followed you.
+     * @since Mastodon 0.9.9
+     */
+    @SerialName("follow")
+    FOLLOW,
+
+    /**
+     * Someone requested to follow you.
+     * @since Mastodon 3.1.0
+     */
+    @SerialName("follow_request")
+    FOLLOW_REQUEST,
+
+    /**
+     * Someone mentioned you in their status.
+     * @since Mastodon 0.9.9
+     */
+    @SerialName("mention")
+    MENTION,
+
+    /**
+     * A moderator has taken action against your account or has sent you a warning.
+     * @since Mastodon 4.3.0
+     */
+    @SerialName("moderation_warning")
+    MODERATION_WARNING,
+
+    /**
+     * A poll you have voted in or created has ended.
+     * @since Mastodon 2.8.0
+     */
+    @SerialName("poll")
+    POLL,
+
+    /**
+     * Someone boosted one of your statuses.
+     * @since Mastodon 0.9.9
+     */
+    @SerialName("reblog")
+    REBLOG,
+
+    /**
+     * Some of your follow relationships have been severed as a result of a moderation or block event.
+     * @since Mastodon 4.3.0
+     */
+    @SerialName("severed_relationships")
+    SEVERED_RELATIONSHIPS,
+
+    /**
+     * Someone you enabled notifications for has posted a status.
+     * @since Mastodon 3.3.0
+     */
+    @SerialName("status")
+    STATUS,
+
+    /**
+     * A status you interacted with has been edited.
+     * @since Mastodon 3.5.0
+     */
+    @SerialName("update")
+    UPDATE;
+
+    @OptIn(ExperimentalSerializationApi::class)
+    val apiName: String get() = serializer().descriptor.getElementName(ordinal)
+}
diff --git a/bigbone/src/main/kotlin/social/bigbone/api/entity/RelationshipSeveranceEvent.kt b/bigbone/src/main/kotlin/social/bigbone/api/entity/RelationshipSeveranceEvent.kt
new file mode 100644
index 0000000..4e0e386
--- /dev/null
+++ b/bigbone/src/main/kotlin/social/bigbone/api/entity/RelationshipSeveranceEvent.kt
@@ -0,0 +1,91 @@
+package social.bigbone.api.entity
+
+import kotlinx.serialization.ExperimentalSerializationApi
+import kotlinx.serialization.SerialName
+import kotlinx.serialization.Serializable
+import social.bigbone.DateTimeSerializer
+import social.bigbone.PrecisionDateTime
+import social.bigbone.PrecisionDateTime.InvalidPrecisionDateTime
+
+/**
+ * Summary of a moderation or block event that caused follow relationships to be severed.
+ * @see <a href="https://docs.joinmastodon.org/entities/RelationshipSeveranceEvent/">entities/RelationshipSeveranceEvent</a>
+ * @since Mastodon 4.3.0
+ */
+@Serializable
+data class RelationshipSeveranceEvent(
+    /**
+     * The ID of the relationship severance event in the database.
+     * Type: String (cast from integer)
+     * @since Mastodon 4.3.0
+     */
+    @SerialName("id")
+    val id: String = "",
+
+    /**
+     * Type of event.
+     * @since Mastodon 4.3.0
+     */
+    @SerialName("type")
+    val type: Type? = null,
+
+    /**
+     * Whether the list of severed relationships is unavailable because the underlying issue has been purged.
+     * @since Mastodon 4.3.0
+     */
+    @SerialName("purged")
+    val purged: Boolean = false,
+
+    /**
+     * Name of the target of the moderation/block event.
+     * This is either a domain name or a user handle, depending on the event type.
+     * @since Mastodon 4.3.0
+     */
+    @SerialName("target_name")
+    val targetName: String = "",
+
+    /**
+     * Number of follow relationships (in either direction) that were severed.
+     * @since Mastodon 4.3.0
+     */
+    @SerialName("relationships_count")
+    val relationshipsCount: Int? = null,
+
+    /**
+     * When the event took place.
+     * @since Mastodon 4.3.0
+     */
+    @Serializable(with = DateTimeSerializer::class)
+    @SerialName("created_at")
+    val createdAt: PrecisionDateTime = InvalidPrecisionDateTime.Unavailable,
+) {
+    /**
+     * Type of RelationshipSeveranceEvent.
+     */
+    @Serializable
+    enum class Type {
+        /**
+         * A moderator suspended a whole domain.
+         * @since Mastodon 4.3.0
+         */
+        @SerialName("domain_block")
+        DOMAIN_BLOCK,
+
+        /**
+         * The user blocked a whole domain.
+         * @since Mastodon 4.3.0
+         */
+        @SerialName("user_domain_block")
+        USER_DOMAIN_BLOCK,
+
+        /**
+         * A moderator suspended a specific account.
+         * @since Mastodon 4.3.0
+         */
+        @SerialName("account_suspension")
+        ACCOUNT_SUSPENSION;
+
+        @OptIn(ExperimentalSerializationApi::class)
+        val apiName: String get() = serializer().descriptor.getElementName(ordinal)
+    }
+}
diff --git a/bigbone/src/main/kotlin/social/bigbone/api/entity/UnreadNotificationCount.kt b/bigbone/src/main/kotlin/social/bigbone/api/entity/UnreadNotificationCount.kt
new file mode 100644
index 0000000..618d2eb
--- /dev/null
+++ b/bigbone/src/main/kotlin/social/bigbone/api/entity/UnreadNotificationCount.kt
@@ -0,0 +1,14 @@
+package social.bigbone.api.entity
+
+import kotlinx.serialization.SerialName
+import kotlinx.serialization.Serializable
+
+/**
+ * Capped count of unread notifications.
+ * @since Mastodon 4.3.0
+ */
+@Serializable
+data class UnreadNotificationCount(
+    @SerialName("count")
+    val count: Int
+)
diff --git a/bigbone/src/main/kotlin/social/bigbone/api/method/GroupedNotificationMethods.kt b/bigbone/src/main/kotlin/social/bigbone/api/method/GroupedNotificationMethods.kt
new file mode 100644
index 0000000..42a1bb1
--- /dev/null
+++ b/bigbone/src/main/kotlin/social/bigbone/api/method/GroupedNotificationMethods.kt
@@ -0,0 +1,201 @@
+package social.bigbone.api.method
+
+import social.bigbone.MastodonClient
+import social.bigbone.MastodonRequest
+import social.bigbone.Parameters
+import social.bigbone.api.Pageable
+import social.bigbone.api.Range
+import social.bigbone.api.entity.Account
+import social.bigbone.api.entity.GroupedNotificationsResults
+import social.bigbone.api.entity.NotificationType
+import social.bigbone.api.entity.UnreadNotificationCount
+import social.bigbone.api.exception.BigBoneRequestException
+
+/**
+ * Allows access to API methods with endpoints having an "api/v2/notifications" prefix.
+ *
+ * This differs from [NotificationMethods] in that it offers grouping.
+ *
+ * Grouped notifications were implemented server-side so that:
+ * 1. grouping is consistent across clients
+ * 2. clients do not run into the issue of going through entire pages that do not
+ * contribute to any new group; instead, notifications are already deduplicated server-side
+ *
+ * The API shape is a bit different from the non-grouped notifications,
+ * because large notification groups usually tend to involve the same accounts,
+ * and moving accounts to a root key can avoid a lot of duplication,
+ * resulting in less server-side work and smaller network payloads.
+ *
+ * @see <a href="https://docs.joinmastodon.org/methods/grouped_notifications/">Mastodon grouped notifications API methods</a>
+ * @since Mastodon 4.3.0
+ */
+class GroupedNotificationMethods(private val client: MastodonClient) {
+
+    private val endpoint = "api/v2/notifications"
+
+    /**
+     * ExpandAccounts type that can be passed for [getAllGroupedNotifications].
+     * @see getAllGroupedNotifications
+     */
+    enum class ExpandAccounts {
+        FULL,
+        PARTIAL_AVATARS;
+
+        val apiName = this.name.lowercase()
+    }
+
+    /**
+     * Return grouped notifications concerning the user.
+     *
+     * Notifications of type favourite, follow or reblog with the same type and the same target
+     * made in a similar timeframe are given a same group_key by the server, and querying this
+     * endpoint will return aggregated notifications, with only one object per group_key.
+     *
+     * Other notification types may be grouped in the future.
+     * The grouped_types parameter should be used by the client to explicitly list the types it
+     * supports showing grouped notifications for.
+     *
+     * @param includeTypes Types to include in the results.
+     * @param excludeTypes Types to exclude from the results.
+     * @param accountId Return only notifications received from the specified account.
+     * @param expandAccounts One of [ExpandAccounts.FULL] or [ExpandAccounts.PARTIAL_AVATARS].
+     * When set to [ExpandAccounts.PARTIAL_AVATARS], some accounts will not be rendered in full
+     * in the returned accounts list but will be instead returned in stripped-down form in the
+     * partial_accounts list.
+     * The most recent account in a notification group is always rendered in full in the accounts attribute.
+     * @param groupedTypes Restricts which [NotificationType]s can be grouped.
+     * Use this if there are notification types for which your client does not support grouping.
+     * If omitted, the server will group notifications of all types it supports
+     * (4.3.0: [NotificationType.FAVOURITE], [NotificationType.FOLLOW], [NotificationType.REBLOG]).
+     * If you do not want any notification grouping, use [NotificationMethods.getAllNotifications] instead.
+     * Notifications that would be grouped if not for this parameter will instead be returned as individual
+     * single-notification groups with a unique group_key that can be assumed to be of the form "ungrouped-{notification_id}".
+     * @param includeFiltered Whether to include notifications filtered by the user’s NotificationPolicy. Defaults to false.
+     * @param range optional Range for the pageable return value.
+     * @see <a href="https://docs.joinmastodon.org/methods/grouped_notifications/#get-grouped">
+     *     Mastodon API documentation: methods/grouped_notifications/#get-grouped</a>
+     * @since Mastodon 4.3.0
+     */
+    @JvmOverloads
+    fun getAllGroupedNotifications(
+        includeTypes: List<NotificationType>? = null,
+        excludeTypes: List<NotificationType>? = null,
+        accountId: String? = null,
+        expandAccounts: ExpandAccounts? = null,
+        groupedTypes: List<NotificationType>? = null,
+        includeFiltered: Boolean = false,
+        range: Range = Range()
+    ): MastodonRequest<Pageable<GroupedNotificationsResults>> {
+        return client.getPageableMastodonRequest(
+            endpoint = endpoint,
+            method = MastodonClient.Method.GET,
+            parameters = range.toParameters().apply {
+                includeTypes?.let {
+                    append("types", includeTypes.map(NotificationType::apiName))
+                }
+                excludeTypes?.let {
+                    append("exclude_types", excludeTypes.map(NotificationType::apiName))
+                }
+                accountId?.let { append("account_id", accountId) }
+                expandAccounts?.let { append("expand_accounts", expandAccounts.apiName) }
+                groupedTypes?.let {
+                    append("grouped_types", groupedTypes.map(NotificationType::apiName))
+                }
+                append("include_filtered", includeFiltered)
+            }
+        )
+    }
+
+    /**
+     * View information about a specific notification group with a given group key.
+     * @param groupKey The group key of the notification group
+     * @see <a href="https://docs.joinmastodon.org/methods/grouped_notifications/#get-notification-group">
+     *     Mastodon API documentation: methods/grouped_notifications/#get-notification-group</a>
+     * @since Mastodon 4.3.0
+     */
+    fun getGroupedNotification(groupKey: String): MastodonRequest<GroupedNotificationsResults> {
+        return client.getMastodonRequest(
+            endpoint = "$endpoint/$groupKey",
+            method = MastodonClient.Method.GET
+        )
+    }
+
+    /**
+     * Dismiss a single notification group from the server.
+     * @param groupKey The group key of the notifications to discard.
+     * @see <a href="https://docs.joinmastodon.org/methods/grouped_notifications/#dismiss-group">
+     *     Mastodon API documentation: methods/grouped_notifications/#dismiss-group</a>
+     * @since Mastodon 4.3.0
+     */
+    @Throws(BigBoneRequestException::class)
+    fun dismissNotification(groupKey: String) {
+        client.performAction(
+            endpoint = "$endpoint/$groupKey/dismiss",
+            method = MastodonClient.Method.POST
+        )
+    }
+
+    /**
+     * Get accounts of all notifications in a notification group.
+     * @param groupKey The group key of the notifications to get accounts from.
+     * @see <a href="https://docs.joinmastodon.org/methods/grouped_notifications/#get-group-accounts">
+     *     Mastodon API documentation: methods/grouped_notifications/#get-group-accounts</a>
+     * @since Mastodon 4.3.0
+     */
+    fun getAccountsOfAllNotificationsInGroup(groupKey: String): MastodonRequest<List<Account>> {
+        return client.getMastodonRequestForList(
+            endpoint = "$endpoint/$groupKey/accounts",
+            method = MastodonClient.Method.GET
+        )
+    }
+
+    /**
+     * Get the (capped) number of unread notification groups for the current user.
+     * A notification is considered unread if it is more recent than the notifications read marker.
+     * Because the count is dependent on the parameters, it is computed every time and is thus a relatively slow operation
+     * (although faster than getting the full corresponding notifications), therefore the number of returned notifications is capped.
+     * @param limit Maximum number of results to return. Defaults to 100 notifications. Max 1_000 notifications.
+     * @param types [NotificationType]s that should count towards unread notifications.
+     * @param excludeTypes [NotificationType]s that should not count towards unread notifications.
+     * @param accountId Only count unread notifications received from the specified account.
+     * @param groupedTypes Restrict which [NotificationType]s can be grouped.
+     * Use this if there are [NotificationType]s for which your client does not support grouping.
+     * If omitted, the server will group notifications of all types it supports
+     * (4.3.0: [NotificationType.FAVOURITE], [NotificationType.FOLLOW], [NotificationType.REBLOG]).
+     * If you do not want any notification grouping, use [NotificationMethods.getUnreadCount] instead.
+     * @see <a href="https://docs.joinmastodon.org/methods/grouped_notifications/#unread-group-count">
+     *     Mastodon API documentation: methods/grouped_notifications/#unread-group-count</a>
+     * @since Mastodon 4.3.0
+     * @throws IllegalArgumentException if [limit] is set and higher than 1_000.
+     */
+    @Throws(IllegalArgumentException::class)
+    fun getNumberOfUnreadNotifications(
+        limit: Int? = null,
+        types: List<NotificationType>? = null,
+        excludeTypes: List<NotificationType>? = null,
+        accountId: String? = null,
+        groupedTypes: List<NotificationType>? = null
+    ): MastodonRequest<UnreadNotificationCount> {
+        if (limit != null) {
+            require(limit <= 1_000) { "Limit must be no larger than 1000 but was $limit" }
+        }
+
+        return client.getMastodonRequest(
+            endpoint = "$endpoint/unread_count",
+            method = MastodonClient.Method.GET,
+            parameters = Parameters().apply {
+                limit?.let { append("limit", limit) }
+                accountId?.let { append("account_id", accountId) }
+                types?.let {
+                    append("types", types.map(NotificationType::apiName))
+                }
+                excludeTypes?.let {
+                    append("exclude_types", excludeTypes.map(NotificationType::apiName))
+                }
+                groupedTypes?.let {
+                    append("types", groupedTypes.map(NotificationType::apiName))
+                }
+            }
+        )
+    }
+}
diff --git a/bigbone/src/main/kotlin/social/bigbone/api/method/NotificationMethods.kt b/bigbone/src/main/kotlin/social/bigbone/api/method/NotificationMethods.kt
index c7556e8..512da9a 100644
--- a/bigbone/src/main/kotlin/social/bigbone/api/method/NotificationMethods.kt
+++ b/bigbone/src/main/kotlin/social/bigbone/api/method/NotificationMethods.kt
@@ -2,9 +2,12 @@ package social.bigbone.api.method
 
 import social.bigbone.MastodonClient
 import social.bigbone.MastodonRequest
+import social.bigbone.Parameters
 import social.bigbone.api.Pageable
 import social.bigbone.api.Range
 import social.bigbone.api.entity.Notification
+import social.bigbone.api.entity.NotificationType
+import social.bigbone.api.entity.UnreadNotificationCount
 import social.bigbone.api.exception.BigBoneRequestException
 
 /**
@@ -25,8 +28,8 @@ class NotificationMethods(private val client: MastodonClient) {
      */
     @JvmOverloads
     fun getAllNotifications(
-        includeTypes: List<Notification.NotificationType>? = null,
-        excludeTypes: List<Notification.NotificationType>? = null,
+        includeTypes: List<NotificationType>? = null,
+        excludeTypes: List<NotificationType>? = null,
         accountId: String? = null,
         range: Range = Range()
     ): MastodonRequest<Pageable<Notification>> {
@@ -35,10 +38,10 @@ class NotificationMethods(private val client: MastodonClient) {
             method = MastodonClient.Method.GET,
             parameters = range.toParameters().apply {
                 includeTypes?.let {
-                    append("types", includeTypes.map(Notification.NotificationType::apiName))
+                    append("types", includeTypes.map(NotificationType::apiName))
                 }
                 excludeTypes?.let {
-                    append("exclude_types", excludeTypes.map(Notification.NotificationType::apiName))
+                    append("exclude_types", excludeTypes.map(NotificationType::apiName))
                 }
                 accountId?.let { append("account_id", accountId) }
             }
@@ -81,4 +84,44 @@ class NotificationMethods(private val client: MastodonClient) {
             method = MastodonClient.Method.POST
         )
     }
+
+    /**
+     * Get the (capped) number of unread notification groups for the current user.
+     * A notification is considered unread if it is more recent than the notifications read marker.
+     * Because the count is dependent on the parameters, it is computed every time and is thus a relatively slow operation
+     * (although faster than getting the full corresponding notifications), therefore the number of returned notifications is capped.
+     * @param limit Maximum number of results to return. Defaults to 100 notifications. Max 1_000 notifications.
+     * @param types [NotificationType]s that should count towards unread notifications.
+     * @param excludeTypes [NotificationType]s that should not count towards unread notifications.
+     * @param accountId Only count unread notifications received from the specified account.
+     * @see <a href="https://docs.joinmastodon.org/methods/notifications/#unread-count">Mastodon API documentation: methods/notifications/#unread-count</a>
+     * @since Mastodon 4.3.0
+     * @throws IllegalArgumentException if [limit] is set and higher than 1_000.
+     */
+    @Throws(IllegalArgumentException::class)
+    fun getNumberOfUnreadNotifications(
+        limit: Int? = null,
+        types: List<NotificationType>? = null,
+        excludeTypes: List<NotificationType>? = null,
+        accountId: String? = null
+    ): MastodonRequest<UnreadNotificationCount> {
+        if (limit != null) {
+            require(limit <= 1_000) { "Limit must be no larger than 1000" }
+        }
+
+        return client.getMastodonRequest(
+            endpoint = "$endpoint/unread_count",
+            method = MastodonClient.Method.GET,
+            parameters = Parameters().apply {
+                limit?.let { append("limit", limit) }
+                accountId?.let { append("account_id", accountId) }
+                types?.let {
+                    append("types", types.map(NotificationType::apiName))
+                }
+                excludeTypes?.let {
+                    append("exclude_types", excludeTypes.map(NotificationType::apiName))
+                }
+            }
+        )
+    }
 }
diff --git a/docs/api-coverage/grouped-notifications.md b/docs/api-coverage/grouped-notifications.md
new file mode 100644
index 0000000..c33beed
--- /dev/null
+++ b/docs/api-coverage/grouped-notifications.md
@@ -0,0 +1,20 @@
+---
+title: Grouped Notifications
+layout: default
+parent: API Coverage
+nav_order: 38
+---
+
+# Grouped Notifications
+
+Receive grouped notifications for activity on your account or statuses.
+
+<a href="https://docs.joinmastodon.org/methods/grouped_notifications/" target="_blank">https://docs.joinmastodon.org/methods/grouped_notifications/</a>
+
+| Method                                          | Description                                               | Status                          | Comments        | 
+|-------------------------------------------------|-----------------------------------------------------------|---------------------------------|-----------------|
+| `GET /api/v2/notifications`                     | Get all grouped notifications                             | <img src="/assets/green16.png"> | Fully supported |
+| `GET /api/v2/notifications/:group_key`          | Get a single notification group                           | <img src="/assets/green16.png"> | Fully supported |
+| `POST /api/v2/notifications/:group_key/dismiss` | Dismiss a single notification group                       | <img src="/assets/green16.png"> | Fully supported |
+| `GET /api/v2/notifications/:group_key/accounts` | Get accounts of all notifications in a notification group | <img src="/assets/green16.png"> | Fully supported |
+| `GET /api/v2/notifications/unread_count`        | Get the number of unread notifications                    | <img src="/assets/green16.png"> | Fully supported |
diff --git a/docs/api-coverage/notifications.md b/docs/api-coverage/notifications.md
index 1bd9543..daa754a 100644
--- a/docs/api-coverage/notifications.md
+++ b/docs/api-coverage/notifications.md
@@ -42,4 +42,54 @@ Receive notifications for activity on your account or statuses.
     <td style="width:10%;text-align:center;"><img src="/assets/red16.png"></td>
     <td style="width:45%;text-align:left;"><b>Will not be implemented</b>.</td>
   </tr>
+  <tr>
+    <td style="width:45%;text-align:left;"><code>GET /api/v1/notifications/unread_count</code><br>Get the number of unread notifications</td>
+    <td style="width:10%;text-align:center;"><img src="/assets/green16.png"></td>
+    <td style="width:45%;text-align:left;">Fully supported.</td>
+  </tr>
+  <tr>
+    <td style="width:45%;text-align:left;"><code>GET /api/v2/notifications/policy</code><br>Get the filtering policy for notifications</td>
+    <td style="width:10%;text-align:center;"><img src="/assets/red16.png"></td>
+    <td style="width:45%;text-align:left;">Not yet implemented.</td>
+  </tr>
+  <tr>
+    <td style="width:45%;text-align:left;"><code>PATCH /api/v2/notifications/policy</code><br>Update the filtering policy for notifications</td>
+    <td style="width:10%;text-align:center;"><img src="/assets/red16.png"></td>
+    <td style="width:45%;text-align:left;">Not yet implemented.</td>
+  </tr>
+  <tr>
+    <td style="width:45%;text-align:left;"><code>GET /api/v1/notifications/requests</code><br>Get all notification requests</td>
+    <td style="width:10%;text-align:center;"><img src="/assets/red16.png"></td>
+    <td style="width:45%;text-align:left;">Not yet implemented.</td>
+  </tr>
+  <tr>
+    <td style="width:45%;text-align:left;"><code>GET /api/v1/notifications/requests/:id</code><br>Get a single notification requests</td>
+    <td style="width:10%;text-align:center;"><img src="/assets/red16.png"></td>
+    <td style="width:45%;text-align:left;">Not yet implemented.</td>
+  </tr>
+  <tr>
+    <td style="width:45%;text-align:left;"><code>POST /api/v1/notifications/requests/:id/accept</code><br>Accept a single notification request</td>
+    <td style="width:10%;text-align:center;"><img src="/assets/red16.png"></td>
+    <td style="width:45%;text-align:left;">Not yet implemented.</td>
+  </tr>
+  <tr>
+    <td style="width:45%;text-align:left;"><code>POST /api/v1/notifications/requests/:id/dismiss</code><br>Dismiss a single notification request</td>
+    <td style="width:10%;text-align:center;"><img src="/assets/red16.png"></td>
+    <td style="width:45%;text-align:left;">Not yet implemented.</td>
+  </tr>
+  <tr>
+    <td style="width:45%;text-align:left;"><code>POST /api/v1/notifications/requests/accept</code><br>Accept multiple notification requests</td>
+    <td style="width:10%;text-align:center;"><img src="/assets/red16.png"></td>
+    <td style="width:45%;text-align:left;">Not yet implemented.</td>
+  </tr>
+  <tr>
+    <td style="width:45%;text-align:left;"><code>POST /api/v1/notifications/requests/dismiss</code><br>Dismiss multiple notification requests</td>
+    <td style="width:10%;text-align:center;"><img src="/assets/red16.png"></td>
+    <td style="width:45%;text-align:left;">Not yet implemented.</td>
+  </tr>
+  <tr>
+    <td style="width:45%;text-align:left;"><code>GET /api/v1/notifications/requests/merged</code><br>Check if accepted notification requests have been merged</td>
+    <td style="width:10%;text-align:center;"><img src="/assets/red16.png"></td>
+    <td style="width:45%;text-align:left;">Not yet implemented.</td>
+  </tr>
 </table>
diff --git a/sample-kotlin/src/main/kotlin/social/bigbone/sample/GetGroupedNotifications.kt b/sample-kotlin/src/main/kotlin/social/bigbone/sample/GetGroupedNotifications.kt
new file mode 100644
index 0000000..d40c74b
--- /dev/null
+++ b/sample-kotlin/src/main/kotlin/social/bigbone/sample/GetGroupedNotifications.kt
@@ -0,0 +1,61 @@
+package social.bigbone.sample
+
+import social.bigbone.MastodonClient
+import social.bigbone.api.Pageable
+import social.bigbone.api.Range
+import social.bigbone.api.entity.GroupedNotificationsResults
+import social.bigbone.api.entity.NotificationType
+
+object GetGroupedNotifications {
+
+    /**
+     * Get all grouped notifications for the account having the access token supplied via [args].
+     *
+     * Example call: "mastodon.social" "$TOKEN" "favourite,mention" "poll,reblog" "$ACCOUNT_ID"
+     */
+    @JvmStatic
+    fun main(args: Array<String>) {
+        val instance: String = args[0]
+        val accessToken: String = args[1]
+        val includeTypes: String = args.getOrElse(2) { "" } // Comma-separated
+        val excludeTypes: String = args.getOrElse(3) { "" } // Comma-separated
+        val accountId: String? = args.getOrNull(4)
+
+        // Instantiate client
+        val client = MastodonClient.Builder(instance)
+            .accessToken(accessToken)
+            .build()
+
+        // Get grouped notifications
+        val firstPart: Pageable<GroupedNotificationsResults> = client.groupedNotifications.getAllGroupedNotifications(
+            includeTypes = includeTypes.explodeToNotificationTypes(),
+            excludeTypes = excludeTypes.explodeToNotificationTypes(),
+            accountId = accountId,
+            range = Range(limit = 2)
+        ).execute()
+
+        println("Part 1: ${firstPart.part.first()}")
+
+        val secondPart: Pageable<GroupedNotificationsResults> = client.groupedNotifications.getAllGroupedNotifications(
+            includeTypes = includeTypes.explodeToNotificationTypes(),
+            excludeTypes = excludeTypes.explodeToNotificationTypes(),
+            accountId = accountId,
+            range = firstPart.nextRange(limit = 2)
+        ).execute()
+
+        println("Part 2: ${secondPart.part.first()}")
+    }
+
+    private fun String.explodeToNotificationTypes(): List<NotificationType>? {
+        return split(",")
+            .mapNotNull { it.toNotificationType() }
+            .takeIf { it.isNotEmpty() }
+    }
+
+    private fun String.toNotificationType(): NotificationType? {
+        for (notificationType in NotificationType.entries) {
+            if (notificationType.apiName.equals(this.trim(), ignoreCase = true)) return notificationType
+        }
+        return null
+    }
+}
diff --git a/sample-kotlin/src/main/kotlin/social/bigbone/sample/GetNotifications.kt b/sample-kotlin/src/main/kotlin/social/bigbone/sample/GetNotifications.kt
index 8e1b65d..3bce564 100644
--- a/sample-kotlin/src/main/kotlin/social/bigbone/sample/GetNotifications.kt
+++ b/sample-kotlin/src/main/kotlin/social/bigbone/sample/GetNotifications.kt
@@ -3,6 +3,7 @@ package social.bigbone.sample
 import social.bigbone.MastodonClient
 import social.bigbone.api.Pageable
 import social.bigbone.api.entity.Notification
+import social.bigbone.api.entity.NotificationType
 
 object GetNotifications {
 
@@ -34,14 +35,14 @@ object GetNotifications {
         notifications.part.forEach(::println)
     }
 
-    private fun String.explodeToNotificationTypes(): List<Notification.NotificationType>? {
+    private fun String.explodeToNotificationTypes(): List<NotificationType>? {
         return split(",")
             .mapNotNull { it.toNotificationType() }
             .takeIf { it.isNotEmpty() }
     }
 
-    private fun String.toNotificationType(): Notification.NotificationType? {
-        for (notificationType in Notification.NotificationType.entries) {
+    private fun String.toNotificationType(): NotificationType? {
+        for (notificationType in NotificationType.entries) {
             if (notificationType.apiName.equals(this.trim(), ignoreCase = true)) return notificationType
         }
         return null


Test patch:
diff --git a/bigbone/src/test/assets/grouped_notifications_get_accounts_of_notifications_success.json b/bigbone/src/test/assets/grouped_notifications_get_accounts_of_notifications_success.json
new file mode 100644
index 0000000..7c04199
--- /dev/null
+++ b/bigbone/src/test/assets/grouped_notifications_get_accounts_of_notifications_success.json
@@ -0,0 +1,12 @@
+[
+  {
+    "id": "16",
+    "username": "eve",
+    "acct": "eve"
+  },
+  {
+    "id": "3547",
+    "username": "alice",
+    "acct": "alice"
+  }
+]
diff --git a/bigbone/src/test/assets/grouped_notifications_get_all_success.json b/bigbone/src/test/assets/grouped_notifications_get_all_success.json
new file mode 100644
index 0000000..bb10150
--- /dev/null
+++ b/bigbone/src/test/assets/grouped_notifications_get_all_success.json
@@ -0,0 +1,74 @@
+{
+  "accounts": [
+    {
+      "id": "16",
+      "username": "eve",
+      "acct": "eve"
+    },
+    {
+      "id": "3547",
+      "username": "alice",
+      "acct": "alice"
+    },
+    {
+      "id": "31460",
+      "username": "bob",
+      "acct": "bob"
+    },
+    {
+      "id": "36509",
+      "username": "mallory",
+      "acct": "mallory"
+    }
+  ],
+  "statuses": [
+    {
+      "id": "113010503322889311",
+      "created_at": "2024-08-23T08:57:12.057Z",
+      "account": {
+        "id": "55911",
+        "username": "user",
+        "acct": "user"
+      }
+    },
+    {
+      "id": "113006771938929950",
+      "created_at": "2024-08-22T17:08:15.654Z",
+      "account": {
+        "id": "55911",
+        "username": "user",
+        "acct": "user"
+      }
+    }
+  ],
+  "notification_groups": [
+    {
+      "group_key": "favourite-113010503322889311-479000",
+      "notifications_count": 2,
+      "type": "favourite",
+      "most_recent_notification_id": 196014,
+      "page_min_id": "196013",
+      "page_max_id": "196014",
+      "latest_page_notification_at": "2024-08-23T08:59:56.743Z",
+      "sample_account_ids": [
+        "16",
+        "3547"
+      ],
+      "status_id": "113010503322889311"
+    },
+    {
+      "group_key": "favourite-113006771938929950-478999",
+      "notifications_count": 2,
+      "type": "favourite",
+      "most_recent_notification_id": 196012,
+      "page_min_id": "196012",
+      "page_max_id": "196012",
+      "latest_page_notification_at": "2024-08-23T08:16:32.112Z",
+      "sample_account_ids": [
+        "31460",
+        "36509"
+      ],
+      "status_id": "113006771938929950"
+    }
+  ]
+}
diff --git a/bigbone/src/test/assets/grouped_notifications_get_single_success.json b/bigbone/src/test/assets/grouped_notifications_get_single_success.json
new file mode 100644
index 0000000..d5a9751
--- /dev/null
+++ b/bigbone/src/test/assets/grouped_notifications_get_single_success.json
@@ -0,0 +1,38 @@
+{
+  "accounts": [
+    {
+      "id": "16",
+      "username": "eve",
+      "acct": "eve"
+    },
+    {
+      "id": "3547",
+      "username": "alice",
+      "acct": "alice"
+    }
+  ],
+  "statuses": [
+    {
+      "id": "113010503322889311",
+      "created_at": "2024-08-23T08:57:12.057Z",
+      "account": {
+        "id": "55911",
+        "username": "user",
+        "acct": "user"
+      }
+    }
+  ],
+  "notification_groups": [
+    {
+      "group_key": "favourite-113010503322889311-479000",
+      "notifications_count": 2,
+      "type": "favourite",
+      "most_recent_notification_id": 196014,
+      "sample_account_ids": [
+        "16",
+        "3547"
+      ],
+      "status_id": "113010503322889311"
+    }
+  ]
+}
diff --git a/bigbone/src/test/kotlin/social/bigbone/api/method/GroupedNotificationMethodsTest.kt b/bigbone/src/test/kotlin/social/bigbone/api/method/GroupedNotificationMethodsTest.kt
new file mode 100644
index 0000000..8c6ef73
--- /dev/null
+++ b/bigbone/src/test/kotlin/social/bigbone/api/method/GroupedNotificationMethodsTest.kt
@@ -0,0 +1,225 @@
+package social.bigbone.api.method
+
+import io.mockk.slot
+import io.mockk.verify
+import org.amshove.kluent.AnyException
+import org.amshove.kluent.invoking
+import org.amshove.kluent.shouldBeEqualTo
+import org.amshove.kluent.shouldContainAll
+import org.amshove.kluent.shouldHaveSize
+import org.amshove.kluent.shouldNotBeNull
+import org.amshove.kluent.shouldNotThrow
+import org.amshove.kluent.shouldThrow
+import org.amshove.kluent.withMessage
+import org.junit.jupiter.api.Test
+import social.bigbone.MastodonClient.Method
+import social.bigbone.Parameters
+import social.bigbone.PrecisionDateTime.ValidPrecisionDateTime.ExactTime
+import social.bigbone.api.Range
+import social.bigbone.api.entity.NotificationType
+import social.bigbone.testtool.MockClient
+import java.time.Instant
+
+class GroupedNotificationMethodsTest {
+
+    @Test
+    fun testGroupedNotificationResponseParsing() {
+        val client = MockClient.mock("grouped_notifications_get_all_success.json")
+        val groupedNotificationMethods = GroupedNotificationMethods(client)
+
+        val pageable = groupedNotificationMethods.getAllGroupedNotifications(
+            range = Range(limit = 2)
+        ).execute()
+
+        with(pageable.part.first()) {
+            with(accounts) {
+                shouldNotBeNull()
+                shouldHaveSize(4)
+                with(first()) {
+                    id shouldBeEqualTo "16"
+                    username shouldBeEqualTo "eve"
+                    acct shouldBeEqualTo "eve"
+                }
+                with(get(1)) {
+                    id shouldBeEqualTo "3547"
+                    username shouldBeEqualTo "alice"
+                    acct shouldBeEqualTo "alice"
+                }
+            }
+            with(statuses) {
+                shouldNotBeNull()
+                shouldHaveSize(2)
+                with(first()) {
+                    id shouldBeEqualTo "113010503322889311"
+                    createdAt shouldBeEqualTo ExactTime(Instant.parse("2024-08-23T08:57:12.057Z"))
+                    with(account) {
+                        shouldNotBeNull()
+                        id shouldBeEqualTo "55911"
+                        username shouldBeEqualTo "user"
+                        acct shouldBeEqualTo "user"
+                    }
+                }
+            }
+            with(notificationGroups) {
+                shouldHaveSize(2)
+                with(first()) {
+                    groupKey shouldBeEqualTo "favourite-113010503322889311-479000"
+                    notificationsCount shouldBeEqualTo 2
+                    type shouldBeEqualTo NotificationType.FAVOURITE
+                    mostRecentNotificationId shouldBeEqualTo 196_014
+                    pageMinId shouldBeEqualTo "196013"
+                    pageMaxId shouldBeEqualTo "196014"
+                    latestPageNotificationAt shouldBeEqualTo ExactTime(Instant.parse("2024-08-23T08:59:56.743Z"))
+                    with(sampleAccountIds) {
+                        shouldHaveSize(2)
+                        get(0) shouldBeEqualTo "16"
+                        get(1) shouldBeEqualTo "3547"
+                    }
+                    statusId shouldBeEqualTo "113010503322889311"
+                }
+            }
+        }
+    }
+
+    @Test
+    fun `When getting all grouped notifications with all parameters, then call endpoint with correct parameters`() {
+        val client = MockClient.mock("grouped_notifications_get_all_success.json")
+        val groupedNotificationMethods = GroupedNotificationMethods(client)
+
+        val includeTypes = listOf(NotificationType.FOLLOW, NotificationType.MENTION)
+        val excludeTypes = listOf(NotificationType.FAVOURITE)
+        val groupedTypes = listOf(NotificationType.FOLLOW)
+        groupedNotificationMethods.getAllGroupedNotifications(
+            includeTypes = includeTypes,
+            excludeTypes = excludeTypes,
+            accountId = "1234567",
+            expandAccounts = GroupedNotificationMethods.ExpandAccounts.FULL,
+            groupedTypes = groupedTypes,
+            includeFiltered = true,
+            range = Range(
+                minId = "23",
+                maxId = "42",
+                sinceId = "7",
+                limit = 2
+            )
+        ).execute()
+
+        val parametersCapturingSlot = slot<Parameters>()
+        verify {
+            client.get(
+                path = "api/v2/notifications",
+                query = capture(parametersCapturingSlot)
+            )
+        }
+        with(parametersCapturingSlot.captured) {
+            parameters["types[]"]?.shouldContainAll(includeTypes.map(NotificationType::apiName))
+            parameters["exclude_types[]"]?.shouldContainAll(excludeTypes.map(NotificationType::apiName))
+            parameters["grouped_types[]"]?.shouldContainAll(groupedTypes.map(NotificationType::apiName))
+            parameters["account_id"]?.shouldContainAll(listOf("1234567"))
+
+            toQuery() shouldBeEqualTo "max_id=42" +
+                "&min_id=23" +
+                "&since_id=7" +
+                "&limit=2" +
+                "&types[]=follow" +
+                "&types[]=mention" +
+                "&exclude_types[]=favourite" +
+                "&account_id=1234567" +
+                "&expand_accounts=full" +
+                "&grouped_types[]=follow" +
+                "&include_filtered=true"
+        }
+    }
+
+    @Test
+    fun `When getting grouped notifications, then call endpoint with correct parameters`() {
+        val client = MockClient.mock("grouped_notifications_get_single_success.json")
+        val groupedNotificationMethods = GroupedNotificationMethods(client)
+
+        groupedNotificationMethods.getGroupedNotification(groupKey = "favourite-113010503322889311-479000").execute()
+
+        verify {
+            client.get(
+                path = "api/v2/notifications/favourite-113010503322889311-479000"
+            )
+        }
+    }
+
+    @Test
+    fun `Given a client returning success, when dismissing a specific notification, then expect no exceptions and verify correct method was called`() {
+        val client = MockClient.mock(jsonName = "notifications_dismiss_success.json")
+        val groupedNotificationMethods = GroupedNotificationMethods(client)
+
+        invoking {
+            groupedNotificationMethods.dismissNotification("favourite-113010503322889311-479000")
+        } shouldNotThrow AnyException
+
+        verify {
+            client.performAction(
+                endpoint = "api/v2/notifications/favourite-113010503322889311-479000/dismiss",
+                method = Method.POST
+            )
+        }
+    }
+
+    @Test
+    fun `When getting accounts of all notifications in a group, then call endpoint with correct parameters`() {
+        val client = MockClient.mock("grouped_notifications_get_accounts_of_notifications_success.json")
+        val groupedNotificationMethods = GroupedNotificationMethods(client)
+
+        groupedNotificationMethods.getAccountsOfAllNotificationsInGroup(
+            groupKey = "favourite-113010503322889311-479000"
+        ).execute()
+
+        verify {
+            client.get(
+                path = "api/v2/notifications/favourite-113010503322889311-479000/accounts"
+            )
+        }
+    }
+
+    @Test
+    fun `Given a client returning success, when getting number of unread notifications with parameters, then call endpoint with correct parameter`() {
+        val client = MockClient.mockClearText("""{"count": 42}""")
+        val groupedNotificationMethods = GroupedNotificationMethods(client)
+
+        groupedNotificationMethods.getNumberOfUnreadNotifications(
+            limit = 450,
+            types = listOf(NotificationType.MENTION, NotificationType.REBLOG),
+            excludeTypes = emptyList(),
+            accountId = null,
+            groupedTypes = listOf(NotificationType.REBLOG)
+        ).execute()
+
+        val parametersCapturingSlot = slot<Parameters>()
+        verify {
+            client.get(
+                path = "api/v2/notifications/unread_count",
+                query = capture(parametersCapturingSlot)
+            )
+        }
+        with(parametersCapturingSlot.captured) {
+            toQuery() shouldBeEqualTo "limit=450&types[]=mention&types[]=reblog&types[]=reblog"
+        }
+    }
+
+    @Test
+    fun `Given a client returning success, when getting number of unread notifications, then return UnreadNotificationCount`() {
+        val client = MockClient.mockClearText("""{"count": 42}""")
+        val groupedNotificationMethods = GroupedNotificationMethods(client)
+
+        val unreadNotifications = groupedNotificationMethods.getNumberOfUnreadNotifications().execute()
+
+        unreadNotifications.count shouldBeEqualTo 42
+    }
+
+    @Test
+    fun `Given a client returning success, when attempting get number of unread notifications with too high limit, then throw IllegalArgumentException`() {
+        val client = MockClient.mockClearText("""{"count": 42}""")
+        val groupedNotificationMethods = GroupedNotificationMethods(client)
+
+        invoking {
+            groupedNotificationMethods.getNumberOfUnreadNotifications(limit = 9_001).execute()
+        } shouldThrow IllegalArgumentException::class withMessage "Limit must be no larger than 1000 but was 9001"
+    }
+}
diff --git a/bigbone/src/test/kotlin/social/bigbone/api/method/NotificationMethodsTest.kt b/bigbone/src/test/kotlin/social/bigbone/api/method/NotificationMethodsTest.kt
index 24d9f84..2f40eea 100644
--- a/bigbone/src/test/kotlin/social/bigbone/api/method/NotificationMethodsTest.kt
+++ b/bigbone/src/test/kotlin/social/bigbone/api/method/NotificationMethodsTest.kt
@@ -16,6 +16,7 @@ import social.bigbone.MastodonClient
 import social.bigbone.Parameters
 import social.bigbone.PrecisionDateTime.ValidPrecisionDateTime.ExactTime
 import social.bigbone.api.entity.Notification
+import social.bigbone.api.entity.NotificationType
 import social.bigbone.api.exception.BigBoneRequestException
 import social.bigbone.testtool.MockClient
 import java.time.Instant
@@ -67,11 +68,11 @@ class NotificationMethodsTest {
         val notificationMethods = NotificationMethods(client)
 
         val includeTypes = listOf(
-            Notification.NotificationType.FOLLOW,
-            Notification.NotificationType.MENTION
+            NotificationType.FOLLOW,
+            NotificationType.MENTION
         )
         val excludeTypes = listOf(
-            Notification.NotificationType.FAVOURITE
+            NotificationType.FAVOURITE
         )
         notificationMethods.getAllNotifications(
             includeTypes = includeTypes,
@@ -87,8 +88,8 @@ class NotificationMethodsTest {
             )
         }
         with(parametersCapturingSlot.captured) {
-            parameters["types[]"]?.shouldContainAll(includeTypes.map(Notification.NotificationType::apiName))
-            parameters["exclude_types[]"]?.shouldContainAll(excludeTypes.map(Notification.NotificationType::apiName))
+            parameters["types[]"]?.shouldContainAll(includeTypes.map(NotificationType::apiName))
+            parameters["exclude_types[]"]?.shouldContainAll(excludeTypes.map(NotificationType::apiName))
             parameters["account_id"]?.shouldContainAll(listOf("1234567"))
 
             toQuery() shouldBeEqualTo "types[]=follow&types[]=mention&exclude_types[]=favourite&account_id=1234567"