Compare commits

...

11 Commits

Author SHA1 Message Date
usbharu 8e71c81e73
Merge pull request #98 from usbharu/feature/job-queue
Feature/job queue
2023-10-27 14:17:59 +09:00
usbharu 2353414d16
test: テストを修正 2023-10-27 14:13:07 +09:00
usbharu 0fdceb4d9e
style: fix lint 2023-10-27 14:10:04 +09:00
usbharu cefde4df45
style: fix lint 2023-10-27 14:08:38 +09:00
usbharu 2c2a392c92
refactor: jobを別のクラスに切り出し2 2023-10-27 13:54:33 +09:00
usbharu 0eb2cfbb86
refactor: jobを別のクラスに切り出し 2023-10-27 13:22:18 +09:00
usbharu 70adcd1c56
Merge pull request #97 from usbharu/feature/logging-2
Feature/logging 2
2023-10-27 11:43:57 +09:00
usbharu d2fcf658e3
Apply suggestions from code review
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2023-10-27 11:38:21 +09:00
usbharu 4b6f86da95
feat: 署名されないリクエストもログをとるように 2023-10-27 11:28:35 +09:00
usbharu 68c6e8937c
feat: ActivityPubリクエストのログを追加 2023-10-27 11:17:05 +09:00
usbharu d70fd208a6
feat: フォローリクエストが飛んできたときのログを追加 2023-10-27 11:16:40 +09:00
20 changed files with 363 additions and 221 deletions

View File

