mirror of https://github.com/usbharu/Hideout.git
feat: 通知のMastodon互換APIを実装
This commit is contained in:
parent
ee3681468b
commit
31240b8797
|
@ -8,6 +8,7 @@ import java.time.Instant
|
||||||
data class MastodonNotification(
|
data class MastodonNotification(
|
||||||
@Id
|
@Id
|
||||||
val id: Long,
|
val id: Long,
|
||||||
|
val userId: Long,
|
||||||
val type: NotificationType,
|
val type: NotificationType,
|
||||||
val createdAt: Instant,
|
val createdAt: Instant,
|
||||||
val accountId: Long,
|
val accountId: Long,
|
||||||
|
|
|
@ -4,4 +4,16 @@ interface MastodonNotificationRepository {
|
||||||
suspend fun save(mastodonNotification: MastodonNotification): MastodonNotification
|
suspend fun save(mastodonNotification: MastodonNotification): MastodonNotification
|
||||||
suspend fun deleteById(id: Long)
|
suspend fun deleteById(id: Long)
|
||||||
suspend fun findById(id: Long): MastodonNotification?
|
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)
|
||||||
}
|
}
|
||||||
|
|
|
@ -12,4 +12,22 @@ enum class NotificationType {
|
||||||
admin_sign_up,
|
admin_sign_up,
|
||||||
admin_report,
|
admin_report,
|
||||||
severed_relationships;
|
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
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,6 +1,7 @@
|
||||||
package dev.usbharu.hideout.mastodon.infrastructure.exposedrepository
|
package dev.usbharu.hideout.mastodon.infrastructure.exposedrepository
|
||||||
|
|
||||||
import dev.usbharu.hideout.core.infrastructure.exposedrepository.AbstractRepository
|
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.MastodonNotification
|
||||||
import dev.usbharu.hideout.mastodon.domain.model.MastodonNotificationRepository
|
import dev.usbharu.hideout.mastodon.domain.model.MastodonNotificationRepository
|
||||||
import dev.usbharu.hideout.mastodon.domain.model.NotificationType
|
import dev.usbharu.hideout.mastodon.domain.model.NotificationType
|
||||||
|
@ -58,6 +59,48 @@ class ExposedMastodonNotificationRepository : MastodonNotificationRepository, Ab
|
||||||
MastodonNotifications.select { MastodonNotifications.id eq id }.singleOrNull()?.toMastodonNotification()
|
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 {
|
companion object {
|
||||||
private val logger = LoggerFactory.getLogger(ExposedMastodonNotificationRepository::class.java)
|
private val logger = LoggerFactory.getLogger(ExposedMastodonNotificationRepository::class.java)
|
||||||
}
|
}
|
||||||
|
@ -66,6 +109,7 @@ class ExposedMastodonNotificationRepository : MastodonNotificationRepository, Ab
|
||||||
fun ResultRow.toMastodonNotification(): MastodonNotification {
|
fun ResultRow.toMastodonNotification(): MastodonNotification {
|
||||||
return MastodonNotification(
|
return MastodonNotification(
|
||||||
this[MastodonNotifications.id],
|
this[MastodonNotifications.id],
|
||||||
|
this[MastodonNotifications.userId],
|
||||||
NotificationType.valueOf(this[MastodonNotifications.type]),
|
NotificationType.valueOf(this[MastodonNotifications.type]),
|
||||||
this[MastodonNotifications.createdAt],
|
this[MastodonNotifications.createdAt],
|
||||||
this[MastodonNotifications.accountId],
|
this[MastodonNotifications.accountId],
|
||||||
|
@ -77,6 +121,7 @@ fun ResultRow.toMastodonNotification(): MastodonNotification {
|
||||||
|
|
||||||
object MastodonNotifications : Table("mastodon_notifications") {
|
object MastodonNotifications : Table("mastodon_notifications") {
|
||||||
val id = long("id")
|
val id = long("id")
|
||||||
|
val userId = long("user_id")
|
||||||
val type = varchar("type", 100)
|
val type = varchar("type", 100)
|
||||||
val createdAt = timestamp("created_at")
|
val createdAt = timestamp("created_at")
|
||||||
val accountId = long("account_id")
|
val accountId = long("account_id")
|
||||||
|
|
|
@ -5,4 +5,9 @@ import org.springframework.data.mongodb.repository.MongoRepository
|
||||||
|
|
||||||
interface MongoMastodonNotificationRepository : MongoRepository<MastodonNotification, Long> {
|
interface MongoMastodonNotificationRepository : MongoRepository<MastodonNotification, Long> {
|
||||||
|
|
||||||
|
|
||||||
|
fun deleteByUserId(userId: Long): Long
|
||||||
|
|
||||||
|
|
||||||
|
fun deleteByIdAndUserId(id: Long, userId: Long): Long
|
||||||
}
|
}
|
||||||
|
|
|
@ -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.MastodonNotification
|
||||||
import dev.usbharu.hideout.mastodon.domain.model.MastodonNotificationRepository
|
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.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 org.springframework.stereotype.Repository
|
||||||
import kotlin.jvm.optionals.getOrNull
|
import kotlin.jvm.optionals.getOrNull
|
||||||
|
|
||||||
@Repository
|
@Repository
|
||||||
@ConditionalOnProperty("hideout.use-mongodb", havingValue = "true", matchIfMissing = false)
|
@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 {
|
MastodonNotificationRepository {
|
||||||
override suspend fun save(mastodonNotification: MastodonNotification): MastodonNotification {
|
override suspend fun save(mastodonNotification: MastodonNotification): MastodonNotification {
|
||||||
return mongoMastodonNotificationRepository.save(mastodonNotification)
|
return mongoMastodonNotificationRepository.save(mastodonNotification)
|
||||||
|
@ -21,4 +29,43 @@ class MongoMastodonNotificationRepositoryWrapper(private val mongoMastodonNotifi
|
||||||
override suspend fun findById(id: Long): MastodonNotification? {
|
override suspend fun findById(id: Long): MastodonNotification? {
|
||||||
return mongoMastodonNotificationRepository.findById(id).getOrNull()
|
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)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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)
|
||||||
|
}
|
||||||
|
}
|
|
@ -41,6 +41,7 @@ class MastodonNotificationStore(private val mastodonNotificationRepository: Mast
|
||||||
|
|
||||||
val mastodonNotification = MastodonNotification(
|
val mastodonNotification = MastodonNotification(
|
||||||
id = notification.id,
|
id = notification.id,
|
||||||
|
notification.userId,
|
||||||
type = notificationType,
|
type = notificationType,
|
||||||
createdAt = notification.createdAt,
|
createdAt = notification.createdAt,
|
||||||
accountId = notification.sourceActorId,
|
accountId = notification.sourceActorId,
|
||||||
|
|
|
@ -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)
|
||||||
|
}
|
|
@ -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
|
||||||
|
}
|
||||||
|
}
|
Loading…
Reference in New Issue