feat: 通知の仕組みを実装

This commit is contained in:
usbharu 2024-01-26 23:59:59 +09:00
parent ffdf34e934
commit 90da507e41
10 changed files with 192 additions and 13 deletions

View File

@ -27,6 +27,7 @@ class ExposedNotificationRepository(private val idGenerateService: IdGenerateSer
if (singleOrNull == null) {
Notifications.insert {
it[id] = notification.id
it[type] = notification.type
it[userId] = notification.userId
it[sourceActorId] = notification.sourceActorId
it[postId] = notification.postId
@ -36,6 +37,7 @@ class ExposedNotificationRepository(private val idGenerateService: IdGenerateSer
}
} else {
Notifications.update({ Notifications.id eq notification.id }) {
it[type] = notification.type
it[userId] = notification.userId
it[sourceActorId] = notification.sourceActorId
it[postId] = notification.postId
@ -62,6 +64,7 @@ class ExposedNotificationRepository(private val idGenerateService: IdGenerateSer
fun ResultRow.toNotifications() = Notification(
this[Notifications.id],
this[Notifications.type],
this[Notifications.userId],
this[Notifications.sourceActorId],
this[Notifications.postId],
@ -72,6 +75,7 @@ fun ResultRow.toNotifications() = Notification(
object Notifications : Table("notifications") {
val id = long("id")
val type = varchar("type", 100)
val userId = long("user_id").references(Actors.id)
val sourceActorId = long("source_actor_id").references(Actors.id).nullable()
val postId = long("post_id").references(Posts.id).nullable()

View File

@ -4,6 +4,7 @@ import java.time.Instant
data class Notification(
val id: Long,
val type: String,
val userId: Long,
val sourceActorId: Long?,
val postId: Long?,

View File

@ -13,6 +13,7 @@ interface ReactionRepository {
suspend fun deleteByActorId(actorId: Long): Int
suspend fun deleteByPostIdAndActorId(postId: Long, actorId: Long)
suspend fun deleteByPostIdAndActorIdAndEmoji(postId: Long, actorId: Long, emoji: Emoji)
suspend fun findById(id: Long): Reaction?
suspend fun findByPostId(postId: Long): List<Reaction>
suspend fun findByPostIdAndActorIdAndEmojiId(postId: Long, actorId: Long, emojiId: Long): Reaction?
suspend fun existByPostIdAndActorIdAndEmojiId(postId: Long, actorId: Long, emojiId: Long): Boolean

View File

@ -101,6 +101,10 @@ class ReactionRepositoryImpl(
}
}
override suspend fun findById(id: Long): Reaction? = query {
return@query Reactions.select { Reactions.id eq id }.singleOrNull()?.toReaction()
}
override suspend fun findByPostId(postId: Long): List<Reaction> = query {
return@query Reactions.leftJoin(CustomEmojis).select { Reactions.postId eq postId }.map { it.toReaction() }
}

View File

@ -1,6 +1,11 @@
package dev.usbharu.hideout.core.service.notification
sealed class NotificationRequest(open val userId: Long, open val sourceActorId: Long)
import dev.usbharu.hideout.core.domain.model.notification.Notification
import java.time.Instant
sealed class NotificationRequest(open val userId: Long, open val sourceActorId: Long?, val type: String) {
abstract fun buildNotification(id: Long, createdAt: Instant): Notification
}
interface PostId {
val postId: Long
@ -9,28 +14,94 @@ interface PostId {
data class MentionNotificationRequest(
override val userId: Long, override val sourceActorId: Long, override val postId: Long
) : NotificationRequest(
userId, sourceActorId
), PostId
userId, sourceActorId,
"mention"
), PostId {
override fun buildNotification(id: Long, createdAt: Instant): Notification = Notification(
id,
type,
userId,
sourceActorId,
postId,
null,
null,
createdAt
)
}
data class PostNotificationRequest(
override val userId: Long, override val sourceActorId: Long, override val postId: Long
) : NotificationRequest(userId, sourceActorId), PostId
) : NotificationRequest(userId, sourceActorId, "post"), PostId {
override fun buildNotification(id: Long, createdAt: Instant): Notification = Notification(
id,
type,
userId,
sourceActorId,
postId,
null,
null,
createdAt
)
}
data class RepostNotificationRequest(
override val userId: Long, override val sourceActorId: Long, override val postId: Long
) : NotificationRequest(userId, sourceActorId), PostId
) : NotificationRequest(userId, sourceActorId, "repost"), PostId {
override fun buildNotification(id: Long, createdAt: Instant): Notification = Notification(
id,
type,
userId,
sourceActorId,
postId,
null,
null,
createdAt
)
}
data class FollowNotificationRequest(
override val userId: Long, override val sourceActorId: Long, override val postId: Long
) : NotificationRequest(userId, sourceActorId), PostId
override val userId: Long, override val sourceActorId: Long
) : NotificationRequest(userId, sourceActorId, "follow") {
override fun buildNotification(id: Long, createdAt: Instant): Notification = Notification(
id,
type,
userId,
sourceActorId,
null,
null,
null,
createdAt
)
}
data class FollowRequestNotificationRequest(
override val userId: Long, override val sourceActorId: Long, override val postId: Long
) : NotificationRequest(userId, sourceActorId), PostId
override val userId: Long, override val sourceActorId: Long
) : NotificationRequest(userId, sourceActorId, "follow-request") {
override fun buildNotification(id: Long, createdAt: Instant): Notification = Notification(
id,
type,
userId,
sourceActorId,
null,
null,
null,
createdAt
)
}
data class ReactionNotificationRequest(
override val userId: Long, override val sourceActorId: Long, override val postId: Long, val reactionId: Long
) : NotificationRequest(userId, sourceActorId), PostId
) : NotificationRequest(userId, sourceActorId, "reaction"), PostId {
override fun buildNotification(id: Long, createdAt: Instant): Notification = Notification(
id,
type,
userId,
sourceActorId,
postId,
null,
reactionId,
createdAt
)
}

View File

@ -3,6 +3,6 @@ package dev.usbharu.hideout.core.service.notification
import dev.usbharu.hideout.core.domain.model.notification.Notification
interface NotificationService {
suspend fun publishNotify(notificationRequest: NotificationRequest): Notification
suspend fun publishNotify(notificationRequest: NotificationRequest): Notification?
suspend fun unpublishNotify(notificationId: Long)
}

View File

@ -0,0 +1,70 @@
package dev.usbharu.hideout.core.service.notification
import dev.usbharu.hideout.core.domain.model.actor.ActorRepository
import dev.usbharu.hideout.core.domain.model.notification.Notification
import dev.usbharu.hideout.core.domain.model.notification.NotificationRepository
import dev.usbharu.hideout.core.domain.model.post.PostRepository
import dev.usbharu.hideout.core.domain.model.reaction.ReactionRepository
import dev.usbharu.hideout.core.domain.model.relationship.RelationshipRepository
import org.springframework.stereotype.Service
import java.time.Instant
@Service
class NotificationServiceImpl(
private val relationshipNotificationManagementService: RelationshipNotificationManagementService,
private val relationshipRepository: RelationshipRepository,
private val notificationStoreList: List<NotificationStore>,
private val notificationRepository: NotificationRepository,
private val actorRepository: ActorRepository,
private val postRepository: PostRepository,
private val reactionRepository: ReactionRepository
) : NotificationService {
@Suppress("ReplaceNotNullAssertionWithElvisReturn")
override suspend fun publishNotify(notificationRequest: NotificationRequest): Notification? {
// とりあえず個人間のRelationshipに基づいてきめる。今後増やす
if (!relationship(notificationRequest)) {
return null
}
val id = notificationRepository.generateId()
val createdAt = Instant.now()
val notification = notificationRequest.buildNotification(id, createdAt)
val savedNotification = notificationRepository.save(notification)
// saveで参照整合性違反が発生するはずなので
val user = actorRepository.findById(savedNotification.userId)!!
val sourceActor = savedNotification.sourceActorId?.let { actorRepository.findById(it) }
val post = savedNotification.postId?.let { postRepository.findById(it) }
val reaction = savedNotification.reactionId?.let { reactionRepository.findById(it) }
for (it in notificationStoreList) {
it.publishNotification(savedNotification, user, sourceActor, post, reaction)
}
return savedNotification
}
override suspend fun unpublishNotify(notificationId: Long) {
notificationRepository.deleteById(notificationId)
for (notificationStore in notificationStoreList) {
notificationStore.unpulishNotification(notificationId)
}
}
/**
* 個人間のRelationshipに基づいて通知を送信するか判断します
*
* @param notificationRequest
* @return trueの場合送信する
*/
private suspend fun relationship(notificationRequest: NotificationRequest): Boolean {
val targetActorId = notificationRequest.sourceActorId ?: return true
val relationship =
relationshipRepository.findByUserIdAndTargetUserId(notificationRequest.userId, targetActorId) ?: return true
return relationshipNotificationManagementService.sendNotification(relationship, notificationRequest)
}
}

View File

@ -0,0 +1,18 @@
package dev.usbharu.hideout.core.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
interface NotificationStore {
suspend fun publishNotification(
notification: Notification,
user: Actor,
sourceActor: Actor?,
post: Post?,
reaction: Reaction?
): Boolean
suspend fun unpulishNotification(notificationId: Long): Boolean
}

View File

@ -2,6 +2,6 @@ package dev.usbharu.hideout.core.service.notification
import dev.usbharu.hideout.core.domain.model.relationship.Relationship
interface NotificationManagimentService {
interface RelationshipNotificationManagementService {
fun sendNotification(relationship: Relationship, notificationRequest: NotificationRequest): Boolean
}

View File

@ -0,0 +1,10 @@
package dev.usbharu.hideout.core.service.notification
import dev.usbharu.hideout.core.domain.model.relationship.Relationship
import org.springframework.stereotype.Service
@Service
class RelationshipNotificationManagementServiceImpl : RelationshipNotificationManagementService {
override fun sendNotification(relationship: Relationship, notificationRequest: NotificationRequest): Boolean =
relationship.muting.not()
}