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) + } +}