Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add missing types and parameters to Notifications API #332

Merged
merged 13 commits into from
Nov 14, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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)
PattaFeuFeu marked this conversation as resolved.
Show resolved Hide resolved
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