From ee3681468b79ce49e15c10c077f935f6fe559c73 Mon Sep 17 00:00:00 2001 From: usbharu <64310155+usbharu@users.noreply.github.com> Date: Sat, 27 Jan 2024 16:31:06 +0900 Subject: [PATCH] =?UTF-8?q?feat:=20Mastodon=E3=81=AE=E9=80=9A=E7=9F=A5API?= =?UTF-8?q?=E3=81=AENotificationStore=E3=82=92=E8=BF=BD=E5=8A=A0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../domain/model/MastodonNotification.kt | 17 ++ .../model/MastodonNotificationRepository.kt | 7 + .../mastodon/domain/model/NotificationType.kt | 15 ++ .../ExposedMastodonNotificationRepository.kt | 86 ++++++++++ .../MongoMastodonNotificationRepository.kt | 8 + ...goMastodonNotificationRepositoryWrapper.kt | 24 +++ .../notification/MastodonNotificationStore.kt | 65 +++++++ src/main/resources/openapi/mastodon.yaml | 159 ++++++++++++++++++ 8 files changed, 381 insertions(+) create mode 100644 src/main/kotlin/dev/usbharu/hideout/mastodon/domain/model/MastodonNotification.kt create mode 100644 src/main/kotlin/dev/usbharu/hideout/mastodon/domain/model/MastodonNotificationRepository.kt create mode 100644 src/main/kotlin/dev/usbharu/hideout/mastodon/domain/model/NotificationType.kt create mode 100644 src/main/kotlin/dev/usbharu/hideout/mastodon/infrastructure/exposedrepository/ExposedMastodonNotificationRepository.kt create mode 100644 src/main/kotlin/dev/usbharu/hideout/mastodon/infrastructure/mongorepository/MongoMastodonNotificationRepository.kt create mode 100644 src/main/kotlin/dev/usbharu/hideout/mastodon/infrastructure/mongorepository/MongoMastodonNotificationRepositoryWrapper.kt create mode 100644 src/main/kotlin/dev/usbharu/hideout/mastodon/service/notification/MastodonNotificationStore.kt diff --git a/src/main/kotlin/dev/usbharu/hideout/mastodon/domain/model/MastodonNotification.kt b/src/main/kotlin/dev/usbharu/hideout/mastodon/domain/model/MastodonNotification.kt new file mode 100644 index 00000000..fd449a87 --- /dev/null +++ b/src/main/kotlin/dev/usbharu/hideout/mastodon/domain/model/MastodonNotification.kt @@ -0,0 +1,17 @@ +package dev.usbharu.hideout.mastodon.domain.model + +import org.springframework.data.annotation.Id +import org.springframework.data.mongodb.core.mapping.Document +import java.time.Instant + +@Document +data class MastodonNotification( + @Id + val id: Long, + val type: NotificationType, + val createdAt: Instant, + val accountId: Long, + val statusId: Long?, + val reportId: Long?, + val relationshipServeranceEvent: Long? +) diff --git a/src/main/kotlin/dev/usbharu/hideout/mastodon/domain/model/MastodonNotificationRepository.kt b/src/main/kotlin/dev/usbharu/hideout/mastodon/domain/model/MastodonNotificationRepository.kt new file mode 100644 index 00000000..7c43c7a4 --- /dev/null +++ b/src/main/kotlin/dev/usbharu/hideout/mastodon/domain/model/MastodonNotificationRepository.kt @@ -0,0 +1,7 @@ +package dev.usbharu.hideout.mastodon.domain.model + +interface MastodonNotificationRepository { + suspend fun save(mastodonNotification: MastodonNotification): MastodonNotification + suspend fun deleteById(id: Long) + suspend fun findById(id: Long): MastodonNotification? +} diff --git a/src/main/kotlin/dev/usbharu/hideout/mastodon/domain/model/NotificationType.kt b/src/main/kotlin/dev/usbharu/hideout/mastodon/domain/model/NotificationType.kt new file mode 100644 index 00000000..7c78de58 --- /dev/null +++ b/src/main/kotlin/dev/usbharu/hideout/mastodon/domain/model/NotificationType.kt @@ -0,0 +1,15 @@ +package dev.usbharu.hideout.mastodon.domain.model + +enum class NotificationType { + mention, + status, + reblog, + follow, + follow_request, + favourite, + poll, + update, + admin_sign_up, + admin_report, + severed_relationships; +} diff --git a/src/main/kotlin/dev/usbharu/hideout/mastodon/infrastructure/exposedrepository/ExposedMastodonNotificationRepository.kt b/src/main/kotlin/dev/usbharu/hideout/mastodon/infrastructure/exposedrepository/ExposedMastodonNotificationRepository.kt new file mode 100644 index 00000000..1bfc17dc --- /dev/null +++ b/src/main/kotlin/dev/usbharu/hideout/mastodon/infrastructure/exposedrepository/ExposedMastodonNotificationRepository.kt @@ -0,0 +1,86 @@ +package dev.usbharu.hideout.mastodon.infrastructure.exposedrepository + +import dev.usbharu.hideout.core.infrastructure.exposedrepository.AbstractRepository +import dev.usbharu.hideout.mastodon.domain.model.MastodonNotification +import dev.usbharu.hideout.mastodon.domain.model.MastodonNotificationRepository +import dev.usbharu.hideout.mastodon.domain.model.NotificationType +import org.jetbrains.exposed.sql.* +import org.jetbrains.exposed.sql.SqlExpressionBuilder.eq +import org.jetbrains.exposed.sql.javatime.timestamp +import org.slf4j.Logger +import org.slf4j.LoggerFactory +import org.springframework.beans.factory.annotation.Qualifier +import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty +import org.springframework.stereotype.Repository + +@Repository +@Qualifier("jdbc") +@ConditionalOnProperty("hideout.use-mongodb", havingValue = "false", matchIfMissing = true) +class ExposedMastodonNotificationRepository : MastodonNotificationRepository, AbstractRepository() { + override val logger: Logger + get() = Companion.logger + + override suspend fun save(mastodonNotification: MastodonNotification): MastodonNotification = query { + val singleOrNull = + MastodonNotifications.select { MastodonNotifications.id eq mastodonNotification.id }.singleOrNull() + if (singleOrNull == null) { + MastodonNotifications.insert { + it[MastodonNotifications.id] = mastodonNotification.id + it[MastodonNotifications.type] = mastodonNotification.type.name + it[MastodonNotifications.createdAt] = mastodonNotification.createdAt + it[MastodonNotifications.accountId] = mastodonNotification.accountId + it[MastodonNotifications.statusId] = mastodonNotification.statusId + it[MastodonNotifications.reportId] = mastodonNotification.reportId + it[MastodonNotifications.relationshipServeranceEventId] = + mastodonNotification.relationshipServeranceEvent + } + } else { + MastodonNotifications.update({ MastodonNotifications.id eq mastodonNotification.id }) { + it[MastodonNotifications.type] = mastodonNotification.type.name + it[MastodonNotifications.createdAt] = mastodonNotification.createdAt + it[MastodonNotifications.accountId] = mastodonNotification.accountId + it[MastodonNotifications.statusId] = mastodonNotification.statusId + it[MastodonNotifications.reportId] = mastodonNotification.reportId + it[MastodonNotifications.relationshipServeranceEventId] = + mastodonNotification.relationshipServeranceEvent + } + } + mastodonNotification + } + + override suspend fun deleteById(id: Long): Unit = query { + MastodonNotifications.deleteWhere { + MastodonNotifications.id eq id + } + } + + override suspend fun findById(id: Long): MastodonNotification? = query { + MastodonNotifications.select { MastodonNotifications.id eq id }.singleOrNull()?.toMastodonNotification() + } + + companion object { + private val logger = LoggerFactory.getLogger(ExposedMastodonNotificationRepository::class.java) + } +} + +fun ResultRow.toMastodonNotification(): MastodonNotification { + return MastodonNotification( + this[MastodonNotifications.id], + NotificationType.valueOf(this[MastodonNotifications.type]), + this[MastodonNotifications.createdAt], + this[MastodonNotifications.accountId], + this[MastodonNotifications.statusId], + this[MastodonNotifications.reportId], + this[MastodonNotifications.relationshipServeranceEventId], + ) +} + +object MastodonNotifications : Table("mastodon_notifications") { + val id = long("id") + val type = varchar("type", 100) + val createdAt = timestamp("created_at") + val accountId = long("account_id") + val statusId = long("status_id").nullable() + val reportId = long("report_id").nullable() + val relationshipServeranceEventId = long("relationship_serverance_event_id").nullable() +} diff --git a/src/main/kotlin/dev/usbharu/hideout/mastodon/infrastructure/mongorepository/MongoMastodonNotificationRepository.kt b/src/main/kotlin/dev/usbharu/hideout/mastodon/infrastructure/mongorepository/MongoMastodonNotificationRepository.kt new file mode 100644 index 00000000..1cde3f26 --- /dev/null +++ b/src/main/kotlin/dev/usbharu/hideout/mastodon/infrastructure/mongorepository/MongoMastodonNotificationRepository.kt @@ -0,0 +1,8 @@ +package dev.usbharu.hideout.mastodon.infrastructure.mongorepository + +import dev.usbharu.hideout.mastodon.domain.model.MastodonNotification +import org.springframework.data.mongodb.repository.MongoRepository + +interface MongoMastodonNotificationRepository : MongoRepository { + +} diff --git a/src/main/kotlin/dev/usbharu/hideout/mastodon/infrastructure/mongorepository/MongoMastodonNotificationRepositoryWrapper.kt b/src/main/kotlin/dev/usbharu/hideout/mastodon/infrastructure/mongorepository/MongoMastodonNotificationRepositoryWrapper.kt new file mode 100644 index 00000000..84c7bcfd --- /dev/null +++ b/src/main/kotlin/dev/usbharu/hideout/mastodon/infrastructure/mongorepository/MongoMastodonNotificationRepositoryWrapper.kt @@ -0,0 +1,24 @@ +package dev.usbharu.hideout.mastodon.infrastructure.mongorepository + +import dev.usbharu.hideout.mastodon.domain.model.MastodonNotification +import dev.usbharu.hideout.mastodon.domain.model.MastodonNotificationRepository +import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty +import org.springframework.stereotype.Repository +import kotlin.jvm.optionals.getOrNull + +@Repository +@ConditionalOnProperty("hideout.use-mongodb", havingValue = "true", matchIfMissing = false) +class MongoMastodonNotificationRepositoryWrapper(private val mongoMastodonNotificationRepository: MongoMastodonNotificationRepository) : + MastodonNotificationRepository { + override suspend fun save(mastodonNotification: MastodonNotification): MastodonNotification { + return mongoMastodonNotificationRepository.save(mastodonNotification) + } + + override suspend fun deleteById(id: Long) { + mongoMastodonNotificationRepository.deleteById(id) + } + + override suspend fun findById(id: Long): MastodonNotification? { + return mongoMastodonNotificationRepository.findById(id).getOrNull() + } +} diff --git a/src/main/kotlin/dev/usbharu/hideout/mastodon/service/notification/MastodonNotificationStore.kt b/src/main/kotlin/dev/usbharu/hideout/mastodon/service/notification/MastodonNotificationStore.kt new file mode 100644 index 00000000..604daa51 --- /dev/null +++ b/src/main/kotlin/dev/usbharu/hideout/mastodon/service/notification/MastodonNotificationStore.kt @@ -0,0 +1,65 @@ +package dev.usbharu.hideout.mastodon.service.notification + +import dev.usbharu.hideout.core.domain.model.actor.Actor +import dev.usbharu.hideout.core.domain.model.notification.Notification +import dev.usbharu.hideout.core.domain.model.post.Post +import dev.usbharu.hideout.core.domain.model.reaction.Reaction +import dev.usbharu.hideout.core.service.notification.NotificationStore +import dev.usbharu.hideout.mastodon.domain.model.MastodonNotification +import dev.usbharu.hideout.mastodon.domain.model.MastodonNotificationRepository +import dev.usbharu.hideout.mastodon.domain.model.NotificationType +import org.slf4j.LoggerFactory +import org.springframework.stereotype.Component + +@Component +class MastodonNotificationStore(private val mastodonNotificationRepository: MastodonNotificationRepository) : + NotificationStore { + override suspend fun publishNotification( + notification: Notification, + user: Actor, + sourceActor: Actor?, + post: Post?, + reaction: Reaction? + ): Boolean { + val notificationType = when (notification.type) { + "mention" -> NotificationType.mention + "post" -> NotificationType.status + "repost" -> NotificationType.reblog + "follow" -> NotificationType.follow + "follow-request" -> NotificationType.follow_request + "reaction" -> NotificationType.favourite + else -> { + logger.debug("Notification type does not support. type: {}", notification.type) + return false + } + } + + if (notification.sourceActorId == null) { + logger.debug("Notification does not support. notification.sourceActorId is null") + return false + } + + val mastodonNotification = MastodonNotification( + id = notification.id, + type = notificationType, + createdAt = notification.createdAt, + accountId = notification.sourceActorId, + statusId = notification.postId, + reportId = null, + relationshipServeranceEvent = null + ) + + mastodonNotificationRepository.save(mastodonNotification) + + return true + } + + override suspend fun unpulishNotification(notificationId: Long): Boolean { + mastodonNotificationRepository.deleteById(notificationId) + return true + } + + companion object { + private val logger = LoggerFactory.getLogger(MastodonNotificationStore::class.java) + } +} diff --git a/src/main/resources/openapi/mastodon.yaml b/src/main/resources/openapi/mastodon.yaml index 829da25c..c628586e 100644 --- a/src/main/resources/openapi/mastodon.yaml +++ b/src/main/resources/openapi/mastodon.yaml @@ -19,6 +19,8 @@ tags: description: timeline - name: media description: media + - name: notification + description: notification paths: /api/v2/instance: @@ -803,6 +805,122 @@ paths: schema: $ref: "#/components/schemas/Relationship" + /api/v1/notifications: + get: + tags: + - notifications + security: + - OAuth2: + - "read:notifications" + parameters: + - in: query + name: max_id + required: false + schema: + type: string + - in: query + name: since_id + required: false + schema: + type: string + - in: query + name: min_id + required: false + schema: + type: string + - in: query + name: limit + required: false + schema: + type: integer + - in: query + name: types[] + required: false + schema: + type: array + items: + type: string + - in: query + name: exclude_types[] + required: false + schema: + type: array + items: + type: string + - in: query + name: account_id + required: false + schema: + type: array + items: + type: string + responses: + 200: + description: 成功 + content: + application/json: + schema: + type: array + items: + $ref: "#/components/schemas/Notification" + + /api/v1/notifications/{id}: + get: + tags: + - notifications + security: + - OAuth2: + - "read:notifications" + parameters: + - in: path + required: true + name: id + schema: + type: string + responses: + 200: + description: 成功 + content: + application/json: + schema: + $ref: "#/components/schemas/Notification" + + /api/v1/notifications/clear: + post: + tags: + - notifications + security: + - OAuth2: + - "write:notifications" + responses: + 200: + description: 成功 + content: + application/json: + schema: + type: object + + /api/v1/notifications/{id}/dismiss: + post: + tags: + - notifications + security: + - OAuth2: + - "write:notifications" + parameters: + - in: path + name: id + required: true + schema: + type: string + responses: + 200: + description: 成功 + content: + application/json: + schema: + type: object + components: schemas: V1MediaRequest: @@ -1935,6 +2053,47 @@ components: value: type: string + Report: + type: object + + RelationshipServeranceEvent: + type: object + + Notification: + type: object + properties: + id: + type: string + type: + type: string + enum: + - mention + - status + - reblog + - follow + - follow_request + - favourite + - poll + - update + - admin.sign_up + - admin.report + - severed_relationships + created_at: + type: string + account: + $ref: "#/components/schemas/Account" + status: + $ref: "#/components/schemas/Status" + report: + $ref: "#/components/schemas/Report" + relationship_severance_event: + $ref: "#/components/schemas/RelationshipServeranceEvent" + required: + - id + - type + - created_at + - account + securitySchemes: OAuth2: type: oauth2