Merge pull request #19 from usbharu/feature/refactor-service

Feature/refactor service
This commit is contained in:
usbharu 2023-06-04 01:03:55 +09:00 committed by GitHub
commit 3fb5c0e19a
15 changed files with 523 additions and 672 deletions

View File

@ -14,6 +14,7 @@ import dev.usbharu.hideout.repository.IUserRepository
import dev.usbharu.hideout.routing.register import dev.usbharu.hideout.routing.register
import dev.usbharu.hideout.service.activitypub.ActivityPubService import dev.usbharu.hideout.service.activitypub.ActivityPubService
import dev.usbharu.hideout.service.activitypub.ActivityPubUserService import dev.usbharu.hideout.service.activitypub.ActivityPubUserService
import dev.usbharu.hideout.service.api.IPostApiService
import dev.usbharu.hideout.service.api.IUserApiService import dev.usbharu.hideout.service.api.IUserApiService
import dev.usbharu.hideout.service.auth.HttpSignatureVerifyService import dev.usbharu.hideout.service.auth.HttpSignatureVerifyService
import dev.usbharu.hideout.service.auth.IJwtService import dev.usbharu.hideout.service.auth.IJwtService
@ -23,7 +24,6 @@ import dev.usbharu.hideout.service.core.IdGenerateService
import dev.usbharu.hideout.service.core.TwitterSnowflakeIdGenerateService import dev.usbharu.hideout.service.core.TwitterSnowflakeIdGenerateService
import dev.usbharu.hideout.service.job.JobQueueParentService import dev.usbharu.hideout.service.job.JobQueueParentService
import dev.usbharu.hideout.service.job.KJobJobQueueParentService import dev.usbharu.hideout.service.job.KJobJobQueueParentService
import dev.usbharu.hideout.service.post.IPostService
import dev.usbharu.hideout.service.user.IUserAuthService import dev.usbharu.hideout.service.user.IUserAuthService
import dev.usbharu.hideout.service.user.IUserService import dev.usbharu.hideout.service.user.IUserService
import dev.usbharu.kjob.exposed.ExposedKJob import dev.usbharu.kjob.exposed.ExposedKJob
@ -107,12 +107,12 @@ fun Application.parent() {
inject<JwkProvider>().value, inject<JwkProvider>().value,
) )
configureRouting( configureRouting(
httpSignatureVerifyService = inject<HttpSignatureVerifyService>().value, httpSignatureVerifyService = inject<HttpSignatureVerifyService>().value,
activityPubService = inject<ActivityPubService>().value, activityPubService = inject<ActivityPubService>().value,
userService = inject<IUserService>().value, userService = inject<IUserService>().value,
activityPubUserService = inject<ActivityPubUserService>().value, activityPubUserService = inject<ActivityPubUserService>().value,
postService = inject<IPostService>().value, postService = inject<IPostApiService>().value,
userApiService = inject<IUserApiService>().value, userApiService = inject<IUserApiService>().value,
) )
} }

View File

