mirror of https://github.com/usbharu/Hideout.git
refactor: interfaceとデフォルト実装を同じファイルに
This commit is contained in:
parent
5f85a25daf
commit
60f0e1a4bf
|
@ -1,8 +1,36 @@
|
||||||
package dev.usbharu.hideout.service.ap
|
package dev.usbharu.hideout.service.ap
|
||||||
|
|
||||||
import dev.usbharu.hideout.domain.model.ActivityPubResponse
|
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.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 {
|
interface APAcceptService {
|
||||||
suspend fun receiveAccept(accept: Accept): ActivityPubResponse
|
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")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -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")
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,8 +1,33 @@
|
||||||
package dev.usbharu.hideout.service.ap
|
package dev.usbharu.hideout.service.ap
|
||||||
|
|
||||||
import dev.usbharu.hideout.domain.model.ActivityPubResponse
|
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.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 {
|
interface APCreateService {
|
||||||
suspend fun receiveCreate(create: Create): ActivityPubResponse
|
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")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -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")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,8 +1,46 @@
|
||||||
package dev.usbharu.hideout.service.ap
|
package dev.usbharu.hideout.service.ap
|
||||||
|
|
||||||
import dev.usbharu.hideout.domain.model.ActivityPubResponse
|
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.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 {
|
interface APLikeService {
|
||||||
suspend fun receiveLike(like: Like): ActivityPubResponse
|
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, "")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -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, "")
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,9 +1,26 @@
|
||||||
package dev.usbharu.hideout.service.ap
|
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.ap.Note
|
||||||
import dev.usbharu.hideout.domain.model.hideout.entity.Post
|
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.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 kjob.core.job.JobProps
|
||||||
|
import org.koin.core.annotation.Single
|
||||||
|
import org.slf4j.LoggerFactory
|
||||||
|
import java.time.Instant
|
||||||
|
|
||||||
interface APNoteService {
|
interface APNoteService {
|
||||||
|
|
||||||
|
@ -13,3 +30,151 @@ interface APNoteService {
|
||||||
suspend fun fetchNote(url: String, targetActor: String? = null): Note
|
suspend fun fetchNote(url: String, targetActor: String? = null): Note
|
||||||
suspend fun fetchNote(note: Note, 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"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -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"
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,9 +1,22 @@
|
||||||
package dev.usbharu.hideout.service.ap
|
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.hideout.entity.Reaction
|
||||||
import dev.usbharu.hideout.domain.model.job.DeliverReactionJob
|
import dev.usbharu.hideout.domain.model.job.DeliverReactionJob
|
||||||
import dev.usbharu.hideout.domain.model.job.DeliverRemoveReactionJob
|
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 kjob.core.job.JobProps
|
||||||
|
import org.koin.core.annotation.Single
|
||||||
|
import java.time.Instant
|
||||||
|
|
||||||
interface APReactionService {
|
interface APReactionService {
|
||||||
suspend fun reaction(like: Reaction)
|
suspend fun reaction(like: Reaction)
|
||||||
|
@ -11,3 +24,80 @@ interface APReactionService {
|
||||||
suspend fun reactionJob(props: JobProps<DeliverReactionJob>)
|
suspend fun reactionJob(props: JobProps<DeliverReactionJob>)
|
||||||
suspend fun removeReactionJob(props: JobProps<DeliverRemoveReactionJob>)
|
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()
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -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()
|
|
||||||
)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,11 +1,67 @@
|
||||||
package dev.usbharu.hideout.service.ap
|
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.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.ap.Follow
|
||||||
import dev.usbharu.hideout.domain.model.job.ReceiveFollowJob
|
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 kjob.core.job.JobProps
|
||||||
|
import org.koin.core.annotation.Single
|
||||||
|
|
||||||
interface APReceiveFollowService {
|
interface APReceiveFollowService {
|
||||||
suspend fun receiveFollow(follow: Follow): ActivityPubResponse
|
suspend fun receiveFollow(follow: Follow): ActivityPubResponse
|
||||||
suspend fun receiveFollowJob(props: JobProps<ReceiveFollowJob>)
|
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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -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)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,7 +1,27 @@
|
||||||
package dev.usbharu.hideout.service.ap
|
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.domain.model.hideout.dto.SendFollowDto
|
||||||
|
import dev.usbharu.hideout.plugins.postAp
|
||||||
|
import io.ktor.client.*
|
||||||
|
import org.koin.core.annotation.Single
|
||||||
|
|
||||||
interface APSendFollowService {
|
interface APSendFollowService {
|
||||||
suspend fun sendFollow(sendFollowDto: SendFollowDto)
|
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
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -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
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,8 +1,17 @@
|
||||||
package dev.usbharu.hideout.service.ap
|
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.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.dsl.JobContextWithProps
|
||||||
|
import kjob.core.job.JobProps
|
||||||
|
import org.koin.core.annotation.Single
|
||||||
|
import org.slf4j.Logger
|
||||||
|
import org.slf4j.LoggerFactory
|
||||||
|
|
||||||
interface APService {
|
interface APService {
|
||||||
fun parseActivity(json: String): ActivityType
|
fun parseActivity(json: String): ActivityType
|
||||||
|
@ -162,3 +171,68 @@ enum class ExtendedActivityVocabulary {
|
||||||
enum class ExtendedVocabulary {
|
enum class ExtendedVocabulary {
|
||||||
Emoji
|
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>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -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>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,8 +1,54 @@
|
||||||
package dev.usbharu.hideout.service.ap
|
package dev.usbharu.hideout.service.ap
|
||||||
|
|
||||||
import dev.usbharu.hideout.domain.model.ActivityPubResponse
|
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.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 {
|
interface APUndoService {
|
||||||
suspend fun receiveUndo(undo: Undo): ActivityPubResponse
|
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()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -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()
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,6 +1,22 @@
|
||||||
package dev.usbharu.hideout.service.ap
|
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.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 {
|
interface APUserService {
|
||||||
suspend fun getPersonByName(name: String): Person
|
suspend fun getPersonByName(name: String): Person
|
||||||
|
@ -14,3 +30,99 @@ interface APUserService {
|
||||||
*/
|
*/
|
||||||
suspend fun fetchPerson(url: String, targetActor: String? = null): Person
|
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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -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
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,7 +1,18 @@
|
||||||
package dev.usbharu.hideout.service.api
|
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.PostResponse
|
||||||
import dev.usbharu.hideout.domain.model.hideout.dto.ReactionResponse
|
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
|
import java.time.Instant
|
||||||
|
|
||||||
@Suppress("LongParameterList")
|
@Suppress("LongParameterList")
|
||||||
|
@ -31,3 +42,83 @@ interface IPostApiService {
|
||||||
suspend fun appendReaction(reaction: String, userId: Long, postId: Long)
|
suspend fun appendReaction(reaction: String, userId: Long, postId: Long)
|
||||||
suspend fun removeReaction(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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -1,7 +1,16 @@
|
||||||
package dev.usbharu.hideout.service.api
|
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.Acct
|
||||||
|
import dev.usbharu.hideout.domain.model.hideout.dto.UserCreateDto
|
||||||
import dev.usbharu.hideout.domain.model.hideout.dto.UserResponse
|
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 {
|
interface IUserApiService {
|
||||||
suspend fun findAll(limit: Int? = 100, offset: Long = 0): List<UserResponse>
|
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
|
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)))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -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)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -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)))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,9 +1,40 @@
|
||||||
package dev.usbharu.hideout.service.api
|
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.dto.JwtToken
|
||||||
import dev.usbharu.hideout.domain.model.hideout.form.RefreshToken
|
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 {
|
interface UserAuthApiService {
|
||||||
suspend fun login(username: String, password: String): JwtToken
|
suspend fun login(username: String, password: String): JwtToken
|
||||||
suspend fun refreshToken(refreshToken: RefreshToken): 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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -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)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,7 +1,20 @@
|
||||||
package dev.usbharu.hideout.service.api
|
package dev.usbharu.hideout.service.api
|
||||||
|
|
||||||
import dev.usbharu.hideout.domain.model.hideout.entity.User
|
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 {
|
interface WebFingerApiService {
|
||||||
suspend fun findByNameAndDomain(name: String, domain: String): User
|
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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -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)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,7 +1,33 @@
|
||||||
package dev.usbharu.hideout.service.auth
|
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 io.ktor.http.*
|
||||||
|
import org.koin.core.annotation.Single
|
||||||
|
import tech.barbero.http.message.signing.SignatureHeaderVerifier
|
||||||
|
|
||||||
interface HttpSignatureVerifyService {
|
interface HttpSignatureVerifyService {
|
||||||
fun verify(headers: Headers): Boolean
|
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()
|
||||||
|
// }
|
||||||
|
//
|
||||||
|
// })
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -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()
|
|
||||||
// }
|
|
||||||
//
|
|
||||||
// })
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,8 +1,23 @@
|
||||||
package dev.usbharu.hideout.service.auth
|
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.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.entity.User
|
||||||
import dev.usbharu.hideout.domain.model.hideout.form.RefreshToken
|
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 {
|
interface IJwtService {
|
||||||
suspend fun createToken(user: User): JwtToken
|
suspend fun createToken(user: User): JwtToken
|
||||||
|
@ -12,3 +27,78 @@ interface IJwtService {
|
||||||
suspend fun revokeToken(user: User)
|
suspend fun revokeToken(user: User)
|
||||||
suspend fun revokeAll()
|
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()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -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()
|
|
||||||
}
|
|
||||||
}
|
|
Loading…
Reference in New Issue