From f06961b0bdbc7624f3f273658c56c00a4e547ba4 Mon Sep 17 00:00:00 2001 From: usbharu <64310155+usbharu@users.noreply.github.com> Date: Mon, 8 Jan 2024 17:06:09 +0900 Subject: [PATCH] =?UTF-8?q?feat:=20=E7=B5=B5=E6=96=87=E5=AD=97=E3=83=AA?= =?UTF-8?q?=E3=82=A2=E3=82=AF=E3=82=B7=E3=83=A7=E3=83=B3API=E3=82=92?= =?UTF-8?q?=E4=BF=AE=E6=AD=A3?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../kotlin/mastodon/status/StatusTest.kt | 72 ++++++++++++++++++- .../resources/sql/test-custom-emoji.sql | 3 + .../CustomEmojiRepositoryImpl.kt | 12 ++++ .../exposedquery/StatusQueryServiceImpl.kt | 17 ++++- .../status/MastodonStatusesApiContoller.kt | 10 ++- 5 files changed, 110 insertions(+), 4 deletions(-) create mode 100644 src/intTest/resources/sql/test-custom-emoji.sql diff --git a/src/intTest/kotlin/mastodon/status/StatusTest.kt b/src/intTest/kotlin/mastodon/status/StatusTest.kt index 54b61c9d..93785f56 100644 --- a/src/intTest/kotlin/mastodon/status/StatusTest.kt +++ b/src/intTest/kotlin/mastodon/status/StatusTest.kt @@ -1,7 +1,15 @@ package mastodon.status import dev.usbharu.hideout.SpringApplication +import dev.usbharu.hideout.core.domain.model.emoji.CustomEmoji +import dev.usbharu.hideout.core.domain.model.emoji.UnicodeEmoji +import dev.usbharu.hideout.core.infrastructure.exposedrepository.CustomEmojis +import dev.usbharu.hideout.core.infrastructure.exposedrepository.Reactions +import dev.usbharu.hideout.core.infrastructure.exposedrepository.toReaction +import org.assertj.core.api.Assertions.assertThat import org.flywaydb.core.Flyway +import org.jetbrains.exposed.sql.and +import org.jetbrains.exposed.sql.select import org.junit.jupiter.api.AfterAll import org.junit.jupiter.api.BeforeEach import org.junit.jupiter.api.Test @@ -13,19 +21,24 @@ import org.springframework.security.core.authority.SimpleGrantedAuthority import org.springframework.security.test.context.support.WithAnonymousUser import org.springframework.security.test.web.servlet.request.SecurityMockMvcRequestPostProcessors import org.springframework.security.test.web.servlet.request.SecurityMockMvcRequestPostProcessors.csrf +import org.springframework.security.test.web.servlet.request.SecurityMockMvcRequestPostProcessors.jwt import org.springframework.security.test.web.servlet.setup.SecurityMockMvcConfigurers import org.springframework.test.context.jdbc.Sql import org.springframework.test.web.servlet.MockMvc import org.springframework.test.web.servlet.post +import org.springframework.test.web.servlet.put import org.springframework.test.web.servlet.setup.DefaultMockMvcBuilder import org.springframework.test.web.servlet.setup.MockMvcBuilders import org.springframework.transaction.annotation.Transactional import org.springframework.web.context.WebApplicationContext +import java.time.Instant @SpringBootTest(classes = [SpringApplication::class]) @AutoConfigureMockMvc @Transactional @Sql("/sql/test-user.sql", executionPhase = Sql.ExecutionPhase.BEFORE_TEST_CLASS) +@Sql("/sql/test-post.sql", executionPhase = Sql.ExecutionPhase.BEFORE_TEST_CLASS) +@Sql("/sql/test-custom-emoji.sql", executionPhase = Sql.ExecutionPhase.BEFORE_TEST_CLASS) class StatusTest { @Autowired @@ -124,7 +137,6 @@ class StatusTest { } @Test - @Sql("/sql/test-post.sql") fun in_reply_to_idを指定したら返信として処理される() { mockMvc .post("/api/v1/statuses") { @@ -145,6 +157,64 @@ class StatusTest { .andExpect { jsonPath("\$.in_reply_to_id") { value("1") } } } + @Test + fun ユニコード絵文字をリアクションできる() { + mockMvc + .put("/api/v1/statuses/1/emoji_reactions/😭") { + with(jwt().jwt { it.claim("uid", "1") }.authorities(SimpleGrantedAuthority("SCOPE_write"))) + } + .andDo { print() } + .asyncDispatch() + .andExpect { status { isOk() } } + + val reaction = Reactions.select { Reactions.postId eq 1 and (Reactions.actorId eq 1) }.single().toReaction() + assertThat(reaction.emoji).isEqualTo(UnicodeEmoji("😭")) + assertThat(reaction.postId).isEqualTo(1) + assertThat(reaction.actorId).isEqualTo(1) + } + + @Test + fun 存在しない絵文字はフォールバックされる() { + mockMvc + .put("/api/v1/statuses/1/emoji_reactions/hoge") { + with(jwt().jwt { it.claim("uid", "1") }.authorities(SimpleGrantedAuthority("SCOPE_write"))) + } + .andDo { print() } + .asyncDispatch() + .andExpect { status { isOk() } } + + val reaction = Reactions.select { Reactions.postId eq 1 and (Reactions.actorId eq 1) }.single().toReaction() + assertThat(reaction.emoji).isEqualTo(UnicodeEmoji("❤")) + assertThat(reaction.postId).isEqualTo(1) + assertThat(reaction.actorId).isEqualTo(1) + } + + @Test + fun カスタム絵文字をリアクションできる() { + mockMvc + .put("/api/v1/statuses/1/emoji_reactions/kotlin") { + with(jwt().jwt { it.claim("uid", "1") }.authorities(SimpleGrantedAuthority("SCOPE_write"))) + } + .andDo { print() } + .asyncDispatch() + .andExpect { status { isOk() } } + + val reaction = + Reactions.leftJoin(CustomEmojis).select { Reactions.postId eq 1 and (Reactions.actorId eq 1) }.single() + .toReaction() + assertThat(reaction.emoji).isEqualTo( + CustomEmoji( + 1, + "kotlin", + "example.com", + null, + "https://example.com/emojis/kotlin", + null, + Instant.ofEpochMilli(1704700290036) + ) + ) + } + companion object { @JvmStatic @AfterAll diff --git a/src/intTest/resources/sql/test-custom-emoji.sql b/src/intTest/resources/sql/test-custom-emoji.sql new file mode 100644 index 00000000..4d70e3b4 --- /dev/null +++ b/src/intTest/resources/sql/test-custom-emoji.sql @@ -0,0 +1,3 @@ +insert into emojis(id, name, domain, instance_id, url, category, created_at) +VALUES (1, 'kotlin', 'example.com', null, 'https://example.com/emojis/kotlin', null, + TIMESTAMP '2024-01-08 16:51:30.036'); diff --git a/src/main/kotlin/dev/usbharu/hideout/core/infrastructure/exposedrepository/CustomEmojiRepositoryImpl.kt b/src/main/kotlin/dev/usbharu/hideout/core/infrastructure/exposedrepository/CustomEmojiRepositoryImpl.kt index 0df58975..07c735ff 100644 --- a/src/main/kotlin/dev/usbharu/hideout/core/infrastructure/exposedrepository/CustomEmojiRepositoryImpl.kt +++ b/src/main/kotlin/dev/usbharu/hideout/core/infrastructure/exposedrepository/CustomEmojiRepositoryImpl.kt @@ -74,6 +74,18 @@ fun ResultRow.toCustomEmoji(): CustomEmoji = CustomEmoji( this[CustomEmojis.createdAt] ) +fun ResultRow.toCustomEmojiOrNull(): CustomEmoji? { + return CustomEmoji( + id = this.getOrNull(CustomEmojis.id) ?: return null, + name = this.getOrNull(CustomEmojis.name) ?: return null, + domain = this.getOrNull(CustomEmojis.domain) ?: return null, + instanceId = this[CustomEmojis.instanceId], + url = this.getOrNull(CustomEmojis.url) ?: return null, + category = this[CustomEmojis.category], + createdAt = this.getOrNull(CustomEmojis.createdAt) ?: return null + ) +} + object CustomEmojis : Table("emojis") { val id = long("id") val name = varchar("name", 1000) diff --git a/src/main/kotlin/dev/usbharu/hideout/mastodon/infrastructure/exposedquery/StatusQueryServiceImpl.kt b/src/main/kotlin/dev/usbharu/hideout/mastodon/infrastructure/exposedquery/StatusQueryServiceImpl.kt index 40e92ee8..86509c08 100644 --- a/src/main/kotlin/dev/usbharu/hideout/mastodon/infrastructure/exposedquery/StatusQueryServiceImpl.kt +++ b/src/main/kotlin/dev/usbharu/hideout/mastodon/infrastructure/exposedquery/StatusQueryServiceImpl.kt @@ -109,7 +109,22 @@ class StatusQueryServiceImpl : StatusQueryService { } override suspend fun findByPostId(id: Long): Status { - TODO("Not yet implemented") + val map = Posts + .leftJoin(PostsMedia) + .leftJoin(Actors) + .leftJoin(Media) + .select { Posts.id eq id } + .groupBy { it[Posts.id] } + .map { it.value } + .map { + toStatus(it.first()).copy( + mediaAttachments = it.mapNotNull { resultRow -> + resultRow.toMediaOrNull()?.toMediaAttachments() + }, + emojis = it.mapNotNull { resultRow -> resultRow.toCustomEmojiOrNull()?.toMastodonEmoji() } + ) to it.first()[Posts.repostId] + } + return resolveReplyAndRepost(map).single() } private fun resolveReplyAndRepost(pairs: List>): List { diff --git a/src/main/kotlin/dev/usbharu/hideout/mastodon/interfaces/api/status/MastodonStatusesApiContoller.kt b/src/main/kotlin/dev/usbharu/hideout/mastodon/interfaces/api/status/MastodonStatusesApiContoller.kt index 705f94f7..6c4a6709 100644 --- a/src/main/kotlin/dev/usbharu/hideout/mastodon/interfaces/api/status/MastodonStatusesApiContoller.kt +++ b/src/main/kotlin/dev/usbharu/hideout/mastodon/interfaces/api/status/MastodonStatusesApiContoller.kt @@ -26,11 +26,17 @@ class MastodonStatusesApiContoller(private val statusesApiService: StatusesApiSe } override suspend fun apiV1StatusesIdEmojiReactionsEmojiDelete(id: String, emoji: String): ResponseEntity { - return super.apiV1StatusesIdEmojiReactionsEmojiDelete(id, emoji) + val uid = + (SecurityContextHolder.getContext().authentication.principal as Jwt).getClaim("uid").toLong() + + return ResponseEntity.ok(statusesApiService.removeEmojiReactions(id.toLong(), uid, emoji)) } override suspend fun apiV1StatusesIdEmojiReactionsEmojiPut(id: String, emoji: String): ResponseEntity { - return super.apiV1StatusesIdEmojiReactionsEmojiPut(id, emoji) + val uid = + (SecurityContextHolder.getContext().authentication.principal as Jwt).getClaim("uid").toLong() + + return ResponseEntity.ok(statusesApiService.emojiReactions(id.toLong(), uid, emoji)) } override suspend fun apiV1StatusesIdGet(id: String): ResponseEntity {