@ -1,7 +1,7 @@
package dev.usbharu.hideout
import dev.usbharu.hideout.domain.model.job.HideoutJob
import dev.usbharu.hideout.service.ap.APService
import dev.usbharu.hideout.service.ap.job.ApJobService
import dev.usbharu.hideout.service.job.JobQueueParentService
import dev.usbharu.hideout.service.job.JobQueueWorkerService
import org.slf4j.Logger
@ -27,7 +27,7 @@ class JobQueueRunner(private val jobQueueParentService: JobQueueParentService, p
class JobQueueWorkerRunner(
private val jobQueueWorkerService: JobQueueWorkerService,
private val jobs: List<HideoutJob>,
private val apService: APService
private val apJobService: ApJobService
) : ApplicationRunner {
override fun run(args: ApplicationArguments?) {
LOGGER.info("Init job queue worker.")
@ -36,7 +36,7 @@ class JobQueueWorkerRunner(
it to {
execute {
LOGGER.debug("excute job ${it.name}")
apService.processActivity(
apJobService.processActivity(
job = this,
hideoutJob = it
)

View File

@ -9,6 +9,7 @@ import org.springframework.web.bind.annotation.RestController
@RestController
class InboxControllerImpl(private val apService: APService) : InboxController {
@Suppress("TooGenericExceptionCaught")
override suspend fun inbox(@RequestBody string: String): ResponseEntity<Unit> {
val parseActivity = try {
apService.parseActivity(string)

View File

@ -6,7 +6,5 @@ import org.springframework.stereotype.Component
@Component
class UserQueryMapper(private val userResultRowMapper: ResultRowMapper<User>) : QueryMapper<User> {
override fun map(query: Query): List<User> {
return query.map(userResultRowMapper::map)
}
override fun map(query: Query): List<User> = query.map(userResultRowMapper::map)
}

View File

@ -55,9 +55,8 @@ class UserRepositoryImpl(
return user
}
override suspend fun findById(id: Long): User? {
return Users.select { Users.id eq id }.singleOrNull()?.let(userResultRowMapper::map)
}
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) }

View File

@ -1,10 +1,6 @@
package dev.usbharu.hideout.service.ap
import com.fasterxml.jackson.databind.ObjectMapper
import com.fasterxml.jackson.module.kotlin.readValue
import dev.usbharu.hideout.config.ApplicationConfig
import dev.usbharu.hideout.domain.model.ap.Create
import dev.usbharu.hideout.domain.model.ap.Document
import dev.usbharu.hideout.domain.model.ap.Note
import dev.usbharu.hideout.domain.model.hideout.entity.Post
import dev.usbharu.hideout.domain.model.hideout.entity.Visibility
@ -19,12 +15,10 @@ import dev.usbharu.hideout.query.UserQueryService
import dev.usbharu.hideout.repository.PostRepository
import dev.usbharu.hideout.service.ap.resource.APResourceResolveService
import dev.usbharu.hideout.service.ap.resource.resolve
import dev.usbharu.hideout.service.core.Transaction
import dev.usbharu.hideout.service.job.JobQueueParentService
import dev.usbharu.hideout.service.post.PostCreateInterceptor
import dev.usbharu.hideout.service.post.PostService
import io.ktor.client.plugins.*
import kjob.core.job.JobProps
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Deferred
import kotlinx.coroutines.Dispatchers
@ -40,7 +34,6 @@ import java.time.Instant
interface APNoteService {
suspend fun createNote(post: Post)
suspend fun createNoteJob(props: JobProps<DeliverPostJob>)
@Cacheable("fetchNote")
fun fetchNoteAsync(url: String, targetActor: String? = null): Deferred<Note> {
@ -66,12 +59,9 @@ class APNoteServiceImpl(
private val postQueryService: PostQueryService,
private val mediaQueryService: MediaQueryService,
@Qualifier("activitypub") private val objectMapper: ObjectMapper,
private val applicationConfig: ApplicationConfig,
private val postService: PostService,
private val apResourceResolveService: APResourceResolveService,
private val apRequestService: APRequestService,
private val postBuilder: Post.PostBuilder,
private val transaction: Transaction
private val postBuilder: Post.PostBuilder
) : APNoteService, PostCreateInterceptor {
@ -104,41 +94,6 @@ class APNoteServiceImpl(
logger.debug("SUCCESS Create Local Note ${post.url}")
}
override suspend fun createNoteJob(props: JobProps<DeliverPostJob>) {
val actor = props[DeliverPostJob.actor]
val postEntity = objectMapper.readValue<Post>(props[DeliverPostJob.post])
val mediaList =
objectMapper.readValue<List<dev.usbharu.hideout.domain.model.hideout.entity.Media>>(
props[DeliverPostJob.media]
)
val note = Note(
name = "Note",
id = postEntity.url,
attributedTo = actor,
content = postEntity.text,
published = Instant.ofEpochMilli(postEntity.createdAt).toString(),
to = listOf(public, "$actor/follower"),
attachment = mediaList.map { Document(mediaType = "image/jpeg", url = it.url) }
)
val inbox = props[DeliverPostJob.inbox]
logger.debug("createNoteJob: actor={}, note={}, inbox={}", actor, postEntity, inbox)
transaction.transaction {
val signer = userQueryService.findByUrl(actor)
apRequestService.apPost(
inbox,
Create(
name = "Create Note",
`object` = note,
actor = note.attributedTo,
id = "${applicationConfig.url}/create/note/${postEntity.id}"
),
signer
)
}
}
override suspend fun fetchNote(url: String, targetActor: String?): Note {
logger.debug("START Fetch Note url: {}", url)
try {

View File

@ -1,10 +1,6 @@
package dev.usbharu.hideout.service.ap
import com.fasterxml.jackson.databind.ObjectMapper
import com.fasterxml.jackson.module.kotlin.readValue
import dev.usbharu.hideout.config.ApplicationConfig
import dev.usbharu.hideout.domain.model.ap.Like
import dev.usbharu.hideout.domain.model.ap.Undo
import dev.usbharu.hideout.domain.model.hideout.entity.Reaction
import dev.usbharu.hideout.domain.model.job.DeliverReactionJob
import dev.usbharu.hideout.domain.model.job.DeliverRemoveReactionJob
@ -12,16 +8,12 @@ import dev.usbharu.hideout.query.FollowerQueryService
import dev.usbharu.hideout.query.PostQueryService
import dev.usbharu.hideout.query.UserQueryService
import dev.usbharu.hideout.service.job.JobQueueParentService
import kjob.core.job.JobProps
import org.springframework.beans.factory.annotation.Qualifier
import org.springframework.stereotype.Service
import java.time.Instant
interface APReactionService {
suspend fun reaction(like: Reaction)
suspend fun removeReaction(like: Reaction)
suspend fun reactionJob(props: JobProps<DeliverReactionJob>)
suspend fun removeReactionJob(props: JobProps<DeliverRemoveReactionJob>)
}
@Service
@ -30,9 +22,7 @@ class APReactionServiceImpl(
private val userQueryService: UserQueryService,
private val followerQueryService: FollowerQueryService,
private val postQueryService: PostQueryService,
@Qualifier("activitypub") private val objectMapper: ObjectMapper,
private val applicationConfig: ApplicationConfig,
private val apRequestService: APRequestService
@Qualifier("activitypub") private val objectMapper: ObjectMapper
) : APReactionService {
override suspend fun reaction(like: Reaction) {
val followers = followerQueryService.findFollowersById(like.userId)
@ -64,46 +54,4 @@ class APReactionServiceImpl(
}
}
}
override suspend fun reactionJob(props: JobProps<DeliverReactionJob>) {
val inbox = props[DeliverReactionJob.inbox]
val actor = props[DeliverReactionJob.actor]
val postUrl = props[DeliverReactionJob.postUrl]
val id = props[DeliverReactionJob.id]
val content = props[DeliverReactionJob.reaction]
val signer = userQueryService.findByUrl(actor)
apRequestService.apPost(
inbox,
Like(
name = "Like",
actor = actor,
`object` = postUrl,
id = "${applicationConfig.url}/like/note/$id",
content = content
),
signer
)
}
override suspend fun removeReactionJob(props: JobProps<DeliverRemoveReactionJob>) {
val inbox = props[DeliverRemoveReactionJob.inbox]
val actor = props[DeliverRemoveReactionJob.actor]
val like = objectMapper.readValue<Like>(props[DeliverRemoveReactionJob.like])
val signer = userQueryService.findByUrl(actor)
apRequestService.apPost(
inbox,
Undo(
name = "Undo Reaction",
actor = actor,
`object` = like,
id = "${applicationConfig.url}/undo/note/${like.id}",
published = Instant.now()
),
signer
)
}
}

View File

@ -1,38 +1,27 @@
package dev.usbharu.hideout.service.ap
import com.fasterxml.jackson.databind.ObjectMapper
import com.fasterxml.jackson.module.kotlin.readValue
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.domain.model.job.ReceiveFollowJob
import dev.usbharu.hideout.query.UserQueryService
import dev.usbharu.hideout.service.core.Transaction
import dev.usbharu.hideout.service.job.JobQueueParentService
import dev.usbharu.hideout.service.user.UserService
import io.ktor.http.*
import kjob.core.job.JobProps
import org.slf4j.LoggerFactory
import org.springframework.beans.factory.annotation.Qualifier
import org.springframework.stereotype.Service
interface APReceiveFollowService {
suspend fun receiveFollow(follow: Follow): ActivityPubResponse
suspend fun receiveFollowJob(props: JobProps<ReceiveFollowJob>)
}
@Service
class APReceiveFollowServiceImpl(
private val jobQueueParentService: JobQueueParentService,
private val apUserService: APUserService,
private val userService: UserService,
private val userQueryService: UserQueryService,
private val transaction: Transaction,
@Qualifier("activitypub") private val objectMapper: ObjectMapper,
private val apRequestService: APRequestService
@Qualifier("activitypub") private val objectMapper: ObjectMapper
) : APReceiveFollowService {
override suspend fun receiveFollow(follow: Follow): ActivityPubResponse {
// TODO: Verify HTTP Signature
logger.info("FOLLOW from: {} to: {}", follow.actor, follow.`object`)
jobQueueParentService.schedule(ReceiveFollowJob) {
props[ReceiveFollowJob.actor] = follow.actor
props[ReceiveFollowJob.follow] = objectMapper.writeValueAsString(follow)
@ -41,33 +30,7 @@ class APReceiveFollowServiceImpl(
return ActivityPubStringResponse(HttpStatusCode.OK, "{}", ContentType.Application.Json)
}
override suspend fun receiveFollowJob(props: JobProps<ReceiveFollowJob>) {
// throw Exception()
transaction.transaction {
val actor = props[ReceiveFollowJob.actor]
val targetActor = props[ReceiveFollowJob.targetActor]
val person = apUserService.fetchPerson(actor, targetActor)
val follow = objectMapper.readValue<Follow>(props[ReceiveFollowJob.follow])
val signer = userQueryService.findByUrl(targetActor)
val urlString = person.inbox ?: throw IllegalArgumentException("inbox is not found")
apRequestService.apPost(
url = urlString,
body = Accept(
name = "Follow",
`object` = follow,
actor = targetActor
),
signer = signer
)
val targetEntity = userQueryService.findByUrl(targetActor)
val followActorEntity =
userQueryService.findByUrl(follow.actor ?: throw java.lang.IllegalArgumentException("Actor is null"))
userService.followRequest(targetEntity.id, followActorEntity.id)
}
companion object {
private val logger = LoggerFactory.getLogger(APReceiveFollowServiceImpl::class.java)
}
}

View File

@ -34,6 +34,7 @@ class APRequestServiceImpl(
) : APRequestService {
override suspend fun <R : Object> apGet(url: String, signer: User?, responseClass: Class<R>): R {
logger.debug("START ActivityPub Request GET url: {}, signer: {}", url, signer?.url)
val date = dateTimeFormatter.format(ZonedDateTime.now(ZoneId.of("GMT")))
val u = URL(url)
if (signer?.privateKey == null) {
@ -41,6 +42,7 @@ class APRequestServiceImpl(
header("Accept", ContentType.Application.Activity)
header("Date", date)
}.bodyAsText()
logBody(bodyAsText, url)
return objectMapper.readValue(bodyAsText, responseClass)
}
@ -63,7 +65,7 @@ class APRequestServiceImpl(
signHeaders = listOf("(request-target)", "date", "host", "accept")
)
val bodyAsText = httpClient.get(url) {
val httpResponse = httpClient.get(url) {
headers {
headers {
appendAll(headers)
@ -72,8 +74,16 @@ class APRequestServiceImpl(
}
}
contentType(ContentType.Application.Activity)
}.bodyAsText()
return objectMapper.readValue(bodyAsText, responseClass)
}
val bodyAsText = httpResponse.bodyAsText()
val readValue = objectMapper.readValue(bodyAsText, responseClass)
logger.debug(
"SUCCESS ActivityPub Request GET status: {} url: {}",
httpResponse.status,
httpResponse.request.url
)
logBody(bodyAsText, url)
return readValue
}
override suspend fun <T : Object, R : Object> apPost(
@ -87,6 +97,7 @@ class APRequestServiceImpl(
}
override suspend fun <T : Object> apPost(url: String, body: T?, signer: User?): String {
logger.debug("START ActivityPub Request POST url: {}, signer: {}", url, signer?.url)
if (body != null) {
val mutableListOf = mutableListOf<String>()
mutableListOf.add("https://www.w3.org/ns/activitystreams")
@ -96,6 +107,20 @@ class APRequestServiceImpl(
val requestBody = objectMapper.writeValueAsString(body)
logger.trace(
"""
|
|***** BEGIN HTTP Request Trace url: {} *****
|
|$requestBody
|
|***** END HTTP Request Trace url: {} *****
|
""".trimMargin(),
url,
url
)
val sha256 = MessageDigest.getInstance("SHA-256")
val digest = Base64Util.encode(sha256.digest(requestBody.toByteArray()))
@ -103,19 +128,17 @@ class APRequestServiceImpl(
val date = dateTimeFormatter.format(ZonedDateTime.now(ZoneId.of("GMT")))
val u = URL(url)
if (signer?.privateKey == null) {
logger.debug("NOT SIGN Request: {}", url)
logger.trace("{}", signer)
return httpClient.post(url) {
val bodyAsText = httpClient.post(url) {
header("Accept", ContentType.Application.Activity)
header("Date", date)
header("Digest", "sha-256=$digest")
setBody(requestBody)
contentType(ContentType.Application.Activity)
}.bodyAsText()
logBody(bodyAsText, url)
return bodyAsText
}
logger.debug("SIGN Request: {}", url)
val headers = headers {
append("Accept", ContentType.Application.Activity)
append("Date", date)
@ -136,7 +159,7 @@ class APRequestServiceImpl(
signHeaders = listOf("(request-target)", "date", "host", "digest")
)
return httpClient.post(url) {
val httpResponse = httpClient.post(url) {
headers {
headers {
appendAll(headers)
@ -146,7 +169,31 @@ class APRequestServiceImpl(
}
setBody(requestBody)
contentType(ContentType.Application.Activity)
}.bodyAsText()
}
val bodyAsText = httpResponse.bodyAsText()
logger.debug(
"SUCCESS ActivityPub Request POST status: {} url: {}",
httpResponse.status,
httpResponse.request.url
)
logBody(bodyAsText, url)
return bodyAsText
}
private fun logBody(bodyAsText: String, url: String) {
logger.trace(
"""
|
|***** BEGIN HTTP Response Trace url: {} *****
|
|$bodyAsText
|
|***** END HTTP Response TRACE url: {} *****
|
""".trimMargin(),
url,
url
)
}
companion object {

View File

@ -5,10 +5,10 @@ import com.fasterxml.jackson.databind.ObjectMapper
import com.fasterxml.jackson.module.kotlin.readValue
import dev.usbharu.hideout.domain.model.ActivityPubResponse
import dev.usbharu.hideout.domain.model.ap.Follow
import dev.usbharu.hideout.domain.model.job.*
import dev.usbharu.hideout.exception.JsonParseException
import kjob.core.dsl.JobContextWithProps
import kjob.core.job.JobProps
import dev.usbharu.hideout.service.ap.job.APReceiveFollowJobService
import dev.usbharu.hideout.service.ap.job.ApNoteJobService
import dev.usbharu.hideout.service.ap.job.ApReactionJobService
import org.slf4j.Logger
import org.slf4j.LoggerFactory
import org.springframework.beans.factory.annotation.Qualifier
@ -18,8 +18,6 @@ interface APService {
fun parseActivity(json: String): ActivityType
suspend fun processActivity(json: String, type: ActivityType): ActivityPubResponse?
suspend fun <T : HideoutJob> processActivity(job: JobContextWithProps<T>, hideoutJob: HideoutJob)
}
enum class ActivityType {
@ -176,13 +174,14 @@ enum class ExtendedVocabulary {
@Service
class APServiceImpl(
private val apReceiveFollowService: APReceiveFollowService,
private val apNoteService: APNoteService,
private val apUndoService: APUndoService,
private val apAcceptService: APAcceptService,
private val apCreateService: APCreateService,
private val apLikeService: APLikeService,
private val apReactionService: APReactionService,
@Qualifier("activitypub") private val objectMapper: ObjectMapper
@Qualifier("activitypub") private val objectMapper: ObjectMapper,
private val apReceiveFollowJobService: APReceiveFollowJobService,
private val apNoteJobService: ApNoteJobService,
private val apReactionJobService: ApReactionJobService
) : APService {
val logger: Logger = LoggerFactory.getLogger(APServiceImpl::class.java)
@ -197,7 +196,8 @@ class APServiceImpl(
|
|***** Trace End Activity *****
|
""".trimMargin(), readTree.toPrettyString()
""".trimMargin(),
readTree.toPrettyString()
)
if (readTree.isObject.not()) {
throw JsonParseException("Json is not object.")
@ -228,29 +228,4 @@ class APServiceImpl(
}
}
}
@Suppress("REDUNDANT_ELSE_IN_WHEN")
override suspend fun <T : HideoutJob> processActivity(job: JobContextWithProps<T>, hideoutJob: HideoutJob) {
logger.debug("processActivity: ${hideoutJob.name}")
@Suppress("ElseCaseInsteadOfExhaustiveWhen")
// Springで作成されるプロキシの都合上パターンマッチングが壊れるので必須
when (hideoutJob) {
is ReceiveFollowJob -> {
apReceiveFollowService.receiveFollowJob(
job.props as JobProps<ReceiveFollowJob>
)
}
is DeliverPostJob -> apNoteService.createNoteJob(job.props as JobProps<DeliverPostJob>)
is DeliverReactionJob -> apReactionService.reactionJob(job.props as JobProps<DeliverReactionJob>)
is DeliverRemoveReactionJob -> apReactionService.removeReactionJob(
job.props as JobProps<DeliverRemoveReactionJob>
)
else -> {
throw IllegalStateException("WTF")
}
}
}
}

View File

@ -0,0 +1,8 @@
package dev.usbharu.hideout.service.ap.job
import dev.usbharu.hideout.domain.model.job.ReceiveFollowJob
import kjob.core.job.JobProps
interface APReceiveFollowJobService {
suspend fun receiveFollowJob(props: JobProps<ReceiveFollowJob>)
}

View File

@ -0,0 +1,61 @@
package dev.usbharu.hideout.service.ap.job
import com.fasterxml.jackson.databind.ObjectMapper
import com.fasterxml.jackson.module.kotlin.readValue
import dev.usbharu.hideout.domain.model.ap.Accept
import dev.usbharu.hideout.domain.model.ap.Follow
import dev.usbharu.hideout.domain.model.job.ReceiveFollowJob
import dev.usbharu.hideout.query.UserQueryService
import dev.usbharu.hideout.service.ap.APRequestService
import dev.usbharu.hideout.service.ap.APUserService
import dev.usbharu.hideout.service.core.Transaction
import dev.usbharu.hideout.service.user.UserService
import kjob.core.job.JobProps
import org.slf4j.LoggerFactory
import org.springframework.beans.factory.annotation.Qualifier
import org.springframework.stereotype.Component
@Component
class APReceiveFollowJobServiceImpl(
private val apUserService: APUserService,
private val userQueryService: UserQueryService,
private val apRequestService: APRequestService,
private val userService: UserService,
@Qualifier("activitypub") private val objectMapper: ObjectMapper,
private val transaction: Transaction
) : APReceiveFollowJobService {
override suspend fun receiveFollowJob(props: JobProps<ReceiveFollowJob>) {
transaction.transaction {
val actor = props[ReceiveFollowJob.actor]
val targetActor = props[ReceiveFollowJob.targetActor]
val person = apUserService.fetchPerson(actor, targetActor)
val follow = objectMapper.readValue<Follow>(props[ReceiveFollowJob.follow])
logger.info("START Follow from: {} to: {}", targetActor, actor)
val signer = userQueryService.findByUrl(targetActor)
val urlString = person.inbox ?: throw IllegalArgumentException("inbox is not found")
apRequestService.apPost(
url = urlString,
body = Accept(
name = "Follow",
`object` = follow,
actor = targetActor
),
signer = signer
)
val targetEntity = userQueryService.findByUrl(targetActor)
val followActorEntity =
userQueryService.findByUrl(follow.actor ?: throw java.lang.IllegalArgumentException("Actor is null"))
userService.followRequest(targetEntity.id, followActorEntity.id)
logger.info("SUCCESS Follow from: {} to: {}", targetActor, actor)
}
}
companion object {
private val logger = LoggerFactory.getLogger(APReceiveFollowJobServiceImpl::class.java)
}
}

View File

@ -0,0 +1,8 @@
package dev.usbharu.hideout.service.ap.job
import dev.usbharu.hideout.domain.model.job.HideoutJob
import kjob.core.dsl.JobContextWithProps
interface ApJobService {
suspend fun <T : HideoutJob> processActivity(job: JobContextWithProps<T>, hideoutJob: HideoutJob)
}

View File

@ -0,0 +1,43 @@
package dev.usbharu.hideout.service.ap.job
import dev.usbharu.hideout.domain.model.job.*
import kjob.core.dsl.JobContextWithProps
import kjob.core.job.JobProps
import org.slf4j.LoggerFactory
import org.springframework.stereotype.Service
@Service
class ApJobServiceImpl(
private val apReceiveFollowJobService: APReceiveFollowJobService,
private val apNoteJobService: ApNoteJobService,
private val apReactionJobService: ApReactionJobService
) : ApJobService {
@Suppress("REDUNDANT_ELSE_IN_WHEN")
override suspend fun <T : HideoutJob> processActivity(job: JobContextWithProps<T>, hideoutJob: HideoutJob) {
logger.debug("processActivity: ${hideoutJob.name}")
@Suppress("ElseCaseInsteadOfExhaustiveWhen")
// Springで作成されるプロキシの都合上パターンマッチングが壊れるので必須
when (hideoutJob) {
is ReceiveFollowJob -> {
apReceiveFollowJobService.receiveFollowJob(
job.props as JobProps<ReceiveFollowJob>
)
}
is DeliverPostJob -> apNoteJobService.createNoteJob(job.props as JobProps<DeliverPostJob>)
is DeliverReactionJob -> apReactionJobService.reactionJob(job.props as JobProps<DeliverReactionJob>)
is DeliverRemoveReactionJob -> apReactionJobService.removeReactionJob(
job.props as JobProps<DeliverRemoveReactionJob>
)
else -> {
throw IllegalStateException("WTF")
}
}
}
companion object {
private val logger = LoggerFactory.getLogger(ApJobServiceImpl::class.java)
}
}

View File

@ -0,0 +1,8 @@
package dev.usbharu.hideout.service.ap.job
import dev.usbharu.hideout.domain.model.job.DeliverPostJob
import kjob.core.job.JobProps
interface ApNoteJobService {
suspend fun createNoteJob(props: JobProps<DeliverPostJob>)
}

View File

@ -0,0 +1,67 @@
package dev.usbharu.hideout.service.ap.job
import com.fasterxml.jackson.databind.ObjectMapper
import com.fasterxml.jackson.module.kotlin.readValue
import dev.usbharu.hideout.config.ApplicationConfig
import dev.usbharu.hideout.domain.model.ap.Create
import dev.usbharu.hideout.domain.model.ap.Document
import dev.usbharu.hideout.domain.model.ap.Note
import dev.usbharu.hideout.domain.model.hideout.entity.Post
import dev.usbharu.hideout.domain.model.job.DeliverPostJob
import dev.usbharu.hideout.query.UserQueryService
import dev.usbharu.hideout.service.ap.APNoteServiceImpl
import dev.usbharu.hideout.service.ap.APRequestService
import dev.usbharu.hideout.service.core.Transaction
import kjob.core.job.JobProps
import org.slf4j.LoggerFactory
import org.springframework.beans.factory.annotation.Qualifier
import org.springframework.stereotype.Component
import java.time.Instant
@Component
class ApNoteJobServiceImpl(
private val userQueryService: UserQueryService,
private val apRequestService: APRequestService,
@Qualifier("activitypub") private val objectMapper: ObjectMapper,
private val transaction: Transaction,
private val applicationConfig: ApplicationConfig
) : ApNoteJobService {
override suspend fun createNoteJob(props: JobProps<DeliverPostJob>) {
val actor = props[DeliverPostJob.actor]
val postEntity = objectMapper.readValue<Post>(props[DeliverPostJob.post])
val mediaList =
objectMapper.readValue<List<dev.usbharu.hideout.domain.model.hideout.entity.Media>>(
props[DeliverPostJob.media]
)
val note = Note(
name = "Note",
id = postEntity.url,
attributedTo = actor,
content = postEntity.text,
published = Instant.ofEpochMilli(postEntity.createdAt).toString(),
to = listOf(APNoteServiceImpl.public, "$actor/follower"),
attachment = mediaList.map { Document(mediaType = "image/jpeg", url = it.url) }
)
val inbox = props[DeliverPostJob.inbox]
logger.debug("createNoteJob: actor={}, note={}, inbox={}", actor, postEntity, inbox)
transaction.transaction {
val signer = userQueryService.findByUrl(actor)
apRequestService.apPost(
inbox,
Create(
name = "Create Note",
`object` = note,
actor = note.attributedTo,
id = "${applicationConfig.url}/create/note/${postEntity.id}"
),
signer
)
}
}
companion object {
private val logger = LoggerFactory.getLogger(ApNoteJobServiceImpl::class.java)
}
}

View File

@ -0,0 +1,10 @@
package dev.usbharu.hideout.service.ap.job
import dev.usbharu.hideout.domain.model.job.DeliverReactionJob
import dev.usbharu.hideout.domain.model.job.DeliverRemoveReactionJob
import kjob.core.job.JobProps
interface ApReactionJobService {
suspend fun reactionJob(props: JobProps<DeliverReactionJob>)
suspend fun removeReactionJob(props: JobProps<DeliverRemoveReactionJob>)
}

View File

@ -0,0 +1,65 @@
package dev.usbharu.hideout.service.ap.job
import com.fasterxml.jackson.databind.ObjectMapper
import com.fasterxml.jackson.module.kotlin.readValue
import dev.usbharu.hideout.config.ApplicationConfig
import dev.usbharu.hideout.domain.model.ap.Like
import dev.usbharu.hideout.domain.model.ap.Undo
import dev.usbharu.hideout.domain.model.job.DeliverReactionJob
import dev.usbharu.hideout.domain.model.job.DeliverRemoveReactionJob
import dev.usbharu.hideout.query.UserQueryService
import dev.usbharu.hideout.service.ap.APRequestService
import kjob.core.job.JobProps
import org.springframework.beans.factory.annotation.Qualifier
import org.springframework.stereotype.Service
import java.time.Instant
@Service
class ApReactionJobServiceImpl(
private val userQueryService: UserQueryService,
private val apRequestService: APRequestService,
private val applicationConfig: ApplicationConfig,
@Qualifier("activitypub") private val objectMapper: ObjectMapper
) : ApReactionJobService {
override suspend fun reactionJob(props: JobProps<DeliverReactionJob>) {
val inbox = props[DeliverReactionJob.inbox]
val actor = props[DeliverReactionJob.actor]
val postUrl = props[DeliverReactionJob.postUrl]
val id = props[DeliverReactionJob.id]
val content = props[DeliverReactionJob.reaction]
val signer = userQueryService.findByUrl(actor)
apRequestService.apPost(
inbox,
Like(
name = "Like",
actor = actor,
`object` = postUrl,
id = "${applicationConfig.url}/like/note/$id",
content = content
),
signer
)
}
override suspend fun removeReactionJob(props: JobProps<DeliverRemoveReactionJob>) {
val inbox = props[DeliverRemoveReactionJob.inbox]
val actor = props[DeliverRemoveReactionJob.actor]
val like = objectMapper.readValue<Like>(props[DeliverRemoveReactionJob.like])
val signer = userQueryService.findByUrl(actor)
apRequestService.apPost(
inbox,
Undo(
name = "Undo Reaction",
actor = actor,
`object` = like,
id = "${applicationConfig.url}/undo/note/${like.id}",
published = Instant.now()
),
signer
)
}
}

View File

@ -40,6 +40,7 @@ class HttpSignatureUserDetailsService(
}
}
@Suppress("TooGenericExceptionCaught")
val verify = try {
httpSignatureVerifier.verify(
token.credentials as HttpRequest,

View File

@ -12,6 +12,7 @@ import dev.usbharu.hideout.domain.model.job.DeliverPostJob
import dev.usbharu.hideout.query.FollowerQueryService
import dev.usbharu.hideout.query.MediaQueryService
import dev.usbharu.hideout.query.UserQueryService
import dev.usbharu.hideout.service.ap.job.ApNoteJobServiceImpl
import dev.usbharu.hideout.service.job.JobQueueParentService
import io.ktor.client.*
import io.ktor.client.engine.mock.*
@ -24,7 +25,7 @@ import org.mockito.Mockito.anyLong
import org.mockito.Mockito.eq
import org.mockito.kotlin.*
import utils.JsonObjectMapper.objectMapper
import utils.TestApplicationConfig.testApplicationConfig
import utils.TestTransaction
import java.net.URL
import java.time.Instant
import kotlin.test.assertEquals
@ -104,11 +105,8 @@ class APNoteServiceImplTest {
postQueryService = mock(),
mediaQueryService = mediaQueryService,
objectMapper = objectMapper,
applicationConfig = testApplicationConfig,
postService = mock(),
apResourceResolveService = mock(),
apRequestService = mock(),
transaction = mock(),
postBuilder = postBuilder
)
val postEntity = postBuilder.of(
@ -138,21 +136,13 @@ class APNoteServiceImplTest {
respondOk()
}
)
val activityPubNoteService = APNoteServiceImpl(
jobQueueParentService = mock(),
postRepository = mock(),
apUserService = mock(),
val activityPubNoteService = ApNoteJobServiceImpl(
userQueryService = mock(),
followerQueryService = mock(),
postQueryService = mock(),
mediaQueryService = mediaQueryService,
objectMapper = objectMapper,
applicationConfig = testApplicationConfig,
postService = mock(),
apResourceResolveService = mock(),
apRequestService = mock(),
transaction = mock(),
postBuilder = postBuilder
transaction = TestTransaction,
applicationConfig = ApplicationConfig(URL("https://example.com"))
)
activityPubNoteService.createNoteJob(
JobProps(

View File

@ -13,6 +13,7 @@ import dev.usbharu.hideout.domain.model.hideout.entity.Post
import dev.usbharu.hideout.domain.model.hideout.entity.User
import dev.usbharu.hideout.domain.model.job.ReceiveFollowJob
import dev.usbharu.hideout.query.UserQueryService
import dev.usbharu.hideout.service.ap.job.APReceiveFollowJobServiceImpl
import dev.usbharu.hideout.service.job.JobQueueParentService
import dev.usbharu.hideout.service.user.UserService
import kjob.core.dsl.ScheduleContext
@ -42,12 +43,7 @@ class APReceiveFollowServiceImplTest {
val activityPubFollowService =
APReceiveFollowServiceImpl(
jobQueueParentService,
mock(),
mock(),
mock(),
TestTransaction,
objectMapper,
mock()
objectMapper
)
activityPubFollowService.receiveFollow(
Follow(
@ -151,14 +147,13 @@ class APReceiveFollowServiceImplTest {
onBlocking { followRequest(any(), any()) } doReturn false
}
val activityPubFollowService =
APReceiveFollowServiceImpl(
mock(),
APReceiveFollowJobServiceImpl(
apUserService,
userService,
userQueryService,
TestTransaction,
mock(),
userService,
objectMapper,
mock()
TestTransaction
)
activityPubFollowService.receiveFollowJob(
JobProps(