diff --git a/build.gradle.kts b/build.gradle.kts index c8d31ca7..9c892615 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -287,7 +287,7 @@ project.gradle.taskGraph.whenReady { kover { excludeSourceSets { - names("aot") + names("aot", "e2eTest", "intTest") } } diff --git a/src/intTest/resources/sql/note/httpSignature認証でフォロワーがfollowers投稿を取得できる.sql b/src/intTest/resources/sql/note/httpSignature認証でフォロワーがfollowers投稿を取得できる.sql index a01f34bb..d3076eb1 100644 --- a/src/intTest/resources/sql/note/httpSignature認証でフォロワーがfollowers投稿を取得できる.sql +++ b/src/intTest/resources/sql/note/httpSignature認証でフォロワーがfollowers投稿を取得できる.sql @@ -21,8 +21,9 @@ VALUES (9, 'test-user9', 'follower.example.com', 'Im test-user9.', 'THis account 'https://follower.example.com/users/test-user9/following', 'https://follower.example.com/users/test-user9/followers', null); -insert into USERS_FOLLOWERS (USER_ID, FOLLOWER_ID) -VALUES (8, 9); +insert into relationships (user_id, target_user_id, following, blocking, muting, follow_request, + ignore_follow_request) +VALUES (9, 8, true, false, false, false, false); insert into POSTS (ID, USER_ID, OVERVIEW, TEXT, CREATED_AT, VISIBILITY, URL, REPLY_ID, REPOST_ID, SENSITIVE, AP_ID) VALUES (1239, 8, null, 'test post', 12345680, 2, 'https://example.com/users/test-user8/posts/1239', null, null, false, diff --git a/src/intTest/resources/sql/note/httpSignature認証でフォロワーがpublic投稿を取得できる.sql b/src/intTest/resources/sql/note/httpSignature認証でフォロワーがpublic投稿を取得できる.sql index 6c78ae20..82d37cc0 100644 --- a/src/intTest/resources/sql/note/httpSignature認証でフォロワーがpublic投稿を取得できる.sql +++ b/src/intTest/resources/sql/note/httpSignature認証でフォロワーがpublic投稿を取得できる.sql @@ -21,8 +21,9 @@ VALUES (5, 'test-user5', 'follower.example.com', 'Im test user5.', 'THis account 'https://follower.example.com/users/test-user5/following', 'https://follower.example.com/users/test-user5/followers', null); -insert into USERS_FOLLOWERS (USER_ID, FOLLOWER_ID) -VALUES (4, 5); +insert into relationships (user_id, target_user_id, following, blocking, muting, follow_request, + ignore_follow_request) +VALUES (5, 4, true, false, false, false, false); insert into POSTS (ID, "USER_ID", OVERVIEW, TEXT, "CREATED_AT", VISIBILITY, URL, "REPOST_ID", "REPLY_ID", SENSITIVE, AP_ID) diff --git a/src/intTest/resources/sql/note/httpSignature認証でフォロワーがunlisted投稿を取得できる.sql b/src/intTest/resources/sql/note/httpSignature認証でフォロワーがunlisted投稿を取得できる.sql index 94de8f2e..cce85d78 100644 --- a/src/intTest/resources/sql/note/httpSignature認証でフォロワーがunlisted投稿を取得できる.sql +++ b/src/intTest/resources/sql/note/httpSignature認証でフォロワーがunlisted投稿を取得できる.sql @@ -21,8 +21,9 @@ VALUES (7, 'test-user7', 'follower.example.com', 'Im test-user7.', 'THis account 'https://follower.example.com/users/test-user7/following', 'https://follower.example.com/users/test-user7/followers', null); -insert into USERS_FOLLOWERS (USER_ID, FOLLOWER_ID) -VALUES (6, 7); +insert into relationships (user_id, target_user_id, following, blocking, muting, follow_request, + ignore_follow_request) +VALUES (7, 6, true, false, false, false, false); insert into POSTS (ID, "USER_ID", OVERVIEW, TEXT, "CREATED_AT", VISIBILITY, URL, "REPOST_ID", "REPLY_ID", SENSITIVE, AP_ID) diff --git a/src/main/kotlin/dev/usbharu/hideout/activitypub/domain/model/Accept.kt b/src/main/kotlin/dev/usbharu/hideout/activitypub/domain/model/Accept.kt index 2cd730db..67a8ed0f 100644 --- a/src/main/kotlin/dev/usbharu/hideout/activitypub/domain/model/Accept.kt +++ b/src/main/kotlin/dev/usbharu/hideout/activitypub/domain/model/Accept.kt @@ -8,7 +8,6 @@ import dev.usbharu.hideout.activitypub.domain.model.objects.ObjectDeserializer open class Accept @JsonCreator constructor( type: List = emptyList(), - override val name: String, @JsonDeserialize(using = ObjectDeserializer::class) @JsonProperty("object") val apObject: Object, @@ -16,9 +15,7 @@ open class Accept @JsonCreator constructor( ) : Object( type = add(type, "Accept") ), - HasActor, - HasName { - + HasActor { override fun equals(other: Any?): Boolean { if (this === other) return true if (javaClass != other?.javaClass) return false @@ -26,7 +23,6 @@ open class Accept @JsonCreator constructor( other as Accept - if (name != other.name) return false if (apObject != other.apObject) return false if (actor != other.actor) return false @@ -35,7 +31,6 @@ open class Accept @JsonCreator constructor( override fun hashCode(): Int { var result = super.hashCode() - result = 31 * result + name.hashCode() result = 31 * result + apObject.hashCode() result = 31 * result + actor.hashCode() return result @@ -43,7 +38,6 @@ open class Accept @JsonCreator constructor( override fun toString(): String { return "Accept(" + - "name='$name', " + "apObject=$apObject, " + "actor='$actor'" + ")" + diff --git a/src/main/kotlin/dev/usbharu/hideout/activitypub/domain/model/Block.kt b/src/main/kotlin/dev/usbharu/hideout/activitypub/domain/model/Block.kt new file mode 100644 index 00000000..88a7b319 --- /dev/null +++ b/src/main/kotlin/dev/usbharu/hideout/activitypub/domain/model/Block.kt @@ -0,0 +1,43 @@ +package dev.usbharu.hideout.activitypub.domain.model + +import com.fasterxml.jackson.annotation.JsonProperty +import dev.usbharu.hideout.activitypub.domain.model.objects.Object + +open class Block( + override val actor: String, + override val id: String, + @JsonProperty("object") val apObject: String +) : + Object(listOf("Block")), HasId, HasActor { + + override fun equals(other: Any?): Boolean { + if (this === other) return true + if (javaClass != other?.javaClass) return false + if (!super.equals(other)) return false + + other as Block + + if (actor != other.actor) return false + if (id != other.id) return false + if (apObject != other.apObject) return false + + return true + } + + override fun hashCode(): Int { + var result = super.hashCode() + result = 31 * result + actor.hashCode() + result = 31 * result + id.hashCode() + result = 31 * result + apObject.hashCode() + return result + } + + override fun toString(): String { + return "Block(" + + "actor='$actor', " + + "id='$id', " + + "apObject='$apObject'" + + ")" + + " ${super.toString()}" + } +} diff --git a/src/main/kotlin/dev/usbharu/hideout/activitypub/domain/model/Create.kt b/src/main/kotlin/dev/usbharu/hideout/activitypub/domain/model/Create.kt index d5b269dd..2cbc9a3c 100644 --- a/src/main/kotlin/dev/usbharu/hideout/activitypub/domain/model/Create.kt +++ b/src/main/kotlin/dev/usbharu/hideout/activitypub/domain/model/Create.kt @@ -7,7 +7,7 @@ import dev.usbharu.hideout.activitypub.domain.model.objects.ObjectDeserializer open class Create( type: List = emptyList(), - override val name: String, + val name: String? = null, @JsonDeserialize(using = ObjectDeserializer::class) @JsonProperty("object") val apObject: Object, @@ -19,7 +19,6 @@ open class Create( type = add(type, "Create") ), HasId, - HasName, HasActor { override fun equals(other: Any?): Boolean { @@ -41,7 +40,7 @@ open class Create( override fun hashCode(): Int { var result = super.hashCode() - result = 31 * result + name.hashCode() + result = 31 * result + (name?.hashCode() ?: 0) result = 31 * result + apObject.hashCode() result = 31 * result + actor.hashCode() result = 31 * result + id.hashCode() @@ -52,7 +51,7 @@ open class Create( override fun toString(): String { return "Create(" + - "name='$name', " + + "name=$name, " + "apObject=$apObject, " + "actor='$actor', " + "id='$id', " + diff --git a/src/main/kotlin/dev/usbharu/hideout/activitypub/domain/model/Reject.kt b/src/main/kotlin/dev/usbharu/hideout/activitypub/domain/model/Reject.kt new file mode 100644 index 00000000..82cb5b8a --- /dev/null +++ b/src/main/kotlin/dev/usbharu/hideout/activitypub/domain/model/Reject.kt @@ -0,0 +1,43 @@ +package dev.usbharu.hideout.activitypub.domain.model + +import com.fasterxml.jackson.annotation.JsonProperty +import com.fasterxml.jackson.databind.annotation.JsonDeserialize +import dev.usbharu.hideout.activitypub.domain.model.objects.Object +import dev.usbharu.hideout.activitypub.domain.model.objects.ObjectDeserializer + +open class Reject( + override val actor: String, + override val id: String, + @JsonDeserialize(using = ObjectDeserializer::class) @JsonProperty("object") val apObject: Object +) : Object(listOf("Reject")), HasId, HasActor { + override fun equals(other: Any?): Boolean { + if (this === other) return true + if (javaClass != other?.javaClass) return false + if (!super.equals(other)) return false + + other as Reject + + if (actor != other.actor) return false + if (id != other.id) return false + if (apObject != other.apObject) return false + + return true + } + + override fun hashCode(): Int { + var result = super.hashCode() + result = 31 * result + actor.hashCode() + result = 31 * result + id.hashCode() + result = 31 * result + apObject.hashCode() + return result + } + + override fun toString(): String { + return "Reject(" + + "actor='$actor', " + + "id='$id', " + + "apObject=$apObject" + + ")" + + " ${super.toString()}" + } +} diff --git a/src/main/kotlin/dev/usbharu/hideout/activitypub/domain/model/Undo.kt b/src/main/kotlin/dev/usbharu/hideout/activitypub/domain/model/Undo.kt index 01dbc17c..b1399777 100644 --- a/src/main/kotlin/dev/usbharu/hideout/activitypub/domain/model/Undo.kt +++ b/src/main/kotlin/dev/usbharu/hideout/activitypub/domain/model/Undo.kt @@ -1,5 +1,6 @@ package dev.usbharu.hideout.activitypub.domain.model +import com.fasterxml.jackson.annotation.JsonProperty import com.fasterxml.jackson.databind.annotation.JsonDeserialize import dev.usbharu.hideout.activitypub.domain.model.objects.Object import dev.usbharu.hideout.activitypub.domain.model.objects.ObjectDeserializer @@ -9,7 +10,7 @@ open class Undo( override val actor: String, override val id: String, @JsonDeserialize(using = ObjectDeserializer::class) - @Suppress("VariableNaming") val `object`: Object, + @JsonProperty("object") val apObject: Object, val published: String ) : Object(add(type, "Undo")), HasId, HasActor { @@ -20,7 +21,7 @@ open class Undo( other as Undo - if (`object` != other.`object`) return false + if (apObject != other.apObject) return false if (published != other.published) return false if (actor != other.actor) return false if (id != other.id) return false @@ -30,7 +31,7 @@ open class Undo( override fun hashCode(): Int { var result = super.hashCode() - result = 31 * result + `object`.hashCode() + result = 31 * result + apObject.hashCode() result = 31 * result + published.hashCode() result = 31 * result + actor.hashCode() result = 31 * result + id.hashCode() @@ -38,5 +39,5 @@ open class Undo( } override fun toString(): String = - "Undo(`object`=$`object`, published=$published, actor='$actor', id='$id') ${super.toString()}" + "Undo(`object`=$apObject, published=$published, actor='$actor', id='$id') ${super.toString()}" } diff --git a/src/main/kotlin/dev/usbharu/hideout/activitypub/domain/model/objects/ObjectDeserializer.kt b/src/main/kotlin/dev/usbharu/hideout/activitypub/domain/model/objects/ObjectDeserializer.kt index f28070e6..caa6caff 100644 --- a/src/main/kotlin/dev/usbharu/hideout/activitypub/domain/model/objects/ObjectDeserializer.kt +++ b/src/main/kotlin/dev/usbharu/hideout/activitypub/domain/model/objects/ObjectDeserializer.kt @@ -44,7 +44,7 @@ class ObjectDeserializer : JsonDeserializer() { ExtendedActivityVocabulary.Add -> TODO() ExtendedActivityVocabulary.Announce -> TODO() ExtendedActivityVocabulary.Arrive -> TODO() - ExtendedActivityVocabulary.Block -> TODO() + ExtendedActivityVocabulary.Block -> p.codec.treeToValue(treeNode, Block::class.java) ExtendedActivityVocabulary.Create -> p.codec.treeToValue(treeNode, Create::class.java) ExtendedActivityVocabulary.Delete -> p.codec.treeToValue(treeNode, Delete::class.java) ExtendedActivityVocabulary.Dislike -> TODO() @@ -58,7 +58,7 @@ class ObjectDeserializer : JsonDeserializer() { ExtendedActivityVocabulary.Move -> TODO() ExtendedActivityVocabulary.Offer -> TODO() ExtendedActivityVocabulary.Question -> TODO() - ExtendedActivityVocabulary.Reject -> TODO() + ExtendedActivityVocabulary.Reject -> p.codec.treeToValue(treeNode, Reject::class.java) ExtendedActivityVocabulary.Read -> TODO() ExtendedActivityVocabulary.Remove -> TODO() ExtendedActivityVocabulary.TentativeReject -> TODO() diff --git a/src/main/kotlin/dev/usbharu/hideout/activitypub/service/activity/accept/APDeliverAcceptJobProcessor.kt b/src/main/kotlin/dev/usbharu/hideout/activitypub/service/activity/accept/APDeliverAcceptJobProcessor.kt new file mode 100644 index 00000000..343dcb50 --- /dev/null +++ b/src/main/kotlin/dev/usbharu/hideout/activitypub/service/activity/accept/APDeliverAcceptJobProcessor.kt @@ -0,0 +1,24 @@ +package dev.usbharu.hideout.activitypub.service.activity.accept + +import dev.usbharu.hideout.activitypub.service.common.APRequestService +import dev.usbharu.hideout.application.external.Transaction +import dev.usbharu.hideout.core.external.job.DeliverAcceptJob +import dev.usbharu.hideout.core.external.job.DeliverAcceptJobParam +import dev.usbharu.hideout.core.query.UserQueryService +import dev.usbharu.hideout.core.service.job.JobProcessor +import org.springframework.stereotype.Service + +@Service +class APDeliverAcceptJobProcessor( + private val apRequestService: APRequestService, + private val userQueryService: UserQueryService, + private val deliverAcceptJob: DeliverAcceptJob, + private val transaction: Transaction +) : + JobProcessor { + override suspend fun process(param: DeliverAcceptJobParam): Unit = transaction.transaction { + apRequestService.apPost(param.inbox, param.accept, userQueryService.findById(param.signer)) + } + + override fun job(): DeliverAcceptJob = deliverAcceptJob +} diff --git a/src/main/kotlin/dev/usbharu/hideout/activitypub/service/activity/accept/ApAcceptProcessor.kt b/src/main/kotlin/dev/usbharu/hideout/activitypub/service/activity/accept/ApAcceptProcessor.kt index 2ee0f07e..24a54ef5 100644 --- a/src/main/kotlin/dev/usbharu/hideout/activitypub/service/activity/accept/ApAcceptProcessor.kt +++ b/src/main/kotlin/dev/usbharu/hideout/activitypub/service/activity/accept/ApAcceptProcessor.kt @@ -7,22 +7,20 @@ import dev.usbharu.hideout.activitypub.service.common.AbstractActivityPubProcess import dev.usbharu.hideout.activitypub.service.common.ActivityPubProcessContext import dev.usbharu.hideout.activitypub.service.common.ActivityType import dev.usbharu.hideout.application.external.Transaction -import dev.usbharu.hideout.core.query.FollowerQueryService import dev.usbharu.hideout.core.query.UserQueryService -import dev.usbharu.hideout.core.service.user.UserService +import dev.usbharu.hideout.core.service.relationship.RelationshipService import org.springframework.stereotype.Service @Service class ApAcceptProcessor( transaction: Transaction, private val userQueryService: UserQueryService, - private val followerQueryService: FollowerQueryService, - private val userService: UserService + private val relationshipService: RelationshipService ) : AbstractActivityPubProcessor(transaction) { override suspend fun internalProcess(activity: ActivityPubProcessContext) { - val value = activity.activity.apObject ?: throw IllegalActivityPubObjectException("object is null") + val value = activity.activity.apObject if (value.type.contains("Follow").not()) { logger.warn("FAILED Activity type isn't Follow.") @@ -37,13 +35,8 @@ class ApAcceptProcessor( val user = userQueryService.findByUrl(userUrl) val follower = userQueryService.findByUrl(followerUrl) - if (followerQueryService.alreadyFollow(user.id, follower.id)) { - logger.debug("END User already follow from ${follower.url} to ${user.url}.") - return - } - - userService.follow(user.id, follower.id) - logger.debug("SUCCESS Follow from ${follower.url} to ${user.url}.") + relationshipService.acceptFollowRequest(user.id, follower.id) + logger.debug("SUCCESS Follow from ${user.url} to ${follower.url}.") } override fun isSupported(activityType: ActivityType): Boolean = activityType == ActivityType.Accept 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..35270235 --- /dev/null +++ b/src/main/kotlin/dev/usbharu/hideout/activitypub/service/activity/accept/ApSendAcceptService.kt @@ -0,0 +1,35 @@ +package dev.usbharu.hideout.activitypub.service.activity.accept + +import dev.usbharu.hideout.activitypub.domain.model.Accept +import dev.usbharu.hideout.activitypub.domain.model.Follow +import dev.usbharu.hideout.core.domain.model.user.User +import dev.usbharu.hideout.core.external.job.DeliverAcceptJob +import dev.usbharu.hideout.core.external.job.DeliverAcceptJobParam +import dev.usbharu.hideout.core.service.job.JobQueueParentService +import org.springframework.stereotype.Service + +interface ApSendAcceptService { + suspend fun sendAcceptFollow(user: User, target: User) +} + +@Service +class ApSendAcceptServiceImpl( + private val jobQueueParentService: JobQueueParentService, + private val deliverAcceptJob: DeliverAcceptJob +) : ApSendAcceptService { + override suspend fun sendAcceptFollow(user: User, target: User) { + val deliverAcceptJobParam = DeliverAcceptJobParam( + Accept( + apObject = Follow( + apObject = user.url, + actor = target.url + ), + actor = user.url + ), + target.inbox, + user.id + ) + + jobQueueParentService.scheduleTypeSafe(deliverAcceptJob, deliverAcceptJobParam) + } +} diff --git a/src/main/kotlin/dev/usbharu/hideout/activitypub/service/activity/block/APDeliverBlockJobProcessor.kt b/src/main/kotlin/dev/usbharu/hideout/activitypub/service/activity/block/APDeliverBlockJobProcessor.kt new file mode 100644 index 00000000..a97a0f19 --- /dev/null +++ b/src/main/kotlin/dev/usbharu/hideout/activitypub/service/activity/block/APDeliverBlockJobProcessor.kt @@ -0,0 +1,36 @@ +package dev.usbharu.hideout.activitypub.service.activity.block + +import dev.usbharu.hideout.activitypub.service.common.APRequestService +import dev.usbharu.hideout.application.external.Transaction +import dev.usbharu.hideout.core.domain.model.user.UserRepository +import dev.usbharu.hideout.core.external.job.DeliverBlockJob +import dev.usbharu.hideout.core.external.job.DeliverBlockJobParam +import dev.usbharu.hideout.core.service.job.JobProcessor +import org.springframework.stereotype.Service + +/** + * ブロックアクティビティ配送を処理します + */ +@Service +class APDeliverBlockJobProcessor( + private val apRequestService: APRequestService, + private val userRepository: UserRepository, + private val transaction: Transaction, + private val deliverBlockJob: DeliverBlockJob +) : JobProcessor { + override suspend fun process(param: DeliverBlockJobParam): Unit = transaction.transaction { + val signer = userRepository.findById(param.signer) + apRequestService.apPost( + param.inbox, + param.reject, + signer + ) + apRequestService.apPost( + param.inbox, + param.block, + signer + ) + } + + override fun job(): DeliverBlockJob = deliverBlockJob +} 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/block/BlockActivityPubProcessor.kt b/src/main/kotlin/dev/usbharu/hideout/activitypub/service/activity/block/BlockActivityPubProcessor.kt new file mode 100644 index 00000000..075bf893 --- /dev/null +++ b/src/main/kotlin/dev/usbharu/hideout/activitypub/service/activity/block/BlockActivityPubProcessor.kt @@ -0,0 +1,31 @@ +package dev.usbharu.hideout.activitypub.service.activity.block + +import dev.usbharu.hideout.activitypub.domain.model.Block +import dev.usbharu.hideout.activitypub.service.common.AbstractActivityPubProcessor +import dev.usbharu.hideout.activitypub.service.common.ActivityPubProcessContext +import dev.usbharu.hideout.activitypub.service.common.ActivityType +import dev.usbharu.hideout.application.external.Transaction +import dev.usbharu.hideout.core.query.UserQueryService +import dev.usbharu.hideout.core.service.relationship.RelationshipService +import org.springframework.stereotype.Service + +/** + * ブロックアクティビティを処理します + */ +@Service +class BlockActivityPubProcessor( + private val userQueryService: UserQueryService, + private val relationshipService: RelationshipService, + transaction: Transaction +) : + AbstractActivityPubProcessor(transaction) { + override suspend fun internalProcess(activity: ActivityPubProcessContext) { + val user = userQueryService.findByUrl(activity.activity.actor) + val target = userQueryService.findByUrl(activity.activity.apObject) + relationshipService.block(user.id, target.id) + } + + override fun isSupported(activityType: ActivityType): Boolean = activityType == ActivityType.Block + + override fun type(): Class = Block::class.java +} diff --git a/src/main/kotlin/dev/usbharu/hideout/activitypub/service/activity/follow/APReceiveFollowJobProcessor.kt b/src/main/kotlin/dev/usbharu/hideout/activitypub/service/activity/follow/APReceiveFollowJobProcessor.kt index 0c3957b1..1f79af46 100644 --- a/src/main/kotlin/dev/usbharu/hideout/activitypub/service/activity/follow/APReceiveFollowJobProcessor.kt +++ b/src/main/kotlin/dev/usbharu/hideout/activitypub/service/activity/follow/APReceiveFollowJobProcessor.kt @@ -2,16 +2,14 @@ package dev.usbharu.hideout.activitypub.service.activity.follow import com.fasterxml.jackson.databind.ObjectMapper import com.fasterxml.jackson.module.kotlin.readValue -import dev.usbharu.hideout.activitypub.domain.model.Accept import dev.usbharu.hideout.activitypub.domain.model.Follow -import dev.usbharu.hideout.activitypub.service.common.APRequestService import dev.usbharu.hideout.activitypub.service.objects.user.APUserService import dev.usbharu.hideout.application.external.Transaction import dev.usbharu.hideout.core.external.job.ReceiveFollowJob import dev.usbharu.hideout.core.external.job.ReceiveFollowJobParam import dev.usbharu.hideout.core.query.UserQueryService import dev.usbharu.hideout.core.service.job.JobProcessor -import dev.usbharu.hideout.core.service.user.UserService +import dev.usbharu.hideout.core.service.relationship.RelationshipService import org.slf4j.LoggerFactory import org.springframework.stereotype.Service @@ -21,35 +19,21 @@ class APReceiveFollowJobProcessor( private val userQueryService: UserQueryService, private val apUserService: APUserService, private val objectMapper: ObjectMapper, - private val apRequestService: APRequestService, - private val userService: UserService + private val relationshipService: RelationshipService ) : JobProcessor { override suspend fun process(param: ReceiveFollowJobParam) = transaction.transaction { - val person = apUserService.fetchPerson(param.actor, param.targetActor) + apUserService.fetchPerson(param.actor, param.targetActor) val follow = objectMapper.readValue(param.follow) logger.info("START Follow from: {} to {}", param.targetActor, param.actor) - val signer = userQueryService.findByUrl(param.targetActor) - - val urlString = person.inbox - - apRequestService.apPost( - url = urlString, - body = Accept( - name = "Follow", - apObject = follow, - actor = param.targetActor - ), - signer = signer - ) - val targetEntity = userQueryService.findByUrl(param.targetActor) val followActorEntity = userQueryService.findByUrl(follow.actor) - userService.followRequest(targetEntity.id, followActorEntity.id) + relationshipService.followRequest(followActorEntity.id, targetEntity.id) + logger.info("SUCCESS Follow from: {} to: {}", param.targetActor, param.actor) } diff --git a/src/main/kotlin/dev/usbharu/hideout/activitypub/service/activity/like/ApRemoveReactionJobProcessor.kt b/src/main/kotlin/dev/usbharu/hideout/activitypub/service/activity/like/ApRemoveReactionJobProcessor.kt index 285670b5..0409b9a0 100644 --- a/src/main/kotlin/dev/usbharu/hideout/activitypub/service/activity/like/ApRemoveReactionJobProcessor.kt +++ b/src/main/kotlin/dev/usbharu/hideout/activitypub/service/activity/like/ApRemoveReactionJobProcessor.kt @@ -31,7 +31,7 @@ class ApRemoveReactionJobProcessor( param.inbox, Undo( actor = param.actor, - `object` = like, + apObject = like, id = "${applicationConfig.url}/undo/like/${param.id}", published = Instant.now().toString() ), diff --git a/src/main/kotlin/dev/usbharu/hideout/activitypub/service/activity/reject/APDeliverRejectJobProcessor.kt b/src/main/kotlin/dev/usbharu/hideout/activitypub/service/activity/reject/APDeliverRejectJobProcessor.kt new file mode 100644 index 00000000..575a34de --- /dev/null +++ b/src/main/kotlin/dev/usbharu/hideout/activitypub/service/activity/reject/APDeliverRejectJobProcessor.kt @@ -0,0 +1,24 @@ +package dev.usbharu.hideout.activitypub.service.activity.reject + +import dev.usbharu.hideout.activitypub.service.common.APRequestService +import dev.usbharu.hideout.application.external.Transaction +import dev.usbharu.hideout.core.external.job.DeliverRejectJob +import dev.usbharu.hideout.core.external.job.DeliverRejectJobParam +import dev.usbharu.hideout.core.query.UserQueryService +import dev.usbharu.hideout.core.service.job.JobProcessor +import org.springframework.stereotype.Component + +@Component +class APDeliverRejectJobProcessor( + private val apRequestService: APRequestService, + private val userQueryService: UserQueryService, + private val deliverRejectJob: DeliverRejectJob, + private val transaction: Transaction +) : + JobProcessor { + override suspend fun process(param: DeliverRejectJobParam): Unit = transaction.transaction { + apRequestService.apPost(param.inbox, param.reject, userQueryService.findById(param.signer)) + } + + override fun job(): DeliverRejectJob = deliverRejectJob +} diff --git a/src/main/kotlin/dev/usbharu/hideout/activitypub/service/activity/reject/ApRejectProcessor.kt b/src/main/kotlin/dev/usbharu/hideout/activitypub/service/activity/reject/ApRejectProcessor.kt new file mode 100644 index 00000000..d86a583a --- /dev/null +++ b/src/main/kotlin/dev/usbharu/hideout/activitypub/service/activity/reject/ApRejectProcessor.kt @@ -0,0 +1,49 @@ +package dev.usbharu.hideout.activitypub.service.activity.reject + +import dev.usbharu.hideout.activitypub.domain.model.Follow +import dev.usbharu.hideout.activitypub.domain.model.Reject +import dev.usbharu.hideout.activitypub.service.common.AbstractActivityPubProcessor +import dev.usbharu.hideout.activitypub.service.common.ActivityPubProcessContext +import dev.usbharu.hideout.activitypub.service.common.ActivityType +import dev.usbharu.hideout.application.external.Transaction +import dev.usbharu.hideout.core.query.UserQueryService +import dev.usbharu.hideout.core.service.relationship.RelationshipService +import org.springframework.stereotype.Service + +@Service +class ApRejectProcessor( + private val relationshipService: RelationshipService, + private val userQueryService: UserQueryService, + transaction: Transaction +) : + AbstractActivityPubProcessor(transaction) { + override suspend fun internalProcess(activity: ActivityPubProcessContext) { + val activityType = activity.activity.apObject.type.firstOrNull { it == "Follow" } + + if (activityType == null) { + logger.warn("FAILED Process Reject Activity type: {}", activity.activity.apObject.type) + return + } + when (activityType) { + "Follow" -> { + val user = userQueryService.findByUrl(activity.activity.actor) + + activity.activity.apObject as Follow + + val actor = activity.activity.apObject.actor + + val target = userQueryService.findByUrl(actor) + + logger.debug("REJECT Follow user {} target {}", user.url, target.url) + + relationshipService.rejectFollowRequest(user.id, target.id) + } + + else -> {} + } + } + + override fun isSupported(activityType: ActivityType): Boolean = activityType == ActivityType.Reject + + override fun type(): Class = Reject::class.java +} 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..ecb675c2 --- /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 sendRejectFollow(user: User, target: User) +} diff --git a/src/main/kotlin/dev/usbharu/hideout/activitypub/service/activity/reject/ApSendRejectServiceImpl.kt b/src/main/kotlin/dev/usbharu/hideout/activitypub/service/activity/reject/ApSendRejectServiceImpl.kt new file mode 100644 index 00000000..fb7610bb --- /dev/null +++ b/src/main/kotlin/dev/usbharu/hideout/activitypub/service/activity/reject/ApSendRejectServiceImpl.kt @@ -0,0 +1,31 @@ +package dev.usbharu.hideout.activitypub.service.activity.reject + +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.DeliverRejectJob +import dev.usbharu.hideout.core.external.job.DeliverRejectJobParam +import dev.usbharu.hideout.core.service.job.JobQueueParentService +import org.springframework.stereotype.Service + +@Service +class ApSendRejectServiceImpl( + private val applicationConfig: ApplicationConfig, + private val jobQueueParentService: JobQueueParentService, + private val deliverRejectJob: DeliverRejectJob +) : ApSendRejectService { + override suspend fun sendRejectFollow(user: User, target: User) { + val deliverRejectJobParam = DeliverRejectJobParam( + Reject( + user.url, + "${applicationConfig.url}/reject/${user.id}/${target.id}", + Follow(apObject = user.url, actor = target.url) + ), + target.inbox, + user.id + ) + + jobQueueParentService.scheduleTypeSafe(deliverRejectJob, deliverRejectJobParam) + } +} diff --git a/src/main/kotlin/dev/usbharu/hideout/activitypub/service/activity/undo/APDeliverUndoJobProcessor.kt b/src/main/kotlin/dev/usbharu/hideout/activitypub/service/activity/undo/APDeliverUndoJobProcessor.kt new file mode 100644 index 00000000..d5f1fec2 --- /dev/null +++ b/src/main/kotlin/dev/usbharu/hideout/activitypub/service/activity/undo/APDeliverUndoJobProcessor.kt @@ -0,0 +1,23 @@ +package dev.usbharu.hideout.activitypub.service.activity.undo + +import dev.usbharu.hideout.activitypub.service.common.APRequestService +import dev.usbharu.hideout.application.external.Transaction +import dev.usbharu.hideout.core.external.job.DeliverUndoJob +import dev.usbharu.hideout.core.external.job.DeliverUndoJobParam +import dev.usbharu.hideout.core.query.UserQueryService +import dev.usbharu.hideout.core.service.job.JobProcessor +import org.springframework.stereotype.Service + +@Service +class APDeliverUndoJobProcessor( + private val deliverUndoJob: DeliverUndoJob, + private val apRequestService: APRequestService, + private val userQueryService: UserQueryService, + private val transaction: Transaction +) : JobProcessor { + override suspend fun process(param: DeliverUndoJobParam): Unit = transaction.transaction { + apRequestService.apPost(param.inbox, param.undo, userQueryService.findById(param.signer)) + } + + override fun job(): DeliverUndoJob = deliverUndoJob +} 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/activitypub/service/activity/undo/APSendUndoServiceImpl.kt b/src/main/kotlin/dev/usbharu/hideout/activitypub/service/activity/undo/APSendUndoServiceImpl.kt new file mode 100644 index 00000000..e1d4c6fd --- /dev/null +++ b/src/main/kotlin/dev/usbharu/hideout/activitypub/service/activity/undo/APSendUndoServiceImpl.kt @@ -0,0 +1,56 @@ +package dev.usbharu.hideout.activitypub.service.activity.undo + +import dev.usbharu.hideout.activitypub.domain.model.Block +import dev.usbharu.hideout.activitypub.domain.model.Follow +import dev.usbharu.hideout.activitypub.domain.model.Undo +import dev.usbharu.hideout.application.config.ApplicationConfig +import dev.usbharu.hideout.core.domain.model.user.User +import dev.usbharu.hideout.core.external.job.DeliverUndoJob +import dev.usbharu.hideout.core.external.job.DeliverUndoJobParam +import dev.usbharu.hideout.core.service.job.JobQueueParentService +import org.springframework.stereotype.Service +import java.time.Instant + +@Service +class APSendUndoServiceImpl( + private val jobQueueParentService: JobQueueParentService, + private val deliverUndoJob: DeliverUndoJob, + private val applicationConfig: ApplicationConfig +) : APSendUndoService { + override suspend fun sendUndoFollow(user: User, target: User) { + val deliverUndoJobParam = DeliverUndoJobParam( + Undo( + actor = user.url, + id = "${applicationConfig.url}/undo/follow/${user.id}/${target.url}", + apObject = Follow( + apObject = user.url, + actor = target.url + ), + published = Instant.now().toString() + ), + target.inbox, + user.id + ) + + jobQueueParentService.scheduleTypeSafe(deliverUndoJob, deliverUndoJobParam) + } + + override suspend fun sendUndoBlock(user: User, target: User) { + val deliverUndoJobParam = DeliverUndoJobParam( + Undo( + actor = user.url, + id = "${applicationConfig.url}/undo/block/${user.id}/${target.url}", + apObject = Block( + apObject = user.url, + actor = target.url, + id = "${applicationConfig.url}/block/${user.id}/${target.id}" + ), + published = Instant.now().toString() + ), + target.inbox, + user.id + ) + + jobQueueParentService.scheduleTypeSafe(deliverUndoJob, deliverUndoJobParam) + } +} diff --git a/src/main/kotlin/dev/usbharu/hideout/activitypub/service/activity/undo/APUndoProcessor.kt b/src/main/kotlin/dev/usbharu/hideout/activitypub/service/activity/undo/APUndoProcessor.kt index 2c8067a4..f476ce36 100644 --- a/src/main/kotlin/dev/usbharu/hideout/activitypub/service/activity/undo/APUndoProcessor.kt +++ b/src/main/kotlin/dev/usbharu/hideout/activitypub/service/activity/undo/APUndoProcessor.kt @@ -1,14 +1,17 @@ package dev.usbharu.hideout.activitypub.service.activity.undo +import dev.usbharu.hideout.activitypub.domain.model.Accept +import dev.usbharu.hideout.activitypub.domain.model.Block import dev.usbharu.hideout.activitypub.domain.model.Follow import dev.usbharu.hideout.activitypub.domain.model.Undo +import dev.usbharu.hideout.activitypub.domain.model.objects.ObjectValue import dev.usbharu.hideout.activitypub.service.common.AbstractActivityPubProcessor import dev.usbharu.hideout.activitypub.service.common.ActivityPubProcessContext import dev.usbharu.hideout.activitypub.service.common.ActivityType import dev.usbharu.hideout.activitypub.service.objects.user.APUserService import dev.usbharu.hideout.application.external.Transaction import dev.usbharu.hideout.core.query.UserQueryService -import dev.usbharu.hideout.core.service.user.UserService +import dev.usbharu.hideout.core.service.relationship.RelationshipService import org.springframework.stereotype.Service @Service @@ -16,34 +19,57 @@ class APUndoProcessor( transaction: Transaction, private val apUserService: APUserService, private val userQueryService: UserQueryService, - private val userService: UserService + private val relationshipService: RelationshipService ) : AbstractActivityPubProcessor(transaction) { override suspend fun internalProcess(activity: ActivityPubProcessContext) { val undo = activity.activity - if (undo.actor == null) { - return - } val type = - undo.`object`.type.orEmpty() + undo.apObject.type .firstOrNull { it == "Block" || it == "Follow" || it == "Like" || it == "Announce" || it == "Accept" } ?: return when (type) { "Follow" -> { - val follow = undo.`object` as Follow + val follow = undo.apObject as Follow - if (follow.apObject == null) { - return - } apUserService.fetchPerson(undo.actor, follow.apObject) val follower = userQueryService.findByUrl(undo.actor) val target = userQueryService.findByUrl(follow.apObject) - userService.unfollow(target.id, follower.id) + + relationshipService.unfollow(follower.id, target.id) return } + "Block" -> { + val block = undo.apObject as Block + + val blocker = apUserService.fetchPersonWithEntity(undo.actor, block.apObject).second + val target = userQueryService.findByUrl(block.apObject) + + relationshipService.unblock(blocker.id, target.id) + return + } + + "Accept" -> { + val accept = undo.apObject as Accept + + val acceptObject = if (accept.apObject is ObjectValue) { + accept.apObject.`object` + } else if (accept.apObject is Follow) { + accept.apObject.apObject + } else { + logger.warn("FAILED Unsupported type. Undo Accept {}", accept.apObject.type) + return + } + + val accepter = apUserService.fetchPersonWithEntity(undo.actor, acceptObject).second + val target = userQueryService.findByUrl(acceptObject) + + relationshipService.rejectFollowRequest(accepter.id, target.id) + } + else -> {} } TODO() diff --git a/src/main/kotlin/dev/usbharu/hideout/activitypub/service/common/AbstractActivityPubProcessor.kt b/src/main/kotlin/dev/usbharu/hideout/activitypub/service/common/AbstractActivityPubProcessor.kt index 0e04262e..1bb106e3 100644 --- a/src/main/kotlin/dev/usbharu/hideout/activitypub/service/common/AbstractActivityPubProcessor.kt +++ b/src/main/kotlin/dev/usbharu/hideout/activitypub/service/common/AbstractActivityPubProcessor.kt @@ -7,9 +7,7 @@ import dev.usbharu.hideout.activitypub.domain.model.objects.Object import dev.usbharu.hideout.application.external.Transaction import org.slf4j.Logger import org.slf4j.LoggerFactory -import org.springframework.stereotype.Service -@Service abstract class AbstractActivityPubProcessor( private val transaction: Transaction, private val allowUnauthorized: Boolean = false diff --git a/src/main/kotlin/dev/usbharu/hideout/activitypub/service/inbox/InboxJobProcessor.kt b/src/main/kotlin/dev/usbharu/hideout/activitypub/service/inbox/InboxJobProcessor.kt index 301ac7ce..ec25c727 100644 --- a/src/main/kotlin/dev/usbharu/hideout/activitypub/service/inbox/InboxJobProcessor.kt +++ b/src/main/kotlin/dev/usbharu/hideout/activitypub/service/inbox/InboxJobProcessor.kt @@ -106,7 +106,7 @@ class InboxJobProcessor( if (activityPubProcessor == null) { logger.warn("ActivityType {} is not support.", param.type) - throw IllegalStateException("ActivityPubProcessor not found.") + throw IllegalStateException("ActivityPubProcessor not found. type: ${param.type}") } val value = objectMapper.treeToValue(jsonNode, activityPubProcessor.type()) diff --git a/src/main/kotlin/dev/usbharu/hideout/application/config/SecurityConfig.kt b/src/main/kotlin/dev/usbharu/hideout/application/config/SecurityConfig.kt index eb19802f..dadee6f4 100644 --- a/src/main/kotlin/dev/usbharu/hideout/application/config/SecurityConfig.kt +++ b/src/main/kotlin/dev/usbharu/hideout/application/config/SecurityConfig.kt @@ -197,6 +197,9 @@ class SecurityConfig { authorize(GET, "/api/v1/accounts/*", permitAll) authorize(GET, "/api/v1/accounts/*/statuses", permitAll) authorize(POST, "/api/v1/accounts/*/follow", hasAnyScope("write", "write:follows")) + authorize(POST, "/api/v1/accounts/*/unfollow", hasAnyScope("write", "write:follows")) + authorize(POST, "/api/v1/accounts/*/block", hasAnyScope("write", "write:blocks")) + authorize(POST, "/api/v1/accounts/*/unblock", hasAnyScope("write", "write:blocks")) authorize(POST, "/api/v1/media", hasAnyScope("write", "write:media")) authorize(POST, "/api/v1/statuses", hasAnyScope("write", "write:statuses")) 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/domain/model/relationship/RelationshipRepositoryImpl.kt b/src/main/kotlin/dev/usbharu/hideout/core/domain/model/relationship/RelationshipRepositoryImpl.kt new file mode 100644 index 00000000..c7e6986d --- /dev/null +++ b/src/main/kotlin/dev/usbharu/hideout/core/domain/model/relationship/RelationshipRepositoryImpl.kt @@ -0,0 +1,84 @@ +package dev.usbharu.hideout.core.domain.model.relationship + +import dev.usbharu.hideout.core.infrastructure.exposedrepository.Users +import org.jetbrains.exposed.dao.id.LongIdTable +import org.jetbrains.exposed.sql.* +import org.jetbrains.exposed.sql.SqlExpressionBuilder.eq +import org.springframework.stereotype.Service + +@Service +class RelationshipRepositoryImpl : RelationshipRepository { + override suspend fun save(relationship: Relationship): Relationship { + val singleOrNull = + Relationships + .select { + (Relationships.userId eq relationship.userId) + .and(Relationships.targetUserId eq relationship.targetUserId) + } + .singleOrNull() + + if (singleOrNull == null) { + Relationships.insert { + it[userId] = relationship.userId + it[targetUserId] = relationship.targetUserId + it[following] = relationship.following + it[blocking] = relationship.blocking + it[muting] = relationship.muting + it[followRequest] = relationship.followRequest + it[ignoreFollowRequestFromTarget] = relationship.ignoreFollowRequestFromTarget + } + } else { + Relationships + .update({ + (Relationships.userId eq relationship.userId) + .and(Relationships.targetUserId eq relationship.targetUserId) + }) { + it[following] = relationship.following + it[blocking] = relationship.blocking + it[muting] = relationship.muting + it[followRequest] = relationship.followRequest + it[ignoreFollowRequestFromTarget] = relationship.ignoreFollowRequestFromTarget + } + } + return relationship + } + + override suspend fun delete(relationship: Relationship) { + Relationships.deleteWhere { + (Relationships.userId eq relationship.userId) + .and(Relationships.targetUserId eq relationship.targetUserId) + } + } + + override suspend fun findByUserIdAndTargetUserId(userId: Long, targetUserId: Long): Relationship? { + return Relationships.select { + (Relationships.userId eq userId) + .and(Relationships.targetUserId eq targetUserId) + }.singleOrNull() + ?.toRelationships() + } +} + +fun ResultRow.toRelationships(): Relationship = Relationship( + userId = this[Relationships.userId], + targetUserId = this[Relationships.targetUserId], + following = this[Relationships.following], + blocking = this[Relationships.blocking], + muting = this[Relationships.muting], + followRequest = this[Relationships.followRequest], + ignoreFollowRequestFromTarget = this[Relationships.ignoreFollowRequestFromTarget] +) + +object Relationships : LongIdTable("relationships") { + val userId = long("user_id").references(Users.id) + val targetUserId = long("target_user_id").references(Users.id) + val following = bool("following") + val blocking = bool("blocking") + val muting = bool("muting") + val followRequest = bool("follow_request") + val ignoreFollowRequestFromTarget = bool("ignore_follow_request") + + init { + uniqueIndex(userId, targetUserId) + } +} diff --git a/src/main/kotlin/dev/usbharu/hideout/core/domain/model/user/UserRepository.kt b/src/main/kotlin/dev/usbharu/hideout/core/domain/model/user/UserRepository.kt index bbc0d346..f37d45fe 100644 --- a/src/main/kotlin/dev/usbharu/hideout/core/domain/model/user/UserRepository.kt +++ b/src/main/kotlin/dev/usbharu/hideout/core/domain/model/user/UserRepository.kt @@ -10,9 +10,5 @@ interface UserRepository { suspend fun delete(id: Long) - suspend fun deleteFollowRequest(id: Long, follower: Long) - - suspend fun findFollowRequestsById(id: Long, follower: Long): Boolean - suspend fun nextId(): Long } diff --git a/src/main/kotlin/dev/usbharu/hideout/core/external/job/DeliverAcceptJob.kt b/src/main/kotlin/dev/usbharu/hideout/core/external/job/DeliverAcceptJob.kt new file mode 100644 index 00000000..0de9fafb --- /dev/null +++ b/src/main/kotlin/dev/usbharu/hideout/core/external/job/DeliverAcceptJob.kt @@ -0,0 +1,37 @@ +package dev.usbharu.hideout.core.external.job + +import com.fasterxml.jackson.databind.ObjectMapper +import com.fasterxml.jackson.module.kotlin.readValue +import dev.usbharu.hideout.activitypub.domain.model.Accept +import kjob.core.dsl.ScheduleContext +import kjob.core.job.JobProps +import org.springframework.stereotype.Component + +data class DeliverAcceptJobParam( + val accept: Accept, + val inbox: String, + val signer: Long +) + +@Component +class DeliverAcceptJob(private val objectMapper: ObjectMapper) : + HideoutJob("DeliverAcceptJob") { + + val accept = string("accept") + val inbox = string("inbox") + val signer = long("signer") + + override fun convert(value: DeliverAcceptJobParam): ScheduleContext.(DeliverAcceptJob) -> Unit = { + props[accept] = objectMapper.writeValueAsString(value.accept) + props[inbox] = value.inbox + props[signer] = value.signer + } + + override fun convert(props: JobProps): DeliverAcceptJobParam { + return DeliverAcceptJobParam( + objectMapper.readValue(props[accept]), + props[inbox], + props[signer] + ) + } +} diff --git a/src/main/kotlin/dev/usbharu/hideout/core/external/job/DeliverBlockJob.kt b/src/main/kotlin/dev/usbharu/hideout/core/external/job/DeliverBlockJob.kt new file mode 100644 index 00000000..482760e1 --- /dev/null +++ b/src/main/kotlin/dev/usbharu/hideout/core/external/job/DeliverBlockJob.kt @@ -0,0 +1,52 @@ +package dev.usbharu.hideout.core.external.job + +import com.fasterxml.jackson.databind.ObjectMapper +import com.fasterxml.jackson.module.kotlin.readValue +import dev.usbharu.hideout.activitypub.domain.model.Block +import dev.usbharu.hideout.activitypub.domain.model.Reject +import kjob.core.dsl.ScheduleContext +import kjob.core.job.JobProps +import org.springframework.beans.factory.annotation.Qualifier +import org.springframework.stereotype.Component + +/** + * ブロックアクティビティ配送のジョブパラメーター + * + * @property signer ブロック操作を行ったユーザーid + * @property block 配送するブロックアクティビティ + * @property reject 配送するフォロー解除アクティビティ + * @property inbox 配送先url + */ +data class DeliverBlockJobParam( + val signer: Long, + val block: Block, + val reject: Reject, + val inbox: String +) + +/** + * ブロックアクティビティ配送のジョブ + */ +@Component +class DeliverBlockJob(@Qualifier("activitypub") private val objectMapper: ObjectMapper) : + HideoutJob("DeliverBlockJob") { + + val block = string("block") + val reject = string("reject") + val inbox = string("inbox") + val signer = long("signer") + + override fun convert(value: DeliverBlockJobParam): ScheduleContext.(DeliverBlockJob) -> Unit = { + props[block] = objectMapper.writeValueAsString(value.block) + props[reject] = objectMapper.writeValueAsString(value.reject) + props[inbox] = value.inbox + props[signer] = value.signer + } + + override fun convert(props: JobProps): DeliverBlockJobParam = DeliverBlockJobParam( + signer = props[signer], + block = objectMapper.readValue(props[block]), + reject = objectMapper.readValue(props[reject]), + inbox = props[inbox] + ) +} diff --git a/src/main/kotlin/dev/usbharu/hideout/core/external/job/DeliverRejectJob.kt b/src/main/kotlin/dev/usbharu/hideout/core/external/job/DeliverRejectJob.kt new file mode 100644 index 00000000..c52dc5ab --- /dev/null +++ b/src/main/kotlin/dev/usbharu/hideout/core/external/job/DeliverRejectJob.kt @@ -0,0 +1,36 @@ +package dev.usbharu.hideout.core.external.job + +import com.fasterxml.jackson.databind.ObjectMapper +import com.fasterxml.jackson.module.kotlin.readValue +import dev.usbharu.hideout.activitypub.domain.model.Reject +import kjob.core.dsl.ScheduleContext +import kjob.core.job.JobProps +import org.springframework.beans.factory.annotation.Qualifier +import org.springframework.stereotype.Component + +data class DeliverRejectJobParam( + val reject: Reject, + val inbox: String, + val signer: Long +) + +@Component +class DeliverRejectJob(@Qualifier("activitypub") private val objectMapper: ObjectMapper) : + HideoutJob("DeliverRejectJob") { + val reject = string("reject") + val inbox = string("inbox") + val signer = long("signer") + + override fun convert(value: DeliverRejectJobParam): ScheduleContext.(DeliverRejectJob) -> Unit = + { + props[reject] = objectMapper.writeValueAsString(value.reject) + props[inbox] = value.inbox + props[signer] = value.signer + } + + override fun convert(props: JobProps): DeliverRejectJobParam = DeliverRejectJobParam( + objectMapper.readValue(props[reject]), + props[inbox], + props[signer] + ) +} diff --git a/src/main/kotlin/dev/usbharu/hideout/core/external/job/DeliverUndoJob.kt b/src/main/kotlin/dev/usbharu/hideout/core/external/job/DeliverUndoJob.kt new file mode 100644 index 00000000..50efd716 --- /dev/null +++ b/src/main/kotlin/dev/usbharu/hideout/core/external/job/DeliverUndoJob.kt @@ -0,0 +1,38 @@ +package dev.usbharu.hideout.core.external.job + +import com.fasterxml.jackson.databind.ObjectMapper +import com.fasterxml.jackson.module.kotlin.readValue +import dev.usbharu.hideout.activitypub.domain.model.Undo +import kjob.core.dsl.ScheduleContext +import kjob.core.job.JobProps +import org.springframework.beans.factory.annotation.Qualifier +import org.springframework.stereotype.Component + +data class DeliverUndoJobParam( + val undo: Undo, + val inbox: String, + val signer: Long +) + +@Component +class DeliverUndoJob(@Qualifier("activitypub") private val objectMapper: ObjectMapper) : + HideoutJob("DeliverUndoJob") { + + val undo = string("undo") + val inbox = string("inbox") + val signer = long("signer") + + override fun convert(value: DeliverUndoJobParam): ScheduleContext.(DeliverUndoJob) -> Unit = { + props[undo] = objectMapper.writeValueAsString(value.undo) + props[inbox] = value.inbox + props[signer] = value.signer + } + + override fun convert(props: JobProps): DeliverUndoJobParam { + return DeliverUndoJobParam( + objectMapper.readValue(props[undo]), + props[inbox], + props[signer] + ) + } +} diff --git a/src/main/kotlin/dev/usbharu/hideout/core/external/job/HideoutJob.kt b/src/main/kotlin/dev/usbharu/hideout/core/external/job/HideoutJob.kt index d201fc9a..3efdf09d 100644 --- a/src/main/kotlin/dev/usbharu/hideout/core/external/job/HideoutJob.kt +++ b/src/main/kotlin/dev/usbharu/hideout/core/external/job/HideoutJob.kt @@ -7,7 +7,7 @@ import kjob.core.dsl.ScheduleContext import kjob.core.job.JobProps import org.springframework.stereotype.Component -abstract class HideoutJob>(name: String = "") : Job(name) { +abstract class HideoutJob>(name: String) : Job(name) { abstract fun convert(value: @UnsafeVariance T): ScheduleContext<@UnsafeVariance R>.(@UnsafeVariance R) -> Unit fun convertUnsafe(props: JobProps<*>): T = convert(props as JobProps) abstract fun convert(props: JobProps<@UnsafeVariance R>): T diff --git a/src/main/kotlin/dev/usbharu/hideout/core/infrastructure/exposedquery/FollowerQueryServiceImpl.kt b/src/main/kotlin/dev/usbharu/hideout/core/infrastructure/exposedquery/FollowerQueryServiceImpl.kt index 4c62003e..df5acb77 100644 --- a/src/main/kotlin/dev/usbharu/hideout/core/infrastructure/exposedquery/FollowerQueryServiceImpl.kt +++ b/src/main/kotlin/dev/usbharu/hideout/core/infrastructure/exposedquery/FollowerQueryServiceImpl.kt @@ -1,242 +1,24 @@ package dev.usbharu.hideout.core.infrastructure.exposedquery +import dev.usbharu.hideout.core.domain.model.relationship.RelationshipRepository import dev.usbharu.hideout.core.domain.model.user.User -import dev.usbharu.hideout.core.infrastructure.exposedrepository.Users -import dev.usbharu.hideout.core.infrastructure.exposedrepository.UsersFollowers import dev.usbharu.hideout.core.query.FollowerQueryService -import org.jetbrains.exposed.sql.* -import org.jetbrains.exposed.sql.SqlExpressionBuilder.eq +import dev.usbharu.hideout.core.query.RelationshipQueryService +import dev.usbharu.hideout.core.query.UserQueryService import org.springframework.stereotype.Repository -import java.time.Instant @Repository -class FollowerQueryServiceImpl(private val userBuilder: User.UserBuilder) : FollowerQueryService { +class FollowerQueryServiceImpl( + private val relationshipQueryService: RelationshipQueryService, + private val userQueryService: UserQueryService, + private val relationshipRepository: RelationshipRepository +) : FollowerQueryService { override suspend fun findFollowersById(id: Long): List { - val followers = Users.alias("FOLLOWERS") - return Users.innerJoin( - otherTable = UsersFollowers, - onColumn = { Users.id }, - otherColumn = { userId } + return userQueryService.findByIds( + relationshipQueryService.findByTargetIdAndFollowing(id, true).map { it.userId } ) - .innerJoin( - otherTable = followers, - onColumn = { UsersFollowers.followerId }, - otherColumn = { followers[Users.id] } - ) - .slice( - followers[Users.id], - followers[Users.name], - followers[Users.domain], - followers[Users.screenName], - followers[Users.description], - followers[Users.password], - followers[Users.inbox], - followers[Users.outbox], - followers[Users.url], - followers[Users.publicKey], - followers[Users.privateKey], - followers[Users.createdAt], - followers[Users.keyId], - followers[Users.following], - followers[Users.followers], - followers[Users.instance] - ) - .select { Users.id eq id } - .map { - userBuilder.of( - id = it[followers[Users.id]], - name = it[followers[Users.name]], - domain = it[followers[Users.domain]], - screenName = it[followers[Users.screenName]], - description = it[followers[Users.description]], - password = it[followers[Users.password]], - inbox = it[followers[Users.inbox]], - outbox = it[followers[Users.outbox]], - url = it[followers[Users.url]], - publicKey = it[followers[Users.publicKey]], - privateKey = it[followers[Users.privateKey]], - createdAt = Instant.ofEpochMilli(it[followers[Users.createdAt]]), - keyId = it[followers[Users.keyId]], - followers = it[followers[Users.followers]], - following = it[followers[Users.following]], - instance = it[followers[Users.instance]] - ) - } } - override suspend fun findFollowersByNameAndDomain(name: String, domain: String): List { - val followers = Users.alias("FOLLOWERS") - return Users.innerJoin( - otherTable = UsersFollowers, - onColumn = { id }, - otherColumn = { userId } - ) - .innerJoin( - otherTable = followers, - onColumn = { UsersFollowers.followerId }, - otherColumn = { followers[Users.id] } - ) - .slice( - followers[Users.id], - followers[Users.name], - followers[Users.domain], - followers[Users.screenName], - followers[Users.description], - followers[Users.password], - followers[Users.inbox], - followers[Users.outbox], - followers[Users.url], - followers[Users.publicKey], - followers[Users.privateKey], - followers[Users.createdAt], - followers[Users.keyId], - followers[Users.following], - followers[Users.followers], - followers[Users.instance] - ) - .select { Users.name eq name and (Users.domain eq domain) } - .map { - userBuilder.of( - id = it[followers[Users.id]], - name = it[followers[Users.name]], - domain = it[followers[Users.domain]], - screenName = it[followers[Users.screenName]], - description = it[followers[Users.description]], - password = it[followers[Users.password]], - inbox = it[followers[Users.inbox]], - outbox = it[followers[Users.outbox]], - url = it[followers[Users.url]], - publicKey = it[followers[Users.publicKey]], - privateKey = it[followers[Users.privateKey]], - createdAt = Instant.ofEpochMilli(it[followers[Users.createdAt]]), - keyId = it[followers[Users.keyId]], - followers = it[followers[Users.followers]], - following = it[followers[Users.following]], - instance = it[followers[Users.instance]] - ) - } - } - - override suspend fun findFollowingById(id: Long): List { - val followers = Users.alias("FOLLOWERS") - return Users.innerJoin( - otherTable = UsersFollowers, - onColumn = { Users.id }, - otherColumn = { userId } - ) - .innerJoin( - otherTable = followers, - onColumn = { UsersFollowers.followerId }, - otherColumn = { followers[Users.id] } - ) - .slice( - followers[Users.id], - followers[Users.name], - followers[Users.domain], - followers[Users.screenName], - followers[Users.description], - followers[Users.password], - followers[Users.inbox], - followers[Users.outbox], - followers[Users.url], - followers[Users.publicKey], - followers[Users.privateKey], - followers[Users.createdAt], - followers[Users.keyId], - followers[Users.following], - followers[Users.followers], - followers[Users.instance] - ) - .select { followers[Users.id] eq id } - .map { - userBuilder.of( - id = it[followers[Users.id]], - name = it[followers[Users.name]], - domain = it[followers[Users.domain]], - screenName = it[followers[Users.screenName]], - description = it[followers[Users.description]], - password = it[followers[Users.password]], - inbox = it[followers[Users.inbox]], - outbox = it[followers[Users.outbox]], - url = it[followers[Users.url]], - publicKey = it[followers[Users.publicKey]], - privateKey = it[followers[Users.privateKey]], - createdAt = Instant.ofEpochMilli(it[followers[Users.createdAt]]), - keyId = it[followers[Users.keyId]], - followers = it[followers[Users.followers]], - following = it[followers[Users.following]], - instance = it[followers[Users.instance]] - ) - } - } - - override suspend fun findFollowingByNameAndDomain(name: String, domain: String): List { - val followers = Users.alias("FOLLOWERS") - return Users.innerJoin( - otherTable = UsersFollowers, - onColumn = { id }, - otherColumn = { userId } - ) - .innerJoin( - otherTable = followers, - onColumn = { UsersFollowers.followerId }, - otherColumn = { followers[Users.id] } - ) - .slice( - followers[Users.id], - followers[Users.name], - followers[Users.domain], - followers[Users.screenName], - followers[Users.description], - followers[Users.password], - followers[Users.inbox], - followers[Users.outbox], - followers[Users.url], - followers[Users.publicKey], - followers[Users.privateKey], - followers[Users.createdAt], - followers[Users.keyId], - followers[Users.following], - followers[Users.followers], - followers[Users.instance] - ) - .select { followers[Users.name] eq name and (followers[Users.domain] eq domain) } - .map { - userBuilder.of( - id = it[followers[Users.id]], - name = it[followers[Users.name]], - domain = it[followers[Users.domain]], - screenName = it[followers[Users.screenName]], - description = it[followers[Users.description]], - password = it[followers[Users.password]], - inbox = it[followers[Users.inbox]], - outbox = it[followers[Users.outbox]], - url = it[followers[Users.url]], - publicKey = it[followers[Users.publicKey]], - privateKey = it[followers[Users.privateKey]], - createdAt = Instant.ofEpochMilli(it[followers[Users.createdAt]]), - keyId = it[followers[Users.keyId]], - followers = it[followers[Users.followers]], - following = it[followers[Users.following]], - instance = it[followers[Users.instance]] - ) - } - } - - override suspend fun appendFollower(user: Long, follower: Long) { - UsersFollowers.insert { - it[userId] = user - it[followerId] = follower - } - } - - override suspend fun removeFollower(user: Long, follower: Long) { - UsersFollowers.deleteWhere { userId eq user and (followerId eq follower) } - } - - override suspend fun alreadyFollow(userId: Long, followerId: Long): Boolean { - return UsersFollowers.select { UsersFollowers.userId eq userId or (UsersFollowers.followerId eq followerId) } - .empty() - .not() - } + override suspend fun alreadyFollow(userId: Long, followerId: Long): Boolean = + relationshipRepository.findByUserIdAndTargetUserId(followerId, userId)?.following ?: false } diff --git a/src/main/kotlin/dev/usbharu/hideout/core/infrastructure/exposedquery/RelationshipQueryServiceImpl.kt b/src/main/kotlin/dev/usbharu/hideout/core/infrastructure/exposedquery/RelationshipQueryServiceImpl.kt new file mode 100644 index 00000000..f1a122c9 --- /dev/null +++ b/src/main/kotlin/dev/usbharu/hideout/core/infrastructure/exposedquery/RelationshipQueryServiceImpl.kt @@ -0,0 +1,16 @@ +package dev.usbharu.hideout.core.infrastructure.exposedquery + +import dev.usbharu.hideout.core.domain.model.relationship.Relationship +import dev.usbharu.hideout.core.domain.model.relationship.Relationships +import dev.usbharu.hideout.core.domain.model.relationship.toRelationships +import dev.usbharu.hideout.core.query.RelationshipQueryService +import org.jetbrains.exposed.sql.and +import org.jetbrains.exposed.sql.select +import org.springframework.stereotype.Service + +@Service +class RelationshipQueryServiceImpl : RelationshipQueryService { + override suspend fun findByTargetIdAndFollowing(targetId: Long, following: Boolean): List = + Relationships.select { Relationships.targetUserId eq targetId and (Relationships.following eq following) } + .map { it.toRelationships() } +} diff --git a/src/main/kotlin/dev/usbharu/hideout/core/infrastructure/exposedrepository/UserRepositoryImpl.kt b/src/main/kotlin/dev/usbharu/hideout/core/infrastructure/exposedrepository/UserRepositoryImpl.kt index ecd910a6..b4adbaad 100644 --- a/src/main/kotlin/dev/usbharu/hideout/core/infrastructure/exposedrepository/UserRepositoryImpl.kt +++ b/src/main/kotlin/dev/usbharu/hideout/core/infrastructure/exposedrepository/UserRepositoryImpl.kt @@ -4,7 +4,6 @@ import dev.usbharu.hideout.application.infrastructure.exposed.ResultRowMapper import dev.usbharu.hideout.application.service.id.IdGenerateService import dev.usbharu.hideout.core.domain.model.user.User import dev.usbharu.hideout.core.domain.model.user.UserRepository -import org.jetbrains.exposed.dao.id.LongIdTable import org.jetbrains.exposed.sql.* import org.jetbrains.exposed.sql.SqlExpressionBuilder.eq import org.springframework.stereotype.Repository @@ -62,15 +61,6 @@ class UserRepositoryImpl( override suspend fun findById(id: Long): User? = Users.select { Users.id eq id }.singleOrNull()?.let(userResultRowMapper::map) - override suspend fun deleteFollowRequest(id: Long, follower: Long) { - FollowRequests.deleteWhere { userId.eq(id) and followerId.eq(follower) } - } - - override suspend fun findFollowRequestsById(id: Long, follower: Long): Boolean { - return FollowRequests.select { (FollowRequests.userId eq id) and (FollowRequests.followerId eq follower) } - .singleOrNull() != null - } - override suspend fun delete(id: Long) { Users.deleteWhere { Users.id.eq(id) } } @@ -108,21 +98,3 @@ object Users : Table("users") { uniqueIndex(name, domain) } } - -object UsersFollowers : LongIdTable("users_followers") { - val userId: Column = long("user_id").references(Users.id).index() - val followerId: Column = long("follower_id").references(Users.id) - - init { - uniqueIndex(userId, followerId) - } -} - -object FollowRequests : LongIdTable("follow_requests") { - val userId: Column = long("user_id").references(Users.id) - val followerId: Column = long("follower_id").references(Users.id) - - init { - uniqueIndex(userId, followerId) - } -} diff --git a/src/main/kotlin/dev/usbharu/hideout/core/infrastructure/kjobexposed/KJobJobQueueWorkerService.kt b/src/main/kotlin/dev/usbharu/hideout/core/infrastructure/kjobexposed/KJobJobQueueWorkerService.kt index ff8e07e8..28b437c6 100644 --- a/src/main/kotlin/dev/usbharu/hideout/core/infrastructure/kjobexposed/KJobJobQueueWorkerService.kt +++ b/src/main/kotlin/dev/usbharu/hideout/core/infrastructure/kjobexposed/KJobJobQueueWorkerService.kt @@ -36,10 +36,14 @@ class KJobJobQueueWorkerService(private val jobQueueProcessorList: List - suspend fun findFollowersByNameAndDomain(name: String, domain: String): List - suspend fun findFollowingById(id: Long): List - suspend fun findFollowingByNameAndDomain(name: String, domain: String): List - suspend fun appendFollower(user: Long, follower: Long) - suspend fun removeFollower(user: Long, follower: Long) suspend fun alreadyFollow(userId: Long, followerId: Long): Boolean } diff --git a/src/main/kotlin/dev/usbharu/hideout/core/query/RelationshipQueryService.kt b/src/main/kotlin/dev/usbharu/hideout/core/query/RelationshipQueryService.kt new file mode 100644 index 00000000..5f051397 --- /dev/null +++ b/src/main/kotlin/dev/usbharu/hideout/core/query/RelationshipQueryService.kt @@ -0,0 +1,8 @@ +package dev.usbharu.hideout.core.query + +import dev.usbharu.hideout.core.domain.model.relationship.Relationship + +interface RelationshipQueryService { + + suspend fun findByTargetIdAndFollowing(targetId: Long, following: Boolean): List +} diff --git a/src/main/kotlin/dev/usbharu/hideout/core/service/media/LocalFileSystemMediaDataStore.kt b/src/main/kotlin/dev/usbharu/hideout/core/service/media/LocalFileSystemMediaDataStore.kt index 412b50a5..2b329ebb 100644 --- a/src/main/kotlin/dev/usbharu/hideout/core/service/media/LocalFileSystemMediaDataStore.kt +++ b/src/main/kotlin/dev/usbharu/hideout/core/service/media/LocalFileSystemMediaDataStore.kt @@ -36,6 +36,7 @@ class LocalFileSystemMediaDataStore( savePath.createDirectories() } + @Suppress("NestedBlockDepth") override suspend fun save(dataMediaSave: MediaSave): SavedMedia { val fileSavePath = buildSavePath(savePath, dataMediaSave.name) val thumbnailSavePath = buildSavePath(savePath, "thumbnail-" + dataMediaSave.name) 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..9032bd2e --- /dev/null +++ b/src/main/kotlin/dev/usbharu/hideout/core/service/relationship/RelationshipService.kt @@ -0,0 +1,22 @@ +package dev.usbharu.hideout.core.service.relationship + +interface RelationshipService { + suspend fun followRequest(userId: Long, targetId: Long) + suspend fun block(userId: Long, targetId: Long) + + /** + * フォローリクエストを承認します + * [userId]が[targetId]からのフォローリクエストを承認します + * + * @param userId 承認操作をするユーザー + * @param targetId 承認するフォローリクエストを送ってきたユーザー + * @param force 強制的にAcceptアクティビティを発行する + */ + suspend fun acceptFollowRequest(userId: Long, targetId: Long, force: Boolean = false) + 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..a94332f9 --- /dev/null +++ b/src/main/kotlin/dev/usbharu/hideout/core/service/relationship/RelationshipServiceImpl.kt @@ -0,0 +1,305 @@ +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) { + logger.info("START Follow Request userId: {} targetId: {}", userId, targetId) + + 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 + } + + if (relationship.following) { + logger.debug("SUCCESS User already follow. userId: {} targetId: {}", userId, targetId) + acceptFollowRequest(targetId, userId, true) + return + } + + relationshipRepository.save(relationship) + + val remoteUser = isRemoteUser(targetId) + + if (remoteUser != null) { + val user = userQueryService.findById(userId) + apSendFollowService.sendFollow(SendFollowDto(user, remoteUser)) + } else { + // TODO: フォロー許可制ユーザーを実装したら消す + acceptFollowRequest(targetId, userId) + } + + logger.info("SUCCESS Follow Request userId: {} targetId: {}", 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 + ) + + val inverseRelationship = relationshipRepository.findByUserIdAndTargetUserId(targetId, userId) + ?.copy(followRequest = false, following = false) + + relationshipRepository.save(relationship) + if (inverseRelationship != null) { + relationshipRepository.save(inverseRelationship) + } + + val remoteUser = isRemoteUser(targetId) + + if (remoteUser != null) { + val user = userQueryService.findById(userId) + apSendBlockService.sendBlock(user, remoteUser) + } + } + + override suspend fun acceptFollowRequest(userId: Long, targetId: Long, force: Boolean) { + logger.info("START Accept follow request userId: {} targetId: {}", userId, targetId) + + val relationship = relationshipRepository.findByUserIdAndTargetUserId(targetId, userId) + + val inverseRelationship = relationshipRepository.findByUserIdAndTargetUserId(userId, targetId) ?: Relationship( + userId = targetId, + targetUserId = userId, + following = false, + blocking = false, + muting = false, + followRequest = false, + ignoreFollowRequestFromTarget = false + ) + + if (relationship == null) { + logger.warn("FAILED Follow Request Not Found. (Relationship) userId: {} targetId: {}", userId, targetId) + return + } + + if (relationship.followRequest.not() && force.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" + ) + } + + if (inverseRelationship.blocking) { + logger.warn("FAILED BLocked by user userId: {} targetId: {}", userId, targetId) + throw IllegalStateException( + "Cannot accept a follow request from a blocking 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.sendAcceptFollow(user, remoteUser) + } + } + + override suspend fun rejectFollowRequest(userId: Long, targetId: Long) { + val relationship = relationshipRepository.findByUserIdAndTargetUserId(targetId, userId) + + if (relationship == null) { + logger.warn("FAILED Follow Request Not Found. (Relationship) userId: {} targetId: {}", userId, targetId) + return + } + + if (relationship.followRequest.not() && relationship.following.not()) { + logger.warn("FAILED Follow Request Not Found. (Follow Request) userId: {} targetId: {}", userId, targetId) + return + } + + val copy = relationship.copy(followRequest = false, following = false) + + relationshipRepository.save(copy) + + val remoteUser = isRemoteUser(targetId) + + if (remoteUser != null) { + val user = userQueryService.findById(userId) + apSendRejectService.sendRejectFollow(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, remoteUser) + } + } + + 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? { + logger.trace("isRemoteUser({})", userId) + val user = try { + userQueryService.findById(userId) + } catch (e: FailedToGetResourcesException) { + logger.warn("User not found.", e) + throw IllegalStateException("User not found.", e) + } + + logger.trace("user info {}", user) + + if (user.domain == applicationConfig.url.host) { + logger.trace("user: {} is local user", userId) + return null + } + logger.trace("user: {} is remote user", userId) + return user + } + + companion object { + private val logger = LoggerFactory.getLogger(RelationshipServiceImpl::class.java) + } +} diff --git a/src/main/kotlin/dev/usbharu/hideout/core/service/user/UserService.kt b/src/main/kotlin/dev/usbharu/hideout/core/service/user/UserService.kt index fc34c36c..a9c0de2d 100644 --- a/src/main/kotlin/dev/usbharu/hideout/core/service/user/UserService.kt +++ b/src/main/kotlin/dev/usbharu/hideout/core/service/user/UserService.kt @@ -11,23 +11,4 @@ interface UserService { suspend fun createLocalUser(user: UserCreateDto): User suspend fun createRemoteUser(user: RemoteUserCreateDto): User - - /** - * フォローリクエストを送信する - * - * @param id - * @param followerId - * @return リクエストが成功したか - */ - suspend fun followRequest(id: Long, followerId: Long): Boolean - - /** - * フォローする - * - * @param id - * @param followerId - */ - suspend fun follow(id: Long, followerId: Long) - - suspend fun unfollow(id: Long, followerId: Long): Boolean } diff --git a/src/main/kotlin/dev/usbharu/hideout/core/service/user/UserServiceImpl.kt b/src/main/kotlin/dev/usbharu/hideout/core/service/user/UserServiceImpl.kt index e70ec782..b7bb12cf 100644 --- a/src/main/kotlin/dev/usbharu/hideout/core/service/user/UserServiceImpl.kt +++ b/src/main/kotlin/dev/usbharu/hideout/core/service/user/UserServiceImpl.kt @@ -1,13 +1,9 @@ package dev.usbharu.hideout.core.service.user -import dev.usbharu.hideout.activitypub.service.activity.follow.APSendFollowService import dev.usbharu.hideout.application.config.ApplicationConfig -import dev.usbharu.hideout.core.domain.exception.UserNotFoundException import dev.usbharu.hideout.core.domain.model.user.User import dev.usbharu.hideout.core.domain.model.user.UserRepository -import dev.usbharu.hideout.core.query.FollowerQueryService import dev.usbharu.hideout.core.query.UserQueryService -import dev.usbharu.hideout.core.service.follow.SendFollowDto import dev.usbharu.hideout.core.service.instance.InstanceService import org.jetbrains.exposed.exceptions.ExposedSQLException import org.slf4j.LoggerFactory @@ -19,9 +15,7 @@ import java.time.Instant class UserServiceImpl( private val userRepository: UserRepository, private val userAuthService: UserAuthService, - private val apSendFollowService: APSendFollowService, private val userQueryService: UserQueryService, - private val followerQueryService: FollowerQueryService, private val userBuilder: User.UserBuilder, private val applicationConfig: ApplicationConfig, private val instanceService: InstanceService @@ -96,38 +90,6 @@ class UserServiceImpl( } } - // TODO APのフォロー処理を作る - override suspend fun followRequest(id: Long, followerId: Long): Boolean { - val user = userRepository.findById(id) ?: throw UserNotFoundException("$id was not found.") - val follower = userRepository.findById(followerId) ?: throw UserNotFoundException("$followerId was not found.") - return if (user.domain == applicationConfig.url.host) { - follow(id, followerId) - true - } else { - if (userRepository.findFollowRequestsById(id, followerId)) { - // do-nothing - } else { - apSendFollowService.sendFollow(SendFollowDto(follower, user)) - } - false - } - } - - override suspend fun follow(id: Long, followerId: Long) { - logger.debug("START Follow id: {} → target: {}", followerId, id) - followerQueryService.appendFollower(id, followerId) - if (userRepository.findFollowRequestsById(id, followerId)) { - logger.debug("Follow request is accepted! ") - userRepository.deleteFollowRequest(id, followerId) - } - logger.debug("SUCCESS Follow id: {} → target: {}", followerId, id) - } - - override suspend fun unfollow(id: Long, followerId: Long): Boolean { - followerQueryService.removeFollower(id, followerId) - return false - } - companion object { private val logger = LoggerFactory.getLogger(UserServiceImpl::class.java) } diff --git a/src/main/kotlin/dev/usbharu/hideout/mastodon/interfaces/api/account/MastodonAccountApiController.kt b/src/main/kotlin/dev/usbharu/hideout/mastodon/interfaces/api/account/MastodonAccountApiController.kt index a7f3741c..f9af1c7d 100644 --- a/src/main/kotlin/dev/usbharu/hideout/mastodon/interfaces/api/account/MastodonAccountApiController.kt +++ b/src/main/kotlin/dev/usbharu/hideout/mastodon/interfaces/api/account/MastodonAccountApiController.kt @@ -75,17 +75,17 @@ class MastodonAccountApiController( val userid = principal.getClaim("uid").toLong() val statusFlow = accountApiService.accountsStatuses( - id.toLong(), - maxId?.toLongOrNull(), - sinceId?.toLongOrNull(), - minId?.toLongOrNull(), - limit, - onlyMedia, - excludeReplies, - excludeReblogs, - pinned, - tagged, - userid + userid = id.toLong(), + maxId = maxId?.toLongOrNull(), + sinceId = sinceId?.toLongOrNull(), + minId = minId?.toLongOrNull(), + limit = limit, + onlyMedia = onlyMedia, + excludeReplies = excludeReplies, + excludeReblogs = excludeReblogs, + pinned = pinned, + tagged = tagged, + loginUser = userid ).asFlow() ResponseEntity.ok(statusFlow) } @@ -103,4 +103,44 @@ class MastodonAccountApiController( .asFlow() ) } + + override suspend fun apiV1AccountsIdBlockPost(id: String): ResponseEntity { + val principal = SecurityContextHolder.getContext().getAuthentication().principal as Jwt + + val userid = principal.getClaim("uid").toLong() + + val block = accountApiService.block(userid, id.toLong()) + + return ResponseEntity.ok(block) + } + + override suspend fun apiV1AccountsIdUnblockPost(id: String): ResponseEntity { + val principal = SecurityContextHolder.getContext().getAuthentication().principal as Jwt + + val userid = principal.getClaim("uid").toLong() + + val unblock = accountApiService.unblock(userid, id.toLong()) + + return ResponseEntity.ok(unblock) + } + + override suspend fun apiV1AccountsIdUnfollowPost(id: String): ResponseEntity { + val principal = SecurityContextHolder.getContext().getAuthentication().principal as Jwt + + val userid = principal.getClaim("uid").toLong() + + val unfollow = accountApiService.unfollow(userid, id.toLong()) + + return ResponseEntity.ok(unfollow) + } + + override suspend fun apiV1AccountsIdRemoveFromFollowersPost(id: String): ResponseEntity { + val principal = SecurityContextHolder.getContext().getAuthentication().principal as Jwt + + val userid = principal.getClaim("uid").toLong() + + val removeFromFollowers = accountApiService.removeFromFollowers(userid, id.toLong()) + + return ResponseEntity.ok(removeFromFollowers) + } } diff --git a/src/main/kotlin/dev/usbharu/hideout/mastodon/service/account/AccountApiService.kt b/src/main/kotlin/dev/usbharu/hideout/mastodon/service/account/AccountApiService.kt index a3315107..f639afc8 100644 --- a/src/main/kotlin/dev/usbharu/hideout/mastodon/service/account/AccountApiService.kt +++ b/src/main/kotlin/dev/usbharu/hideout/mastodon/service/account/AccountApiService.kt @@ -1,8 +1,8 @@ package dev.usbharu.hideout.mastodon.service.account import dev.usbharu.hideout.application.external.Transaction -import dev.usbharu.hideout.core.domain.model.user.UserRepository -import dev.usbharu.hideout.core.query.FollowerQueryService +import dev.usbharu.hideout.core.domain.model.relationship.RelationshipRepository +import dev.usbharu.hideout.core.service.relationship.RelationshipService import dev.usbharu.hideout.core.service.user.UserCreateDto import dev.usbharu.hideout.core.service.user.UserService import dev.usbharu.hideout.domain.mastodon.model.generated.* @@ -13,6 +13,7 @@ import kotlin.math.min @Service interface AccountApiService { + @Suppress("LongParameterList") suspend fun accountsStatuses( userid: Long, maxId: Long?, @@ -29,9 +30,21 @@ interface AccountApiService { suspend fun verifyCredentials(userid: Long): CredentialAccount suspend fun registerAccount(userCreateDto: UserCreateDto): Unit - suspend fun follow(userid: Long, followeeId: Long): Relationship + suspend fun follow(loginUser: Long, followTargetUserId: Long): Relationship suspend fun account(id: Long): Account suspend fun relationships(userid: Long, id: List, withSuspended: Boolean): List + + /** + * ブロック操作を行う + * + * @param userid ブロック操作を行ったユーザーid + * @param target ブロック対象のユーザーid + * @return ブロック後のブロック対象ユーザーとの[Relationship] + */ + suspend fun block(userid: Long, target: Long): Relationship + suspend fun unblock(userid: Long, target: Long): Relationship + suspend fun unfollow(userid: Long, target: Long): Relationship + suspend fun removeFromFollowers(userid: Long, target: Long): Relationship } @Service @@ -39,9 +52,9 @@ class AccountApiServiceImpl( private val accountService: AccountService, private val transaction: Transaction, private val userService: UserService, - private val followerQueryService: FollowerQueryService, - private val userRepository: UserRepository, - private val statusQueryService: StatusQueryService + private val statusQueryService: StatusQueryService, + private val relationshipService: RelationshipService, + private val relationshipRepository: RelationshipRepository ) : AccountApiService { override suspend fun accountsStatuses( @@ -61,23 +74,23 @@ class AccountApiServiceImpl( false } else { transaction.transaction { - followerQueryService.alreadyFollow(userid, loginUser) + isFollowing(loginUser, userid) } } return transaction.transaction { statusQueryService.accountsStatus( - userid, - maxId, - sinceId, - minId, - limit, - onlyMedia, - excludeReplies, - excludeReblogs, - pinned, - tagged, - canViewFollowers + accountId = userid, + maxId = maxId, + sinceId = sinceId, + minId = minId, + limit = limit, + onlyMedia = onlyMedia, + excludeReplies = excludeReplies, + excludeReblogs = excludeReblogs, + pinned = pinned, + tagged = tagged, + includeFollowers = canViewFollowers ) } } @@ -91,34 +104,10 @@ class AccountApiServiceImpl( userService.createLocalUser(UserCreateDto(userCreateDto.name, userCreateDto.name, "", userCreateDto.password)) } - override suspend fun follow(userid: Long, followeeId: Long): Relationship = transaction.transaction { - val alreadyFollow = followerQueryService.alreadyFollow(followeeId, userid) + override suspend fun follow(loginUser: Long, followTargetUserId: Long): Relationship = transaction.transaction { + relationshipService.followRequest(loginUser, followTargetUserId) - val followRequest = if (alreadyFollow) { - true - } else { - userService.followRequest(followeeId, userid) - } - - val alreadyFollow1 = followerQueryService.alreadyFollow(userid, followeeId) - - val followRequestsById = userRepository.findFollowRequestsById(followeeId, userid) - - return@transaction Relationship( - followeeId.toString(), - followRequest, - true, - false, - alreadyFollow1, - false, - false, - false, - false, - followRequestsById, - false, - false, - "" - ) + return@transaction fetchRelationship(loginUser, followTargetUserId) } override suspend fun account(id: Long): Account = transaction.transaction { @@ -136,30 +125,34 @@ class AccountApiServiceImpl( val subList = id.subList(0, min(id.size, 20)) return@transaction subList.map { - val alreadyFollow = followerQueryService.alreadyFollow(userid, it) - - val followed = followerQueryService.alreadyFollow(it, userid) - - val requested = userRepository.findFollowRequestsById(it, userid) - - Relationship( - id = it.toString(), - following = alreadyFollow, - showingReblogs = true, - notifying = false, - followedBy = followed, - blocking = false, - blockedBy = false, - muting = false, - mutingNotifications = false, - requested = requested, - domainBlocking = false, - endorsed = false, - note = "" - ) + fetchRelationship(userid, it) } } + override suspend fun block(userid: Long, target: Long): Relationship = transaction.transaction { + relationshipService.block(userid, target) + + fetchRelationship(userid, target) + } + + override suspend fun unblock(userid: Long, target: Long): Relationship = transaction.transaction { + relationshipService.unblock(userid, target) + + return@transaction fetchRelationship(userid, target) + } + + override suspend fun unfollow(userid: Long, target: Long): Relationship = transaction.transaction { + relationshipService.unfollow(userid, target) + + return@transaction fetchRelationship(userid, target) + } + + override suspend fun removeFromFollowers(userid: Long, target: Long): Relationship = transaction.transaction { + relationshipService.rejectFollowRequest(userid, target) + + return@transaction fetchRelationship(userid, target) + } + private fun from(account: Account): CredentialAccount { return CredentialAccount( id = account.id, @@ -198,6 +191,49 @@ class AccountApiServiceImpl( ) } + private suspend fun fetchRelationship(userid: Long, targetId: Long): Relationship { + val relationship = relationshipRepository.findByUserIdAndTargetUserId(userid, targetId) + ?: dev.usbharu.hideout.core.domain.model.relationship.Relationship( + userId = userid, + targetUserId = targetId, + following = false, + blocking = false, + muting = false, + followRequest = false, + ignoreFollowRequestFromTarget = false + ) + + val inverseRelationship = relationshipRepository.findByUserIdAndTargetUserId(targetId, userid) + ?: dev.usbharu.hideout.core.domain.model.relationship.Relationship( + userId = targetId, + targetUserId = userid, + following = false, + blocking = false, + muting = false, + followRequest = false, + ignoreFollowRequestFromTarget = false + ) + + return Relationship( + id = targetId.toString(), + following = relationship.following, + showingReblogs = true, + notifying = false, + followedBy = inverseRelationship.following, + blocking = relationship.blocking, + blockedBy = inverseRelationship.blocking, + muting = relationship.muting, + mutingNotifications = relationship.muting, + requested = relationship.followRequest, + domainBlocking = false, + endorsed = false, + note = "" + ) + } + + private suspend fun isFollowing(userid: Long, target: Long): Boolean = + relationshipRepository.findByUserIdAndTargetUserId(userid, target)?.following ?: false + companion object { private val logger = LoggerFactory.getLogger(AccountApiServiceImpl::class.java) } diff --git a/src/main/kotlin/dev/usbharu/hideout/mastodon/service/app/AppApiService.kt b/src/main/kotlin/dev/usbharu/hideout/mastodon/service/app/AppApiService.kt index 7324dd33..c2139a27 100644 --- a/src/main/kotlin/dev/usbharu/hideout/mastodon/service/app/AppApiService.kt +++ b/src/main/kotlin/dev/usbharu/hideout/mastodon/service/app/AppApiService.kt @@ -55,12 +55,12 @@ class AppApiServiceImpl( registeredClientRepository.save(registeredClient) Application( - appsRequest.clientName, - "invalid-vapid-key", - appsRequest.website, - id, - clientSecret, - appsRequest.redirectUris + name = appsRequest.clientName, + vapidKey = "invalid-vapid-key", + website = appsRequest.website, + clientId = id, + clientSecret = clientSecret, + redirectUri = appsRequest.redirectUris ) } } diff --git a/src/main/resources/db/migration/V1__Init_DB.sql b/src/main/resources/db/migration/V1__Init_DB.sql index 1440fb61..234078e7 100644 --- a/src/main/resources/db/migration/V1__Init_DB.sql +++ b/src/main/resources/db/migration/V1__Init_DB.sql @@ -34,14 +34,7 @@ create table if not exists users unique ("name", "domain"), constraint fk_users_instance__id foreign key ("instance") references instance (id) on delete restrict on update restrict ); -create table if not exists follow_requests -( - id bigserial primary key, - user_id bigint not null, - follower_id bigint not null, - constraint fk_follow_requests_user_id__id foreign key (user_id) references users (id) on delete restrict on update restrict, - constraint fk_follow_requests_follower_id__id foreign key (follower_id) references users (id) on delete restrict on update restrict -); + create table if not exists media ( id bigint primary key, @@ -119,14 +112,7 @@ create table if not exists timelines is_pure_repost boolean not null, media_ids varchar(255) not null ); -create table if not exists users_followers -( - id bigserial primary key, - user_id bigint not null, - follower_id bigint not null, - constraint fk_users_followers_user_id__id foreign key (user_id) references users (id) on delete restrict on update restrict, - constraint fk_users_followers_follower_id__id foreign key (follower_id) references users (id) on delete restrict on update restrict -); + create table if not exists application_authorization ( id varchar(255) primary key, @@ -186,4 +172,19 @@ create table if not exists registered_client scopes varchar(1000) not null, client_settings varchar(2000) not null, token_settings varchar(2000) not null +); + +create table if not exists relationships +( + id bigserial primary key, + user_id bigint not null, + target_user_id bigint not null, + following boolean not null, + blocking boolean not null, + muting boolean not null, + follow_request boolean not null, + ignore_follow_request boolean not null, + constraint fk_relationships_user_id__id foreign key (user_id) references users (id) on delete restrict on update restrict, + constraint fk_relationships_target_user_id__id foreign key (target_user_id) references users (id) on delete restrict on update restrict, + unique (user_id, target_user_id) ) diff --git a/src/main/resources/logback.xml b/src/main/resources/logback.xml index 9ba872ba..26a50765 100644 --- a/src/main/resources/logback.xml +++ b/src/main/resources/logback.xml @@ -1,7 +1,9 @@ - %d{YYYY-MM-dd HH:mm:ss.SSS} [%thread] %-5level [%X{x-request-id}] %logger{36} - %msg%n + %d{YYYY-MM-dd HH:mm:ss.SSS} [%thread] %-5level [%X{x-request-id}] [%X{x-job-id}] %logger{36} - + %msg%n + diff --git a/src/main/resources/openapi/mastodon.yaml b/src/main/resources/openapi/mastodon.yaml index 3049c1d9..a43da7de 100644 --- a/src/main/resources/openapi/mastodon.yaml +++ b/src/main/resources/openapi/mastodon.yaml @@ -287,6 +287,89 @@ paths: schema: $ref: "#/components/schemas/Relationship" + /api/v1/accounts/{id}/block: + post: + tags: + - account + security: + - OAuth2: + - "write:blocks" + parameters: + - in: path + name: id + required: true + schema: + type: string + responses: + 200: + description: 成功 + content: + application/json: + schema: + $ref: "#/components/schemas/Relationship" + + /api/v1/accounts/{id}/unfollow: + post: + tags: + - account + security: + - OAuth2: + - "write:follows" + parameters: + - in: path + name: id + required: true + schema: + type: string + responses: + 200: + description: 成功 + content: + application/json: + schema: + $ref: "#/components/schemas/Relationship" + + /api/v1/accounts/{id}/unblock: + post: + tags: + - account + security: + - OAuth2: + - "write:blocks" + parameters: + - in: path + name: id + required: true + schema: + type: string + responses: + 200: + description: 成功 + content: + application/json: + schema: + $ref: "#/components/schemas/Relationship" + /api/v1/accounts/{id}/remove_from_followers: + post: + tags: + - account + security: + - OAuth2: + - "write:follows" + parameters: + - in: path + name: id + required: true + schema: + type: string + responses: + 200: + description: 成功 + content: + application/json: + schema: + $ref: "#/components/schemas/Relationship" + /api/v1/accounts/{id}/statuses: get: tags: diff --git a/src/test/kotlin/dev/usbharu/hideout/activitypub/domain/model/BlockTest.kt b/src/test/kotlin/dev/usbharu/hideout/activitypub/domain/model/BlockTest.kt new file mode 100644 index 00000000..093aa98d --- /dev/null +++ b/src/test/kotlin/dev/usbharu/hideout/activitypub/domain/model/BlockTest.kt @@ -0,0 +1,78 @@ +package dev.usbharu.hideout.activitypub.domain.model + +import com.fasterxml.jackson.module.kotlin.readValue +import dev.usbharu.hideout.application.config.ActivityPubConfig +import org.assertj.core.api.Assertions.assertThat +import org.intellij.lang.annotations.Language +import org.junit.jupiter.api.Test +import org.springframework.boot.test.json.BasicJsonTester + +class BlockTest { + @Test + fun blockDeserializeTest() { + @Language("JSON") val json = """{ + "@context" : [ "https://www.w3.org/ns/activitystreams", "https://w3id.org/security/v1", { + "manuallyApprovesFollowers" : "as:manuallyApprovesFollowers", + "sensitive" : "as:sensitive", + "Hashtag" : "as:Hashtag", + "quoteUrl" : "as:quoteUrl", + "toot" : "http://joinmastodon.org/ns#", + "Emoji" : "toot:Emoji", + "featured" : "toot:featured", + "discoverable" : "toot:discoverable", + "schema" : "http://schema.org#", + "PropertyValue" : "schema:PropertyValue", + "value" : "schema:value", + "misskey" : "https://misskey-hub.net/ns#", + "_misskey_content" : "misskey:_misskey_content", + "_misskey_quote" : "misskey:_misskey_quote", + "_misskey_reaction" : "misskey:_misskey_reaction", + "_misskey_votes" : "misskey:_misskey_votes", + "_misskey_summary" : "misskey:_misskey_summary", + "isCat" : "misskey:isCat", + "vcard" : "http://www.w3.org/2006/vcard/ns#" + } ], + "type" : "Block", + "id" : "https://misskey.usbharu.dev/blocks/9myf6e40vm", + "actor" : "https://misskey.usbharu.dev/users/97ws8y3rj6", + "object" : "https://test-hideout.usbharu.dev/users/test-user2" +} +""" + + val objectMapper = ActivityPubConfig().objectMapper() + + val block = objectMapper.readValue(json) + + val expected = Block( + "https://misskey.usbharu.dev/users/97ws8y3rj6", + "https://misskey.usbharu.dev/blocks/9myf6e40vm", + "https://test-hideout.usbharu.dev/users/test-user2" + ).apply { context = listOf("https://www.w3.org/ns/activitystreams", "https://w3id.org/security/v1") } + assertThat(block).isEqualTo(expected) + } + + @Test + fun blockSerializeTest() { + val basicJsonTester = BasicJsonTester(javaClass) + + val block = Block( + "https://misskey.usbharu.dev/users/97ws8y3rj6", + "https://misskey.usbharu.dev/blocks/9myf6e40vm", + "https://test-hideout.usbharu.dev/users/test-user2" + ).apply { context = listOf("https://www.w3.org/ns/activitystreams", "https://w3id.org/security/v1") } + + val objectMapper = ActivityPubConfig().objectMapper() + + val writeValueAsString = objectMapper.writeValueAsString(block) + + val from = basicJsonTester.from(writeValueAsString) + assertThat(from).extractingJsonPathStringValue("$.actor") + .isEqualTo("https://misskey.usbharu.dev/users/97ws8y3rj6") + assertThat(from).extractingJsonPathStringValue("$.id") + .isEqualTo("https://misskey.usbharu.dev/blocks/9myf6e40vm") + assertThat(from).extractingJsonPathStringValue("$.object") + .isEqualTo("https://test-hideout.usbharu.dev/users/test-user2") + assertThat(from).extractingJsonPathStringValue("$.type").isEqualTo("Block") + + } +} diff --git a/src/test/kotlin/dev/usbharu/hideout/activitypub/domain/model/RejectTest.kt b/src/test/kotlin/dev/usbharu/hideout/activitypub/domain/model/RejectTest.kt new file mode 100644 index 00000000..19c48209 --- /dev/null +++ b/src/test/kotlin/dev/usbharu/hideout/activitypub/domain/model/RejectTest.kt @@ -0,0 +1,93 @@ +package dev.usbharu.hideout.activitypub.domain.model + +import com.fasterxml.jackson.module.kotlin.readValue +import dev.usbharu.hideout.application.config.ActivityPubConfig +import org.assertj.core.api.Assertions.assertThat +import org.intellij.lang.annotations.Language +import org.junit.jupiter.api.Test +import org.springframework.boot.test.json.BasicJsonTester + +class RejectTest { + @Test + fun rejectDeserializeTest() { + @Language("JSON") val json = """{ + "@context" : [ "https://www.w3.org/ns/activitystreams", "https://w3id.org/security/v1", { + "manuallyApprovesFollowers" : "as:manuallyApprovesFollowers", + "sensitive" : "as:sensitive", + "Hashtag" : "as:Hashtag", + "quoteUrl" : "as:quoteUrl", + "toot" : "http://joinmastodon.org/ns#", + "Emoji" : "toot:Emoji", + "featured" : "toot:featured", + "discoverable" : "toot:discoverable", + "schema" : "http://schema.org#", + "PropertyValue" : "schema:PropertyValue", + "value" : "schema:value", + "misskey" : "https://misskey-hub.net/ns#", + "_misskey_content" : "misskey:_misskey_content", + "_misskey_quote" : "misskey:_misskey_quote", + "_misskey_reaction" : "misskey:_misskey_reaction", + "_misskey_votes" : "misskey:_misskey_votes", + "_misskey_summary" : "misskey:_misskey_summary", + "isCat" : "misskey:isCat", + "vcard" : "http://www.w3.org/2006/vcard/ns#" + } ], + "type" : "Reject", + "actor" : "https://misskey.usbharu.dev/users/97ws8y3rj6", + "object" : { + "id" : "https://misskey.usbharu.dev/follows/9mxh6mawru/97ws8y3rj6", + "type" : "Follow", + "actor" : "https://test-hideout.usbharu.dev/users/test-user2", + "object" : "https://misskey.usbharu.dev/users/97ws8y3rj6" + }, + "id" : "https://misskey.usbharu.dev/06407419-5aeb-4e2d-8885-aa54b03decf0" +} +""" + + val objectMapper = ActivityPubConfig().objectMapper() + + val reject = objectMapper.readValue(json) + + val expected = Reject( + "https://misskey.usbharu.dev/users/97ws8y3rj6", + "https://misskey.usbharu.dev/06407419-5aeb-4e2d-8885-aa54b03decf0", + Follow( + apObject = "https://misskey.usbharu.dev/users/97ws8y3rj6", + actor = "https://test-hideout.usbharu.dev/users/test-user2" + ) + ).apply { context = listOf("https://www.w3.org/ns/activitystreams", "https://w3id.org/security/v1") } + + assertThat(reject).isEqualTo(expected) + } + + @Test + fun rejectSerializeTest() { + val basicJsonTester = BasicJsonTester(javaClass) + + val reject = Reject( + "https://misskey.usbharu.dev/users/97ws8y3rj6", + "https://misskey.usbharu.dev/06407419-5aeb-4e2d-8885-aa54b03decf0", + Follow( + apObject = "https://misskey.usbharu.dev/users/97ws8y3rj6", + actor = "https://test-hideout.usbharu.dev/users/test-user2" + ) + ).apply { context = listOf("https://www.w3.org/ns/activitystreams", "https://w3id.org/security/v1") } + + val objectMapper = ActivityPubConfig().objectMapper() + + val writeValueAsString = objectMapper.writeValueAsString(reject) + + val from = basicJsonTester.from(writeValueAsString) + + assertThat(from).extractingJsonPathStringValue("$.actor") + .isEqualTo("https://misskey.usbharu.dev/users/97ws8y3rj6") + assertThat(from).extractingJsonPathStringValue("$.id") + .isEqualTo("https://misskey.usbharu.dev/06407419-5aeb-4e2d-8885-aa54b03decf0") + assertThat(from).extractingJsonPathStringValue("$.type").isEqualTo("Reject") + assertThat(from).extractingJsonPathStringValue("$.object.actor") + .isEqualTo("https://test-hideout.usbharu.dev/users/test-user2") + assertThat(from).extractingJsonPathStringValue("$.object.object") + .isEqualTo("https://misskey.usbharu.dev/users/97ws8y3rj6") + assertThat(from).extractingJsonPathStringValue("$.object.type").isEqualTo("Follow") + } +} diff --git a/src/test/kotlin/dev/usbharu/hideout/activitypub/service/activity/accept/APDeliverAcceptJobProcessorTest.kt b/src/test/kotlin/dev/usbharu/hideout/activitypub/service/activity/accept/APDeliverAcceptJobProcessorTest.kt new file mode 100644 index 00000000..4eeded48 --- /dev/null +++ b/src/test/kotlin/dev/usbharu/hideout/activitypub/service/activity/accept/APDeliverAcceptJobProcessorTest.kt @@ -0,0 +1,70 @@ +package dev.usbharu.hideout.activitypub.service.activity.accept + +import dev.usbharu.hideout.activitypub.domain.model.Accept +import dev.usbharu.hideout.activitypub.domain.model.Follow +import dev.usbharu.hideout.activitypub.service.common.APRequestService +import dev.usbharu.hideout.core.external.job.DeliverAcceptJob +import dev.usbharu.hideout.core.external.job.DeliverAcceptJobParam +import dev.usbharu.hideout.core.query.UserQueryService +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.TestTransaction +import utils.UserBuilder + +@ExtendWith(MockitoExtension::class) +class APDeliverAcceptJobProcessorTest { + + @Mock + private lateinit var apRequestService: APRequestService + + @Mock + private lateinit var userQueryService: UserQueryService + + @Mock + private lateinit var deliverAcceptJob: DeliverAcceptJob + + @Spy + private val transaction = TestTransaction + + @InjectMocks + private lateinit var apDeliverAcceptJobProcessor: APDeliverAcceptJobProcessor + + @Test + fun `process apPostが発行される`() = runTest { + val user = UserBuilder.localUserOf() + + whenever(userQueryService.findById(eq(1))).doReturn(user) + + val accept = Accept( + apObject = Follow( + apObject = "https://example.com", + actor = "https://remote.example.com" + ), + actor = "https://example.com" + ) + val param = DeliverAcceptJobParam( + accept = accept, + "https://remote.example.com", + 1 + ) + + apDeliverAcceptJobProcessor.process(param) + + verify(apRequestService, times(1)).apPost(eq("https://remote.example.com"), eq(accept), eq(user)) + } + + @Test + fun `job DeliverAcceptJobが返ってくる`() { + val actual = apDeliverAcceptJobProcessor.job() + + assertThat(actual).isEqualTo(deliverAcceptJob) + + } +} diff --git a/src/test/kotlin/dev/usbharu/hideout/activitypub/service/activity/accept/ApAcceptProcessorTest.kt b/src/test/kotlin/dev/usbharu/hideout/activitypub/service/activity/accept/ApAcceptProcessorTest.kt new file mode 100644 index 00000000..b9015e1d --- /dev/null +++ b/src/test/kotlin/dev/usbharu/hideout/activitypub/service/activity/accept/ApAcceptProcessorTest.kt @@ -0,0 +1,130 @@ +package dev.usbharu.hideout.activitypub.service.activity.accept + +import dev.usbharu.hideout.activitypub.domain.exception.IllegalActivityPubObjectException +import dev.usbharu.hideout.activitypub.domain.model.Accept +import dev.usbharu.hideout.activitypub.domain.model.Follow +import dev.usbharu.hideout.activitypub.domain.model.Like +import dev.usbharu.hideout.activitypub.service.common.ActivityPubProcessContext +import dev.usbharu.hideout.activitypub.service.common.ActivityType +import dev.usbharu.hideout.application.config.ActivityPubConfig +import dev.usbharu.hideout.core.query.UserQueryService +import dev.usbharu.hideout.core.service.relationship.RelationshipService +import dev.usbharu.httpsignature.common.HttpHeaders +import dev.usbharu.httpsignature.common.HttpMethod +import dev.usbharu.httpsignature.common.HttpRequest +import kotlinx.coroutines.test.runTest +import org.assertj.core.api.Assertions.assertThat +import org.junit.jupiter.api.DynamicTest +import org.junit.jupiter.api.DynamicTest.dynamicTest +import org.junit.jupiter.api.Test +import org.junit.jupiter.api.TestFactory +import org.junit.jupiter.api.assertThrows +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.TestTransaction +import utils.UserBuilder +import java.net.URL + + +@ExtendWith(MockitoExtension::class) +class ApAcceptProcessorTest { + + @Mock + private lateinit var userQueryService: UserQueryService + + @Mock + private lateinit var relationshipService: RelationshipService + + @Spy + private val transaction = TestTransaction + + @InjectMocks + private lateinit var apAcceptProcessor: ApAcceptProcessor + + @Test + fun `internalProcess objectがFollowの場合フォローを承認する`() = runTest { + + val json = """""" + val objectMapper = ActivityPubConfig().objectMapper() + val jsonNode = objectMapper.readTree(json) + + val accept = Accept( + apObject = Follow( + apObject = "https://example.com", + actor = "https://remote.example.com" + ), + actor = "https://example.com" + ) + val activity = ActivityPubProcessContext( + accept, jsonNode, HttpRequest( + URL("https://example.com"), + HttpHeaders(emptyMap()), HttpMethod.POST + ), null, true + ) + + val user = UserBuilder.localUserOf() + whenever(userQueryService.findByUrl(eq("https://example.com"))).doReturn(user) + val remoteUser = UserBuilder.remoteUserOf() + whenever(userQueryService.findByUrl(eq("https://remote.example.com"))).doReturn(remoteUser) + + apAcceptProcessor.internalProcess(activity) + + verify(relationshipService, times(1)).acceptFollowRequest(eq(user.id), eq(remoteUser.id), eq(false)) + } + + @Test + fun `internalProcess objectがFollow以外の場合IllegalActivityPubObjecExceptionが発生する`() = runTest { + val json = """""" + val objectMapper = ActivityPubConfig().objectMapper() + val jsonNode = objectMapper.readTree(json) + + val accept = Accept( + apObject = Like( + apObject = "https://example.com", + actor = "https://remote.example.com", + content = "", + id = "" + ), + actor = "https://example.com" + ) + val activity = ActivityPubProcessContext( + accept, jsonNode, HttpRequest( + URL("https://example.com"), + HttpHeaders(emptyMap()), HttpMethod.POST + ), null, true + ) + + assertThrows { + apAcceptProcessor.internalProcess(activity) + } + } + + @Test + fun `isSupproted Acceptにはtrue`() { + val actual = apAcceptProcessor.isSupported(ActivityType.Accept) + assertThat(actual).isTrue() + } + + @TestFactory + fun `isSupported Accept以外にはfalse`(): List { + return ActivityType + .values() + .filterNot { it == ActivityType.Accept } + .map { + dynamicTest("isSupported $it にはfalse") { + + val actual = apAcceptProcessor.isSupported(it) + assertThat(actual).isFalse() + } + } + } + + @Test + fun `type Acceptのclassjavaが返ってくる`() { + assertThat(apAcceptProcessor.type()).isEqualTo(Accept::class.java) + } +} diff --git a/src/test/kotlin/dev/usbharu/hideout/activitypub/service/activity/accept/ApSendAcceptServiceImplTest.kt b/src/test/kotlin/dev/usbharu/hideout/activitypub/service/activity/accept/ApSendAcceptServiceImplTest.kt new file mode 100644 index 00000000..3d06c4d3 --- /dev/null +++ b/src/test/kotlin/dev/usbharu/hideout/activitypub/service/activity/accept/ApSendAcceptServiceImplTest.kt @@ -0,0 +1,45 @@ +package dev.usbharu.hideout.activitypub.service.activity.accept + +import dev.usbharu.hideout.activitypub.domain.model.Accept +import dev.usbharu.hideout.activitypub.domain.model.Follow +import dev.usbharu.hideout.core.external.job.DeliverAcceptJob +import dev.usbharu.hideout.core.external.job.DeliverAcceptJobParam +import dev.usbharu.hideout.core.service.job.JobQueueParentService +import kotlinx.coroutines.test.runTest +import org.junit.jupiter.api.Test +import org.junit.jupiter.api.extension.ExtendWith +import org.mockito.InjectMocks +import org.mockito.Mock +import org.mockito.junit.jupiter.MockitoExtension +import org.mockito.kotlin.eq +import org.mockito.kotlin.times +import org.mockito.kotlin.verify +import utils.UserBuilder + +@ExtendWith(MockitoExtension::class) +class ApSendAcceptServiceImplTest { + + @Mock + private lateinit var jobQueueParentService: JobQueueParentService + + @Mock + private lateinit var deliverAcceptJob: DeliverAcceptJob + + @InjectMocks + private lateinit var apSendAcceptServiceImpl: ApSendAcceptServiceImpl + + @Test + fun `sendAccept DeliverAcceptJobが発行される`() = runTest { + val user = UserBuilder.localUserOf() + val remoteUser = UserBuilder.remoteUserOf() + + apSendAcceptServiceImpl.sendAcceptFollow(user, remoteUser) + + val deliverAcceptJobParam = DeliverAcceptJobParam( + Accept(apObject = Follow(apObject = user.url, actor = remoteUser.url), actor = user.url), + remoteUser.inbox, + user.id + ) + verify(jobQueueParentService, times(1)).scheduleTypeSafe(eq(deliverAcceptJob), eq(deliverAcceptJobParam)) + } +} diff --git a/src/test/kotlin/dev/usbharu/hideout/activitypub/service/activity/block/APDeliverBlockJobProcessorTest.kt b/src/test/kotlin/dev/usbharu/hideout/activitypub/service/activity/block/APDeliverBlockJobProcessorTest.kt new file mode 100644 index 00000000..55e52ff9 --- /dev/null +++ b/src/test/kotlin/dev/usbharu/hideout/activitypub/service/activity/block/APDeliverBlockJobProcessorTest.kt @@ -0,0 +1,78 @@ +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.activitypub.service.common.APRequestService +import dev.usbharu.hideout.core.domain.model.user.UserRepository +import dev.usbharu.hideout.core.external.job.DeliverBlockJob +import dev.usbharu.hideout.core.external.job.DeliverBlockJobParam +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.TestTransaction +import utils.UserBuilder + +@ExtendWith(MockitoExtension::class) +class APDeliverBlockJobProcessorTest { + + @Mock + private lateinit var apRequestService: APRequestService + + @Mock + private lateinit var userRepository: UserRepository + + @Spy + private val transaction = TestTransaction + + @Mock + private lateinit var deliverBlockJob: DeliverBlockJob + + @InjectMocks + private lateinit var apDeliverBlockJobProcessor: APDeliverBlockJobProcessor + + @Test + fun `process rejectとblockがapPostされる`() = runTest { + val user = UserBuilder.localUserOf() + whenever(userRepository.findById(eq(user.id))).doReturn(user) + + + val block = Block( + actor = user.url, + "https://example.com/block", + apObject = "https://remote.example.com" + ) + val reject = Reject( + actor = user.url, + "https://example.com/reject/follow", + apObject = Follow( + apObject = user.url, + actor = "https://remote.example.com" + ) + ) + val param = DeliverBlockJobParam( + user.id, + block, + reject, + "https://remote.example.com" + ) + + + apDeliverBlockJobProcessor.process(param) + + verify(apRequestService, times(1)).apPost(eq("https://remote.example.com"), eq(block), eq(user)) + verify(apRequestService, times(1)).apPost(eq("https://remote.example.com"), eq(reject), eq(user)) + } + + @Test + fun `job deliverBlockJobが返ってくる`() { + val actual = apDeliverBlockJobProcessor.job() + assertThat(actual).isEqualTo(deliverBlockJob) + } +} diff --git a/src/test/kotlin/dev/usbharu/hideout/ap/ContextSerializerTest.kt b/src/test/kotlin/dev/usbharu/hideout/ap/ContextSerializerTest.kt index a141c11b..1403b190 100644 --- a/src/test/kotlin/dev/usbharu/hideout/ap/ContextSerializerTest.kt +++ b/src/test/kotlin/dev/usbharu/hideout/ap/ContextSerializerTest.kt @@ -10,7 +10,6 @@ class ContextSerializerTest { @Test fun serialize() { val accept = Accept( - name = "aaa", actor = "bbb", apObject = Follow( apObject = "ddd", diff --git a/src/test/kotlin/dev/usbharu/hideout/core/service/relationship/RelationshipServiceImplTest.kt b/src/test/kotlin/dev/usbharu/hideout/core/service/relationship/RelationshipServiceImplTest.kt new file mode 100644 index 00000000..be2fca26 --- /dev/null +++ b/src/test/kotlin/dev/usbharu/hideout/core/service/relationship/RelationshipServiceImplTest.kt @@ -0,0 +1,786 @@ +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.model.relationship.Relationship +import dev.usbharu.hideout.core.domain.model.relationship.RelationshipRepository +import dev.usbharu.hideout.core.query.UserQueryService +import dev.usbharu.hideout.core.service.follow.SendFollowDto +import kotlinx.coroutines.test.runTest +import org.junit.jupiter.api.Test +import org.junit.jupiter.api.assertThrows +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 RelationshipServiceImplTest { + + @Spy + private val applicationConfig = ApplicationConfig(URL("https://example.com")) + + @Mock + private lateinit var userQueryService: UserQueryService + + @Mock + private lateinit var relationshipRepository: RelationshipRepository + + @Mock + private lateinit var apSendFollowService: APSendFollowService + + @Mock + private lateinit var apSendBlockService: APSendBlockService + + @Mock + private lateinit var apSendAcceptService: ApSendAcceptService + + @Mock + private lateinit var apSendRejectService: ApSendRejectService + + @Mock + private lateinit var apSendUndoService: APSendUndoService + + @InjectMocks + private lateinit var relationshipServiceImpl: RelationshipServiceImpl + + @Test + fun `followRequest ローカルの場合followRequestフラグがtrueで永続化される`() = runTest { + whenever(userQueryService.findById(eq(5678))).doReturn(UserBuilder.localUserOf(domain = "example.com")) + + relationshipServiceImpl.followRequest(1234, 5678) + + verify(relationshipRepository, times(1)).save( + eq( + Relationship( + userId = 1234, + targetUserId = 5678, + following = false, + blocking = false, + muting = false, + followRequest = true, + ignoreFollowRequestFromTarget = false + ) + ) + ) + } + + @Test + fun `followRequest リモートの場合Followアクティビティが配送される`() = runTest { + val localUser = UserBuilder.localUserOf(domain = "example.com") + whenever(userQueryService.findById(eq(1234))).doReturn(localUser) + val remoteUser = UserBuilder.remoteUserOf(domain = "remote.example.com") + whenever(userQueryService.findById(eq(5678))).doReturn(remoteUser) + + relationshipServiceImpl.followRequest(1234, 5678) + + verify(relationshipRepository, times(1)).save( + eq( + Relationship( + userId = 1234, + targetUserId = 5678, + following = false, + blocking = false, + muting = false, + followRequest = true, + ignoreFollowRequestFromTarget = false + ) + ) + ) + + verify(apSendFollowService, times(1)).sendFollow(eq(SendFollowDto(localUser, remoteUser))) + } + + @Test + fun `followRequest ブロックされている場合フォローリクエスト出来ない`() = runTest { + whenever(relationshipRepository.findByUserIdAndTargetUserId(eq(1234), eq(5678))).doReturn(null) + whenever(relationshipRepository.findByUserIdAndTargetUserId(eq(5678), eq(1234))).doReturn( + Relationship( + userId = 5678, + targetUserId = 1234, + following = false, + blocking = true, + muting = false, + followRequest = false, + ignoreFollowRequestFromTarget = false + ) + ) + + relationshipServiceImpl.followRequest(1234, 5678) + + verify(relationshipRepository, never()).save(any()) + } + + @Test + fun `followRequest ブロックしている場合フォローリクエスト出来ない`() = runTest { + whenever(relationshipRepository.findByUserIdAndTargetUserId(eq(1234), eq(5678))).doReturn( + Relationship( + userId = 1234, + targetUserId = 5678, + following = false, + blocking = true, + muting = false, + followRequest = false, + ignoreFollowRequestFromTarget = false + ) + ) + + relationshipServiceImpl.followRequest(1234, 5678) + + verify(relationshipRepository, never()).save(any()) + } + + @Test + fun `followRequest 既にフォローしている場合は念の為フォロー承認を自動で行う`() = runTest { + val remoteUser = UserBuilder.remoteUserOf(domain = "remote.example.com") + whenever(userQueryService.findById(eq(1234))).doReturn(remoteUser) + val localUser = UserBuilder.localUserOf(domain = "example.com") + whenever(userQueryService.findById(eq(5678))).doReturn(localUser) + whenever(relationshipRepository.findByUserIdAndTargetUserId(eq(1234), eq(5678))).doReturn( + Relationship( + userId = 1234, + targetUserId = 5678, + following = true, + blocking = false, + muting = false, + followRequest = false, + ignoreFollowRequestFromTarget = false + ) + ) + + relationshipServiceImpl.followRequest(1234, 5678) + + verify(relationshipRepository, times(1)).save( + eq( + Relationship( + userId = 1234, + targetUserId = 5678, + following = true, + blocking = false, + muting = false, + followRequest = false, + ignoreFollowRequestFromTarget = false + ) + ) + ) + + verify(apSendAcceptService, times(1)).sendAcceptFollow(eq(localUser), eq(remoteUser)) + verify(apSendFollowService, never()).sendFollow(any()) + } + + @Test + fun `followRequest フォローリクエスト無視の場合は無視する`() = runTest { + whenever(relationshipRepository.findByUserIdAndTargetUserId(eq(1234), eq(5678))).doReturn( + Relationship( + userId = 1234, + targetUserId = 5678, + following = false, + blocking = false, + muting = false, + followRequest = false, + ignoreFollowRequestFromTarget = false + ) + ) + + whenever(relationshipRepository.findByUserIdAndTargetUserId(eq(5678), eq(1234))).doReturn( + Relationship( + userId = 5678, + targetUserId = 1234, + following = false, + blocking = false, + muting = false, + followRequest = false, + ignoreFollowRequestFromTarget = true + ) + ) + + relationshipServiceImpl.followRequest(1234, 5678) + + verify(relationshipRepository, never()).save(any()) + } + + @Test + fun `block ローカルユーザーの場合永続化される`() = runTest { + whenever(userQueryService.findById(eq(5678))).doReturn(UserBuilder.localUserOf(domain = "example.com")) + + relationshipServiceImpl.block(1234, 5678) + + verify(relationshipRepository, times(1)).save( + eq( + Relationship( + userId = 1234, + targetUserId = 5678, + following = false, + blocking = true, + muting = false, + followRequest = false, + ignoreFollowRequestFromTarget = false + ) + ) + ) + } + + @Test + fun `block リモートユーザーの場合永続化されて配送される`() = runTest { + val localUser = UserBuilder.localUserOf(domain = "example.com") + whenever(userQueryService.findById(eq(1234))).doReturn(localUser) + val remoteUser = UserBuilder.remoteUserOf(domain = "remote.example.com") + whenever(userQueryService.findById(eq(5678))).doReturn(remoteUser) + + relationshipServiceImpl.block(1234, 5678) + + verify(relationshipRepository, times(1)).save( + eq( + Relationship( + userId = 1234, + targetUserId = 5678, + following = false, + blocking = true, + muting = false, + followRequest = false, + ignoreFollowRequestFromTarget = false + ) + ) + ) + + verify(apSendBlockService, times(1)).sendBlock(eq(localUser), eq(remoteUser)) + } + + @Test + fun `acceptFollowRequest ローカルユーザーの場合永続化される`() = runTest { + whenever(userQueryService.findById(eq(5678))).doReturn(UserBuilder.localUserOf(domain = "example.com")) + + whenever(relationshipRepository.findByUserIdAndTargetUserId(eq(5678), eq(1234))).doReturn( + Relationship( + userId = 5678, + targetUserId = 1234, + following = false, + blocking = false, + muting = false, + followRequest = true, + ignoreFollowRequestFromTarget = false + ) + ) + + relationshipServiceImpl.acceptFollowRequest(1234, 5678, false) + + verify(relationshipRepository, times(1)).save( + Relationship( + userId = 5678, + targetUserId = 1234, + following = true, + blocking = false, + muting = false, + followRequest = false, + ignoreFollowRequestFromTarget = false + ) + ) + + verify(apSendAcceptService, never()).sendAcceptFollow(any(), any()) + } + + @Test + fun `acceptFollowRequest リモートユーザーの場合永続化されて配送される`() = runTest { + val localUser = UserBuilder.localUserOf(domain = "example.com") + whenever(userQueryService.findById(eq(1234))).doReturn(localUser) + val remoteUser = UserBuilder.remoteUserOf(domain = "remote.example.com") + whenever(userQueryService.findById(eq(5678))).doReturn(remoteUser) + + whenever(relationshipRepository.findByUserIdAndTargetUserId(eq(5678), eq(1234))).doReturn( + Relationship( + userId = 5678, + targetUserId = 1234, + following = false, + blocking = false, + muting = false, + followRequest = true, + ignoreFollowRequestFromTarget = false + ) + ) + + relationshipServiceImpl.acceptFollowRequest(1234, 5678, false) + + verify(relationshipRepository, times(1)).save( + eq( + Relationship( + userId = 5678, + targetUserId = 1234, + following = true, + blocking = false, + muting = false, + followRequest = false, + ignoreFollowRequestFromTarget = false + ) + ) + ) + + verify(apSendAcceptService, times(1)).sendAcceptFollow(eq(localUser), eq(remoteUser)) + } + + @Test + fun `acceptFollowRequest Relationshipが存在しないときは何もしない`() = runTest { + relationshipServiceImpl.acceptFollowRequest(1234, 5678, false) + + verify(apSendAcceptService, never()).sendAcceptFollow(any(), any()) + } + + @Test + fun `acceptFollowRequest フォローリクエストが存在せずforceがfalseのとき何もしない`() = runTest { + whenever(relationshipRepository.findByUserIdAndTargetUserId(eq(5678), eq(1234))).doReturn( + Relationship( + 5678, 1234, false, false, false, false, false + ) + ) + + relationshipServiceImpl.acceptFollowRequest(1234, 5678, false) + + verify(apSendAcceptService, never()).sendAcceptFollow(any(), any()) + } + + @Test + fun `acceptFollowRequest フォローリクエストが存在せずforceがtrueのときフォローを承認する`() = runTest { + whenever(userQueryService.findById(eq(5678))).doReturn(UserBuilder.remoteUserOf(domain = "remote.example.com")) + + whenever(relationshipRepository.findByUserIdAndTargetUserId(eq(5678), eq(1234))).doReturn( + Relationship( + 5678, 1234, false, false, false, false, false + ) + ) + + relationshipServiceImpl.acceptFollowRequest(1234, 5678, true) + + verify(relationshipRepository, times(1)).save( + eq( + Relationship( + userId = 5678, + targetUserId = 1234, + following = true, + blocking = false, + muting = false, + followRequest = false, + ignoreFollowRequestFromTarget = false + ) + ) + ) + } + + @Test + fun `acceptFollowRequest ブロックしている場合は何もしない`() = runTest { + whenever(relationshipRepository.findByUserIdAndTargetUserId(eq(5678), eq(1234))).doReturn( + Relationship( + 5678, 1234, false, true, false, true, false + ) + ) + + assertThrows { + relationshipServiceImpl.acceptFollowRequest(1234, 5678, false) + } + + verify(relationshipRepository, never()).save(any()) + } + + @Test + fun `acceptFollowRequest ブロックされている場合は何もしない`() = runTest { + whenever(relationshipRepository.findByUserIdAndTargetUserId(eq(1234), eq(5678))).doReturn( + Relationship( + 1234, 5678, false, false, false, true, false + ) + ) + + whenever(relationshipRepository.findByUserIdAndTargetUserId(eq(5678), eq(1234))).doReturn( + Relationship( + 5678, 1234, false, true, false, true, false + ) + ) + + assertThrows { + relationshipServiceImpl.acceptFollowRequest(1234, 5678, false) + } + + verify(relationshipRepository, never()).save(any()) + } + + @Test + fun `rejectFollowRequest ローカルユーザーの場合永続化される`() = runTest { + whenever(userQueryService.findById(eq(5678))).doReturn(UserBuilder.localUserOf(domain = "example.com")) + + whenever(relationshipRepository.findByUserIdAndTargetUserId(eq(5678), eq(1234))).doReturn( + Relationship( + userId = 5678, + targetUserId = 1234, + following = false, + blocking = false, + muting = false, + followRequest = true, + ignoreFollowRequestFromTarget = false + ) + ) + + relationshipServiceImpl.rejectFollowRequest(1234, 5678) + + verify(relationshipRepository, times(1)).save( + eq( + Relationship( + userId = 5678, + targetUserId = 1234, + following = false, + blocking = false, + muting = false, + followRequest = false, + ignoreFollowRequestFromTarget = false + ) + ) + ) + + verify(apSendRejectService, never()).sendRejectFollow(any(), any()) + } + + @Test + fun `rejectFollowRequest リモートユーザーの場合永続化されて配送される`() = runTest { + val localUser = UserBuilder.localUserOf(domain = "example.com") + whenever(userQueryService.findById(eq(1234))).doReturn(localUser) + + val remoteUser = UserBuilder.remoteUserOf(domain = "remote.example.com") + whenever(userQueryService.findById(eq(5678))).doReturn(remoteUser) + + whenever(relationshipRepository.findByUserIdAndTargetUserId(eq(5678), eq(1234))).doReturn( + Relationship( + userId = 5678, + targetUserId = 1234, + following = false, + blocking = false, + muting = false, + followRequest = true, + ignoreFollowRequestFromTarget = false + ) + ) + + relationshipServiceImpl.rejectFollowRequest(1234, 5678) + + verify(relationshipRepository, times(1)).save( + eq( + Relationship( + userId = 5678, + targetUserId = 1234, + following = false, + blocking = false, + muting = false, + followRequest = false, + ignoreFollowRequestFromTarget = false + ) + ) + ) + + verify(apSendRejectService, times(1)).sendRejectFollow(eq(localUser), eq(remoteUser)) + } + + @Test + fun `rejectFollowRequest Relationshipが存在しないとき何もしない`() = runTest { + + relationshipServiceImpl.rejectFollowRequest(1234, 5678) + + verify(relationshipRepository, never()).save(any()) + } + + @Test + fun `rejectFollowRequest フォローリクエストが存在しない場合何もしない`() = runTest { + whenever(relationshipRepository.findByUserIdAndTargetUserId(eq(5678), eq(1234))).doReturn( + Relationship( + userId = 5678, + targetUserId = 1234, + following = false, + blocking = false, + muting = false, + followRequest = false, + ignoreFollowRequestFromTarget = false + ) + ) + + relationshipServiceImpl.rejectFollowRequest(1234, 5678) + + verify(relationshipRepository, never()).save(any()) + } + + @Test + fun `ignoreFollowRequest 永続化される`() = runTest { + relationshipServiceImpl.ignoreFollowRequest(1234, 5678) + + verify(relationshipRepository, times(1)).save( + eq( + Relationship( + userId = 1234, + targetUserId = 5678, + following = false, + blocking = false, + muting = false, + followRequest = false, + ignoreFollowRequestFromTarget = true + ) + ) + ) + } + + @Test + fun `unfollow ローカルユーザーの場合永続化される`() = runTest { + whenever(userQueryService.findById(eq(5678))).doReturn(UserBuilder.localUserOf(domain = "example.com")) + whenever(relationshipRepository.findByUserIdAndTargetUserId(eq(1234), eq(5678))).doReturn( + Relationship( + userId = 1234, + targetUserId = 5678, + following = true, + blocking = false, + muting = false, + followRequest = false, + ignoreFollowRequestFromTarget = false + ) + ) + + relationshipServiceImpl.unfollow(1234, 5678) + + verify(relationshipRepository, times(1)).save( + eq( + Relationship( + userId = 1234, + targetUserId = 5678, + following = false, + blocking = false, + muting = false, + followRequest = false, + ignoreFollowRequestFromTarget = false + ) + ) + ) + + verify(apSendUndoService, never()).sendUndoFollow(any(), any()) + } + + @Test + fun `unfollow リモートユーザー場合永続化されて配送される`() = runTest { + val localUser = UserBuilder.localUserOf(domain = "example.com") + whenever(userQueryService.findById(eq(1234))).doReturn(localUser) + + val remoteUser = UserBuilder.remoteUserOf(domain = "remote.example.com") + whenever(userQueryService.findById(eq(5678))).doReturn(remoteUser) + + whenever(relationshipRepository.findByUserIdAndTargetUserId(eq(1234), eq(5678))).doReturn( + Relationship( + userId = 1234, + targetUserId = 5678, + following = true, + blocking = false, + muting = false, + followRequest = false, + ignoreFollowRequestFromTarget = false + ) + ) + + relationshipServiceImpl.unfollow(1234, 5678) + + verify(relationshipRepository, times(1)).save( + eq( + Relationship( + userId = 1234, + targetUserId = 5678, + following = false, + blocking = false, + muting = false, + followRequest = false, + ignoreFollowRequestFromTarget = false + ) + ) + ) + + verify(apSendUndoService, times(1)).sendUndoFollow(eq(localUser), eq(remoteUser)) + } + + @Test + fun `unfollow Relationshipが存在しないときは何もしない`() = runTest { + relationshipServiceImpl.unfollow(1234, 5678) + + verify(relationshipRepository, never()).save(any()) + } + + @Test + fun `unfollow フォローしていなかった場合は何もしない`() = runTest { + whenever(relationshipRepository.findByUserIdAndTargetUserId(eq(1234), eq(5678))).doReturn( + Relationship( + userId = 1234, + targetUserId = 5678, + following = false, + blocking = false, + muting = false, + followRequest = false, + ignoreFollowRequestFromTarget = false + ) + ) + + relationshipServiceImpl.unfollow(1234, 5678) + + verify(relationshipRepository, never()).save(any()) + } + + @Test + fun `unblock ローカルユーザーの場合永続化される`() = runTest { + whenever(userQueryService.findById(eq(5678))).doReturn(UserBuilder.localUserOf(domain = "example.com")) + whenever(relationshipRepository.findByUserIdAndTargetUserId(eq(1234), eq(5678))).doReturn( + Relationship( + userId = 1234, + targetUserId = 5678, + following = false, + blocking = true, + muting = false, + followRequest = false, + ignoreFollowRequestFromTarget = false + ) + ) + + relationshipServiceImpl.unblock(1234, 5678) + + verify(relationshipRepository, times(1)).save( + eq( + Relationship( + userId = 1234, + targetUserId = 5678, + following = false, + blocking = false, + muting = false, + followRequest = false, + ignoreFollowRequestFromTarget = false + ) + ) + ) + + verify(apSendUndoService, never()).sendUndoBlock(any(), any()) + } + + @Test + fun `unblock リモートユーザーの場合永続化されて配送される`() = runTest { + val localUser = UserBuilder.localUserOf(domain = "example.com") + whenever(userQueryService.findById(eq(1234))).doReturn(localUser) + + val remoteUser = UserBuilder.remoteUserOf(domain = "remote.example.com") + whenever(userQueryService.findById(eq(5678))).doReturn(remoteUser) + + whenever(relationshipRepository.findByUserIdAndTargetUserId(eq(1234), eq(5678))).doReturn( + Relationship( + userId = 1234, + targetUserId = 5678, + following = false, + blocking = true, + muting = false, + followRequest = false, + ignoreFollowRequestFromTarget = false + ) + ) + + relationshipServiceImpl.unblock(1234, 5678) + + verify(relationshipRepository, times(1)).save( + eq( + Relationship( + 1234, + 5678, + false, + false, + false, + false, + false + ) + ) + ) + + verify(apSendUndoService, times(1)).sendUndoBlock(eq(localUser), eq(remoteUser)) + } + + @Test + fun `unblock Relationshipがない場合何もしない`() = runTest { + relationshipServiceImpl.unblock(1234, 5678) + + verify(relationshipRepository, never()).save(any()) + } + + @Test + fun `unblock ブロックしていない場合は何もしない`() = runTest { + whenever(relationshipRepository.findByUserIdAndTargetUserId(eq(1234), eq(5678))).doReturn( + Relationship( + userId = 1234, + targetUserId = 5678, + following = false, + blocking = false, + muting = false, + followRequest = false, + ignoreFollowRequestFromTarget = false + ) + ) + + relationshipServiceImpl.unblock(1234, 5678) + + verify(relationshipRepository, never()).save(any()) + } + + @Test + fun `mute ミュートが永続化される`() = runTest { + relationshipServiceImpl.mute(1234, 5678) + + verify(relationshipRepository, times(1)).save( + eq( + Relationship( + userId = 1234, + targetUserId = 5678, + following = false, + blocking = false, + muting = true, + followRequest = false, + ignoreFollowRequestFromTarget = false + ) + ) + ) + } + + @Test + fun `unmute 永続化される`() = runTest { + + whenever(relationshipRepository.findByUserIdAndTargetUserId(eq(1234), eq(5678))).doReturn( + Relationship( + userId = 1234, + targetUserId = 5678, + following = false, + blocking = false, + muting = true, + followRequest = false, + ignoreFollowRequestFromTarget = false + ) + ) + + relationshipServiceImpl.unmute(1234, 5678) + + verify(relationshipRepository, times(1)).save( + eq( + Relationship( + userId = 1234, + targetUserId = 5678, + following = false, + blocking = false, + muting = false, + followRequest = false, + ignoreFollowRequestFromTarget = false + ) + ) + ) + } + + @Test + fun `unmute Relationshipが存在しない場合は何もしない`() = runTest { + relationshipServiceImpl.unmute(1234, 5678) + + verify(relationshipRepository, never()).save(any()) + } +} diff --git a/src/test/kotlin/dev/usbharu/hideout/core/service/user/UserServiceTest.kt b/src/test/kotlin/dev/usbharu/hideout/core/service/user/UserServiceTest.kt index 65a88e8b..c524a0f5 100644 --- a/src/test/kotlin/dev/usbharu/hideout/core/service/user/UserServiceTest.kt +++ b/src/test/kotlin/dev/usbharu/hideout/core/service/user/UserServiceTest.kt @@ -37,8 +37,6 @@ class UserServiceTest { userRepository, userAuthService, mock(), - mock(), - mock(), userBuilder, testApplicationConfig, mock() @@ -68,7 +66,7 @@ class UserServiceTest { onBlocking { nextId() } doReturn 113345L } val userService = - UserServiceImpl(userRepository, mock(), mock(), mock(), mock(), userBuilder, testApplicationConfig, mock()) + UserServiceImpl(userRepository, mock(), mock(), userBuilder, testApplicationConfig, mock()) val user = RemoteUserCreateDto( name = "test", domain = "remote.example.com", diff --git a/src/test/kotlin/dev/usbharu/hideout/mastodon/service/account/AccountApiServiceImplTest.kt b/src/test/kotlin/dev/usbharu/hideout/mastodon/service/account/AccountApiServiceImplTest.kt index 5cc6a833..19834065 100644 --- a/src/test/kotlin/dev/usbharu/hideout/mastodon/service/account/AccountApiServiceImplTest.kt +++ b/src/test/kotlin/dev/usbharu/hideout/mastodon/service/account/AccountApiServiceImplTest.kt @@ -1,8 +1,10 @@ package dev.usbharu.hideout.mastodon.service.account import dev.usbharu.hideout.application.external.Transaction +import dev.usbharu.hideout.core.domain.model.relationship.RelationshipRepository import dev.usbharu.hideout.core.domain.model.user.UserRepository import dev.usbharu.hideout.core.query.FollowerQueryService +import dev.usbharu.hideout.core.service.relationship.RelationshipService import dev.usbharu.hideout.core.service.user.UserService import dev.usbharu.hideout.domain.mastodon.model.generated.Account import dev.usbharu.hideout.domain.mastodon.model.generated.Relationship @@ -40,6 +42,12 @@ class AccountApiServiceImplTest { @Spy private val transaction: Transaction = TestTransaction + @Mock + private lateinit var relationshipService: RelationshipService + + @Mock + private lateinit var relationshipRepository: RelationshipRepository + @InjectMocks private lateinit var accountApiServiceImpl: AccountApiServiceImpl @@ -157,9 +165,6 @@ class AccountApiServiceImplTest { ) ).doReturn(statusList) - whenever(followerQueryService.alreadyFollow(eq(userId), eq(loginUser))).doReturn(false) - - val accountsStatuses = accountApiServiceImpl.accountsStatuses( userid = userId, maxId = null, @@ -197,7 +202,17 @@ class AccountApiServiceImplTest { ) ).doReturn(statusList) - whenever(followerQueryService.alreadyFollow(eq(userId), eq(loginUser))).doReturn(true) + whenever(relationshipRepository.findByUserIdAndTargetUserId(eq(loginUser), eq(userId))).doReturn( + dev.usbharu.hideout.core.domain.model.relationship.Relationship( + userId = loginUser, + targetUserId = userId, + following = true, + blocking = false, + muting = false, + followRequest = false, + ignoreFollowRequestFromTarget = false + ) + ) val accountsStatuses = accountApiServiceImpl.accountsStatuses( @@ -217,51 +232,34 @@ class AccountApiServiceImplTest { assertThat(accountsStatuses).hasSize(1) } - @Test - fun `follow 既にフォローしている場合は何もしない`() = runTest { - val userId = 1234L - val followeeId = 1L - - whenever(followerQueryService.alreadyFollow(eq(followeeId), eq(userId))).doReturn(true) - - whenever(followerQueryService.alreadyFollow(eq(userId), eq(followeeId))).doReturn(true) - - whenever(userRepository.findFollowRequestsById(eq(followeeId), eq(userId))).doReturn(false) - - val follow = accountApiServiceImpl.follow(userId, followeeId) - - val expected = Relationship( - id = followeeId.toString(), - following = true, - showingReblogs = true, - notifying = false, - followedBy = true, - blocking = false, - blockedBy = false, - muting = false, - mutingNotifications = false, - requested = false, - domainBlocking = false, - endorsed = false, - note = "" - ) - assertThat(follow).isEqualTo(expected) - - verify(userService, never()).followRequest(any(), any()) - } - @Test fun `follow 未フォローの場合フォローリクエストが発生する`() = runTest { val userId = 1234L val followeeId = 1L - whenever(followerQueryService.alreadyFollow(eq(followeeId), eq(userId))).doReturn(false) + whenever(relationshipRepository.findByUserIdAndTargetUserId(eq(followeeId), eq(userId))).doReturn( + dev.usbharu.hideout.core.domain.model.relationship.Relationship( + userId = followeeId, + targetUserId = userId, + following = true, + blocking = false, + muting = false, + followRequest = false, + ignoreFollowRequestFromTarget = false + ) + ) + whenever(relationshipRepository.findByUserIdAndTargetUserId(eq(userId), eq(followeeId))).doReturn( + dev.usbharu.hideout.core.domain.model.relationship.Relationship( + userId = userId, + targetUserId = followeeId, + following = true, + blocking = false, + muting = false, + followRequest = false, + ignoreFollowRequestFromTarget = false + ) + ) - whenever(userService.followRequest(eq(followeeId), eq(userId))).doReturn(true) - - whenever(followerQueryService.alreadyFollow(eq(userId), eq(followeeId))).doReturn(true) - - whenever(userRepository.findFollowRequestsById(eq(followeeId), eq(userId))).doReturn(false) val follow = accountApiServiceImpl.follow(userId, followeeId) @@ -282,14 +280,11 @@ class AccountApiServiceImplTest { ) assertThat(follow).isEqualTo(expected) - verify(userService, times(1)).followRequest(eq(followeeId), eq(userId)) + verify(relationshipService, times(1)).followRequest(eq(userId), eq(followeeId)) } @Test fun `relationships idが長すぎたら省略する`() = runTest { - whenever(followerQueryService.alreadyFollow(any(), any())).doReturn(true) - - whenever(userRepository.findFollowRequestsById(any(), any())).doReturn(true) val relationships = accountApiServiceImpl.relationships( userid = 1234L, @@ -297,7 +292,7 @@ class AccountApiServiceImplTest { withSuspended = false ) - assertThat(relationships).hasSizeLessThanOrEqualTo(20) + assertThat(relationships).hasSize(20) } @Test @@ -310,14 +305,10 @@ class AccountApiServiceImplTest { assertThat(relationships).hasSize(0) verify(followerQueryService, never()).alreadyFollow(any(), any()) - verify(userRepository, never()).findFollowRequestsById(any(), any()) } @Test fun `relationships idに指定されたアカウントの関係を取得する`() = runTest { - whenever(followerQueryService.alreadyFollow(any(), any())).doReturn(true) - - whenever(userRepository.findFollowRequestsById(any(), any())).doReturn(true) val relationships = accountApiServiceImpl.relationships( userid = 1234L,