Merge pull request #16 from usbharu/feature/ap-follow

Feature/ap follow
This commit is contained in:
usbharu 2023-05-22 16:45:24 +09:00 committed by GitHub
commit ae6fcbd599
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
12 changed files with 115 additions and 22 deletions

View File

@ -0,0 +1,5 @@
package dev.usbharu.hideout.domain.model.hideout.dto
import dev.usbharu.hideout.domain.model.hideout.entity.User
data class SendFollowDto(val userId: User, val followTargetUserId: User)

View File

@ -15,4 +15,8 @@ data class User(
val publicKey: String, val publicKey: String,
val privateKey: String? = null, val privateKey: String? = null,
val createdAt: Instant val createdAt: Instant
) ) {
override fun toString(): String {
return "User(id=$id, name='$name', domain='$domain', screenName='$screenName', description='$description', password=****, inbox='$inbox', outbox='$outbox', url='$url', publicKey='$publicKey', privateKey=****, createdAt=$createdAt)"
}
}

View File

@ -0,0 +1,8 @@
package dev.usbharu.hideout.service.activitypub
import dev.usbharu.hideout.domain.model.ActivityPubResponse
import dev.usbharu.hideout.domain.model.ap.Accept
interface ActivityPubAcceptService {
suspend fun receiveAccept(accept: Accept): ActivityPubResponse
}

View File

@ -0,0 +1,28 @@
package dev.usbharu.hideout.service.activitypub
import dev.usbharu.hideout.domain.model.ActivityPubResponse
import dev.usbharu.hideout.domain.model.ActivityPubStringResponse
import dev.usbharu.hideout.domain.model.ap.Accept
import dev.usbharu.hideout.domain.model.ap.Follow
import dev.usbharu.hideout.exception.ap.IllegalActivityPubObjectException
import dev.usbharu.hideout.service.impl.IUserService
import io.ktor.http.*
import org.koin.core.annotation.Single
@Single
class ActivityPubAcceptServiceImpl(private val userService: IUserService) : ActivityPubAcceptService {
override suspend fun receiveAccept(accept: Accept): ActivityPubResponse {
val value = accept.`object` ?: throw IllegalActivityPubObjectException("object is null")
if (value.type.contains("Follow").not()) {
throw IllegalActivityPubObjectException("Invalid type ${value.type}")
}
val follow = value as Follow
val userUrl = follow.`object` ?: throw IllegalActivityPubObjectException("object is null")
val followerUrl = follow.actor ?: throw IllegalActivityPubObjectException("actor is null")
val user = userService.findByUrl(userUrl)
val follower = userService.findByUrl(followerUrl)
userService.follow(user.id, follower.id)
return ActivityPubStringResponse(HttpStatusCode.OK, "accepted")
}
}

View File

@ -5,7 +5,7 @@ import dev.usbharu.hideout.domain.model.ap.Follow
import dev.usbharu.hideout.domain.model.job.ReceiveFollowJob import dev.usbharu.hideout.domain.model.job.ReceiveFollowJob
import kjob.core.job.JobProps import kjob.core.job.JobProps
interface ActivityPubFollowService { interface ActivityPubReceiveFollowService {
suspend fun receiveFollow(follow: Follow): ActivityPubResponse suspend fun receiveFollow(follow: Follow): ActivityPubResponse
suspend fun receiveFollowJob(props: JobProps<ReceiveFollowJob>) suspend fun receiveFollowJob(props: JobProps<ReceiveFollowJob>)
} }

View File

