refactor: interfaceとデフォルト実装を同じファイルに

This commit is contained in:
usbharu 2023-08-11 16:38:56 +09:00
parent 5f85a25daf
commit 60f0e1a4bf
32 changed files with 957 additions and 1026 deletions

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -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<DeliverPostJob>) {
val actor = props[DeliverPostJob.actor]
val postEntity = Config.configData.objectMapper.readValue<Post>(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<Note>(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"
}
}

View File

@ -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<DeliverPostJob>) {
val actor = props[DeliverPostJob.actor]
val postEntity = Config.configData.objectMapper.readValue<Post>(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<Note>(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"
}
}

View File

@ -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<DeliverReactionJob>)
suspend fun removeReactionJob(props: JobProps<DeliverRemoveReactionJob>)
}
@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<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]
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<DeliverRemoveReactionJob>) {
val inbox = props[DeliverRemoveReactionJob.inbox]
val actor = props[DeliverRemoveReactionJob.actor]
val like = Config.configData.objectMapper.readValue<Like>(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()
)
)
}
}

View File

@ -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<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]
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<DeliverRemoveReactionJob>) {
val inbox = props[DeliverRemoveReactionJob.inbox]
val actor = props[DeliverRemoveReactionJob.actor]
val like = Config.configData.objectMapper.readValue<Like>(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()
)
)
}
}

View File

@ -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<ReceiveFollowJob>)
}
@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<ReceiveFollowJob>) {
transaction.transaction {
val actor = props[ReceiveFollowJob.actor]
val targetActor = props[ReceiveFollowJob.targetActor]
val person = apUserService.fetchPerson(actor, targetActor)
val follow = Config.configData.objectMapper.readValue<Follow>(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)
}
}
}

View File

@ -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<ReceiveFollowJob>) {
transaction.transaction {
val actor = props[ReceiveFollowJob.actor]
val targetActor = props[ReceiveFollowJob.targetActor]
val person = apUserService.fetchPerson(actor, targetActor)
val follow = Config.configData.objectMapper.readValue<Follow>(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)
}
}
}

View File

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

View File

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

View File

@ -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 <T : HideoutJob> processActivity(job: JobContextWithProps<T>, hideoutJob: HideoutJob) {
logger.debug("processActivity: ${hideoutJob.name}")
when (hideoutJob) {
ReceiveFollowJob -> apReceiveFollowService.receiveFollowJob(
job.props as JobProps<ReceiveFollowJob>
)
DeliverPostJob -> apNoteService.createNoteJob(job.props as JobProps<DeliverPostJob>)
DeliverReactionJob -> apReactionService.reactionJob(job.props as JobProps<DeliverReactionJob>)
DeliverRemoveReactionJob -> apReactionService.removeReactionJob(
job.props as JobProps<DeliverRemoveReactionJob>
)
}
}
}

View File

@ -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 <T : HideoutJob> processActivity(job: JobContextWithProps<T>, hideoutJob: HideoutJob) {
logger.debug("processActivity: ${hideoutJob.name}")
when (hideoutJob) {
ReceiveFollowJob -> apReceiveFollowService.receiveFollowJob(
job.props as JobProps<ReceiveFollowJob>
)
DeliverPostJob -> apNoteService.createNoteJob(job.props as JobProps<DeliverPostJob>)
DeliverReactionJob -> apReactionService.reactionJob(job.props as JobProps<DeliverReactionJob>)
DeliverRemoveReactionJob -> apReactionService.removeReactionJob(
job.props as JobProps<DeliverRemoveReactionJob>
)
}
}
}

View File

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

View File

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

View File

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

View File

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

View File

@ -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<PostResponse> = 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<PostResponse> {
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<ReactionResponse> =
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)
}
}
}

View File

@ -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<UserResponse>
@ -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<UserResponse> =
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<Long>): List<UserResponse> =
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<UserResponse> =
followerQueryService.findFollowersById(userId).map { UserResponse.from(it) }
override suspend fun findFollowings(userId: Long): List<UserResponse> =
followerQueryService.findFollowingById(userId).map { UserResponse.from(it) }
override suspend fun findFollowersByAcct(acct: Acct): List<UserResponse> =
followerQueryService.findFollowersByNameAndDomain(acct.username, acct.domain ?: Config.configData.domain)
.map { UserResponse.from(it) }
override suspend fun findFollowingsByAcct(acct: Acct): List<UserResponse> =
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)))
}
}
}

View File

@ -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<PostResponse> = 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<PostResponse> {
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<ReactionResponse> =
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)
}
}
}

View File

@ -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<UserResponse> =
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<Long>): List<UserResponse> =
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<UserResponse> =
followerQueryService.findFollowersById(userId).map { UserResponse.from(it) }
override suspend fun findFollowings(userId: Long): List<UserResponse> =
followerQueryService.findFollowingById(userId).map { UserResponse.from(it) }
override suspend fun findFollowersByAcct(acct: Acct): List<UserResponse> =
followerQueryService.findFollowersByNameAndDomain(acct.username, acct.domain ?: Config.configData.domain)
.map { UserResponse.from(it) }
override suspend fun findFollowingsByAcct(acct: Acct): List<UserResponse> =
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)))
}
}
}

View File

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

View File

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

View File

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

View File

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

View File

@ -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<String> {
// return name?.let { headers.getAll(it) }?.toMutableList() ?: mutableListOf()
// }
//
// override fun addHeader(name: String?, value: String?) {
// TODO()
// }
//
// })
}
}

View File

@ -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<String> {
// return name?.let { headers.getAll(it) }?.toMutableList() ?: mutableListOf()
// }
//
// override fun addHeader(name: String?, value: String?) {
// TODO()
// }
//
// })
}
}

View File

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

View File

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