feat: 投稿のAPIを作成

This commit is contained in:
usbharu 2023-05-08 11:13:01 +09:00
parent e1f10ab064
commit 7fad9fcfa6
7 changed files with 248 additions and 8 deletions

View File

@ -1,7 +1,6 @@
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 dev.usbharu.hideout.repository.toPost
import dev.usbharu.hideout.service.IdGenerateService import dev.usbharu.hideout.service.IdGenerateService
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import org.jetbrains.exposed.sql.* import org.jetbrains.exposed.sql.*

View File

@ -2,7 +2,9 @@ package dev.usbharu.hideout.routing.api.internal.v1
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.form.Post import dev.usbharu.hideout.domain.model.hideout.form.Post
import dev.usbharu.hideout.plugins.TOKEN_AUTH
import dev.usbharu.hideout.service.IPostService import dev.usbharu.hideout.service.IPostService
import dev.usbharu.hideout.util.InstantParseUtil
import io.ktor.server.application.* import io.ktor.server.application.*
import io.ktor.server.auth.* import io.ktor.server.auth.*
import io.ktor.server.auth.jwt.* import io.ktor.server.auth.jwt.*
@ -11,15 +13,26 @@ import io.ktor.server.routing.*
fun Route.posts(postService: IPostService) { fun Route.posts(postService: IPostService) {
route("/posts") { route("/posts") {
authenticate(){ authenticate(TOKEN_AUTH) {
post { post {
val principal = call.principal<JWTPrincipal>() ?: throw RuntimeException("no principal") val principal = call.principal<JWTPrincipal>() ?: throw RuntimeException("no principal")
val username = principal.payload.getClaim("username").asString() val username = principal.payload.getClaim("uid").asString()
val receive = call.receive<Post>() val receive = call.receive<Post>()
val postCreateDto = PostCreateDto(receive.text, username) val postCreateDto = PostCreateDto(receive.text, username)
postService.create(postCreateDto) postService.create(postCreateDto)
} }
} }
authenticate(TOKEN_AUTH, optional = true) {
get {
val userId = call.principal<JWTPrincipal>()?.payload?.getClaim("uid")?.asLong()
val since = InstantParseUtil.parse(call.request.queryParameters["since"])
val until = InstantParseUtil.parse(call.request.queryParameters["until"])
val minId = call.request.queryParameters["minId"]?.toLong()
val maxId = call.request.queryParameters["maxId"]?.toLong()
val limit = call.request.queryParameters["limit"]?.toInt()
postService.findAll(since, until, minId, maxId, limit, userId)
}
}
} }
} }

View File

@ -2,8 +2,20 @@ package dev.usbharu.hideout.service
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
interface IPostService { interface IPostService {
suspend fun create(post: Post) suspend fun create(post: Post)
suspend fun create(post: PostCreateDto) suspend fun create(post: PostCreateDto)
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
suspend fun delete(id: String)
} }

View File

@ -52,7 +52,7 @@ class JwtServiceImpl(
.withAudience("${Config.configData.url}/users/${user.name}") .withAudience("${Config.configData.url}/users/${user.name}")
.withIssuer(Config.configData.url) .withIssuer(Config.configData.url)
.withKeyId(keyId.await().toString()) .withKeyId(keyId.await().toString())
.withClaim("username", user.name) .withClaim("uid", user.id)
.withExpiresAt(now.plus(30, ChronoUnit.MINUTES)) .withExpiresAt(now.plus(30, ChronoUnit.MINUTES))
.sign(Algorithm.RSA256(publicKey.await(), privateKey.await())) .sign(Algorithm.RSA256(publicKey.await(), privateKey.await()))

View File

@ -3,8 +3,14 @@ package dev.usbharu.hideout.service.impl
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 dev.usbharu.hideout.repository.IPostRepository 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.IPostService import dev.usbharu.hideout.service.IPostService
import dev.usbharu.hideout.service.activitypub.ActivityPubNoteService import dev.usbharu.hideout.service.activitypub.ActivityPubNoteService
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.koin.core.annotation.Single
import org.slf4j.LoggerFactory import org.slf4j.LoggerFactory
import java.time.Instant import java.time.Instant
@ -34,4 +40,35 @@ class PostService(
) )
postRepository.save(postEntity) postRepository.save(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(0)
}
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 delete(id: String) {
TODO("Not yet implemented")
}
} }

View File

@ -0,0 +1,18 @@
package dev.usbharu.hideout.util
import java.time.Instant
import java.time.format.DateTimeParseException
object InstantParseUtil {
fun parse(str: String?): Instant? {
return try {
Instant.ofEpochMilli(str?.toLong() ?: return null)
} catch (e: NumberFormatException) {
try {
Instant.parse(str)
} catch (e: DateTimeParseException) {
null
}
}
}
}

View File

@ -0,0 +1,161 @@
@file:OptIn(ExperimentalCoroutinesApi::class)
package dev.usbharu.hideout.service.impl
import dev.usbharu.hideout.domain.model.hideout.entity.Post
import dev.usbharu.hideout.repository.Posts
import dev.usbharu.hideout.repository.UsersFollowers
import dev.usbharu.hideout.service.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: Int = 0): Post {
return Post(
TwitterSnowflakeIdGenerateService.generateId(),
userId,
null,
text,
Instant.now().toEpochMilli(),
visibility,
"https://example.com"
)
}
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", 1),
createPost(userB, "good bay1", 1),
createPost(userB, "good bay2", 1),
createPost(userB, "good bay3", 1),
createPost(userB, "good bay4", 1),
createPost(userB, "good bay5", 1),
createPost(userB, "good bay6", 1),
)
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
this[Posts.url] = it.url
this[Posts.replyId] = it.replyId
this[Posts.repostId] = it.repostId
}
}
val expect = posts.filter { it.visibility == 0 }
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: Int = 0): Post {
return Post(
TwitterSnowflakeIdGenerateService.generateId(),
userId,
null,
text,
Instant.now().toEpochMilli(),
visibility,
"https://example.com"
)
}
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", 1),
createPost(userB, "good bay1", 1),
createPost(userB, "good bay2", 1),
createPost(userB, "good bay3", 1),
createPost(userB, "good bay4", 1),
createPost(userB, "good bay5", 1),
createPost(userB, "good bay6", 1),
)
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
this[Posts.url] = it.url
this[Posts.replyId] = it.replyId
this[Posts.repostId] = it.repostId
}
UsersFollowers.insert {
it[id] = 100L
it[userId] = userB
it[followerId] = userA
}
}
val actual = postService.findAll(userId = userA)
assertContentEquals(posts, actual)
}
}