feat: 通知のMastodon互換APIを実装

This commit is contained in:
usbharu 2024-01-27 17:52:52 +09:00
parent ee3681468b
commit 31240b8797
10 changed files with 320 additions and 1 deletions

View File

@ -8,6 +8,7 @@ import java.time.Instant
data class MastodonNotification(
@Id
val id: Long,
val userId: Long,
val type: NotificationType,
val createdAt: Instant,
val accountId: Long,

View File

@ -4,4 +4,16 @@ interface MastodonNotificationRepository {
suspend fun save(mastodonNotification: MastodonNotification): MastodonNotification
suspend fun deleteById(id: Long)
suspend fun findById(id: Long): MastodonNotification?
suspend fun findByUserIdAndMaxIdAndMinIdAndSinceIdAndInTypesAndInSourceActorId(
loginUser: Long,
maxId: Long?,
minId: Long?,
sinceId: Long?,
limit: Int,
typesTmp: MutableList<NotificationType>,
accountId: List<Long>
): List<MastodonNotification>
suspend fun deleteByUserId(userId: Long)
suspend fun deleteByUserIdAndId(userId: Long, id: Long)
}

View File

@ -12,4 +12,22 @@ enum class NotificationType {
admin_sign_up,
admin_report,
severed_relationships;
companion object {
fun parse(string: String): NotificationType? = when (string) {
"mention" -> mention
"status" -> status
"reblog" -> reblog
"follow" -> follow
"follow_request" -> follow_request
"favourite" -> favourite
"poll" -> poll
"update" -> update
"admin.aign_up" -> admin_sign_up
"admin.report" -> admin_report
"servered_relationships" -> severed_relationships
else -> null
}
}
}

View File

@ -1,6 +1,7 @@
package dev.usbharu.hideout.mastodon.infrastructure.exposedrepository
import dev.usbharu.hideout.core.infrastructure.exposedrepository.AbstractRepository
import dev.usbharu.hideout.core.infrastructure.exposedrepository.Timelines
import dev.usbharu.hideout.mastodon.domain.model.MastodonNotification
import dev.usbharu.hideout.mastodon.domain.model.MastodonNotificationRepository
import dev.usbharu.hideout.mastodon.domain.model.NotificationType
@ -58,6 +59,48 @@ class ExposedMastodonNotificationRepository : MastodonNotificationRepository, Ab
MastodonNotifications.select { MastodonNotifications.id eq id }.singleOrNull()?.toMastodonNotification()
}
override suspend fun findByUserIdAndMaxIdAndMinIdAndSinceIdAndInTypesAndInSourceActorId(
loginUser: Long,
maxId: Long?,
minId: Long?,
sinceId: Long?,
limit: Int,
typesTmp: MutableList<NotificationType>,
accountId: List<Long>
): List<MastodonNotification> = query {
val query = MastodonNotifications.select {
MastodonNotifications.userId eq loginUser
}
if (maxId != null) {
query.andWhere { MastodonNotifications.id lessEq maxId }
}
if (minId != null) {
query.andWhere { MastodonNotifications.id greaterEq minId }
}
if (sinceId != null) {
query.andWhere { MastodonNotifications.id greaterEq sinceId }
}
val result = query
.limit(limit)
.orderBy(Timelines.createdAt, SortOrder.DESC)
return@query result.map { it.toMastodonNotification() }
}
override suspend fun deleteByUserId(userId: Long) {
MastodonNotifications.deleteWhere {
MastodonNotifications.userId eq userId
}
}
override suspend fun deleteByUserIdAndId(userId: Long, id: Long) {
MastodonNotifications.deleteWhere {
MastodonNotifications.userId eq userId and (MastodonNotifications.id eq id)
}
}
companion object {
private val logger = LoggerFactory.getLogger(ExposedMastodonNotificationRepository::class.java)
}
@ -66,6 +109,7 @@ class ExposedMastodonNotificationRepository : MastodonNotificationRepository, Ab
fun ResultRow.toMastodonNotification(): MastodonNotification {
return MastodonNotification(
this[MastodonNotifications.id],
this[MastodonNotifications.userId],
NotificationType.valueOf(this[MastodonNotifications.type]),
this[MastodonNotifications.createdAt],
this[MastodonNotifications.accountId],
@ -77,6 +121,7 @@ fun ResultRow.toMastodonNotification(): MastodonNotification {
object MastodonNotifications : Table("mastodon_notifications") {
val id = long("id")
val userId = long("user_id")
val type = varchar("type", 100)
val createdAt = timestamp("created_at")
val accountId = long("account_id")

View File

@ -5,4 +5,9 @@ import org.springframework.data.mongodb.repository.MongoRepository
interface MongoMastodonNotificationRepository : MongoRepository<MastodonNotification, Long> {
fun deleteByUserId(userId: Long): Long
fun deleteByIdAndUserId(id: Long, userId: Long): Long
}

View File

@ -2,13 +2,21 @@ package dev.usbharu.hideout.mastodon.infrastructure.mongorepository
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.springframework.boot.autoconfigure.condition.ConditionalOnProperty
import org.springframework.data.domain.Sort
import org.springframework.data.mongodb.core.MongoTemplate
import org.springframework.data.mongodb.core.query.Criteria
import org.springframework.data.mongodb.core.query.Query
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) :
class MongoMastodonNotificationRepositoryWrapper(
private val mongoMastodonNotificationRepository: MongoMastodonNotificationRepository,
private val mongoTemplate: MongoTemplate
) :
MastodonNotificationRepository {
override suspend fun save(mastodonNotification: MastodonNotification): MastodonNotification {
return mongoMastodonNotificationRepository.save(mastodonNotification)
@ -21,4 +29,43 @@ class MongoMastodonNotificationRepositoryWrapper(private val mongoMastodonNotifi
override suspend fun findById(id: Long): MastodonNotification? {
return mongoMastodonNotificationRepository.findById(id).getOrNull()
}
override suspend fun findByUserIdAndMaxIdAndMinIdAndSinceIdAndInTypesAndInSourceActorId(
loginUser: Long,
maxId: Long?,
minId: Long?,
sinceId: Long?,
limit: Int,
typesTmp: MutableList<NotificationType>,
accountId: List<Long>
): List<MastodonNotification> {
val query = Query()
if (maxId != null) {
val criteria = Criteria.where("id").lte(maxId)
query.addCriteria(criteria)
}
if (minId != null) {
val criteria = Criteria.where("id").gte(minId)
query.addCriteria(criteria)
}
if (sinceId != null) {
val criteria = Criteria.where("id").gte(sinceId)
query.addCriteria(criteria)
}
query.limit(limit)
query.with(Sort.by(Sort.Direction.DESC, "createdAt"))
return mongoTemplate.find(query, MastodonNotification::class.java)
}
override suspend fun deleteByUserId(userId: Long) {
mongoMastodonNotificationRepository.deleteByUserId(userId)
}
override suspend fun deleteByUserIdAndId(userId: Long, id: Long) {
mongoMastodonNotificationRepository.deleteByIdAndUserId(id, userId)
}
}

View File

@ -0,0 +1,56 @@
package dev.usbharu.hideout.mastodon.interfaces.api.notification
import dev.usbharu.hideout.controller.mastodon.generated.NotificationsApi
import dev.usbharu.hideout.core.infrastructure.springframework.security.LoginUserContextHolder
import dev.usbharu.hideout.domain.mastodon.model.generated.Notification
import dev.usbharu.hideout.mastodon.domain.model.NotificationType
import dev.usbharu.hideout.mastodon.service.notification.NotificationApiService
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.asFlow
import kotlinx.coroutines.runBlocking
import org.springframework.http.ResponseEntity
import org.springframework.stereotype.Controller
@Controller
class MastodonNotificationApiController(
private val loginUserContextHolder: LoginUserContextHolder,
private val notificationApiService: NotificationApiService
) : NotificationsApi {
override suspend fun apiV1NotificationsClearPost(): ResponseEntity<Any> {
notificationApiService.clearAll(loginUserContextHolder.getLoginUserId())
return ResponseEntity.ok(null)
}
override fun apiV1NotificationsGet(
maxId: String?,
sinceId: String?,
minId: String?,
limit: Int?,
types: List<String>?,
excludeTypes: List<String>?,
accountId: List<String>?
): ResponseEntity<Flow<Notification>> = runBlocking {
val notificationFlow = notificationApiService.notifications(
loginUserContextHolder.getLoginUserId(),
maxId?.toLong(),
minId?.toLong(),
sinceId?.toLong(),
limit ?: 20,
types.orEmpty().mapNotNull { NotificationType.parse(it) },
excludeTypes = excludeTypes.orEmpty().mapNotNull { NotificationType.parse(it) },
accountId = accountId.orEmpty().mapNotNull { it.toLongOrNull() }
).asFlow()
ResponseEntity.ok(notificationFlow)
}
override suspend fun apiV1NotificationsIdDismissPost(id: String): ResponseEntity<Any> {
notificationApiService.dismiss(loginUserContextHolder.getLoginUserId(), id.toLong())
return ResponseEntity.ok(null)
}
override suspend fun apiV1NotificationsIdGet(id: String): ResponseEntity<Notification> {
val notification = notificationApiService.fingById(loginUserContextHolder.getLoginUserId(), id.toLong())
return ResponseEntity.ok(notification)
}
}

View File

@ -41,6 +41,7 @@ class MastodonNotificationStore(private val mastodonNotificationRepository: Mast
val mastodonNotification = MastodonNotification(
id = notification.id,
notification.userId,
type = notificationType,
createdAt = notification.createdAt,
accountId = notification.sourceActorId,

View File

@ -0,0 +1,23 @@
package dev.usbharu.hideout.mastodon.service.notification
import dev.usbharu.hideout.domain.mastodon.model.generated.Notification
import dev.usbharu.hideout.mastodon.domain.model.NotificationType
interface NotificationApiService {
suspend fun notifications(
loginUser: Long,
maxId: Long?,
minId: Long?,
sinceId: Long?,
limit: Int,
types: List<NotificationType>,
excludeTypes: List<NotificationType>,
accountId: List<Long>
): List<Notification>
suspend fun fingById(loginUser: Long, notificationId: Long): Notification?
suspend fun clearAll(loginUser: Long)
suspend fun dismiss(loginUser: Long, notificationId: Long)
}

View File

@ -0,0 +1,111 @@
package dev.usbharu.hideout.mastodon.service.notification
import dev.usbharu.hideout.application.external.Transaction
import dev.usbharu.hideout.domain.mastodon.model.generated.Notification
import dev.usbharu.hideout.mastodon.domain.model.MastodonNotificationRepository
import dev.usbharu.hideout.mastodon.domain.model.NotificationType
import dev.usbharu.hideout.mastodon.domain.model.NotificationType.*
import dev.usbharu.hideout.mastodon.query.StatusQueryService
import dev.usbharu.hideout.mastodon.service.account.AccountService
import org.springframework.stereotype.Service
@Service
class NotificationApiServiceImpl(
private val mastodonNotificationRepository: MastodonNotificationRepository,
private val transaction: Transaction,
private val accountService: AccountService,
private val statusQueryService: StatusQueryService
) :
NotificationApiService {
override suspend fun notifications(
loginUser: Long,
maxId: Long?,
minId: Long?,
sinceId: Long?,
limit: Int,
types: List<NotificationType>,
excludeTypes: List<NotificationType>,
accountId: List<Long>
): List<Notification> = transaction.transaction {
val typesTmp = mutableListOf<NotificationType>()
typesTmp.addAll(types)
typesTmp.removeAll(excludeTypes)
val mastodonNotifications =
mastodonNotificationRepository.findByUserIdAndMaxIdAndMinIdAndSinceIdAndInTypesAndInSourceActorId(
loginUser,
maxId,
minId,
sinceId,
limit,
typesTmp,
accountId
)
val accounts = accountService.findByIds(mastodonNotifications.map {
it.accountId
}).associateBy { it.id.toLong() }
val statuses = statusQueryService.findByPostIds(mastodonNotifications.mapNotNull { it.statusId })
.associateBy { it.id.toLong() }
mastodonNotifications.map {
Notification(
id = it.id.toString(),
type = convertNotificationType(it.type),
createdAt = it.createdAt.toString(),
account = accounts.getValue(it.accountId),
status = statuses[it.statusId],
report = null,
relationshipSeveranceEvent = null
)
}
}
override suspend fun fingById(loginUser: Long, notificationId: Long): Notification? {
val findById = mastodonNotificationRepository.findById(notificationId) ?: return null
if (findById.userId != loginUser) {
return null
}
val account = accountService.findById(findById.accountId)
val status = findById.statusId?.let { statusQueryService.findByPostId(it) }
return Notification(
id = findById.id.toString(),
type = convertNotificationType(findById.type),
createdAt = findById.createdAt.toString(),
account = account,
status = status,
report = null,
relationshipSeveranceEvent = null
)
}
override suspend fun clearAll(loginUser: Long) {
mastodonNotificationRepository.deleteByUserId(loginUser)
}
override suspend fun dismiss(loginUser: Long, notificationId: Long) {
mastodonNotificationRepository.deleteByUserIdAndId(loginUser, notificationId)
}
private fun convertNotificationType(notificationType: NotificationType): Notification.Type =
when (notificationType) {
mention -> Notification.Type.mention
status -> Notification.Type.status
reblog -> Notification.Type.reblog
follow -> Notification.Type.follow
follow_request -> Notification.Type.follow
favourite -> Notification.Type.followRequest
poll -> Notification.Type.poll
update -> Notification.Type.update
admin_sign_up -> Notification.Type.adminPeriodSignUp
admin_report -> Notification.Type.adminPeriodReport
severed_relationships -> Notification.Type.severedRelationships
}
}