diff --git a/src/main/kotlin/dev/usbharu/hideout/service/ap/APAcceptService.kt b/src/main/kotlin/dev/usbharu/hideout/service/ap/APAcceptService.kt index fdcbbf61..951a8c78 100644 --- a/src/main/kotlin/dev/usbharu/hideout/service/ap/APAcceptService.kt +++ b/src/main/kotlin/dev/usbharu/hideout/service/ap/APAcceptService.kt @@ -1,8 +1,36 @@ package dev.usbharu.hideout.service.ap 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.query.UserQueryService +import dev.usbharu.hideout.service.user.IUserService +import io.ktor.http.* +import org.koin.core.annotation.Single interface APAcceptService { suspend fun receiveAccept(accept: Accept): ActivityPubResponse } + +@Single +class APAcceptServiceImpl( + private val userService: IUserService, + private val userQueryService: UserQueryService +) : APAcceptService { + 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 = userQueryService.findByUrl(userUrl) + val follower = userQueryService.findByUrl(followerUrl) + userService.follow(user.id, follower.id) + return ActivityPubStringResponse(HttpStatusCode.OK, "accepted") + } +} diff --git a/src/main/kotlin/dev/usbharu/hideout/service/ap/APAcceptServiceImpl.kt b/src/main/kotlin/dev/usbharu/hideout/service/ap/APAcceptServiceImpl.kt deleted file mode 100644 index b8fd337f..00000000 --- a/src/main/kotlin/dev/usbharu/hideout/service/ap/APAcceptServiceImpl.kt +++ /dev/null @@ -1,32 +0,0 @@ -package dev.usbharu.hideout.service.ap - -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.query.UserQueryService -import dev.usbharu.hideout.service.user.IUserService -import io.ktor.http.* -import org.koin.core.annotation.Single - -@Single -class APAcceptServiceImpl( - private val userService: IUserService, - private val userQueryService: UserQueryService -) : APAcceptService { - 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 = userQueryService.findByUrl(userUrl) - val follower = userQueryService.findByUrl(followerUrl) - userService.follow(user.id, follower.id) - return ActivityPubStringResponse(HttpStatusCode.OK, "accepted") - } -} diff --git a/src/main/kotlin/dev/usbharu/hideout/service/ap/APCreateService.kt b/src/main/kotlin/dev/usbharu/hideout/service/ap/APCreateService.kt index 29fa38de..b3e84557 100644 --- a/src/main/kotlin/dev/usbharu/hideout/service/ap/APCreateService.kt +++ b/src/main/kotlin/dev/usbharu/hideout/service/ap/APCreateService.kt @@ -1,8 +1,33 @@ package dev.usbharu.hideout.service.ap import dev.usbharu.hideout.domain.model.ActivityPubResponse +import dev.usbharu.hideout.domain.model.ActivityPubStringResponse import dev.usbharu.hideout.domain.model.ap.Create +import dev.usbharu.hideout.domain.model.ap.Note +import dev.usbharu.hideout.exception.ap.IllegalActivityPubObjectException +import dev.usbharu.hideout.service.core.Transaction +import io.ktor.http.* +import org.koin.core.annotation.Single interface APCreateService { suspend fun receiveCreate(create: Create): ActivityPubResponse } + +@Single +class APCreateServiceImpl( + private val apNoteService: APNoteService, + private val transaction: Transaction +) : APCreateService { + override suspend fun receiveCreate(create: Create): ActivityPubResponse { + val value = create.`object` ?: throw IllegalActivityPubObjectException("object is null") + if (value.type.contains("Note").not()) { + throw IllegalActivityPubObjectException("object is not Note") + } + + return transaction.transaction { + val note = value as Note + apNoteService.fetchNote(note) + ActivityPubStringResponse(HttpStatusCode.OK, "Created") + } + } +} diff --git a/src/main/kotlin/dev/usbharu/hideout/service/ap/APCreateServiceImpl.kt b/src/main/kotlin/dev/usbharu/hideout/service/ap/APCreateServiceImpl.kt deleted file mode 100644 index 2f1e4bd0..00000000 --- a/src/main/kotlin/dev/usbharu/hideout/service/ap/APCreateServiceImpl.kt +++ /dev/null @@ -1,29 +0,0 @@ -package dev.usbharu.hideout.service.ap - -import dev.usbharu.hideout.domain.model.ActivityPubResponse -import dev.usbharu.hideout.domain.model.ActivityPubStringResponse -import dev.usbharu.hideout.domain.model.ap.Create -import dev.usbharu.hideout.domain.model.ap.Note -import dev.usbharu.hideout.exception.ap.IllegalActivityPubObjectException -import dev.usbharu.hideout.service.core.Transaction -import io.ktor.http.* -import org.koin.core.annotation.Single - -@Single -class APCreateServiceImpl( - private val apNoteService: APNoteService, - private val transaction: Transaction -) : APCreateService { - override suspend fun receiveCreate(create: Create): ActivityPubResponse { - val value = create.`object` ?: throw IllegalActivityPubObjectException("object is null") - if (value.type.contains("Note").not()) { - throw IllegalActivityPubObjectException("object is not Note") - } - - return transaction.transaction { - val note = value as Note - apNoteService.fetchNote(note) - ActivityPubStringResponse(HttpStatusCode.OK, "Created") - } - } -} diff --git a/src/main/kotlin/dev/usbharu/hideout/service/ap/APLikeService.kt b/src/main/kotlin/dev/usbharu/hideout/service/ap/APLikeService.kt index 0ecb30a6..e8be8ec7 100644 --- a/src/main/kotlin/dev/usbharu/hideout/service/ap/APLikeService.kt +++ b/src/main/kotlin/dev/usbharu/hideout/service/ap/APLikeService.kt @@ -1,8 +1,46 @@ package dev.usbharu.hideout.service.ap import dev.usbharu.hideout.domain.model.ActivityPubResponse +import dev.usbharu.hideout.domain.model.ActivityPubStringResponse import dev.usbharu.hideout.domain.model.ap.Like +import dev.usbharu.hideout.exception.ap.IllegalActivityPubObjectException +import dev.usbharu.hideout.query.PostQueryService +import dev.usbharu.hideout.query.UserQueryService +import dev.usbharu.hideout.service.core.Transaction +import dev.usbharu.hideout.service.reaction.IReactionService +import io.ktor.http.* +import org.koin.core.annotation.Single interface APLikeService { suspend fun receiveLike(like: Like): ActivityPubResponse } + +@Single +class APLikeServiceImpl( + private val reactionService: IReactionService, + private val apUserService: APUserService, + private val apNoteService: APNoteService, + private val userQueryService: UserQueryService, + private val postQueryService: PostQueryService, + private val transaction: Transaction +) : APLikeService { + override suspend fun receiveLike(like: Like): ActivityPubResponse { + val actor = like.actor ?: throw IllegalActivityPubObjectException("actor is null") + val content = like.content ?: throw IllegalActivityPubObjectException("content is null") + like.`object` ?: throw IllegalActivityPubObjectException("object is null") + transaction.transaction { + val person = apUserService.fetchPerson(actor) + apNoteService.fetchNote(like.`object`!!) + + val user = userQueryService.findByUrl( + person.url + ?: throw IllegalActivityPubObjectException("actor is not found") + ) + + val post = postQueryService.findByUrl(like.`object`!!) + + reactionService.receiveReaction(content, actor.substringAfter("://").substringBefore("/"), user.id, post.id) + } + return ActivityPubStringResponse(HttpStatusCode.OK, "") + } +} diff --git a/src/main/kotlin/dev/usbharu/hideout/service/ap/APLikeServiceImpl.kt b/src/main/kotlin/dev/usbharu/hideout/service/ap/APLikeServiceImpl.kt deleted file mode 100644 index 929f79c5..00000000 --- a/src/main/kotlin/dev/usbharu/hideout/service/ap/APLikeServiceImpl.kt +++ /dev/null @@ -1,42 +0,0 @@ -package dev.usbharu.hideout.service.ap - -import dev.usbharu.hideout.domain.model.ActivityPubResponse -import dev.usbharu.hideout.domain.model.ActivityPubStringResponse -import dev.usbharu.hideout.domain.model.ap.Like -import dev.usbharu.hideout.exception.ap.IllegalActivityPubObjectException -import dev.usbharu.hideout.query.PostQueryService -import dev.usbharu.hideout.query.UserQueryService -import dev.usbharu.hideout.service.core.Transaction -import dev.usbharu.hideout.service.reaction.IReactionService -import io.ktor.http.* -import org.koin.core.annotation.Single - -@Single -class APLikeServiceImpl( - private val reactionService: IReactionService, - private val apUserService: APUserService, - private val apNoteService: APNoteService, - private val userQueryService: UserQueryService, - private val postQueryService: PostQueryService, - private val transaction: Transaction -) : APLikeService { - override suspend fun receiveLike(like: Like): ActivityPubResponse { - val actor = like.actor ?: throw IllegalActivityPubObjectException("actor is null") - val content = like.content ?: throw IllegalActivityPubObjectException("content is null") - like.`object` ?: throw IllegalActivityPubObjectException("object is null") - transaction.transaction { - val person = apUserService.fetchPerson(actor) - apNoteService.fetchNote(like.`object`!!) - - val user = userQueryService.findByUrl( - person.url - ?: throw IllegalActivityPubObjectException("actor is not found") - ) - - val post = postQueryService.findByUrl(like.`object`!!) - - reactionService.receiveReaction(content, actor.substringAfter("://").substringBefore("/"), user.id, post.id) - } - return ActivityPubStringResponse(HttpStatusCode.OK, "") - } -} diff --git a/src/main/kotlin/dev/usbharu/hideout/service/ap/APNoteService.kt b/src/main/kotlin/dev/usbharu/hideout/service/ap/APNoteService.kt index 33b9d457..f32f7b4e 100644 --- a/src/main/kotlin/dev/usbharu/hideout/service/ap/APNoteService.kt +++ b/src/main/kotlin/dev/usbharu/hideout/service/ap/APNoteService.kt @@ -1,9 +1,26 @@ package dev.usbharu.hideout.service.ap +import com.fasterxml.jackson.module.kotlin.readValue +import dev.usbharu.hideout.config.Config +import dev.usbharu.hideout.domain.model.ap.Create 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 import dev.usbharu.hideout.domain.model.job.DeliverPostJob +import dev.usbharu.hideout.exception.ap.IllegalActivityPubObjectException +import dev.usbharu.hideout.plugins.getAp +import dev.usbharu.hideout.plugins.postAp +import dev.usbharu.hideout.query.FollowerQueryService +import dev.usbharu.hideout.query.PostQueryService +import dev.usbharu.hideout.query.UserQueryService +import dev.usbharu.hideout.repository.IPostRepository +import dev.usbharu.hideout.service.job.JobQueueParentService +import io.ktor.client.* +import io.ktor.client.statement.* import kjob.core.job.JobProps +import org.koin.core.annotation.Single +import org.slf4j.LoggerFactory +import java.time.Instant interface APNoteService { @@ -13,3 +30,151 @@ interface APNoteService { suspend fun fetchNote(url: String, targetActor: String? = null): Note suspend fun fetchNote(note: Note, targetActor: String? = null): Note } + +@Single +class APNoteServiceImpl( + private val httpClient: HttpClient, + private val jobQueueParentService: JobQueueParentService, + private val postRepository: IPostRepository, + private val apUserService: APUserService, + private val userQueryService: UserQueryService, + private val followerQueryService: FollowerQueryService, + private val postQueryService: PostQueryService +) : APNoteService { + + private val logger = LoggerFactory.getLogger(this::class.java) + + override suspend fun createNote(post: Post) { + val followers = followerQueryService.findFollowersById(post.userId) + val userEntity = userQueryService.findById(post.userId) + val note = Config.configData.objectMapper.writeValueAsString(post) + followers.forEach { followerEntity -> + jobQueueParentService.schedule(DeliverPostJob) { + props[DeliverPostJob.actor] = userEntity.url + props[DeliverPostJob.post] = note + props[DeliverPostJob.inbox] = followerEntity.inbox + } + } + } + + override suspend fun createNoteJob(props: JobProps) { + val actor = props[DeliverPostJob.actor] + val postEntity = Config.configData.objectMapper.readValue(props[DeliverPostJob.post]) + val note = Note( + name = "Note", + id = postEntity.url, + attributedTo = actor, + content = postEntity.text, + published = Instant.ofEpochMilli(postEntity.createdAt).toString(), + to = listOf(public, actor + "/follower") + ) + val inbox = props[DeliverPostJob.inbox] + logger.debug("createNoteJob: actor={}, note={}, inbox={}", actor, postEntity, inbox) + httpClient.postAp( + urlString = inbox, + username = "$actor#pubkey", + jsonLd = Create( + name = "Create Note", + `object` = note, + actor = note.attributedTo, + id = "${Config.configData.url}/create/note/${postEntity.id}" + ) + ) + } + + override suspend fun fetchNote(url: String, targetActor: String?): Note { + val post = postQueryService.findByUrl(url) + try { + return postToNote(post) + } catch (_: NoSuchElementException) { + } catch (_: IllegalArgumentException) { + } + + val response = httpClient.getAp( + url, + targetActor?.let { "$targetActor#pubkey" } + ) + val note = Config.configData.objectMapper.readValue(response.bodyAsText()) + return note(note, targetActor, url) + } + + private suspend fun postToNote(post: Post): Note { + val user = userQueryService.findById(post.userId) + val reply = post.replyId?.let { postQueryService.findById(it) } + return Note( + name = "Post", + id = post.apId, + attributedTo = user.url, + content = post.text, + published = Instant.ofEpochMilli(post.createdAt).toString(), + to = listOf(public, user.url + "/follower"), + sensitive = post.sensitive, + cc = listOf(public, user.url + "/follower"), + inReplyTo = reply?.url + ) + } + + private suspend fun note( + note: Note, + targetActor: String?, + url: String + ): Note { + val findByApId = try { + postQueryService.findByApId(url) + } catch (_: NoSuchElementException) { + return internalNote(note, targetActor, url) + } catch (_: IllegalArgumentException) { + return internalNote(note, targetActor, url) + } + return postToNote(findByApId) + } + + private suspend fun internalNote(note: Note, targetActor: String?, url: String): Note { + val person = apUserService.fetchPerson( + note.attributedTo ?: throw IllegalActivityPubObjectException("note.attributedTo is null"), + targetActor + ) + val user = + userQueryService.findByUrl(person.url ?: throw IllegalActivityPubObjectException("person.url is null")) + + val visibility = + if (note.to.contains(public) && note.cc.contains(public)) { + Visibility.PUBLIC + } else if (note.to.find { it.endsWith("/followers") } != null && note.cc.contains(public)) { + Visibility.UNLISTED + } else if (note.to.find { it.endsWith("/followers") } != null) { + Visibility.FOLLOWERS + } else { + Visibility.DIRECT + } + + val reply = note.inReplyTo?.let { + fetchNote(it, targetActor) + postQueryService.findByUrl(it) + } + + postRepository.save( + Post( + id = postRepository.generateId(), + userId = user.id, + overview = null, + text = note.content.orEmpty(), + createdAt = Instant.parse(note.published).toEpochMilli(), + visibility = visibility, + url = note.id ?: url, + repostId = null, + replyId = reply?.id, + sensitive = note.sensitive, + apId = note.id ?: url, + ) + ) + return note + } + + override suspend fun fetchNote(note: Note, targetActor: String?): Note = + note(note, targetActor, note.id ?: throw IllegalArgumentException("note.id is null")) + + companion object { + const val public: String = "https://www.w3.org/ns/activitystreams#Public" + } +} diff --git a/src/main/kotlin/dev/usbharu/hideout/service/ap/APNoteServiceImpl.kt b/src/main/kotlin/dev/usbharu/hideout/service/ap/APNoteServiceImpl.kt deleted file mode 100644 index d46d02a7..00000000 --- a/src/main/kotlin/dev/usbharu/hideout/service/ap/APNoteServiceImpl.kt +++ /dev/null @@ -1,171 +0,0 @@ -package dev.usbharu.hideout.service.ap - -import com.fasterxml.jackson.module.kotlin.readValue -import dev.usbharu.hideout.config.Config -import dev.usbharu.hideout.domain.model.ap.Create -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 -import dev.usbharu.hideout.domain.model.job.DeliverPostJob -import dev.usbharu.hideout.exception.ap.IllegalActivityPubObjectException -import dev.usbharu.hideout.plugins.getAp -import dev.usbharu.hideout.plugins.postAp -import dev.usbharu.hideout.query.FollowerQueryService -import dev.usbharu.hideout.query.PostQueryService -import dev.usbharu.hideout.query.UserQueryService -import dev.usbharu.hideout.repository.IPostRepository -import dev.usbharu.hideout.service.job.JobQueueParentService -import io.ktor.client.* -import io.ktor.client.statement.* -import kjob.core.job.JobProps -import org.koin.core.annotation.Single -import org.slf4j.LoggerFactory -import java.time.Instant - -@Single -class APNoteServiceImpl( - private val httpClient: HttpClient, - private val jobQueueParentService: JobQueueParentService, - private val postRepository: IPostRepository, - private val apUserService: APUserService, - private val userQueryService: UserQueryService, - private val followerQueryService: FollowerQueryService, - private val postQueryService: PostQueryService -) : APNoteService { - - private val logger = LoggerFactory.getLogger(this::class.java) - - override suspend fun createNote(post: Post) { - val followers = followerQueryService.findFollowersById(post.userId) - val userEntity = userQueryService.findById(post.userId) - val note = Config.configData.objectMapper.writeValueAsString(post) - followers.forEach { followerEntity -> - jobQueueParentService.schedule(DeliverPostJob) { - props[it.actor] = userEntity.url - props[it.post] = note - props[it.inbox] = followerEntity.inbox - } - } - } - - override suspend fun createNoteJob(props: JobProps) { - val actor = props[DeliverPostJob.actor] - val postEntity = Config.configData.objectMapper.readValue(props[DeliverPostJob.post]) - val note = Note( - name = "Note", - id = postEntity.url, - attributedTo = actor, - content = postEntity.text, - published = Instant.ofEpochMilli(postEntity.createdAt).toString(), - to = listOf(public, actor + "/follower") - ) - val inbox = props[DeliverPostJob.inbox] - logger.debug("createNoteJob: actor={}, note={}, inbox={}", actor, postEntity, inbox) - httpClient.postAp( - urlString = inbox, - username = "$actor#pubkey", - jsonLd = Create( - name = "Create Note", - `object` = note, - actor = note.attributedTo, - id = "${Config.configData.url}/create/note/${postEntity.id}" - ) - ) - } - - override suspend fun fetchNote(url: String, targetActor: String?): Note { - val post = postQueryService.findByUrl(url) - try { - return postToNote(post) - } catch (_: NoSuchElementException) { - } catch (_: IllegalArgumentException) { - } - - val response = httpClient.getAp( - url, - targetActor?.let { "$targetActor#pubkey" } - ) - val note = Config.configData.objectMapper.readValue(response.bodyAsText()) - return note(note, targetActor, url) - } - - private suspend fun postToNote(post: Post): Note { - val user = userQueryService.findById(post.userId) - val reply = post.replyId?.let { postQueryService.findById(it) } - return Note( - name = "Post", - id = post.apId, - attributedTo = user.url, - content = post.text, - published = Instant.ofEpochMilli(post.createdAt).toString(), - to = listOf(public, user.url + "/follower"), - sensitive = post.sensitive, - cc = listOf(public, user.url + "/follower"), - inReplyTo = reply?.url - ) - } - - private suspend fun note( - note: Note, - targetActor: String?, - url: String - ): Note { - val findByApId = try { - postQueryService.findByApId(url) - } catch (_: NoSuchElementException) { - return internalNote(note, targetActor, url) - } catch (_: IllegalArgumentException) { - return internalNote(note, targetActor, url) - } - return postToNote(findByApId) - } - - private suspend fun internalNote(note: Note, targetActor: String?, url: String): Note { - val person = apUserService.fetchPerson( - note.attributedTo ?: throw IllegalActivityPubObjectException("note.attributedTo is null"), - targetActor - ) - val user = - userQueryService.findByUrl(person.url ?: throw IllegalActivityPubObjectException("person.url is null")) - - val visibility = - if (note.to.contains(public) && note.cc.contains(public)) { - Visibility.PUBLIC - } else if (note.to.find { it.endsWith("/followers") } != null && note.cc.contains(public)) { - Visibility.UNLISTED - } else if (note.to.find { it.endsWith("/followers") } != null) { - Visibility.FOLLOWERS - } else { - Visibility.DIRECT - } - - val reply = note.inReplyTo?.let { - fetchNote(it, targetActor) - postQueryService.findByUrl(it) - } - - postRepository.save( - Post( - id = postRepository.generateId(), - userId = user.id, - overview = null, - text = note.content.orEmpty(), - createdAt = Instant.parse(note.published).toEpochMilli(), - visibility = visibility, - url = note.id ?: url, - repostId = null, - replyId = reply?.id, - sensitive = note.sensitive, - apId = note.id ?: url, - ) - ) - return note - } - - override suspend fun fetchNote(note: Note, targetActor: String?): Note = - note(note, targetActor, note.id ?: throw IllegalArgumentException("note.id is null")) - - companion object { - const val public: String = "https://www.w3.org/ns/activitystreams#Public" - } -} diff --git a/src/main/kotlin/dev/usbharu/hideout/service/ap/APReactionService.kt b/src/main/kotlin/dev/usbharu/hideout/service/ap/APReactionService.kt index aa12198c..e8b21f84 100644 --- a/src/main/kotlin/dev/usbharu/hideout/service/ap/APReactionService.kt +++ b/src/main/kotlin/dev/usbharu/hideout/service/ap/APReactionService.kt @@ -1,9 +1,22 @@ package dev.usbharu.hideout.service.ap +import com.fasterxml.jackson.module.kotlin.readValue +import dev.usbharu.hideout.config.Config +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 +import dev.usbharu.hideout.plugins.postAp +import dev.usbharu.hideout.query.FollowerQueryService +import dev.usbharu.hideout.query.PostQueryService +import dev.usbharu.hideout.query.UserQueryService +import dev.usbharu.hideout.repository.IPostRepository +import dev.usbharu.hideout.service.job.JobQueueParentService +import io.ktor.client.* import kjob.core.job.JobProps +import org.koin.core.annotation.Single +import java.time.Instant interface APReactionService { suspend fun reaction(like: Reaction) @@ -11,3 +24,80 @@ interface APReactionService { suspend fun reactionJob(props: JobProps) suspend fun removeReactionJob(props: JobProps) } + +@Single +class APReactionServiceImpl( + private val jobQueueParentService: JobQueueParentService, + private val iPostRepository: IPostRepository, + private val httpClient: HttpClient, + private val userQueryService: UserQueryService, + private val followerQueryService: FollowerQueryService, + private val postQueryService: PostQueryService +) : APReactionService { + override suspend fun reaction(like: Reaction) { + val followers = followerQueryService.findFollowersById(like.userId) + val user = userQueryService.findById(like.userId) + val post = + postQueryService.findById(like.postId) + followers.forEach { follower -> + jobQueueParentService.schedule(DeliverReactionJob) { + props[DeliverReactionJob.actor] = user.url + props[DeliverReactionJob.reaction] = "❤" + props[DeliverReactionJob.inbox] = follower.inbox + props[DeliverReactionJob.postUrl] = post.url + props[DeliverReactionJob.id] = post.id.toString() + } + } + } + + override suspend fun removeReaction(like: Reaction) { + val followers = followerQueryService.findFollowersById(like.userId) + val user = userQueryService.findById(like.userId) + val post = + postQueryService.findById(like.postId) + followers.forEach { follower -> + jobQueueParentService.schedule(DeliverRemoveReactionJob) { + props[DeliverRemoveReactionJob.actor] = user.url + props[DeliverRemoveReactionJob.inbox] = follower.inbox + props[DeliverRemoveReactionJob.id] = post.id.toString() + props[DeliverRemoveReactionJob.like] = Config.configData.objectMapper.writeValueAsString(like) + } + } + } + + override suspend fun reactionJob(props: JobProps) { + 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] + httpClient.postAp( + urlString = inbox, + username = "$actor#pubkey", + jsonLd = Like( + name = "Like", + actor = actor, + `object` = postUrl, + id = "${Config.configData.url}/like/note/$id", + content = content + ) + ) + } + + override suspend fun removeReactionJob(props: JobProps) { + val inbox = props[DeliverRemoveReactionJob.inbox] + val actor = props[DeliverRemoveReactionJob.actor] + val like = Config.configData.objectMapper.readValue(props[DeliverRemoveReactionJob.like]) + httpClient.postAp( + urlString = inbox, + username = "$actor#pubkey", + jsonLd = Undo( + name = "Undo Reaction", + actor = actor, + `object` = like, + id = "${Config.configData.url}/undo/note/${like.id}", + published = Instant.now() + ) + ) + } +} diff --git a/src/main/kotlin/dev/usbharu/hideout/service/ap/APReactionServiceImpl.kt b/src/main/kotlin/dev/usbharu/hideout/service/ap/APReactionServiceImpl.kt deleted file mode 100644 index 3635836b..00000000 --- a/src/main/kotlin/dev/usbharu/hideout/service/ap/APReactionServiceImpl.kt +++ /dev/null @@ -1,96 +0,0 @@ -package dev.usbharu.hideout.service.ap - -import com.fasterxml.jackson.module.kotlin.readValue -import dev.usbharu.hideout.config.Config -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 -import dev.usbharu.hideout.plugins.postAp -import dev.usbharu.hideout.query.FollowerQueryService -import dev.usbharu.hideout.query.PostQueryService -import dev.usbharu.hideout.query.UserQueryService -import dev.usbharu.hideout.repository.IPostRepository -import dev.usbharu.hideout.service.job.JobQueueParentService -import io.ktor.client.* -import kjob.core.job.JobProps -import org.koin.core.annotation.Single -import java.time.Instant - -@Single -class APReactionServiceImpl( - private val jobQueueParentService: JobQueueParentService, - private val iPostRepository: IPostRepository, - private val httpClient: HttpClient, - private val userQueryService: UserQueryService, - private val followerQueryService: FollowerQueryService, - private val postQueryService: PostQueryService -) : APReactionService { - override suspend fun reaction(like: Reaction) { - val followers = followerQueryService.findFollowersById(like.userId) - val user = userQueryService.findById(like.userId) - val post = - postQueryService.findById(like.postId) - followers.forEach { follower -> - jobQueueParentService.schedule(DeliverReactionJob) { - props[it.actor] = user.url - props[it.reaction] = "❤" - props[it.inbox] = follower.inbox - props[it.postUrl] = post.url - props[it.id] = post.id.toString() - } - } - } - - override suspend fun removeReaction(like: Reaction) { - val followers = followerQueryService.findFollowersById(like.userId) - val user = userQueryService.findById(like.userId) - val post = - postQueryService.findById(like.postId) - followers.forEach { follower -> - jobQueueParentService.schedule(DeliverRemoveReactionJob) { - props[it.actor] = user.url - props[it.inbox] = follower.inbox - props[it.id] = post.id.toString() - props[it.like] = Config.configData.objectMapper.writeValueAsString(like) - } - } - } - - override suspend fun reactionJob(props: JobProps) { - 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] - httpClient.postAp( - urlString = inbox, - username = "$actor#pubkey", - jsonLd = Like( - name = "Like", - actor = actor, - `object` = postUrl, - id = "${Config.configData.url}/like/note/$id", - content = content - ) - ) - } - - override suspend fun removeReactionJob(props: JobProps) { - val inbox = props[DeliverRemoveReactionJob.inbox] - val actor = props[DeliverRemoveReactionJob.actor] - val like = Config.configData.objectMapper.readValue(props[DeliverRemoveReactionJob.like]) - httpClient.postAp( - urlString = inbox, - username = "$actor#pubkey", - jsonLd = Undo( - name = "Undo Reaction", - actor = actor, - `object` = like, - id = "${Config.configData.url}/undo/note/${like.id}", - published = Instant.now() - ) - ) - } -} diff --git a/src/main/kotlin/dev/usbharu/hideout/service/ap/APReceiveFollowService.kt b/src/main/kotlin/dev/usbharu/hideout/service/ap/APReceiveFollowService.kt index 7aaf21e5..54c0d39d 100644 --- a/src/main/kotlin/dev/usbharu/hideout/service/ap/APReceiveFollowService.kt +++ b/src/main/kotlin/dev/usbharu/hideout/service/ap/APReceiveFollowService.kt @@ -1,11 +1,67 @@ package dev.usbharu.hideout.service.ap +import com.fasterxml.jackson.module.kotlin.readValue +import dev.usbharu.hideout.config.Config 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.plugins.postAp +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.IUserService +import io.ktor.client.* +import io.ktor.http.* import kjob.core.job.JobProps +import org.koin.core.annotation.Single interface APReceiveFollowService { suspend fun receiveFollow(follow: Follow): ActivityPubResponse suspend fun receiveFollowJob(props: JobProps) } + +@Single +class APReceiveFollowServiceImpl( + private val jobQueueParentService: JobQueueParentService, + private val apUserService: APUserService, + private val userService: IUserService, + private val httpClient: HttpClient, + private val userQueryService: UserQueryService, + private val transaction: Transaction +) : APReceiveFollowService { + override suspend fun receiveFollow(follow: Follow): ActivityPubResponse { + // TODO: Verify HTTP Signature + jobQueueParentService.schedule(ReceiveFollowJob) { + props[ReceiveFollowJob.actor] = follow.actor + props[ReceiveFollowJob.follow] = Config.configData.objectMapper.writeValueAsString(follow) + props[ReceiveFollowJob.targetActor] = follow.`object` + } + return ActivityPubStringResponse(HttpStatusCode.OK, "{}", ContentType.Application.Json) + } + + override suspend fun receiveFollowJob(props: JobProps) { + transaction.transaction { + val actor = props[ReceiveFollowJob.actor] + val targetActor = props[ReceiveFollowJob.targetActor] + val person = apUserService.fetchPerson(actor, targetActor) + val follow = Config.configData.objectMapper.readValue(props[ReceiveFollowJob.follow]) + httpClient.postAp( + urlString = person.inbox ?: throw IllegalArgumentException("inbox is not found"), + username = "$targetActor#pubkey", + jsonLd = Accept( + name = "Follow", + `object` = follow, + actor = targetActor + ) + ) + + val targetEntity = userQueryService.findByUrl(targetActor) + val followActorEntity = + userQueryService.findByUrl(follow.actor ?: throw java.lang.IllegalArgumentException("Actor is null")) + + userService.followRequest(targetEntity.id, followActorEntity.id) + } + } +} diff --git a/src/main/kotlin/dev/usbharu/hideout/service/ap/APReceiveFollowServiceImpl.kt b/src/main/kotlin/dev/usbharu/hideout/service/ap/APReceiveFollowServiceImpl.kt deleted file mode 100644 index 3e8500df..00000000 --- a/src/main/kotlin/dev/usbharu/hideout/service/ap/APReceiveFollowServiceImpl.kt +++ /dev/null @@ -1,62 +0,0 @@ -package dev.usbharu.hideout.service.ap - -import com.fasterxml.jackson.module.kotlin.readValue -import dev.usbharu.hideout.config.Config -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.plugins.postAp -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.IUserService -import io.ktor.client.* -import io.ktor.http.* -import kjob.core.job.JobProps -import org.koin.core.annotation.Single - -@Single -class APReceiveFollowServiceImpl( - private val jobQueueParentService: JobQueueParentService, - private val apUserService: APUserService, - private val userService: IUserService, - private val httpClient: HttpClient, - private val userQueryService: UserQueryService, - private val transaction: Transaction -) : APReceiveFollowService { - override suspend fun receiveFollow(follow: Follow): ActivityPubResponse { - // TODO: Verify HTTP Signature - jobQueueParentService.schedule(ReceiveFollowJob) { - props[it.actor] = follow.actor - props[it.follow] = Config.configData.objectMapper.writeValueAsString(follow) - props[it.targetActor] = follow.`object` - } - return ActivityPubStringResponse(HttpStatusCode.OK, "{}", ContentType.Application.Json) - } - - override suspend fun receiveFollowJob(props: JobProps) { - transaction.transaction { - val actor = props[ReceiveFollowJob.actor] - val targetActor = props[ReceiveFollowJob.targetActor] - val person = apUserService.fetchPerson(actor, targetActor) - val follow = Config.configData.objectMapper.readValue(props[ReceiveFollowJob.follow]) - httpClient.postAp( - urlString = person.inbox ?: throw IllegalArgumentException("inbox is not found"), - username = "$targetActor#pubkey", - jsonLd = Accept( - name = "Follow", - `object` = follow, - actor = targetActor - ) - ) - - val targetEntity = userQueryService.findByUrl(targetActor) - val followActorEntity = - userQueryService.findByUrl(follow.actor ?: throw java.lang.IllegalArgumentException("Actor is null")) - - userService.followRequest(targetEntity.id, followActorEntity.id) - } - } -} diff --git a/src/main/kotlin/dev/usbharu/hideout/service/ap/APSendFollowService.kt b/src/main/kotlin/dev/usbharu/hideout/service/ap/APSendFollowService.kt index d4058113..58ae74f5 100644 --- a/src/main/kotlin/dev/usbharu/hideout/service/ap/APSendFollowService.kt +++ b/src/main/kotlin/dev/usbharu/hideout/service/ap/APSendFollowService.kt @@ -1,7 +1,27 @@ package dev.usbharu.hideout.service.ap +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 interface APSendFollowService { suspend fun sendFollow(sendFollowDto: SendFollowDto) } + +@Single +class APSendFollowServiceImpl(private val httpClient: HttpClient) : APSendFollowService { + 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 + ) + } +} diff --git a/src/main/kotlin/dev/usbharu/hideout/service/ap/APSendFollowServiceImpl.kt b/src/main/kotlin/dev/usbharu/hideout/service/ap/APSendFollowServiceImpl.kt deleted file mode 100644 index a2ccb74f..00000000 --- a/src/main/kotlin/dev/usbharu/hideout/service/ap/APSendFollowServiceImpl.kt +++ /dev/null @@ -1,23 +0,0 @@ -package dev.usbharu.hideout.service.ap - -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 APSendFollowServiceImpl(private val httpClient: HttpClient) : APSendFollowService { - 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 - ) - } -} diff --git a/src/main/kotlin/dev/usbharu/hideout/service/ap/APService.kt b/src/main/kotlin/dev/usbharu/hideout/service/ap/APService.kt index 719149c1..baf1b023 100644 --- a/src/main/kotlin/dev/usbharu/hideout/service/ap/APService.kt +++ b/src/main/kotlin/dev/usbharu/hideout/service/ap/APService.kt @@ -1,8 +1,17 @@ package dev.usbharu.hideout.service.ap +import com.fasterxml.jackson.databind.JsonNode +import com.fasterxml.jackson.module.kotlin.readValue +import dev.usbharu.hideout.config.Config import dev.usbharu.hideout.domain.model.ActivityPubResponse -import dev.usbharu.hideout.domain.model.job.HideoutJob +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 org.koin.core.annotation.Single +import org.slf4j.Logger +import org.slf4j.LoggerFactory interface APService { fun parseActivity(json: String): ActivityType @@ -162,3 +171,68 @@ enum class ExtendedActivityVocabulary { enum class ExtendedVocabulary { Emoji } + +@Single +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 +) : APService { + + val logger: Logger = LoggerFactory.getLogger(this::class.java) + override fun parseActivity(json: String): ActivityType { + val readTree = Config.configData.objectMapper.readTree(json) + logger.trace("readTree: {}", readTree) + if (readTree.isObject.not()) { + throw JsonParseException("Json is not object.") + } + val type = readTree["type"] + if (type.isArray) { + return type.firstNotNullOf { jsonNode: JsonNode -> + ActivityType.values().firstOrNull { it.name.equals(jsonNode.asText(), true) } + } + } + return ActivityType.values().first { it.name.equals(type.asText(), true) } + } + + @Suppress("CyclomaticComplexMethod", "NotImplementedDeclaration") + override suspend fun processActivity(json: String, type: ActivityType): ActivityPubResponse { + logger.debug("proccess activity: {}", type) + return when (type) { + ActivityType.Accept -> apAcceptService.receiveAccept(Config.configData.objectMapper.readValue(json)) + ActivityType.Follow -> apReceiveFollowService.receiveFollow( + Config.configData.objectMapper.readValue( + json, + Follow::class.java + ) + ) + + ActivityType.Create -> apCreateService.receiveCreate(Config.configData.objectMapper.readValue(json)) + ActivityType.Like -> apLikeService.receiveLike(Config.configData.objectMapper.readValue(json)) + ActivityType.Undo -> apUndoService.receiveUndo(Config.configData.objectMapper.readValue(json)) + + else -> { + throw IllegalArgumentException("$type is not supported.") + } + } + } + + override suspend fun processActivity(job: JobContextWithProps, hideoutJob: HideoutJob) { + logger.debug("processActivity: ${hideoutJob.name}") + when (hideoutJob) { + ReceiveFollowJob -> apReceiveFollowService.receiveFollowJob( + job.props as JobProps + ) + + DeliverPostJob -> apNoteService.createNoteJob(job.props as JobProps) + DeliverReactionJob -> apReactionService.reactionJob(job.props as JobProps) + DeliverRemoveReactionJob -> apReactionService.removeReactionJob( + job.props as JobProps + ) + } + } +} diff --git a/src/main/kotlin/dev/usbharu/hideout/service/ap/APServiceImpl.kt b/src/main/kotlin/dev/usbharu/hideout/service/ap/APServiceImpl.kt deleted file mode 100644 index f2cd4000..00000000 --- a/src/main/kotlin/dev/usbharu/hideout/service/ap/APServiceImpl.kt +++ /dev/null @@ -1,79 +0,0 @@ -package dev.usbharu.hideout.service.ap - -import com.fasterxml.jackson.databind.JsonNode -import com.fasterxml.jackson.module.kotlin.readValue -import dev.usbharu.hideout.config.Config.configData -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 org.koin.core.annotation.Single -import org.slf4j.Logger -import org.slf4j.LoggerFactory - -@Single -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 -) : APService { - - val logger: Logger = LoggerFactory.getLogger(this::class.java) - override fun parseActivity(json: String): ActivityType { - val readTree = configData.objectMapper.readTree(json) - logger.trace("readTree: {}", readTree) - if (readTree.isObject.not()) { - throw JsonParseException("Json is not object.") - } - val type = readTree["type"] - if (type.isArray) { - return type.firstNotNullOf { jsonNode: JsonNode -> - ActivityType.values().firstOrNull { it.name.equals(jsonNode.asText(), true) } - } - } - return ActivityType.values().first { it.name.equals(type.asText(), true) } - } - - @Suppress("CyclomaticComplexMethod", "NotImplementedDeclaration") - override suspend fun processActivity(json: String, type: ActivityType): ActivityPubResponse { - logger.debug("proccess activity: {}", type) - return when (type) { - ActivityType.Accept -> apAcceptService.receiveAccept(configData.objectMapper.readValue(json)) - ActivityType.Follow -> apReceiveFollowService.receiveFollow( - configData.objectMapper.readValue( - json, - Follow::class.java - ) - ) - - ActivityType.Create -> apCreateService.receiveCreate(configData.objectMapper.readValue(json)) - ActivityType.Like -> apLikeService.receiveLike(configData.objectMapper.readValue(json)) - ActivityType.Undo -> apUndoService.receiveUndo(configData.objectMapper.readValue(json)) - - else -> { - throw IllegalArgumentException("$type is not supported.") - } - } - } - - override suspend fun processActivity(job: JobContextWithProps, hideoutJob: HideoutJob) { - logger.debug("processActivity: ${hideoutJob.name}") - when (hideoutJob) { - ReceiveFollowJob -> apReceiveFollowService.receiveFollowJob( - job.props as JobProps - ) - - DeliverPostJob -> apNoteService.createNoteJob(job.props as JobProps) - DeliverReactionJob -> apReactionService.reactionJob(job.props as JobProps) - DeliverRemoveReactionJob -> apReactionService.removeReactionJob( - job.props as JobProps - ) - } - } -} diff --git a/src/main/kotlin/dev/usbharu/hideout/service/ap/APUndoService.kt b/src/main/kotlin/dev/usbharu/hideout/service/ap/APUndoService.kt index f3b0c587..a058dbd5 100644 --- a/src/main/kotlin/dev/usbharu/hideout/service/ap/APUndoService.kt +++ b/src/main/kotlin/dev/usbharu/hideout/service/ap/APUndoService.kt @@ -1,8 +1,54 @@ package dev.usbharu.hideout.service.ap import dev.usbharu.hideout.domain.model.ActivityPubResponse +import dev.usbharu.hideout.domain.model.ActivityPubStringResponse +import dev.usbharu.hideout.domain.model.ap.Follow import dev.usbharu.hideout.domain.model.ap.Undo +import dev.usbharu.hideout.query.UserQueryService +import dev.usbharu.hideout.service.core.Transaction +import dev.usbharu.hideout.service.user.IUserService +import io.ktor.http.* +import org.koin.core.annotation.Single interface APUndoService { suspend fun receiveUndo(undo: Undo): ActivityPubResponse } + +@Single +@Suppress("UnsafeCallOnNullableType") +class APUndoServiceImpl( + private val userService: IUserService, + private val apUserService: APUserService, + private val userQueryService: UserQueryService, + private val transaction: Transaction +) : APUndoService { + override suspend fun receiveUndo(undo: Undo): ActivityPubResponse { + if (undo.actor == null) { + return ActivityPubStringResponse(HttpStatusCode.BadRequest, "actor is null") + } + + val type = + undo.`object`?.type.orEmpty() + .firstOrNull { it == "Block" || it == "Follow" || it == "Like" || it == "Announce" || it == "Accept" } + ?: return ActivityPubStringResponse(HttpStatusCode.BadRequest, "unknown type ${undo.`object`?.type}") + + when (type) { + "Follow" -> { + val follow = undo.`object` as Follow + + if (follow.`object` == null) { + return ActivityPubStringResponse(HttpStatusCode.BadRequest, "object.object is null") + } + transaction.transaction { + apUserService.fetchPerson(undo.actor!!, follow.`object`) + val follower = userQueryService.findByUrl(undo.actor!!) + val target = userQueryService.findByUrl(follow.`object`!!) + userService.unfollow(target.id, follower.id) + } + } + + else -> {} + } + TODO() + } +} diff --git a/src/main/kotlin/dev/usbharu/hideout/service/ap/APUndoServiceImpl.kt b/src/main/kotlin/dev/usbharu/hideout/service/ap/APUndoServiceImpl.kt deleted file mode 100644 index 381e4160..00000000 --- a/src/main/kotlin/dev/usbharu/hideout/service/ap/APUndoServiceImpl.kt +++ /dev/null @@ -1,50 +0,0 @@ -package dev.usbharu.hideout.service.ap - -import dev.usbharu.hideout.domain.model.ActivityPubResponse -import dev.usbharu.hideout.domain.model.ActivityPubStringResponse -import dev.usbharu.hideout.domain.model.ap.Follow -import dev.usbharu.hideout.domain.model.ap.Undo -import dev.usbharu.hideout.query.UserQueryService -import dev.usbharu.hideout.service.core.Transaction -import dev.usbharu.hideout.service.user.IUserService -import io.ktor.http.* -import org.koin.core.annotation.Single - -@Single -@Suppress("UnsafeCallOnNullableType") -class APUndoServiceImpl( - private val userService: IUserService, - private val apUserService: APUserService, - private val userQueryService: UserQueryService, - private val transaction: Transaction -) : APUndoService { - override suspend fun receiveUndo(undo: Undo): ActivityPubResponse { - if (undo.actor == null) { - return ActivityPubStringResponse(HttpStatusCode.BadRequest, "actor is null") - } - - val type = - undo.`object`?.type.orEmpty() - .firstOrNull { it == "Block" || it == "Follow" || it == "Like" || it == "Announce" || it == "Accept" } - ?: return ActivityPubStringResponse(HttpStatusCode.BadRequest, "unknown type ${undo.`object`?.type}") - - when (type) { - "Follow" -> { - val follow = undo.`object` as Follow - - if (follow.`object` == null) { - return ActivityPubStringResponse(HttpStatusCode.BadRequest, "object.object is null") - } - transaction.transaction { - apUserService.fetchPerson(undo.actor!!, follow.`object`) - val follower = userQueryService.findByUrl(undo.actor!!) - val target = userQueryService.findByUrl(follow.`object`!!) - userService.unfollow(target.id, follower.id) - } - } - - else -> {} - } - TODO() - } -} diff --git a/src/main/kotlin/dev/usbharu/hideout/service/ap/APUserService.kt b/src/main/kotlin/dev/usbharu/hideout/service/ap/APUserService.kt index d0c71b66..c31803eb 100644 --- a/src/main/kotlin/dev/usbharu/hideout/service/ap/APUserService.kt +++ b/src/main/kotlin/dev/usbharu/hideout/service/ap/APUserService.kt @@ -1,6 +1,22 @@ package dev.usbharu.hideout.service.ap +import com.fasterxml.jackson.module.kotlin.readValue +import dev.usbharu.hideout.config.Config +import dev.usbharu.hideout.domain.model.ap.Image +import dev.usbharu.hideout.domain.model.ap.Key import dev.usbharu.hideout.domain.model.ap.Person +import dev.usbharu.hideout.domain.model.hideout.dto.RemoteUserCreateDto +import dev.usbharu.hideout.exception.ap.IllegalActivityPubObjectException +import dev.usbharu.hideout.plugins.getAp +import dev.usbharu.hideout.query.UserQueryService +import dev.usbharu.hideout.service.core.Transaction +import dev.usbharu.hideout.service.user.IUserService +import dev.usbharu.hideout.util.HttpUtil.Activity +import io.ktor.client.* +import io.ktor.client.request.* +import io.ktor.client.statement.* +import io.ktor.http.* +import org.koin.core.annotation.Single interface APUserService { suspend fun getPersonByName(name: String): Person @@ -14,3 +30,99 @@ interface APUserService { */ suspend fun fetchPerson(url: String, targetActor: String? = null): Person } + +@Single +class APUserServiceImpl( + private val userService: IUserService, + private val httpClient: HttpClient, + private val userQueryService: UserQueryService, + private val transaction: Transaction +) : + APUserService { + + override suspend fun getPersonByName(name: String): Person { + val userEntity = transaction.transaction { + userQueryService.findByNameAndDomain(name, Config.configData.domain) + } + // TODO: JOINで書き直し + val userUrl = "${Config.configData.url}/users/$name" + return Person( + type = emptyList(), + name = userEntity.name, + id = userUrl, + preferredUsername = name, + summary = userEntity.description, + inbox = "$userUrl/inbox", + outbox = "$userUrl/outbox", + url = userUrl, + icon = Image( + type = emptyList(), + name = "$userUrl/icon.png", + mediaType = "image/png", + url = "$userUrl/icon.png" + ), + publicKey = Key( + type = emptyList(), + name = "Public Key", + id = "$userUrl#pubkey", + owner = userUrl, + publicKeyPem = userEntity.publicKey + ) + ) + } + + override suspend fun fetchPerson(url: String, targetActor: String?): Person { + return try { + val userEntity = userQueryService.findByUrl(url) + return Person( + type = emptyList(), + name = userEntity.name, + id = url, + preferredUsername = userEntity.name, + summary = userEntity.description, + inbox = "$url/inbox", + outbox = "$url/outbox", + url = url, + icon = Image( + type = emptyList(), + name = "$url/icon.png", + mediaType = "image/png", + url = "$url/icon.png" + ), + publicKey = Key( + type = emptyList(), + name = "Public Key", + id = "$url#pubkey", + owner = url, + publicKeyPem = userEntity.publicKey + ) + ) + } catch (ignore: NoSuchElementException) { + val httpResponse = if (targetActor != null) { + httpClient.getAp(url, "$targetActor#pubkey") + } else { + httpClient.get(url) { + accept(ContentType.Application.Activity) + } + } + val person = Config.configData.objectMapper.readValue(httpResponse.bodyAsText()) + + userService.createRemoteUser( + RemoteUserCreateDto( + name = person.preferredUsername + ?: throw IllegalActivityPubObjectException("preferredUsername is null"), + domain = url.substringAfter("://").substringBefore("/"), + screenName = (person.name ?: person.preferredUsername) + ?: throw IllegalActivityPubObjectException("preferredUsername is null"), + description = person.summary.orEmpty(), + inbox = person.inbox ?: throw IllegalActivityPubObjectException("inbox is null"), + outbox = person.outbox ?: throw IllegalActivityPubObjectException("outbox is null"), + url = url, + publicKey = person.publicKey?.publicKeyPem + ?: throw IllegalActivityPubObjectException("publicKey is null"), + ) + ) + person + } + } +} diff --git a/src/main/kotlin/dev/usbharu/hideout/service/ap/APUserServiceImpl.kt b/src/main/kotlin/dev/usbharu/hideout/service/ap/APUserServiceImpl.kt deleted file mode 100644 index ec846191..00000000 --- a/src/main/kotlin/dev/usbharu/hideout/service/ap/APUserServiceImpl.kt +++ /dev/null @@ -1,115 +0,0 @@ -package dev.usbharu.hideout.service.ap - -import com.fasterxml.jackson.module.kotlin.readValue -import dev.usbharu.hideout.config.Config -import dev.usbharu.hideout.domain.model.ap.Image -import dev.usbharu.hideout.domain.model.ap.Key -import dev.usbharu.hideout.domain.model.ap.Person -import dev.usbharu.hideout.domain.model.hideout.dto.RemoteUserCreateDto -import dev.usbharu.hideout.exception.ap.IllegalActivityPubObjectException -import dev.usbharu.hideout.plugins.getAp -import dev.usbharu.hideout.query.UserQueryService -import dev.usbharu.hideout.service.core.Transaction -import dev.usbharu.hideout.service.user.IUserService -import dev.usbharu.hideout.util.HttpUtil.Activity -import io.ktor.client.* -import io.ktor.client.request.* -import io.ktor.client.statement.* -import io.ktor.http.* -import org.koin.core.annotation.Single - -@Single -class APUserServiceImpl( - private val userService: IUserService, - private val httpClient: HttpClient, - private val userQueryService: UserQueryService, - private val transaction: Transaction -) : - APUserService { - - override suspend fun getPersonByName(name: String): Person { - val userEntity = transaction.transaction { - userQueryService.findByNameAndDomain(name, Config.configData.domain) - } - // TODO: JOINで書き直し - val userUrl = "${Config.configData.url}/users/$name" - return Person( - type = emptyList(), - name = userEntity.name, - id = userUrl, - preferredUsername = name, - summary = userEntity.description, - inbox = "$userUrl/inbox", - outbox = "$userUrl/outbox", - url = userUrl, - icon = Image( - type = emptyList(), - name = "$userUrl/icon.png", - mediaType = "image/png", - url = "$userUrl/icon.png" - ), - publicKey = Key( - type = emptyList(), - name = "Public Key", - id = "$userUrl#pubkey", - owner = userUrl, - publicKeyPem = userEntity.publicKey - ) - ) - } - - override suspend fun fetchPerson(url: String, targetActor: String?): Person { - return try { - val userEntity = userQueryService.findByUrl(url) - return Person( - type = emptyList(), - name = userEntity.name, - id = url, - preferredUsername = userEntity.name, - summary = userEntity.description, - inbox = "$url/inbox", - outbox = "$url/outbox", - url = url, - icon = Image( - type = emptyList(), - name = "$url/icon.png", - mediaType = "image/png", - url = "$url/icon.png" - ), - publicKey = Key( - type = emptyList(), - name = "Public Key", - id = "$url#pubkey", - owner = url, - publicKeyPem = userEntity.publicKey - ) - ) - } catch (ignore: NoSuchElementException) { - val httpResponse = if (targetActor != null) { - httpClient.getAp(url, "$targetActor#pubkey") - } else { - httpClient.get(url) { - accept(ContentType.Application.Activity) - } - } - val person = Config.configData.objectMapper.readValue(httpResponse.bodyAsText()) - - userService.createRemoteUser( - RemoteUserCreateDto( - name = person.preferredUsername - ?: throw IllegalActivityPubObjectException("preferredUsername is null"), - domain = url.substringAfter("://").substringBefore("/"), - screenName = (person.name ?: person.preferredUsername) - ?: throw IllegalActivityPubObjectException("preferredUsername is null"), - description = person.summary.orEmpty(), - inbox = person.inbox ?: throw IllegalActivityPubObjectException("inbox is null"), - outbox = person.outbox ?: throw IllegalActivityPubObjectException("outbox is null"), - url = url, - publicKey = person.publicKey?.publicKeyPem - ?: throw IllegalActivityPubObjectException("publicKey is null"), - ) - ) - person - } - } -} diff --git a/src/main/kotlin/dev/usbharu/hideout/service/api/IPostApiService.kt b/src/main/kotlin/dev/usbharu/hideout/service/api/IPostApiService.kt index 3ce027fb..61715cc0 100644 --- a/src/main/kotlin/dev/usbharu/hideout/service/api/IPostApiService.kt +++ b/src/main/kotlin/dev/usbharu/hideout/service/api/IPostApiService.kt @@ -1,7 +1,18 @@ package dev.usbharu.hideout.service.api +import dev.usbharu.hideout.config.Config +import dev.usbharu.hideout.domain.model.hideout.dto.PostCreateDto import dev.usbharu.hideout.domain.model.hideout.dto.PostResponse import dev.usbharu.hideout.domain.model.hideout.dto.ReactionResponse +import dev.usbharu.hideout.domain.model.hideout.form.Post +import dev.usbharu.hideout.query.PostResponseQueryService +import dev.usbharu.hideout.query.ReactionQueryService +import dev.usbharu.hideout.repository.IUserRepository +import dev.usbharu.hideout.service.core.Transaction +import dev.usbharu.hideout.service.post.IPostService +import dev.usbharu.hideout.service.reaction.IReactionService +import dev.usbharu.hideout.util.AcctUtil +import org.koin.core.annotation.Single import java.time.Instant @Suppress("LongParameterList") @@ -31,3 +42,83 @@ interface IPostApiService { suspend fun appendReaction(reaction: String, userId: Long, postId: Long) suspend fun removeReaction(userId: Long, postId: Long) } + +@Single +class PostApiServiceImpl( + private val postService: IPostService, + private val userRepository: IUserRepository, + private val postResponseQueryService: PostResponseQueryService, + private val reactionQueryService: ReactionQueryService, + private val reactionService: IReactionService, + private val transaction: Transaction +) : IPostApiService { + override suspend fun createPost(postForm: Post, userId: Long): PostResponse { + return transaction.transaction { + val createdPost = postService.createLocal( + PostCreateDto( + text = postForm.text, + overview = postForm.overview, + visibility = postForm.visibility, + repostId = postForm.repostId, + repolyId = postForm.replyId, + userId = userId + ) + ) + val creator = userRepository.findById(userId) + PostResponse.from(createdPost, creator!!) + } + } + + override suspend fun getById(id: Long, userId: Long?): PostResponse = postResponseQueryService.findById(id, userId) + + override suspend fun getAll( + since: Instant?, + until: Instant?, + minId: Long?, + maxId: Long?, + limit: Int?, + userId: Long? + ): List = transaction.transaction { + postResponseQueryService.findAll( + since = since?.toEpochMilli(), + until = until?.toEpochMilli(), + minId = minId, + maxId = maxId, + limit = limit, + userId = userId + ) + } + + override suspend fun getByUser( + nameOrId: String, + since: Instant?, + until: Instant?, + minId: Long?, + maxId: Long?, + limit: Int?, + userId: Long? + ): List { + val idOrNull = nameOrId.toLongOrNull() + return if (idOrNull == null) { + val acct = AcctUtil.parse(nameOrId) + postResponseQueryService.findByUserNameAndUserDomain(acct.username, acct.domain ?: Config.configData.domain) + } else { + postResponseQueryService.findByUserId(idOrNull) + } + } + + override suspend fun getReactionByPostId(postId: Long, userId: Long?): List = + transaction.transaction { reactionQueryService.findByPostIdWithUsers(postId, userId) } + + override suspend fun appendReaction(reaction: String, userId: Long, postId: Long) { + transaction.transaction { + reactionService.sendReaction(reaction, userId, postId) + } + } + + override suspend fun removeReaction(userId: Long, postId: Long) { + transaction.transaction { + reactionService.removeReaction(userId, postId) + } + } +} diff --git a/src/main/kotlin/dev/usbharu/hideout/service/api/IUserApiService.kt b/src/main/kotlin/dev/usbharu/hideout/service/api/IUserApiService.kt index 7d902de3..26135f8a 100644 --- a/src/main/kotlin/dev/usbharu/hideout/service/api/IUserApiService.kt +++ b/src/main/kotlin/dev/usbharu/hideout/service/api/IUserApiService.kt @@ -1,7 +1,16 @@ package dev.usbharu.hideout.service.api +import dev.usbharu.hideout.config.Config import dev.usbharu.hideout.domain.model.Acct +import dev.usbharu.hideout.domain.model.hideout.dto.UserCreateDto import dev.usbharu.hideout.domain.model.hideout.dto.UserResponse +import dev.usbharu.hideout.exception.UsernameAlreadyExistException +import dev.usbharu.hideout.query.FollowerQueryService +import dev.usbharu.hideout.query.UserQueryService +import dev.usbharu.hideout.service.core.Transaction +import dev.usbharu.hideout.service.user.IUserService +import org.koin.core.annotation.Single +import kotlin.math.min interface IUserApiService { suspend fun findAll(limit: Int? = 100, offset: Long = 0): List @@ -22,3 +31,45 @@ interface IUserApiService { suspend fun createUser(username: String, password: String): UserResponse } + +@Single +class UserApiServiceImpl( + private val userQueryService: UserQueryService, + private val followerQueryService: FollowerQueryService, + private val userService: IUserService, + private val transaction: Transaction +) : IUserApiService { + override suspend fun findAll(limit: Int?, offset: Long): List = + userQueryService.findAll(min(limit ?: 100, 100), offset).map { UserResponse.from(it) } + + override suspend fun findById(id: Long): UserResponse = UserResponse.from(userQueryService.findById(id)) + + override suspend fun findByIds(ids: List): List = + userQueryService.findByIds(ids).map { UserResponse.from(it) } + + override suspend fun findByAcct(acct: Acct): UserResponse = + UserResponse.from(userQueryService.findByNameAndDomain(acct.username, acct.domain ?: Config.configData.domain)) + + override suspend fun findFollowers(userId: Long): List = + followerQueryService.findFollowersById(userId).map { UserResponse.from(it) } + + override suspend fun findFollowings(userId: Long): List = + followerQueryService.findFollowingById(userId).map { UserResponse.from(it) } + + override suspend fun findFollowersByAcct(acct: Acct): List = + followerQueryService.findFollowersByNameAndDomain(acct.username, acct.domain ?: Config.configData.domain) + .map { UserResponse.from(it) } + + override suspend fun findFollowingsByAcct(acct: Acct): List = + followerQueryService.findFollowingByNameAndDomain(acct.username, acct.domain ?: Config.configData.domain) + .map { UserResponse.from(it) } + + override suspend fun createUser(username: String, password: String): UserResponse { + return transaction.transaction { + if (userQueryService.existByNameAndDomain(username, Config.configData.domain)) { + throw UsernameAlreadyExistException() + } + UserResponse.from(userService.createLocalUser(UserCreateDto(username, username, "", password))) + } + } +} diff --git a/src/main/kotlin/dev/usbharu/hideout/service/api/PostApiServiceImpl.kt b/src/main/kotlin/dev/usbharu/hideout/service/api/PostApiServiceImpl.kt deleted file mode 100644 index ee08b7d9..00000000 --- a/src/main/kotlin/dev/usbharu/hideout/service/api/PostApiServiceImpl.kt +++ /dev/null @@ -1,96 +0,0 @@ -package dev.usbharu.hideout.service.api - -import dev.usbharu.hideout.config.Config -import dev.usbharu.hideout.domain.model.hideout.dto.PostCreateDto -import dev.usbharu.hideout.domain.model.hideout.dto.PostResponse -import dev.usbharu.hideout.domain.model.hideout.dto.ReactionResponse -import dev.usbharu.hideout.query.PostResponseQueryService -import dev.usbharu.hideout.query.ReactionQueryService -import dev.usbharu.hideout.repository.IUserRepository -import dev.usbharu.hideout.service.core.Transaction -import dev.usbharu.hideout.service.post.IPostService -import dev.usbharu.hideout.service.reaction.IReactionService -import dev.usbharu.hideout.util.AcctUtil -import org.koin.core.annotation.Single -import java.time.Instant -import dev.usbharu.hideout.domain.model.hideout.form.Post as FormPost - -@Single -class PostApiServiceImpl( - private val postService: IPostService, - private val userRepository: IUserRepository, - private val postResponseQueryService: PostResponseQueryService, - private val reactionQueryService: ReactionQueryService, - private val reactionService: IReactionService, - private val transaction: Transaction -) : IPostApiService { - override suspend fun createPost(postForm: FormPost, userId: Long): PostResponse { - return transaction.transaction { - val createdPost = postService.createLocal( - PostCreateDto( - text = postForm.text, - overview = postForm.overview, - visibility = postForm.visibility, - repostId = postForm.repostId, - repolyId = postForm.replyId, - userId = userId - ) - ) - val creator = userRepository.findById(userId) - PostResponse.from(createdPost, creator!!) - } - } - - override suspend fun getById(id: Long, userId: Long?): PostResponse = postResponseQueryService.findById(id, userId) - - override suspend fun getAll( - since: Instant?, - until: Instant?, - minId: Long?, - maxId: Long?, - limit: Int?, - userId: Long? - ): List = transaction.transaction { - postResponseQueryService.findAll( - since = since?.toEpochMilli(), - until = until?.toEpochMilli(), - minId = minId, - maxId = maxId, - limit = limit, - userId = userId - ) - } - - override suspend fun getByUser( - nameOrId: String, - since: Instant?, - until: Instant?, - minId: Long?, - maxId: Long?, - limit: Int?, - userId: Long? - ): List { - val idOrNull = nameOrId.toLongOrNull() - return if (idOrNull == null) { - val acct = AcctUtil.parse(nameOrId) - postResponseQueryService.findByUserNameAndUserDomain(acct.username, acct.domain ?: Config.configData.domain) - } else { - postResponseQueryService.findByUserId(idOrNull) - } - } - - override suspend fun getReactionByPostId(postId: Long, userId: Long?): List = - transaction.transaction { reactionQueryService.findByPostIdWithUsers(postId, userId) } - - override suspend fun appendReaction(reaction: String, userId: Long, postId: Long) { - transaction.transaction { - reactionService.sendReaction(reaction, userId, postId) - } - } - - override suspend fun removeReaction(userId: Long, postId: Long) { - transaction.transaction { - reactionService.removeReaction(userId, postId) - } - } -} diff --git a/src/main/kotlin/dev/usbharu/hideout/service/api/UserApiServiceImpl.kt b/src/main/kotlin/dev/usbharu/hideout/service/api/UserApiServiceImpl.kt deleted file mode 100644 index 0cd675c2..00000000 --- a/src/main/kotlin/dev/usbharu/hideout/service/api/UserApiServiceImpl.kt +++ /dev/null @@ -1,55 +0,0 @@ -package dev.usbharu.hideout.service.api - -import dev.usbharu.hideout.config.Config -import dev.usbharu.hideout.domain.model.Acct -import dev.usbharu.hideout.domain.model.hideout.dto.UserCreateDto -import dev.usbharu.hideout.domain.model.hideout.dto.UserResponse -import dev.usbharu.hideout.exception.UsernameAlreadyExistException -import dev.usbharu.hideout.query.FollowerQueryService -import dev.usbharu.hideout.query.UserQueryService -import dev.usbharu.hideout.service.core.Transaction -import dev.usbharu.hideout.service.user.IUserService -import org.koin.core.annotation.Single -import kotlin.math.min - -@Single -class UserApiServiceImpl( - private val userQueryService: UserQueryService, - private val followerQueryService: FollowerQueryService, - private val userService: IUserService, - private val transaction: Transaction -) : IUserApiService { - override suspend fun findAll(limit: Int?, offset: Long): List = - userQueryService.findAll(min(limit ?: 100, 100), offset).map { UserResponse.from(it) } - - override suspend fun findById(id: Long): UserResponse = UserResponse.from(userQueryService.findById(id)) - - override suspend fun findByIds(ids: List): List = - userQueryService.findByIds(ids).map { UserResponse.from(it) } - - override suspend fun findByAcct(acct: Acct): UserResponse = - UserResponse.from(userQueryService.findByNameAndDomain(acct.username, acct.domain ?: Config.configData.domain)) - - override suspend fun findFollowers(userId: Long): List = - followerQueryService.findFollowersById(userId).map { UserResponse.from(it) } - - override suspend fun findFollowings(userId: Long): List = - followerQueryService.findFollowingById(userId).map { UserResponse.from(it) } - - override suspend fun findFollowersByAcct(acct: Acct): List = - followerQueryService.findFollowersByNameAndDomain(acct.username, acct.domain ?: Config.configData.domain) - .map { UserResponse.from(it) } - - override suspend fun findFollowingsByAcct(acct: Acct): List = - followerQueryService.findFollowingByNameAndDomain(acct.username, acct.domain ?: Config.configData.domain) - .map { UserResponse.from(it) } - - override suspend fun createUser(username: String, password: String): UserResponse { - return transaction.transaction { - if (userQueryService.existByNameAndDomain(username, Config.configData.domain)) { - throw UsernameAlreadyExistException() - } - UserResponse.from(userService.createLocalUser(UserCreateDto(username, username, "", password))) - } - } -} diff --git a/src/main/kotlin/dev/usbharu/hideout/service/api/UserAuthApiService.kt b/src/main/kotlin/dev/usbharu/hideout/service/api/UserAuthApiService.kt index 0c8d35f5..d56bbc4f 100644 --- a/src/main/kotlin/dev/usbharu/hideout/service/api/UserAuthApiService.kt +++ b/src/main/kotlin/dev/usbharu/hideout/service/api/UserAuthApiService.kt @@ -1,9 +1,40 @@ package dev.usbharu.hideout.service.api +import dev.usbharu.hideout.config.Config import dev.usbharu.hideout.domain.model.hideout.dto.JwtToken import dev.usbharu.hideout.domain.model.hideout.form.RefreshToken +import dev.usbharu.hideout.exception.InvalidUsernameOrPasswordException +import dev.usbharu.hideout.query.UserQueryService +import dev.usbharu.hideout.service.auth.IJwtService +import dev.usbharu.hideout.service.core.Transaction +import dev.usbharu.hideout.service.user.UserAuthService +import org.koin.core.annotation.Single interface UserAuthApiService { suspend fun login(username: String, password: String): JwtToken suspend fun refreshToken(refreshToken: RefreshToken): JwtToken } + +@Single +class UserAuthApiServiceImpl( + private val userAuthService: UserAuthService, + private val userQueryService: UserQueryService, + private val jwtService: IJwtService, + private val transaction: Transaction +) : UserAuthApiService { + override suspend fun login(username: String, password: String): JwtToken { + return transaction.transaction { + if (userAuthService.verifyAccount(username, password).not()) { + throw InvalidUsernameOrPasswordException() + } + val user = userQueryService.findByNameAndDomain(username, Config.configData.domain) + jwtService.createToken(user) + } + } + + override suspend fun refreshToken(refreshToken: RefreshToken): JwtToken { + return transaction.transaction { + jwtService.refreshToken(refreshToken) + } + } +} diff --git a/src/main/kotlin/dev/usbharu/hideout/service/api/UserAuthApiServiceImpl.kt b/src/main/kotlin/dev/usbharu/hideout/service/api/UserAuthApiServiceImpl.kt deleted file mode 100644 index 6896419b..00000000 --- a/src/main/kotlin/dev/usbharu/hideout/service/api/UserAuthApiServiceImpl.kt +++ /dev/null @@ -1,35 +0,0 @@ -package dev.usbharu.hideout.service.api - -import dev.usbharu.hideout.config.Config -import dev.usbharu.hideout.domain.model.hideout.dto.JwtToken -import dev.usbharu.hideout.domain.model.hideout.form.RefreshToken -import dev.usbharu.hideout.exception.InvalidUsernameOrPasswordException -import dev.usbharu.hideout.query.UserQueryService -import dev.usbharu.hideout.service.auth.IJwtService -import dev.usbharu.hideout.service.core.Transaction -import dev.usbharu.hideout.service.user.UserAuthService -import org.koin.core.annotation.Single - -@Single -class UserAuthApiServiceImpl( - private val userAuthService: UserAuthService, - private val userQueryService: UserQueryService, - private val jwtService: IJwtService, - private val transaction: Transaction -) : UserAuthApiService { - override suspend fun login(username: String, password: String): JwtToken { - return transaction.transaction { - if (userAuthService.verifyAccount(username, password).not()) { - throw InvalidUsernameOrPasswordException() - } - val user = userQueryService.findByNameAndDomain(username, Config.configData.domain) - jwtService.createToken(user) - } - } - - override suspend fun refreshToken(refreshToken: RefreshToken): JwtToken { - return transaction.transaction { - jwtService.refreshToken(refreshToken) - } - } -} diff --git a/src/main/kotlin/dev/usbharu/hideout/service/api/WebFingerApiService.kt b/src/main/kotlin/dev/usbharu/hideout/service/api/WebFingerApiService.kt index cdda80b7..5311723c 100644 --- a/src/main/kotlin/dev/usbharu/hideout/service/api/WebFingerApiService.kt +++ b/src/main/kotlin/dev/usbharu/hideout/service/api/WebFingerApiService.kt @@ -1,7 +1,20 @@ package dev.usbharu.hideout.service.api import dev.usbharu.hideout.domain.model.hideout.entity.User +import dev.usbharu.hideout.query.UserQueryService +import dev.usbharu.hideout.service.core.Transaction +import org.koin.core.annotation.Single interface WebFingerApiService { suspend fun findByNameAndDomain(name: String, domain: String): User } + +@Single +class WebFingerApiServiceImpl(private val transaction: Transaction, private val userQueryService: UserQueryService) : + WebFingerApiService { + override suspend fun findByNameAndDomain(name: String, domain: String): User { + return transaction.transaction { + userQueryService.findByNameAndDomain(name, domain) + } + } +} diff --git a/src/main/kotlin/dev/usbharu/hideout/service/api/WebFingerApiServiceImpl.kt b/src/main/kotlin/dev/usbharu/hideout/service/api/WebFingerApiServiceImpl.kt deleted file mode 100644 index 66934625..00000000 --- a/src/main/kotlin/dev/usbharu/hideout/service/api/WebFingerApiServiceImpl.kt +++ /dev/null @@ -1,16 +0,0 @@ -package dev.usbharu.hideout.service.api - -import dev.usbharu.hideout.domain.model.hideout.entity.User -import dev.usbharu.hideout.query.UserQueryService -import dev.usbharu.hideout.service.core.Transaction -import org.koin.core.annotation.Single - -@Single -class WebFingerApiServiceImpl(private val transaction: Transaction, private val userQueryService: UserQueryService) : - WebFingerApiService { - override suspend fun findByNameAndDomain(name: String, domain: String): User { - return transaction.transaction { - userQueryService.findByNameAndDomain(name, domain) - } - } -} diff --git a/src/main/kotlin/dev/usbharu/hideout/service/auth/HttpSignatureVerifyService.kt b/src/main/kotlin/dev/usbharu/hideout/service/auth/HttpSignatureVerifyService.kt index ad326e3b..eb30c903 100644 --- a/src/main/kotlin/dev/usbharu/hideout/service/auth/HttpSignatureVerifyService.kt +++ b/src/main/kotlin/dev/usbharu/hideout/service/auth/HttpSignatureVerifyService.kt @@ -1,7 +1,33 @@ package dev.usbharu.hideout.service.auth +import dev.usbharu.hideout.plugins.KtorKeyMap +import dev.usbharu.hideout.query.UserQueryService +import dev.usbharu.hideout.service.core.Transaction import io.ktor.http.* +import org.koin.core.annotation.Single +import tech.barbero.http.message.signing.SignatureHeaderVerifier interface HttpSignatureVerifyService { fun verify(headers: Headers): Boolean } + +@Single +class HttpSignatureVerifyServiceImpl( + private val userQueryService: UserQueryService, + private val transaction: Transaction +) : HttpSignatureVerifyService { + override fun verify(headers: Headers): Boolean { + val build = SignatureHeaderVerifier.builder().keyMap(KtorKeyMap(userQueryService, transaction)).build() + return true +// build.verify(object : HttpMessage { +// override fun headerValues(name: String?): MutableList { +// return name?.let { headers.getAll(it) }?.toMutableList() ?: mutableListOf() +// } +// +// override fun addHeader(name: String?, value: String?) { +// TODO() +// } +// +// }) + } +} diff --git a/src/main/kotlin/dev/usbharu/hideout/service/auth/HttpSignatureVerifyServiceImpl.kt b/src/main/kotlin/dev/usbharu/hideout/service/auth/HttpSignatureVerifyServiceImpl.kt deleted file mode 100644 index e9282d34..00000000 --- a/src/main/kotlin/dev/usbharu/hideout/service/auth/HttpSignatureVerifyServiceImpl.kt +++ /dev/null @@ -1,29 +0,0 @@ -package dev.usbharu.hideout.service.auth - -import dev.usbharu.hideout.plugins.KtorKeyMap -import dev.usbharu.hideout.query.UserQueryService -import dev.usbharu.hideout.service.core.Transaction -import io.ktor.http.* -import org.koin.core.annotation.Single -import tech.barbero.http.message.signing.SignatureHeaderVerifier - -@Single -class HttpSignatureVerifyServiceImpl( - private val userQueryService: UserQueryService, - private val transaction: Transaction -) : HttpSignatureVerifyService { - override fun verify(headers: Headers): Boolean { - val build = SignatureHeaderVerifier.builder().keyMap(KtorKeyMap(userQueryService, transaction)).build() - return true -// build.verify(object : HttpMessage { -// override fun headerValues(name: String?): MutableList { -// return name?.let { headers.getAll(it) }?.toMutableList() ?: mutableListOf() -// } -// -// override fun addHeader(name: String?, value: String?) { -// TODO() -// } -// -// }) - } -} diff --git a/src/main/kotlin/dev/usbharu/hideout/service/auth/IJwtService.kt b/src/main/kotlin/dev/usbharu/hideout/service/auth/IJwtService.kt index e1976818..2f74c2fa 100644 --- a/src/main/kotlin/dev/usbharu/hideout/service/auth/IJwtService.kt +++ b/src/main/kotlin/dev/usbharu/hideout/service/auth/IJwtService.kt @@ -1,8 +1,23 @@ package dev.usbharu.hideout.service.auth +import com.auth0.jwt.JWT +import com.auth0.jwt.algorithms.Algorithm +import dev.usbharu.hideout.config.Config import dev.usbharu.hideout.domain.model.hideout.dto.JwtToken +import dev.usbharu.hideout.domain.model.hideout.entity.JwtRefreshToken import dev.usbharu.hideout.domain.model.hideout.entity.User import dev.usbharu.hideout.domain.model.hideout.form.RefreshToken +import dev.usbharu.hideout.exception.InvalidRefreshTokenException +import dev.usbharu.hideout.query.JwtRefreshTokenQueryService +import dev.usbharu.hideout.query.UserQueryService +import dev.usbharu.hideout.repository.IJwtRefreshTokenRepository +import dev.usbharu.hideout.service.core.IMetaService +import dev.usbharu.hideout.util.RsaUtil +import kotlinx.coroutines.runBlocking +import org.koin.core.annotation.Single +import java.time.Instant +import java.time.temporal.ChronoUnit +import java.util.* interface IJwtService { suspend fun createToken(user: User): JwtToken @@ -12,3 +27,78 @@ interface IJwtService { suspend fun revokeToken(user: User) suspend fun revokeAll() } + +@Suppress("InjectDispatcher") +@Single +class JwtServiceImpl( + private val metaService: IMetaService, + private val refreshTokenRepository: IJwtRefreshTokenRepository, + private val userQueryService: UserQueryService, + private val refreshTokenQueryService: JwtRefreshTokenQueryService +) : IJwtService { + + private val privateKey = runBlocking { + RsaUtil.decodeRsaPrivateKey(metaService.getJwtMeta().privateKey) + } + + private val publicKey = runBlocking { + RsaUtil.decodeRsaPublicKey(metaService.getJwtMeta().publicKey) + } + + private val keyId = runBlocking { metaService.getJwtMeta().kid } + + @Suppress("MagicNumber") + override suspend fun createToken(user: User): JwtToken { + val now = Instant.now() + val token = JWT.create() + .withAudience("${Config.configData.url}/users/${user.name}") + .withIssuer(Config.configData.url) + .withKeyId(keyId.toString()) + .withClaim("uid", user.id) + .withExpiresAt(now.plus(30, ChronoUnit.MINUTES)) + .sign(Algorithm.RSA256(publicKey, privateKey)) + + val jwtRefreshToken = JwtRefreshToken( + id = refreshTokenRepository.generateId(), + userId = user.id, + refreshToken = UUID.randomUUID().toString(), + createdAt = now, + expiresAt = now.plus(14, ChronoUnit.DAYS) + ) + refreshTokenRepository.save(jwtRefreshToken) + return JwtToken(token, jwtRefreshToken.refreshToken) + } + + override suspend fun refreshToken(refreshToken: RefreshToken): JwtToken { + val token = try { + refreshTokenQueryService.findByToken(refreshToken.refreshToken) + } catch (_: NoSuchElementException) { + throw InvalidRefreshTokenException("Invalid Refresh Token") + } + + val user = userQueryService.findById(token.userId) + + val now = Instant.now() + if (token.createdAt.isAfter(now)) { + throw InvalidRefreshTokenException("Invalid Refresh Token") + } + + if (token.expiresAt.isBefore(now)) { + throw InvalidRefreshTokenException("Refresh Token Expired") + } + + return createToken(user) + } + + override suspend fun revokeToken(refreshToken: RefreshToken) { + refreshTokenQueryService.deleteByToken(refreshToken.refreshToken) + } + + override suspend fun revokeToken(user: User) { + refreshTokenQueryService.deleteByUserId(user.id) + } + + override suspend fun revokeAll() { + refreshTokenQueryService.deleteAll() + } +} diff --git a/src/main/kotlin/dev/usbharu/hideout/service/auth/JwtServiceImpl.kt b/src/main/kotlin/dev/usbharu/hideout/service/auth/JwtServiceImpl.kt deleted file mode 100644 index a04dc2f9..00000000 --- a/src/main/kotlin/dev/usbharu/hideout/service/auth/JwtServiceImpl.kt +++ /dev/null @@ -1,95 +0,0 @@ -package dev.usbharu.hideout.service.auth - -import com.auth0.jwt.JWT -import com.auth0.jwt.algorithms.Algorithm -import dev.usbharu.hideout.config.Config -import dev.usbharu.hideout.domain.model.hideout.dto.JwtToken -import dev.usbharu.hideout.domain.model.hideout.entity.JwtRefreshToken -import dev.usbharu.hideout.domain.model.hideout.entity.User -import dev.usbharu.hideout.domain.model.hideout.form.RefreshToken -import dev.usbharu.hideout.exception.InvalidRefreshTokenException -import dev.usbharu.hideout.query.JwtRefreshTokenQueryService -import dev.usbharu.hideout.query.UserQueryService -import dev.usbharu.hideout.repository.IJwtRefreshTokenRepository -import dev.usbharu.hideout.service.core.IMetaService -import dev.usbharu.hideout.util.RsaUtil -import kotlinx.coroutines.runBlocking -import org.koin.core.annotation.Single -import java.time.Instant -import java.time.temporal.ChronoUnit -import java.util.* - -@Suppress("InjectDispatcher") -@Single -class JwtServiceImpl( - private val metaService: IMetaService, - private val refreshTokenRepository: IJwtRefreshTokenRepository, - private val userQueryService: UserQueryService, - private val refreshTokenQueryService: JwtRefreshTokenQueryService -) : IJwtService { - - private val privateKey = runBlocking { - RsaUtil.decodeRsaPrivateKey(metaService.getJwtMeta().privateKey) - } - - private val publicKey = runBlocking { - RsaUtil.decodeRsaPublicKey(metaService.getJwtMeta().publicKey) - } - - private val keyId = runBlocking { metaService.getJwtMeta().kid } - - @Suppress("MagicNumber") - override suspend fun createToken(user: User): JwtToken { - val now = Instant.now() - val token = JWT.create() - .withAudience("${Config.configData.url}/users/${user.name}") - .withIssuer(Config.configData.url) - .withKeyId(keyId.toString()) - .withClaim("uid", user.id) - .withExpiresAt(now.plus(30, ChronoUnit.MINUTES)) - .sign(Algorithm.RSA256(publicKey, privateKey)) - - val jwtRefreshToken = JwtRefreshToken( - id = refreshTokenRepository.generateId(), - userId = user.id, - refreshToken = UUID.randomUUID().toString(), - createdAt = now, - expiresAt = now.plus(14, ChronoUnit.DAYS) - ) - refreshTokenRepository.save(jwtRefreshToken) - return JwtToken(token, jwtRefreshToken.refreshToken) - } - - override suspend fun refreshToken(refreshToken: RefreshToken): JwtToken { - val token = try { - refreshTokenQueryService.findByToken(refreshToken.refreshToken) - } catch (_: NoSuchElementException) { - throw InvalidRefreshTokenException("Invalid Refresh Token") - } - - val user = userQueryService.findById(token.userId) - - val now = Instant.now() - if (token.createdAt.isAfter(now)) { - throw InvalidRefreshTokenException("Invalid Refresh Token") - } - - if (token.expiresAt.isBefore(now)) { - throw InvalidRefreshTokenException("Refresh Token Expired") - } - - return createToken(user) - } - - override suspend fun revokeToken(refreshToken: RefreshToken) { - refreshTokenQueryService.deleteByToken(refreshToken.refreshToken) - } - - override suspend fun revokeToken(user: User) { - refreshTokenQueryService.deleteByUserId(user.id) - } - - override suspend fun revokeAll() { - refreshTokenQueryService.deleteAll() - } -}