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 e7531e5d3..4bf073100 100644
--- a/bigbone-rx/src/main/kotlin/social/bigbone/rx/RxNotificationMethods.kt
+++ b/bigbone-rx/src/main/kotlin/social/bigbone/rx/RxNotificationMethods.kt
@@ -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 Mastodon API documentation: methods/notifications/#get
+ */
@JvmOverloads
fun getAllNotifications(
+ includeTypes: List? = null,
excludeTypes: List? = null,
+ accountId: String? = null,
range: Range = Range()
): Single> = Single.fromCallable {
- notificationMethods.getAllNotifications(excludeTypes, range).execute()
+ notificationMethods.getAllNotifications(includeTypes, excludeTypes, accountId, range).execute()
}
fun getNotification(id: String): Single = Single.fromCallable {
diff --git a/bigbone-rx/src/main/kotlin/social/bigbone/rx/admin/RxAdminMeasuresMethods.kt b/bigbone-rx/src/main/kotlin/social/bigbone/rx/admin/RxAdminMeasuresMethods.kt
new file mode 100644
index 000000000..d9b815aab
--- /dev/null
+++ b/bigbone-rx/src/main/kotlin/social/bigbone/rx/admin/RxAdminMeasuresMethods.kt
@@ -0,0 +1,41 @@
+package social.bigbone.rx.admin
+
+import io.reactivex.rxjava3.core.Single
+import social.bigbone.MastodonClient
+import social.bigbone.api.entity.admin.AdminMeasure
+import social.bigbone.api.entity.admin.AdminMeasure.Key
+import social.bigbone.api.method.admin.AdminMeasureMethods
+import social.bigbone.api.method.admin.RequestMeasure
+import java.time.Instant
+
+/**
+ * Reactive implementation of [AdminMeasureMethods].
+ *
+ * Obtain quantitative metrics about the server.
+ * @see Mastodon admin/measures API methods
+ */
+class RxAdminMeasuresMethods(client: MastodonClient) {
+
+ private val adminMeasureMethods = AdminMeasureMethods(client)
+
+ /**
+ * Obtain statistical measures for your server.
+ *
+ * @param measures Request specific measures. Uses helper wrapper [RequestMeasure] to ensure that required fields are set for any given [Key].
+ * @param startAt The start date for the time period. If a time is provided, it will be ignored.
+ * @param endAt The end date for the time period. If a time is provided, it will be ignored.
+ *
+ * @see Mastodon API documentation: admin/measures/#get
+ */
+ fun getMeasurableDate(
+ measures: List,
+ startAt: Instant,
+ endAt: Instant
+ ): Single> = Single.fromCallable {
+ adminMeasureMethods.getMeasurableData(
+ measures = measures,
+ startAt = startAt,
+ endAt = endAt
+ ).execute()
+ }
+}
diff --git a/bigbone/src/main/kotlin/social/bigbone/MastodonClient.kt b/bigbone/src/main/kotlin/social/bigbone/MastodonClient.kt
index 6c25f6c9d..c537d1278 100644
--- a/bigbone/src/main/kotlin/social/bigbone/MastodonClient.kt
+++ b/bigbone/src/main/kotlin/social/bigbone/MastodonClient.kt
@@ -43,6 +43,7 @@ import social.bigbone.api.method.StreamingMethods
import social.bigbone.api.method.SuggestionMethods
import social.bigbone.api.method.TagMethods
import social.bigbone.api.method.TimelineMethods
+import social.bigbone.api.method.admin.AdminMeasureMethods
import social.bigbone.api.method.admin.AdminRetentionMethods
import social.bigbone.extension.emptyRequestBody
import social.bigbone.nodeinfo.NodeInfoClient
@@ -76,6 +77,13 @@ private constructor(
@get:JvmName("accounts")
val accounts: AccountMethods by lazy { AccountMethods(this) }
+ /**
+ * Access API methods under the "admin/measures" endpoint.
+ */
+ @Suppress("unused") // public API
+ @get:JvmName("adminMeasures")
+ val adminMeasures: AdminMeasureMethods by lazy { AdminMeasureMethods(this) }
+
/**
* Access API methods under the "admin/retention" endpoint.
*/
@@ -244,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.
*/
@@ -293,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.
*/
diff --git a/bigbone/src/main/kotlin/social/bigbone/Parameters.kt b/bigbone/src/main/kotlin/social/bigbone/Parameters.kt
index 3bb5a05ff..5e93391cd 100644
--- a/bigbone/src/main/kotlin/social/bigbone/Parameters.kt
+++ b/bigbone/src/main/kotlin/social/bigbone/Parameters.kt
@@ -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>()
+
+ internal val parameters: MutableMap> = mutableMapOf()
/**
* Appends a new key/value pair with a String value.
@@ -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
}
@@ -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
@@ -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()
}
}
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 855afcc64..317a95abc 100644
--- a/bigbone/src/main/kotlin/social/bigbone/api/entity/Notification.kt
+++ b/bigbone/src/main/kotlin/social/bigbone/api/entity/Notification.kt
@@ -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
@@ -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.
@@ -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)
}
}
diff --git a/bigbone/src/main/kotlin/social/bigbone/api/entity/admin/AdminMeasure.kt b/bigbone/src/main/kotlin/social/bigbone/api/entity/admin/AdminMeasure.kt
new file mode 100644
index 000000000..83783c372
--- /dev/null
+++ b/bigbone/src/main/kotlin/social/bigbone/api/entity/admin/AdminMeasure.kt
@@ -0,0 +1,167 @@
+package social.bigbone.api.entity.admin
+
+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
+
+/**
+ * Represents quantitative data about the server.
+ * @see ? = null
+) {
+
+ /**
+ * The unique keystring for the requested measure.
+ */
+ @Serializable
+ enum class Key {
+ /**
+ * Total active users on your instance within the time period.
+ */
+ @SerialName("active_users")
+ ACTIVE_USERS,
+
+ /**
+ * Users who joined your instance within the time period.
+ */
+ @SerialName("new_users")
+ NEW_USERS,
+
+ /**
+ * Total interactions (favourites, boosts, replies) on local statuses within the time period.
+ */
+ @SerialName("interactions")
+ INTERACTIONS,
+
+ /**
+ * Total reports filed within the time period.
+ */
+ @SerialName("opened_reports")
+ OPENED_REPORTS,
+
+ /**
+ * Total reports resolved within the time period.
+ */
+ @SerialName("resolved_reports")
+ RESOLVED_REPORTS,
+
+ /**
+ * Total accounts who used a tag in at least one status within the time period.
+ */
+ @SerialName("tag_accounts")
+ TAG_ACCOUNTS,
+
+ /**
+ * Total statuses which used a tag within the time period.
+ */
+ @SerialName("tag_uses")
+ TAG_USES,
+
+ /**
+ * Total remote origin servers for statuses which used a tag within the time period.
+ */
+ @SerialName("tag_servers")
+ TAG_SERVERS,
+
+ /**
+ * Total accounts originating from a remote domain within the time period.
+ */
+ @SerialName("instance_accounts")
+ INSTANCE_ACCOUNTS,
+
+ /**
+ * Total space used by media attachments from a remote domain within the time period.
+ */
+ @SerialName("instance_media_attachments")
+ INSTANCE_MEDIA_ATTACHMENTS,
+
+ /**
+ * Total reports filed against accounts from a remote domain within the time period.
+ */
+ @SerialName("instance_reports")
+ INSTANCE_REPORTS,
+
+ /**
+ * Total statuses originating from a remote domain within the time period.
+ */
+ @SerialName("instance_statuses")
+ INSTANCE_STATUSES,
+
+ /**
+ * Total accounts from a remote domain followed by a local user within the time period.
+ */
+ @SerialName("instance_follows")
+ INSTANCE_FOLLOWS,
+
+ /**
+ * Total local accounts followed by accounts from a remote domain within the time period.
+ */
+ @SerialName("instance_followers")
+ INSTANCE_FOLLOWERS;
+
+ @OptIn(ExperimentalSerializationApi::class)
+ val apiName: String get() = serializer().descriptor.getElementName(ordinal)
+ }
+
+ /**
+ * The data available for the requested measure, split into daily buckets.
+ */
+ @Serializable
+ data class Data(
+ /**
+ * Midnight on the requested day in the time period.
+ */
+ @SerialName("date")
+ @Serializable(with = DateTimeSerializer::class)
+ val date: PrecisionDateTime = InvalidPrecisionDateTime.Unavailable,
+
+ /**
+ * The numeric value for the requested measure.
+ */
+ @SerialName("value")
+ val value: String? = null
+ )
+}
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 14fdc607f..04cea3dcb 100644
--- a/bigbone/src/main/kotlin/social/bigbone/api/method/NotificationMethods.kt
+++ b/bigbone/src/main/kotlin/social/bigbone/api/method/NotificationMethods.kt
@@ -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 Mastodon API documentation: methods/notifications/#get
*/
@JvmOverloads
fun getAllNotifications(
+ includeTypes: List? = null,
excludeTypes: List? = null,
+ accountId: String? = null,
range: Range = Range()
): MastodonRequest> {
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) }
}
)
}
diff --git a/bigbone/src/main/kotlin/social/bigbone/api/method/SearchMethods.kt b/bigbone/src/main/kotlin/social/bigbone/api/method/SearchMethods.kt
index 4feee9beb..7ef88de07 100644
--- a/bigbone/src/main/kotlin/social/bigbone/api/method/SearchMethods.kt
+++ b/bigbone/src/main/kotlin/social/bigbone/api/method/SearchMethods.kt
@@ -1,5 +1,6 @@
package social.bigbone.api.method
+import kotlinx.serialization.ExperimentalSerializationApi
import kotlinx.serialization.SerialName
import kotlinx.serialization.Serializable
import social.bigbone.MastodonClient
@@ -25,7 +26,10 @@ class SearchMethods(private val client: MastodonClient) {
HASHTAGS,
@SerialName("statuses")
- STATUSES
+ STATUSES;
+
+ @OptIn(ExperimentalSerializationApi::class)
+ val apiName: String get() = serializer().descriptor.getElementName(ordinal)
}
/**
@@ -87,7 +91,7 @@ class SearchMethods(private val client: MastodonClient) {
append("exclude_unreviewed", true)
}
if (type != null) {
- append("type", type.name)
+ append("type", type.apiName)
}
if (!accountId.isNullOrEmpty() && accountId.isNotBlank()) {
append("account_id", accountId)
diff --git a/bigbone/src/main/kotlin/social/bigbone/api/method/admin/AdminMeasureMethods.kt b/bigbone/src/main/kotlin/social/bigbone/api/method/admin/AdminMeasureMethods.kt
new file mode 100644
index 000000000..d56d99aef
--- /dev/null
+++ b/bigbone/src/main/kotlin/social/bigbone/api/method/admin/AdminMeasureMethods.kt
@@ -0,0 +1,226 @@
+package social.bigbone.api.method.admin
+
+import social.bigbone.MastodonClient
+import social.bigbone.MastodonRequest
+import social.bigbone.Parameters
+import social.bigbone.api.entity.admin.AdminMeasure
+import social.bigbone.api.entity.admin.AdminMeasure.Key
+import java.time.Instant
+
+/**
+ * Obtain quantitative metrics about the server.
+ * @see Mastodon admin/measures API methods
+ */
+class AdminMeasureMethods(private val client: MastodonClient) {
+
+ private val adminMeasuresEndpoint = "api/v1/admin/measures"
+
+ /**
+ * Obtain statistical measures for your server.
+ *
+ * @param measures Request specific measures. Uses helper wrapper [RequestMeasure] to ensure that required fields are set for any given [Key].
+ * @param startAt The start date for the time period. If a time is provided, it will be ignored.
+ * @param endAt The end date for the time period. If a time is provided, it will be ignored.
+ *
+ * @see Mastodon API documentation: admin/measures/#get
+ */
+ fun getMeasurableData(
+ measures: List,
+ startAt: Instant,
+ endAt: Instant
+ ): MastodonRequest> {
+ return client.getMastodonRequestForList(
+ endpoint = adminMeasuresEndpoint,
+ method = MastodonClient.Method.POST,
+ parameters = Parameters().apply {
+ measures
+ .flatMap(RequestMeasure::getRequestKeyValues)
+ .forEach { (key, value) -> append(key, value) }
+
+ append("start_at", startAt.toString())
+ append("end_at", endAt.toString())
+ }
+ )
+ }
+}
+
+/**
+ * Wrapper class to ensure that required parameters are added when sending a specific [Key] for which to get measures.
+ */
+sealed class RequestMeasure(val key: Key, val apiName: String = key.apiName) {
+
+ /**
+ * Get the key=value pairs to request this measure.
+ * Can be used to craft a [Parameters] entry from.
+ * Defaults to [KEYS_NAME] = [apiName] for implementations of [RequestMeasure] that do not have additional properties.
+ */
+ open fun getRequestKeyValues(): List> = listOf(
+ KEYS_NAME to apiName
+ )
+
+ companion object {
+ private const val KEYS_NAME = "keys[]"
+ private const val TAG_ID_NAME = "id"
+ private const val REMOTE_DOMAIN_NAME = "domain"
+ }
+
+ /**
+ * Requests [Key.ACTIVE_USERS], i.e. Total active users on your instance within the time period.
+ */
+ data object ActiveUsers : RequestMeasure(key = Key.ACTIVE_USERS)
+
+ /**
+ * Requests [Key.NEW_USERS], i.e. Users who joined your instance within the time period.
+ */
+ data object NewUsers : RequestMeasure(key = Key.NEW_USERS)
+
+ /**
+ * Requests [Key.INTERACTIONS], i.e., Total interactions (favourites, boosts, replies) on local statuses within the time period.
+ */
+ data object Interactions : RequestMeasure(key = Key.INTERACTIONS)
+
+ /**
+ * Requests [Key.OPENED_REPORTS], i.e., Total reports filed within the time period.
+ */
+ data object OpenedReports : RequestMeasure(key = Key.OPENED_REPORTS)
+
+ /**
+ * Requests [Key.RESOLVED_REPORTS], i.e., Total reports resolved within the time period.
+ */
+ data object ResolvedReports : RequestMeasure(key = Key.RESOLVED_REPORTS)
+
+ /**
+ * Requests [Key.TAG_ACCOUNTS], i.e., Total accounts who used a tag in at least one status within the time period.
+ */
+ data class TagAccounts(
+ /**
+ * When [Key.TAG_ACCOUNTS] is one of the requested keys, you must provide a tag ID
+ * to obtain the measure of how many accounts used that hashtag in at least one status
+ * within the given time period.
+ */
+ val tagId: String
+ ) : RequestMeasure(key = Key.TAG_ACCOUNTS) {
+ override fun getRequestKeyValues(): List> =
+ super.getRequestKeyValues() + listOf("$apiName[$TAG_ID_NAME]" to tagId)
+ }
+
+ /**
+ * Requests [Key.TAG_USES], i.e., Total statuses which used a tag within the time period.
+ */
+ data class TagUses(
+ /**
+ * When [Key.TAG_USES] is one of the requested keys, you must provide a tag ID
+ * to obtain the measure of how many statuses used that hashtag
+ * within the given time period.
+ */
+ val tagId: String
+ ) : RequestMeasure(Key.TAG_USES) {
+ override fun getRequestKeyValues(): List> =
+ super.getRequestKeyValues() + listOf("$apiName[$TAG_ID_NAME]" to tagId)
+ }
+
+ /**
+ * Requests [Key.TAG_SERVERS], i.e., Total remote origin servers for statuses which used a tag within the time period.
+ */
+ data class TagServers(
+ /**
+ * When [Key.TAG_SERVERS] is one of the requested keys, you must provide a tag ID
+ * to obtain the measure of how many servers used that hashtag in at least one status
+ * within the given time period.
+ */
+ val tagId: String
+ ) : RequestMeasure(Key.TAG_SERVERS) {
+ override fun getRequestKeyValues(): List> =
+ super.getRequestKeyValues() + listOf("$apiName[$TAG_ID_NAME]" to tagId)
+ }
+
+ /**
+ * Requests [Key.INSTANCE_ACCOUNTS], i.e., Total accounts originating from a remote domain within the time period.
+ */
+ data class InstanceAccounts(
+ /**
+ * When [Key.INSTANCE_ACCOUNTS] is one of the requested keys, you must provide a remote domain
+ * to obtain the measure of how many accounts have been discovered from that server
+ * within the given time period.
+ */
+ val remoteDomain: String
+ ) : RequestMeasure(Key.INSTANCE_ACCOUNTS) {
+ override fun getRequestKeyValues(): List> =
+ super.getRequestKeyValues() + listOf("$apiName[$REMOTE_DOMAIN_NAME]" to remoteDomain)
+ }
+
+ /**
+ * Requests [Key.INSTANCE_MEDIA_ATTACHMENTS], i.e., Total space used by media attachments from a remote domain within the time period.
+ */
+ data class InstanceMediaAttachments(
+ /**
+ * When [Key.INSTANCE_MEDIA_ATTACHMENTS] is one of the requested keys, you must provide a remote domain
+ * to obtain the measure of how much space is used by media attachments from that server
+ * within the given time period.
+ */
+ val remoteDomain: String
+ ) : RequestMeasure(Key.INSTANCE_MEDIA_ATTACHMENTS) {
+ override fun getRequestKeyValues(): List> =
+ super.getRequestKeyValues() + listOf("$apiName[$REMOTE_DOMAIN_NAME]" to remoteDomain)
+ }
+
+ /**
+ * Requests [Key.INSTANCE_REPORTS], i.e., Total reports filed against accounts from a remote domain within the time period.
+ */
+ data class InstanceReports(
+ /**
+ * When [Key.INSTANCE_REPORTS] is one of the requested keys, you must provide a remote domain
+ * to obtain the measure of how many reports have been filed against accounts from that server
+ * within the given time period.
+ */
+ val remoteDomain: String
+ ) : RequestMeasure(Key.INSTANCE_REPORTS) {
+ override fun getRequestKeyValues(): List> =
+ super.getRequestKeyValues() + listOf("$apiName[$REMOTE_DOMAIN_NAME]" to remoteDomain)
+ }
+
+ /**
+ * Requests [Key.INSTANCE_STATUSES], i.e., Total statuses originating from a remote domain within the time period.
+ */
+ data class InstanceStatuses(
+ /**
+ * When [Key.INSTANCE_STATUSES] is one of the requested keys, you must provide a remote domain
+ * to obtain the measure of how many statuses originate from that server
+ * within the given time period.
+ */
+ val remoteDomain: String
+ ) : RequestMeasure(Key.INSTANCE_STATUSES) {
+ override fun getRequestKeyValues(): List> =
+ super.getRequestKeyValues() + listOf("$apiName[$REMOTE_DOMAIN_NAME]" to remoteDomain)
+ }
+
+ /**
+ * Requests [Key.INSTANCE_FOLLOWS], i.e., Total accounts from a remote domain followed by a local user within the time period.
+ */
+ data class InstanceFollows(
+ /**
+ * When [Key.INSTANCE_FOLLOWS] is one of the requested keys, you must provide a remote domain
+ * to obtain the measure of how many follows were performed on accounts from that server by local accounts
+ * within the given time period.
+ */
+ val remoteDomain: String
+ ) : RequestMeasure(Key.INSTANCE_FOLLOWS) {
+ override fun getRequestKeyValues(): List> =
+ super.getRequestKeyValues() + listOf("$apiName[$REMOTE_DOMAIN_NAME]" to remoteDomain)
+ }
+
+ /**
+ * Requests [Key.INSTANCE_FOLLOWERS], i.e., Total local accounts followed by accounts from a remote domain within the time period.
+ */
+ data class InstanceFollowers(
+ /**
+ * When [Key.INSTANCE_STATUSES] is one of the requested keys, you must provide a remote domain
+ * to obtain the measure of how many follows were performed by accounts from that server on local accounts
+ * within the given time period.
+ */
+ val remoteDomain: String
+ ) : RequestMeasure(Key.INSTANCE_FOLLOWERS) {
+ override fun getRequestKeyValues(): List> =
+ super.getRequestKeyValues() + listOf("$apiName[$REMOTE_DOMAIN_NAME]" to remoteDomain)
+ }
+}
diff --git a/bigbone/src/test/assets/admin_measures_get_measurable_data_success.json b/bigbone/src/test/assets/admin_measures_get_measurable_data_success.json
new file mode 100644
index 000000000..fb7b507c4
--- /dev/null
+++ b/bigbone/src/test/assets/admin_measures_get_measurable_data_success.json
@@ -0,0 +1,165 @@
+[
+ {
+ "key": "active_users",
+ "unit": null,
+ "total": "2",
+ "previous_total": "0",
+ "data": [
+ {
+ "date": "2022-09-14T00:00:00Z",
+ "value": "0"
+ }
+ ]
+ },
+ {
+ "key": "new_users",
+ "unit": null,
+ "total": "2",
+ "previous_total": "0",
+ "data": [
+ {
+ "date": "2022-09-14T00:00:00.000+00:00",
+ "value": "0"
+ }
+ ]
+ },
+ {
+ "key": "interactions",
+ "unit": null,
+ "total": "0",
+ "previous_total": "0",
+ "data": [
+ {
+ "date": "2022-09-14T00:00:00Z",
+ "value": "0"
+ }
+ ]
+ },
+ {
+ "key": "opened_reports",
+ "unit": null,
+ "total": "0",
+ "previous_total": "0",
+ "data": [
+ {
+ "date": "2022-09-14T00:00:00.000+00:00",
+ "value": "0"
+ }
+ ]
+ },
+ {
+ "key": "resolved_reports",
+ "unit": null,
+ "total": "0",
+ "previous_total": "0",
+ "data": [
+ {
+ "date": "2022-09-14T00:00:00.000+00:00",
+ "value": "0"
+ }
+ ]
+ },
+ {
+ "key": "tag_accounts",
+ "unit": null,
+ "total": "1",
+ "previous_total": "0",
+ "data": [
+ {
+ "date": "2022-09-14T00:00:00Z",
+ "value": "0"
+ }
+ ]
+ },
+ {
+ "key": "tag_uses",
+ "unit": null,
+ "total": "2",
+ "previous_total": "0",
+ "data": [
+ {
+ "date": "2022-09-14T00:00:00Z",
+ "value": "0"
+ }
+ ]
+ },
+ {
+ "key": "tag_servers",
+ "unit": null,
+ "total": "0",
+ "previous_total": "0",
+ "data": [
+ {
+ "date": "2022-09-14T00:00:00.000+00:00",
+ "value": "0"
+ }
+ ]
+ },
+ {
+ "key": "instance_accounts",
+ "unit": null,
+ "total": "0",
+ "data": [
+ {
+ "date": "2022-09-14T00:00:00.000+00:00",
+ "value": "0"
+ }
+ ]
+ },
+ {
+ "key": "instance_media_attachments",
+ "unit": "bytes",
+ "total": "0",
+ "human_value": "0 Bytes",
+ "data": [
+ {
+ "date": "2022-09-14T00:00:00.000+00:00",
+ "value": ""
+ }
+ ]
+ },
+ {
+ "key": "instance_reports",
+ "unit": null,
+ "total": "0",
+ "data": [
+ {
+ "date": "2022-09-14T00:00:00.000+00:00",
+ "value": "0"
+ }
+ ]
+ },
+ {
+ "key": "instance_statuses",
+ "unit": null,
+ "total": "0",
+ "data": [
+ {
+ "date": "2022-09-14T00:00:00.000+00:00",
+ "value": "0"
+ }
+ ]
+ },
+ {
+ "key": "instance_follows",
+ "unit": null,
+ "total": "0",
+ "data": [
+ {
+ "date": "2022-09-14T00:00:00.000+00:00",
+ "value": "0"
+ }
+ ]
+ },
+ {
+ "key": "instance_followers",
+ "unit": null,
+ "total": "0",
+ "data": [
+ {
+ "date": "2022-09-14T00:00:00.000+00:00",
+ "value": "0"
+ }
+ ]
+ }
+]
diff --git a/bigbone/src/test/assets/error_403_forbidden.json b/bigbone/src/test/assets/error_403_forbidden.json
new file mode 100644
index 000000000..a9264822d
--- /dev/null
+++ b/bigbone/src/test/assets/error_403_forbidden.json
@@ -0,0 +1,3 @@
+{
+ "error": "This action is not allowed"
+}
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 488a44b94..24d9f842f 100644
--- a/bigbone/src/test/kotlin/social/bigbone/api/method/NotificationMethodsTest.kt
+++ b/bigbone/src/test/kotlin/social/bigbone/api/method/NotificationMethodsTest.kt
@@ -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")
@@ -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()
}
@@ -35,12 +55,72 @@ class NotificationMethodsTest {
verify {
client.get(
- path = "/api/v1/notifications",
+ path = "api/v1/notifications",
query = any()
)
}
}
+ @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()
+ 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()
+ 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")
@@ -48,7 +128,8 @@ class NotificationMethodsTest {
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) {
@@ -58,7 +139,7 @@ class NotificationMethodsTest {
verify {
client.get(
- path = "/api/v1/notifications",
+ path = "api/v1/notifications",
query = any()
)
}
@@ -85,7 +166,7 @@ class NotificationMethodsTest {
verify {
client.get(
- path = "/api/v1/notifications/1",
+ path = "api/v1/notifications/1",
query = any()
)
}
@@ -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
)
}
@@ -115,7 +196,7 @@ class NotificationMethodsTest {
verify {
client.performAction(
- endpoint = "/api/v1/notifications/clear",
+ endpoint = "api/v1/notifications/clear",
method = MastodonClient.Method.POST
)
}
diff --git a/bigbone/src/test/kotlin/social/bigbone/api/method/SearchMethodsTest.kt b/bigbone/src/test/kotlin/social/bigbone/api/method/SearchMethodsTest.kt
index 5ce742cd5..e160deb5f 100644
--- a/bigbone/src/test/kotlin/social/bigbone/api/method/SearchMethodsTest.kt
+++ b/bigbone/src/test/kotlin/social/bigbone/api/method/SearchMethodsTest.kt
@@ -1,9 +1,14 @@
package social.bigbone.api.method
+import io.mockk.slot
+import io.mockk.verify
import org.amshove.kluent.`should be equal to`
import org.amshove.kluent.shouldBeEqualTo
+import org.amshove.kluent.shouldContainAll
+import org.amshove.kluent.shouldNotContain
import org.junit.jupiter.api.Assertions
import org.junit.jupiter.api.Test
+import social.bigbone.Parameters
import social.bigbone.api.exception.BigBoneRequestException
import social.bigbone.testtool.MockClient
@@ -45,6 +50,25 @@ class SearchMethodsTest {
result.statuses.all { it.id.toLong() in minId.toLong()..maxId.toLong() }
}
+ @Test
+ fun searchTypeParameterIsProperlyCapitalized() {
+ val client = MockClient.mock("search.json")
+ val searchMethodsMethod = SearchMethods(client)
+
+ searchMethodsMethod.searchContent("query", SearchMethods.SearchType.STATUSES).execute()
+ val parametersCapturingSlot = slot()
+ verify {
+ client.get(
+ path = "api/v2/search",
+ query = capture(parametersCapturingSlot)
+ )
+ }
+ with(parametersCapturingSlot.captured) {
+ parameters["type"]?.shouldContainAll(listOf("statuses"))
+ parameters["type"]?.shouldNotContain(listOf("STATUSES"))
+ }
+ }
+
@Test
fun searchWithException() {
Assertions.assertThrows(BigBoneRequestException::class.java) {
diff --git a/bigbone/src/test/kotlin/social/bigbone/api/method/admin/AdminMeasureMethodsTest.kt b/bigbone/src/test/kotlin/social/bigbone/api/method/admin/AdminMeasureMethodsTest.kt
new file mode 100644
index 000000000..66b3f0781
--- /dev/null
+++ b/bigbone/src/test/kotlin/social/bigbone/api/method/admin/AdminMeasureMethodsTest.kt
@@ -0,0 +1,126 @@
+package social.bigbone.api.method.admin
+
+import io.mockk.slot
+import io.mockk.verify
+import org.amshove.kluent.invoking
+import org.amshove.kluent.shouldBeEqualTo
+import org.amshove.kluent.shouldBeNull
+import org.amshove.kluent.shouldHaveSize
+import org.amshove.kluent.shouldNotBeNull
+import org.amshove.kluent.shouldThrow
+import org.amshove.kluent.withMessage
+import org.junit.jupiter.api.Test
+import social.bigbone.Parameters
+import social.bigbone.PrecisionDateTime.ValidPrecisionDateTime.ExactTime
+import social.bigbone.api.entity.admin.AdminMeasure
+import social.bigbone.api.entity.admin.AdminMeasure.Key
+import social.bigbone.api.exception.BigBoneRequestException
+import social.bigbone.testtool.MockClient
+import java.net.URLEncoder
+import java.time.Instant
+
+class AdminMeasureMethodsTest {
+
+ @Test
+ fun `Given client returning success, when calling getMeasurableDate, then ensure proper deserialisation and correct endpoint and parameter usage`() {
+ val client = MockClient.mock("admin_measures_get_measurable_data_success.json")
+ val adminMeasureMethods = AdminMeasureMethods(client)
+ val measures: List = listOf(
+ RequestMeasure.ActiveUsers,
+ RequestMeasure.NewUsers,
+ RequestMeasure.Interactions,
+ RequestMeasure.OpenedReports,
+ RequestMeasure.ResolvedReports,
+ RequestMeasure.TagAccounts(tagId = "123"),
+ RequestMeasure.TagUses(tagId = "123"),
+ RequestMeasure.TagServers(tagId = "123"),
+ RequestMeasure.InstanceAccounts(remoteDomain = "mastodon.social"),
+ RequestMeasure.InstanceMediaAttachments(remoteDomain = "mastodon.social"),
+ RequestMeasure.InstanceReports(remoteDomain = "mastodon.social"),
+ RequestMeasure.InstanceStatuses(remoteDomain = "mastodon.social"),
+ RequestMeasure.InstanceFollows(remoteDomain = "mastodon.social"),
+ RequestMeasure.InstanceFollowers(remoteDomain = "mastodon.social")
+ )
+ val startAt = Instant.now().minusSeconds(600)
+ val endAt = Instant.now()
+
+ val measurableData: List = adminMeasureMethods.getMeasurableData(
+ measures = measures,
+ startAt = startAt,
+ endAt = endAt
+ ).execute()
+ with(measurableData) {
+ shouldHaveSize(14)
+
+ with(get(0)) {
+ key shouldBeEqualTo Key.ACTIVE_USERS
+ unit.shouldBeNull()
+ total shouldBeEqualTo "2"
+ previousTotal shouldBeEqualTo "0"
+
+ with(data) {
+ shouldNotBeNull()
+ shouldHaveSize(1)
+
+ get(0).date shouldBeEqualTo ExactTime(Instant.parse("2022-09-14T00:00:00Z"))
+ get(0).value shouldBeEqualTo "0"
+ }
+ }
+ }
+
+ val parametersCapturingSlot = slot()
+ verify {
+ client.post(
+ path = "api/v1/admin/measures",
+ body = capture(parametersCapturingSlot),
+ addIdempotencyKey = false
+ )
+ }
+ with(parametersCapturingSlot.captured) {
+ val measuresString: String = "keys[]=active_users&" +
+ "keys[]=new_users&" +
+ "keys[]=interactions&" +
+ "keys[]=opened_reports&" +
+ "keys[]=resolved_reports&" +
+ "keys[]=tag_accounts&" +
+ "keys[]=tag_uses&" +
+ "keys[]=tag_servers&" +
+ "keys[]=instance_accounts&" +
+ "keys[]=instance_media_attachments&" +
+ "keys[]=instance_reports&" +
+ "keys[]=instance_statuses&" +
+ "keys[]=instance_follows&" +
+ "keys[]=instance_followers&" +
+ "tag_accounts[id]=123&" +
+ "tag_uses[id]=123&" +
+ "tag_servers[id]=123&" +
+ "instance_accounts[domain]=mastodon.social&" +
+ "instance_media_attachments[domain]=mastodon.social&" +
+ "instance_reports[domain]=mastodon.social&" +
+ "instance_statuses[domain]=mastodon.social&" +
+ "instance_follows[domain]=mastodon.social&" +
+ "instance_followers[domain]=mastodon.social"
+ val startString = URLEncoder.encode(startAt.toString(), "utf-8")
+ val endString = URLEncoder.encode(endAt.toString(), "utf-8")
+
+ toQuery() shouldBeEqualTo "$measuresString&start_at=$startString&end_at=$endString"
+ }
+ }
+
+ @Test
+ fun `Given a client returning forbidden, when getting measurable data, then propagate error`() {
+ val client = MockClient.failWithResponse(
+ responseJsonAssetPath = "error_403_forbidden.json",
+ responseCode = 403,
+ message = "Forbidden"
+ )
+
+ invoking {
+ AdminMeasureMethods(client).getMeasurableData(
+ measures = listOf(RequestMeasure.ActiveUsers),
+ startAt = Instant.now().minusSeconds(600),
+ endAt = Instant.now()
+ ).execute()
+ } shouldThrow BigBoneRequestException::class withMessage "Forbidden"
+ }
+}
diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml
index 88043fd8e..f6b8cca3a 100644
--- a/gradle/libs.versions.toml
+++ b/gradle/libs.versions.toml
@@ -3,9 +3,9 @@
bigbone = "2.0.0-SNAPSHOT"
# 3rd party dependencies
-junit-jupiter = "5.10.0"
+junit-jupiter = "5.10.1"
junit-platform-launcher = "1.10.1"
-junit-platform-suite-engine = "1.10.0"
+junit-platform-suite-engine = "1.10.1"
kluent = "1.73"
kotlin = "1.9.20"
kotlin-coroutines = "1.7.3"
@@ -17,7 +17,7 @@ rxjava = "3.1.8"
# Gradle plugins
jacoco = "0.8.9"
dependency-analysis = "1.25.0"
-detekt = "1.23.1"
+detekt = "1.23.3"
versions = "0.49.0"
ktlint = "11.6.1"
diff --git a/sample-kotlin/src/main/kotlin/social/bigbone/sample/GetNotifications.kt b/sample-kotlin/src/main/kotlin/social/bigbone/sample/GetNotifications.kt
new file mode 100644
index 000000000..8e1b65d75
--- /dev/null
+++ b/sample-kotlin/src/main/kotlin/social/bigbone/sample/GetNotifications.kt
@@ -0,0 +1,49 @@
+package social.bigbone.sample
+
+import social.bigbone.MastodonClient
+import social.bigbone.api.Pageable
+import social.bigbone.api.entity.Notification
+
+object GetNotifications {
+
+ /**
+ * Get all 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) {
+ 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 notifications
+ val notifications: Pageable = client.notifications.getAllNotifications(
+ includeTypes = includeTypes.explodeToNotificationTypes(),
+ excludeTypes = excludeTypes.explodeToNotificationTypes(),
+ accountId = accountId
+ ).execute()
+
+ notifications.part.forEach(::println)
+ }
+
+ private fun String.explodeToNotificationTypes(): List? {
+ return split(",")
+ .mapNotNull { it.toNotificationType() }
+ .takeIf { it.isNotEmpty() }
+ }
+
+ private fun String.toNotificationType(): Notification.NotificationType? {
+ for (notificationType in Notification.NotificationType.entries) {
+ if (notificationType.apiName.equals(this.trim(), ignoreCase = true)) return notificationType
+ }
+ return null
+ }
+}