diff --git a/src/main/kotlin/dev/usbharu/hideout/repository/PostRepositoryImpl.kt b/src/main/kotlin/dev/usbharu/hideout/repository/PostRepositoryImpl.kt index 86d01ad5..a2661b6b 100644 --- a/src/main/kotlin/dev/usbharu/hideout/repository/PostRepositoryImpl.kt +++ b/src/main/kotlin/dev/usbharu/hideout/repository/PostRepositoryImpl.kt @@ -1,7 +1,6 @@ package dev.usbharu.hideout.repository import dev.usbharu.hideout.domain.model.hideout.entity.Post -import dev.usbharu.hideout.repository.toPost import dev.usbharu.hideout.service.IdGenerateService import kotlinx.coroutines.Dispatchers import org.jetbrains.exposed.sql.* diff --git a/src/main/kotlin/dev/usbharu/hideout/routing/api/internal/v1/Posts.kt b/src/main/kotlin/dev/usbharu/hideout/routing/api/internal/v1/Posts.kt index 357eb131..ab5fb85b 100644 --- a/src/main/kotlin/dev/usbharu/hideout/routing/api/internal/v1/Posts.kt +++ b/src/main/kotlin/dev/usbharu/hideout/routing/api/internal/v1/Posts.kt @@ -2,24 +2,37 @@ package dev.usbharu.hideout.routing.api.internal.v1 import dev.usbharu.hideout.domain.model.hideout.dto.PostCreateDto 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.util.InstantParseUtil import io.ktor.server.application.* import io.ktor.server.auth.* import io.ktor.server.auth.jwt.* import io.ktor.server.request.* import io.ktor.server.routing.* -fun Route.posts(postService: IPostService){ - route("/posts"){ - authenticate(){ - post{ +fun Route.posts(postService: IPostService) { + route("/posts") { + authenticate(TOKEN_AUTH) { + post { val principal = call.principal() ?: throw RuntimeException("no principal") - val username = principal.payload.getClaim("username").asString() + val username = principal.payload.getClaim("uid").asString() val receive = call.receive() - val postCreateDto = PostCreateDto(receive.text,username) + val postCreateDto = PostCreateDto(receive.text, username) postService.create(postCreateDto) } } + authenticate(TOKEN_AUTH, optional = true) { + get { + val userId = call.principal()?.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) + } + } } } diff --git a/src/main/kotlin/dev/usbharu/hideout/service/IPostService.kt b/src/main/kotlin/dev/usbharu/hideout/service/IPostService.kt index 2e16bdae..bf64ac10 100644 --- a/src/main/kotlin/dev/usbharu/hideout/service/IPostService.kt +++ b/src/main/kotlin/dev/usbharu/hideout/service/IPostService.kt @@ -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.entity.Post +import java.time.Instant interface IPostService { suspend fun create(post: Post) 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 + + suspend fun findById(id: String): Post + suspend fun delete(id: String) } diff --git a/src/main/kotlin/dev/usbharu/hideout/service/JwtServiceImpl.kt b/src/main/kotlin/dev/usbharu/hideout/service/JwtServiceImpl.kt index e8519f2c..4dea9bb1 100644 --- a/src/main/kotlin/dev/usbharu/hideout/service/JwtServiceImpl.kt +++ b/src/main/kotlin/dev/usbharu/hideout/service/JwtServiceImpl.kt @@ -52,7 +52,7 @@ class JwtServiceImpl( .withAudience("${Config.configData.url}/users/${user.name}") .withIssuer(Config.configData.url) .withKeyId(keyId.await().toString()) - .withClaim("username", user.name) + .withClaim("uid", user.id) .withExpiresAt(now.plus(30, ChronoUnit.MINUTES)) .sign(Algorithm.RSA256(publicKey.await(), privateKey.await())) diff --git a/src/main/kotlin/dev/usbharu/hideout/service/impl/PostService.kt b/src/main/kotlin/dev/usbharu/hideout/service/impl/PostService.kt index fdb10359..8de2594d 100644 --- a/src/main/kotlin/dev/usbharu/hideout/service/impl/PostService.kt +++ b/src/main/kotlin/dev/usbharu/hideout/service/impl/PostService.kt @@ -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.entity.Post 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.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.slf4j.LoggerFactory import java.time.Instant @@ -34,4 +40,35 @@ class PostService( ) postRepository.save(postEntity) } + + override suspend fun findAll( + since: Instant?, + until: Instant?, + minId: Long?, + maxId: Long?, + limit: Int?, + userId: Long? + ): List { + 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") + } } diff --git a/src/main/kotlin/dev/usbharu/hideout/util/InstantParseUtil.kt b/src/main/kotlin/dev/usbharu/hideout/util/InstantParseUtil.kt new file mode 100644 index 00000000..66da1b60 --- /dev/null +++ b/src/main/kotlin/dev/usbharu/hideout/util/InstantParseUtil.kt @@ -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 + } + } + } +} diff --git a/src/test/kotlin/dev/usbharu/hideout/service/impl/PostServiceTest.kt b/src/test/kotlin/dev/usbharu/hideout/service/impl/PostServiceTest.kt new file mode 100644 index 00000000..8758bd3f --- /dev/null +++ b/src/test/kotlin/dev/usbharu/hideout/service/impl/PostServiceTest.kt @@ -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) + } +}