Skip to content

Commit

Permalink
Add missing types and parameters to Notifications API (#332)
Browse files Browse the repository at this point in the history
* Add includeTypes property for types endpoint parameter

* Improve testability of Parameters class

* Add missing NotificationTypes, default to null if not known or available

* Add parameters tests for include types and exclude types

* Define endpoint without leading slash

* Add account_id parameter

* DRY: Get the api name via the SerialName annotation's value
  • Loading branch information
PattaFeuFeu authored Nov 14, 2023
1 parent 2f3fcea commit c89ec6d
Show file tree
Hide file tree
Showing 8 changed files with 228 additions and 54 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -17,12 +17,22 @@ class RxNotificationMethods(client: MastodonClient) {

private val notificationMethods = NotificationMethods(client)

/**
* Notifications concerning the user.
* @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 range optional Range for the pageable return value.
* @see <a href="https://docs.joinmastodon.org/methods/notifications/#get">Mastodon API documentation: methods/notifications/#get</a>
*/
@JvmOverloads
fun getAllNotifications(
includeTypes: List<Notification.NotificationType>? = null,
excludeTypes: List<Notification.NotificationType>? = null,
accountId: String? = null,
range: Range = Range()
): Single<Pageable<Notification>> = Single.fromCallable {
notificationMethods.getAllNotifications(excludeTypes, range).execute()
notificationMethods.getAllNotifications(includeTypes, excludeTypes, accountId, range).execute()
}

fun getNotification(id: String): Single<Notification> = Single.fromCallable {
Expand Down
14 changes: 7 additions & 7 deletions bigbone/src/main/kotlin/social/bigbone/MastodonClient.kt
Original file line number Diff line number Diff line change
Expand Up @@ -252,6 +252,13 @@ private constructor(
@get:JvmName("preferences")
val preferences: PreferenceMethods by lazy { PreferenceMethods(this) }

/**
* Access API methods under "push" endpoint.
*/
@Suppress("unused") // public API
@get:JvmName("pushNotifications")
val pushNotifications: PushNotificationMethods by lazy { PushNotificationMethods(this) }

/**
* Access API methods under the "reports" endpoint.
*/
Expand Down Expand Up @@ -301,13 +308,6 @@ 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.
*/
Expand Down
38 changes: 25 additions & 13 deletions bigbone/src/main/kotlin/social/bigbone/Parameters.kt
Original file line number Diff line number Diff line change
@@ -1,15 +1,18 @@
package social.bigbone

import java.net.URLEncoder
import java.util.ArrayList
import java.util.UUID

typealias Key = String
typealias Value = String

/**
* Parameters holds a list of String key/value pairs that can be used as query part of a URL, or in the body of a request.
* Parameters holds a mapping of [Key] to [Value]s that can be used as query part of a URL, or in the body of a request.
* Add new pairs using one of the available append() functions.
*/
class Parameters {
private val parameterList = ArrayList<Pair<String, String>>()

internal val parameters: MutableMap<Key, MutableList<Value>> = mutableMapOf()

/**
* Appends a new key/value pair with a String value.
Expand All @@ -18,7 +21,7 @@ class Parameters {
* @return this Parameters instance
*/
fun append(key: String, value: String): Parameters {
parameterList.add(Pair(key, value))
parameters.getOrPut(key, ::mutableListOf).add(value)
return this
}

Expand Down Expand Up @@ -77,10 +80,15 @@ class Parameters {
* Converts this Parameters instance into a query string.
* @return String, formatted like: "key1=value1&key2=value2&..."
*/
fun toQuery(): String =
parameterList.joinToString(separator = "&") {
"${it.first}=${URLEncoder.encode(it.second, "utf-8")}"
}
fun toQuery(): String {
return parameters
.map { (key, values) ->
values.joinToString(separator = "&") { value ->
"$key=${URLEncoder.encode(value, "utf-8")}"
}
}
.joinToString(separator = "&")
}

/**
* Generates a UUID string for this parameter list. UUIDs returned for different Parameters instances should be
Expand All @@ -89,10 +97,14 @@ class Parameters {
* @return Type 3 UUID as a String.
*/
fun uuid(): String {
val parameterString = parameterList
.sortedWith(compareBy { it.first })
.joinToString { "${it.first}${it.second}" }
val uuid = UUID.nameUUIDFromBytes(parameterString.toByteArray())
return uuid.toString()
return UUID
.nameUUIDFromBytes(
parameters
.entries
.sortedWith(compareBy { (key, _) -> key })
.joinToString { (key, value) -> "$key$value" }
.toByteArray()
)
.toString()
}
}
33 changes: 28 additions & 5 deletions bigbone/src/main/kotlin/social/bigbone/api/entity/Notification.kt
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
package social.bigbone.api.entity

import kotlinx.serialization.ExperimentalSerializationApi
import kotlinx.serialization.SerialName
import kotlinx.serialization.Serializable
import social.bigbone.DateTimeSerializer
Expand All @@ -22,7 +23,7 @@ data class Notification(
* The type of event that resulted in the notification.
*/
@SerialName("type")
val type: NotificationType = NotificationType.MENTION,
val type: NotificationType? = null,

/**
* The timestamp of the notification.
Expand Down Expand Up @@ -54,16 +55,38 @@ data class Notification(
*/
@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("poll")
POLL,

@SerialName("reblog")
REBLOG,

@SerialName("favourite")
FAVOURITE,
@SerialName("status")
STATUS,

@SerialName("follow")
FOLLOW
@SerialName("update")
UPDATE;

@OptIn(ExperimentalSerializationApi::class)
val apiName: String get() = serializer().descriptor.getElementName(ordinal)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -13,26 +13,34 @@ import social.bigbone.api.exception.BigBoneRequestException
*/
class NotificationMethods(private val client: MastodonClient) {

private val notificationsEndpoint = "/api/v1/notifications"
private val notificationsEndpoint = "api/v1/notifications"

/**
* Notifications concerning the user.
* @param excludeTypes Types to exclude from the results. See Mastodon API documentation for details.
* @param range optional Range for the pageable return value
* @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 range optional Range for the pageable return value.
* @see <a href="https://docs.joinmastodon.org/methods/notifications/#get">Mastodon API documentation: methods/notifications/#get</a>
*/
@JvmOverloads
fun getAllNotifications(
includeTypes: List<Notification.NotificationType>? = null,
excludeTypes: List<Notification.NotificationType>? = null,
accountId: String? = null,
range: Range = Range()
): MastodonRequest<Pageable<Notification>> {
return client.getPageableMastodonRequest(
endpoint = notificationsEndpoint,
method = MastodonClient.Method.GET,
parameters = range.toParameters().apply {
includeTypes?.let {
append("types", includeTypes.map(Notification.NotificationType::apiName))
}
excludeTypes?.let {
append("exclude_types", excludeTypes.map { it.name.lowercase() })
append("exclude_types", excludeTypes.map(Notification.NotificationType::apiName))
}
accountId?.let { append("account_id", accountId) }
}
)
}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,21 +1,40 @@
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.shouldBeNull
import org.amshove.kluent.shouldContainAll
import org.amshove.kluent.shouldNotBeNull
import org.amshove.kluent.shouldNotThrow
import org.amshove.kluent.shouldThrow
import org.junit.jupiter.api.Test
import social.bigbone.JSON_SERIALIZER
import social.bigbone.MastodonClient
import social.bigbone.Parameters
import social.bigbone.PrecisionDateTime.ValidPrecisionDateTime.ExactTime
import social.bigbone.api.entity.Notification
import social.bigbone.api.exception.BigBoneRequestException
import social.bigbone.testtool.MockClient
import java.time.Instant

class NotificationMethodsTest {

@Test
fun `Given a JSON response with invalid status, when deserialising, then default to null`() {
val json = """
{
"type": "new_unknown_type"
}
""".trimIndent()

val notification: Notification = JSON_SERIALIZER.decodeFromString(json)

notification.type.shouldBeNull()
}

@Test
fun getMentionNotification() {
val client = MockClient.mock("notifications.json")
Expand All @@ -24,7 +43,8 @@ class NotificationMethodsTest {
val pageable = notificationMethods.getAllNotifications().execute()

with(pageable.part.first()) {
type.name.lowercase() shouldBeEqualTo "mention"
type.shouldNotBeNull()
type!!.name.lowercase() shouldBeEqualTo "mention"
createdAt shouldBeEqualTo ExactTime(Instant.parse("2019-11-23T07:49:02.064Z"))
account.shouldNotBeNull()
}
Expand All @@ -35,20 +55,81 @@ class NotificationMethodsTest {

verify {
client.get(
path = "/api/v1/notifications",
path = "api/v1/notifications",
query = any<Parameters>()
)
}
}

@Test
fun `When getting all notifications with valid includeTypes, excludeTypes, and accountId, then call endpoint with correct parameters`() {
val client = MockClient.mock("notifications.json")
val notificationMethods = NotificationMethods(client)

val includeTypes = listOf(
Notification.NotificationType.FOLLOW,
Notification.NotificationType.MENTION
)
val excludeTypes = listOf(
Notification.NotificationType.FAVOURITE
)
notificationMethods.getAllNotifications(
includeTypes = includeTypes,
excludeTypes = excludeTypes,
accountId = "1234567"
).execute()

val parametersCapturingSlot = slot<Parameters>()
verify {
client.get(
path = "api/v1/notifications",
query = capture(parametersCapturingSlot)
)
}
with(parametersCapturingSlot.captured) {
parameters["types[]"]?.shouldContainAll(includeTypes.map(Notification.NotificationType::apiName))
parameters["exclude_types[]"]?.shouldContainAll(excludeTypes.map(Notification.NotificationType::apiName))
parameters["account_id"]?.shouldContainAll(listOf("1234567"))

toQuery() shouldBeEqualTo "types[]=follow&types[]=mention&exclude_types[]=favourite&account_id=1234567"
}
}

@Test
fun `When getting all notifications with empty includeTypes and includeTypes, then call endpoint without types`() {
val client = MockClient.mock("notifications.json")
val notificationMethods = NotificationMethods(client)

notificationMethods.getAllNotifications(
includeTypes = emptyList(),
excludeTypes = emptyList()
).execute()

val parametersCapturingSlot = slot<Parameters>()
verify {
client.get(
path = "api/v1/notifications",
query = capture(parametersCapturingSlot)
)
}
with(parametersCapturingSlot.captured) {
parameters["types[]"].shouldBeNull()
parameters["exclude_types[]"].shouldBeNull()
parameters["account_id"].shouldBeNull()

toQuery() shouldBeEqualTo ""
}
}

@Test
fun getFavouriteNotification() {
val client = MockClient.mock("notifications.json")
val notificationMethods = NotificationMethods(client)
val pageable = notificationMethods.getAllNotifications().execute()

with(pageable.part[1]) {
type.name.lowercase() shouldBeEqualTo "favourite"
type.shouldNotBeNull()
type!!.name.lowercase() shouldBeEqualTo "favourite"
createdAt shouldBeEqualTo ExactTime(Instant.parse("2019-11-23T07:29:18.903Z"))
}
with(pageable.part[1].status) {
Expand All @@ -58,7 +139,7 @@ class NotificationMethodsTest {

verify {
client.get(
path = "/api/v1/notifications",
path = "api/v1/notifications",
query = any<Parameters>()
)
}
Expand All @@ -85,7 +166,7 @@ class NotificationMethodsTest {

verify {
client.get(
path = "/api/v1/notifications/1",
path = "api/v1/notifications/1",
query = any<Parameters>()
)
}
Expand All @@ -100,7 +181,7 @@ class NotificationMethodsTest {

verify {
client.performAction(
endpoint = "/api/v1/notifications/1/dismiss",
endpoint = "api/v1/notifications/1/dismiss",
method = MastodonClient.Method.POST
)
}
Expand All @@ -115,7 +196,7 @@ class NotificationMethodsTest {

verify {
client.performAction(
endpoint = "/api/v1/notifications/clear",
endpoint = "api/v1/notifications/clear",
method = MastodonClient.Method.POST
)
}
Expand Down
Loading

0 comments on commit c89ec6d

Please sign in to comment.