mirror of https://github.com/usbharu/Hideout.git
Merge pull request #231 from usbharu/feature/custom-emoji
Feature/custom emoji
This commit is contained in:
commit
c1e1a4d963
|
@ -142,6 +142,15 @@ repositories {
|
||||||
password = project.findProperty("gpr.key") as String? ?: System.getenv("TOKEN")
|
password = project.findProperty("gpr.key") as String? ?: System.getenv("TOKEN")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
maven {
|
||||||
|
name = "GitHubPackages2"
|
||||||
|
url = uri("https://maven.pkg.github.com/multim-dev/emoji-kt")
|
||||||
|
credentials {
|
||||||
|
|
||||||
|
username = project.findProperty("gpr.user") as String? ?: System.getenv("USERNAME")
|
||||||
|
password = project.findProperty("gpr.key") as String? ?: System.getenv("TOKEN")
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
kotlin {
|
kotlin {
|
||||||
|
@ -205,6 +214,8 @@ dependencies {
|
||||||
implementation("org.bytedeco:javacv-platform:1.5.9")
|
implementation("org.bytedeco:javacv-platform:1.5.9")
|
||||||
implementation("org.flywaydb:flyway-core")
|
implementation("org.flywaydb:flyway-core")
|
||||||
|
|
||||||
|
implementation("dev.usbharu:emoji-kt:2.0.0")
|
||||||
|
|
||||||
implementation("io.ktor:ktor-client-logging-jvm:$ktor_version")
|
implementation("io.ktor:ktor-client-logging-jvm:$ktor_version")
|
||||||
|
|
||||||
testImplementation("org.jetbrains.kotlin:kotlin-test-junit:$kotlin_version")
|
testImplementation("org.jetbrains.kotlin:kotlin-test-junit:$kotlin_version")
|
||||||
|
|
|
@ -112,18 +112,17 @@ class InboxCommonTest {
|
||||||
|
|
||||||
@BeforeAll
|
@BeforeAll
|
||||||
@JvmStatic
|
@JvmStatic
|
||||||
fun beforeAll(@Autowired flyway: Flyway) {
|
fun beforeAll() {
|
||||||
server = MockServer.feature("classpath:federation/InboxxCommonMockServerTest.feature").http(0).build()
|
server = MockServer.feature("classpath:federation/InboxxCommonMockServerTest.feature").http(0).build()
|
||||||
_remotePort = server.port.toString()
|
_remotePort = server.port.toString()
|
||||||
|
|
||||||
flyway.clean()
|
|
||||||
flyway.migrate()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@AfterAll
|
@AfterAll
|
||||||
@JvmStatic
|
@JvmStatic
|
||||||
fun afterAll() {
|
fun afterAll(@Autowired flyway: Flyway) {
|
||||||
server.stop()
|
server.stop()
|
||||||
|
flyway.clean()
|
||||||
|
flyway.migrate()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,7 +1,15 @@
|
||||||
package mastodon.status
|
package mastodon.status
|
||||||
|
|
||||||
import dev.usbharu.hideout.SpringApplication
|
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.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.AfterAll
|
||||||
import org.junit.jupiter.api.BeforeEach
|
import org.junit.jupiter.api.BeforeEach
|
||||||
import org.junit.jupiter.api.Test
|
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.context.support.WithAnonymousUser
|
||||||
import org.springframework.security.test.web.servlet.request.SecurityMockMvcRequestPostProcessors
|
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.csrf
|
||||||
|
import org.springframework.security.test.web.servlet.request.SecurityMockMvcRequestPostProcessors.jwt
|
||||||
import org.springframework.security.test.web.servlet.setup.SecurityMockMvcConfigurers
|
import org.springframework.security.test.web.servlet.setup.SecurityMockMvcConfigurers
|
||||||
import org.springframework.test.context.jdbc.Sql
|
import org.springframework.test.context.jdbc.Sql
|
||||||
import org.springframework.test.web.servlet.MockMvc
|
import org.springframework.test.web.servlet.MockMvc
|
||||||
import org.springframework.test.web.servlet.post
|
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.DefaultMockMvcBuilder
|
||||||
import org.springframework.test.web.servlet.setup.MockMvcBuilders
|
import org.springframework.test.web.servlet.setup.MockMvcBuilders
|
||||||
import org.springframework.transaction.annotation.Transactional
|
import org.springframework.transaction.annotation.Transactional
|
||||||
import org.springframework.web.context.WebApplicationContext
|
import org.springframework.web.context.WebApplicationContext
|
||||||
|
import java.time.Instant
|
||||||
|
|
||||||
@SpringBootTest(classes = [SpringApplication::class])
|
@SpringBootTest(classes = [SpringApplication::class])
|
||||||
@AutoConfigureMockMvc
|
@AutoConfigureMockMvc
|
||||||
@Transactional
|
@Transactional
|
||||||
@Sql("/sql/test-user.sql", executionPhase = Sql.ExecutionPhase.BEFORE_TEST_CLASS)
|
@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 {
|
class StatusTest {
|
||||||
|
|
||||||
@Autowired
|
@Autowired
|
||||||
|
@ -124,7 +137,6 @@ class StatusTest {
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
@Sql("/sql/test-post.sql")
|
|
||||||
fun in_reply_to_idを指定したら返信として処理される() {
|
fun in_reply_to_idを指定したら返信として処理される() {
|
||||||
mockMvc
|
mockMvc
|
||||||
.post("/api/v1/statuses") {
|
.post("/api/v1/statuses") {
|
||||||
|
@ -145,6 +157,64 @@ class StatusTest {
|
||||||
.andExpect { jsonPath("\$.in_reply_to_id") { value("1") } }
|
.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 {
|
companion object {
|
||||||
@JvmStatic
|
@JvmStatic
|
||||||
@AfterAll
|
@AfterAll
|
||||||
|
|
|
@ -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 07:51:30.036Z');
|
|
@ -1,6 +1,8 @@
|
||||||
package dev.usbharu.hideout.activitypub.domain.model
|
package dev.usbharu.hideout.activitypub.domain.model
|
||||||
|
|
||||||
|
import com.fasterxml.jackson.databind.annotation.JsonDeserialize
|
||||||
import dev.usbharu.hideout.activitypub.domain.model.objects.Object
|
import dev.usbharu.hideout.activitypub.domain.model.objects.Object
|
||||||
|
import dev.usbharu.hideout.activitypub.domain.model.objects.ObjectDeserializer
|
||||||
|
|
||||||
open class Note
|
open class Note
|
||||||
@Suppress("LongParameterList")
|
@Suppress("LongParameterList")
|
||||||
|
@ -14,7 +16,9 @@ constructor(
|
||||||
val cc: List<String> = emptyList(),
|
val cc: List<String> = emptyList(),
|
||||||
val sensitive: Boolean = false,
|
val sensitive: Boolean = false,
|
||||||
val inReplyTo: String? = null,
|
val inReplyTo: String? = null,
|
||||||
val attachment: List<Document> = emptyList()
|
val attachment: List<Document> = emptyList(),
|
||||||
|
@JsonDeserialize(contentUsing = ObjectDeserializer::class)
|
||||||
|
val tag: List<Object> = emptyList()
|
||||||
) : Object(
|
) : Object(
|
||||||
type = add(type, "Note")
|
type = add(type, "Note")
|
||||||
),
|
),
|
||||||
|
@ -36,6 +40,7 @@ constructor(
|
||||||
if (sensitive != other.sensitive) return false
|
if (sensitive != other.sensitive) return false
|
||||||
if (inReplyTo != other.inReplyTo) return false
|
if (inReplyTo != other.inReplyTo) return false
|
||||||
if (attachment != other.attachment) return false
|
if (attachment != other.attachment) return false
|
||||||
|
if (tag != other.tag) return false
|
||||||
|
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
@ -51,6 +56,7 @@ constructor(
|
||||||
result = 31 * result + sensitive.hashCode()
|
result = 31 * result + sensitive.hashCode()
|
||||||
result = 31 * result + (inReplyTo?.hashCode() ?: 0)
|
result = 31 * result + (inReplyTo?.hashCode() ?: 0)
|
||||||
result = 31 * result + attachment.hashCode()
|
result = 31 * result + attachment.hashCode()
|
||||||
|
result = 31 * result + tag.hashCode()
|
||||||
return result
|
return result
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -64,7 +70,8 @@ constructor(
|
||||||
"cc=$cc, " +
|
"cc=$cc, " +
|
||||||
"sensitive=$sensitive, " +
|
"sensitive=$sensitive, " +
|
||||||
"inReplyTo=$inReplyTo, " +
|
"inReplyTo=$inReplyTo, " +
|
||||||
"attachment=$attachment" +
|
"attachment=$attachment, " +
|
||||||
|
"tag=$tag" +
|
||||||
")" +
|
")" +
|
||||||
" ${super.toString()}"
|
" ${super.toString()}"
|
||||||
}
|
}
|
||||||
|
|
|
@ -9,7 +9,7 @@ import dev.usbharu.hideout.activitypub.service.common.ExtendedActivityVocabulary
|
||||||
|
|
||||||
class ObjectDeserializer : JsonDeserializer<Object>() {
|
class ObjectDeserializer : JsonDeserializer<Object>() {
|
||||||
@Suppress("LongMethod", "CyclomaticComplexMethod")
|
@Suppress("LongMethod", "CyclomaticComplexMethod")
|
||||||
override fun deserialize(p: JsonParser?, ctxt: DeserializationContext?): Object {
|
override fun deserialize(p: JsonParser?, ctxt: DeserializationContext?): Object? {
|
||||||
requireNotNull(p)
|
requireNotNull(p)
|
||||||
val treeNode: JsonNode = requireNotNull(p.codec?.readTree(p))
|
val treeNode: JsonNode = requireNotNull(p.codec?.readTree(p))
|
||||||
if (treeNode.isValueNode) {
|
if (treeNode.isValueNode) {
|
||||||
|
@ -24,70 +24,71 @@ class ObjectDeserializer : JsonDeserializer<Object>() {
|
||||||
ExtendedActivityVocabulary.values().firstOrNull { it.name.equals(jsonNode.asText(), true) }
|
ExtendedActivityVocabulary.values().firstOrNull { it.name.equals(jsonNode.asText(), true) }
|
||||||
}
|
}
|
||||||
} else if (type.isValueNode) {
|
} else if (type.isValueNode) {
|
||||||
ExtendedActivityVocabulary.values().first { it.name.equals(type.asText(), true) }
|
ExtendedActivityVocabulary.values().firstOrNull { it.name.equals(type.asText(), true) }
|
||||||
} else {
|
} else {
|
||||||
TODO()
|
null
|
||||||
}
|
}
|
||||||
|
|
||||||
return when (activityType) {
|
return when (activityType) {
|
||||||
ExtendedActivityVocabulary.Follow -> p.codec.treeToValue(treeNode, Follow::class.java)
|
ExtendedActivityVocabulary.Follow -> p.codec.treeToValue(treeNode, Follow::class.java)
|
||||||
ExtendedActivityVocabulary.Note -> p.codec.treeToValue(treeNode, Note::class.java)
|
ExtendedActivityVocabulary.Note -> p.codec.treeToValue(treeNode, Note::class.java)
|
||||||
ExtendedActivityVocabulary.Object -> p.codec.treeToValue(treeNode, Object::class.java)
|
ExtendedActivityVocabulary.Object -> p.codec.treeToValue(treeNode, Object::class.java)
|
||||||
ExtendedActivityVocabulary.Link -> TODO()
|
ExtendedActivityVocabulary.Link -> null
|
||||||
ExtendedActivityVocabulary.Activity -> TODO()
|
ExtendedActivityVocabulary.Activity -> null
|
||||||
ExtendedActivityVocabulary.IntransitiveActivity -> TODO()
|
ExtendedActivityVocabulary.IntransitiveActivity -> null
|
||||||
ExtendedActivityVocabulary.Collection -> TODO()
|
ExtendedActivityVocabulary.Collection -> null
|
||||||
ExtendedActivityVocabulary.OrderedCollection -> TODO()
|
ExtendedActivityVocabulary.OrderedCollection -> null
|
||||||
ExtendedActivityVocabulary.CollectionPage -> TODO()
|
ExtendedActivityVocabulary.CollectionPage -> null
|
||||||
ExtendedActivityVocabulary.OrderedCollectionPage -> TODO()
|
ExtendedActivityVocabulary.OrderedCollectionPage -> null
|
||||||
ExtendedActivityVocabulary.Accept -> p.codec.treeToValue(treeNode, Accept::class.java)
|
ExtendedActivityVocabulary.Accept -> p.codec.treeToValue(treeNode, Accept::class.java)
|
||||||
ExtendedActivityVocabulary.Add -> TODO()
|
ExtendedActivityVocabulary.Add -> null
|
||||||
ExtendedActivityVocabulary.Announce -> TODO()
|
ExtendedActivityVocabulary.Announce -> null
|
||||||
ExtendedActivityVocabulary.Arrive -> TODO()
|
ExtendedActivityVocabulary.Arrive -> null
|
||||||
ExtendedActivityVocabulary.Block -> p.codec.treeToValue(treeNode, Block::class.java)
|
ExtendedActivityVocabulary.Block -> p.codec.treeToValue(treeNode, Block::class.java)
|
||||||
ExtendedActivityVocabulary.Create -> p.codec.treeToValue(treeNode, Create::class.java)
|
ExtendedActivityVocabulary.Create -> p.codec.treeToValue(treeNode, Create::class.java)
|
||||||
ExtendedActivityVocabulary.Delete -> p.codec.treeToValue(treeNode, Delete::class.java)
|
ExtendedActivityVocabulary.Delete -> p.codec.treeToValue(treeNode, Delete::class.java)
|
||||||
ExtendedActivityVocabulary.Dislike -> TODO()
|
ExtendedActivityVocabulary.Dislike -> null
|
||||||
ExtendedActivityVocabulary.Flag -> TODO()
|
ExtendedActivityVocabulary.Flag -> null
|
||||||
ExtendedActivityVocabulary.Ignore -> TODO()
|
ExtendedActivityVocabulary.Ignore -> null
|
||||||
ExtendedActivityVocabulary.Invite -> TODO()
|
ExtendedActivityVocabulary.Invite -> null
|
||||||
ExtendedActivityVocabulary.Join -> TODO()
|
ExtendedActivityVocabulary.Join -> null
|
||||||
ExtendedActivityVocabulary.Leave -> TODO()
|
ExtendedActivityVocabulary.Leave -> null
|
||||||
ExtendedActivityVocabulary.Like -> p.codec.treeToValue(treeNode, Like::class.java)
|
ExtendedActivityVocabulary.Like -> p.codec.treeToValue(treeNode, Like::class.java)
|
||||||
ExtendedActivityVocabulary.Listen -> TODO()
|
ExtendedActivityVocabulary.Listen -> null
|
||||||
ExtendedActivityVocabulary.Move -> TODO()
|
ExtendedActivityVocabulary.Move -> null
|
||||||
ExtendedActivityVocabulary.Offer -> TODO()
|
ExtendedActivityVocabulary.Offer -> null
|
||||||
ExtendedActivityVocabulary.Question -> TODO()
|
ExtendedActivityVocabulary.Question -> null
|
||||||
ExtendedActivityVocabulary.Reject -> p.codec.treeToValue(treeNode, Reject::class.java)
|
ExtendedActivityVocabulary.Reject -> p.codec.treeToValue(treeNode, Reject::class.java)
|
||||||
ExtendedActivityVocabulary.Read -> TODO()
|
ExtendedActivityVocabulary.Read -> null
|
||||||
ExtendedActivityVocabulary.Remove -> TODO()
|
ExtendedActivityVocabulary.Remove -> null
|
||||||
ExtendedActivityVocabulary.TentativeReject -> TODO()
|
ExtendedActivityVocabulary.TentativeReject -> null
|
||||||
ExtendedActivityVocabulary.TentativeAccept -> TODO()
|
ExtendedActivityVocabulary.TentativeAccept -> null
|
||||||
ExtendedActivityVocabulary.Travel -> TODO()
|
ExtendedActivityVocabulary.Travel -> null
|
||||||
ExtendedActivityVocabulary.Undo -> p.codec.treeToValue(treeNode, Undo::class.java)
|
ExtendedActivityVocabulary.Undo -> p.codec.treeToValue(treeNode, Undo::class.java)
|
||||||
ExtendedActivityVocabulary.Update -> TODO()
|
ExtendedActivityVocabulary.Update -> null
|
||||||
ExtendedActivityVocabulary.View -> TODO()
|
ExtendedActivityVocabulary.View -> null
|
||||||
ExtendedActivityVocabulary.Application -> TODO()
|
ExtendedActivityVocabulary.Application -> null
|
||||||
ExtendedActivityVocabulary.Group -> TODO()
|
ExtendedActivityVocabulary.Group -> null
|
||||||
ExtendedActivityVocabulary.Organization -> TODO()
|
ExtendedActivityVocabulary.Organization -> null
|
||||||
ExtendedActivityVocabulary.Person -> p.codec.treeToValue(treeNode, Person::class.java)
|
ExtendedActivityVocabulary.Person -> p.codec.treeToValue(treeNode, Person::class.java)
|
||||||
ExtendedActivityVocabulary.Service -> TODO()
|
ExtendedActivityVocabulary.Service -> null
|
||||||
ExtendedActivityVocabulary.Article -> TODO()
|
ExtendedActivityVocabulary.Article -> null
|
||||||
ExtendedActivityVocabulary.Audio -> TODO()
|
ExtendedActivityVocabulary.Audio -> null
|
||||||
ExtendedActivityVocabulary.Document -> p.codec.treeToValue(treeNode, Document::class.java)
|
ExtendedActivityVocabulary.Document -> p.codec.treeToValue(treeNode, Document::class.java)
|
||||||
ExtendedActivityVocabulary.Event -> TODO()
|
ExtendedActivityVocabulary.Event -> null
|
||||||
ExtendedActivityVocabulary.Image -> p.codec.treeToValue(treeNode, Image::class.java)
|
ExtendedActivityVocabulary.Image -> p.codec.treeToValue(treeNode, Image::class.java)
|
||||||
ExtendedActivityVocabulary.Page -> TODO()
|
ExtendedActivityVocabulary.Page -> null
|
||||||
ExtendedActivityVocabulary.Place -> TODO()
|
ExtendedActivityVocabulary.Place -> null
|
||||||
ExtendedActivityVocabulary.Profile -> TODO()
|
ExtendedActivityVocabulary.Profile -> null
|
||||||
ExtendedActivityVocabulary.Relationship -> TODO()
|
ExtendedActivityVocabulary.Relationship -> null
|
||||||
ExtendedActivityVocabulary.Tombstone -> p.codec.treeToValue(treeNode, Tombstone::class.java)
|
ExtendedActivityVocabulary.Tombstone -> p.codec.treeToValue(treeNode, Tombstone::class.java)
|
||||||
ExtendedActivityVocabulary.Video -> TODO()
|
ExtendedActivityVocabulary.Video -> null
|
||||||
ExtendedActivityVocabulary.Mention -> TODO()
|
ExtendedActivityVocabulary.Mention -> null
|
||||||
ExtendedActivityVocabulary.Emoji -> p.codec.treeToValue(treeNode, Emoji::class.java)
|
ExtendedActivityVocabulary.Emoji -> p.codec.treeToValue(treeNode, Emoji::class.java)
|
||||||
|
null -> null
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
TODO()
|
return null
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,13 +1,16 @@
|
||||||
package dev.usbharu.hideout.activitypub.service.activity.like
|
package dev.usbharu.hideout.activitypub.service.activity.like
|
||||||
|
|
||||||
import dev.usbharu.hideout.activitypub.domain.exception.FailedToGetActivityPubResourceException
|
import dev.usbharu.hideout.activitypub.domain.exception.FailedToGetActivityPubResourceException
|
||||||
|
import dev.usbharu.hideout.activitypub.domain.model.Emoji
|
||||||
import dev.usbharu.hideout.activitypub.domain.model.Like
|
import dev.usbharu.hideout.activitypub.domain.model.Like
|
||||||
import dev.usbharu.hideout.activitypub.service.common.AbstractActivityPubProcessor
|
import dev.usbharu.hideout.activitypub.service.common.AbstractActivityPubProcessor
|
||||||
import dev.usbharu.hideout.activitypub.service.common.ActivityPubProcessContext
|
import dev.usbharu.hideout.activitypub.service.common.ActivityPubProcessContext
|
||||||
import dev.usbharu.hideout.activitypub.service.common.ActivityType
|
import dev.usbharu.hideout.activitypub.service.common.ActivityType
|
||||||
|
import dev.usbharu.hideout.activitypub.service.objects.emoji.EmojiService
|
||||||
import dev.usbharu.hideout.activitypub.service.objects.note.APNoteService
|
import dev.usbharu.hideout.activitypub.service.objects.note.APNoteService
|
||||||
import dev.usbharu.hideout.activitypub.service.objects.user.APUserService
|
import dev.usbharu.hideout.activitypub.service.objects.user.APUserService
|
||||||
import dev.usbharu.hideout.application.external.Transaction
|
import dev.usbharu.hideout.application.external.Transaction
|
||||||
|
import dev.usbharu.hideout.core.domain.model.emoji.UnicodeEmoji
|
||||||
import dev.usbharu.hideout.core.service.reaction.ReactionService
|
import dev.usbharu.hideout.core.service.reaction.ReactionService
|
||||||
import org.springframework.stereotype.Service
|
import org.springframework.stereotype.Service
|
||||||
|
|
||||||
|
@ -16,7 +19,8 @@ class APLikeProcessor(
|
||||||
transaction: Transaction,
|
transaction: Transaction,
|
||||||
private val apUserService: APUserService,
|
private val apUserService: APUserService,
|
||||||
private val apNoteService: APNoteService,
|
private val apNoteService: APNoteService,
|
||||||
private val reactionService: ReactionService
|
private val reactionService: ReactionService,
|
||||||
|
private val emojiService: EmojiService
|
||||||
) :
|
) :
|
||||||
AbstractActivityPubProcessor<Like>(transaction) {
|
AbstractActivityPubProcessor<Like>(transaction) {
|
||||||
override suspend fun internalProcess(activity: ActivityPubProcessContext<Like>) {
|
override suspend fun internalProcess(activity: ActivityPubProcessContext<Like>) {
|
||||||
|
@ -29,9 +33,16 @@ class APLikeProcessor(
|
||||||
|
|
||||||
try {
|
try {
|
||||||
val post = apNoteService.fetchNoteWithEntity(target).second
|
val post = apNoteService.fetchNoteWithEntity(target).second
|
||||||
|
|
||||||
|
val emoji = if (content.startsWith(":")) {
|
||||||
|
val tag = activity.activity.tag
|
||||||
|
(tag.firstOrNull { it is Emoji } as? Emoji)?.let { emojiService.fetchEmoji(it).second }
|
||||||
|
} else {
|
||||||
|
UnicodeEmoji(content)
|
||||||
|
}
|
||||||
|
|
||||||
reactionService.receiveReaction(
|
reactionService.receiveReaction(
|
||||||
content,
|
emoji ?: UnicodeEmoji("❤"),
|
||||||
actor.substringAfter("://").substringBefore("/"),
|
|
||||||
personWithEntity.second.id,
|
personWithEntity.second.id,
|
||||||
post.id
|
post.id
|
||||||
)
|
)
|
||||||
|
|
|
@ -83,8 +83,6 @@ class APResourceResolveServiceImpl(
|
||||||
return objects == other.objects
|
return objects == other.objects
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun hashCode(): Int {
|
override fun hashCode(): Int = objects.hashCode()
|
||||||
return objects.hashCode()
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -5,8 +5,10 @@ import dev.usbharu.hideout.activitypub.domain.exception.FailedProcessException
|
||||||
import dev.usbharu.hideout.activitypub.domain.exception.HttpSignatureUnauthorizedException
|
import dev.usbharu.hideout.activitypub.domain.exception.HttpSignatureUnauthorizedException
|
||||||
import dev.usbharu.hideout.activitypub.domain.model.objects.Object
|
import dev.usbharu.hideout.activitypub.domain.model.objects.Object
|
||||||
import dev.usbharu.hideout.application.external.Transaction
|
import dev.usbharu.hideout.application.external.Transaction
|
||||||
|
import dev.usbharu.hideout.core.domain.exception.resource.ResourceAccessException
|
||||||
import org.slf4j.Logger
|
import org.slf4j.Logger
|
||||||
import org.slf4j.LoggerFactory
|
import org.slf4j.LoggerFactory
|
||||||
|
import java.sql.SQLException
|
||||||
|
|
||||||
abstract class AbstractActivityPubProcessor<T : Object>(
|
abstract class AbstractActivityPubProcessor<T : Object>(
|
||||||
private val transaction: Transaction,
|
private val transaction: Transaction,
|
||||||
|
@ -21,7 +23,11 @@ abstract class AbstractActivityPubProcessor<T : Object>(
|
||||||
logger.info("START ActivityPub process. {}", this.type())
|
logger.info("START ActivityPub process. {}", this.type())
|
||||||
try {
|
try {
|
||||||
transaction.transaction {
|
transaction.transaction {
|
||||||
|
try {
|
||||||
internalProcess(activity)
|
internalProcess(activity)
|
||||||
|
} catch (e: ResourceAccessException) {
|
||||||
|
throw SQLException(e)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
} catch (e: ActivityPubProcessException) {
|
} catch (e: ActivityPubProcessException) {
|
||||||
logger.warn("FAILED ActivityPub process", e)
|
logger.warn("FAILED ActivityPub process", e)
|
||||||
|
|
|
@ -112,7 +112,7 @@ class InboxJobProcessor(
|
||||||
logger.debug("Is verifying success? {}", verify)
|
logger.debug("Is verifying success? {}", verify)
|
||||||
|
|
||||||
val activityPubProcessor =
|
val activityPubProcessor =
|
||||||
activityPubProcessorList.firstOrNull { it.isSupported(param.type) } as ActivityPubProcessor<Object>?
|
activityPubProcessorList.firstOrNull { it.isSupported(param.type) } as? ActivityPubProcessor<Object>
|
||||||
|
|
||||||
if (activityPubProcessor == null) {
|
if (activityPubProcessor == null) {
|
||||||
logger.warn("ActivityType {} is not support.", param.type)
|
logger.warn("ActivityType {} is not support.", param.type)
|
||||||
|
|
|
@ -0,0 +1,10 @@
|
||||||
|
package dev.usbharu.hideout.activitypub.service.objects.emoji
|
||||||
|
|
||||||
|
import dev.usbharu.hideout.activitypub.domain.model.Emoji
|
||||||
|
import dev.usbharu.hideout.core.domain.model.emoji.CustomEmoji
|
||||||
|
|
||||||
|
interface EmojiService {
|
||||||
|
suspend fun fetchEmoji(url: String): Pair<Emoji, CustomEmoji>
|
||||||
|
suspend fun fetchEmoji(emoji: Emoji): Pair<Emoji, CustomEmoji>
|
||||||
|
suspend fun findByEmojiName(emojiName: String): CustomEmoji?
|
||||||
|
}
|
|
@ -0,0 +1,79 @@
|
||||||
|
package dev.usbharu.hideout.activitypub.service.objects.emoji
|
||||||
|
|
||||||
|
import dev.usbharu.hideout.activitypub.domain.model.Emoji
|
||||||
|
import dev.usbharu.hideout.activitypub.service.common.APResourceResolveServiceImpl
|
||||||
|
import dev.usbharu.hideout.activitypub.service.common.resolve
|
||||||
|
import dev.usbharu.hideout.application.config.ApplicationConfig
|
||||||
|
import dev.usbharu.hideout.core.domain.model.emoji.CustomEmoji
|
||||||
|
import dev.usbharu.hideout.core.domain.model.emoji.CustomEmojiRepository
|
||||||
|
import dev.usbharu.hideout.core.service.instance.InstanceService
|
||||||
|
import dev.usbharu.hideout.core.service.media.MediaService
|
||||||
|
import dev.usbharu.hideout.core.service.media.RemoteMedia
|
||||||
|
import org.springframework.stereotype.Service
|
||||||
|
import java.net.URL
|
||||||
|
import java.time.Instant
|
||||||
|
|
||||||
|
@Service
|
||||||
|
class EmojiServiceImpl(
|
||||||
|
private val customEmojiRepository: CustomEmojiRepository,
|
||||||
|
private val instanceService: InstanceService,
|
||||||
|
private val mediaService: MediaService,
|
||||||
|
private val apResourceResolveServiceImpl: APResourceResolveServiceImpl,
|
||||||
|
private val applicationConfig: ApplicationConfig
|
||||||
|
) : EmojiService {
|
||||||
|
override suspend fun fetchEmoji(url: String): Pair<Emoji, CustomEmoji> {
|
||||||
|
val emoji = apResourceResolveServiceImpl.resolve<Emoji>(url, null as Long?)
|
||||||
|
return fetchEmoji(emoji)
|
||||||
|
}
|
||||||
|
|
||||||
|
override suspend fun fetchEmoji(emoji: Emoji): Pair<Emoji, CustomEmoji> = emoji to save(emoji)
|
||||||
|
|
||||||
|
private suspend fun save(emoji: Emoji): CustomEmoji {
|
||||||
|
val domain = URL(emoji.id).host
|
||||||
|
val name = emoji.name.trim(':')
|
||||||
|
val customEmoji = customEmojiRepository.findByNameAndDomain(name, domain)
|
||||||
|
|
||||||
|
if (customEmoji != null) {
|
||||||
|
return customEmoji
|
||||||
|
}
|
||||||
|
|
||||||
|
val instance = instanceService.fetchInstance(emoji.id)
|
||||||
|
|
||||||
|
val media = mediaService.uploadRemoteMedia(
|
||||||
|
RemoteMedia(
|
||||||
|
emoji.name,
|
||||||
|
emoji.icon.url,
|
||||||
|
emoji.icon.mediaType.orEmpty(),
|
||||||
|
null
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
val customEmoji1 = CustomEmoji(
|
||||||
|
id = customEmojiRepository.generateId(),
|
||||||
|
name = name,
|
||||||
|
domain = domain,
|
||||||
|
instanceId = instance.id,
|
||||||
|
url = media.url,
|
||||||
|
category = null,
|
||||||
|
createdAt = Instant.now()
|
||||||
|
)
|
||||||
|
|
||||||
|
return customEmojiRepository.save(customEmoji1)
|
||||||
|
}
|
||||||
|
|
||||||
|
override suspend fun findByEmojiName(emojiName: String): CustomEmoji? {
|
||||||
|
val split = emojiName.trim(':').split("@")
|
||||||
|
|
||||||
|
return when (split.size) {
|
||||||
|
1 -> {
|
||||||
|
customEmojiRepository.findByNameAndDomain(split.first(), applicationConfig.url.host)
|
||||||
|
}
|
||||||
|
|
||||||
|
2 -> {
|
||||||
|
customEmojiRepository.findByNameAndDomain(split.first(), split[1])
|
||||||
|
}
|
||||||
|
|
||||||
|
else -> throw IllegalArgumentException("Unknown Emoji Format. $emojiName")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,10 +1,12 @@
|
||||||
package dev.usbharu.hideout.activitypub.service.objects.note
|
package dev.usbharu.hideout.activitypub.service.objects.note
|
||||||
|
|
||||||
import dev.usbharu.hideout.activitypub.domain.exception.FailedToGetActivityPubResourceException
|
import dev.usbharu.hideout.activitypub.domain.exception.FailedToGetActivityPubResourceException
|
||||||
|
import dev.usbharu.hideout.activitypub.domain.model.Emoji
|
||||||
import dev.usbharu.hideout.activitypub.domain.model.Note
|
import dev.usbharu.hideout.activitypub.domain.model.Note
|
||||||
import dev.usbharu.hideout.activitypub.query.NoteQueryService
|
import dev.usbharu.hideout.activitypub.query.NoteQueryService
|
||||||
import dev.usbharu.hideout.activitypub.service.common.APResourceResolveService
|
import dev.usbharu.hideout.activitypub.service.common.APResourceResolveService
|
||||||
import dev.usbharu.hideout.activitypub.service.common.resolve
|
import dev.usbharu.hideout.activitypub.service.common.resolve
|
||||||
|
import dev.usbharu.hideout.activitypub.service.objects.emoji.EmojiService
|
||||||
import dev.usbharu.hideout.activitypub.service.objects.user.APUserService
|
import dev.usbharu.hideout.activitypub.service.objects.user.APUserService
|
||||||
import dev.usbharu.hideout.core.domain.model.post.Post
|
import dev.usbharu.hideout.core.domain.model.post.Post
|
||||||
import dev.usbharu.hideout.core.domain.model.post.PostRepository
|
import dev.usbharu.hideout.core.domain.model.post.PostRepository
|
||||||
|
@ -32,7 +34,8 @@ class APNoteServiceImpl(
|
||||||
private val apResourceResolveService: APResourceResolveService,
|
private val apResourceResolveService: APResourceResolveService,
|
||||||
private val postBuilder: Post.PostBuilder,
|
private val postBuilder: Post.PostBuilder,
|
||||||
private val noteQueryService: NoteQueryService,
|
private val noteQueryService: NoteQueryService,
|
||||||
private val mediaService: MediaService
|
private val mediaService: MediaService,
|
||||||
|
private val emojiService: EmojiService
|
||||||
|
|
||||||
) : APNoteService {
|
) : APNoteService {
|
||||||
|
|
||||||
|
@ -101,6 +104,15 @@ class APNoteServiceImpl(
|
||||||
postRepository.findByUrl(it)
|
postRepository.findByUrl(it)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
val emojis = note.tag
|
||||||
|
.filterIsInstance<Emoji>()
|
||||||
|
.map {
|
||||||
|
emojiService.fetchEmoji(it).second
|
||||||
|
}
|
||||||
|
.map {
|
||||||
|
it.id
|
||||||
|
}
|
||||||
|
|
||||||
val mediaList = note.attachment.map {
|
val mediaList = note.attachment.map {
|
||||||
mediaService.uploadRemoteMedia(
|
mediaService.uploadRemoteMedia(
|
||||||
RemoteMedia(
|
RemoteMedia(
|
||||||
|
@ -123,7 +135,8 @@ class APNoteServiceImpl(
|
||||||
replyId = reply?.id,
|
replyId = reply?.id,
|
||||||
sensitive = note.sensitive,
|
sensitive = note.sensitive,
|
||||||
apId = note.id,
|
apId = note.id,
|
||||||
mediaIds = mediaList
|
mediaIds = mediaList,
|
||||||
|
emojiIds = emojis
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
return note to createRemote
|
return note to createRemote
|
||||||
|
|
|
@ -13,12 +13,6 @@ import java.sql.Connection
|
||||||
@Service
|
@Service
|
||||||
class ExposedTransaction : Transaction {
|
class ExposedTransaction : Transaction {
|
||||||
override suspend fun <T> transaction(block: suspend () -> T): T {
|
override suspend fun <T> transaction(block: suspend () -> T): T {
|
||||||
// return newSuspendedTransaction(MDCContext(), transactionIsolation = java.sql.Connection.TRANSACTION_READ_COMMITTED) {
|
|
||||||
// warnLongQueriesDuration = 1000
|
|
||||||
// addLogger(Slf4jSqlDebugLogger)
|
|
||||||
// block()
|
|
||||||
// }
|
|
||||||
|
|
||||||
return transaction(transactionIsolation = Connection.TRANSACTION_READ_COMMITTED) {
|
return transaction(transactionIsolation = Connection.TRANSACTION_READ_COMMITTED) {
|
||||||
debug = true
|
debug = true
|
||||||
warnLongQueriesDuration = 1000
|
warnLongQueriesDuration = 1000
|
||||||
|
|
|
@ -26,7 +26,8 @@ data class Actor private constructor(
|
||||||
val followersCount: Int = 0,
|
val followersCount: Int = 0,
|
||||||
val followingCount: Int = 0,
|
val followingCount: Int = 0,
|
||||||
val postsCount: Int = 0,
|
val postsCount: Int = 0,
|
||||||
val lastPostDate: Instant? = null
|
val lastPostDate: Instant? = null,
|
||||||
|
val emojis: List<Long> = emptyList()
|
||||||
) {
|
) {
|
||||||
|
|
||||||
@Component
|
@Component
|
||||||
|
@ -55,7 +56,8 @@ data class Actor private constructor(
|
||||||
followersCount: Int = 0,
|
followersCount: Int = 0,
|
||||||
followingCount: Int = 0,
|
followingCount: Int = 0,
|
||||||
postsCount: Int = 0,
|
postsCount: Int = 0,
|
||||||
lastPostDate: Instant? = null
|
lastPostDate: Instant? = null,
|
||||||
|
emojis: List<Long> = emptyList()
|
||||||
): Actor {
|
): Actor {
|
||||||
if (id == 0L) {
|
if (id == 0L) {
|
||||||
return Actor(
|
return Actor(
|
||||||
|
@ -78,7 +80,8 @@ data class Actor private constructor(
|
||||||
followersCount = followersCount,
|
followersCount = followersCount,
|
||||||
followingCount = followingCount,
|
followingCount = followingCount,
|
||||||
postsCount = postsCount,
|
postsCount = postsCount,
|
||||||
lastPostDate = lastPostDate
|
lastPostDate = lastPostDate,
|
||||||
|
emojis = emojis
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -188,7 +191,8 @@ data class Actor private constructor(
|
||||||
followersCount = followersCount,
|
followersCount = followersCount,
|
||||||
followingCount = followingCount,
|
followingCount = followingCount,
|
||||||
postsCount = postsCount,
|
postsCount = postsCount,
|
||||||
lastPostDate = lastPostDate
|
lastPostDate = lastPostDate,
|
||||||
|
emojis = emojis
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -206,7 +210,6 @@ data class Actor private constructor(
|
||||||
fun decrementPostsCount(): Actor = this.copy(postsCount = this.postsCount - 1)
|
fun decrementPostsCount(): Actor = this.copy(postsCount = this.postsCount - 1)
|
||||||
|
|
||||||
fun withLastPostAt(lastPostDate: Instant): Actor = this.copy(lastPostDate = lastPostDate)
|
fun withLastPostAt(lastPostDate: Instant): Actor = this.copy(lastPostDate = lastPostDate)
|
||||||
|
|
||||||
override fun toString(): String {
|
override fun toString(): String {
|
||||||
return "Actor(" +
|
return "Actor(" +
|
||||||
"id=$id, " +
|
"id=$id, " +
|
||||||
|
@ -228,7 +231,8 @@ data class Actor private constructor(
|
||||||
"followersCount=$followersCount, " +
|
"followersCount=$followersCount, " +
|
||||||
"followingCount=$followingCount, " +
|
"followingCount=$followingCount, " +
|
||||||
"postsCount=$postsCount, " +
|
"postsCount=$postsCount, " +
|
||||||
"lastPostDate=$lastPostDate" +
|
"lastPostDate=$lastPostDate, " +
|
||||||
|
"emojis=$emojis" +
|
||||||
")"
|
")"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,36 @@
|
||||||
|
package dev.usbharu.hideout.core.domain.model.emoji
|
||||||
|
|
||||||
|
import java.time.Instant
|
||||||
|
|
||||||
|
sealed class Emoji {
|
||||||
|
abstract val domain: String
|
||||||
|
abstract val name: String
|
||||||
|
|
||||||
|
@Suppress("FunctionMinLength")
|
||||||
|
abstract fun id(): String
|
||||||
|
override fun toString(): String {
|
||||||
|
return "Emoji(" +
|
||||||
|
"domain='$domain', " +
|
||||||
|
"name='$name'" +
|
||||||
|
")"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
data class CustomEmoji(
|
||||||
|
val id: Long,
|
||||||
|
override val name: String,
|
||||||
|
override val domain: String,
|
||||||
|
val instanceId: Long?,
|
||||||
|
val url: String,
|
||||||
|
val category: String?,
|
||||||
|
val createdAt: Instant
|
||||||
|
) : Emoji() {
|
||||||
|
override fun id(): String = id.toString()
|
||||||
|
}
|
||||||
|
|
||||||
|
data class UnicodeEmoji(
|
||||||
|
override val name: String
|
||||||
|
) : Emoji() {
|
||||||
|
override val domain: String = "unicode.org"
|
||||||
|
override fun id(): String = name
|
||||||
|
}
|
|
@ -0,0 +1,9 @@
|
||||||
|
package dev.usbharu.hideout.core.domain.model.emoji
|
||||||
|
|
||||||
|
interface CustomEmojiRepository {
|
||||||
|
suspend fun generateId(): Long
|
||||||
|
suspend fun save(customEmoji: CustomEmoji): CustomEmoji
|
||||||
|
suspend fun findById(id: Long): CustomEmoji?
|
||||||
|
suspend fun delete(customEmoji: CustomEmoji)
|
||||||
|
suspend fun findByNameAndDomain(name: String, domain: String): CustomEmoji?
|
||||||
|
}
|
|
@ -12,9 +12,7 @@ class Nodeinfo private constructor() {
|
||||||
return links == other.links
|
return links == other.links
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun hashCode(): Int {
|
override fun hashCode(): Int = links.hashCode()
|
||||||
return links.hashCode()
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
class Links private constructor() {
|
class Links private constructor() {
|
||||||
|
|
|
@ -17,7 +17,8 @@ data class Post private constructor(
|
||||||
val sensitive: Boolean = false,
|
val sensitive: Boolean = false,
|
||||||
val apId: String = url,
|
val apId: String = url,
|
||||||
val mediaIds: List<Long> = emptyList(),
|
val mediaIds: List<Long> = emptyList(),
|
||||||
val delted: Boolean = false
|
val delted: Boolean = false,
|
||||||
|
val emojiIds: List<Long> = emptyList()
|
||||||
) {
|
) {
|
||||||
|
|
||||||
@Component
|
@Component
|
||||||
|
@ -35,7 +36,8 @@ data class Post private constructor(
|
||||||
replyId: Long? = null,
|
replyId: Long? = null,
|
||||||
sensitive: Boolean = false,
|
sensitive: Boolean = false,
|
||||||
apId: String = url,
|
apId: String = url,
|
||||||
mediaIds: List<Long> = emptyList()
|
mediaIds: List<Long> = emptyList(),
|
||||||
|
emojiIds: List<Long> = emptyList()
|
||||||
): Post {
|
): Post {
|
||||||
require(id >= 0) { "id must be greater than or equal to 0." }
|
require(id >= 0) { "id must be greater than or equal to 0." }
|
||||||
|
|
||||||
|
@ -74,7 +76,8 @@ data class Post private constructor(
|
||||||
sensitive = sensitive,
|
sensitive = sensitive,
|
||||||
apId = apId,
|
apId = apId,
|
||||||
mediaIds = mediaIds,
|
mediaIds = mediaIds,
|
||||||
delted = false
|
delted = false,
|
||||||
|
emojiIds = emojiIds
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -1,3 +1,5 @@
|
||||||
package dev.usbharu.hideout.core.domain.model.reaction
|
package dev.usbharu.hideout.core.domain.model.reaction
|
||||||
|
|
||||||
data class Reaction(val id: Long, val emojiId: Long, val postId: Long, val actorId: Long)
|
import dev.usbharu.hideout.core.domain.model.emoji.Emoji
|
||||||
|
|
||||||
|
data class Reaction(val id: Long, val emoji: Emoji, val postId: Long, val actorId: Long)
|
||||||
|
|
|
@ -1,17 +1,23 @@
|
||||||
package dev.usbharu.hideout.core.domain.model.reaction
|
package dev.usbharu.hideout.core.domain.model.reaction
|
||||||
|
|
||||||
|
import dev.usbharu.hideout.core.domain.model.emoji.Emoji
|
||||||
import org.springframework.stereotype.Repository
|
import org.springframework.stereotype.Repository
|
||||||
|
|
||||||
@Repository
|
@Repository
|
||||||
@Suppress("FunctionMaxLength")
|
@Suppress("FunctionMaxLength", "TooManyFunction")
|
||||||
interface ReactionRepository {
|
interface ReactionRepository {
|
||||||
suspend fun generateId(): Long
|
suspend fun generateId(): Long
|
||||||
suspend fun save(reaction: Reaction): Reaction
|
suspend fun save(reaction: Reaction): Reaction
|
||||||
suspend fun delete(reaction: Reaction): Reaction
|
suspend fun delete(reaction: Reaction): Reaction
|
||||||
suspend fun deleteByPostId(postId: Long): Int
|
suspend fun deleteByPostId(postId: Long): Int
|
||||||
suspend fun deleteByActorId(actorId: Long): Int
|
suspend fun deleteByActorId(actorId: Long): Int
|
||||||
|
suspend fun deleteByPostIdAndActorId(postId: Long, actorId: Long)
|
||||||
|
suspend fun deleteByPostIdAndActorIdAndEmoji(postId: Long, actorId: Long, emoji: Emoji)
|
||||||
suspend fun findByPostId(postId: Long): List<Reaction>
|
suspend fun findByPostId(postId: Long): List<Reaction>
|
||||||
suspend fun findByPostIdAndActorIdAndEmojiId(postId: Long, actorId: Long, emojiId: Long): Reaction?
|
suspend fun findByPostIdAndActorIdAndEmojiId(postId: Long, actorId: Long, emojiId: Long): Reaction?
|
||||||
suspend fun existByPostIdAndActorIdAndEmojiId(postId: Long, actorId: Long, emojiId: Long): Boolean
|
suspend fun existByPostIdAndActorIdAndEmojiId(postId: Long, actorId: Long, emojiId: Long): Boolean
|
||||||
|
suspend fun existByPostIdAndActorIdAndUnicodeEmoji(postId: Long, actorId: Long, unicodeEmoji: String): Boolean
|
||||||
|
suspend fun existByPostIdAndActorIdAndEmoji(postId: Long, actorId: Long, emoji: Emoji): Boolean
|
||||||
|
suspend fun existByPostIdAndActor(postId: Long, actorId: Long): Boolean
|
||||||
suspend fun findByPostIdAndActorId(postId: Long, actorId: Long): List<Reaction>
|
suspend fun findByPostIdAndActorId(postId: Long, actorId: Long): List<Reaction>
|
||||||
}
|
}
|
||||||
|
|
|
@ -11,6 +11,9 @@ import org.springframework.stereotype.Service
|
||||||
|
|
||||||
@Service
|
@Service
|
||||||
class RelationshipRepositoryImpl : RelationshipRepository, AbstractRepository() {
|
class RelationshipRepositoryImpl : RelationshipRepository, AbstractRepository() {
|
||||||
|
override val logger: Logger
|
||||||
|
get() = Companion.logger
|
||||||
|
|
||||||
override suspend fun save(relationship: Relationship): Relationship = query {
|
override suspend fun save(relationship: Relationship): Relationship = query {
|
||||||
val singleOrNull = Relationships.select {
|
val singleOrNull = Relationships.select {
|
||||||
(Relationships.actorId eq relationship.actorId).and(
|
(Relationships.actorId eq relationship.actorId).and(
|
||||||
|
@ -94,9 +97,6 @@ class RelationshipRepositoryImpl : RelationshipRepository, AbstractRepository()
|
||||||
return@query query.map { it.toRelationships() }
|
return@query query.map { it.toRelationships() }
|
||||||
}
|
}
|
||||||
|
|
||||||
override val logger: Logger
|
|
||||||
get() = Companion.logger
|
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
private val logger = LoggerFactory.getLogger(RelationshipRepositoryImpl::class.java)
|
private val logger = LoggerFactory.getLogger(RelationshipRepositoryImpl::class.java)
|
||||||
}
|
}
|
||||||
|
|
|
@ -21,5 +21,6 @@ data class Timeline(
|
||||||
val sensitive: Boolean,
|
val sensitive: Boolean,
|
||||||
val isLocal: Boolean,
|
val isLocal: Boolean,
|
||||||
val isPureRepost: Boolean = false,
|
val isPureRepost: Boolean = false,
|
||||||
val mediaIds: List<Long> = emptyList()
|
val mediaIds: List<Long> = emptyList(),
|
||||||
|
val emojiIds: List<Long> = emptyList()
|
||||||
)
|
)
|
||||||
|
|
|
@ -4,6 +4,7 @@ import dev.usbharu.hideout.application.infrastructure.exposed.QueryMapper
|
||||||
import dev.usbharu.hideout.application.infrastructure.exposed.ResultRowMapper
|
import dev.usbharu.hideout.application.infrastructure.exposed.ResultRowMapper
|
||||||
import dev.usbharu.hideout.core.domain.model.post.Post
|
import dev.usbharu.hideout.core.domain.model.post.Post
|
||||||
import dev.usbharu.hideout.core.infrastructure.exposedrepository.Posts
|
import dev.usbharu.hideout.core.infrastructure.exposedrepository.Posts
|
||||||
|
import dev.usbharu.hideout.core.infrastructure.exposedrepository.PostsEmojis
|
||||||
import dev.usbharu.hideout.core.infrastructure.exposedrepository.PostsMedia
|
import dev.usbharu.hideout.core.infrastructure.exposedrepository.PostsMedia
|
||||||
import org.jetbrains.exposed.sql.Query
|
import org.jetbrains.exposed.sql.Query
|
||||||
import org.springframework.stereotype.Component
|
import org.springframework.stereotype.Component
|
||||||
|
@ -15,7 +16,10 @@ class PostQueryMapper(private val postResultRowMapper: ResultRowMapper<Post>) :
|
||||||
.map { it.value }
|
.map { it.value }
|
||||||
.map {
|
.map {
|
||||||
it.first().let(postResultRowMapper::map)
|
it.first().let(postResultRowMapper::map)
|
||||||
.copy(mediaIds = it.mapNotNull { resultRow -> resultRow.getOrNull(PostsMedia.mediaId) })
|
.copy(
|
||||||
|
mediaIds = it.mapNotNull { resultRow -> resultRow.getOrNull(PostsMedia.mediaId) },
|
||||||
|
emojiIds = it.mapNotNull { resultRow -> resultRow.getOrNull(PostsEmojis.emojiId) }
|
||||||
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -31,6 +31,7 @@ class UserResultRowMapper(private val actorBuilder: Actor.UserBuilder) : ResultR
|
||||||
followersCount = resultRow[Actors.followersCount],
|
followersCount = resultRow[Actors.followersCount],
|
||||||
postsCount = resultRow[Actors.postsCount],
|
postsCount = resultRow[Actors.postsCount],
|
||||||
lastPostDate = resultRow[Actors.lastPostAt],
|
lastPostDate = resultRow[Actors.lastPostAt],
|
||||||
|
emojis = resultRow[Actors.emojis].split(",").filter { it.isNotEmpty() }.map { it.toLong() }
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,6 +1,7 @@
|
||||||
package dev.usbharu.hideout.core.infrastructure.exposedrepository
|
package dev.usbharu.hideout.core.infrastructure.exposedrepository
|
||||||
|
|
||||||
import dev.usbharu.hideout.core.domain.exception.SpringDataAccessExceptionSQLExceptionTranslator
|
import dev.usbharu.hideout.core.domain.exception.SpringDataAccessExceptionSQLExceptionTranslator
|
||||||
|
import org.jetbrains.exposed.sql.transactions.TransactionManager
|
||||||
import org.slf4j.Logger
|
import org.slf4j.Logger
|
||||||
import org.springframework.beans.factory.annotation.Value
|
import org.springframework.beans.factory.annotation.Value
|
||||||
import org.springframework.jdbc.support.SQLErrorCodeSQLExceptionTranslator
|
import org.springframework.jdbc.support.SQLErrorCodeSQLExceptionTranslator
|
||||||
|
@ -37,6 +38,7 @@ ${Throwable().stackTrace.joinToString("\n")}
|
||||||
if (traceQueryException) {
|
if (traceQueryException) {
|
||||||
logger.trace("FAILED EXECUTE SQL", e)
|
logger.trace("FAILED EXECUTE SQL", e)
|
||||||
}
|
}
|
||||||
|
TransactionManager.currentOrNull()?.rollback()
|
||||||
if (e.cause !is SQLException) {
|
if (e.cause !is SQLException) {
|
||||||
throw e
|
throw e
|
||||||
}
|
}
|
||||||
|
|
|
@ -45,6 +45,7 @@ class ActorRepositoryImpl(
|
||||||
it[followingCount] = actor.followingCount
|
it[followingCount] = actor.followingCount
|
||||||
it[postsCount] = actor.postsCount
|
it[postsCount] = actor.postsCount
|
||||||
it[lastPostAt] = actor.lastPostDate
|
it[lastPostAt] = actor.lastPostDate
|
||||||
|
it[emojis] = actor.emojis.joinToString(",")
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
Actors.update({ Actors.id eq actor.id }) {
|
Actors.update({ Actors.id eq actor.id }) {
|
||||||
|
@ -67,6 +68,7 @@ class ActorRepositoryImpl(
|
||||||
it[followingCount] = actor.followingCount
|
it[followingCount] = actor.followingCount
|
||||||
it[postsCount] = actor.postsCount
|
it[postsCount] = actor.postsCount
|
||||||
it[lastPostAt] = actor.lastPostDate
|
it[lastPostAt] = actor.lastPostDate
|
||||||
|
it[emojis] = actor.emojis.joinToString(",")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return@query actor
|
return@query actor
|
||||||
|
@ -152,7 +154,7 @@ object Actors : Table("actors") {
|
||||||
val followersCount = integer("followers_count")
|
val followersCount = integer("followers_count")
|
||||||
val postsCount = integer("posts_count")
|
val postsCount = integer("posts_count")
|
||||||
val lastPostAt = timestamp("last_post_at").nullable()
|
val lastPostAt = timestamp("last_post_at").nullable()
|
||||||
|
val emojis = varchar("emojis", 3000)
|
||||||
override val primaryKey: PrimaryKey = PrimaryKey(id)
|
override val primaryKey: PrimaryKey = PrimaryKey(id)
|
||||||
|
|
||||||
init {
|
init {
|
||||||
|
|
|
@ -0,0 +1,103 @@
|
||||||
|
package dev.usbharu.hideout.core.infrastructure.exposedrepository
|
||||||
|
|
||||||
|
import dev.usbharu.hideout.application.service.id.IdGenerateService
|
||||||
|
import dev.usbharu.hideout.core.domain.model.emoji.CustomEmoji
|
||||||
|
import dev.usbharu.hideout.core.domain.model.emoji.CustomEmojiRepository
|
||||||
|
import org.jetbrains.exposed.sql.*
|
||||||
|
import org.jetbrains.exposed.sql.SqlExpressionBuilder.eq
|
||||||
|
import org.jetbrains.exposed.sql.javatime.CurrentTimestamp
|
||||||
|
import org.jetbrains.exposed.sql.javatime.timestamp
|
||||||
|
import org.slf4j.Logger
|
||||||
|
import org.slf4j.LoggerFactory
|
||||||
|
import org.springframework.stereotype.Repository
|
||||||
|
|
||||||
|
@Repository
|
||||||
|
class CustomEmojiRepositoryImpl(private val idGenerateService: IdGenerateService) : CustomEmojiRepository,
|
||||||
|
AbstractRepository() {
|
||||||
|
override val logger: Logger
|
||||||
|
get() = Companion.logger
|
||||||
|
|
||||||
|
override suspend fun generateId(): Long = idGenerateService.generateId()
|
||||||
|
|
||||||
|
override suspend fun save(customEmoji: CustomEmoji): CustomEmoji = query {
|
||||||
|
val singleOrNull = CustomEmojis.select { CustomEmojis.id eq customEmoji.id }.forUpdate().singleOrNull()
|
||||||
|
if (singleOrNull == null) {
|
||||||
|
CustomEmojis.insert {
|
||||||
|
it[CustomEmojis.id] = customEmoji.id
|
||||||
|
it[CustomEmojis.name] = customEmoji.name
|
||||||
|
it[CustomEmojis.domain] = customEmoji.domain
|
||||||
|
it[CustomEmojis.instanceId] = customEmoji.instanceId
|
||||||
|
it[CustomEmojis.url] = customEmoji.url
|
||||||
|
it[CustomEmojis.category] = customEmoji.category
|
||||||
|
it[CustomEmojis.createdAt] = customEmoji.createdAt
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
CustomEmojis.update({ CustomEmojis.id eq customEmoji.id }) {
|
||||||
|
it[CustomEmojis.name] = customEmoji.name
|
||||||
|
it[CustomEmojis.domain] = customEmoji.domain
|
||||||
|
it[CustomEmojis.instanceId] = customEmoji.instanceId
|
||||||
|
it[CustomEmojis.url] = customEmoji.url
|
||||||
|
it[CustomEmojis.category] = customEmoji.category
|
||||||
|
it[CustomEmojis.createdAt] = customEmoji.createdAt
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return@query customEmoji
|
||||||
|
}
|
||||||
|
|
||||||
|
override suspend fun findById(id: Long): CustomEmoji? = query {
|
||||||
|
return@query CustomEmojis.select { CustomEmojis.id eq id }.singleOrNull()?.toCustomEmoji()
|
||||||
|
}
|
||||||
|
|
||||||
|
override suspend fun delete(customEmoji: CustomEmoji): Unit = query {
|
||||||
|
CustomEmojis.deleteWhere { CustomEmojis.id eq customEmoji.id }
|
||||||
|
}
|
||||||
|
|
||||||
|
override suspend fun findByNameAndDomain(name: String, domain: String): CustomEmoji? = query {
|
||||||
|
return@query CustomEmojis
|
||||||
|
.select { CustomEmojis.name eq name and (CustomEmojis.domain eq domain) }
|
||||||
|
.singleOrNull()
|
||||||
|
?.toCustomEmoji()
|
||||||
|
}
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
private val logger = LoggerFactory.getLogger(CustomEmojiRepositoryImpl::class.java)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun ResultRow.toCustomEmoji(): CustomEmoji = CustomEmoji(
|
||||||
|
id = this[CustomEmojis.id],
|
||||||
|
name = this[CustomEmojis.name],
|
||||||
|
domain = this[CustomEmojis.domain],
|
||||||
|
instanceId = this[CustomEmojis.instanceId],
|
||||||
|
url = this[CustomEmojis.url],
|
||||||
|
category = this[CustomEmojis.category],
|
||||||
|
createdAt = 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)
|
||||||
|
val domain = varchar("domain", 1000)
|
||||||
|
val instanceId = long("instance_id").references(Instance.id).nullable()
|
||||||
|
val url = varchar("url", 255).uniqueIndex()
|
||||||
|
val category = varchar("category", 255).nullable()
|
||||||
|
val createdAt = timestamp("created_at").defaultExpression(CurrentTimestamp())
|
||||||
|
|
||||||
|
override val primaryKey: PrimaryKey = PrimaryKey(id)
|
||||||
|
|
||||||
|
init {
|
||||||
|
uniqueIndex(name, instanceId)
|
||||||
|
}
|
||||||
|
}
|
|
@ -37,6 +37,7 @@ class ExposedTimelineRepository(private val idGenerateService: IdGenerateService
|
||||||
it[isLocal] = timeline.isLocal
|
it[isLocal] = timeline.isLocal
|
||||||
it[isPureRepost] = timeline.isPureRepost
|
it[isPureRepost] = timeline.isPureRepost
|
||||||
it[mediaIds] = timeline.mediaIds.joinToString(",")
|
it[mediaIds] = timeline.mediaIds.joinToString(",")
|
||||||
|
it[emojiIds] = timeline.emojiIds.joinToString(",")
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
Timelines.update({ Timelines.id eq timeline.id }) {
|
Timelines.update({ Timelines.id eq timeline.id }) {
|
||||||
|
@ -52,6 +53,7 @@ class ExposedTimelineRepository(private val idGenerateService: IdGenerateService
|
||||||
it[isLocal] = timeline.isLocal
|
it[isLocal] = timeline.isLocal
|
||||||
it[isPureRepost] = timeline.isPureRepost
|
it[isPureRepost] = timeline.isPureRepost
|
||||||
it[mediaIds] = timeline.mediaIds.joinToString(",")
|
it[mediaIds] = timeline.mediaIds.joinToString(",")
|
||||||
|
it[emojiIds] = timeline.emojiIds.joinToString(",")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return@query timeline
|
return@query timeline
|
||||||
|
@ -72,6 +74,7 @@ class ExposedTimelineRepository(private val idGenerateService: IdGenerateService
|
||||||
this[Timelines.isLocal] = it.isLocal
|
this[Timelines.isLocal] = it.isLocal
|
||||||
this[Timelines.isPureRepost] = it.isPureRepost
|
this[Timelines.isPureRepost] = it.isPureRepost
|
||||||
this[Timelines.mediaIds] = it.mediaIds.joinToString(",")
|
this[Timelines.mediaIds] = it.mediaIds.joinToString(",")
|
||||||
|
this[Timelines.emojiIds] = it.emojiIds.joinToString(",")
|
||||||
}
|
}
|
||||||
return@query timelines
|
return@query timelines
|
||||||
}
|
}
|
||||||
|
@ -104,7 +107,8 @@ fun ResultRow.toTimeline(): Timeline {
|
||||||
sensitive = this[Timelines.sensitive],
|
sensitive = this[Timelines.sensitive],
|
||||||
isLocal = this[Timelines.isLocal],
|
isLocal = this[Timelines.isLocal],
|
||||||
isPureRepost = this[Timelines.isPureRepost],
|
isPureRepost = this[Timelines.isPureRepost],
|
||||||
mediaIds = this[Timelines.mediaIds].split(",").mapNotNull { it.toLongOrNull() }
|
mediaIds = this[Timelines.mediaIds].split(",").mapNotNull { it.toLongOrNull() },
|
||||||
|
emojiIds = this[Timelines.emojiIds].split(",").mapNotNull { it.toLongOrNull() }
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -122,6 +126,7 @@ object Timelines : Table("timelines") {
|
||||||
val isLocal = bool("is_local")
|
val isLocal = bool("is_local")
|
||||||
val isPureRepost = bool("is_pure_repost")
|
val isPureRepost = bool("is_pure_repost")
|
||||||
val mediaIds = varchar("media_ids", 255)
|
val mediaIds = varchar("media_ids", 255)
|
||||||
|
val emojiIds = varchar("emoji_ids", 255)
|
||||||
|
|
||||||
override val primaryKey: PrimaryKey = PrimaryKey(id)
|
override val primaryKey: PrimaryKey = PrimaryKey(id)
|
||||||
|
|
||||||
|
|
|
@ -41,14 +41,25 @@ class PostRepositoryImpl(
|
||||||
this[PostsMedia.postId] = post.id
|
this[PostsMedia.postId] = post.id
|
||||||
this[PostsMedia.mediaId] = it
|
this[PostsMedia.mediaId] = it
|
||||||
}
|
}
|
||||||
|
PostsEmojis.batchInsert(post.emojiIds) {
|
||||||
|
this[PostsEmojis.postId] = post.id
|
||||||
|
this[PostsEmojis.emojiId] = it
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
PostsMedia.deleteWhere {
|
PostsMedia.deleteWhere {
|
||||||
postId eq post.id
|
postId eq post.id
|
||||||
}
|
}
|
||||||
|
PostsEmojis.deleteWhere {
|
||||||
|
postId eq post.id
|
||||||
|
}
|
||||||
PostsMedia.batchInsert(post.mediaIds) {
|
PostsMedia.batchInsert(post.mediaIds) {
|
||||||
this[PostsMedia.postId] = post.id
|
this[PostsMedia.postId] = post.id
|
||||||
this[PostsMedia.mediaId] = it
|
this[PostsMedia.mediaId] = it
|
||||||
}
|
}
|
||||||
|
PostsEmojis.batchInsert(post.emojiIds) {
|
||||||
|
this[PostsEmojis.postId] = post.id
|
||||||
|
this[PostsEmojis.emojiId] = it
|
||||||
|
}
|
||||||
Posts.update({ Posts.id eq post.id }) {
|
Posts.update({ Posts.id eq post.id }) {
|
||||||
it[actorId] = post.actorId
|
it[actorId] = post.actorId
|
||||||
it[overview] = post.overview
|
it[overview] = post.overview
|
||||||
|
@ -67,21 +78,27 @@ class PostRepositoryImpl(
|
||||||
}
|
}
|
||||||
|
|
||||||
override suspend fun findById(id: Long): Post? = query {
|
override suspend fun findById(id: Long): Post? = query {
|
||||||
return@query Posts.leftJoin(PostsMedia)
|
return@query Posts
|
||||||
|
.leftJoin(PostsMedia)
|
||||||
|
.leftJoin(PostsEmojis)
|
||||||
.select { Posts.id eq id }
|
.select { Posts.id eq id }
|
||||||
.let(postQueryMapper::map)
|
.let(postQueryMapper::map)
|
||||||
.singleOrNull()
|
.singleOrNull()
|
||||||
}
|
}
|
||||||
|
|
||||||
override suspend fun findByUrl(url: String): Post? = query {
|
override suspend fun findByUrl(url: String): Post? = query {
|
||||||
return@query Posts.leftJoin(PostsMedia)
|
return@query Posts
|
||||||
|
.leftJoin(PostsMedia)
|
||||||
|
.leftJoin(PostsEmojis)
|
||||||
.select { Posts.url eq url }
|
.select { Posts.url eq url }
|
||||||
.let(postQueryMapper::map)
|
.let(postQueryMapper::map)
|
||||||
.singleOrNull()
|
.singleOrNull()
|
||||||
}
|
}
|
||||||
|
|
||||||
override suspend fun findByApId(apId: String): Post? = query {
|
override suspend fun findByApId(apId: String): Post? = query {
|
||||||
return@query Posts.leftJoin(PostsMedia)
|
return@query Posts
|
||||||
|
.leftJoin(PostsMedia)
|
||||||
|
.leftJoin(PostsEmojis)
|
||||||
.select { Posts.apId eq apId }
|
.select { Posts.apId eq apId }
|
||||||
.let(postQueryMapper::map)
|
.let(postQueryMapper::map)
|
||||||
.singleOrNull()
|
.singleOrNull()
|
||||||
|
@ -92,7 +109,10 @@ class PostRepositoryImpl(
|
||||||
}
|
}
|
||||||
|
|
||||||
override suspend fun findByActorId(actorId: Long): List<Post> = query {
|
override suspend fun findByActorId(actorId: Long): List<Post> = query {
|
||||||
return@query Posts.select { Posts.actorId eq actorId }.let(postQueryMapper::map)
|
return@query Posts
|
||||||
|
.leftJoin(PostsMedia)
|
||||||
|
.leftJoin(PostsEmojis)
|
||||||
|
.select { Posts.actorId eq actorId }.let(postQueryMapper::map)
|
||||||
}
|
}
|
||||||
|
|
||||||
override suspend fun delete(id: Long): Unit = query {
|
override suspend fun delete(id: Long): Unit = query {
|
||||||
|
@ -125,3 +145,9 @@ object PostsMedia : Table("posts_media") {
|
||||||
val mediaId = long("media_id").references(Media.id, ReferenceOption.CASCADE, ReferenceOption.CASCADE)
|
val mediaId = long("media_id").references(Media.id, ReferenceOption.CASCADE, ReferenceOption.CASCADE)
|
||||||
override val primaryKey = PrimaryKey(postId, mediaId)
|
override val primaryKey = PrimaryKey(postId, mediaId)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
object PostsEmojis : Table("posts_emojis") {
|
||||||
|
val postId = long("post_id").references(Posts.id)
|
||||||
|
val emojiId = long("emoji_id").references(CustomEmojis.id)
|
||||||
|
override val primaryKey: PrimaryKey = PrimaryKey(postId, emojiId)
|
||||||
|
}
|
||||||
|
|
|
@ -1,6 +1,9 @@
|
||||||
package dev.usbharu.hideout.core.infrastructure.exposedrepository
|
package dev.usbharu.hideout.core.infrastructure.exposedrepository
|
||||||
|
|
||||||
import dev.usbharu.hideout.application.service.id.IdGenerateService
|
import dev.usbharu.hideout.application.service.id.IdGenerateService
|
||||||
|
import dev.usbharu.hideout.core.domain.model.emoji.CustomEmoji
|
||||||
|
import dev.usbharu.hideout.core.domain.model.emoji.Emoji
|
||||||
|
import dev.usbharu.hideout.core.domain.model.emoji.UnicodeEmoji
|
||||||
import dev.usbharu.hideout.core.domain.model.reaction.Reaction
|
import dev.usbharu.hideout.core.domain.model.reaction.Reaction
|
||||||
import dev.usbharu.hideout.core.domain.model.reaction.ReactionRepository
|
import dev.usbharu.hideout.core.domain.model.reaction.ReactionRepository
|
||||||
import org.jetbrains.exposed.dao.id.LongIdTable
|
import org.jetbrains.exposed.dao.id.LongIdTable
|
||||||
|
@ -23,13 +26,25 @@ class ReactionRepositoryImpl(
|
||||||
if (Reactions.select { Reactions.id eq reaction.id }.forUpdate().empty()) {
|
if (Reactions.select { Reactions.id eq reaction.id }.forUpdate().empty()) {
|
||||||
Reactions.insert {
|
Reactions.insert {
|
||||||
it[id] = reaction.id
|
it[id] = reaction.id
|
||||||
it[emojiId] = reaction.emojiId
|
if (reaction.emoji is CustomEmoji) {
|
||||||
|
it[customEmojiId] = reaction.emoji.id
|
||||||
|
it[unicodeEmoji] = null
|
||||||
|
} else {
|
||||||
|
it[customEmojiId] = null
|
||||||
|
it[unicodeEmoji] = reaction.emoji.name
|
||||||
|
}
|
||||||
it[postId] = reaction.postId
|
it[postId] = reaction.postId
|
||||||
it[actorId] = reaction.actorId
|
it[actorId] = reaction.actorId
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
Reactions.update({ Reactions.id eq reaction.id }) {
|
Reactions.update({ Reactions.id eq reaction.id }) {
|
||||||
it[emojiId] = reaction.emojiId
|
if (reaction.emoji is CustomEmoji) {
|
||||||
|
it[customEmojiId] = reaction.emoji.id
|
||||||
|
it[unicodeEmoji] = null
|
||||||
|
} else {
|
||||||
|
it[customEmojiId] = null
|
||||||
|
it[unicodeEmoji] = reaction.emoji.name
|
||||||
|
}
|
||||||
it[postId] = reaction.postId
|
it[postId] = reaction.postId
|
||||||
it[actorId] = reaction.actorId
|
it[actorId] = reaction.actorId
|
||||||
}
|
}
|
||||||
|
@ -38,9 +53,16 @@ class ReactionRepositoryImpl(
|
||||||
}
|
}
|
||||||
|
|
||||||
override suspend fun delete(reaction: Reaction): Reaction = query {
|
override suspend fun delete(reaction: Reaction): Reaction = query {
|
||||||
|
if (reaction.emoji is CustomEmoji) {
|
||||||
Reactions.deleteWhere {
|
Reactions.deleteWhere {
|
||||||
id.eq(reaction.id).and(postId.eq(reaction.postId)).and(actorId.eq(reaction.actorId))
|
id.eq(reaction.id).and(postId.eq(reaction.postId)).and(actorId.eq(reaction.actorId))
|
||||||
.and(emojiId.eq(reaction.emojiId))
|
.and(customEmojiId.eq(reaction.emoji.id))
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
Reactions.deleteWhere {
|
||||||
|
id.eq(reaction.id).and(postId.eq(reaction.postId)).and(actorId.eq(reaction.actorId))
|
||||||
|
.and(unicodeEmoji.eq(reaction.emoji.name))
|
||||||
|
}
|
||||||
}
|
}
|
||||||
return@query reaction
|
return@query reaction
|
||||||
}
|
}
|
||||||
|
@ -57,15 +79,37 @@ class ReactionRepositoryImpl(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
override suspend fun deleteByPostIdAndActorId(postId: Long, actorId: Long): Unit = query {
|
||||||
|
Reactions.deleteWhere {
|
||||||
|
Reactions.postId eq postId and (Reactions.actorId eq actorId)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override suspend fun deleteByPostIdAndActorIdAndEmoji(postId: Long, actorId: Long, emoji: Emoji): Unit = query {
|
||||||
|
if (emoji is CustomEmoji) {
|
||||||
|
Reactions.deleteWhere {
|
||||||
|
Reactions.postId.eq(postId)
|
||||||
|
.and(Reactions.actorId.eq(actorId))
|
||||||
|
.and(Reactions.customEmojiId.eq(emoji.id))
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
Reactions.deleteWhere {
|
||||||
|
Reactions.postId.eq(postId)
|
||||||
|
.and(Reactions.actorId.eq(actorId))
|
||||||
|
.and(Reactions.unicodeEmoji.eq(emoji.name))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
override suspend fun findByPostId(postId: Long): List<Reaction> = query {
|
override suspend fun findByPostId(postId: Long): List<Reaction> = query {
|
||||||
return@query Reactions.select { Reactions.postId eq postId }.map { it.toReaction() }
|
return@query Reactions.leftJoin(CustomEmojis).select { Reactions.postId eq postId }.map { it.toReaction() }
|
||||||
}
|
}
|
||||||
|
|
||||||
override suspend fun findByPostIdAndActorIdAndEmojiId(postId: Long, actorId: Long, emojiId: Long): Reaction? =
|
override suspend fun findByPostIdAndActorIdAndEmojiId(postId: Long, actorId: Long, emojiId: Long): Reaction? =
|
||||||
query {
|
query {
|
||||||
return@query Reactions.select {
|
return@query Reactions.leftJoin(CustomEmojis).select {
|
||||||
Reactions.postId eq postId and (Reactions.actorId eq actorId).and(
|
Reactions.postId eq postId and (Reactions.actorId eq actorId).and(
|
||||||
Reactions.emojiId.eq(
|
Reactions.customEmojiId.eq(
|
||||||
emojiId
|
emojiId
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
@ -78,12 +122,49 @@ class ReactionRepositoryImpl(
|
||||||
Reactions.postId
|
Reactions.postId
|
||||||
.eq(postId)
|
.eq(postId)
|
||||||
.and(Reactions.actorId.eq(actorId))
|
.and(Reactions.actorId.eq(actorId))
|
||||||
.and(Reactions.emojiId.eq(emojiId))
|
.and(Reactions.customEmojiId.eq(emojiId))
|
||||||
|
}.empty().not()
|
||||||
|
}
|
||||||
|
|
||||||
|
override suspend fun existByPostIdAndActorIdAndUnicodeEmoji(
|
||||||
|
postId: Long,
|
||||||
|
actorId: Long,
|
||||||
|
unicodeEmoji: String
|
||||||
|
): Boolean = query {
|
||||||
|
return@query Reactions.select {
|
||||||
|
Reactions.postId
|
||||||
|
.eq(postId)
|
||||||
|
.and(Reactions.actorId.eq(actorId))
|
||||||
|
.and(Reactions.unicodeEmoji.eq(unicodeEmoji))
|
||||||
|
}.empty().not()
|
||||||
|
}
|
||||||
|
|
||||||
|
override suspend fun existByPostIdAndActorIdAndEmoji(postId: Long, actorId: Long, emoji: Emoji): Boolean = query {
|
||||||
|
val query = Reactions.select {
|
||||||
|
Reactions.postId
|
||||||
|
.eq(postId)
|
||||||
|
.and(Reactions.actorId.eq(actorId))
|
||||||
|
}
|
||||||
|
|
||||||
|
if (emoji is UnicodeEmoji) {
|
||||||
|
query.andWhere { Reactions.unicodeEmoji eq emoji.name }
|
||||||
|
} else {
|
||||||
|
emoji as CustomEmoji
|
||||||
|
query.andWhere { Reactions.customEmojiId eq emoji.id }
|
||||||
|
}
|
||||||
|
|
||||||
|
return@query query.empty().not()
|
||||||
|
}
|
||||||
|
|
||||||
|
override suspend fun existByPostIdAndActor(postId: Long, actorId: Long): Boolean = query {
|
||||||
|
Reactions.select {
|
||||||
|
Reactions.postId.eq(postId).and(Reactions.actorId.eq(actorId))
|
||||||
}.empty().not()
|
}.empty().not()
|
||||||
}
|
}
|
||||||
|
|
||||||
override suspend fun findByPostIdAndActorId(postId: Long, actorId: Long): List<Reaction> = query {
|
override suspend fun findByPostIdAndActorId(postId: Long, actorId: Long): List<Reaction> = query {
|
||||||
return@query Reactions.select { Reactions.postId eq postId and (Reactions.actorId eq actorId) }
|
return@query Reactions.leftJoin(CustomEmojis)
|
||||||
|
.select { Reactions.postId eq postId and (Reactions.actorId eq actorId) }
|
||||||
.map { it.toReaction() }
|
.map { it.toReaction() }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -93,22 +174,39 @@ class ReactionRepositoryImpl(
|
||||||
}
|
}
|
||||||
|
|
||||||
fun ResultRow.toReaction(): Reaction {
|
fun ResultRow.toReaction(): Reaction {
|
||||||
|
val emoji = if (this[Reactions.customEmojiId] != null) {
|
||||||
|
CustomEmoji(
|
||||||
|
id = this[Reactions.customEmojiId]!!,
|
||||||
|
name = this[CustomEmojis.name],
|
||||||
|
domain = this[CustomEmojis.domain],
|
||||||
|
instanceId = this[CustomEmojis.instanceId],
|
||||||
|
url = this[CustomEmojis.url],
|
||||||
|
category = this[CustomEmojis.category],
|
||||||
|
createdAt = this[CustomEmojis.createdAt]
|
||||||
|
)
|
||||||
|
} else if (this[Reactions.unicodeEmoji] != null) {
|
||||||
|
UnicodeEmoji(this[Reactions.unicodeEmoji]!!)
|
||||||
|
} else {
|
||||||
|
throw IllegalStateException("customEmojiId and unicodeEmoji is null.")
|
||||||
|
}
|
||||||
|
|
||||||
return Reaction(
|
return Reaction(
|
||||||
this[Reactions.id].value,
|
this[Reactions.id].value,
|
||||||
this[Reactions.emojiId],
|
emoji,
|
||||||
this[Reactions.postId],
|
this[Reactions.postId],
|
||||||
this[Reactions.actorId]
|
this[Reactions.actorId]
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
object Reactions : LongIdTable("reactions") {
|
object Reactions : LongIdTable("reactions") {
|
||||||
val emojiId: Column<Long> = long("emoji_id")
|
val customEmojiId = long("custom_emoji_id").references(CustomEmojis.id).nullable()
|
||||||
|
val unicodeEmoji = varchar("unicode_emoji", 255).nullable()
|
||||||
val postId: Column<Long> =
|
val postId: Column<Long> =
|
||||||
long("post_id").references(Posts.id, onDelete = ReferenceOption.CASCADE, onUpdate = ReferenceOption.CASCADE)
|
long("post_id").references(Posts.id, onDelete = ReferenceOption.CASCADE, onUpdate = ReferenceOption.CASCADE)
|
||||||
val actorId: Column<Long> =
|
val actorId: Column<Long> =
|
||||||
long("actor_id").references(Actors.id, onDelete = ReferenceOption.CASCADE, onUpdate = ReferenceOption.CASCADE)
|
long("actor_id").references(Actors.id, onDelete = ReferenceOption.CASCADE, onUpdate = ReferenceOption.CASCADE)
|
||||||
|
|
||||||
init {
|
init {
|
||||||
uniqueIndex(emojiId, postId, actorId)
|
uniqueIndex(customEmojiId, postId, actorId)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -26,11 +26,6 @@ class UserDetailsImpl(
|
||||||
accountNonLocked: Boolean,
|
accountNonLocked: Boolean,
|
||||||
authorities: MutableCollection<out GrantedAuthority>?
|
authorities: MutableCollection<out GrantedAuthority>?
|
||||||
) : User(username, password, enabled, accountNonExpired, credentialsNonExpired, accountNonLocked, authorities) {
|
) : User(username, password, enabled, accountNonExpired, credentialsNonExpired, accountNonLocked, authorities) {
|
||||||
companion object {
|
|
||||||
@Serial
|
|
||||||
private const val serialVersionUID: Long = -899168205656607781L
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun toString(): String {
|
override fun toString(): String {
|
||||||
return "UserDetailsImpl(" +
|
return "UserDetailsImpl(" +
|
||||||
"id=$id" +
|
"id=$id" +
|
||||||
|
@ -53,6 +48,11 @@ class UserDetailsImpl(
|
||||||
result = 31 * result + id.hashCode()
|
result = 31 * result + id.hashCode()
|
||||||
return result
|
return result
|
||||||
}
|
}
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
@Serial
|
||||||
|
private const val serialVersionUID: Long = -899168205656607781L
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@JsonTypeInfo(use = JsonTypeInfo.Id.CLASS, include = JsonTypeInfo.As.PROPERTY)
|
@JsonTypeInfo(use = JsonTypeInfo.Id.CLASS, include = JsonTypeInfo.As.PROPERTY)
|
||||||
|
|
|
@ -10,9 +10,7 @@ sealed class SavedMedia(val success: Boolean) {
|
||||||
return success == other.success
|
return success == other.success
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun hashCode(): Int {
|
override fun hashCode(): Int = success.hashCode()
|
||||||
return success.hashCode()
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
class SuccessSavedMedia(
|
class SuccessSavedMedia(
|
||||||
|
|
|
@ -1,11 +1,12 @@
|
||||||
package dev.usbharu.hideout.core.service.reaction
|
package dev.usbharu.hideout.core.service.reaction
|
||||||
|
|
||||||
|
import dev.usbharu.hideout.core.domain.model.emoji.Emoji
|
||||||
import org.springframework.stereotype.Service
|
import org.springframework.stereotype.Service
|
||||||
|
|
||||||
@Service
|
@Service
|
||||||
interface ReactionService {
|
interface ReactionService {
|
||||||
suspend fun receiveReaction(name: String, domain: String, actorId: Long, postId: Long)
|
suspend fun receiveReaction(emoji: Emoji, actorId: Long, postId: Long)
|
||||||
suspend fun receiveRemoveReaction(actorId: Long, postId: Long)
|
suspend fun receiveRemoveReaction(actorId: Long, postId: Long)
|
||||||
suspend fun sendReaction(name: String, actorId: Long, postId: Long)
|
suspend fun sendReaction(emoji: Emoji, actorId: Long, postId: Long)
|
||||||
suspend fun removeReaction(actorId: Long, postId: Long)
|
suspend fun removeReaction(actorId: Long, postId: Long)
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,9 +1,10 @@
|
||||||
package dev.usbharu.hideout.core.service.reaction
|
package dev.usbharu.hideout.core.service.reaction
|
||||||
|
|
||||||
import dev.usbharu.hideout.activitypub.service.activity.like.APReactionService
|
import dev.usbharu.hideout.activitypub.service.activity.like.APReactionService
|
||||||
|
import dev.usbharu.hideout.core.domain.exception.resource.DuplicateException
|
||||||
|
import dev.usbharu.hideout.core.domain.model.emoji.Emoji
|
||||||
import dev.usbharu.hideout.core.domain.model.reaction.Reaction
|
import dev.usbharu.hideout.core.domain.model.reaction.Reaction
|
||||||
import dev.usbharu.hideout.core.domain.model.reaction.ReactionRepository
|
import dev.usbharu.hideout.core.domain.model.reaction.ReactionRepository
|
||||||
import org.jetbrains.exposed.exceptions.ExposedSQLException
|
|
||||||
import org.slf4j.Logger
|
import org.slf4j.Logger
|
||||||
import org.slf4j.LoggerFactory
|
import org.slf4j.LoggerFactory
|
||||||
import org.springframework.stereotype.Service
|
import org.springframework.stereotype.Service
|
||||||
|
@ -13,14 +14,17 @@ class ReactionServiceImpl(
|
||||||
private val reactionRepository: ReactionRepository,
|
private val reactionRepository: ReactionRepository,
|
||||||
private val apReactionService: APReactionService
|
private val apReactionService: APReactionService
|
||||||
) : ReactionService {
|
) : ReactionService {
|
||||||
override suspend fun receiveReaction(name: String, domain: String, actorId: Long, postId: Long) {
|
override suspend fun receiveReaction(
|
||||||
if (reactionRepository.existByPostIdAndActorIdAndEmojiId(postId, actorId, 0).not()) {
|
emoji: Emoji,
|
||||||
try {
|
actorId: Long,
|
||||||
reactionRepository.save(
|
postId: Long
|
||||||
Reaction(reactionRepository.generateId(), 0, postId, actorId)
|
) {
|
||||||
)
|
if (reactionRepository.existByPostIdAndActor(postId, actorId)) {
|
||||||
} catch (_: ExposedSQLException) {
|
reactionRepository.deleteByPostIdAndActorId(postId, actorId)
|
||||||
}
|
}
|
||||||
|
try {
|
||||||
|
reactionRepository.save(Reaction(reactionRepository.generateId(), emoji, postId, actorId))
|
||||||
|
} catch (_: DuplicateException) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -33,7 +37,7 @@ class ReactionServiceImpl(
|
||||||
reactionRepository.delete(reaction)
|
reactionRepository.delete(reaction)
|
||||||
}
|
}
|
||||||
|
|
||||||
override suspend fun sendReaction(name: String, actorId: Long, postId: Long) {
|
override suspend fun sendReaction(emoji: Emoji, actorId: Long, postId: Long) {
|
||||||
val findByPostIdAndUserIdAndEmojiId =
|
val findByPostIdAndUserIdAndEmojiId =
|
||||||
reactionRepository.findByPostIdAndActorIdAndEmojiId(postId, actorId, 0)
|
reactionRepository.findByPostIdAndActorIdAndEmojiId(postId, actorId, 0)
|
||||||
|
|
||||||
|
@ -42,7 +46,7 @@ class ReactionServiceImpl(
|
||||||
reactionRepository.delete(findByPostIdAndUserIdAndEmojiId)
|
reactionRepository.delete(findByPostIdAndUserIdAndEmojiId)
|
||||||
}
|
}
|
||||||
|
|
||||||
val reaction = Reaction(reactionRepository.generateId(), 0, postId, actorId)
|
val reaction = Reaction(reactionRepository.generateId(), emoji, postId, actorId)
|
||||||
reactionRepository.save(reaction)
|
reactionRepository.save(reaction)
|
||||||
apReactionService.reaction(reaction)
|
apReactionService.reaction(reaction)
|
||||||
}
|
}
|
||||||
|
|
|
@ -45,7 +45,8 @@ class ExposedGenerateTimelineService(private val statusQueryService: StatusQuery
|
||||||
it[Timelines.postId],
|
it[Timelines.postId],
|
||||||
it[Timelines.replyId],
|
it[Timelines.replyId],
|
||||||
it[Timelines.repostId],
|
it[Timelines.repostId],
|
||||||
it[Timelines.mediaIds].split(",").mapNotNull { s -> s.toLongOrNull() }
|
it[Timelines.mediaIds].split(",").mapNotNull { s -> s.toLongOrNull() },
|
||||||
|
it[Timelines.emojiIds].split(",").mapNotNull { s -> s.toLongOrNull() }
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -57,7 +57,8 @@ class MongoGenerateTimelineService(
|
||||||
it.postId,
|
it.postId,
|
||||||
it.replyId,
|
it.replyId,
|
||||||
it.repostId,
|
it.repostId,
|
||||||
it.mediaIds
|
it.mediaIds,
|
||||||
|
it.emojiIds
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
|
@ -37,7 +37,8 @@ class TimelineService(
|
||||||
sensitive = post.sensitive,
|
sensitive = post.sensitive,
|
||||||
isLocal = isLocal,
|
isLocal = isLocal,
|
||||||
isPureRepost = post.repostId == null || (post.text.isBlank() && post.overview.isNullOrBlank()),
|
isPureRepost = post.repostId == null || (post.text.isBlank() && post.overview.isNullOrBlank()),
|
||||||
mediaIds = post.mediaIds
|
mediaIds = post.mediaIds,
|
||||||
|
emojiIds = post.emojiIds
|
||||||
)
|
)
|
||||||
}.toMutableList()
|
}.toMutableList()
|
||||||
if (post.visibility == Visibility.PUBLIC) {
|
if (post.visibility == Visibility.PUBLIC) {
|
||||||
|
@ -55,7 +56,8 @@ class TimelineService(
|
||||||
sensitive = post.sensitive,
|
sensitive = post.sensitive,
|
||||||
isLocal = isLocal,
|
isLocal = isLocal,
|
||||||
isPureRepost = post.repostId == null || (post.text.isBlank() && post.overview.isNullOrBlank()),
|
isPureRepost = post.repostId == null || (post.text.isBlank() && post.overview.isNullOrBlank()),
|
||||||
mediaIds = post.mediaIds
|
mediaIds = post.mediaIds,
|
||||||
|
emojiIds = post.emojiIds
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,5 +1,6 @@
|
||||||
package dev.usbharu.hideout.mastodon.infrastructure.exposedquery
|
package dev.usbharu.hideout.mastodon.infrastructure.exposedquery
|
||||||
|
|
||||||
|
import dev.usbharu.hideout.core.domain.model.emoji.CustomEmoji
|
||||||
import dev.usbharu.hideout.core.domain.model.media.toMediaAttachments
|
import dev.usbharu.hideout.core.domain.model.media.toMediaAttachments
|
||||||
import dev.usbharu.hideout.core.infrastructure.exposedrepository.*
|
import dev.usbharu.hideout.core.infrastructure.exposedrepository.*
|
||||||
import dev.usbharu.hideout.domain.mastodon.model.generated.Account
|
import dev.usbharu.hideout.domain.mastodon.model.generated.Account
|
||||||
|
@ -12,6 +13,7 @@ import org.jetbrains.exposed.sql.andWhere
|
||||||
import org.jetbrains.exposed.sql.select
|
import org.jetbrains.exposed.sql.select
|
||||||
import org.springframework.stereotype.Repository
|
import org.springframework.stereotype.Repository
|
||||||
import java.time.Instant
|
import java.time.Instant
|
||||||
|
import dev.usbharu.hideout.domain.mastodon.model.generated.CustomEmoji as MastodonEmoji
|
||||||
|
|
||||||
@Suppress("IncompleteDestructuring")
|
@Suppress("IncompleteDestructuring")
|
||||||
@Repository
|
@Repository
|
||||||
|
@ -23,6 +25,10 @@ class StatusQueryServiceImpl : StatusQueryService {
|
||||||
postIdSet.addAll(statusQueries.flatMap { listOfNotNull(it.postId, it.replyId, it.repostId) })
|
postIdSet.addAll(statusQueries.flatMap { listOfNotNull(it.postId, it.replyId, it.repostId) })
|
||||||
val mediaIdSet = mutableSetOf<Long>()
|
val mediaIdSet = mutableSetOf<Long>()
|
||||||
mediaIdSet.addAll(statusQueries.flatMap { it.mediaIds })
|
mediaIdSet.addAll(statusQueries.flatMap { it.mediaIds })
|
||||||
|
|
||||||
|
val emojiIdSet = mutableSetOf<Long>()
|
||||||
|
emojiIdSet.addAll(statusQueries.flatMap { it.emojiIds })
|
||||||
|
|
||||||
val postMap = Posts
|
val postMap = Posts
|
||||||
.leftJoin(Actors)
|
.leftJoin(Actors)
|
||||||
.select { Posts.id inList postIdSet }
|
.select { Posts.id inList postIdSet }
|
||||||
|
@ -32,12 +38,16 @@ class StatusQueryServiceImpl : StatusQueryService {
|
||||||
it[Media.id] to it.toMedia().toMediaAttachments()
|
it[Media.id] to it.toMedia().toMediaAttachments()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
val emojiMap = CustomEmojis.select { CustomEmojis.id inList emojiIdSet }.associate {
|
||||||
|
it[CustomEmojis.id] to it.toCustomEmoji().toMastodonEmoji()
|
||||||
|
}
|
||||||
return statusQueries.mapNotNull { statusQuery ->
|
return statusQueries.mapNotNull { statusQuery ->
|
||||||
postMap[statusQuery.postId]?.copy(
|
postMap[statusQuery.postId]?.copy(
|
||||||
inReplyToId = statusQuery.replyId?.toString(),
|
inReplyToId = statusQuery.replyId?.toString(),
|
||||||
inReplyToAccountId = postMap[statusQuery.replyId]?.account?.id,
|
inReplyToAccountId = postMap[statusQuery.replyId]?.account?.id,
|
||||||
reblog = postMap[statusQuery.repostId],
|
reblog = postMap[statusQuery.repostId],
|
||||||
mediaAttachments = statusQuery.mediaIds.mapNotNull { mediaMap[it] }
|
mediaAttachments = statusQuery.mediaIds.mapNotNull { mediaMap[it] },
|
||||||
|
emojis = statusQuery.emojiIds.mapNotNull { emojiMap[it] }
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -98,6 +108,25 @@ class StatusQueryServiceImpl : StatusQueryService {
|
||||||
return resolveReplyAndRepost(pairs)
|
return resolveReplyAndRepost(pairs)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
override suspend fun findByPostId(id: Long): Status {
|
||||||
|
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<Pair<Status, Long?>>): List<Status> {
|
private fun resolveReplyAndRepost(pairs: List<Pair<Status, Long?>>): List<Status> {
|
||||||
val statuses = pairs.map { it.first }
|
val statuses = pairs.map { it.first }
|
||||||
return pairs
|
return pairs
|
||||||
|
@ -120,6 +149,8 @@ class StatusQueryServiceImpl : StatusQueryService {
|
||||||
private suspend fun findByPostIdsWithMedia(ids: List<Long>): List<Status> {
|
private suspend fun findByPostIdsWithMedia(ids: List<Long>): List<Status> {
|
||||||
val pairs = Posts
|
val pairs = Posts
|
||||||
.leftJoin(PostsMedia)
|
.leftJoin(PostsMedia)
|
||||||
|
.leftJoin(PostsEmojis)
|
||||||
|
.leftJoin(CustomEmojis)
|
||||||
.leftJoin(Actors)
|
.leftJoin(Actors)
|
||||||
.leftJoin(Media)
|
.leftJoin(Media)
|
||||||
.select { Posts.id inList ids }
|
.select { Posts.id inList ids }
|
||||||
|
@ -129,13 +160,22 @@ class StatusQueryServiceImpl : StatusQueryService {
|
||||||
toStatus(it.first()).copy(
|
toStatus(it.first()).copy(
|
||||||
mediaAttachments = it.mapNotNull { resultRow ->
|
mediaAttachments = it.mapNotNull { resultRow ->
|
||||||
resultRow.toMediaOrNull()?.toMediaAttachments()
|
resultRow.toMediaOrNull()?.toMediaAttachments()
|
||||||
}
|
},
|
||||||
|
emojis = it.mapNotNull { resultRow -> resultRow.toCustomEmojiOrNull()?.toMastodonEmoji() }
|
||||||
) to it.first()[Posts.repostId]
|
) to it.first()[Posts.repostId]
|
||||||
}
|
}
|
||||||
return resolveReplyAndRepost(pairs)
|
return resolveReplyAndRepost(pairs)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fun CustomEmoji.toMastodonEmoji(): MastodonEmoji = MastodonEmoji(
|
||||||
|
shortcode = this.name,
|
||||||
|
url = this.url,
|
||||||
|
staticUrl = this.url,
|
||||||
|
visibleInPicker = true,
|
||||||
|
category = this.category.orEmpty()
|
||||||
|
)
|
||||||
|
|
||||||
private fun toStatus(it: ResultRow) = Status(
|
private fun toStatus(it: ResultRow) = Status(
|
||||||
id = it[Posts.id].toString(),
|
id = it[Posts.id].toString(),
|
||||||
uri = it[Posts.apId],
|
uri = it[Posts.apId],
|
||||||
|
|
|
@ -24,4 +24,20 @@ class MastodonStatusesApiContoller(private val statusesApiService: StatusesApiSe
|
||||||
HttpStatus.OK
|
HttpStatus.OK
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
override suspend fun apiV1StatusesIdEmojiReactionsEmojiDelete(id: String, emoji: String): ResponseEntity<Status> {
|
||||||
|
val uid =
|
||||||
|
(SecurityContextHolder.getContext().authentication.principal as Jwt).getClaim<String>("uid").toLong()
|
||||||
|
|
||||||
|
return ResponseEntity.ok(statusesApiService.removeEmojiReactions(id.toLong(), uid, emoji))
|
||||||
|
}
|
||||||
|
|
||||||
|
override suspend fun apiV1StatusesIdEmojiReactionsEmojiPut(id: String, emoji: String): ResponseEntity<Status> {
|
||||||
|
val uid =
|
||||||
|
(SecurityContextHolder.getContext().authentication.principal as Jwt).getClaim<String>("uid").toLong()
|
||||||
|
|
||||||
|
return ResponseEntity.ok(statusesApiService.emojiReactions(id.toLong(), uid, emoji))
|
||||||
|
}
|
||||||
|
|
||||||
|
override suspend fun apiV1StatusesIdGet(id: String): ResponseEntity<Status> = super.apiV1StatusesIdGet(id)
|
||||||
}
|
}
|
||||||
|
|
|
@ -4,5 +4,6 @@ data class StatusQuery(
|
||||||
val postId: Long,
|
val postId: Long,
|
||||||
val replyId: Long?,
|
val replyId: Long?,
|
||||||
val repostId: Long?,
|
val repostId: Long?,
|
||||||
val mediaIds: List<Long>
|
val mediaIds: List<Long>,
|
||||||
|
val emojiIds: List<Long>
|
||||||
)
|
)
|
||||||
|
|
|
@ -36,4 +36,6 @@ interface StatusQueryService {
|
||||||
tagged: String? = null,
|
tagged: String? = null,
|
||||||
includeFollowers: Boolean = false
|
includeFollowers: Boolean = false
|
||||||
): List<Status>
|
): List<Status>
|
||||||
|
|
||||||
|
suspend fun findByPostId(id: Long): Status
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,17 +1,24 @@
|
||||||
package dev.usbharu.hideout.mastodon.service.status
|
package dev.usbharu.hideout.mastodon.service.status
|
||||||
|
|
||||||
|
import dev.usbharu.hideout.activitypub.service.objects.emoji.EmojiService
|
||||||
import dev.usbharu.hideout.application.external.Transaction
|
import dev.usbharu.hideout.application.external.Transaction
|
||||||
import dev.usbharu.hideout.core.domain.model.actor.ActorRepository
|
import dev.usbharu.hideout.core.domain.model.actor.ActorRepository
|
||||||
|
import dev.usbharu.hideout.core.domain.model.emoji.UnicodeEmoji
|
||||||
import dev.usbharu.hideout.core.domain.model.media.MediaRepository
|
import dev.usbharu.hideout.core.domain.model.media.MediaRepository
|
||||||
import dev.usbharu.hideout.core.domain.model.media.toMediaAttachments
|
import dev.usbharu.hideout.core.domain.model.media.toMediaAttachments
|
||||||
import dev.usbharu.hideout.core.domain.model.post.PostRepository
|
import dev.usbharu.hideout.core.domain.model.post.PostRepository
|
||||||
|
import dev.usbharu.hideout.core.domain.model.relationship.RelationshipRepository
|
||||||
import dev.usbharu.hideout.core.service.post.PostCreateDto
|
import dev.usbharu.hideout.core.service.post.PostCreateDto
|
||||||
import dev.usbharu.hideout.core.service.post.PostService
|
import dev.usbharu.hideout.core.service.post.PostService
|
||||||
|
import dev.usbharu.hideout.core.service.reaction.ReactionService
|
||||||
import dev.usbharu.hideout.domain.mastodon.model.generated.Status
|
import dev.usbharu.hideout.domain.mastodon.model.generated.Status
|
||||||
|
import dev.usbharu.hideout.domain.mastodon.model.generated.Status.Visibility.*
|
||||||
import dev.usbharu.hideout.mastodon.interfaces.api.status.StatusesRequest
|
import dev.usbharu.hideout.mastodon.interfaces.api.status.StatusesRequest
|
||||||
import dev.usbharu.hideout.mastodon.interfaces.api.status.toPostVisibility
|
import dev.usbharu.hideout.mastodon.interfaces.api.status.toPostVisibility
|
||||||
import dev.usbharu.hideout.mastodon.interfaces.api.status.toStatusVisibility
|
import dev.usbharu.hideout.mastodon.interfaces.api.status.toStatusVisibility
|
||||||
|
import dev.usbharu.hideout.mastodon.query.StatusQueryService
|
||||||
import dev.usbharu.hideout.mastodon.service.account.AccountService
|
import dev.usbharu.hideout.mastodon.service.account.AccountService
|
||||||
|
import dev.usbharu.hideout.util.EmojiUtil
|
||||||
import org.slf4j.LoggerFactory
|
import org.slf4j.LoggerFactory
|
||||||
import org.springframework.stereotype.Service
|
import org.springframework.stereotype.Service
|
||||||
import java.time.Instant
|
import java.time.Instant
|
||||||
|
@ -22,16 +29,38 @@ interface StatusesApiService {
|
||||||
statusesRequest: StatusesRequest,
|
statusesRequest: StatusesRequest,
|
||||||
userId: Long
|
userId: Long
|
||||||
): Status
|
): Status
|
||||||
|
|
||||||
|
suspend fun findById(
|
||||||
|
id: Long,
|
||||||
|
userId: Long?
|
||||||
|
): Status?
|
||||||
|
|
||||||
|
suspend fun emojiReactions(
|
||||||
|
postId: Long,
|
||||||
|
userId: Long,
|
||||||
|
emojiName: String
|
||||||
|
): Status?
|
||||||
|
|
||||||
|
suspend fun removeEmojiReactions(
|
||||||
|
postId: Long,
|
||||||
|
userId: Long,
|
||||||
|
emojiName: String
|
||||||
|
): Status?
|
||||||
}
|
}
|
||||||
|
|
||||||
@Service
|
@Service
|
||||||
|
@Suppress("LongParameterList")
|
||||||
class StatsesApiServiceImpl(
|
class StatsesApiServiceImpl(
|
||||||
private val postService: PostService,
|
private val postService: PostService,
|
||||||
private val accountService: AccountService,
|
private val accountService: AccountService,
|
||||||
private val mediaRepository: MediaRepository,
|
private val mediaRepository: MediaRepository,
|
||||||
private val transaction: Transaction,
|
private val transaction: Transaction,
|
||||||
private val actorRepository: ActorRepository,
|
private val actorRepository: ActorRepository,
|
||||||
private val postRepository: PostRepository
|
private val postRepository: PostRepository,
|
||||||
|
private val statusQueryService: StatusQueryService,
|
||||||
|
private val relationshipRepository: RelationshipRepository,
|
||||||
|
private val reactionService: ReactionService,
|
||||||
|
private val emojiService: EmojiService
|
||||||
) :
|
) :
|
||||||
StatusesApiService {
|
StatusesApiService {
|
||||||
override suspend fun postStatus(
|
override suspend fun postStatus(
|
||||||
|
@ -95,6 +124,61 @@ class StatsesApiServiceImpl(
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
override suspend fun findById(id: Long, userId: Long?): Status? {
|
||||||
|
val status = statusQueryService.findByPostId(id)
|
||||||
|
|
||||||
|
return status(status, userId)
|
||||||
|
}
|
||||||
|
|
||||||
|
private suspend fun status(
|
||||||
|
status: Status,
|
||||||
|
userId: Long?
|
||||||
|
): Status? {
|
||||||
|
return when (status.visibility) {
|
||||||
|
public -> status
|
||||||
|
unlisted -> status
|
||||||
|
private -> {
|
||||||
|
if (userId == null) {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
val relationship =
|
||||||
|
relationshipRepository.findByUserIdAndTargetUserId(userId, status.account.id.toLong())
|
||||||
|
?: return null
|
||||||
|
if (relationship.following) {
|
||||||
|
return status
|
||||||
|
}
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
direct -> null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override suspend fun emojiReactions(postId: Long, userId: Long, emojiName: String): Status? {
|
||||||
|
status(statusQueryService.findByPostId(postId), userId) ?: return null
|
||||||
|
|
||||||
|
val emoji = try {
|
||||||
|
if (EmojiUtil.isEmoji(emojiName)) {
|
||||||
|
UnicodeEmoji(emojiName)
|
||||||
|
} else {
|
||||||
|
emojiService.findByEmojiName(emojiName)!!
|
||||||
|
}
|
||||||
|
} catch (_: IllegalStateException) {
|
||||||
|
UnicodeEmoji("❤")
|
||||||
|
} catch (_: NullPointerException) {
|
||||||
|
UnicodeEmoji("❤")
|
||||||
|
}
|
||||||
|
reactionService.sendReaction(emoji, userId, postId)
|
||||||
|
return statusQueryService.findByPostId(postId)
|
||||||
|
}
|
||||||
|
|
||||||
|
override suspend fun removeEmojiReactions(postId: Long, userId: Long, emojiName: String): Status? {
|
||||||
|
reactionService.removeReaction(userId, postId)
|
||||||
|
|
||||||
|
return status(statusQueryService.findByPostId(postId), userId)
|
||||||
|
}
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
private val logger = LoggerFactory.getLogger(StatusesApiService::class.java)
|
private val logger = LoggerFactory.getLogger(StatusesApiService::class.java)
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,14 @@
|
||||||
|
package dev.usbharu.hideout.util
|
||||||
|
|
||||||
|
import Emojis
|
||||||
|
|
||||||
|
object EmojiUtil {
|
||||||
|
|
||||||
|
val emojiMap by lazy {
|
||||||
|
Emojis.allEmojis
|
||||||
|
.associate { it.code.replace(" ", "-") to it.char }
|
||||||
|
.filterValues { it != "™" }
|
||||||
|
}
|
||||||
|
|
||||||
|
fun isEmoji(string: String): Boolean = emojiMap.any { it.value == string }
|
||||||
|
}
|
|
@ -19,7 +19,5 @@ class TempFile<T : Path?>(val path: T) : AutoCloseable {
|
||||||
return path == other.path
|
return path == other.path
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun hashCode(): Int {
|
override fun hashCode(): Int = path?.hashCode() ?: 0
|
||||||
return path?.hashCode() ?: 0
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,3 +1,15 @@
|
||||||
|
create table if not exists emojis
|
||||||
|
(
|
||||||
|
id bigint primary key,
|
||||||
|
"name" varchar(1000) not null,
|
||||||
|
domain varchar(1000) not null,
|
||||||
|
instance_id bigint null,
|
||||||
|
url varchar(255) not null unique,
|
||||||
|
category varchar(255),
|
||||||
|
created_at timestamp not null default current_timestamp,
|
||||||
|
unique ("name", instance_id)
|
||||||
|
);
|
||||||
|
|
||||||
create table if not exists instance
|
create table if not exists instance
|
||||||
(
|
(
|
||||||
id bigint primary key,
|
id bigint primary key,
|
||||||
|
@ -13,6 +25,10 @@ create table if not exists instance
|
||||||
moderation_note varchar(10000) not null,
|
moderation_note varchar(10000) not null,
|
||||||
created_at timestamp not null
|
created_at timestamp not null
|
||||||
);
|
);
|
||||||
|
|
||||||
|
alter table emojis
|
||||||
|
add constraint fk_emojis_instance_id__id foreign key (instance_id) references instance (id) on delete cascade on update cascade;
|
||||||
|
|
||||||
create table if not exists actors
|
create table if not exists actors
|
||||||
(
|
(
|
||||||
id bigint primary key,
|
id bigint primary key,
|
||||||
|
@ -35,6 +51,7 @@ create table if not exists actors
|
||||||
followers_count int not null,
|
followers_count int not null,
|
||||||
posts_count int not null,
|
posts_count int not null,
|
||||||
last_post_at timestamp null default null,
|
last_post_at timestamp null default null,
|
||||||
|
emojis varchar(300) not null default '',
|
||||||
unique ("name", "domain"),
|
unique ("name", "domain"),
|
||||||
constraint fk_actors_instance__id foreign key ("instance") references instance (id) on delete restrict on update restrict
|
constraint fk_actors_instance__id foreign key ("instance") references instance (id) on delete restrict on update restrict
|
||||||
);
|
);
|
||||||
|
@ -99,17 +116,35 @@ alter table posts_media
|
||||||
add constraint fk_posts_media_post_id__id foreign key (post_id) references posts (id) on delete cascade on update cascade;
|
add constraint fk_posts_media_post_id__id foreign key (post_id) references posts (id) on delete cascade on update cascade;
|
||||||
alter table posts_media
|
alter table posts_media
|
||||||
add constraint fk_posts_media_media_id__id foreign key (media_id) references media (id) on delete cascade on update cascade;
|
add constraint fk_posts_media_media_id__id foreign key (media_id) references media (id) on delete cascade on update cascade;
|
||||||
|
|
||||||
|
create table if not exists posts_emojis
|
||||||
|
(
|
||||||
|
post_id bigint not null,
|
||||||
|
emoji_id bigint not null,
|
||||||
|
constraint pk_postsemoji primary key (post_id, emoji_id)
|
||||||
|
);
|
||||||
|
|
||||||
|
alter table posts_emojis
|
||||||
|
add constraint fk_posts_emojis_post_id__id foreign key (post_id) references posts (id) on delete cascade on update cascade;
|
||||||
|
alter table posts_emojis
|
||||||
|
add constraint fk_posts_emojis_emoji_id__id foreign key (emoji_id) references emojis (id) on delete cascade on update cascade;
|
||||||
|
|
||||||
create table if not exists reactions
|
create table if not exists reactions
|
||||||
(
|
(
|
||||||
id bigserial primary key,
|
id bigint primary key,
|
||||||
emoji_id bigint not null,
|
unicode_emoji varchar(255) null default null,
|
||||||
|
custom_emoji_id bigint null default null,
|
||||||
post_id bigint not null,
|
post_id bigint not null,
|
||||||
actor_id bigint not null
|
actor_id bigint not null,
|
||||||
|
unique (post_id, actor_id)
|
||||||
);
|
);
|
||||||
alter table reactions
|
alter table reactions
|
||||||
add constraint fk_reactions_post_id__id foreign key (post_id) references posts (id) on delete restrict on update restrict;
|
add constraint fk_reactions_post_id__id foreign key (post_id) references posts (id) on delete restrict on update restrict;
|
||||||
alter table reactions
|
alter table reactions
|
||||||
add constraint fk_reactions_actor_id__id foreign key (actor_id) references actors (id) on delete restrict on update restrict;
|
add constraint fk_reactions_actor_id__id foreign key (actor_id) references actors (id) on delete restrict on update restrict;
|
||||||
|
alter table reactions
|
||||||
|
add constraint fk_reactions_custom_emoji_id__id foreign key (custom_emoji_id) references emojis (id) on delete cascade on update cascade;
|
||||||
|
|
||||||
create table if not exists timelines
|
create table if not exists timelines
|
||||||
(
|
(
|
||||||
id bigint primary key,
|
id bigint primary key,
|
||||||
|
@ -124,7 +159,8 @@ create table if not exists timelines
|
||||||
"sensitive" boolean not null,
|
"sensitive" boolean not null,
|
||||||
is_local boolean not null,
|
is_local boolean not null,
|
||||||
is_pure_repost boolean not null,
|
is_pure_repost boolean not null,
|
||||||
media_ids varchar(255) not null
|
media_ids varchar(255) not null,
|
||||||
|
emoji_ids varchar(255) not null
|
||||||
);
|
);
|
||||||
|
|
||||||
create table if not exists application_authorization
|
create table if not exists application_authorization
|
||||||
|
|
|
@ -152,6 +152,79 @@ paths:
|
||||||
schema:
|
schema:
|
||||||
$ref: "#/components/schemas/Status"
|
$ref: "#/components/schemas/Status"
|
||||||
|
|
||||||
|
/api/v1/statuses/{id}:
|
||||||
|
get:
|
||||||
|
tags:
|
||||||
|
- status
|
||||||
|
security:
|
||||||
|
- OAuth2:
|
||||||
|
- "write:statuses"
|
||||||
|
parameters:
|
||||||
|
- in: path
|
||||||
|
name: id
|
||||||
|
required: true
|
||||||
|
schema:
|
||||||
|
type: string
|
||||||
|
responses:
|
||||||
|
200:
|
||||||
|
description: 成功
|
||||||
|
content:
|
||||||
|
application/json:
|
||||||
|
schema:
|
||||||
|
$ref: "#/components/schemas/Status"
|
||||||
|
|
||||||
|
/api/v1/statuses/{id}/emoji_reactions/{emoji}:
|
||||||
|
put:
|
||||||
|
tags:
|
||||||
|
- status
|
||||||
|
security:
|
||||||
|
- OAuth2:
|
||||||
|
- "write:statuses"
|
||||||
|
parameters:
|
||||||
|
- in: path
|
||||||
|
name: id
|
||||||
|
required: true
|
||||||
|
schema:
|
||||||
|
type: string
|
||||||
|
- in: path
|
||||||
|
name: emoji
|
||||||
|
required: true
|
||||||
|
schema:
|
||||||
|
type: string
|
||||||
|
responses:
|
||||||
|
200:
|
||||||
|
description: 成功
|
||||||
|
content:
|
||||||
|
application/json:
|
||||||
|
schema:
|
||||||
|
$ref: "#/components/schemas/Status"
|
||||||
|
|
||||||
|
delete:
|
||||||
|
tags:
|
||||||
|
- status
|
||||||
|
security:
|
||||||
|
- OAuth2:
|
||||||
|
- "write:statuses"
|
||||||
|
parameters:
|
||||||
|
- in: path
|
||||||
|
name: id
|
||||||
|
required: true
|
||||||
|
schema:
|
||||||
|
type: string
|
||||||
|
- in: path
|
||||||
|
name: emoji
|
||||||
|
required: true
|
||||||
|
schema:
|
||||||
|
type: string
|
||||||
|
responses:
|
||||||
|
200:
|
||||||
|
description: 成功
|
||||||
|
content:
|
||||||
|
application/json:
|
||||||
|
schema:
|
||||||
|
$ref: "#/components/schemas/Status"
|
||||||
|
|
||||||
|
|
||||||
/api/v1/apps:
|
/api/v1/apps:
|
||||||
post:
|
post:
|
||||||
tags:
|
tags:
|
||||||
|
|
|
@ -3,6 +3,7 @@ package dev.usbharu.hideout
|
||||||
import com.fasterxml.jackson.module.kotlin.isKotlinClass
|
import com.fasterxml.jackson.module.kotlin.isKotlinClass
|
||||||
import com.jparams.verifier.tostring.ToStringVerifier
|
import com.jparams.verifier.tostring.ToStringVerifier
|
||||||
import com.jparams.verifier.tostring.preset.Presets
|
import com.jparams.verifier.tostring.preset.Presets
|
||||||
|
import dev.usbharu.hideout.core.domain.model.emoji.UnicodeEmoji
|
||||||
import nl.jqno.equalsverifier.EqualsVerifier
|
import nl.jqno.equalsverifier.EqualsVerifier
|
||||||
import nl.jqno.equalsverifier.Warning
|
import nl.jqno.equalsverifier.Warning
|
||||||
import nl.jqno.equalsverifier.internal.reflection.PackageScanner
|
import nl.jqno.equalsverifier.internal.reflection.PackageScanner
|
||||||
|
@ -95,6 +96,7 @@ class EqualsAndToStringTest {
|
||||||
.filter {
|
.filter {
|
||||||
it.superclass == Any::class.java || it.superclass?.packageName?.startsWith("dev.usbharu") ?: true
|
it.superclass == Any::class.java || it.superclass?.packageName?.startsWith("dev.usbharu") ?: true
|
||||||
}
|
}
|
||||||
|
.filterNot { it == UnicodeEmoji::class.java }
|
||||||
.map {
|
.map {
|
||||||
|
|
||||||
dynamicTest(it.name) {
|
dynamicTest(it.name) {
|
||||||
|
|
|
@ -3,6 +3,7 @@ package dev.usbharu.hideout.activitypub.domain.model
|
||||||
import com.fasterxml.jackson.module.kotlin.readValue
|
import com.fasterxml.jackson.module.kotlin.readValue
|
||||||
import dev.usbharu.hideout.activitypub.service.objects.note.APNoteServiceImpl.Companion.public
|
import dev.usbharu.hideout.activitypub.service.objects.note.APNoteServiceImpl.Companion.public
|
||||||
import dev.usbharu.hideout.application.config.ActivityPubConfig
|
import dev.usbharu.hideout.application.config.ActivityPubConfig
|
||||||
|
import org.assertj.core.api.Assertions.assertThat
|
||||||
import org.junit.jupiter.api.Assertions.assertEquals
|
import org.junit.jupiter.api.Assertions.assertEquals
|
||||||
import org.junit.jupiter.api.Test
|
import org.junit.jupiter.api.Test
|
||||||
|
|
||||||
|
@ -51,11 +52,7 @@ class NoteSerializeTest {
|
||||||
"attachment": [],
|
"attachment": [],
|
||||||
"sensitive": false,
|
"sensitive": false,
|
||||||
"tag": [
|
"tag": [
|
||||||
{
|
|
||||||
"type": "Mention",
|
|
||||||
"href": "https://calckey.jp/users/9bu1xzwjyb",
|
|
||||||
"name": "@trapezial@calckey.jp"
|
|
||||||
}
|
|
||||||
]
|
]
|
||||||
}"""
|
}"""
|
||||||
|
|
||||||
|
@ -77,4 +74,105 @@ class NoteSerializeTest {
|
||||||
)
|
)
|
||||||
assertEquals(note, readValue)
|
assertEquals(note, readValue)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun 絵文字付きNoteのデシリアライズができる() {
|
||||||
|
val json = """{
|
||||||
|
"@context": [
|
||||||
|
"https://www.w3.org/ns/activitystreams",
|
||||||
|
"https://w3id.org/security/v1",
|
||||||
|
{
|
||||||
|
"manuallyApprovesFollowers": "as:manuallyApprovesFollowers",
|
||||||
|
"sensitive": "as:sensitive",
|
||||||
|
"Hashtag": "as:Hashtag",
|
||||||
|
"quoteUrl": "as:quoteUrl",
|
||||||
|
"toot": "http://joinmastodon.org/ns#",
|
||||||
|
"Emoji": "toot:Emoji",
|
||||||
|
"featured": "toot:featured",
|
||||||
|
"discoverable": "toot:discoverable",
|
||||||
|
"schema": "http://schema.org#",
|
||||||
|
"PropertyValue": "schema:PropertyValue",
|
||||||
|
"value": "schema:value",
|
||||||
|
"misskey": "https://misskey-hub.net/ns#",
|
||||||
|
"_misskey_content": "misskey:_misskey_content",
|
||||||
|
"_misskey_quote": "misskey:_misskey_quote",
|
||||||
|
"_misskey_reaction": "misskey:_misskey_reaction",
|
||||||
|
"_misskey_votes": "misskey:_misskey_votes",
|
||||||
|
"_misskey_summary": "misskey:_misskey_summary",
|
||||||
|
"isCat": "misskey:isCat",
|
||||||
|
"vcard": "http://www.w3.org/2006/vcard/ns#"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"id": "https://misskey.usbharu.dev/notes/9nj1omt1rn",
|
||||||
|
"type": "Note",
|
||||||
|
"attributedTo": "https://misskey.usbharu.dev/users/97ws8y3rj6",
|
||||||
|
"content": "<p>:oyasumi:</p>",
|
||||||
|
"_misskey_content": ":oyasumi:",
|
||||||
|
"source": {
|
||||||
|
"content": ":oyasumi:",
|
||||||
|
"mediaType": "text/x.misskeymarkdown"
|
||||||
|
},
|
||||||
|
"published": "2023-12-21T17:32:36.853Z",
|
||||||
|
"to": [
|
||||||
|
"https://www.w3.org/ns/activitystreams#Public"
|
||||||
|
],
|
||||||
|
"cc": [
|
||||||
|
"https://misskey.usbharu.dev/users/97ws8y3rj6/followers"
|
||||||
|
],
|
||||||
|
"inReplyTo": null,
|
||||||
|
"attachment": [],
|
||||||
|
"sensitive": false,
|
||||||
|
"tag": [
|
||||||
|
{
|
||||||
|
"id": "https://misskey.usbharu.dev/emojis/oyasumi",
|
||||||
|
"type": "Emoji",
|
||||||
|
"name": ":oyasumi:",
|
||||||
|
"updated": "2023-04-07T08:21:25.246Z",
|
||||||
|
"icon": {
|
||||||
|
"type": "Image",
|
||||||
|
"mediaType": "image/png",
|
||||||
|
"url": "https://s3misskey.usbharu.dev/misskey-minio/misskey-minio/data/cf8db710-1d70-4076-8a00-dbb28131096e.png"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}"""
|
||||||
|
|
||||||
|
|
||||||
|
val objectMapper = ActivityPubConfig().objectMapper()
|
||||||
|
|
||||||
|
val expected = Note(
|
||||||
|
type = emptyList(),
|
||||||
|
id = "https://misskey.usbharu.dev/notes/9nj1omt1rn",
|
||||||
|
attributedTo = "https://misskey.usbharu.dev/users/97ws8y3rj6",
|
||||||
|
content = "<p>\u200B:oyasumi:\u200B</p>",
|
||||||
|
published = "2023-12-21T17:32:36.853Z",
|
||||||
|
to = listOf("https://www.w3.org/ns/activitystreams#Public"),
|
||||||
|
cc = listOf("https://misskey.usbharu.dev/users/97ws8y3rj6/followers"),
|
||||||
|
sensitive = false,
|
||||||
|
inReplyTo = null,
|
||||||
|
attachment = emptyList(),
|
||||||
|
tag = listOf(
|
||||||
|
Emoji(
|
||||||
|
type = emptyList(),
|
||||||
|
name = ":oyasumi:",
|
||||||
|
id = "https://misskey.usbharu.dev/emojis/oyasumi",
|
||||||
|
updated = "2023-04-07T08:21:25.246Z",
|
||||||
|
icon = Image(
|
||||||
|
type = emptyList(),
|
||||||
|
mediaType = "image/png",
|
||||||
|
url = "https://s3misskey.usbharu.dev/misskey-minio/misskey-minio/data/cf8db710-1d70-4076-8a00-dbb28131096e.png"
|
||||||
|
)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
expected.context = listOf(
|
||||||
|
"https://www.w3.org/ns/activitystreams",
|
||||||
|
"https://w3id.org/security/v1"
|
||||||
|
)
|
||||||
|
|
||||||
|
val note = objectMapper.readValue<Note>(json)
|
||||||
|
|
||||||
|
assertThat(note).isEqualTo(expected)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -3,6 +3,7 @@ package dev.usbharu.hideout.activitypub.service.activity.like
|
||||||
|
|
||||||
import dev.usbharu.hideout.application.service.id.TwitterSnowflakeIdGenerateService
|
import dev.usbharu.hideout.application.service.id.TwitterSnowflakeIdGenerateService
|
||||||
import dev.usbharu.hideout.core.domain.model.actor.ActorRepository
|
import dev.usbharu.hideout.core.domain.model.actor.ActorRepository
|
||||||
|
import dev.usbharu.hideout.core.domain.model.emoji.UnicodeEmoji
|
||||||
import dev.usbharu.hideout.core.domain.model.post.PostRepository
|
import dev.usbharu.hideout.core.domain.model.post.PostRepository
|
||||||
import dev.usbharu.hideout.core.domain.model.reaction.Reaction
|
import dev.usbharu.hideout.core.domain.model.reaction.Reaction
|
||||||
import dev.usbharu.hideout.core.external.job.DeliverReactionJob
|
import dev.usbharu.hideout.core.external.job.DeliverReactionJob
|
||||||
|
@ -48,7 +49,7 @@ class APReactionServiceImplTest {
|
||||||
apReactionServiceImpl.reaction(
|
apReactionServiceImpl.reaction(
|
||||||
Reaction(
|
Reaction(
|
||||||
id = TwitterSnowflakeIdGenerateService.generateId(),
|
id = TwitterSnowflakeIdGenerateService.generateId(),
|
||||||
emojiId = 0,
|
emoji = UnicodeEmoji("❤"),
|
||||||
postId = post.id,
|
postId = post.id,
|
||||||
actorId = user.id
|
actorId = user.id
|
||||||
)
|
)
|
||||||
|
@ -88,7 +89,7 @@ class APReactionServiceImplTest {
|
||||||
apReactionServiceImpl.removeReaction(
|
apReactionServiceImpl.removeReaction(
|
||||||
Reaction(
|
Reaction(
|
||||||
id = TwitterSnowflakeIdGenerateService.generateId(),
|
id = TwitterSnowflakeIdGenerateService.generateId(),
|
||||||
emojiId = 0,
|
emoji = UnicodeEmoji("❤"),
|
||||||
postId = post.id,
|
postId = post.id,
|
||||||
actorId = user.id
|
actorId = user.id
|
||||||
)
|
)
|
||||||
|
|
|
@ -73,6 +73,7 @@ class APNoteServiceImplTest {
|
||||||
apResourceResolveService = mock(),
|
apResourceResolveService = mock(),
|
||||||
postBuilder = Post.PostBuilder(CharacterLimit()),
|
postBuilder = Post.PostBuilder(CharacterLimit()),
|
||||||
noteQueryService = noteQueryService,
|
noteQueryService = noteQueryService,
|
||||||
|
mock(),
|
||||||
mock()
|
mock()
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -142,7 +143,8 @@ class APNoteServiceImplTest {
|
||||||
apResourceResolveService = apResourceResolveService,
|
apResourceResolveService = apResourceResolveService,
|
||||||
postBuilder = Post.PostBuilder(CharacterLimit()),
|
postBuilder = Post.PostBuilder(CharacterLimit()),
|
||||||
noteQueryService = noteQueryService,
|
noteQueryService = noteQueryService,
|
||||||
mock()
|
mock(),
|
||||||
|
mock { }
|
||||||
)
|
)
|
||||||
|
|
||||||
val actual = apNoteServiceImpl.fetchNote(url)
|
val actual = apNoteServiceImpl.fetchNote(url)
|
||||||
|
@ -190,6 +192,7 @@ class APNoteServiceImplTest {
|
||||||
apResourceResolveService = apResourceResolveService,
|
apResourceResolveService = apResourceResolveService,
|
||||||
postBuilder = Post.PostBuilder(CharacterLimit()),
|
postBuilder = Post.PostBuilder(CharacterLimit()),
|
||||||
noteQueryService = noteQueryService,
|
noteQueryService = noteQueryService,
|
||||||
|
mock(),
|
||||||
mock()
|
mock()
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -240,6 +243,7 @@ class APNoteServiceImplTest {
|
||||||
apResourceResolveService = mock(),
|
apResourceResolveService = mock(),
|
||||||
postBuilder = postBuilder,
|
postBuilder = postBuilder,
|
||||||
noteQueryService = noteQueryService,
|
noteQueryService = noteQueryService,
|
||||||
|
mock(),
|
||||||
mock()
|
mock()
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -292,6 +296,7 @@ class APNoteServiceImplTest {
|
||||||
apResourceResolveService = mock(),
|
apResourceResolveService = mock(),
|
||||||
postBuilder = postBuilder,
|
postBuilder = postBuilder,
|
||||||
noteQueryService = noteQueryService,
|
noteQueryService = noteQueryService,
|
||||||
|
mock(),
|
||||||
mock()
|
mock()
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
|
@ -3,10 +3,10 @@ package dev.usbharu.hideout.core.service.reaction
|
||||||
|
|
||||||
import dev.usbharu.hideout.activitypub.service.activity.like.APReactionService
|
import dev.usbharu.hideout.activitypub.service.activity.like.APReactionService
|
||||||
import dev.usbharu.hideout.application.service.id.TwitterSnowflakeIdGenerateService
|
import dev.usbharu.hideout.application.service.id.TwitterSnowflakeIdGenerateService
|
||||||
|
import dev.usbharu.hideout.core.domain.model.emoji.UnicodeEmoji
|
||||||
import dev.usbharu.hideout.core.domain.model.reaction.Reaction
|
import dev.usbharu.hideout.core.domain.model.reaction.Reaction
|
||||||
import dev.usbharu.hideout.core.domain.model.reaction.ReactionRepository
|
import dev.usbharu.hideout.core.domain.model.reaction.ReactionRepository
|
||||||
import kotlinx.coroutines.test.runTest
|
import kotlinx.coroutines.test.runTest
|
||||||
import org.jetbrains.exposed.exceptions.ExposedSQLException
|
|
||||||
import org.junit.jupiter.api.Test
|
import org.junit.jupiter.api.Test
|
||||||
import org.junit.jupiter.api.extension.ExtendWith
|
import org.junit.jupiter.api.extension.ExtendWith
|
||||||
import org.mockito.InjectMocks
|
import org.mockito.InjectMocks
|
||||||
|
@ -32,59 +32,33 @@ class ReactionServiceImplTest {
|
||||||
|
|
||||||
val post = PostBuilder.of()
|
val post = PostBuilder.of()
|
||||||
|
|
||||||
whenever(reactionRepository.existByPostIdAndActorIdAndEmojiId(eq(post.id), eq(post.actorId), eq(0))).doReturn(
|
whenever(reactionRepository.existByPostIdAndActor(eq(post.id), eq(post.actorId))).doReturn(
|
||||||
false
|
false
|
||||||
)
|
)
|
||||||
val generateId = TwitterSnowflakeIdGenerateService.generateId()
|
val generateId = TwitterSnowflakeIdGenerateService.generateId()
|
||||||
whenever(reactionRepository.generateId()).doReturn(generateId)
|
whenever(reactionRepository.generateId()).doReturn(generateId)
|
||||||
|
|
||||||
reactionServiceImpl.receiveReaction("❤", "example.com", post.actorId, post.id)
|
reactionServiceImpl.receiveReaction(UnicodeEmoji("❤"), post.actorId, post.id)
|
||||||
|
|
||||||
verify(reactionRepository, times(1)).save(eq(Reaction(generateId, 0, post.id, post.actorId)))
|
verify(reactionRepository, times(1)).save(eq(Reaction(generateId, UnicodeEmoji("❤"), post.id, post.actorId)))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun `receiveReaction リアクションが既に作成されていることを検知出来ずに例外が発生した場合は何もしない`() = runTest {
|
fun `receiveReaction リアクションが既に作成されている場合削除して新しく作成`() = runTest() {
|
||||||
val post = PostBuilder.of()
|
val post = PostBuilder.of()
|
||||||
|
whenever(reactionRepository.existByPostIdAndActor(eq(post.id), eq(post.actorId))).doReturn(
|
||||||
whenever(reactionRepository.existByPostIdAndActorIdAndEmojiId(eq(post.id), eq(post.actorId), eq(0))).doReturn(
|
|
||||||
false
|
|
||||||
)
|
|
||||||
val generateId = TwitterSnowflakeIdGenerateService.generateId()
|
|
||||||
whenever(
|
|
||||||
reactionRepository.save(
|
|
||||||
eq(
|
|
||||||
Reaction(
|
|
||||||
id = generateId,
|
|
||||||
emojiId = 0,
|
|
||||||
postId = post.id,
|
|
||||||
actorId = post.actorId
|
|
||||||
)
|
|
||||||
)
|
|
||||||
)
|
|
||||||
).doAnswer {
|
|
||||||
throw ExposedSQLException(
|
|
||||||
null,
|
|
||||||
emptyList(), mock()
|
|
||||||
)
|
|
||||||
}
|
|
||||||
whenever(reactionRepository.generateId()).doReturn(generateId)
|
|
||||||
|
|
||||||
reactionServiceImpl.receiveReaction("❤", "example.com", post.actorId, post.id)
|
|
||||||
|
|
||||||
verify(reactionRepository, times(1)).save(eq(Reaction(generateId, 0, post.id, post.actorId)))
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
fun `receiveReaction リアクションが既に作成されている場合は何もしない`() = runTest() {
|
|
||||||
val post = PostBuilder.of()
|
|
||||||
whenever(reactionRepository.existByPostIdAndActorIdAndEmojiId(eq(post.id), eq(post.actorId), eq(0))).doReturn(
|
|
||||||
true
|
true
|
||||||
)
|
)
|
||||||
|
|
||||||
reactionServiceImpl.receiveReaction("❤", "example.com", post.actorId, post.id)
|
val generateId = TwitterSnowflakeIdGenerateService.generateId()
|
||||||
|
|
||||||
verify(reactionRepository, never()).save(any())
|
whenever(reactionRepository.generateId()).doReturn(generateId)
|
||||||
|
|
||||||
|
reactionServiceImpl.receiveReaction(UnicodeEmoji("❤"), post.actorId, post.id)
|
||||||
|
|
||||||
|
verify(reactionRepository, times(1)).deleteByPostIdAndActorId(post.id, post.actorId)
|
||||||
|
verify(reactionRepository, times(1)).save(Reaction(generateId, UnicodeEmoji("❤"), post.id, post.actorId))
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
|
@ -96,10 +70,10 @@ class ReactionServiceImplTest {
|
||||||
val generateId = TwitterSnowflakeIdGenerateService.generateId()
|
val generateId = TwitterSnowflakeIdGenerateService.generateId()
|
||||||
whenever(reactionRepository.generateId()).doReturn(generateId)
|
whenever(reactionRepository.generateId()).doReturn(generateId)
|
||||||
|
|
||||||
reactionServiceImpl.sendReaction("❤", post.actorId, post.id)
|
reactionServiceImpl.sendReaction(UnicodeEmoji("❤"), post.actorId, post.id)
|
||||||
|
|
||||||
verify(reactionRepository, times(1)).save(eq(Reaction(generateId, 0, post.id, post.actorId)))
|
verify(reactionRepository, times(1)).save(eq(Reaction(generateId, UnicodeEmoji("❤"), post.id, post.actorId)))
|
||||||
verify(apReactionService, times(1)).reaction(eq(Reaction(generateId, 0, post.id, post.actorId)))
|
verify(apReactionService, times(1)).reaction(eq(Reaction(generateId, UnicodeEmoji("❤"), post.id, post.actorId)))
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
|
@ -107,30 +81,30 @@ class ReactionServiceImplTest {
|
||||||
val post = PostBuilder.of()
|
val post = PostBuilder.of()
|
||||||
val id = TwitterSnowflakeIdGenerateService.generateId()
|
val id = TwitterSnowflakeIdGenerateService.generateId()
|
||||||
whenever(reactionRepository.findByPostIdAndActorIdAndEmojiId(eq(post.id), eq(post.actorId), eq(0))).doReturn(
|
whenever(reactionRepository.findByPostIdAndActorIdAndEmojiId(eq(post.id), eq(post.actorId), eq(0))).doReturn(
|
||||||
Reaction(id, 0, post.id, post.actorId)
|
Reaction(id, UnicodeEmoji("❤"), post.id, post.actorId)
|
||||||
)
|
)
|
||||||
val generateId = TwitterSnowflakeIdGenerateService.generateId()
|
val generateId = TwitterSnowflakeIdGenerateService.generateId()
|
||||||
whenever(reactionRepository.generateId()).doReturn(generateId)
|
whenever(reactionRepository.generateId()).doReturn(generateId)
|
||||||
|
|
||||||
reactionServiceImpl.sendReaction("❤", post.actorId, post.id)
|
reactionServiceImpl.sendReaction(UnicodeEmoji("❤"), post.actorId, post.id)
|
||||||
|
|
||||||
|
|
||||||
verify(reactionRepository, times(1)).delete(eq(Reaction(id, 0, post.id, post.actorId)))
|
verify(reactionRepository, times(1)).delete(eq(Reaction(id, UnicodeEmoji("❤"), post.id, post.actorId)))
|
||||||
verify(reactionRepository, times(1)).save(eq(Reaction(generateId, 0, post.id, post.actorId)))
|
verify(reactionRepository, times(1)).save(eq(Reaction(generateId, UnicodeEmoji("❤"), post.id, post.actorId)))
|
||||||
verify(apReactionService, times(1)).removeReaction(eq(Reaction(id, 0, post.id, post.actorId)))
|
verify(apReactionService, times(1)).removeReaction(eq(Reaction(id, UnicodeEmoji("❤"), post.id, post.actorId)))
|
||||||
verify(apReactionService, times(1)).reaction(eq(Reaction(generateId, 0, post.id, post.actorId)))
|
verify(apReactionService, times(1)).reaction(eq(Reaction(generateId, UnicodeEmoji("❤"), post.id, post.actorId)))
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun `removeReaction リアクションが存在する場合削除して配送`() = runTest {
|
fun `removeReaction リアクションが存在する場合削除して配送`() = runTest {
|
||||||
val post = PostBuilder.of()
|
val post = PostBuilder.of()
|
||||||
whenever(reactionRepository.findByPostIdAndActorIdAndEmojiId(eq(post.id), eq(post.actorId), eq(0))).doReturn(
|
whenever(reactionRepository.findByPostIdAndActorIdAndEmojiId(eq(post.id), eq(post.actorId), eq(0))).doReturn(
|
||||||
Reaction(0, 0, post.id, post.actorId)
|
Reaction(0, UnicodeEmoji("❤"), post.id, post.actorId)
|
||||||
)
|
)
|
||||||
|
|
||||||
reactionServiceImpl.removeReaction(post.actorId, post.id)
|
reactionServiceImpl.removeReaction(post.actorId, post.id)
|
||||||
|
|
||||||
verify(reactionRepository, times(1)).delete(eq(Reaction(0, 0, post.id, post.actorId)))
|
verify(reactionRepository, times(1)).delete(eq(Reaction(0, UnicodeEmoji("❤"), post.id, post.actorId)))
|
||||||
verify(apReactionService, times(1)).removeReaction(eq(Reaction(0, 0, post.id, post.actorId)))
|
verify(apReactionService, times(1)).removeReaction(eq(Reaction(0, UnicodeEmoji("❤"), post.id, post.actorId)))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,41 @@
|
||||||
|
package dev.usbharu.hideout.util
|
||||||
|
|
||||||
|
import org.assertj.core.api.Assertions.assertThat
|
||||||
|
import org.junit.jupiter.api.Test
|
||||||
|
import org.junit.jupiter.params.ParameterizedTest
|
||||||
|
import org.junit.jupiter.params.provider.ValueSource
|
||||||
|
|
||||||
|
class EmojiUtilTest {
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun 絵文字を判定できる() {
|
||||||
|
val emoji = "❤"
|
||||||
|
val actual = EmojiUtil.isEmoji(emoji)
|
||||||
|
|
||||||
|
assertThat(actual).isTrue()
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun ただの文字を判定できる() {
|
||||||
|
val moji = "blobblinkhyper"
|
||||||
|
val actual = EmojiUtil.isEmoji(moji)
|
||||||
|
|
||||||
|
assertThat(actual).isFalse()
|
||||||
|
}
|
||||||
|
|
||||||
|
@ParameterizedTest
|
||||||
|
@ValueSource(strings = ["❤", "🌄", "🤗", "⛺", "🧑🤝🧑", "🖐🏿"])
|
||||||
|
fun `絵文字判定`(s: String) {
|
||||||
|
val actual = EmojiUtil.isEmoji(s)
|
||||||
|
|
||||||
|
assertThat(actual).isTrue()
|
||||||
|
}
|
||||||
|
|
||||||
|
@ParameterizedTest
|
||||||
|
@ValueSource(strings = ["™", "㍂", "㌠"])
|
||||||
|
fun `文字判定`(s: String) {
|
||||||
|
val actual = EmojiUtil.isEmoji(s)
|
||||||
|
|
||||||
|
assertThat(actual).isFalse()
|
||||||
|
}
|
||||||
|
}
|
Loading…
Reference in New Issue