@ -16,12 +16,12 @@ import kjob.core.job.JobProps
import org.koin.core.annotation.Single import org.koin.core.annotation.Single
@Single @Single
class ActivityPubFollowServiceImpl( class ActivityPubReceiveFollowServiceImpl(
private val jobQueueParentService: JobQueueParentService, private val jobQueueParentService: JobQueueParentService,
private val activityPubUserService: ActivityPubUserService, private val activityPubUserService: ActivityPubUserService,
private val userService: IUserService, private val userService: IUserService,
private val httpClient: HttpClient private val httpClient: HttpClient
) : ActivityPubFollowService { ) : ActivityPubReceiveFollowService {
override suspend fun receiveFollow(follow: Follow): ActivityPubResponse { override suspend fun receiveFollow(follow: Follow): ActivityPubResponse {
// TODO: Verify HTTP Signature // TODO: Verify HTTP Signature
jobQueueParentService.schedule(ReceiveFollowJob) { jobQueueParentService.schedule(ReceiveFollowJob) {

View File

@ -0,0 +1,7 @@
package dev.usbharu.hideout.service.activitypub
import dev.usbharu.hideout.domain.model.hideout.dto.SendFollowDto
interface ActivityPubSendFollowService {
suspend fun sendFollow(sendFollowDto: SendFollowDto)
}

View File

@ -0,0 +1,23 @@
package dev.usbharu.hideout.service.activitypub
import dev.usbharu.hideout.domain.model.ap.Follow
import dev.usbharu.hideout.domain.model.hideout.dto.SendFollowDto
import dev.usbharu.hideout.plugins.postAp
import io.ktor.client.*
import org.koin.core.annotation.Single
@Single
class ActivityPubSendFollowServiceImpl(private val httpClient: HttpClient) : ActivityPubSendFollowService {
override suspend fun sendFollow(sendFollowDto: SendFollowDto) {
val follow = Follow(
name = "Follow",
`object` = sendFollowDto.followTargetUserId.url,
actor = sendFollowDto.userId.url
)
httpClient.postAp(
urlString = sendFollowDto.followTargetUserId.inbox,
username = sendFollowDto.userId.url,
jsonLd = follow
)
}
}

View File

@ -2,7 +2,7 @@ package dev.usbharu.hideout.service.activitypub
import com.fasterxml.jackson.databind.JsonNode import com.fasterxml.jackson.databind.JsonNode
import com.fasterxml.jackson.module.kotlin.readValue import com.fasterxml.jackson.module.kotlin.readValue
import dev.usbharu.hideout.config.Config import dev.usbharu.hideout.config.Config.configData
import dev.usbharu.hideout.domain.model.ActivityPubResponse import dev.usbharu.hideout.domain.model.ActivityPubResponse
import dev.usbharu.hideout.domain.model.ap.Follow import dev.usbharu.hideout.domain.model.ap.Follow
import dev.usbharu.hideout.domain.model.job.DeliverPostJob import dev.usbharu.hideout.domain.model.job.DeliverPostJob
@ -17,14 +17,15 @@ import org.slf4j.LoggerFactory
@Single @Single
class ActivityPubServiceImpl( class ActivityPubServiceImpl(
private val activityPubFollowService: ActivityPubFollowService, private val activityPubReceiveFollowService: ActivityPubReceiveFollowService,
private val activityPubNoteService: ActivityPubNoteService, private val activityPubNoteService: ActivityPubNoteService,
private val activityPubUndoService: ActivityPubUndoService private val activityPubUndoService: ActivityPubUndoService,
private val activityPubAcceptService: ActivityPubAcceptService
) : ActivityPubService { ) : ActivityPubService {
val logger: Logger = LoggerFactory.getLogger(this::class.java) val logger: Logger = LoggerFactory.getLogger(this::class.java)
override fun parseActivity(json: String): ActivityType { override fun parseActivity(json: String): ActivityType {
val readTree = Config.configData.objectMapper.readTree(json) val readTree = configData.objectMapper.readTree(json)
logger.debug("readTree: {}", readTree) logger.debug("readTree: {}", readTree)
if (readTree.isObject.not()) { if (readTree.isObject.not()) {
throw JsonParseException("Json is not object.") throw JsonParseException("Json is not object.")
@ -41,7 +42,7 @@ class ActivityPubServiceImpl(
@Suppress("CyclomaticComplexMethod", "NotImplementedDeclaration") @Suppress("CyclomaticComplexMethod", "NotImplementedDeclaration")
override suspend fun processActivity(json: String, type: ActivityType): ActivityPubResponse { override suspend fun processActivity(json: String, type: ActivityType): ActivityPubResponse {
return when (type) { return when (type) {
ActivityType.Accept -> TODO() ActivityType.Accept -> activityPubAcceptService.receiveAccept(configData.objectMapper.readValue(json))
ActivityType.Add -> TODO() ActivityType.Add -> TODO()
ActivityType.Announce -> TODO() ActivityType.Announce -> TODO()
ActivityType.Arrive -> TODO() ActivityType.Arrive -> TODO()
@ -50,8 +51,8 @@ class ActivityPubServiceImpl(
ActivityType.Delete -> TODO() ActivityType.Delete -> TODO()
ActivityType.Dislike -> TODO() ActivityType.Dislike -> TODO()
ActivityType.Flag -> TODO() ActivityType.Flag -> TODO()
ActivityType.Follow -> activityPubFollowService.receiveFollow( ActivityType.Follow -> activityPubReceiveFollowService.receiveFollow(
Config.configData.objectMapper.readValue( configData.objectMapper.readValue(
json, json,
Follow::class.java Follow::class.java
) )
@ -72,7 +73,7 @@ class ActivityPubServiceImpl(
ActivityType.TentativeReject -> TODO() ActivityType.TentativeReject -> TODO()
ActivityType.TentativeAccept -> TODO() ActivityType.TentativeAccept -> TODO()
ActivityType.Travel -> TODO() ActivityType.Travel -> TODO()
ActivityType.Undo -> activityPubUndoService.receiveUndo(Config.configData.objectMapper.readValue(json)) ActivityType.Undo -> activityPubUndoService.receiveUndo(configData.objectMapper.readValue(json))
ActivityType.Update -> TODO() ActivityType.Update -> TODO()
ActivityType.View -> TODO() ActivityType.View -> TODO()
ActivityType.Other -> TODO() ActivityType.Other -> TODO()
@ -82,7 +83,7 @@ class ActivityPubServiceImpl(
override suspend fun <T : HideoutJob> processActivity(job: JobContextWithProps<T>, hideoutJob: HideoutJob) { override suspend fun <T : HideoutJob> processActivity(job: JobContextWithProps<T>, hideoutJob: HideoutJob) {
logger.debug("processActivity: ${hideoutJob.name}") logger.debug("processActivity: ${hideoutJob.name}")
when (hideoutJob) { when (hideoutJob) {
ReceiveFollowJob -> activityPubFollowService.receiveFollowJob(job.props as JobProps<ReceiveFollowJob>) ReceiveFollowJob -> activityPubReceiveFollowService.receiveFollowJob(job.props as JobProps<ReceiveFollowJob>)
DeliverPostJob -> activityPubNoteService.createNoteJob(job.props as JobProps<DeliverPostJob>) DeliverPostJob -> activityPubNoteService.createNoteJob(job.props as JobProps<DeliverPostJob>)
} }
} }

View File

@ -2,17 +2,23 @@ package dev.usbharu.hideout.service.impl
import dev.usbharu.hideout.config.Config import dev.usbharu.hideout.config.Config
import dev.usbharu.hideout.domain.model.hideout.dto.RemoteUserCreateDto import dev.usbharu.hideout.domain.model.hideout.dto.RemoteUserCreateDto
import dev.usbharu.hideout.domain.model.hideout.dto.SendFollowDto
import dev.usbharu.hideout.domain.model.hideout.dto.UserCreateDto import dev.usbharu.hideout.domain.model.hideout.dto.UserCreateDto
import dev.usbharu.hideout.domain.model.hideout.entity.User import dev.usbharu.hideout.domain.model.hideout.entity.User
import dev.usbharu.hideout.exception.UserNotFoundException import dev.usbharu.hideout.exception.UserNotFoundException
import dev.usbharu.hideout.repository.IUserRepository import dev.usbharu.hideout.repository.IUserRepository
import dev.usbharu.hideout.service.IUserAuthService import dev.usbharu.hideout.service.IUserAuthService
import dev.usbharu.hideout.service.activitypub.ActivityPubSendFollowService
import org.koin.core.annotation.Single import org.koin.core.annotation.Single
import java.lang.Integer.min import java.lang.Integer.min
import java.time.Instant import java.time.Instant
@Single @Single
class UserService(private val userRepository: IUserRepository, private val userAuthService: IUserAuthService) : class UserService(
private val userRepository: IUserRepository,
private val userAuthService: IUserAuthService,
private val activityPubSendFollowService: ActivityPubSendFollowService
) :
IUserService { IUserService {
private val maxLimit = 100 private val maxLimit = 100
@ -105,9 +111,19 @@ class UserService(private val userRepository: IUserRepository, private val userA
} }
// TODO APのフォロー処理を作る // TODO APのフォロー処理を作る
override suspend fun follow(id: Long, follower: Long): Boolean { override suspend fun follow(id: Long, followerId: Long): Boolean {
userRepository.createFollower(id, follower) val user = userRepository.findById(id) ?: throw UserNotFoundException("$id was not found.")
return false val follower = userRepository.findById(followerId) ?: throw UserNotFoundException("$followerId was not found.")
if (follower.domain != Config.configData.domain) {
throw IllegalArgumentException("follower is not local user.")
}
return if (user.domain == Config.configData.domain) {
userRepository.createFollower(id, followerId)
true
} else {
activityPubSendFollowService.sendFollow(SendFollowDto(follower, user))
false
}
} }
override suspend fun unfollow(id: Long, follower: Long): Boolean { override suspend fun unfollow(id: Long, follower: Long): Boolean {

View File

@ -25,13 +25,14 @@ import org.mockito.kotlin.*
import utils.JsonObjectMapper import utils.JsonObjectMapper
import java.time.Instant import java.time.Instant
class ActivityPubFollowServiceImplTest { class ActivityPubReceiveFollowServiceImplTest {
@Test @Test
fun `receiveFollow フォロー受付処理`() = runTest { fun `receiveFollow フォロー受付処理`() = runTest {
val jobQueueParentService = mock<JobQueueParentService> { val jobQueueParentService = mock<JobQueueParentService> {
onBlocking { schedule(eq(ReceiveFollowJob), any()) } doReturn Unit onBlocking { schedule(eq(ReceiveFollowJob), any()) } doReturn Unit
} }
val activityPubFollowService = ActivityPubFollowServiceImpl(jobQueueParentService, mock(), mock(), mock()) val activityPubFollowService =
ActivityPubReceiveFollowServiceImpl(jobQueueParentService, mock(), mock(), mock())
activityPubFollowService.receiveFollow( activityPubFollowService.receiveFollow(
Follow( Follow(
emptyList(), emptyList(),
@ -118,7 +119,7 @@ class ActivityPubFollowServiceImplTest {
onBlocking { follow(any(), any()) } doReturn false onBlocking { follow(any(), any()) } doReturn false
} }
val activityPubFollowService = val activityPubFollowService =
ActivityPubFollowServiceImpl( ActivityPubReceiveFollowServiceImpl(
mock(), mock(),
activityPubUserService, activityPubUserService,
userService, userService,

View File

@ -29,7 +29,7 @@ class UserServiceTest {
onBlocking { hash(anyString()) } doReturn "hashedPassword" onBlocking { hash(anyString()) } doReturn "hashedPassword"
onBlocking { generateKeyPair() } doReturn generateKeyPair onBlocking { generateKeyPair() } doReturn generateKeyPair
} }
val userService = UserService(userRepository, userAuthService) val userService = UserService(userRepository, userAuthService, mock())
userService.createLocalUser(UserCreateDto("test", "testUser", "XXXXXXXXXXXXX", "test")) userService.createLocalUser(UserCreateDto("test", "testUser", "XXXXXXXXXXXXX", "test"))
verify(userRepository, times(1)).save(any()) verify(userRepository, times(1)).save(any())
argumentCaptor<dev.usbharu.hideout.domain.model.hideout.entity.User> { argumentCaptor<dev.usbharu.hideout.domain.model.hideout.entity.User> {
@ -55,7 +55,7 @@ class UserServiceTest {
val userRepository = mock<IUserRepository> { val userRepository = mock<IUserRepository> {
onBlocking { nextId() } doReturn 113345L onBlocking { nextId() } doReturn 113345L
} }
val userService = UserService(userRepository, mock()) val userService = UserService(userRepository, mock(), mock())
val user = RemoteUserCreateDto( val user = RemoteUserCreateDto(
"test", "test",
"example.com", "example.com",