From 0b3c27619e450e717ddc4221f92356e0071f9370 Mon Sep 17 00:00:00 2001 From: usbharu <64310155+usbharu@users.noreply.github.com> Date: Sat, 9 Dec 2023 21:09:52 +0900 Subject: [PATCH] =?UTF-8?q?feat:=20=E3=83=A6=E3=83=BC=E3=82=B6=E3=83=BC?= =?UTF-8?q?=E9=96=93=E3=81=AE=E9=96=A2=E4=BF=82=E3=82=92RelationshipServic?= =?UTF-8?q?e=E3=81=A7=E7=AE=A1=E7=90=86=E3=81=99=E3=82=8B=E3=82=88?= =?UTF-8?q?=E3=81=86=E3=81=AB?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../activity/accept/ApSendAcceptService.kt | 16 ++ .../activity/block/APSendBlockService.kt | 43 +++ .../activity/reject/ApSendRejectService.kt | 7 + .../activity/undo/APSendUndoService.kt | 8 + .../domain/model/relationship/Relationship.kt | 22 ++ .../relationship/RelationshipRepository.kt | 31 ++ .../relationship/RelationshipService.kt | 14 + .../relationship/RelationshipServiceImpl.kt | 266 ++++++++++++++++++ 8 files changed, 407 insertions(+) create mode 100644 src/main/kotlin/dev/usbharu/hideout/activitypub/service/activity/accept/ApSendAcceptService.kt create mode 100644 src/main/kotlin/dev/usbharu/hideout/activitypub/service/activity/block/APSendBlockService.kt create mode 100644 src/main/kotlin/dev/usbharu/hideout/activitypub/service/activity/reject/ApSendRejectService.kt create mode 100644 src/main/kotlin/dev/usbharu/hideout/activitypub/service/activity/undo/APSendUndoService.kt create mode 100644 src/main/kotlin/dev/usbharu/hideout/core/domain/model/relationship/Relationship.kt create mode 100644 src/main/kotlin/dev/usbharu/hideout/core/domain/model/relationship/RelationshipRepository.kt create mode 100644 src/main/kotlin/dev/usbharu/hideout/core/service/relationship/RelationshipService.kt create mode 100644 src/main/kotlin/dev/usbharu/hideout/core/service/relationship/RelationshipServiceImpl.kt diff --git a/src/main/kotlin/dev/usbharu/hideout/activitypub/service/activity/accept/ApSendAcceptService.kt b/src/main/kotlin/dev/usbharu/hideout/activitypub/service/activity/accept/ApSendAcceptService.kt new file mode 100644 index 00000000..59f4f4ec --- /dev/null +++ b/src/main/kotlin/dev/usbharu/hideout/activitypub/service/activity/accept/ApSendAcceptService.kt @@ -0,0 +1,16 @@ +package dev.usbharu.hideout.activitypub.service.activity.accept + +import dev.usbharu.hideout.core.domain.model.user.User +import org.springframework.stereotype.Service + +interface ApSendAcceptService { + suspend fun sendAccept(user: User, target: User) +} + +@Service +class ApSendAcceptServiceImpl : ApSendAcceptService { + override suspend fun sendAccept(user: User, target: User) { + TODO("Not yet implemented") + } + +} diff --git a/src/main/kotlin/dev/usbharu/hideout/activitypub/service/activity/block/APSendBlockService.kt b/src/main/kotlin/dev/usbharu/hideout/activitypub/service/activity/block/APSendBlockService.kt new file mode 100644 index 00000000..f814da7e --- /dev/null +++ b/src/main/kotlin/dev/usbharu/hideout/activitypub/service/activity/block/APSendBlockService.kt @@ -0,0 +1,43 @@ +package dev.usbharu.hideout.activitypub.service.activity.block + +import dev.usbharu.hideout.activitypub.domain.model.Block +import dev.usbharu.hideout.activitypub.domain.model.Follow +import dev.usbharu.hideout.activitypub.domain.model.Reject +import dev.usbharu.hideout.application.config.ApplicationConfig +import dev.usbharu.hideout.core.domain.model.user.User +import dev.usbharu.hideout.core.external.job.DeliverBlockJob +import dev.usbharu.hideout.core.external.job.DeliverBlockJobParam +import dev.usbharu.hideout.core.service.job.JobQueueParentService +import org.springframework.stereotype.Service + +interface APSendBlockService { + suspend fun sendBlock(user: User, target: User) +} + +@Service +class ApSendBlockServiceImpl( + private val applicationConfig: ApplicationConfig, + private val jobQueueParentService: JobQueueParentService, + private val deliverBlockJob: DeliverBlockJob +) : APSendBlockService { + override suspend fun sendBlock(user: User, target: User) { + val blockJobParam = DeliverBlockJobParam( + user.id, + Block( + user.url, + "${applicationConfig.url}/block/${user.id}/${target.id}", + target.url + ), + Reject( + user.url, + "${applicationConfig.url}/reject/${user.id}/${target.id}", + Follow( + apObject = user.url, + actor = target.url + ) + ), + target.inbox + ) + jobQueueParentService.scheduleTypeSafe(deliverBlockJob, blockJobParam) + } +} diff --git a/src/main/kotlin/dev/usbharu/hideout/activitypub/service/activity/reject/ApSendRejectService.kt b/src/main/kotlin/dev/usbharu/hideout/activitypub/service/activity/reject/ApSendRejectService.kt new file mode 100644 index 00000000..29c66e28 --- /dev/null +++ b/src/main/kotlin/dev/usbharu/hideout/activitypub/service/activity/reject/ApSendRejectService.kt @@ -0,0 +1,7 @@ +package dev.usbharu.hideout.activitypub.service.activity.reject + +import dev.usbharu.hideout.core.domain.model.user.User + +interface ApSendRejectService { + suspend fun sendReject(user: User, target: User) +} diff --git a/src/main/kotlin/dev/usbharu/hideout/activitypub/service/activity/undo/APSendUndoService.kt b/src/main/kotlin/dev/usbharu/hideout/activitypub/service/activity/undo/APSendUndoService.kt new file mode 100644 index 00000000..827b186e --- /dev/null +++ b/src/main/kotlin/dev/usbharu/hideout/activitypub/service/activity/undo/APSendUndoService.kt @@ -0,0 +1,8 @@ +package dev.usbharu.hideout.activitypub.service.activity.undo + +import dev.usbharu.hideout.core.domain.model.user.User + +interface APSendUndoService { + suspend fun sendUndoFollow(user: User, target: User) + suspend fun sendUndoBlock(user: User, target: User) +} diff --git a/src/main/kotlin/dev/usbharu/hideout/core/domain/model/relationship/Relationship.kt b/src/main/kotlin/dev/usbharu/hideout/core/domain/model/relationship/Relationship.kt new file mode 100644 index 00000000..51a3da60 --- /dev/null +++ b/src/main/kotlin/dev/usbharu/hideout/core/domain/model/relationship/Relationship.kt @@ -0,0 +1,22 @@ +package dev.usbharu.hideout.core.domain.model.relationship + +/** + * ユーザーとの関係を表します + * + * @property userId ユーザー + * @property targetUserId 相手ユーザー + * @property following フォローしているか + * @property blocking ブロックしているか + * @property muting ミュートしているか + * @property followRequest フォローリクエストを送っているか + * @property ignoreFollowRequestFromTarget フォローリクエストを無視しているか + */ +data class Relationship( + val userId: Long, + val targetUserId: Long, + val following: Boolean, + val blocking: Boolean, + val muting: Boolean, + val followRequest: Boolean, + val ignoreFollowRequestFromTarget: Boolean +) diff --git a/src/main/kotlin/dev/usbharu/hideout/core/domain/model/relationship/RelationshipRepository.kt b/src/main/kotlin/dev/usbharu/hideout/core/domain/model/relationship/RelationshipRepository.kt new file mode 100644 index 00000000..4b3fc6bc --- /dev/null +++ b/src/main/kotlin/dev/usbharu/hideout/core/domain/model/relationship/RelationshipRepository.kt @@ -0,0 +1,31 @@ +package dev.usbharu.hideout.core.domain.model.relationship + +/** + * [Relationship]の永続化 + * + */ +interface RelationshipRepository { + /** + * 永続化します + * + * @param relationship 永続化する[Relationship] + * @return 永続化された[Relationship] + */ + suspend fun save(relationship: Relationship): Relationship + + /** + * 永続化されたものを削除します + * + * @param relationship 削除する[Relationship] + */ + suspend fun delete(relationship: Relationship) + + /** + * userIdとtargetUserIdで[Relationship]を取得します + * + * @param userId 取得するユーザーID + * @param targetUserId 対象ユーザーID + * @return 取得された[Relationship] 存在しない場合nullが返ります + */ + suspend fun findByUserIdAndTargetUserId(userId: Long, targetUserId: Long): Relationship? +} diff --git a/src/main/kotlin/dev/usbharu/hideout/core/service/relationship/RelationshipService.kt b/src/main/kotlin/dev/usbharu/hideout/core/service/relationship/RelationshipService.kt new file mode 100644 index 00000000..4f0a164a --- /dev/null +++ b/src/main/kotlin/dev/usbharu/hideout/core/service/relationship/RelationshipService.kt @@ -0,0 +1,14 @@ +package dev.usbharu.hideout.core.service.relationship + +interface RelationshipService { + suspend fun followRequest(userId: Long, targetId: Long) + suspend fun block(userId: Long, targetId: Long) + suspend fun acceptFollowRequest(userId: Long, targetId: Long) + suspend fun rejectFollowRequest(userId: Long, targetId: Long) + suspend fun ignoreFollowRequest(userId: Long, targetId: Long) + suspend fun unfollow(userId: Long, targetId: Long) + suspend fun unblock(userId: Long, targetId: Long) + suspend fun mute(userId: Long, targetId: Long) + suspend fun unmute(userId: Long, targetId: Long) + +} diff --git a/src/main/kotlin/dev/usbharu/hideout/core/service/relationship/RelationshipServiceImpl.kt b/src/main/kotlin/dev/usbharu/hideout/core/service/relationship/RelationshipServiceImpl.kt new file mode 100644 index 00000000..1ee53312 --- /dev/null +++ b/src/main/kotlin/dev/usbharu/hideout/core/service/relationship/RelationshipServiceImpl.kt @@ -0,0 +1,266 @@ +package dev.usbharu.hideout.core.service.relationship + +import dev.usbharu.hideout.activitypub.service.activity.accept.ApSendAcceptService +import dev.usbharu.hideout.activitypub.service.activity.block.APSendBlockService +import dev.usbharu.hideout.activitypub.service.activity.follow.APSendFollowService +import dev.usbharu.hideout.activitypub.service.activity.reject.ApSendRejectService +import dev.usbharu.hideout.activitypub.service.activity.undo.APSendUndoService +import dev.usbharu.hideout.application.config.ApplicationConfig +import dev.usbharu.hideout.core.domain.exception.FailedToGetResourcesException +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.user.User +import dev.usbharu.hideout.core.query.UserQueryService +import dev.usbharu.hideout.core.service.follow.SendFollowDto +import org.slf4j.LoggerFactory +import org.springframework.stereotype.Service + +@Service +class RelationshipServiceImpl( + private val applicationConfig: ApplicationConfig, + private val userQueryService: UserQueryService, + private val relationshipRepository: RelationshipRepository, + private val apSendFollowService: APSendFollowService, + private val apSendBlockService: APSendBlockService, + private val apSendAcceptService: ApSendAcceptService, + private val apSendRejectService: ApSendRejectService, + private val apSendUndoService: APSendUndoService +) : RelationshipService { + override suspend fun followRequest(userId: Long, targetId: Long) { + val relationship = + relationshipRepository.findByUserIdAndTargetUserId(userId, targetId)?.copy(followRequest = true) + ?: Relationship( + userId = userId, + targetUserId = targetId, + following = false, + blocking = false, + muting = false, + followRequest = true, + ignoreFollowRequestFromTarget = false + ) + + val inverseRelationship = relationshipRepository.findByUserIdAndTargetUserId(targetId, userId) ?: Relationship( + userId = targetId, + targetUserId = userId, + following = false, + blocking = false, + muting = false, + followRequest = false, + ignoreFollowRequestFromTarget = false + ) + + if (inverseRelationship.blocking) { + logger.debug("FAILED Blocked by target. userId: {} targetId: {}", userId, targetId) + return + } + + if (relationship.blocking) { + logger.debug("FAILED Blocking user. userId: {} targetId: {}", userId, targetId) + return + } + if (inverseRelationship.ignoreFollowRequestFromTarget) { + logger.debug("SUCCESS Ignore Follow Request. userId: {} targetId: {}", userId, targetId) + return + } + + + relationshipRepository.save(relationship) + + val remoteUser = isRemoteUser(targetId) + + if (remoteUser != null) { + val user = userQueryService.findById(userId) + apSendFollowService.sendFollow(SendFollowDto(user, remoteUser)) + } else { + //TODO: フォロー許可制ユーザーを実装したら消す + acceptFollowRequest(userId, targetId) + } + + } + + override suspend fun block(userId: Long, targetId: Long) { + val relationship = relationshipRepository.findByUserIdAndTargetUserId(userId, targetId) + ?.copy(blocking = true, followRequest = false, following = false) ?: Relationship( + userId = userId, + targetUserId = targetId, + following = false, + blocking = true, + muting = false, + followRequest = false, + ignoreFollowRequestFromTarget = false + ) + + relationshipRepository.save(relationship) + + val remoteUser = isRemoteUser(targetId) + + if (remoteUser != null) { + val user = userQueryService.findById(userId) + apSendBlockService.sendBlock(user, remoteUser) + } + } + + override suspend fun acceptFollowRequest(userId: Long, targetId: Long) { + val relationship = relationshipRepository.findByUserIdAndTargetUserId(userId, targetId) + + if (relationship == null) { + logger.warn("FAILED Follow Request Not Found. (Relationship) userId: {} targetId: {}", userId, targetId) + return + } + + if (relationship.followRequest.not()) { + logger.warn("FAILED Follow Request Not Found. (Follow Request) userId: {} targetId: {}", userId, targetId) + return + } + + if (relationship.blocking) { + logger.warn("FAILED Blocking user userId: {} targetId: {}", userId, targetId) + throw IllegalStateException("Cannot accept a follow request from a blocked user. userId: $userId targetId: $targetId") + } + + val copy = relationship.copy(followRequest = false, following = true, blocking = false) + + relationshipRepository.save(copy) + + val remoteUser = isRemoteUser(targetId) + + if (remoteUser != null) { + val user = userQueryService.findById(userId) + apSendAcceptService.sendAccept(user, remoteUser) + } + } + + override suspend fun rejectFollowRequest(userId: Long, targetId: Long) { + val relationship = relationshipRepository.findByUserIdAndTargetUserId(userId, targetId) + + if (relationship == null) { + logger.warn("FAILED Follow Request Not Found. (Relationship) userId: {} targetId: {}", userId, targetId) + return + } + + if (relationship.followRequest.not()) { + logger.warn("FAILED Follow Request Not Found. (Follow Request) userId: {} targetId: {}", userId, targetId) + return + } + + val copy = relationship.copy(followRequest = false, following = false, blocking = false) + + relationshipRepository.save(copy) + + val remoteUser = isRemoteUser(targetId) + + if (remoteUser != null) { + val user = userQueryService.findById(userId) + apSendRejectService.sendReject(user, remoteUser) + } + } + + override suspend fun ignoreFollowRequest(userId: Long, targetId: Long) { + val relationship = relationshipRepository.findByUserIdAndTargetUserId(userId, targetId) + ?.copy(ignoreFollowRequestFromTarget = true) + ?: Relationship( + userId = userId, + targetUserId = targetId, + following = false, + blocking = false, + muting = false, + followRequest = false, + ignoreFollowRequestFromTarget = true + ) + + relationshipRepository.save(relationship) + } + + override suspend fun unfollow(userId: Long, targetId: Long) { + val relationship = relationshipRepository.findByUserIdAndTargetUserId(userId, targetId) + + if (relationship == null) { + logger.warn("FAILED Unfollow. (Relationship) userId: {} targetId: {}", userId, targetId) + return + } + + if (relationship.following.not()) { + logger.warn("SUCCESS User already unfollow. userId: {} targetId: {}", userId, targetId) + return + } + + val copy = relationship.copy(following = false) + + relationshipRepository.save(copy) + + val remoteUser = isRemoteUser(targetId) + + if (remoteUser != null) { + val user = userQueryService.findById(userId) + apSendUndoService.sendUndoFollow(user, remoteUser) + } + } + + override suspend fun unblock(userId: Long, targetId: Long) { + val relationship = relationshipRepository.findByUserIdAndTargetUserId(userId, targetId) + + if (relationship == null) { + logger.warn("FAILED Unblock. (Relationship) userId: {} targetId: {}", userId, targetId) + return + } + + if (relationship.blocking.not()) { + logger.warn("SUCCESS User is not blocking. userId: {] targetId: {}", userId, targetId) + return + } + + val copy = relationship.copy(blocking = false) + relationshipRepository.save(copy) + + + val remoteUser = isRemoteUser(targetId) + if (remoteUser == null) { + val user = userQueryService.findById(userId) + apSendUndoService.sendUndoBlock(user, targetId) + } + } + + override suspend fun mute(userId: Long, targetId: Long) { + val relationship = relationshipRepository.findByUserIdAndTargetUserId(userId, targetId)?.copy(muting = true) + ?: Relationship( + userId = userId, + targetUserId = targetId, + following = false, + blocking = false, + muting = true, + followRequest = false, + ignoreFollowRequestFromTarget = false + ) + + relationshipRepository.save(relationship) + } + + override suspend fun unmute(userId: Long, targetId: Long) { + val relationship = relationshipRepository.findByUserIdAndTargetUserId(userId, targetId)?.copy(muting = false) + + if (relationship == null) { + logger.warn("FAILED Mute. (Relationship) userId: {} targetId: {}", userId, targetId) + return + } + + relationshipRepository.save(relationship) + } + + private suspend fun isRemoteUser(userId: Long): User? { + val user = try { + userQueryService.findById(userId) + } catch (e: FailedToGetResourcesException) { + logger.warn("User not found.", e) + throw IllegalStateException("User not found.", e) + } + + if (user.domain == applicationConfig.url.host) { + return null + } + return user + } + + companion object { + private val logger = LoggerFactory.getLogger(RelationshipServiceImpl::class.java) + } +}