Merge pull request #205 from usbharu/feature/refactor-user-relationship

Feature/refactor user relationship
This commit is contained in:
usbharu 2023-12-11 14:49:14 +09:00 committed by GitHub
commit 32cd7ad1db
65 changed files with 2782 additions and 553 deletions

View File

@ -287,7 +287,7 @@ project.gradle.taskGraph.whenReady {
kover {
excludeSourceSets {
names("aot")
names("aot", "e2eTest", "intTest")
}
}

View File

@ -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,

View File

@ -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)

View File

@ -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)

View File

@ -8,7 +8,6 @@ import dev.usbharu.hideout.activitypub.domain.model.objects.ObjectDeserializer
open class Accept @JsonCreator constructor(
type: List<String> = 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'" +
")" +

View File

@ -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()}"
}
}

View File

@ -7,7 +7,7 @@ import dev.usbharu.hideout.activitypub.domain.model.objects.ObjectDeserializer
open class Create(
type: List<String> = 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', " +

View File

@ -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()}"
}
}

View File

@ -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()}"
}

View File

@ -44,7 +44,7 @@ class ObjectDeserializer : JsonDeserializer<Object>() {
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<Object>() {
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()

View File

@ -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<DeliverAcceptJobParam, DeliverAcceptJob> {
override suspend fun process(param: DeliverAcceptJobParam): Unit = transaction.transaction {
apRequestService.apPost(param.inbox, param.accept, userQueryService.findById(param.signer))
}
override fun job(): DeliverAcceptJob = deliverAcceptJob
}

View File

@ -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<Accept>(transaction) {
override suspend fun internalProcess(activity: ActivityPubProcessContext<Accept>) {
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

View File

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

View File

@ -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<DeliverBlockJobParam, DeliverBlockJob> {
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
}

View File

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

View File

@ -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<Block>(transaction) {
override suspend fun internalProcess(activity: ActivityPubProcessContext<Block>) {
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> = Block::class.java
}

View File

@ -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<ReceiveFollowJobParam, ReceiveFollowJob> {
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<Follow>(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)
}

View File

@ -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()
),

View File

@ -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<DeliverRejectJobParam, DeliverRejectJob> {
override suspend fun process(param: DeliverRejectJobParam): Unit = transaction.transaction {
apRequestService.apPost(param.inbox, param.reject, userQueryService.findById(param.signer))
}
override fun job(): DeliverRejectJob = deliverRejectJob
}

View File

@ -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<Reject>(transaction) {
override suspend fun internalProcess(activity: ActivityPubProcessContext<Reject>) {
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> = Reject::class.java
}

View File

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

View File

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

View File

@ -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<DeliverUndoJobParam, DeliverUndoJob> {
override suspend fun process(param: DeliverUndoJobParam): Unit = transaction.transaction {
apRequestService.apPost(param.inbox, param.undo, userQueryService.findById(param.signer))
}
override fun job(): DeliverUndoJob = deliverUndoJob
}

View File

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

View File

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

View File

@ -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<Undo>(transaction) {
override suspend fun internalProcess(activity: ActivityPubProcessContext<Undo>) {
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()

View File

@ -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<T : Object>(
private val transaction: Transaction,
private val allowUnauthorized: Boolean = false

View File

@ -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())

View File

@ -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"))

View File

@ -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
)

View File

@ -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?
}

View File

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

View File

@ -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
}

View File

@ -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<DeliverAcceptJobParam, DeliverAcceptJob>("DeliverAcceptJob") {
val accept = string("accept")
val inbox = string("inbox")
val signer = long("signer")
override fun convert(value: DeliverAcceptJobParam): ScheduleContext<DeliverAcceptJob>.(DeliverAcceptJob) -> Unit = {
props[accept] = objectMapper.writeValueAsString(value.accept)
props[inbox] = value.inbox
props[signer] = value.signer
}
override fun convert(props: JobProps<DeliverAcceptJob>): DeliverAcceptJobParam {
return DeliverAcceptJobParam(
objectMapper.readValue(props[accept]),
props[inbox],
props[signer]
)
}
}

View File

@ -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<DeliverBlockJobParam, DeliverBlockJob>("DeliverBlockJob") {
val block = string("block")
val reject = string("reject")
val inbox = string("inbox")
val signer = long("signer")
override fun convert(value: DeliverBlockJobParam): ScheduleContext<DeliverBlockJob>.(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<DeliverBlockJob>): DeliverBlockJobParam = DeliverBlockJobParam(
signer = props[signer],
block = objectMapper.readValue(props[block]),
reject = objectMapper.readValue(props[reject]),
inbox = props[inbox]
)
}

View File

@ -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<DeliverRejectJobParam, DeliverRejectJob>("DeliverRejectJob") {
val reject = string("reject")
val inbox = string("inbox")
val signer = long("signer")
override fun convert(value: DeliverRejectJobParam): ScheduleContext<DeliverRejectJob>.(DeliverRejectJob) -> Unit =
{
props[reject] = objectMapper.writeValueAsString(value.reject)
props[inbox] = value.inbox
props[signer] = value.signer
}
override fun convert(props: JobProps<DeliverRejectJob>): DeliverRejectJobParam = DeliverRejectJobParam(
objectMapper.readValue<Reject>(props[reject]),
props[inbox],
props[signer]
)
}

View File

@ -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<DeliverUndoJobParam, DeliverUndoJob>("DeliverUndoJob") {
val undo = string("undo")
val inbox = string("inbox")
val signer = long("signer")
override fun convert(value: DeliverUndoJobParam): ScheduleContext<DeliverUndoJob>.(DeliverUndoJob) -> Unit = {
props[undo] = objectMapper.writeValueAsString(value.undo)
props[inbox] = value.inbox
props[signer] = value.signer
}
override fun convert(props: JobProps<DeliverUndoJob>): DeliverUndoJobParam {
return DeliverUndoJobParam(
objectMapper.readValue(props[undo]),
props[inbox],
props[signer]
)
}
}

View File

@ -7,7 +7,7 @@ import kjob.core.dsl.ScheduleContext
import kjob.core.job.JobProps
import org.springframework.stereotype.Component
abstract class HideoutJob<out T, out R : HideoutJob<T, R>>(name: String = "") : Job(name) {
abstract class HideoutJob<out T, out R : HideoutJob<T, R>>(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<R>)
abstract fun convert(props: JobProps<@UnsafeVariance R>): T

View File

@ -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<User> {
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<User> {
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<User> {
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<User> {
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
}

View File

@ -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<Relationship> =
Relationships.select { Relationships.targetUserId eq targetId and (Relationships.following eq following) }
.map { it.toRelationships() }
}

View File

@ -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> = long("user_id").references(Users.id).index()
val followerId: Column<Long> = long("follower_id").references(Users.id)
init {
uniqueIndex(userId, followerId)
}
}
object FollowRequests : LongIdTable("follow_requests") {
val userId: Column<Long> = long("user_id").references(Users.id)
val followerId: Column<Long> = long("follower_id").references(Users.id)
init {
uniqueIndex(userId, followerId)
}
}

View File

@ -36,10 +36,14 @@ class KJobJobQueueWorkerService(private val jobQueueProcessorList: List<JobProce
for (jobProcessor in jobQueueProcessorList) {
kjob.register(jobProcessor.job()) {
execute {
@Suppress("TooGenericExceptionCaught")
try {
MDC.put("x-job-id", this.jobId)
val param = it.convertUnsafe(props)
jobProcessor.process(param)
} catch (e: Exception) {
logger.warn("FAILED Execute Job. job name: {} job id: {}", it.name, this.jobId, e)
throw e
} finally {
MDC.remove("x-job-id")
}

View File

@ -2,12 +2,8 @@ package dev.usbharu.hideout.core.query
import dev.usbharu.hideout.core.domain.model.user.User
@Deprecated("Use RelationshipQueryService")
interface FollowerQueryService {
suspend fun findFollowersById(id: Long): List<User>
suspend fun findFollowersByNameAndDomain(name: String, domain: String): List<User>
suspend fun findFollowingById(id: Long): List<User>
suspend fun findFollowingByNameAndDomain(name: String, domain: String): List<User>
suspend fun appendFollower(user: Long, follower: Long)
suspend fun removeFollower(user: Long, follower: Long)
suspend fun alreadyFollow(userId: Long, followerId: Long): Boolean
}

View File

@ -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<Relationship>
}

View File

@ -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)

View File

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

View File

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

View File

@ -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
}

View File

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

View File

@ -75,17 +75,17 @@ class MastodonAccountApiController(
val userid = principal.getClaim<String>("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<Relationship> {
val principal = SecurityContextHolder.getContext().getAuthentication().principal as Jwt
val userid = principal.getClaim<String>("uid").toLong()
val block = accountApiService.block(userid, id.toLong())
return ResponseEntity.ok(block)
}
override suspend fun apiV1AccountsIdUnblockPost(id: String): ResponseEntity<Relationship> {
val principal = SecurityContextHolder.getContext().getAuthentication().principal as Jwt
val userid = principal.getClaim<String>("uid").toLong()
val unblock = accountApiService.unblock(userid, id.toLong())
return ResponseEntity.ok(unblock)
}
override suspend fun apiV1AccountsIdUnfollowPost(id: String): ResponseEntity<Relationship> {
val principal = SecurityContextHolder.getContext().getAuthentication().principal as Jwt
val userid = principal.getClaim<String>("uid").toLong()
val unfollow = accountApiService.unfollow(userid, id.toLong())
return ResponseEntity.ok(unfollow)
}
override suspend fun apiV1AccountsIdRemoveFromFollowersPost(id: String): ResponseEntity<Relationship> {
val principal = SecurityContextHolder.getContext().getAuthentication().principal as Jwt
val userid = principal.getClaim<String>("uid").toLong()
val removeFromFollowers = accountApiService.removeFromFollowers(userid, id.toLong())
return ResponseEntity.ok(removeFromFollowers)
}
}

View File

@ -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<Long>, withSuspended: Boolean): List<Relationship>
/**
* ブロック操作を行う
*
* @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)
}

View File

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

View File

@ -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)
)

View File

@ -1,7 +1,9 @@
<configuration>
<appender name="STDOUT" class="ch.qos.logback.core.ConsoleAppender">
<encoder>
<pattern>%d{YYYY-MM-dd HH:mm:ss.SSS} [%thread] %-5level [%X{x-request-id}] %logger{36} - %msg%n</pattern>
<pattern>%d{YYYY-MM-dd HH:mm:ss.SSS} [%thread] %-5level [%X{x-request-id}] [%X{x-job-id}] %logger{36} -
%msg%n
</pattern>
</encoder>
</appender>
<root level="DEBUG">

View File

@ -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:

View File

@ -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<Block>(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")
}
}

View File

@ -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<Reject>(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")
}
}

View File

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

View File

@ -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>(
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>(
accept, jsonNode, HttpRequest(
URL("https://example.com"),
HttpHeaders(emptyMap()), HttpMethod.POST
), null, true
)
assertThrows<IllegalActivityPubObjectException> {
apAcceptProcessor.internalProcess(activity)
}
}
@Test
fun `isSupproted Acceptにはtrue`() {
val actual = apAcceptProcessor.isSupported(ActivityType.Accept)
assertThat(actual).isTrue()
}
@TestFactory
fun `isSupported Accept以外にはfalse`(): List<DynamicTest> {
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)
}
}

View File

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

View File

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

View File

@ -10,7 +10,6 @@ class ContextSerializerTest {
@Test
fun serialize() {
val accept = Accept(
name = "aaa",
actor = "bbb",
apObject = Follow(
apObject = "ddd",

View File

@ -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<IllegalStateException> {
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<IllegalStateException> {
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())
}
}

View File

@ -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",

View File

@ -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,