mirror of https://github.com/usbharu/Hideout.git
Merge pull request #259 from usbharu/feature/notification
Feature/notification
This commit is contained in:
commit
fe3082db59
|
@ -272,7 +272,9 @@ class SecurityConfig {
|
||||||
fun jwtTokenCustomizer(): OAuth2TokenCustomizer<JwtEncodingContext> {
|
fun jwtTokenCustomizer(): OAuth2TokenCustomizer<JwtEncodingContext> {
|
||||||
return OAuth2TokenCustomizer { context: JwtEncodingContext ->
|
return OAuth2TokenCustomizer { context: JwtEncodingContext ->
|
||||||
|
|
||||||
if (OAuth2TokenType.ACCESS_TOKEN == context.tokenType && context.authorization?.authorizationGrantType == AuthorizationGrantType.AUTHORIZATION_CODE) {
|
if (OAuth2TokenType.ACCESS_TOKEN == context.tokenType &&
|
||||||
|
context.authorization?.authorizationGrantType == AuthorizationGrantType.AUTHORIZATION_CODE
|
||||||
|
) {
|
||||||
val userDetailsImpl = context.getPrincipal<Authentication>().principal as UserDetailsImpl
|
val userDetailsImpl = context.getPrincipal<Authentication>().principal as UserDetailsImpl
|
||||||
context.claims.claim("uid", userDetailsImpl.id.toString())
|
context.claims.claim("uid", userDetailsImpl.id.toString())
|
||||||
}
|
}
|
||||||
|
|
|
@ -2,6 +2,7 @@ package dev.usbharu.hideout.core.domain.exception.media
|
||||||
|
|
||||||
import java.io.Serial
|
import java.io.Serial
|
||||||
|
|
||||||
|
@Suppress("UnnecessaryAbstractClass")
|
||||||
abstract class MediaException : RuntimeException {
|
abstract class MediaException : RuntimeException {
|
||||||
constructor() : super()
|
constructor() : super()
|
||||||
constructor(message: String?) : super(message)
|
constructor(message: String?) : super(message)
|
||||||
|
|
|
@ -0,0 +1,87 @@
|
||||||
|
package dev.usbharu.hideout.core.domain.model.notification
|
||||||
|
|
||||||
|
import dev.usbharu.hideout.application.service.id.IdGenerateService
|
||||||
|
import dev.usbharu.hideout.core.infrastructure.exposedrepository.AbstractRepository
|
||||||
|
import dev.usbharu.hideout.core.infrastructure.exposedrepository.Actors
|
||||||
|
import dev.usbharu.hideout.core.infrastructure.exposedrepository.Posts
|
||||||
|
import dev.usbharu.hideout.core.infrastructure.exposedrepository.Reactions
|
||||||
|
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.stereotype.Repository
|
||||||
|
|
||||||
|
@Repository
|
||||||
|
class ExposedNotificationRepository(private val idGenerateService: IdGenerateService) : NotificationRepository,
|
||||||
|
AbstractRepository() {
|
||||||
|
override val logger: Logger
|
||||||
|
get() = Companion.logger
|
||||||
|
|
||||||
|
override suspend fun generateId(): Long = idGenerateService.generateId()
|
||||||
|
|
||||||
|
override suspend fun save(notification: Notification): Notification = query {
|
||||||
|
val singleOrNull = Notifications.select {
|
||||||
|
Notifications.id eq notification.id
|
||||||
|
}.forUpdate().singleOrNull()
|
||||||
|
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
|
||||||
|
it[text] = notification.text
|
||||||
|
it[reactionId] = notification.reactionId
|
||||||
|
it[createdAt] = notification.createdAt
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
Notifications.update({ Notifications.id eq notification.id }) {
|
||||||
|
it[type] = notification.type
|
||||||
|
it[userId] = notification.userId
|
||||||
|
it[sourceActorId] = notification.sourceActorId
|
||||||
|
it[postId] = notification.postId
|
||||||
|
it[text] = notification.text
|
||||||
|
it[reactionId] = notification.reactionId
|
||||||
|
it[createdAt] = notification.createdAt
|
||||||
|
}
|
||||||
|
}
|
||||||
|
notification
|
||||||
|
}
|
||||||
|
|
||||||
|
override suspend fun findById(id: Long): Notification? = query {
|
||||||
|
Notifications.select { Notifications.id eq id }.singleOrNull()?.toNotifications()
|
||||||
|
}
|
||||||
|
|
||||||
|
override suspend fun deleteById(id: Long) {
|
||||||
|
Notifications.deleteWhere { Notifications.id eq id }
|
||||||
|
}
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
private val logger = LoggerFactory.getLogger(ExposedNotificationRepository::class.java)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun ResultRow.toNotifications() = Notification(
|
||||||
|
id = this[Notifications.id],
|
||||||
|
type = this[Notifications.type],
|
||||||
|
userId = this[Notifications.userId],
|
||||||
|
sourceActorId = this[Notifications.sourceActorId],
|
||||||
|
postId = this[Notifications.postId],
|
||||||
|
text = this[Notifications.text],
|
||||||
|
reactionId = this[Notifications.reactionId],
|
||||||
|
createdAt = this[Notifications.createdAt],
|
||||||
|
)
|
||||||
|
|
||||||
|
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()
|
||||||
|
val text = varchar("text", 3000).nullable()
|
||||||
|
val reactionId = long("reaction_id").references(Reactions.id).nullable()
|
||||||
|
val createdAt = timestamp("created_at")
|
||||||
|
|
||||||
|
override val primaryKey: PrimaryKey = PrimaryKey(id)
|
||||||
|
}
|
|
@ -0,0 +1,14 @@
|
||||||
|
package dev.usbharu.hideout.core.domain.model.notification
|
||||||
|
|
||||||
|
import java.time.Instant
|
||||||
|
|
||||||
|
data class Notification(
|
||||||
|
val id: Long,
|
||||||
|
val type: String,
|
||||||
|
val userId: Long,
|
||||||
|
val sourceActorId: Long?,
|
||||||
|
val postId: Long?,
|
||||||
|
val text: String?,
|
||||||
|
val reactionId: Long?,
|
||||||
|
val createdAt: Instant
|
||||||
|
)
|
|
@ -0,0 +1,8 @@
|
||||||
|
package dev.usbharu.hideout.core.domain.model.notification
|
||||||
|
|
||||||
|
interface NotificationRepository {
|
||||||
|
suspend fun generateId(): Long
|
||||||
|
suspend fun save(notification: Notification): Notification
|
||||||
|
suspend fun findById(id: Long): Notification?
|
||||||
|
suspend fun deleteById(id: Long)
|
||||||
|
}
|
|
@ -4,7 +4,7 @@ import dev.usbharu.hideout.core.domain.model.emoji.Emoji
|
||||||
import org.springframework.stereotype.Repository
|
import org.springframework.stereotype.Repository
|
||||||
|
|
||||||
@Repository
|
@Repository
|
||||||
@Suppress("FunctionMaxLength", "TooManyFunction")
|
@Suppress("FunctionMaxLength", "TooManyFunctions")
|
||||||
interface ReactionRepository {
|
interface ReactionRepository {
|
||||||
suspend fun generateId(): Long
|
suspend fun generateId(): Long
|
||||||
suspend fun save(reaction: Reaction): Reaction
|
suspend fun save(reaction: Reaction): Reaction
|
||||||
|
@ -13,6 +13,7 @@ interface ReactionRepository {
|
||||||
suspend fun deleteByActorId(actorId: Long): Int
|
suspend fun deleteByActorId(actorId: Long): Int
|
||||||
suspend fun deleteByPostIdAndActorId(postId: Long, actorId: Long)
|
suspend fun deleteByPostIdAndActorId(postId: Long, actorId: Long)
|
||||||
suspend fun deleteByPostIdAndActorIdAndEmoji(postId: Long, actorId: Long, emoji: Emoji)
|
suspend fun deleteByPostIdAndActorIdAndEmoji(postId: Long, actorId: Long, emoji: Emoji)
|
||||||
|
suspend fun findById(id: Long): Reaction?
|
||||||
suspend fun findByPostId(postId: Long): List<Reaction>
|
suspend fun findByPostId(postId: Long): List<Reaction>
|
||||||
suspend fun findByPostIdAndActorIdAndEmojiId(postId: Long, actorId: Long, emojiId: Long): Reaction?
|
suspend fun findByPostIdAndActorIdAndEmojiId(postId: Long, actorId: Long, emojiId: Long): Reaction?
|
||||||
suspend fun existByPostIdAndActorIdAndEmojiId(postId: Long, actorId: Long, emojiId: Long): Boolean
|
suspend fun existByPostIdAndActorIdAndEmojiId(postId: Long, actorId: Long, emojiId: Long): Boolean
|
||||||
|
|
|
@ -43,6 +43,7 @@ interface RelationshipRepository {
|
||||||
ignoreFollowRequest: Boolean
|
ignoreFollowRequest: Boolean
|
||||||
): List<Relationship>
|
): List<Relationship>
|
||||||
|
|
||||||
|
@Suppress("FunctionMaxLength")
|
||||||
suspend fun findByActorIdAntMutingAndMaxIdAndSinceId(
|
suspend fun findByActorIdAntMutingAndMaxIdAndSinceId(
|
||||||
actorId: Long,
|
actorId: Long,
|
||||||
muting: Boolean,
|
muting: Boolean,
|
||||||
|
|
|
@ -101,6 +101,10 @@ class ReactionRepositoryImpl(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
override suspend fun findById(id: Long): Reaction? = query {
|
||||||
|
return@query Reactions.leftJoin(CustomEmojis).select { Reactions.id eq id }.singleOrNull()?.toReaction()
|
||||||
|
}
|
||||||
|
|
||||||
override suspend fun findByPostId(postId: Long): List<Reaction> = query {
|
override suspend fun findByPostId(postId: Long): List<Reaction> = query {
|
||||||
return@query Reactions.leftJoin(CustomEmojis).select { Reactions.postId eq postId }.map { it.toReaction() }
|
return@query Reactions.leftJoin(CustomEmojis).select { Reactions.postId eq postId }.map { it.toReaction() }
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,120 @@
|
||||||
|
package dev.usbharu.hideout.core.service.notification
|
||||||
|
|
||||||
|
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
|
||||||
|
}
|
||||||
|
|
||||||
|
data class MentionNotificationRequest(
|
||||||
|
override val userId: Long,
|
||||||
|
override val sourceActorId: Long,
|
||||||
|
override val postId: Long
|
||||||
|
) : NotificationRequest(
|
||||||
|
userId,
|
||||||
|
sourceActorId,
|
||||||
|
"mention"
|
||||||
|
),
|
||||||
|
PostId {
|
||||||
|
override fun buildNotification(id: Long, createdAt: Instant): Notification = Notification(
|
||||||
|
id = id,
|
||||||
|
type = type,
|
||||||
|
userId = userId,
|
||||||
|
sourceActorId = sourceActorId,
|
||||||
|
postId = postId,
|
||||||
|
text = null,
|
||||||
|
reactionId = null,
|
||||||
|
createdAt = createdAt
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
data class PostNotificationRequest(
|
||||||
|
override val userId: Long,
|
||||||
|
override val sourceActorId: Long,
|
||||||
|
override val postId: Long
|
||||||
|
|
||||||
|
) : NotificationRequest(userId, sourceActorId, "post"), PostId {
|
||||||
|
override fun buildNotification(id: Long, createdAt: Instant): Notification = Notification(
|
||||||
|
id = id,
|
||||||
|
type = type,
|
||||||
|
userId = userId,
|
||||||
|
sourceActorId = sourceActorId,
|
||||||
|
postId = postId,
|
||||||
|
text = null,
|
||||||
|
reactionId = null,
|
||||||
|
createdAt = createdAt
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
data class RepostNotificationRequest(
|
||||||
|
override val userId: Long,
|
||||||
|
override val sourceActorId: Long,
|
||||||
|
override val postId: Long
|
||||||
|
) : NotificationRequest(userId, sourceActorId, "repost"), PostId {
|
||||||
|
override fun buildNotification(id: Long, createdAt: Instant): Notification = Notification(
|
||||||
|
id = id,
|
||||||
|
type = type,
|
||||||
|
userId = userId,
|
||||||
|
sourceActorId = sourceActorId,
|
||||||
|
postId = postId,
|
||||||
|
text = null,
|
||||||
|
reactionId = null,
|
||||||
|
createdAt = createdAt
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
data class FollowNotificationRequest(
|
||||||
|
override val userId: Long,
|
||||||
|
override val sourceActorId: Long
|
||||||
|
) : NotificationRequest(userId, sourceActorId, "follow") {
|
||||||
|
override fun buildNotification(id: Long, createdAt: Instant): Notification = Notification(
|
||||||
|
id = id,
|
||||||
|
type = type,
|
||||||
|
userId = userId,
|
||||||
|
sourceActorId = sourceActorId,
|
||||||
|
postId = null,
|
||||||
|
text = null,
|
||||||
|
reactionId = null,
|
||||||
|
createdAt = createdAt
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
data class FollowRequestNotificationRequest(
|
||||||
|
override val userId: Long,
|
||||||
|
override val sourceActorId: Long
|
||||||
|
) : NotificationRequest(userId, sourceActorId, "follow-request") {
|
||||||
|
override fun buildNotification(id: Long, createdAt: Instant): Notification = Notification(
|
||||||
|
id = id,
|
||||||
|
type = type,
|
||||||
|
userId = userId,
|
||||||
|
sourceActorId = sourceActorId,
|
||||||
|
postId = null,
|
||||||
|
text = null,
|
||||||
|
reactionId = null,
|
||||||
|
createdAt = createdAt
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
data class ReactionNotificationRequest(
|
||||||
|
override val userId: Long,
|
||||||
|
override val sourceActorId: Long,
|
||||||
|
override val postId: Long,
|
||||||
|
val reactionId: Long
|
||||||
|
|
||||||
|
) : NotificationRequest(userId, sourceActorId, "reaction"), PostId {
|
||||||
|
override fun buildNotification(id: Long, createdAt: Instant): Notification = Notification(
|
||||||
|
id = id,
|
||||||
|
type = type,
|
||||||
|
userId = userId,
|
||||||
|
sourceActorId = sourceActorId,
|
||||||
|
postId = postId,
|
||||||
|
text = null,
|
||||||
|
reactionId = reactionId,
|
||||||
|
createdAt = createdAt
|
||||||
|
)
|
||||||
|
}
|
|
@ -0,0 +1,8 @@
|
||||||
|
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 unpublishNotify(notificationId: Long)
|
||||||
|
}
|
|
@ -0,0 +1,97 @@
|
||||||
|
package dev.usbharu.hideout.core.service.notification
|
||||||
|
|
||||||
|
import dev.usbharu.hideout.application.config.ApplicationConfig
|
||||||
|
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.slf4j.LoggerFactory
|
||||||
|
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,
|
||||||
|
private val applicationConfig: ApplicationConfig
|
||||||
|
) : NotificationService {
|
||||||
|
override suspend fun publishNotify(notificationRequest: NotificationRequest): Notification? {
|
||||||
|
logger.debug("NOTIFICATION REQUEST user: {} type: {}", notificationRequest.userId, notificationRequest.type)
|
||||||
|
logger.trace("NotificationRequest: {}", notificationRequest)
|
||||||
|
|
||||||
|
val user = actorRepository.findById(notificationRequest.userId)
|
||||||
|
if (user == null || user.domain != applicationConfig.url.host) {
|
||||||
|
logger.debug("NOTIFICATION REQUEST is rejected. (Remote Actor or user not found.)")
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
// とりあえず個人間のRelationshipに基づいてきめる。今後増やす
|
||||||
|
if (!relationship(notificationRequest)) {
|
||||||
|
logger.debug("NOTIFICATION REQUEST is rejected. (relationship)")
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
val id = notificationRepository.generateId()
|
||||||
|
val createdAt = Instant.now()
|
||||||
|
|
||||||
|
val notification = notificationRequest.buildNotification(id, createdAt)
|
||||||
|
|
||||||
|
val savedNotification = notificationRepository.save(notification)
|
||||||
|
|
||||||
|
val sourceActor = savedNotification.sourceActorId?.let { actorRepository.findById(it) }
|
||||||
|
|
||||||
|
val post = savedNotification.postId?.let { postRepository.findById(it) }
|
||||||
|
val reaction = savedNotification.reactionId?.let { reactionRepository.findById(it) }
|
||||||
|
|
||||||
|
logger.info(
|
||||||
|
"NOTIFICATION id: {} user: {} type: {}",
|
||||||
|
savedNotification.id,
|
||||||
|
savedNotification.userId,
|
||||||
|
savedNotification.type
|
||||||
|
)
|
||||||
|
|
||||||
|
logger.debug("push to {} notification store.", notificationStoreList.size)
|
||||||
|
for (it in notificationStoreList) {
|
||||||
|
@Suppress("TooGenericExceptionCaught")
|
||||||
|
try {
|
||||||
|
it.publishNotification(savedNotification, user, sourceActor, post, reaction)
|
||||||
|
} catch (e: Exception) {
|
||||||
|
logger.warn("FAILED Publish to notification.", e)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
logger.debug("SUCCESS Notification id: {}", savedNotification.id)
|
||||||
|
|
||||||
|
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)
|
||||||
|
}
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
private val logger = LoggerFactory.getLogger(NotificationServiceImpl::class.java)
|
||||||
|
}
|
||||||
|
}
|
|
@ -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
|
||||||
|
}
|
|
@ -0,0 +1,7 @@
|
||||||
|
package dev.usbharu.hideout.core.service.notification
|
||||||
|
|
||||||
|
import dev.usbharu.hideout.core.domain.model.relationship.Relationship
|
||||||
|
|
||||||
|
interface RelationshipNotificationManagementService {
|
||||||
|
fun sendNotification(relationship: Relationship, notificationRequest: NotificationRequest): Boolean
|
||||||
|
}
|
|
@ -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()
|
||||||
|
}
|
|
@ -67,13 +67,13 @@ class DefaultPostContentFormatter(private val policyFactory: PolicyFactory) : Po
|
||||||
|
|
||||||
private fun printHtml(element: Elements): String {
|
private fun printHtml(element: Elements): String {
|
||||||
return element.joinToString("\n\n") {
|
return element.joinToString("\n\n") {
|
||||||
it.childNodes().joinToString("") {
|
it.childNodes().joinToString("") { node ->
|
||||||
if (it is Element && it.tagName() == "br") {
|
if (node is Element && node.tagName() == "br") {
|
||||||
"\n"
|
"\n"
|
||||||
} else if (it is Element) {
|
} else if (node is Element) {
|
||||||
it.text()
|
node.text()
|
||||||
} else if (it is TextNode) {
|
} else if (node is TextNode) {
|
||||||
it.text()
|
node.text()
|
||||||
} else {
|
} else {
|
||||||
""
|
""
|
||||||
}
|
}
|
||||||
|
|
|
@ -5,7 +5,6 @@ import dev.usbharu.hideout.activitypub.service.activity.delete.APSendDeleteServi
|
||||||
import dev.usbharu.hideout.core.domain.exception.UserNotFoundException
|
import dev.usbharu.hideout.core.domain.exception.UserNotFoundException
|
||||||
import dev.usbharu.hideout.core.domain.exception.resource.DuplicateException
|
import dev.usbharu.hideout.core.domain.exception.resource.DuplicateException
|
||||||
import dev.usbharu.hideout.core.domain.exception.resource.PostNotFoundException
|
import dev.usbharu.hideout.core.domain.exception.resource.PostNotFoundException
|
||||||
import dev.usbharu.hideout.core.domain.model.actor.Actor
|
|
||||||
import dev.usbharu.hideout.core.domain.model.actor.ActorRepository
|
import dev.usbharu.hideout.core.domain.model.actor.ActorRepository
|
||||||
import dev.usbharu.hideout.core.domain.model.post.Post
|
import dev.usbharu.hideout.core.domain.model.post.Post
|
||||||
import dev.usbharu.hideout.core.domain.model.post.PostRepository
|
import dev.usbharu.hideout.core.domain.model.post.PostRepository
|
||||||
|
@ -38,7 +37,7 @@ class PostServiceImpl(
|
||||||
logger.info("START Create Remote Post user: {}, remote url: {}", post.actorId, post.apId)
|
logger.info("START Create Remote Post user: {}, remote url: {}", post.actorId, post.apId)
|
||||||
val actor =
|
val actor =
|
||||||
actorRepository.findById(post.actorId) ?: throw UserNotFoundException("${post.actorId} was not found.")
|
actorRepository.findById(post.actorId) ?: throw UserNotFoundException("${post.actorId} was not found.")
|
||||||
val createdPost = internalCreate(post, false, actor)
|
val createdPost = internalCreate(post, false)
|
||||||
logger.info("SUCCESS Create Remote Post url: {}", createdPost.url)
|
logger.info("SUCCESS Create Remote Post url: {}", createdPost.url)
|
||||||
return createdPost
|
return createdPost
|
||||||
}
|
}
|
||||||
|
@ -79,11 +78,10 @@ class PostServiceImpl(
|
||||||
actorRepository.save(actor.copy(postsCount = 0, lastPostDate = null))
|
actorRepository.save(actor.copy(postsCount = 0, lastPostDate = null))
|
||||||
}
|
}
|
||||||
|
|
||||||
private suspend fun internalCreate(post: Post, isLocal: Boolean, actor: Actor): Post {
|
private suspend fun internalCreate(post: Post, isLocal: Boolean): Post {
|
||||||
return try {
|
return try {
|
||||||
val save = postRepository.save(post)
|
val save = postRepository.save(post)
|
||||||
timelineService.publishTimeline(post, isLocal)
|
timelineService.publishTimeline(post, isLocal)
|
||||||
// actorRepository.save(actor.incrementPostsCount())
|
|
||||||
save
|
save
|
||||||
} catch (_: DuplicateException) {
|
} catch (_: DuplicateException) {
|
||||||
postRepository.findByApId(post.apId) ?: throw PostNotFoundException.withApId(post.apId)
|
postRepository.findByApId(post.apId) ?: throw PostNotFoundException.withApId(post.apId)
|
||||||
|
@ -105,7 +103,7 @@ class PostServiceImpl(
|
||||||
replyId = post.repolyId,
|
replyId = post.repolyId,
|
||||||
repostId = post.repostId,
|
repostId = post.repostId,
|
||||||
)
|
)
|
||||||
return internalCreate(createPost, isLocal, user)
|
return internalCreate(createPost, isLocal)
|
||||||
}
|
}
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
|
|
|
@ -3,8 +3,11 @@ package dev.usbharu.hideout.core.service.reaction
|
||||||
import dev.usbharu.hideout.activitypub.service.activity.like.APReactionService
|
import dev.usbharu.hideout.activitypub.service.activity.like.APReactionService
|
||||||
import dev.usbharu.hideout.core.domain.exception.resource.DuplicateException
|
import dev.usbharu.hideout.core.domain.exception.resource.DuplicateException
|
||||||
import dev.usbharu.hideout.core.domain.model.emoji.Emoji
|
import dev.usbharu.hideout.core.domain.model.emoji.Emoji
|
||||||
|
import dev.usbharu.hideout.core.domain.model.post.PostRepository
|
||||||
import dev.usbharu.hideout.core.domain.model.reaction.Reaction
|
import dev.usbharu.hideout.core.domain.model.reaction.Reaction
|
||||||
import dev.usbharu.hideout.core.domain.model.reaction.ReactionRepository
|
import dev.usbharu.hideout.core.domain.model.reaction.ReactionRepository
|
||||||
|
import dev.usbharu.hideout.core.service.notification.NotificationService
|
||||||
|
import dev.usbharu.hideout.core.service.notification.ReactionNotificationRequest
|
||||||
import org.slf4j.Logger
|
import org.slf4j.Logger
|
||||||
import org.slf4j.LoggerFactory
|
import org.slf4j.LoggerFactory
|
||||||
import org.springframework.stereotype.Service
|
import org.springframework.stereotype.Service
|
||||||
|
@ -12,7 +15,9 @@ import org.springframework.stereotype.Service
|
||||||
@Service
|
@Service
|
||||||
class ReactionServiceImpl(
|
class ReactionServiceImpl(
|
||||||
private val reactionRepository: ReactionRepository,
|
private val reactionRepository: ReactionRepository,
|
||||||
private val apReactionService: APReactionService
|
private val apReactionService: APReactionService,
|
||||||
|
private val notificationService: NotificationService,
|
||||||
|
private val postRepository: PostRepository
|
||||||
) : ReactionService {
|
) : ReactionService {
|
||||||
override suspend fun receiveReaction(
|
override suspend fun receiveReaction(
|
||||||
emoji: Emoji,
|
emoji: Emoji,
|
||||||
|
@ -23,7 +28,16 @@ class ReactionServiceImpl(
|
||||||
reactionRepository.deleteByPostIdAndActorId(postId, actorId)
|
reactionRepository.deleteByPostIdAndActorId(postId, actorId)
|
||||||
}
|
}
|
||||||
try {
|
try {
|
||||||
reactionRepository.save(Reaction(reactionRepository.generateId(), emoji, postId, actorId))
|
val reaction = reactionRepository.save(Reaction(reactionRepository.generateId(), emoji, postId, actorId))
|
||||||
|
|
||||||
|
notificationService.publishNotify(
|
||||||
|
ReactionNotificationRequest(
|
||||||
|
postRepository.findById(postId)!!.actorId,
|
||||||
|
actorId,
|
||||||
|
postId,
|
||||||
|
reaction.id
|
||||||
|
)
|
||||||
|
)
|
||||||
} catch (_: DuplicateException) {
|
} catch (_: DuplicateException) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -49,6 +63,10 @@ class ReactionServiceImpl(
|
||||||
val reaction = Reaction(reactionRepository.generateId(), emoji, postId, actorId)
|
val reaction = Reaction(reactionRepository.generateId(), emoji, postId, actorId)
|
||||||
reactionRepository.save(reaction)
|
reactionRepository.save(reaction)
|
||||||
apReactionService.reaction(reaction)
|
apReactionService.reaction(reaction)
|
||||||
|
|
||||||
|
val id = postRepository.findById(postId)!!.actorId
|
||||||
|
|
||||||
|
notificationService.publishNotify(ReactionNotificationRequest(id, actorId, postId, reaction.id))
|
||||||
}
|
}
|
||||||
|
|
||||||
override suspend fun removeReaction(actorId: Long, postId: Long) {
|
override suspend fun removeReaction(actorId: Long, postId: Long) {
|
||||||
|
|
|
@ -12,6 +12,9 @@ import dev.usbharu.hideout.core.domain.model.actor.ActorRepository
|
||||||
import dev.usbharu.hideout.core.domain.model.relationship.Relationship
|
import dev.usbharu.hideout.core.domain.model.relationship.Relationship
|
||||||
import dev.usbharu.hideout.core.domain.model.relationship.RelationshipRepository
|
import dev.usbharu.hideout.core.domain.model.relationship.RelationshipRepository
|
||||||
import dev.usbharu.hideout.core.service.follow.SendFollowDto
|
import dev.usbharu.hideout.core.service.follow.SendFollowDto
|
||||||
|
import dev.usbharu.hideout.core.service.notification.FollowNotificationRequest
|
||||||
|
import dev.usbharu.hideout.core.service.notification.FollowRequestNotificationRequest
|
||||||
|
import dev.usbharu.hideout.core.service.notification.NotificationService
|
||||||
import org.slf4j.LoggerFactory
|
import org.slf4j.LoggerFactory
|
||||||
import org.springframework.stereotype.Service
|
import org.springframework.stereotype.Service
|
||||||
|
|
||||||
|
@ -24,7 +27,8 @@ class RelationshipServiceImpl(
|
||||||
private val apSendAcceptService: ApSendAcceptService,
|
private val apSendAcceptService: ApSendAcceptService,
|
||||||
private val apSendRejectService: ApSendRejectService,
|
private val apSendRejectService: ApSendRejectService,
|
||||||
private val apSendUndoService: APSendUndoService,
|
private val apSendUndoService: APSendUndoService,
|
||||||
private val actorRepository: ActorRepository
|
private val actorRepository: ActorRepository,
|
||||||
|
private val notificationService: NotificationService
|
||||||
) : RelationshipService {
|
) : RelationshipService {
|
||||||
override suspend fun followRequest(actorId: Long, targetId: Long) {
|
override suspend fun followRequest(actorId: Long, targetId: Long) {
|
||||||
logger.info("START Follow Request userId: {} targetId: {}", actorId, targetId)
|
logger.info("START Follow Request userId: {} targetId: {}", actorId, targetId)
|
||||||
|
@ -82,6 +86,8 @@ class RelationshipServiceImpl(
|
||||||
val target = actorRepository.findById(targetId) ?: throw UserNotFoundException.withId(targetId)
|
val target = actorRepository.findById(targetId) ?: throw UserNotFoundException.withId(targetId)
|
||||||
if (target.locked.not()) {
|
if (target.locked.not()) {
|
||||||
acceptFollowRequest(targetId, actorId)
|
acceptFollowRequest(targetId, actorId)
|
||||||
|
} else {
|
||||||
|
notificationService.publishNotify(FollowRequestNotificationRequest(targetId, actorId))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -185,6 +191,7 @@ class RelationshipServiceImpl(
|
||||||
if (isRemoteActor(remoteActor)) {
|
if (isRemoteActor(remoteActor)) {
|
||||||
apSendAcceptService.sendAcceptFollow(user, remoteActor)
|
apSendAcceptService.sendAcceptFollow(user, remoteActor)
|
||||||
}
|
}
|
||||||
|
notificationService.publishNotify(FollowNotificationRequest(actorId, targetId))
|
||||||
}
|
}
|
||||||
|
|
||||||
override suspend fun rejectFollowRequest(actorId: Long, targetId: Long) {
|
override suspend fun rejectFollowRequest(actorId: Long, targetId: Long) {
|
||||||
|
|
|
@ -0,0 +1,18 @@
|
||||||
|
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 userId: Long,
|
||||||
|
val type: NotificationType,
|
||||||
|
val createdAt: Instant,
|
||||||
|
val accountId: Long,
|
||||||
|
val statusId: Long?,
|
||||||
|
val reportId: Long?,
|
||||||
|
val relationshipServeranceEvent: Long?
|
||||||
|
)
|
|
@ -0,0 +1,21 @@
|
||||||
|
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?
|
||||||
|
|
||||||
|
@Suppress("LongParameterList", "FunctionMaxLength")
|
||||||
|
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)
|
||||||
|
}
|
|
@ -0,0 +1,33 @@
|
||||||
|
package dev.usbharu.hideout.mastodon.domain.model
|
||||||
|
|
||||||
|
@Suppress("EnumEntryName", "EnumNaming", "EnumEntryNameCase")
|
||||||
|
enum class NotificationType {
|
||||||
|
mention,
|
||||||
|
status,
|
||||||
|
reblog,
|
||||||
|
follow,
|
||||||
|
follow_request,
|
||||||
|
favourite,
|
||||||
|
poll,
|
||||||
|
update,
|
||||||
|
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.sign_up" -> admin_sign_up
|
||||||
|
"admin.report" -> admin_report
|
||||||
|
"servered_relationships" -> severed_relationships
|
||||||
|
else -> null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,128 @@
|
||||||
|
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
|
||||||
|
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()
|
||||||
|
}
|
||||||
|
|
||||||
|
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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun ResultRow.toMastodonNotification(): MastodonNotification = MastodonNotification(
|
||||||
|
id = this[MastodonNotifications.id],
|
||||||
|
userId = this[MastodonNotifications.userId],
|
||||||
|
type = NotificationType.valueOf(this[MastodonNotifications.type]),
|
||||||
|
createdAt = this[MastodonNotifications.createdAt],
|
||||||
|
accountId = this[MastodonNotifications.accountId],
|
||||||
|
statusId = this[MastodonNotifications.statusId],
|
||||||
|
reportId = this[MastodonNotifications.reportId],
|
||||||
|
relationshipServeranceEvent = this[MastodonNotifications.relationshipServeranceEventId],
|
||||||
|
)
|
||||||
|
|
||||||
|
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")
|
||||||
|
val statusId = long("status_id").nullable()
|
||||||
|
val reportId = long("report_id").nullable()
|
||||||
|
val relationshipServeranceEventId = long("relationship_serverance_event_id").nullable()
|
||||||
|
}
|
|
@ -0,0 +1,11 @@
|
||||||
|
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<MastodonNotification, Long> {
|
||||||
|
|
||||||
|
fun deleteByUserId(userId: Long): Long
|
||||||
|
|
||||||
|
fun deleteByIdAndUserId(id: Long, userId: Long): Long
|
||||||
|
}
|
|
@ -0,0 +1,67 @@
|
||||||
|
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,
|
||||||
|
private val mongoTemplate: MongoTemplate
|
||||||
|
) :
|
||||||
|
MastodonNotificationRepository {
|
||||||
|
override suspend fun save(mastodonNotification: MastodonNotification): MastodonNotification =
|
||||||
|
mongoMastodonNotificationRepository.save(mastodonNotification)
|
||||||
|
|
||||||
|
override suspend fun deleteById(id: Long) = mongoMastodonNotificationRepository.deleteById(id)
|
||||||
|
|
||||||
|
override suspend fun findById(id: Long): MastodonNotification? =
|
||||||
|
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(
|
||||||
|
loginUser = loginUserContextHolder.getLoginUserId(),
|
||||||
|
maxId = maxId?.toLong(),
|
||||||
|
minId = minId?.toLong(),
|
||||||
|
sinceId = sinceId?.toLong(),
|
||||||
|
limit = limit ?: 20,
|
||||||
|
types = 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)
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,66 @@
|
||||||
|
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,
|
||||||
|
userId = notification.userId,
|
||||||
|
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)
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,24 @@
|
||||||
|
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 {
|
||||||
|
@Suppress("LongParameterList")
|
||||||
|
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
|
||||||
|
}
|
||||||
|
}
|
|
@ -253,4 +253,20 @@ create table if not exists deleted_actors
|
||||||
public_key varchar(10000) not null,
|
public_key varchar(10000) not null,
|
||||||
deleted_at timestamp not null,
|
deleted_at timestamp not null,
|
||||||
unique ("name", domain)
|
unique ("name", domain)
|
||||||
|
);
|
||||||
|
|
||||||
|
create table if not exists notifications
|
||||||
|
(
|
||||||
|
id bigint primary key,
|
||||||
|
type varchar(100) not null,
|
||||||
|
user_id bigint not null,
|
||||||
|
source_actor_id bigint null,
|
||||||
|
post_id bigint null,
|
||||||
|
text varchar(3000) null,
|
||||||
|
reaction_id bigint null,
|
||||||
|
created_at timestamp not null,
|
||||||
|
constraint fk_notifications_user_id__id foreign key (user_id) references actors (id) on delete cascade on update cascade,
|
||||||
|
constraint fk_notifications_source_actor__id foreign key (source_actor_id) references actors (id) on delete cascade on update cascade,
|
||||||
|
constraint fk_notifications_post_id__id foreign key (post_id) references posts (id) on delete cascade on update cascade,
|
||||||
|
constraint fk_notifications_reaction_id__id foreign key (reaction_id) references reactions (id) on delete cascade on update cascade
|
||||||
)
|
)
|
||||||
|
|
|
@ -19,6 +19,8 @@ tags:
|
||||||
description: timeline
|
description: timeline
|
||||||
- name: media
|
- name: media
|
||||||
description: media
|
description: media
|
||||||
|
- name: notification
|
||||||
|
description: notification
|
||||||
|
|
||||||
paths:
|
paths:
|
||||||
/api/v2/instance:
|
/api/v2/instance:
|
||||||
|
@ -803,6 +805,122 @@ paths:
|
||||||
schema:
|
schema:
|
||||||
$ref: "#/components/schemas/Relationship"
|
$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:
|
components:
|
||||||
schemas:
|
schemas:
|
||||||
V1MediaRequest:
|
V1MediaRequest:
|
||||||
|
@ -1935,6 +2053,47 @@ components:
|
||||||
value:
|
value:
|
||||||
type: string
|
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:
|
securitySchemes:
|
||||||
OAuth2:
|
OAuth2:
|
||||||
type: oauth2
|
type: oauth2
|
||||||
|
|
|
@ -96,6 +96,9 @@ class EqualsAndToStringTest {
|
||||||
.filter {
|
.filter {
|
||||||
it.superclass == Any::class.java || it.superclass?.packageName?.startsWith("dev.usbharu") ?: true
|
it.superclass == Any::class.java || it.superclass?.packageName?.startsWith("dev.usbharu") ?: true
|
||||||
}
|
}
|
||||||
|
.filterNot {
|
||||||
|
it.superclass.isSealed
|
||||||
|
}
|
||||||
.filterNot { it == UnicodeEmoji::class.java }
|
.filterNot { it == UnicodeEmoji::class.java }
|
||||||
.map {
|
.map {
|
||||||
|
|
||||||
|
|
|
@ -0,0 +1,28 @@
|
||||||
|
package dev.usbharu.hideout.core.service.notification
|
||||||
|
|
||||||
|
import dev.usbharu.hideout.core.domain.model.notification.Notification
|
||||||
|
import org.assertj.core.api.Assertions
|
||||||
|
import org.junit.jupiter.api.Test
|
||||||
|
import java.time.Instant
|
||||||
|
|
||||||
|
class FollowNotificationRequestTest {
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun buildNotification() {
|
||||||
|
val createdAt = Instant.now()
|
||||||
|
val actual = FollowNotificationRequest(1, 2).buildNotification(1, createdAt)
|
||||||
|
|
||||||
|
Assertions.assertThat(actual).isEqualTo(
|
||||||
|
Notification(
|
||||||
|
id = 1,
|
||||||
|
type = "follow",
|
||||||
|
userId = 1,
|
||||||
|
sourceActorId = 2,
|
||||||
|
postId = null,
|
||||||
|
text = null,
|
||||||
|
reactionId = null,
|
||||||
|
createdAt = createdAt
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,28 @@
|
||||||
|
package dev.usbharu.hideout.core.service.notification
|
||||||
|
|
||||||
|
import dev.usbharu.hideout.core.domain.model.notification.Notification
|
||||||
|
import org.assertj.core.api.Assertions
|
||||||
|
import org.junit.jupiter.api.Test
|
||||||
|
import java.time.Instant
|
||||||
|
|
||||||
|
class FollowRequestNotificationRequestTest {
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun buildNotification() {
|
||||||
|
val createdAt = Instant.now()
|
||||||
|
val actual = FollowRequestNotificationRequest(1, 2).buildNotification(1, createdAt)
|
||||||
|
|
||||||
|
Assertions.assertThat(actual).isEqualTo(
|
||||||
|
Notification(
|
||||||
|
id = 1,
|
||||||
|
type = "follow-request",
|
||||||
|
userId = 1,
|
||||||
|
sourceActorId = 2,
|
||||||
|
postId = null,
|
||||||
|
text = null,
|
||||||
|
reactionId = null,
|
||||||
|
createdAt = createdAt
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,28 @@
|
||||||
|
package dev.usbharu.hideout.core.service.notification
|
||||||
|
|
||||||
|
import dev.usbharu.hideout.core.domain.model.notification.Notification
|
||||||
|
import org.assertj.core.api.Assertions.assertThat
|
||||||
|
import org.junit.jupiter.api.Test
|
||||||
|
import java.time.Instant
|
||||||
|
|
||||||
|
class MentionNotificationRequestTest {
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun buildNotification() {
|
||||||
|
val createdAt = Instant.now()
|
||||||
|
val actual = MentionNotificationRequest(1, 2, 3).buildNotification(1, createdAt)
|
||||||
|
|
||||||
|
assertThat(actual).isEqualTo(
|
||||||
|
Notification(
|
||||||
|
id = 1,
|
||||||
|
type = "mention",
|
||||||
|
userId = 1,
|
||||||
|
sourceActorId = 2,
|
||||||
|
postId = 3,
|
||||||
|
text = null,
|
||||||
|
reactionId = null,
|
||||||
|
createdAt = createdAt
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,97 @@
|
||||||
|
package dev.usbharu.hideout.core.service.notification
|
||||||
|
|
||||||
|
import dev.usbharu.hideout.application.config.ApplicationConfig
|
||||||
|
import dev.usbharu.hideout.application.service.id.TwitterSnowflakeIdGenerateService
|
||||||
|
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 kotlinx.coroutines.test.runTest
|
||||||
|
import org.assertj.core.api.Assertions.assertThat
|
||||||
|
import org.junit.jupiter.api.Test
|
||||||
|
import org.junit.jupiter.api.extension.ExtendWith
|
||||||
|
import org.mockito.InjectMocks
|
||||||
|
import org.mockito.Mock
|
||||||
|
import org.mockito.Spy
|
||||||
|
import org.mockito.junit.jupiter.MockitoExtension
|
||||||
|
import org.mockito.kotlin.*
|
||||||
|
import utils.UserBuilder
|
||||||
|
import java.net.URL
|
||||||
|
|
||||||
|
@ExtendWith(MockitoExtension::class)
|
||||||
|
class NotificationServiceImplTest {
|
||||||
|
|
||||||
|
|
||||||
|
@Mock
|
||||||
|
private lateinit var relationshipNotificationManagementService: RelationshipNotificationManagementService
|
||||||
|
|
||||||
|
@Mock
|
||||||
|
private lateinit var relationshipRepository: RelationshipRepository
|
||||||
|
|
||||||
|
@Spy
|
||||||
|
private val notificationStoreList: MutableList<NotificationStore> = mutableListOf()
|
||||||
|
|
||||||
|
@Mock
|
||||||
|
private lateinit var notificationRepository: NotificationRepository
|
||||||
|
|
||||||
|
@Mock
|
||||||
|
private lateinit var actorRepository: ActorRepository
|
||||||
|
|
||||||
|
@Mock
|
||||||
|
private lateinit var postRepository: PostRepository
|
||||||
|
|
||||||
|
@Mock
|
||||||
|
private lateinit var reactionRepository: ReactionRepository
|
||||||
|
|
||||||
|
@Spy
|
||||||
|
private val applicationConfig = ApplicationConfig(URL("https://example.com"))
|
||||||
|
|
||||||
|
@InjectMocks
|
||||||
|
private lateinit var notificationServiceImpl: NotificationServiceImpl
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `publishNotifi ローカルユーザーへの通知を発行する`() = runTest {
|
||||||
|
|
||||||
|
val actor = UserBuilder.localUserOf(domain = "example.com")
|
||||||
|
|
||||||
|
whenever(actorRepository.findById(eq(1))).doReturn(actor)
|
||||||
|
|
||||||
|
val id = TwitterSnowflakeIdGenerateService.generateId()
|
||||||
|
|
||||||
|
whenever(notificationRepository.generateId()).doReturn(id)
|
||||||
|
|
||||||
|
whenever(notificationRepository.save(any())).doAnswer { it.arguments[0] as Notification }
|
||||||
|
|
||||||
|
|
||||||
|
val actual = notificationServiceImpl.publishNotify(PostNotificationRequest(1, 2, 3))
|
||||||
|
|
||||||
|
assertThat(actual).isNotNull()
|
||||||
|
|
||||||
|
verify(notificationRepository, times(1)).save(any())
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `publishNotify ユーザーが存在しないときは発行しない`() = runTest {
|
||||||
|
val actual = notificationServiceImpl.publishNotify(PostNotificationRequest(1, 2, 3))
|
||||||
|
|
||||||
|
assertThat(actual).isNull()
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `publishNotify ユーザーがリモートユーザーの場合は発行しない`() = runTest {
|
||||||
|
val actor = UserBuilder.remoteUserOf(domain = "remote.example.com")
|
||||||
|
|
||||||
|
whenever(actorRepository.findById(eq(1))).doReturn(actor)
|
||||||
|
|
||||||
|
val actual = notificationServiceImpl.publishNotify(PostNotificationRequest(1, 2, 3))
|
||||||
|
|
||||||
|
assertThat(actual).isNull()
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun unpublishNotify() = runTest {
|
||||||
|
notificationServiceImpl.unpublishNotify(1)
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,28 @@
|
||||||
|
package dev.usbharu.hideout.core.service.notification
|
||||||
|
|
||||||
|
import dev.usbharu.hideout.core.domain.model.notification.Notification
|
||||||
|
import org.assertj.core.api.Assertions
|
||||||
|
import org.junit.jupiter.api.Test
|
||||||
|
import java.time.Instant
|
||||||
|
|
||||||
|
class PostNotificationRequestTest {
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun buildNotification() {
|
||||||
|
val createdAt = Instant.now()
|
||||||
|
val actual = PostNotificationRequest(1, 2, 3).buildNotification(1, createdAt)
|
||||||
|
|
||||||
|
Assertions.assertThat(actual).isEqualTo(
|
||||||
|
Notification(
|
||||||
|
id = 1,
|
||||||
|
type = "post",
|
||||||
|
userId = 1,
|
||||||
|
sourceActorId = 2,
|
||||||
|
postId = 3,
|
||||||
|
text = null,
|
||||||
|
reactionId = null,
|
||||||
|
createdAt = createdAt
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,28 @@
|
||||||
|
package dev.usbharu.hideout.core.service.notification
|
||||||
|
|
||||||
|
import dev.usbharu.hideout.core.domain.model.notification.Notification
|
||||||
|
import org.assertj.core.api.Assertions
|
||||||
|
import org.junit.jupiter.api.Test
|
||||||
|
import java.time.Instant
|
||||||
|
|
||||||
|
class ReactionNotificationRequestTest {
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun buildNotification() {
|
||||||
|
val createdAt = Instant.now()
|
||||||
|
val actual = ReactionNotificationRequest(1, 2, 3, 4).buildNotification(1, createdAt)
|
||||||
|
|
||||||
|
Assertions.assertThat(actual).isEqualTo(
|
||||||
|
Notification(
|
||||||
|
id = 1,
|
||||||
|
type = "reaction",
|
||||||
|
userId = 1,
|
||||||
|
sourceActorId = 2,
|
||||||
|
postId = 3,
|
||||||
|
text = null,
|
||||||
|
reactionId = 4,
|
||||||
|
createdAt = createdAt
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,25 @@
|
||||||
|
package dev.usbharu.hideout.core.service.notification
|
||||||
|
|
||||||
|
import dev.usbharu.hideout.core.domain.model.relationship.Relationship
|
||||||
|
import org.junit.jupiter.api.Test
|
||||||
|
import kotlin.test.assertTrue
|
||||||
|
|
||||||
|
class RelationshipNotificationManagementServiceImplTest {
|
||||||
|
@Test
|
||||||
|
fun `sendNotification ミューとしていない場合送信する`() {
|
||||||
|
val notification = RelationshipNotificationManagementServiceImpl().sendNotification(
|
||||||
|
Relationship(
|
||||||
|
1,
|
||||||
|
2,
|
||||||
|
false,
|
||||||
|
false,
|
||||||
|
false,
|
||||||
|
false,
|
||||||
|
false
|
||||||
|
), PostNotificationRequest(1, 2, 3)
|
||||||
|
)
|
||||||
|
|
||||||
|
assertTrue(notification)
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,28 @@
|
||||||
|
package dev.usbharu.hideout.core.service.notification
|
||||||
|
|
||||||
|
import dev.usbharu.hideout.core.domain.model.notification.Notification
|
||||||
|
import org.assertj.core.api.Assertions
|
||||||
|
import org.junit.jupiter.api.Test
|
||||||
|
import java.time.Instant
|
||||||
|
|
||||||
|
class RepostNotificationRequestTest {
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun buildNotification() {
|
||||||
|
val createdAt = Instant.now()
|
||||||
|
val actual = RepostNotificationRequest(1, 2, 3).buildNotification(1, createdAt)
|
||||||
|
|
||||||
|
Assertions.assertThat(actual).isEqualTo(
|
||||||
|
Notification(
|
||||||
|
id = 1,
|
||||||
|
type = "repost",
|
||||||
|
userId = 1,
|
||||||
|
sourceActorId = 2,
|
||||||
|
postId = 3,
|
||||||
|
text = null,
|
||||||
|
reactionId = null,
|
||||||
|
createdAt = createdAt
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
|
@ -4,8 +4,10 @@ package dev.usbharu.hideout.core.service.reaction
|
||||||
import dev.usbharu.hideout.activitypub.service.activity.like.APReactionService
|
import dev.usbharu.hideout.activitypub.service.activity.like.APReactionService
|
||||||
import dev.usbharu.hideout.application.service.id.TwitterSnowflakeIdGenerateService
|
import dev.usbharu.hideout.application.service.id.TwitterSnowflakeIdGenerateService
|
||||||
import dev.usbharu.hideout.core.domain.model.emoji.UnicodeEmoji
|
import dev.usbharu.hideout.core.domain.model.emoji.UnicodeEmoji
|
||||||
|
import dev.usbharu.hideout.core.domain.model.post.PostRepository
|
||||||
import dev.usbharu.hideout.core.domain.model.reaction.Reaction
|
import dev.usbharu.hideout.core.domain.model.reaction.Reaction
|
||||||
import dev.usbharu.hideout.core.domain.model.reaction.ReactionRepository
|
import dev.usbharu.hideout.core.domain.model.reaction.ReactionRepository
|
||||||
|
import dev.usbharu.hideout.core.service.notification.NotificationService
|
||||||
import kotlinx.coroutines.test.runTest
|
import kotlinx.coroutines.test.runTest
|
||||||
import org.junit.jupiter.api.Test
|
import org.junit.jupiter.api.Test
|
||||||
import org.junit.jupiter.api.extension.ExtendWith
|
import org.junit.jupiter.api.extension.ExtendWith
|
||||||
|
@ -18,6 +20,12 @@ import utils.PostBuilder
|
||||||
@ExtendWith(MockitoExtension::class)
|
@ExtendWith(MockitoExtension::class)
|
||||||
class ReactionServiceImplTest {
|
class ReactionServiceImplTest {
|
||||||
|
|
||||||
|
@Mock
|
||||||
|
private lateinit var notificationService: NotificationService
|
||||||
|
|
||||||
|
@Mock
|
||||||
|
private lateinit var postRepository: PostRepository
|
||||||
|
|
||||||
@Mock
|
@Mock
|
||||||
private lateinit var reactionRepository: ReactionRepository
|
private lateinit var reactionRepository: ReactionRepository
|
||||||
|
|
||||||
|
@ -35,6 +43,9 @@ class ReactionServiceImplTest {
|
||||||
whenever(reactionRepository.existByPostIdAndActor(eq(post.id), eq(post.actorId))).doReturn(
|
whenever(reactionRepository.existByPostIdAndActor(eq(post.id), eq(post.actorId))).doReturn(
|
||||||
false
|
false
|
||||||
)
|
)
|
||||||
|
whenever(postRepository.findById(eq(post.id))).doReturn(post)
|
||||||
|
whenever(reactionRepository.save(any())).doAnswer { it.arguments[0] as Reaction }
|
||||||
|
|
||||||
val generateId = TwitterSnowflakeIdGenerateService.generateId()
|
val generateId = TwitterSnowflakeIdGenerateService.generateId()
|
||||||
whenever(reactionRepository.generateId()).doReturn(generateId)
|
whenever(reactionRepository.generateId()).doReturn(generateId)
|
||||||
|
|
||||||
|
@ -50,7 +61,8 @@ class ReactionServiceImplTest {
|
||||||
whenever(reactionRepository.existByPostIdAndActor(eq(post.id), eq(post.actorId))).doReturn(
|
whenever(reactionRepository.existByPostIdAndActor(eq(post.id), eq(post.actorId))).doReturn(
|
||||||
true
|
true
|
||||||
)
|
)
|
||||||
|
whenever(postRepository.findById(eq(post.id))).doReturn(post)
|
||||||
|
whenever(reactionRepository.save(any())).doAnswer { it.arguments[0] as Reaction }
|
||||||
val generateId = TwitterSnowflakeIdGenerateService.generateId()
|
val generateId = TwitterSnowflakeIdGenerateService.generateId()
|
||||||
|
|
||||||
whenever(reactionRepository.generateId()).doReturn(generateId)
|
whenever(reactionRepository.generateId()).doReturn(generateId)
|
||||||
|
@ -67,6 +79,8 @@ class ReactionServiceImplTest {
|
||||||
whenever(reactionRepository.findByPostIdAndActorIdAndEmojiId(eq(post.id), eq(post.actorId), eq(0))).doReturn(
|
whenever(reactionRepository.findByPostIdAndActorIdAndEmojiId(eq(post.id), eq(post.actorId), eq(0))).doReturn(
|
||||||
null
|
null
|
||||||
)
|
)
|
||||||
|
whenever(postRepository.findById(eq(post.id))).doReturn(post)
|
||||||
|
whenever(reactionRepository.save(any())).doAnswer { it.arguments[0] as Reaction }
|
||||||
val generateId = TwitterSnowflakeIdGenerateService.generateId()
|
val generateId = TwitterSnowflakeIdGenerateService.generateId()
|
||||||
whenever(reactionRepository.generateId()).doReturn(generateId)
|
whenever(reactionRepository.generateId()).doReturn(generateId)
|
||||||
|
|
||||||
|
@ -83,6 +97,8 @@ class ReactionServiceImplTest {
|
||||||
whenever(reactionRepository.findByPostIdAndActorIdAndEmojiId(eq(post.id), eq(post.actorId), eq(0))).doReturn(
|
whenever(reactionRepository.findByPostIdAndActorIdAndEmojiId(eq(post.id), eq(post.actorId), eq(0))).doReturn(
|
||||||
Reaction(id, UnicodeEmoji("❤"), post.id, post.actorId)
|
Reaction(id, UnicodeEmoji("❤"), post.id, post.actorId)
|
||||||
)
|
)
|
||||||
|
whenever(postRepository.findById(eq(post.id))).doReturn(post)
|
||||||
|
whenever(reactionRepository.save(any())).doAnswer { it.arguments[0] as Reaction }
|
||||||
val generateId = TwitterSnowflakeIdGenerateService.generateId()
|
val generateId = TwitterSnowflakeIdGenerateService.generateId()
|
||||||
whenever(reactionRepository.generateId()).doReturn(generateId)
|
whenever(reactionRepository.generateId()).doReturn(generateId)
|
||||||
|
|
||||||
|
|
|
@ -10,6 +10,7 @@ import dev.usbharu.hideout.core.domain.model.actor.ActorRepository
|
||||||
import dev.usbharu.hideout.core.domain.model.relationship.Relationship
|
import dev.usbharu.hideout.core.domain.model.relationship.Relationship
|
||||||
import dev.usbharu.hideout.core.domain.model.relationship.RelationshipRepository
|
import dev.usbharu.hideout.core.domain.model.relationship.RelationshipRepository
|
||||||
import dev.usbharu.hideout.core.service.follow.SendFollowDto
|
import dev.usbharu.hideout.core.service.follow.SendFollowDto
|
||||||
|
import dev.usbharu.hideout.core.service.notification.NotificationService
|
||||||
import kotlinx.coroutines.test.runTest
|
import kotlinx.coroutines.test.runTest
|
||||||
import org.junit.jupiter.api.Test
|
import org.junit.jupiter.api.Test
|
||||||
import org.junit.jupiter.api.assertThrows
|
import org.junit.jupiter.api.assertThrows
|
||||||
|
@ -25,6 +26,10 @@ import java.net.URL
|
||||||
@ExtendWith(MockitoExtension::class)
|
@ExtendWith(MockitoExtension::class)
|
||||||
class RelationshipServiceImplTest {
|
class RelationshipServiceImplTest {
|
||||||
|
|
||||||
|
|
||||||
|
@Mock
|
||||||
|
private lateinit var notificationService: NotificationService
|
||||||
|
|
||||||
@Spy
|
@Spy
|
||||||
private val applicationConfig = ApplicationConfig(URL("https://example.com"))
|
private val applicationConfig = ApplicationConfig(URL("https://example.com"))
|
||||||
|
|
||||||
|
|
|
@ -0,0 +1,43 @@
|
||||||
|
package dev.usbharu.hideout.mastodon.domain.model
|
||||||
|
|
||||||
|
import org.junit.jupiter.params.ParameterizedTest
|
||||||
|
import org.junit.jupiter.params.provider.Arguments
|
||||||
|
import org.junit.jupiter.params.provider.Arguments.arguments
|
||||||
|
import org.junit.jupiter.params.provider.MethodSource
|
||||||
|
import org.junit.jupiter.params.provider.ValueSource
|
||||||
|
import java.util.stream.Stream
|
||||||
|
import kotlin.test.assertEquals
|
||||||
|
import kotlin.test.assertNull
|
||||||
|
|
||||||
|
class NotificationTypeTest {
|
||||||
|
@ParameterizedTest
|
||||||
|
@MethodSource("parseSuccessProvider")
|
||||||
|
fun parseに成功する(s: String, notificationType: NotificationType) {
|
||||||
|
assertEquals(notificationType, NotificationType.parse(s))
|
||||||
|
}
|
||||||
|
|
||||||
|
@ParameterizedTest
|
||||||
|
@ValueSource(strings = ["hoge", "fuga", "0x1234", "follow_reject", "test", "mentiooon", "emoji_reaction", "reaction"])
|
||||||
|
fun parseに失敗する(s: String) {
|
||||||
|
assertNull(NotificationType.parse(s))
|
||||||
|
}
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
@JvmStatic
|
||||||
|
fun parseSuccessProvider(): Stream<Arguments> {
|
||||||
|
return Stream.of(
|
||||||
|
arguments("mention", NotificationType.mention),
|
||||||
|
arguments("status", NotificationType.status),
|
||||||
|
arguments("reblog", NotificationType.reblog),
|
||||||
|
arguments("follow", NotificationType.follow),
|
||||||
|
arguments("follow_request", NotificationType.follow_request),
|
||||||
|
arguments("favourite", NotificationType.favourite),
|
||||||
|
arguments("poll", NotificationType.poll),
|
||||||
|
arguments("update", NotificationType.update),
|
||||||
|
arguments("admin.sign_up", NotificationType.admin_sign_up),
|
||||||
|
arguments("admin.report", NotificationType.admin_report),
|
||||||
|
arguments("servered_relationships", NotificationType.severed_relationships)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
Loading…
Reference in New Issue