@ -5,13 +5,12 @@ import dev.usbharu.hideout.routing.activitypub.outbox
import dev.usbharu.hideout.routing.activitypub.usersAP import dev.usbharu.hideout.routing.activitypub.usersAP
import dev.usbharu.hideout.routing.api.internal.v1.posts import dev.usbharu.hideout.routing.api.internal.v1.posts
import dev.usbharu.hideout.routing.api.internal.v1.users import dev.usbharu.hideout.routing.api.internal.v1.users
import dev.usbharu.hideout.routing.api.mastodon.v1.statuses
import dev.usbharu.hideout.routing.wellknown.webfinger import dev.usbharu.hideout.routing.wellknown.webfinger
import dev.usbharu.hideout.service.activitypub.ActivityPubService import dev.usbharu.hideout.service.activitypub.ActivityPubService
import dev.usbharu.hideout.service.activitypub.ActivityPubUserService import dev.usbharu.hideout.service.activitypub.ActivityPubUserService
import dev.usbharu.hideout.service.api.IPostApiService
import dev.usbharu.hideout.service.api.IUserApiService import dev.usbharu.hideout.service.api.IUserApiService
import dev.usbharu.hideout.service.auth.HttpSignatureVerifyService import dev.usbharu.hideout.service.auth.HttpSignatureVerifyService
import dev.usbharu.hideout.service.post.IPostService
import dev.usbharu.hideout.service.user.IUserService import dev.usbharu.hideout.service.user.IUserService
import io.ktor.server.application.* import io.ktor.server.application.*
import io.ktor.server.plugins.autohead.* import io.ktor.server.plugins.autohead.*
@ -19,12 +18,12 @@ import io.ktor.server.routing.*
@Suppress("LongParameterList") @Suppress("LongParameterList")
fun Application.configureRouting( fun Application.configureRouting(
httpSignatureVerifyService: HttpSignatureVerifyService, httpSignatureVerifyService: HttpSignatureVerifyService,
activityPubService: ActivityPubService, activityPubService: ActivityPubService,
userService: IUserService, userService: IUserService,
activityPubUserService: ActivityPubUserService, activityPubUserService: ActivityPubUserService,
postService: IPostService, postService: IPostApiService,
userApiService: IUserApiService userApiService: IUserApiService
) { ) {
install(AutoHeadResponse) install(AutoHeadResponse)
routing { routing {
@ -32,10 +31,6 @@ fun Application.configureRouting(
outbox() outbox()
usersAP(activityPubUserService, userService) usersAP(activityPubUserService, userService)
webfinger(userService) webfinger(userService)
route("/api/v1") {
statuses(postService)
}
route("/api/internal/v1") { route("/api/internal/v1") {
posts(postService) posts(postService)
users(userService, userApiService) users(userService, userApiService)

View File

@ -1,11 +1,44 @@
package dev.usbharu.hideout.repository package dev.usbharu.hideout.repository
import dev.usbharu.hideout.domain.model.hideout.entity.Post import dev.usbharu.hideout.domain.model.hideout.entity.Post
import java.time.Instant
@Suppress("LongParameterList")
interface IPostRepository { interface IPostRepository {
suspend fun generateId(): Long suspend fun generateId(): Long
suspend fun save(post: Post): Post suspend fun save(post: Post): Post
suspend fun findOneById(id: Long): Post? suspend fun findOneById(id: Long, userId: Long? = null): Post?
suspend fun findByUrl(url: String): Post? suspend fun findByUrl(url: String): Post?
suspend fun delete(id: Long) suspend fun delete(id: Long)
suspend fun findAll(
since: Instant?,
until: Instant?,
minId: Long?,
maxId: Long?,
limit: Int?,
userId: Long?
): List<Post>
suspend fun findByUserNameAndDomain(
username: String,
s: String,
since: Instant?,
until: Instant?,
minId: Long?,
maxId: Long?,
limit: Int?,
userId: Long?
): List<Post>
suspend fun findByUserId(
idOrNull: Long,
since: Instant?,
until: Instant?,
minId: Long?,
maxId: Long?,
limit: Int?,
userId: Long?
): List<Post>
suspend fun findByApId(id: String): Post?
} }

View File

@ -9,6 +9,7 @@ import org.jetbrains.exposed.sql.SqlExpressionBuilder.eq
import org.jetbrains.exposed.sql.transactions.experimental.newSuspendedTransaction import org.jetbrains.exposed.sql.transactions.experimental.newSuspendedTransaction
import org.jetbrains.exposed.sql.transactions.transaction import org.jetbrains.exposed.sql.transactions.transaction
import org.koin.core.annotation.Single import org.koin.core.annotation.Single
import java.time.Instant
@Single @Single
class PostRepositoryImpl(database: Database, private val idGenerateService: IdGenerateService) : IPostRepository { class PostRepositoryImpl(database: Database, private val idGenerateService: IdGenerateService) : IPostRepository {
@ -28,27 +29,41 @@ class PostRepositoryImpl(database: Database, private val idGenerateService: IdGe
override suspend fun save(post: Post): Post { override suspend fun save(post: Post): Post {
return query { return query {
Posts.insert { val singleOrNull = Posts.select { Posts.id eq post.id }.singleOrNull()
it[id] = post.id if (singleOrNull == null) {
it[userId] = post.userId Posts.insert {
it[overview] = post.overview it[id] = post.id
it[text] = post.text it[userId] = post.userId
it[createdAt] = post.createdAt it[overview] = post.overview
it[visibility] = post.visibility.ordinal it[text] = post.text
it[url] = post.url it[createdAt] = post.createdAt
it[repostId] = post.repostId it[visibility] = post.visibility.ordinal
it[replyId] = post.replyId it[url] = post.url
it[sensitive] = post.sensitive it[repostId] = post.repostId
it[apId] = post.apId it[replyId] = post.replyId
it[sensitive] = post.sensitive
it[apId] = post.apId
}
} else {
Posts.update({ Posts.id eq post.id }) {
it[userId] = post.userId
it[overview] = post.overview
it[text] = post.text
it[createdAt] = post.createdAt
it[visibility] = post.visibility.ordinal
it[url] = post.url
it[repostId] = post.repostId
it[replyId] = post.replyId
it[sensitive] = post.sensitive
it[apId] = post.apId
}
} }
return@query post return@query post
} }
} }
override suspend fun findOneById(id: Long): Post? { override suspend fun findOneById(id: Long, userId: Long?): Post? {
return query { TODO("Not yet implemented")
Posts.select { Posts.id eq id }.singleOrNull()?.toPost()
}
} }
override suspend fun findByUrl(url: String): Post? { override suspend fun findByUrl(url: String): Post? {
@ -62,6 +77,48 @@ class PostRepositoryImpl(database: Database, private val idGenerateService: IdGe
Posts.deleteWhere { Posts.id eq id } Posts.deleteWhere { Posts.id eq id }
} }
} }
override suspend fun findAll(
since: Instant?,
until: Instant?,
minId: Long?,
maxId: Long?,
limit: Int?,
userId: Long?
): List<Post> {
TODO("Not yet implemented")
}
override suspend fun findByUserNameAndDomain(
username: String,
s: String,
since: Instant?,
until: Instant?,
minId: Long?,
maxId: Long?,
limit: Int?,
userId: Long?
): List<Post> {
TODO("Not yet implemented")
}
override suspend fun findByUserId(
idOrNull: Long,
since: Instant?,
until: Instant?,
minId: Long?,
maxId: Long?,
limit: Int?,
userId: Long?
): List<Post> {
TODO("Not yet implemented")
}
override suspend fun findByApId(id: String): Post? {
return query {
Posts.select { Posts.apId eq id }.singleOrNull()?.toPost()
}
}
} }
object Posts : Table() { object Posts : Table() {
@ -81,16 +138,16 @@ object Posts : Table() {
fun ResultRow.toPost(): Post { fun ResultRow.toPost(): Post {
return Post( return Post(
id = this[Posts.id], id = this[Posts.id],
userId = this[Posts.userId], userId = this[Posts.userId],
overview = this[Posts.overview], overview = this[Posts.overview],
text = this[Posts.text], text = this[Posts.text],
createdAt = this[Posts.createdAt], createdAt = this[Posts.createdAt],
visibility = Visibility.values().first { visibility -> visibility.ordinal == this[Posts.visibility] }, visibility = Visibility.values().first { visibility -> visibility.ordinal == this[Posts.visibility] },
url = this[Posts.url], url = this[Posts.url],
repostId = this[Posts.repostId], repostId = this[Posts.repostId],
replyId = this[Posts.replyId], replyId = this[Posts.replyId],
sensitive = this[Posts.sensitive], sensitive = this[Posts.sensitive],
apId = this[Posts.apId] apId = this[Posts.apId]
) )
} }

View File

@ -1,13 +1,9 @@
package dev.usbharu.hideout.routing.api.internal.v1 package dev.usbharu.hideout.routing.api.internal.v1
import dev.usbharu.hideout.config.Config
import dev.usbharu.hideout.domain.model.hideout.dto.PostCreateDto
import dev.usbharu.hideout.domain.model.hideout.form.Post import dev.usbharu.hideout.domain.model.hideout.form.Post
import dev.usbharu.hideout.exception.ParameterNotExistException import dev.usbharu.hideout.exception.ParameterNotExistException
import dev.usbharu.hideout.exception.PostNotFoundException
import dev.usbharu.hideout.plugins.TOKEN_AUTH import dev.usbharu.hideout.plugins.TOKEN_AUTH
import dev.usbharu.hideout.service.post.IPostService import dev.usbharu.hideout.service.api.IPostApiService
import dev.usbharu.hideout.util.AcctUtil
import dev.usbharu.hideout.util.InstantParseUtil import dev.usbharu.hideout.util.InstantParseUtil
import io.ktor.http.* import io.ktor.http.*
import io.ktor.server.application.* import io.ktor.server.application.*
@ -18,7 +14,7 @@ import io.ktor.server.response.*
import io.ktor.server.routing.* import io.ktor.server.routing.*
@Suppress("LongMethod") @Suppress("LongMethod")
fun Route.posts(postService: IPostService) { fun Route.posts(postApiService: IPostApiService) {
route("/posts") { route("/posts") {
authenticate(TOKEN_AUTH) { authenticate(TOKEN_AUTH) {
post { post {
@ -26,15 +22,7 @@ fun Route.posts(postService: IPostService) {
val userId = principal.payload.getClaim("uid").asLong() val userId = principal.payload.getClaim("uid").asLong()
val receive = call.receive<Post>() val receive = call.receive<Post>()
val postCreateDto = PostCreateDto( val create = postApiService.createPost(receive, userId)
text = receive.text,
overview = receive.overview,
visibility = receive.visibility,
repostId = receive.repostId,
repolyId = receive.replyId,
userId = userId
)
val create = postService.create(postCreateDto)
call.response.header("Location", create.url) call.response.header("Location", create.url)
call.respond(HttpStatusCode.OK) call.respond(HttpStatusCode.OK)
} }
@ -47,16 +35,13 @@ fun Route.posts(postService: IPostService) {
val minId = call.request.queryParameters["minId"]?.toLong() val minId = call.request.queryParameters["minId"]?.toLong()
val maxId = call.request.queryParameters["maxId"]?.toLong() val maxId = call.request.queryParameters["maxId"]?.toLong()
val limit = call.request.queryParameters["limit"]?.toInt() val limit = call.request.queryParameters["limit"]?.toInt()
call.respond(HttpStatusCode.OK, postService.findAll(since, until, minId, maxId, limit, userId)) call.respond(HttpStatusCode.OK, postApiService.getAll(since, until, minId, maxId, limit, userId))
} }
get("/{id}") { get("/{id}") {
val userId = call.principal<JWTPrincipal>()?.payload?.getClaim("uid")?.asLong() val userId = call.principal<JWTPrincipal>()?.payload?.getClaim("uid")?.asLong()
val id = call.parameters["id"]?.toLong() val id = call.parameters["id"]?.toLong()
?: throw ParameterNotExistException("Parameter(id='postsId') does not exist.") ?: throw ParameterNotExistException("Parameter(id='postsId') does not exist.")
val post = ( val post = postApiService.getById(id, userId)
postService.findByIdForUser(id, userId)
?: throw PostNotFoundException("$id was not found or is not authorized.")
)
call.respond(post) call.respond(post)
} }
} }
@ -66,28 +51,15 @@ fun Route.posts(postService: IPostService) {
get { get {
val userId = call.principal<JWTPrincipal>()?.payload?.getClaim("uid")?.asLong() val userId = call.principal<JWTPrincipal>()?.payload?.getClaim("uid")?.asLong()
val targetUserName = call.parameters["name"] val targetUserName = call.parameters["name"]
?: throw ParameterNotExistException("Parameter(name='userName@domain') does not exist.") ?: throw ParameterNotExistException("Parameter(name='userName@domain') does not exist.")
val targetUserId = targetUserName.toLongOrNull() val posts = postApiService.getByUser(targetUserName, userId = userId)
val posts = if (targetUserId == null) {
val acct = AcctUtil.parse(targetUserName)
postService.findByUserNameAndDomainForUser(
acct.username,
acct.domain ?: Config.configData.domain,
forUserId = userId
)
} else {
postService.findByUserIdForUser(targetUserId, forUserId = userId)
}
call.respond(posts) call.respond(posts)
} }
get("/{id}") { get("/{id}") {
val userId = call.principal<JWTPrincipal>()?.payload?.getClaim("uid")?.asLong() val userId = call.principal<JWTPrincipal>()?.payload?.getClaim("uid")?.asLong()
val id = call.parameters["id"]?.toLong() val id = call.parameters["id"]?.toLong()
?: throw ParameterNotExistException("Parameter(name='postsId' does not exist.") ?: throw ParameterNotExistException("Parameter(name='postsId' does not exist.")
val post = ( val post = postApiService.getById(id, userId)
postService.findByIdForUser(id, userId)
?: throw PostNotFoundException("$id was not found or is not authorized.")
)
call.respond(post) call.respond(post)
} }
} }

View File

@ -43,7 +43,9 @@ fun Route.users(userService: IUserService, userApiService: IUserApiService) {
get { get {
val userParameter = ( val userParameter = (
call.parameters["name"] call.parameters["name"]
?: throw ParameterNotExistException("Parameter(name='userName@domain') does not exist.") ?: throw ParameterNotExistException(
"Parameter(name='userName@domain') does not exist."
)
) )
if (userParameter.toLongOrNull() != null) { if (userParameter.toLongOrNull() != null) {
return@get call.respond(userApiService.findById(userParameter.toLong())) return@get call.respond(userApiService.findById(userParameter.toLong()))
@ -91,7 +93,9 @@ fun Route.users(userService: IUserService, userApiService: IUserApiService) {
get { get {
val userParameter = ( val userParameter = (
call.parameters["name"] call.parameters["name"]
?: throw ParameterNotExistException("Parameter(name='userName@domain') does not exist.") ?: throw ParameterNotExistException(
"Parameter(name='userName@domain') does not exist."
)
) )
if (userParameter.toLongOrNull() != null) { if (userParameter.toLongOrNull() != null) {
return@get call.respond(userApiService.findFollowings(userParameter.toLong())) return@get call.respond(userApiService.findFollowings(userParameter.toLong()))

View File

@ -1,21 +1,18 @@
package dev.usbharu.hideout.routing.api.mastodon.v1 package dev.usbharu.hideout.routing.api.mastodon.v1
import dev.usbharu.hideout.service.post.IPostService // @Suppress("UnusedPrivateMember")
import io.ktor.server.routing.* // fun Route.statuses(postService: IPostService) {
// // route("/statuses") {
@Suppress("UnusedPrivateMember") // // post {
fun Route.statuses(postService: IPostService) { // // val status: StatusForPost = call.receive()
// route("/statuses") { // // val post = dev.usbharu.hideout.domain.model.hideout.form.Post(
// post { // // userId = status.userId,
// val status: StatusForPost = call.receive() // // createdAt = System.currentTimeMillis(),
// val post = dev.usbharu.hideout.domain.model.hideout.form.Post( // // text = status.status,
// userId = status.userId, // // visibility = 1
// createdAt = System.currentTimeMillis(), // // )
// text = status.status, // // postService.create(post)
// visibility = 1 // // call.respond(status)
// ) // // }
// postService.create(post) // // }
// call.respond(status) // }
// }
// }
}

View File

@ -22,11 +22,11 @@ import java.time.Instant
@Single @Single
class ActivityPubNoteServiceImpl( class ActivityPubNoteServiceImpl(
private val httpClient: HttpClient, private val httpClient: HttpClient,
private val jobQueueParentService: JobQueueParentService, private val jobQueueParentService: JobQueueParentService,
private val userService: IUserService, private val userService: IUserService,
private val postRepository: IPostRepository, private val postRepository: IPostRepository,
private val activityPubUserService: ActivityPubUserService private val activityPubUserService: ActivityPubUserService
) : ActivityPubNoteService { ) : ActivityPubNoteService {
private val logger = LoggerFactory.getLogger(this::class.java) private val logger = LoggerFactory.getLogger(this::class.java)
@ -48,33 +48,44 @@ class ActivityPubNoteServiceImpl(
val actor = props[DeliverPostJob.actor] val actor = props[DeliverPostJob.actor]
val postEntity = Config.configData.objectMapper.readValue<Post>(props[DeliverPostJob.post]) val postEntity = Config.configData.objectMapper.readValue<Post>(props[DeliverPostJob.post])
val note = Note( val note = Note(
name = "Note", name = "Note",
id = postEntity.url, id = postEntity.url,
attributedTo = actor, attributedTo = actor,
content = postEntity.text, content = postEntity.text,
published = Instant.ofEpochMilli(postEntity.createdAt).toString(), published = Instant.ofEpochMilli(postEntity.createdAt).toString(),
to = listOf(public, actor + "/follower") to = listOf(public, actor + "/follower")
) )
val inbox = props[DeliverPostJob.inbox] val inbox = props[DeliverPostJob.inbox]
logger.debug("createNoteJob: actor={}, note={}, inbox={}", actor, postEntity, inbox) logger.debug("createNoteJob: actor={}, note={}, inbox={}", actor, postEntity, inbox)
httpClient.postAp( httpClient.postAp(
urlString = inbox, urlString = inbox,
username = "$actor#pubkey", username = "$actor#pubkey",
jsonLd = Create( jsonLd = Create(
name = "Create Note", name = "Create Note",
`object` = note, `object` = note,
actor = note.attributedTo, actor = note.attributedTo,
id = "${Config.configData.url}/create/note/${postEntity.id}" id = "${Config.configData.url}/create/note/${postEntity.id}"
) )
) )
} }
override suspend fun fetchNote(url: String, targetActor: String?): Note { override suspend fun fetchNote(url: String, targetActor: String?): Note {
val post = postRepository.findByUrl(url) val post = postRepository.findByUrl(url)
if (post != null) { if (post != null) {
val user = userService.findById(post.userId) return postToNote(post)
val reply = post.replyId?.let { postRepository.findOneById(it) } }
return Note( val response = httpClient.getAp(
url,
"$targetActor#pubkey"
)
val note = response.body<Note>()
return note(note, targetActor, url)
}
private suspend fun postToNote(post: Post): Note {
val user = userService.findById(post.userId)
val reply = post.replyId?.let { postRepository.findOneById(it) }
return Note(
name = "Post", name = "Post",
id = post.apId, id = post.apId,
attributedTo = user.url, attributedTo = user.url,
@ -84,38 +95,35 @@ class ActivityPubNoteServiceImpl(
sensitive = post.sensitive, sensitive = post.sensitive,
cc = listOf(public, user.url + "/follower"), cc = listOf(public, user.url + "/follower"),
inReplyTo = reply?.url inReplyTo = reply?.url
)
}
val response = httpClient.getAp(
url,
"$targetActor#pubkey"
) )
val note = response.body<Note>()
return note(note, targetActor, url)
} }
private suspend fun ActivityPubNoteServiceImpl.note( private suspend fun ActivityPubNoteServiceImpl.note(
note: Note, note: Note,
targetActor: String?, targetActor: String?,
url: String url: String
): Note { ): Note {
val findByApId = postRepository.findByApId(url)
if (findByApId != null) {
return postToNote(findByApId)
}
val person = activityPubUserService.fetchPerson( val person = activityPubUserService.fetchPerson(
note.attributedTo ?: throw IllegalActivityPubObjectException("note.attributedTo is null"), note.attributedTo ?: throw IllegalActivityPubObjectException("note.attributedTo is null"),
targetActor targetActor
) )
val user = val user =
userService.findByUrl(person.url ?: throw IllegalActivityPubObjectException("person.url is null")) userService.findByUrl(person.url ?: throw IllegalActivityPubObjectException("person.url is null"))
val visibility = val visibility =
if (note.to.contains(public) && note.cc.contains(public)) { if (note.to.contains(public) && note.cc.contains(public)) {
Visibility.PUBLIC Visibility.PUBLIC
} else if (note.to.find { it.endsWith("/followers") } != null && note.cc.contains(public)) { } else if (note.to.find { it.endsWith("/followers") } != null && note.cc.contains(public)) {
Visibility.UNLISTED Visibility.UNLISTED
} else if (note.to.find { it.endsWith("/followers") } != null) { } else if (note.to.find { it.endsWith("/followers") } != null) {
Visibility.FOLLOWERS Visibility.FOLLOWERS
} else { } else {
Visibility.DIRECT Visibility.DIRECT
} }
val reply = note.inReplyTo?.let { val reply = note.inReplyTo?.let {
fetchNote(it, targetActor) fetchNote(it, targetActor)
@ -123,27 +131,27 @@ class ActivityPubNoteServiceImpl(
} }
postRepository.save( postRepository.save(
Post( Post(
id = postRepository.generateId(), id = postRepository.generateId(),
userId = user.id, userId = user.id,
overview = null, overview = null,
text = note.content.orEmpty(), text = note.content.orEmpty(),
createdAt = Instant.parse(note.published).toEpochMilli(), createdAt = Instant.parse(note.published).toEpochMilli(),
visibility = visibility, visibility = visibility,
url = note.id ?: url, url = note.id ?: url,
repostId = null, repostId = null,
replyId = reply?.id, replyId = reply?.id,
sensitive = note.sensitive, sensitive = note.sensitive,
apId = note.id ?: url, apId = note.id ?: url,
) )
) )
return note return note
} }
override suspend fun fetchNote(note: Note, targetActor: String?): Note = override suspend fun fetchNote(note: Note, targetActor: String?): Note =
note(note, targetActor, note.id ?: throw IllegalArgumentException("note.id is null")) note(note, targetActor, note.id ?: throw IllegalArgumentException("note.id is null"))
companion object { companion object {
val public: String = "https://www.w3.org/ns/activitystreams#Public" const val public: String = "https://www.w3.org/ns/activitystreams#Public"
} }
} }

View File

@ -0,0 +1,27 @@
package dev.usbharu.hideout.service.api
import dev.usbharu.hideout.domain.model.hideout.entity.Post
import java.time.Instant
interface IPostApiService {
suspend fun createPost(postForm: dev.usbharu.hideout.domain.model.hideout.form.Post, userId: Long): Post
suspend fun getById(id: Long, userId: Long?): Post
suspend fun getAll(
since: Instant? = null,
until: Instant? = null,
minId: Long? = null,
maxId: Long? = null,
limit: Int? = null,
userId: Long? = null
): List<Post>
suspend fun getByUser(
nameOrId: String,
since: Instant? = null,
until: Instant? = null,
minId: Long? = null,
maxId: Long? = null,
limit: Int? = null,
userId: Long? = null
): List<Post>
}

View File

@ -0,0 +1,73 @@
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.entity.Post
import dev.usbharu.hideout.exception.PostNotFoundException
import dev.usbharu.hideout.repository.IPostRepository
import dev.usbharu.hideout.service.post.IPostService
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 postRepository: IPostRepository
) : IPostApiService {
override suspend fun createPost(postForm: FormPost, userId: Long): Post {
return postService.createLocal(
PostCreateDto(
text = postForm.text,
overview = postForm.overview,
visibility = postForm.visibility,
repostId = postForm.repostId,
repolyId = postForm.replyId,
userId = userId
)
)
}
override suspend fun getById(id: Long, userId: Long?): Post {
return postRepository.findOneById(id, userId)
?: throw PostNotFoundException("$id was not found or is not authorized.")
}
override suspend fun getAll(
since: Instant?,
until: Instant?,
minId: Long?,
maxId: Long?,
limit: Int?,
userId: Long?
): List<Post> = postRepository.findAll(since, until, minId, maxId, limit, userId)
override suspend fun getByUser(
nameOrId: String,
since: Instant?,
until: Instant?,
minId: Long?,
maxId: Long?,
limit: Int?,
userId: Long?
): List<Post> {
val idOrNull = nameOrId.toLongOrNull()
return if (idOrNull == null) {
val acct = AcctUtil.parse(nameOrId)
postRepository.findByUserNameAndDomain(
acct.username,
acct.domain
?: Config.configData.domain,
since,
until,
minId,
maxId,
limit,
userId
)
} else {
postRepository.findByUserId(idOrNull, since, until, minId, maxId, limit, userId)
}
}
}

View File

@ -1,61 +1,8 @@
package dev.usbharu.hideout.service.post package dev.usbharu.hideout.service.post
import dev.usbharu.hideout.config.Config
import dev.usbharu.hideout.domain.model.hideout.dto.PostCreateDto import dev.usbharu.hideout.domain.model.hideout.dto.PostCreateDto
import dev.usbharu.hideout.domain.model.hideout.entity.Post import dev.usbharu.hideout.domain.model.hideout.entity.Post
import java.time.Instant
@Suppress("LongParameterList")
interface IPostService { interface IPostService {
suspend fun create(post: Post): Post suspend fun createLocal(post: PostCreateDto): Post
suspend fun create(post: PostCreateDto): Post
suspend fun findAll(
since: Instant? = null,
until: Instant? = null,
minId: Long? = null,
maxId: Long? = null,
limit: Int? = 10,
userId: Long? = null
): List<Post>
suspend fun findById(id: String): Post
/**
* 権限を考慮して投稿を取得します
*
* @param id
* @param userId
* @return
*/
suspend fun findByIdForUser(id: Long, userId: Long?): Post?
/**
* 権限を考慮してユーザーの投稿を取得します
*
* @param userId
* @param forUserId
* @return
*/
suspend fun findByUserIdForUser(
userId: Long,
since: Instant? = null,
until: Instant? = null,
minId: Long? = null,
maxId: Long? = null,
limit: Int? = null,
forUserId: Long? = null
): List<Post>
suspend fun findByUserNameAndDomainForUser(
userName: String,
domain: String = Config.configData.domain,
since: Instant? = null,
until: Instant? = null,
minId: Long? = null,
maxId: Long? = null,
limit: Int? = null,
forUserId: Long? = null
): List<Post>
suspend fun delete(id: String)
} }

View File

@ -1,130 +0,0 @@
package dev.usbharu.hideout.service.post
import dev.usbharu.hideout.domain.model.hideout.dto.PostCreateDto
import dev.usbharu.hideout.domain.model.hideout.entity.Post
import dev.usbharu.hideout.domain.model.hideout.entity.Visibility
import dev.usbharu.hideout.repository.IPostRepository
import dev.usbharu.hideout.repository.Posts
import dev.usbharu.hideout.repository.UsersFollowers
import dev.usbharu.hideout.repository.toPost
import dev.usbharu.hideout.service.activitypub.ActivityPubNoteService
import dev.usbharu.hideout.service.user.IUserService
import org.jetbrains.exposed.sql.SqlExpressionBuilder.eq
import org.jetbrains.exposed.sql.SqlExpressionBuilder.inSubQuery
import org.jetbrains.exposed.sql.and
import org.jetbrains.exposed.sql.orIfNotNull
import org.jetbrains.exposed.sql.orWhere
import org.jetbrains.exposed.sql.select
import org.jetbrains.exposed.sql.transactions.transaction
import org.koin.core.annotation.Single
import org.slf4j.LoggerFactory
import java.time.Instant
@Single
class PostService(
private val postRepository: IPostRepository,
private val activityPubNoteService: ActivityPubNoteService,
private val userService: IUserService
) : IPostService {
private val logger = LoggerFactory.getLogger(this::class.java)
override suspend fun create(post: Post): Post {
logger.debug("create post={}", post)
val postEntity = postRepository.save(post)
activityPubNoteService.createNote(postEntity)
return post
}
override suspend fun create(post: PostCreateDto): Post {
logger.debug("create post={}", post)
val user = userService.findById(post.userId)
val id = postRepository.generateId()
val postEntity = Post(
id = id,
userId = user.id,
overview = null,
text = post.text,
createdAt = Instant.now().toEpochMilli(),
visibility = Visibility.PUBLIC,
url = "${user.url}/posts/$id",
repostId = null,
replyId = null
)
postRepository.save(postEntity)
return postEntity
}
override suspend fun findAll(
since: Instant?,
until: Instant?,
minId: Long?,
maxId: Long?,
limit: Int?,
userId: Long?
): List<Post> {
return transaction {
val select = Posts.select {
Posts.visibility.eq(Visibility.PUBLIC.ordinal)
}
if (userId != null) {
select.orWhere {
Posts.userId.inSubQuery(
UsersFollowers.slice(UsersFollowers.userId).select(UsersFollowers.followerId eq userId)
)
}
}
select.map { it.toPost() }
}
}
override suspend fun findById(id: String): Post {
TODO("Not yet implemented")
}
override suspend fun findByIdForUser(id: Long, userId: Long?): Post? {
return transaction {
val select = Posts.select(
Posts.id.eq(id).and(
Posts.visibility.eq(Visibility.PUBLIC.ordinal).orIfNotNull(
userId?.let {
Posts.userId.inSubQuery(
UsersFollowers.slice(UsersFollowers.userId).select(UsersFollowers.followerId.eq(userId))
)
}
)
)
)
select.singleOrNull()?.toPost()
}
}
override suspend fun findByUserIdForUser(
userId: Long,
since: Instant?,
until: Instant?,
minId: Long?,
maxId: Long?,
limit: Int?,
forUserId: Long?
): List<Post> {
TODO("Not yet implemented")
}
override suspend fun findByUserNameAndDomainForUser(
userName: String,
domain: String,
since: Instant?,
until: Instant?,
minId: Long?,
maxId: Long?,
limit: Int?,
forUserId: Long?
): List<Post> {
TODO("Not yet implemented")
}
override suspend fun delete(id: String) {
TODO("Not yet implemented")
}
}

View File

@ -0,0 +1,37 @@
package dev.usbharu.hideout.service.post
import dev.usbharu.hideout.domain.model.hideout.dto.PostCreateDto
import dev.usbharu.hideout.domain.model.hideout.entity.Post
import dev.usbharu.hideout.exception.UserNotFoundException
import dev.usbharu.hideout.repository.IPostRepository
import dev.usbharu.hideout.repository.IUserRepository
import dev.usbharu.hideout.service.activitypub.ActivityPubNoteService
import org.koin.core.annotation.Single
import java.time.Instant
@Single
class PostServiceImpl(
private val postRepository: IPostRepository,
private val userRepository: IUserRepository,
private val activityPubNoteService: ActivityPubNoteService
) : IPostService {
override suspend fun createLocal(post: PostCreateDto): Post {
val user = userRepository.findById(post.userId) ?: throw UserNotFoundException("${post.userId} was not found")
val id = postRepository.generateId()
val createPost = Post(
id = id,
userId = post.userId,
overview = post.overview,
text = post.text,
createdAt = Instant.now().toEpochMilli(),
visibility = post.visibility,
url = "${user.url}/posts/$id",
repostId = null,
replyId = null
)
activityPubNoteService.createNote(createPost)
return internalCreate(createPost)
}
private suspend fun internalCreate(post: Post): Post = postRepository.save(post)
}

View File

@ -4,13 +4,12 @@ import com.auth0.jwt.interfaces.Claim
import com.auth0.jwt.interfaces.Payload import com.auth0.jwt.interfaces.Payload
import com.fasterxml.jackson.module.kotlin.readValue import com.fasterxml.jackson.module.kotlin.readValue
import dev.usbharu.hideout.config.Config import dev.usbharu.hideout.config.Config
import dev.usbharu.hideout.domain.model.hideout.dto.PostCreateDto
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.hideout.entity.Visibility
import dev.usbharu.hideout.plugins.TOKEN_AUTH import dev.usbharu.hideout.plugins.TOKEN_AUTH
import dev.usbharu.hideout.plugins.configureSecurity import dev.usbharu.hideout.plugins.configureSecurity
import dev.usbharu.hideout.plugins.configureSerialization import dev.usbharu.hideout.plugins.configureSerialization
import dev.usbharu.hideout.service.post.IPostService import dev.usbharu.hideout.service.api.IPostApiService
import io.ktor.client.request.* import io.ktor.client.request.*
import io.ktor.client.statement.* import io.ktor.client.statement.*
import io.ktor.http.* import io.ktor.http.*
@ -42,24 +41,24 @@ class PostsTest {
createdAt = Instant.now().toEpochMilli(), createdAt = Instant.now().toEpochMilli(),
url = "https://example.com/posts/1" url = "https://example.com/posts/1"
), ),
Post( Post(
id = 123456, id = 123456,
userId = 4322, userId = 4322,
text = "test2", text = "test2",
visibility = Visibility.PUBLIC, visibility = Visibility.PUBLIC,
createdAt = Instant.now().toEpochMilli(), createdAt = Instant.now().toEpochMilli(),
url = "https://example.com/posts/2" url = "https://example.com/posts/2"
) )
) )
val postService = mock<IPostService> { val postService = mock<IPostApiService> {
onBlocking { onBlocking {
findAll( getAll(
since = anyOrNull(), since = anyOrNull(),
until = anyOrNull(), until = anyOrNull(),
minId = anyOrNull(), minId = anyOrNull(),
maxId = anyOrNull(), maxId = anyOrNull(),
limit = anyOrNull(), limit = anyOrNull(),
userId = isNull() userId = isNull()
) )
} doReturn posts } doReturn posts
} }
@ -118,15 +117,15 @@ class PostsTest {
) )
) )
val postService = mock<IPostService> { val postService = mock<IPostApiService> {
onBlocking { onBlocking {
findAll( getAll(
since = anyOrNull(), since = anyOrNull(),
until = anyOrNull(), until = anyOrNull(),
minId = anyOrNull(), minId = anyOrNull(),
maxId = anyOrNull(), maxId = anyOrNull(),
limit = anyOrNull(), limit = anyOrNull(),
userId = isNotNull() userId = isNotNull()
) )
} doReturn posts } doReturn posts
} }
@ -158,15 +157,15 @@ class PostsTest {
config = ApplicationConfig("empty.conf") config = ApplicationConfig("empty.conf")
} }
val post = Post( val post = Post(
12345, 12345,
1234, 1234,
text = "aaa", text = "aaa",
visibility = Visibility.PUBLIC, visibility = Visibility.PUBLIC,
createdAt = Instant.now().toEpochMilli(), createdAt = Instant.now().toEpochMilli(),
url = "https://example.com/posts/1" url = "https://example.com/posts/1"
) )
val postService = mock<IPostService> { val postService = mock<IPostApiService> {
onBlocking { findByIdForUser(any(), anyOrNull()) } doReturn post onBlocking { getById(any(), anyOrNull()) } doReturn post
} }
application { application {
configureSerialization() configureSerialization()
@ -189,15 +188,15 @@ class PostsTest {
config = ApplicationConfig("empty.conf") config = ApplicationConfig("empty.conf")
} }
val post = Post( val post = Post(
12345, 12345,
1234, 1234,
text = "aaa", text = "aaa",
visibility = Visibility.FOLLOWERS, visibility = Visibility.FOLLOWERS,
createdAt = Instant.now().toEpochMilli(), createdAt = Instant.now().toEpochMilli(),
url = "https://example.com/posts/1" url = "https://example.com/posts/1"
) )
val postService = mock<IPostService> { val postService = mock<IPostApiService> {
onBlocking { findByIdForUser(any(), isNotNull()) } doReturn post onBlocking { getById(any(), isNotNull()) } doReturn post
} }
val claim = mock<Claim> { val claim = mock<Claim> {
on { asLong() } doReturn 1234 on { asLong() } doReturn 1234
@ -239,17 +238,18 @@ class PostsTest {
val payload = mock<Payload> { val payload = mock<Payload> {
on { getClaim(eq("uid")) } doReturn claim on { getClaim(eq("uid")) } doReturn claim
} }
val postService = mock<IPostService> { val postService = mock<IPostApiService> {
onBlocking { create(any<PostCreateDto>()) } doAnswer { onBlocking { createPost(any(), any()) } doAnswer {
val argument = it.getArgument<PostCreateDto>(0) val argument = it.getArgument<dev.usbharu.hideout.domain.model.hideout.form.Post>(0)
val userId = it.getArgument<Long>(1)
Post( Post(
123L, 123L,
argument.userId, userId,
null, null,
argument.text, argument.text,
Instant.now().toEpochMilli(), Instant.now().toEpochMilli(),
Visibility.PUBLIC, Visibility.PUBLIC,
"https://example.com" "https://example.com"
) )
} }
} }
@ -279,9 +279,9 @@ class PostsTest {
assertEquals(HttpStatusCode.OK, status) assertEquals(HttpStatusCode.OK, status)
assertEquals("https://example.com", headers["Location"]) assertEquals("https://example.com", headers["Location"])
} }
argumentCaptor<PostCreateDto> { argumentCaptor<dev.usbharu.hideout.domain.model.hideout.form.Post> {
verify(postService).create(capture()) verify(postService).createPost(capture(), any())
assertEquals(PostCreateDto("test", userId = 1234), firstValue) assertEquals(dev.usbharu.hideout.domain.model.hideout.form.Post("test"), firstValue)
} }
} }
@ -299,25 +299,25 @@ class PostsTest {
createdAt = Instant.now().toEpochMilli(), createdAt = Instant.now().toEpochMilli(),
url = "https://example.com/posts/1" url = "https://example.com/posts/1"
), ),
Post( Post(
id = 123456, id = 123456,
userId = 1, userId = 1,
text = "test2", text = "test2",
visibility = Visibility.PUBLIC, visibility = Visibility.PUBLIC,
createdAt = Instant.now().toEpochMilli(), createdAt = Instant.now().toEpochMilli(),
url = "https://example.com/posts/2" url = "https://example.com/posts/2"
) )
) )
val postService = mock<IPostService> { val postService = mock<IPostApiService> {
onBlocking { onBlocking {
findByUserIdForUser( getByUser(
userId = any(), nameOrId = any(),
since = anyOrNull(), since = anyOrNull(),
until = anyOrNull(), until = anyOrNull(),
minId = anyOrNull(), minId = anyOrNull(),
maxId = anyOrNull(), maxId = anyOrNull(),
limit = anyOrNull(), limit = anyOrNull(),
forUserId = anyOrNull() userId = anyOrNull()
) )
} doReturn posts } doReturn posts
} }
@ -351,26 +351,25 @@ class PostsTest {
createdAt = Instant.now().toEpochMilli(), createdAt = Instant.now().toEpochMilli(),
url = "https://example.com/posts/1" url = "https://example.com/posts/1"
), ),
Post( Post(
id = 123456, id = 123456,
userId = 1, userId = 1,
text = "test2", text = "test2",
visibility = Visibility.PUBLIC, visibility = Visibility.PUBLIC,
createdAt = Instant.now().toEpochMilli(), createdAt = Instant.now().toEpochMilli(),
url = "https://example.com/posts/2" url = "https://example.com/posts/2"
) )
) )
val postService = mock<IPostService> { val postService = mock<IPostApiService> {
onBlocking { onBlocking {
findByUserNameAndDomainForUser( getByUser(
userName = eq("test1"), nameOrId = eq("test1"),
domain = eq(Config.configData.domain), since = anyOrNull(),
since = anyOrNull(), until = anyOrNull(),
until = anyOrNull(), minId = anyOrNull(),
minId = anyOrNull(), maxId = anyOrNull(),
maxId = anyOrNull(), limit = anyOrNull(),
limit = anyOrNull(), userId = anyOrNull()
forUserId = anyOrNull()
) )
} doReturn posts } doReturn posts
} }
@ -404,26 +403,25 @@ class PostsTest {
createdAt = Instant.now().toEpochMilli(), createdAt = Instant.now().toEpochMilli(),
url = "https://example.com/posts/1" url = "https://example.com/posts/1"
), ),
Post( Post(
id = 123456, id = 123456,
userId = 1, userId = 1,
text = "test2", text = "test2",
visibility = Visibility.PUBLIC, visibility = Visibility.PUBLIC,
createdAt = Instant.now().toEpochMilli(), createdAt = Instant.now().toEpochMilli(),
url = "https://example.com/posts/2" url = "https://example.com/posts/2"
) )
) )
val postService = mock<IPostService> { val postService = mock<IPostApiService> {
onBlocking { onBlocking {
findByUserNameAndDomainForUser( getByUser(
userName = eq("test1"), nameOrId = eq("test1@example.com"),
domain = eq("example.com"), since = anyOrNull(),
since = anyOrNull(), until = anyOrNull(),
until = anyOrNull(), minId = anyOrNull(),
minId = anyOrNull(), maxId = anyOrNull(),
maxId = anyOrNull(), limit = anyOrNull(),
limit = anyOrNull(), userId = anyOrNull()
forUserId = anyOrNull()
) )
} doReturn posts } doReturn posts
} }
@ -457,26 +455,25 @@ class PostsTest {
createdAt = Instant.now().toEpochMilli(), createdAt = Instant.now().toEpochMilli(),
url = "https://example.com/posts/1" url = "https://example.com/posts/1"
), ),
Post( Post(
id = 123456, id = 123456,
userId = 1, userId = 1,
text = "test2", text = "test2",
visibility = Visibility.PUBLIC, visibility = Visibility.PUBLIC,
createdAt = Instant.now().toEpochMilli(), createdAt = Instant.now().toEpochMilli(),
url = "https://example.com/posts/2" url = "https://example.com/posts/2"
) )
) )
val postService = mock<IPostService> { val postService = mock<IPostApiService> {
onBlocking { onBlocking {
findByUserNameAndDomainForUser( getByUser(
userName = eq("test1"), nameOrId = eq("@test1@example.com"),
domain = eq("example.com"), since = anyOrNull(),
since = anyOrNull(), until = anyOrNull(),
until = anyOrNull(), minId = anyOrNull(),
minId = anyOrNull(), maxId = anyOrNull(),
maxId = anyOrNull(), limit = anyOrNull(),
limit = anyOrNull(), userId = anyOrNull()
forUserId = anyOrNull()
) )
} doReturn posts } doReturn posts
} }
@ -502,15 +499,15 @@ class PostsTest {
config = ApplicationConfig("empty.conf") config = ApplicationConfig("empty.conf")
} }
val post = Post( val post = Post(
id = 123456, id = 123456,
userId = 1, userId = 1,
text = "test2", text = "test2",
visibility = Visibility.PUBLIC, visibility = Visibility.PUBLIC,
createdAt = Instant.now().toEpochMilli(), createdAt = Instant.now().toEpochMilli(),
url = "https://example.com/posts/2" url = "https://example.com/posts/2"
) )
val postService = mock<IPostService> { val postService = mock<IPostApiService> {
onBlocking { findByIdForUser(eq(12345L), anyOrNull()) } doReturn post onBlocking { getById(eq(12345L), anyOrNull()) } doReturn post
} }
application { application {
configureSerialization() configureSerialization()
@ -534,15 +531,15 @@ class PostsTest {
config = ApplicationConfig("empty.conf") config = ApplicationConfig("empty.conf")
} }
val post = Post( val post = Post(
id = 123456, id = 123456,
userId = 1, userId = 1,
text = "test2", text = "test2",
visibility = Visibility.PUBLIC, visibility = Visibility.PUBLIC,
createdAt = Instant.now().toEpochMilli(), createdAt = Instant.now().toEpochMilli(),
url = "https://example.com/posts/2" url = "https://example.com/posts/2"
) )
val postService = mock<IPostService> { val postService = mock<IPostApiService> {
onBlocking { findByIdForUser(eq(12345L), anyOrNull()) } doReturn post onBlocking { getById(eq(12345L), anyOrNull()) } doReturn post
} }
application { application {
configureSerialization() configureSerialization()
@ -566,15 +563,15 @@ class PostsTest {
config = ApplicationConfig("empty.conf") config = ApplicationConfig("empty.conf")
} }
val post = Post( val post = Post(
id = 123456, id = 123456,
userId = 1, userId = 1,
text = "test2", text = "test2",
visibility = Visibility.PUBLIC, visibility = Visibility.PUBLIC,
createdAt = Instant.now().toEpochMilli(), createdAt = Instant.now().toEpochMilli(),
url = "https://example.com/posts/2" url = "https://example.com/posts/2"
) )
val postService = mock<IPostService> { val postService = mock<IPostApiService> {
onBlocking { findByIdForUser(eq(12345L), anyOrNull()) } doReturn post onBlocking { getById(eq(12345L), anyOrNull()) } doReturn post
} }
application { application {
configureSerialization() configureSerialization()
@ -598,15 +595,15 @@ class PostsTest {
config = ApplicationConfig("empty.conf") config = ApplicationConfig("empty.conf")
} }
val post = Post( val post = Post(
id = 123456, id = 123456,
userId = 1, userId = 1,
text = "test2", text = "test2",
visibility = Visibility.PUBLIC, visibility = Visibility.PUBLIC,
createdAt = Instant.now().toEpochMilli(), createdAt = Instant.now().toEpochMilli(),
url = "https://example.com/posts/2" url = "https://example.com/posts/2"
) )
val postService = mock<IPostService> { val postService = mock<IPostApiService> {
onBlocking { findByIdForUser(eq(12345L), anyOrNull()) } doReturn post onBlocking { getById(eq(12345L), anyOrNull()) } doReturn post
} }
application { application {
configureSerialization() configureSerialization()

View File

@ -1,166 +0,0 @@
@file:OptIn(ExperimentalCoroutinesApi::class)
package dev.usbharu.hideout.service.post
import dev.usbharu.hideout.domain.model.hideout.entity.Post
import dev.usbharu.hideout.domain.model.hideout.entity.Visibility
import dev.usbharu.hideout.repository.Posts
import dev.usbharu.hideout.repository.UsersFollowers
import dev.usbharu.hideout.service.core.TwitterSnowflakeIdGenerateService
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.test.runTest
import org.jetbrains.exposed.sql.Database
import org.jetbrains.exposed.sql.SchemaUtils
import org.jetbrains.exposed.sql.batchInsert
import org.jetbrains.exposed.sql.insert
import org.jetbrains.exposed.sql.transactions.transaction
import org.junit.jupiter.api.AfterEach
import org.junit.jupiter.api.BeforeEach
import org.junit.jupiter.api.Test
import org.mockito.kotlin.mock
import java.time.Instant
import kotlin.test.assertContentEquals
class PostServiceTest {
lateinit var db: Database
@BeforeEach
fun setUp() {
db = Database.connect("jdbc:h2:mem:test;DB_CLOSE_DELAY=-1", driver = "org.h2.Driver")
transaction(db) {
SchemaUtils.create(Posts)
connection.prepareStatement("SET REFERENTIAL_INTEGRITY FALSE", false).executeUpdate()
}
}
@AfterEach
fun tearDown() {
transaction(db) {
SchemaUtils.drop(Posts)
}
}
@Test
fun `findAll 公開投稿を取得できる`() = runTest {
val postService = PostService(mock(), mock(), mock())
suspend fun createPost(userId: Long, text: String, visibility: Visibility = Visibility.PUBLIC): Post {
return Post(
TwitterSnowflakeIdGenerateService.generateId(),
userId,
null,
text,
Instant.now().toEpochMilli(),
visibility,
"https://example.com${(userId.toString() + text).hashCode()}"
)
}
val userA: Long = 1
val userB: Long = 2
val posts = listOf(
createPost(userA, "hello"),
createPost(userA, "hello1"),
createPost(userA, "hello2"),
createPost(userA, "hello3"),
createPost(userA, "hello4"),
createPost(userA, "hello5"),
createPost(userA, "hello6"),
createPost(userB, "good bay ", Visibility.FOLLOWERS),
createPost(userB, "good bay1", Visibility.FOLLOWERS),
createPost(userB, "good bay2", Visibility.FOLLOWERS),
createPost(userB, "good bay3", Visibility.FOLLOWERS),
createPost(userB, "good bay4", Visibility.FOLLOWERS),
createPost(userB, "good bay5", Visibility.FOLLOWERS),
createPost(userB, "good bay6", Visibility.FOLLOWERS),
)
transaction {
Posts.batchInsert(posts) {
this[Posts.id] = it.id
this[Posts.userId] = it.userId
this[Posts.overview] = it.overview
this[Posts.text] = it.text
this[Posts.createdAt] = it.createdAt
this[Posts.visibility] = it.visibility.ordinal
this[Posts.url] = it.url
this[Posts.replyId] = it.replyId
this[Posts.repostId] = it.repostId
this[Posts.sensitive] = it.sensitive
this[Posts.apId] = it.apId
}
}
val expect = posts.filter { it.visibility == Visibility.PUBLIC }
val actual = postService.findAll()
assertContentEquals(expect, actual)
}
@Test
fun `findAll フォロー限定投稿を見れる`() = runTest {
val postService = PostService(mock(), mock(), mock())
suspend fun createPost(userId: Long, text: String, visibility: Visibility = Visibility.PUBLIC): Post {
return Post(
TwitterSnowflakeIdGenerateService.generateId(),
userId,
null,
text,
Instant.now().toEpochMilli(),
visibility,
"https://example.com${(userId.toString() + text).hashCode()}"
)
}
val userA: Long = 1
val userB: Long = 2
val posts = listOf(
createPost(userA, "hello"),
createPost(userA, "hello1"),
createPost(userA, "hello2"),
createPost(userA, "hello3"),
createPost(userA, "hello4"),
createPost(userA, "hello5"),
createPost(userA, "hello6"),
createPost(userB, "good bay ", Visibility.FOLLOWERS),
createPost(userB, "good bay1", Visibility.FOLLOWERS),
createPost(userB, "good bay2", Visibility.FOLLOWERS),
createPost(userB, "good bay3", Visibility.FOLLOWERS),
createPost(userB, "good bay4", Visibility.FOLLOWERS),
createPost(userB, "good bay5", Visibility.FOLLOWERS),
createPost(userB, "good bay6", Visibility.FOLLOWERS),
)
transaction(db) {
SchemaUtils.create(UsersFollowers)
}
transaction {
Posts.batchInsert(posts) {
this[Posts.id] = it.id
this[Posts.userId] = it.userId
this[Posts.overview] = it.overview
this[Posts.text] = it.text
this[Posts.createdAt] = it.createdAt
this[Posts.visibility] = it.visibility.ordinal
this[Posts.url] = it.url
this[Posts.replyId] = it.replyId
this[Posts.repostId] = it.repostId
this[Posts.sensitive] = it.sensitive
this[Posts.apId] = it.apId
}
UsersFollowers.insert {
it[id] = 100L
it[userId] = userB
it[followerId] = userA
}
}
val actual = postService.findAll(userId = userA)
assertContentEquals(posts, actual)
}
}