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 index fd449a87..baba08e8 100644 --- a/src/main/kotlin/dev/usbharu/hideout/mastodon/domain/model/MastodonNotification.kt +++ b/src/main/kotlin/dev/usbharu/hideout/mastodon/domain/model/MastodonNotification.kt @@ -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, 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 index 7c43c7a4..42d769da 100644 --- a/src/main/kotlin/dev/usbharu/hideout/mastodon/domain/model/MastodonNotificationRepository.kt +++ b/src/main/kotlin/dev/usbharu/hideout/mastodon/domain/model/MastodonNotificationRepository.kt @@ -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, + accountId: List + ): List + + suspend fun deleteByUserId(userId: Long) + suspend fun deleteByUserIdAndId(userId: Long, id: Long) } 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 index 7c78de58..fd7caf90 100644 --- a/src/main/kotlin/dev/usbharu/hideout/mastodon/domain/model/NotificationType.kt +++ b/src/main/kotlin/dev/usbharu/hideout/mastodon/domain/model/NotificationType.kt @@ -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 + } + } } 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 index 1bfc17dc..e42bbc9f 100644 --- a/src/main/kotlin/dev/usbharu/hideout/mastodon/infrastructure/exposedrepository/ExposedMastodonNotificationRepository.kt +++ b/src/main/kotlin/dev/usbharu/hideout/mastodon/infrastructure/exposedrepository/ExposedMastodonNotificationRepository.kt @@ -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, + accountId: List + ): List = 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") 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 index 1cde3f26..141a37b2 100644 --- a/src/main/kotlin/dev/usbharu/hideout/mastodon/infrastructure/mongorepository/MongoMastodonNotificationRepository.kt +++ b/src/main/kotlin/dev/usbharu/hideout/mastodon/infrastructure/mongorepository/MongoMastodonNotificationRepository.kt @@ -5,4 +5,9 @@ import org.springframework.data.mongodb.repository.MongoRepository interface MongoMastodonNotificationRepository : MongoRepository { + + fun deleteByUserId(userId: Long): Long + + + fun deleteByIdAndUserId(id: Long, userId: Long): Long } 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 index 84c7bcfd..1016279f 100644 --- a/src/main/kotlin/dev/usbharu/hideout/mastodon/infrastructure/mongorepository/MongoMastodonNotificationRepositoryWrapper.kt +++ b/src/main/kotlin/dev/usbharu/hideout/mastodon/infrastructure/mongorepository/MongoMastodonNotificationRepositoryWrapper.kt @@ -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, + accountId: List + ): List { + 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) + } } diff --git a/src/main/kotlin/dev/usbharu/hideout/mastodon/interfaces/api/notification/MastodonNotificationApiController.kt b/src/main/kotlin/dev/usbharu/hideout/mastodon/interfaces/api/notification/MastodonNotificationApiController.kt new file mode 100644 index 00000000..c8ea7e78 --- /dev/null +++ b/src/main/kotlin/dev/usbharu/hideout/mastodon/interfaces/api/notification/MastodonNotificationApiController.kt @@ -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 { + notificationApiService.clearAll(loginUserContextHolder.getLoginUserId()) + return ResponseEntity.ok(null) + } + + override fun apiV1NotificationsGet( + maxId: String?, + sinceId: String?, + minId: String?, + limit: Int?, + types: List?, + excludeTypes: List?, + accountId: List? + ): ResponseEntity> = 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 { + notificationApiService.dismiss(loginUserContextHolder.getLoginUserId(), id.toLong()) + return ResponseEntity.ok(null) + } + + override suspend fun apiV1NotificationsIdGet(id: String): ResponseEntity { + val notification = notificationApiService.fingById(loginUserContextHolder.getLoginUserId(), id.toLong()) + + return ResponseEntity.ok(notification) + } +} 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 index 604daa51..80e66e14 100644 --- a/src/main/kotlin/dev/usbharu/hideout/mastodon/service/notification/MastodonNotificationStore.kt +++ b/src/main/kotlin/dev/usbharu/hideout/mastodon/service/notification/MastodonNotificationStore.kt @@ -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, diff --git a/src/main/kotlin/dev/usbharu/hideout/mastodon/service/notification/NotificationApiService.kt b/src/main/kotlin/dev/usbharu/hideout/mastodon/service/notification/NotificationApiService.kt new file mode 100644 index 00000000..152b6bab --- /dev/null +++ b/src/main/kotlin/dev/usbharu/hideout/mastodon/service/notification/NotificationApiService.kt @@ -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, + excludeTypes: List, + accountId: List + ): List + + suspend fun fingById(loginUser: Long, notificationId: Long): Notification? + + suspend fun clearAll(loginUser: Long) + + suspend fun dismiss(loginUser: Long, notificationId: Long) +} diff --git a/src/main/kotlin/dev/usbharu/hideout/mastodon/service/notification/NotificationApiServiceImpl.kt b/src/main/kotlin/dev/usbharu/hideout/mastodon/service/notification/NotificationApiServiceImpl.kt new file mode 100644 index 00000000..709b306e --- /dev/null +++ b/src/main/kotlin/dev/usbharu/hideout/mastodon/service/notification/NotificationApiServiceImpl.kt @@ -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, + excludeTypes: List, + accountId: List + ): List = transaction.transaction { + + val typesTmp = mutableListOf() + + 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 + } +}