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")
|
||||
}
|
||||
}
|
||||
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 {
|
||||
|
@ -205,6 +214,8 @@ dependencies {
|
|||
implementation("org.bytedeco:javacv-platform:1.5.9")
|
||||
implementation("org.flywaydb:flyway-core")
|
||||
|
||||
implementation("dev.usbharu:emoji-kt:2.0.0")
|
||||
|
||||
implementation("io.ktor:ktor-client-logging-jvm:$ktor_version")
|
||||
|
||||
testImplementation("org.jetbrains.kotlin:kotlin-test-junit:$kotlin_version")
|
||||
|
|
|
@ -112,18 +112,17 @@ class InboxCommonTest {
|
|||
|
||||
@BeforeAll
|
||||
@JvmStatic
|
||||
fun beforeAll(@Autowired flyway: Flyway) {
|
||||
fun beforeAll() {
|
||||
server = MockServer.feature("classpath:federation/InboxxCommonMockServerTest.feature").http(0).build()
|
||||
_remotePort = server.port.toString()
|
||||
|
||||
flyway.clean()
|
||||
flyway.migrate()
|
||||
}
|
||||
|
||||
@AfterAll
|
||||
@JvmStatic
|
||||
fun afterAll() {
|
||||
fun afterAll(@Autowired flyway: Flyway) {
|
||||
server.stop()
|
||||
flyway.clean()
|
||||
flyway.migrate()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,7 +1,15 @@
|
|||
package mastodon.status
|
||||
|
||||
import dev.usbharu.hideout.SpringApplication
|
||||
import dev.usbharu.hideout.core.domain.model.emoji.CustomEmoji
|
||||
import dev.usbharu.hideout.core.domain.model.emoji.UnicodeEmoji
|
||||
import dev.usbharu.hideout.core.infrastructure.exposedrepository.CustomEmojis
|
||||
import dev.usbharu.hideout.core.infrastructure.exposedrepository.Reactions
|
||||
import dev.usbharu.hideout.core.infrastructure.exposedrepository.toReaction
|
||||
import org.assertj.core.api.Assertions.assertThat
|
||||
import org.flywaydb.core.Flyway
|
||||
import org.jetbrains.exposed.sql.and
|
||||
import org.jetbrains.exposed.sql.select
|
||||
import org.junit.jupiter.api.AfterAll
|
||||
import org.junit.jupiter.api.BeforeEach
|
||||
import org.junit.jupiter.api.Test
|
||||
|
@ -13,19 +21,24 @@ import org.springframework.security.core.authority.SimpleGrantedAuthority
|
|||
import org.springframework.security.test.context.support.WithAnonymousUser
|
||||
import org.springframework.security.test.web.servlet.request.SecurityMockMvcRequestPostProcessors
|
||||
import org.springframework.security.test.web.servlet.request.SecurityMockMvcRequestPostProcessors.csrf
|
||||
import org.springframework.security.test.web.servlet.request.SecurityMockMvcRequestPostProcessors.jwt
|
||||
import org.springframework.security.test.web.servlet.setup.SecurityMockMvcConfigurers
|
||||
import org.springframework.test.context.jdbc.Sql
|
||||
import org.springframework.test.web.servlet.MockMvc
|
||||
import org.springframework.test.web.servlet.post
|
||||
import org.springframework.test.web.servlet.put
|
||||
import org.springframework.test.web.servlet.setup.DefaultMockMvcBuilder
|
||||
import org.springframework.test.web.servlet.setup.MockMvcBuilders
|
||||
import org.springframework.transaction.annotation.Transactional
|
||||
import org.springframework.web.context.WebApplicationContext
|
||||
import java.time.Instant
|
||||
|
||||
@SpringBootTest(classes = [SpringApplication::class])
|
||||
@AutoConfigureMockMvc
|
||||
@Transactional
|
||||
@Sql("/sql/test-user.sql", executionPhase = Sql.ExecutionPhase.BEFORE_TEST_CLASS)
|
||||
@Sql("/sql/test-post.sql", executionPhase = Sql.ExecutionPhase.BEFORE_TEST_CLASS)
|
||||
@Sql("/sql/test-custom-emoji.sql", executionPhase = Sql.ExecutionPhase.BEFORE_TEST_CLASS)
|
||||
class StatusTest {
|
||||
|
||||
@Autowired
|
||||
|
@ -124,7 +137,6 @@ class StatusTest {
|
|||
}
|
||||
|
||||
@Test
|
||||
@Sql("/sql/test-post.sql")
|
||||
fun in_reply_to_idを指定したら返信として処理される() {
|
||||
mockMvc
|
||||
.post("/api/v1/statuses") {
|
||||
|
@ -145,6 +157,64 @@ class StatusTest {
|
|||
.andExpect { jsonPath("\$.in_reply_to_id") { value("1") } }
|
||||
}
|
||||
|
||||
@Test
|
||||
fun ユニコード絵文字をリアクションできる() {
|
||||
mockMvc
|
||||
.put("/api/v1/statuses/1/emoji_reactions/😭") {
|
||||
with(jwt().jwt { it.claim("uid", "1") }.authorities(SimpleGrantedAuthority("SCOPE_write")))
|
||||
}
|
||||
.andDo { print() }
|
||||
.asyncDispatch()
|
||||
.andExpect { status { isOk() } }
|
||||
|
||||
val reaction = Reactions.select { Reactions.postId eq 1 and (Reactions.actorId eq 1) }.single().toReaction()
|
||||
assertThat(reaction.emoji).isEqualTo(UnicodeEmoji("😭"))
|
||||
assertThat(reaction.postId).isEqualTo(1)
|
||||
assertThat(reaction.actorId).isEqualTo(1)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun 存在しない絵文字はフォールバックされる() {
|
||||
mockMvc
|
||||
.put("/api/v1/statuses/1/emoji_reactions/hoge") {
|
||||
with(jwt().jwt { it.claim("uid", "1") }.authorities(SimpleGrantedAuthority("SCOPE_write")))
|
||||
}
|
||||
.andDo { print() }
|
||||
.asyncDispatch()
|
||||
.andExpect { status { isOk() } }
|
||||
|
||||
val reaction = Reactions.select { Reactions.postId eq 1 and (Reactions.actorId eq 1) }.single().toReaction()
|
||||
assertThat(reaction.emoji).isEqualTo(UnicodeEmoji("❤"))
|
||||
assertThat(reaction.postId).isEqualTo(1)
|
||||
assertThat(reaction.actorId).isEqualTo(1)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun カスタム絵文字をリアクションできる() {
|
||||
mockMvc
|
||||
.put("/api/v1/statuses/1/emoji_reactions/kotlin") {
|
||||
with(jwt().jwt { it.claim("uid", "1") }.authorities(SimpleGrantedAuthority("SCOPE_write")))
|
||||
}
|
||||
.andDo { print() }
|
||||
.asyncDispatch()
|
||||
.andExpect { status { isOk() } }
|
||||
|
||||
val reaction =
|
||||
Reactions.leftJoin(CustomEmojis).select { Reactions.postId eq 1 and (Reactions.actorId eq 1) }.single()
|
||||
.toReaction()
|
||||
assertThat(reaction.emoji).isEqualTo(
|
||||
CustomEmoji(
|
||||
1,
|
||||
"kotlin",
|
||||
"example.com",
|
||||
null,
|
||||
"https://example.com/emojis/kotlin",
|
||||
null,
|
||||
Instant.ofEpochMilli(1704700290036)
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
companion object {
|
||||
@JvmStatic
|
||||
@AfterAll
|
||||
|
|
|
@ -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
|
||||
|
||||
import com.fasterxml.jackson.databind.annotation.JsonDeserialize
|
||||
import dev.usbharu.hideout.activitypub.domain.model.objects.Object
|
||||
import dev.usbharu.hideout.activitypub.domain.model.objects.ObjectDeserializer
|
||||
|
||||
open class Note
|
||||
@Suppress("LongParameterList")
|
||||
|
@ -14,7 +16,9 @@ constructor(
|
|||
val cc: List<String> = emptyList(),
|
||||
val sensitive: Boolean = false,
|
||||
val inReplyTo: String? = null,
|
||||
val attachment: List<Document> = emptyList()
|
||||
val attachment: List<Document> = emptyList(),
|
||||
@JsonDeserialize(contentUsing = ObjectDeserializer::class)
|
||||
val tag: List<Object> = emptyList()
|
||||
) : Object(
|
||||
type = add(type, "Note")
|
||||
),
|
||||
|
@ -36,6 +40,7 @@ constructor(
|
|||
if (sensitive != other.sensitive) return false
|
||||
if (inReplyTo != other.inReplyTo) return false
|
||||
if (attachment != other.attachment) return false
|
||||
if (tag != other.tag) return false
|
||||
|
||||
return true
|
||||
}
|
||||
|
@ -51,21 +56,23 @@ constructor(
|
|||
result = 31 * result + sensitive.hashCode()
|
||||
result = 31 * result + (inReplyTo?.hashCode() ?: 0)
|
||||
result = 31 * result + attachment.hashCode()
|
||||
result = 31 * result + tag.hashCode()
|
||||
return result
|
||||
}
|
||||
|
||||
override fun toString(): String {
|
||||
return "Note(" +
|
||||
"id='$id', " +
|
||||
"attributedTo='$attributedTo', " +
|
||||
"content='$content', " +
|
||||
"published='$published', " +
|
||||
"to=$to, " +
|
||||
"cc=$cc, " +
|
||||
"sensitive=$sensitive, " +
|
||||
"inReplyTo=$inReplyTo, " +
|
||||
"attachment=$attachment" +
|
||||
")" +
|
||||
" ${super.toString()}"
|
||||
"id='$id', " +
|
||||
"attributedTo='$attributedTo', " +
|
||||
"content='$content', " +
|
||||
"published='$published', " +
|
||||
"to=$to, " +
|
||||
"cc=$cc, " +
|
||||
"sensitive=$sensitive, " +
|
||||
"inReplyTo=$inReplyTo, " +
|
||||
"attachment=$attachment, " +
|
||||
"tag=$tag" +
|
||||
")" +
|
||||
" ${super.toString()}"
|
||||
}
|
||||
}
|
||||
|
|
|
@ -9,7 +9,7 @@ import dev.usbharu.hideout.activitypub.service.common.ExtendedActivityVocabulary
|
|||
|
||||
class ObjectDeserializer : JsonDeserializer<Object>() {
|
||||
@Suppress("LongMethod", "CyclomaticComplexMethod")
|
||||
override fun deserialize(p: JsonParser?, ctxt: DeserializationContext?): Object {
|
||||
override fun deserialize(p: JsonParser?, ctxt: DeserializationContext?): Object? {
|
||||
requireNotNull(p)
|
||||
val treeNode: JsonNode = requireNotNull(p.codec?.readTree(p))
|
||||
if (treeNode.isValueNode) {
|
||||
|
@ -24,70 +24,71 @@ class ObjectDeserializer : JsonDeserializer<Object>() {
|
|||
ExtendedActivityVocabulary.values().firstOrNull { it.name.equals(jsonNode.asText(), true) }
|
||||
}
|
||||
} else if (type.isValueNode) {
|
||||
ExtendedActivityVocabulary.values().first { it.name.equals(type.asText(), true) }
|
||||
ExtendedActivityVocabulary.values().firstOrNull { it.name.equals(type.asText(), true) }
|
||||
} else {
|
||||
TODO()
|
||||
null
|
||||
}
|
||||
|
||||
return when (activityType) {
|
||||
ExtendedActivityVocabulary.Follow -> p.codec.treeToValue(treeNode, Follow::class.java)
|
||||
ExtendedActivityVocabulary.Note -> p.codec.treeToValue(treeNode, Note::class.java)
|
||||
ExtendedActivityVocabulary.Object -> p.codec.treeToValue(treeNode, Object::class.java)
|
||||
ExtendedActivityVocabulary.Link -> TODO()
|
||||
ExtendedActivityVocabulary.Activity -> TODO()
|
||||
ExtendedActivityVocabulary.IntransitiveActivity -> TODO()
|
||||
ExtendedActivityVocabulary.Collection -> TODO()
|
||||
ExtendedActivityVocabulary.OrderedCollection -> TODO()
|
||||
ExtendedActivityVocabulary.CollectionPage -> TODO()
|
||||
ExtendedActivityVocabulary.OrderedCollectionPage -> TODO()
|
||||
ExtendedActivityVocabulary.Link -> null
|
||||
ExtendedActivityVocabulary.Activity -> null
|
||||
ExtendedActivityVocabulary.IntransitiveActivity -> null
|
||||
ExtendedActivityVocabulary.Collection -> null
|
||||
ExtendedActivityVocabulary.OrderedCollection -> null
|
||||
ExtendedActivityVocabulary.CollectionPage -> null
|
||||
ExtendedActivityVocabulary.OrderedCollectionPage -> null
|
||||
ExtendedActivityVocabulary.Accept -> p.codec.treeToValue(treeNode, Accept::class.java)
|
||||
ExtendedActivityVocabulary.Add -> TODO()
|
||||
ExtendedActivityVocabulary.Announce -> TODO()
|
||||
ExtendedActivityVocabulary.Arrive -> TODO()
|
||||
ExtendedActivityVocabulary.Add -> null
|
||||
ExtendedActivityVocabulary.Announce -> null
|
||||
ExtendedActivityVocabulary.Arrive -> null
|
||||
ExtendedActivityVocabulary.Block -> p.codec.treeToValue(treeNode, Block::class.java)
|
||||
ExtendedActivityVocabulary.Create -> p.codec.treeToValue(treeNode, Create::class.java)
|
||||
ExtendedActivityVocabulary.Delete -> p.codec.treeToValue(treeNode, Delete::class.java)
|
||||
ExtendedActivityVocabulary.Dislike -> TODO()
|
||||
ExtendedActivityVocabulary.Flag -> TODO()
|
||||
ExtendedActivityVocabulary.Ignore -> TODO()
|
||||
ExtendedActivityVocabulary.Invite -> TODO()
|
||||
ExtendedActivityVocabulary.Join -> TODO()
|
||||
ExtendedActivityVocabulary.Leave -> TODO()
|
||||
ExtendedActivityVocabulary.Dislike -> null
|
||||
ExtendedActivityVocabulary.Flag -> null
|
||||
ExtendedActivityVocabulary.Ignore -> null
|
||||
ExtendedActivityVocabulary.Invite -> null
|
||||
ExtendedActivityVocabulary.Join -> null
|
||||
ExtendedActivityVocabulary.Leave -> null
|
||||
ExtendedActivityVocabulary.Like -> p.codec.treeToValue(treeNode, Like::class.java)
|
||||
ExtendedActivityVocabulary.Listen -> TODO()
|
||||
ExtendedActivityVocabulary.Move -> TODO()
|
||||
ExtendedActivityVocabulary.Offer -> TODO()
|
||||
ExtendedActivityVocabulary.Question -> TODO()
|
||||
ExtendedActivityVocabulary.Listen -> null
|
||||
ExtendedActivityVocabulary.Move -> null
|
||||
ExtendedActivityVocabulary.Offer -> null
|
||||
ExtendedActivityVocabulary.Question -> null
|
||||
ExtendedActivityVocabulary.Reject -> p.codec.treeToValue(treeNode, Reject::class.java)
|
||||
ExtendedActivityVocabulary.Read -> TODO()
|
||||
ExtendedActivityVocabulary.Remove -> TODO()
|
||||
ExtendedActivityVocabulary.TentativeReject -> TODO()
|
||||
ExtendedActivityVocabulary.TentativeAccept -> TODO()
|
||||
ExtendedActivityVocabulary.Travel -> TODO()
|
||||
ExtendedActivityVocabulary.Read -> null
|
||||
ExtendedActivityVocabulary.Remove -> null
|
||||
ExtendedActivityVocabulary.TentativeReject -> null
|
||||
ExtendedActivityVocabulary.TentativeAccept -> null
|
||||
ExtendedActivityVocabulary.Travel -> null
|
||||
ExtendedActivityVocabulary.Undo -> p.codec.treeToValue(treeNode, Undo::class.java)
|
||||
ExtendedActivityVocabulary.Update -> TODO()
|
||||
ExtendedActivityVocabulary.View -> TODO()
|
||||
ExtendedActivityVocabulary.Application -> TODO()
|
||||
ExtendedActivityVocabulary.Group -> TODO()
|
||||
ExtendedActivityVocabulary.Organization -> TODO()
|
||||
ExtendedActivityVocabulary.Update -> null
|
||||
ExtendedActivityVocabulary.View -> null
|
||||
ExtendedActivityVocabulary.Application -> null
|
||||
ExtendedActivityVocabulary.Group -> null
|
||||
ExtendedActivityVocabulary.Organization -> null
|
||||
ExtendedActivityVocabulary.Person -> p.codec.treeToValue(treeNode, Person::class.java)
|
||||
ExtendedActivityVocabulary.Service -> TODO()
|
||||
ExtendedActivityVocabulary.Article -> TODO()
|
||||
ExtendedActivityVocabulary.Audio -> TODO()
|
||||
ExtendedActivityVocabulary.Service -> null
|
||||
ExtendedActivityVocabulary.Article -> null
|
||||
ExtendedActivityVocabulary.Audio -> null
|
||||
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.Page -> TODO()
|
||||
ExtendedActivityVocabulary.Place -> TODO()
|
||||
ExtendedActivityVocabulary.Profile -> TODO()
|
||||
ExtendedActivityVocabulary.Relationship -> TODO()
|
||||
ExtendedActivityVocabulary.Page -> null
|
||||
ExtendedActivityVocabulary.Place -> null
|
||||
ExtendedActivityVocabulary.Profile -> null
|
||||
ExtendedActivityVocabulary.Relationship -> null
|
||||
ExtendedActivityVocabulary.Tombstone -> p.codec.treeToValue(treeNode, Tombstone::class.java)
|
||||
ExtendedActivityVocabulary.Video -> TODO()
|
||||
ExtendedActivityVocabulary.Mention -> TODO()
|
||||
ExtendedActivityVocabulary.Video -> null
|
||||
ExtendedActivityVocabulary.Mention -> null
|
||||
ExtendedActivityVocabulary.Emoji -> p.codec.treeToValue(treeNode, Emoji::class.java)
|
||||
null -> null
|
||||
}
|
||||
} else {
|
||||
TODO()
|
||||
return null
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,13 +1,16 @@
|
|||
package dev.usbharu.hideout.activitypub.service.activity.like
|
||||
|
||||
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.service.common.AbstractActivityPubProcessor
|
||||
import dev.usbharu.hideout.activitypub.service.common.ActivityPubProcessContext
|
||||
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.user.APUserService
|
||||
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 org.springframework.stereotype.Service
|
||||
|
||||
|
@ -16,7 +19,8 @@ class APLikeProcessor(
|
|||
transaction: Transaction,
|
||||
private val apUserService: APUserService,
|
||||
private val apNoteService: APNoteService,
|
||||
private val reactionService: ReactionService
|
||||
private val reactionService: ReactionService,
|
||||
private val emojiService: EmojiService
|
||||
) :
|
||||
AbstractActivityPubProcessor<Like>(transaction) {
|
||||
override suspend fun internalProcess(activity: ActivityPubProcessContext<Like>) {
|
||||
|
@ -29,9 +33,16 @@ class APLikeProcessor(
|
|||
|
||||
try {
|
||||
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(
|
||||
content,
|
||||
actor.substringAfter("://").substringBefore("/"),
|
||||
emoji ?: UnicodeEmoji("❤"),
|
||||
personWithEntity.second.id,
|
||||
post.id
|
||||
)
|
||||
|
|
|
@ -83,8 +83,6 @@ class APResourceResolveServiceImpl(
|
|||
return objects == other.objects
|
||||
}
|
||||
|
||||
override fun hashCode(): Int {
|
||||
return objects.hashCode()
|
||||
}
|
||||
override fun hashCode(): Int = 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.model.objects.Object
|
||||
import dev.usbharu.hideout.application.external.Transaction
|
||||
import dev.usbharu.hideout.core.domain.exception.resource.ResourceAccessException
|
||||
import org.slf4j.Logger
|
||||
import org.slf4j.LoggerFactory
|
||||
import java.sql.SQLException
|
||||
|
||||
abstract class AbstractActivityPubProcessor<T : Object>(
|
||||
private val transaction: Transaction,
|
||||
|
@ -21,7 +23,11 @@ abstract class AbstractActivityPubProcessor<T : Object>(
|
|||
logger.info("START ActivityPub process. {}", this.type())
|
||||
try {
|
||||
transaction.transaction {
|
||||
internalProcess(activity)
|
||||
try {
|
||||
internalProcess(activity)
|
||||
} catch (e: ResourceAccessException) {
|
||||
throw SQLException(e)
|
||||
}
|
||||
}
|
||||
} catch (e: ActivityPubProcessException) {
|
||||
logger.warn("FAILED ActivityPub process", e)
|
||||
|
|
|
@ -112,7 +112,7 @@ class InboxJobProcessor(
|
|||
logger.debug("Is verifying success? {}", verify)
|
||||
|
||||
val activityPubProcessor =
|
||||
activityPubProcessorList.firstOrNull { it.isSupported(param.type) } as ActivityPubProcessor<Object>?
|
||||
activityPubProcessorList.firstOrNull { it.isSupported(param.type) } as? ActivityPubProcessor<Object>
|
||||
|
||||
if (activityPubProcessor == null) {
|
||||
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
|
||||
|
||||
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.query.NoteQueryService
|
||||
import dev.usbharu.hideout.activitypub.service.common.APResourceResolveService
|
||||
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.core.domain.model.post.Post
|
||||
import dev.usbharu.hideout.core.domain.model.post.PostRepository
|
||||
|
@ -32,7 +34,8 @@ class APNoteServiceImpl(
|
|||
private val apResourceResolveService: APResourceResolveService,
|
||||
private val postBuilder: Post.PostBuilder,
|
||||
private val noteQueryService: NoteQueryService,
|
||||
private val mediaService: MediaService
|
||||
private val mediaService: MediaService,
|
||||
private val emojiService: EmojiService
|
||||
|
||||
) : APNoteService {
|
||||
|
||||
|
@ -101,6 +104,15 @@ class APNoteServiceImpl(
|
|||
postRepository.findByUrl(it)
|
||||
}
|
||||
|
||||
val emojis = note.tag
|
||||
.filterIsInstance<Emoji>()
|
||||
.map {
|
||||
emojiService.fetchEmoji(it).second
|
||||
}
|
||||
.map {
|
||||
it.id
|
||||
}
|
||||
|
||||
val mediaList = note.attachment.map {
|
||||
mediaService.uploadRemoteMedia(
|
||||
RemoteMedia(
|
||||
|
@ -123,7 +135,8 @@ class APNoteServiceImpl(
|
|||
replyId = reply?.id,
|
||||
sensitive = note.sensitive,
|
||||
apId = note.id,
|
||||
mediaIds = mediaList
|
||||
mediaIds = mediaList,
|
||||
emojiIds = emojis
|
||||
)
|
||||
)
|
||||
return note to createRemote
|
||||
|
|
|
@ -13,12 +13,6 @@ import java.sql.Connection
|
|||
@Service
|
||||
class ExposedTransaction : Transaction {
|
||||
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) {
|
||||
debug = true
|
||||
warnLongQueriesDuration = 1000
|
||||
|
|
|
@ -26,7 +26,8 @@ data class Actor private constructor(
|
|||
val followersCount: Int = 0,
|
||||
val followingCount: Int = 0,
|
||||
val postsCount: Int = 0,
|
||||
val lastPostDate: Instant? = null
|
||||
val lastPostDate: Instant? = null,
|
||||
val emojis: List<Long> = emptyList()
|
||||
) {
|
||||
|
||||
@Component
|
||||
|
@ -55,7 +56,8 @@ data class Actor private constructor(
|
|||
followersCount: Int = 0,
|
||||
followingCount: Int = 0,
|
||||
postsCount: Int = 0,
|
||||
lastPostDate: Instant? = null
|
||||
lastPostDate: Instant? = null,
|
||||
emojis: List<Long> = emptyList()
|
||||
): Actor {
|
||||
if (id == 0L) {
|
||||
return Actor(
|
||||
|
@ -78,7 +80,8 @@ data class Actor private constructor(
|
|||
followersCount = followersCount,
|
||||
followingCount = followingCount,
|
||||
postsCount = postsCount,
|
||||
lastPostDate = lastPostDate
|
||||
lastPostDate = lastPostDate,
|
||||
emojis = emojis
|
||||
)
|
||||
}
|
||||
|
||||
|
@ -188,7 +191,8 @@ data class Actor private constructor(
|
|||
followersCount = followersCount,
|
||||
followingCount = followingCount,
|
||||
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 withLastPostAt(lastPostDate: Instant): Actor = this.copy(lastPostDate = lastPostDate)
|
||||
|
||||
override fun toString(): String {
|
||||
return "Actor(" +
|
||||
"id=$id, " +
|
||||
|
@ -228,7 +231,8 @@ data class Actor private constructor(
|
|||
"followersCount=$followersCount, " +
|
||||
"followingCount=$followingCount, " +
|
||||
"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
|
||||
}
|
||||
|
||||
override fun hashCode(): Int {
|
||||
return links.hashCode()
|
||||
}
|
||||
override fun hashCode(): Int = links.hashCode()
|
||||
}
|
||||
|
||||
class Links private constructor() {
|
||||
|
|
|
@ -17,7 +17,8 @@ data class Post private constructor(
|
|||
val sensitive: Boolean = false,
|
||||
val apId: String = url,
|
||||
val mediaIds: List<Long> = emptyList(),
|
||||
val delted: Boolean = false
|
||||
val delted: Boolean = false,
|
||||
val emojiIds: List<Long> = emptyList()
|
||||
) {
|
||||
|
||||
@Component
|
||||
|
@ -35,7 +36,8 @@ data class Post private constructor(
|
|||
replyId: Long? = null,
|
||||
sensitive: Boolean = false,
|
||||
apId: String = url,
|
||||
mediaIds: List<Long> = emptyList()
|
||||
mediaIds: List<Long> = emptyList(),
|
||||
emojiIds: List<Long> = emptyList()
|
||||
): Post {
|
||||
require(id >= 0) { "id must be greater than or equal to 0." }
|
||||
|
||||
|
@ -74,7 +76,8 @@ data class Post private constructor(
|
|||
sensitive = sensitive,
|
||||
apId = apId,
|
||||
mediaIds = mediaIds,
|
||||
delted = false
|
||||
delted = false,
|
||||
emojiIds = emojiIds
|
||||
)
|
||||
}
|
||||
|
||||
|
|
|
@ -1,3 +1,5 @@
|
|||
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
|
||||
|
||||
import dev.usbharu.hideout.core.domain.model.emoji.Emoji
|
||||
import org.springframework.stereotype.Repository
|
||||
|
||||
@Repository
|
||||
@Suppress("FunctionMaxLength")
|
||||
@Suppress("FunctionMaxLength", "TooManyFunction")
|
||||
interface ReactionRepository {
|
||||
suspend fun generateId(): Long
|
||||
suspend fun save(reaction: Reaction): Reaction
|
||||
suspend fun delete(reaction: Reaction): Reaction
|
||||
suspend fun deleteByPostId(postId: 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 findByPostIdAndActorIdAndEmojiId(postId: Long, actorId: Long, emojiId: Long): Reaction?
|
||||
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>
|
||||
}
|
||||
|
|
|
@ -11,6 +11,9 @@ import org.springframework.stereotype.Service
|
|||
|
||||
@Service
|
||||
class RelationshipRepositoryImpl : RelationshipRepository, AbstractRepository() {
|
||||
override val logger: Logger
|
||||
get() = Companion.logger
|
||||
|
||||
override suspend fun save(relationship: Relationship): Relationship = query {
|
||||
val singleOrNull = Relationships.select {
|
||||
(Relationships.actorId eq relationship.actorId).and(
|
||||
|
@ -94,9 +97,6 @@ class RelationshipRepositoryImpl : RelationshipRepository, AbstractRepository()
|
|||
return@query query.map { it.toRelationships() }
|
||||
}
|
||||
|
||||
override val logger: Logger
|
||||
get() = Companion.logger
|
||||
|
||||
companion object {
|
||||
private val logger = LoggerFactory.getLogger(RelationshipRepositoryImpl::class.java)
|
||||
}
|
||||
|
|
|
@ -21,5 +21,6 @@ data class Timeline(
|
|||
val sensitive: Boolean,
|
||||
val isLocal: Boolean,
|
||||
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.core.domain.model.post.Post
|
||||
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 org.jetbrains.exposed.sql.Query
|
||||
import org.springframework.stereotype.Component
|
||||
|
@ -15,7 +16,10 @@ class PostQueryMapper(private val postResultRowMapper: ResultRowMapper<Post>) :
|
|||
.map { it.value }
|
||||
.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],
|
||||
postsCount = resultRow[Actors.postsCount],
|
||||
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
|
||||
|
||||
import dev.usbharu.hideout.core.domain.exception.SpringDataAccessExceptionSQLExceptionTranslator
|
||||
import org.jetbrains.exposed.sql.transactions.TransactionManager
|
||||
import org.slf4j.Logger
|
||||
import org.springframework.beans.factory.annotation.Value
|
||||
import org.springframework.jdbc.support.SQLErrorCodeSQLExceptionTranslator
|
||||
|
@ -37,6 +38,7 @@ ${Throwable().stackTrace.joinToString("\n")}
|
|||
if (traceQueryException) {
|
||||
logger.trace("FAILED EXECUTE SQL", e)
|
||||
}
|
||||
TransactionManager.currentOrNull()?.rollback()
|
||||
if (e.cause !is SQLException) {
|
||||
throw e
|
||||
}
|
||||
|
|
|
@ -45,6 +45,7 @@ class ActorRepositoryImpl(
|
|||
it[followingCount] = actor.followingCount
|
||||
it[postsCount] = actor.postsCount
|
||||
it[lastPostAt] = actor.lastPostDate
|
||||
it[emojis] = actor.emojis.joinToString(",")
|
||||
}
|
||||
} else {
|
||||
Actors.update({ Actors.id eq actor.id }) {
|
||||
|
@ -67,6 +68,7 @@ class ActorRepositoryImpl(
|
|||
it[followingCount] = actor.followingCount
|
||||
it[postsCount] = actor.postsCount
|
||||
it[lastPostAt] = actor.lastPostDate
|
||||
it[emojis] = actor.emojis.joinToString(",")
|
||||
}
|
||||
}
|
||||
return@query actor
|
||||
|
@ -152,7 +154,7 @@ object Actors : Table("actors") {
|
|||
val followersCount = integer("followers_count")
|
||||
val postsCount = integer("posts_count")
|
||||
val lastPostAt = timestamp("last_post_at").nullable()
|
||||
|
||||
val emojis = varchar("emojis", 3000)
|
||||
override val primaryKey: PrimaryKey = PrimaryKey(id)
|
||||
|
||||
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[isPureRepost] = timeline.isPureRepost
|
||||
it[mediaIds] = timeline.mediaIds.joinToString(",")
|
||||
it[emojiIds] = timeline.emojiIds.joinToString(",")
|
||||
}
|
||||
} else {
|
||||
Timelines.update({ Timelines.id eq timeline.id }) {
|
||||
|
@ -52,6 +53,7 @@ class ExposedTimelineRepository(private val idGenerateService: IdGenerateService
|
|||
it[isLocal] = timeline.isLocal
|
||||
it[isPureRepost] = timeline.isPureRepost
|
||||
it[mediaIds] = timeline.mediaIds.joinToString(",")
|
||||
it[emojiIds] = timeline.emojiIds.joinToString(",")
|
||||
}
|
||||
}
|
||||
return@query timeline
|
||||
|
@ -72,6 +74,7 @@ class ExposedTimelineRepository(private val idGenerateService: IdGenerateService
|
|||
this[Timelines.isLocal] = it.isLocal
|
||||
this[Timelines.isPureRepost] = it.isPureRepost
|
||||
this[Timelines.mediaIds] = it.mediaIds.joinToString(",")
|
||||
this[Timelines.emojiIds] = it.emojiIds.joinToString(",")
|
||||
}
|
||||
return@query timelines
|
||||
}
|
||||
|
@ -104,7 +107,8 @@ fun ResultRow.toTimeline(): Timeline {
|
|||
sensitive = this[Timelines.sensitive],
|
||||
isLocal = this[Timelines.isLocal],
|
||||
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 isPureRepost = bool("is_pure_repost")
|
||||
val mediaIds = varchar("media_ids", 255)
|
||||
val emojiIds = varchar("emoji_ids", 255)
|
||||
|
||||
override val primaryKey: PrimaryKey = PrimaryKey(id)
|
||||
|
||||
|
|
|
@ -41,14 +41,25 @@ class PostRepositoryImpl(
|
|||
this[PostsMedia.postId] = post.id
|
||||
this[PostsMedia.mediaId] = it
|
||||
}
|
||||
PostsEmojis.batchInsert(post.emojiIds) {
|
||||
this[PostsEmojis.postId] = post.id
|
||||
this[PostsEmojis.emojiId] = it
|
||||
}
|
||||
} else {
|
||||
PostsMedia.deleteWhere {
|
||||
postId eq post.id
|
||||
}
|
||||
PostsEmojis.deleteWhere {
|
||||
postId eq post.id
|
||||
}
|
||||
PostsMedia.batchInsert(post.mediaIds) {
|
||||
this[PostsMedia.postId] = post.id
|
||||
this[PostsMedia.mediaId] = it
|
||||
}
|
||||
PostsEmojis.batchInsert(post.emojiIds) {
|
||||
this[PostsEmojis.postId] = post.id
|
||||
this[PostsEmojis.emojiId] = it
|
||||
}
|
||||
Posts.update({ Posts.id eq post.id }) {
|
||||
it[actorId] = post.actorId
|
||||
it[overview] = post.overview
|
||||
|
@ -67,21 +78,27 @@ class PostRepositoryImpl(
|
|||
}
|
||||
|
||||
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 }
|
||||
.let(postQueryMapper::map)
|
||||
.singleOrNull()
|
||||
}
|
||||
|
||||
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 }
|
||||
.let(postQueryMapper::map)
|
||||
.singleOrNull()
|
||||
}
|
||||
|
||||
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 }
|
||||
.let(postQueryMapper::map)
|
||||
.singleOrNull()
|
||||
|
@ -92,7 +109,10 @@ class PostRepositoryImpl(
|
|||
}
|
||||
|
||||
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 {
|
||||
|
@ -125,3 +145,9 @@ object PostsMedia : Table("posts_media") {
|
|||
val mediaId = long("media_id").references(Media.id, ReferenceOption.CASCADE, ReferenceOption.CASCADE)
|
||||
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
|
||||
|
||||
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.ReactionRepository
|
||||
import org.jetbrains.exposed.dao.id.LongIdTable
|
||||
|
@ -23,13 +26,25 @@ class ReactionRepositoryImpl(
|
|||
if (Reactions.select { Reactions.id eq reaction.id }.forUpdate().empty()) {
|
||||
Reactions.insert {
|
||||
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[actorId] = reaction.actorId
|
||||
}
|
||||
} else {
|
||||
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[actorId] = reaction.actorId
|
||||
}
|
||||
|
@ -38,9 +53,16 @@ class ReactionRepositoryImpl(
|
|||
}
|
||||
|
||||
override suspend fun delete(reaction: Reaction): Reaction = query {
|
||||
Reactions.deleteWhere {
|
||||
id.eq(reaction.id).and(postId.eq(reaction.postId)).and(actorId.eq(reaction.actorId))
|
||||
.and(emojiId.eq(reaction.emojiId))
|
||||
if (reaction.emoji is CustomEmoji) {
|
||||
Reactions.deleteWhere {
|
||||
id.eq(reaction.id).and(postId.eq(reaction.postId)).and(actorId.eq(reaction.actorId))
|
||||
.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
|
||||
}
|
||||
|
@ -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 {
|
||||
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? =
|
||||
query {
|
||||
return@query Reactions.select {
|
||||
return@query Reactions.leftJoin(CustomEmojis).select {
|
||||
Reactions.postId eq postId and (Reactions.actorId eq actorId).and(
|
||||
Reactions.emojiId.eq(
|
||||
Reactions.customEmojiId.eq(
|
||||
emojiId
|
||||
)
|
||||
)
|
||||
|
@ -78,12 +122,49 @@ class ReactionRepositoryImpl(
|
|||
Reactions.postId
|
||||
.eq(postId)
|
||||
.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()
|
||||
}
|
||||
|
||||
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() }
|
||||
}
|
||||
|
||||
|
@ -93,22 +174,39 @@ class ReactionRepositoryImpl(
|
|||
}
|
||||
|
||||
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(
|
||||
this[Reactions.id].value,
|
||||
this[Reactions.emojiId],
|
||||
emoji,
|
||||
this[Reactions.postId],
|
||||
this[Reactions.actorId]
|
||||
)
|
||||
}
|
||||
|
||||
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> =
|
||||
long("post_id").references(Posts.id, onDelete = ReferenceOption.CASCADE, onUpdate = ReferenceOption.CASCADE)
|
||||
val actorId: Column<Long> =
|
||||
long("actor_id").references(Actors.id, onDelete = ReferenceOption.CASCADE, onUpdate = ReferenceOption.CASCADE)
|
||||
|
||||
init {
|
||||
uniqueIndex(emojiId, postId, actorId)
|
||||
uniqueIndex(customEmojiId, postId, actorId)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -26,11 +26,6 @@ class UserDetailsImpl(
|
|||
accountNonLocked: Boolean,
|
||||
authorities: MutableCollection<out GrantedAuthority>?
|
||||
) : User(username, password, enabled, accountNonExpired, credentialsNonExpired, accountNonLocked, authorities) {
|
||||
companion object {
|
||||
@Serial
|
||||
private const val serialVersionUID: Long = -899168205656607781L
|
||||
}
|
||||
|
||||
override fun toString(): String {
|
||||
return "UserDetailsImpl(" +
|
||||
"id=$id" +
|
||||
|
@ -53,6 +48,11 @@ class UserDetailsImpl(
|
|||
result = 31 * result + id.hashCode()
|
||||
return result
|
||||
}
|
||||
|
||||
companion object {
|
||||
@Serial
|
||||
private const val serialVersionUID: Long = -899168205656607781L
|
||||
}
|
||||
}
|
||||
|
||||
@JsonTypeInfo(use = JsonTypeInfo.Id.CLASS, include = JsonTypeInfo.As.PROPERTY)
|
||||
|
|
|
@ -10,9 +10,7 @@ sealed class SavedMedia(val success: Boolean) {
|
|||
return success == other.success
|
||||
}
|
||||
|
||||
override fun hashCode(): Int {
|
||||
return success.hashCode()
|
||||
}
|
||||
override fun hashCode(): Int = success.hashCode()
|
||||
}
|
||||
|
||||
class SuccessSavedMedia(
|
||||
|
|
|
@ -1,11 +1,12 @@
|
|||
package dev.usbharu.hideout.core.service.reaction
|
||||
|
||||
import dev.usbharu.hideout.core.domain.model.emoji.Emoji
|
||||
import org.springframework.stereotype.Service
|
||||
|
||||
@Service
|
||||
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 sendReaction(name: String, actorId: Long, postId: Long)
|
||||
suspend fun sendReaction(emoji: Emoji, actorId: Long, postId: Long)
|
||||
suspend fun removeReaction(actorId: Long, postId: Long)
|
||||
}
|
||||
|
|
|
@ -1,9 +1,10 @@
|
|||
package dev.usbharu.hideout.core.service.reaction
|
||||
|
||||
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.ReactionRepository
|
||||
import org.jetbrains.exposed.exceptions.ExposedSQLException
|
||||
import org.slf4j.Logger
|
||||
import org.slf4j.LoggerFactory
|
||||
import org.springframework.stereotype.Service
|
||||
|
@ -13,14 +14,17 @@ class ReactionServiceImpl(
|
|||
private val reactionRepository: ReactionRepository,
|
||||
private val apReactionService: APReactionService
|
||||
) : ReactionService {
|
||||
override suspend fun receiveReaction(name: String, domain: String, actorId: Long, postId: Long) {
|
||||
if (reactionRepository.existByPostIdAndActorIdAndEmojiId(postId, actorId, 0).not()) {
|
||||
try {
|
||||
reactionRepository.save(
|
||||
Reaction(reactionRepository.generateId(), 0, postId, actorId)
|
||||
)
|
||||
} catch (_: ExposedSQLException) {
|
||||
}
|
||||
override suspend fun receiveReaction(
|
||||
emoji: Emoji,
|
||||
actorId: Long,
|
||||
postId: Long
|
||||
) {
|
||||
if (reactionRepository.existByPostIdAndActor(postId, actorId)) {
|
||||
reactionRepository.deleteByPostIdAndActorId(postId, actorId)
|
||||
}
|
||||
try {
|
||||
reactionRepository.save(Reaction(reactionRepository.generateId(), emoji, postId, actorId))
|
||||
} catch (_: DuplicateException) {
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -33,7 +37,7 @@ class ReactionServiceImpl(
|
|||
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 =
|
||||
reactionRepository.findByPostIdAndActorIdAndEmojiId(postId, actorId, 0)
|
||||
|
||||
|
@ -42,7 +46,7 @@ class ReactionServiceImpl(
|
|||
reactionRepository.delete(findByPostIdAndUserIdAndEmojiId)
|
||||
}
|
||||
|
||||
val reaction = Reaction(reactionRepository.generateId(), 0, postId, actorId)
|
||||
val reaction = Reaction(reactionRepository.generateId(), emoji, postId, actorId)
|
||||
reactionRepository.save(reaction)
|
||||
apReactionService.reaction(reaction)
|
||||
}
|
||||
|
|
|
@ -45,7 +45,8 @@ class ExposedGenerateTimelineService(private val statusQueryService: StatusQuery
|
|||
it[Timelines.postId],
|
||||
it[Timelines.replyId],
|
||||
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.replyId,
|
||||
it.repostId,
|
||||
it.mediaIds
|
||||
it.mediaIds,
|
||||
it.emojiIds
|
||||
)
|
||||
}
|
||||
)
|
||||
|
|
|
@ -37,7 +37,8 @@ class TimelineService(
|
|||
sensitive = post.sensitive,
|
||||
isLocal = isLocal,
|
||||
isPureRepost = post.repostId == null || (post.text.isBlank() && post.overview.isNullOrBlank()),
|
||||
mediaIds = post.mediaIds
|
||||
mediaIds = post.mediaIds,
|
||||
emojiIds = post.emojiIds
|
||||
)
|
||||
}.toMutableList()
|
||||
if (post.visibility == Visibility.PUBLIC) {
|
||||
|
@ -55,7 +56,8 @@ class TimelineService(
|
|||
sensitive = post.sensitive,
|
||||
isLocal = isLocal,
|
||||
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
|
||||
|
||||
import dev.usbharu.hideout.core.domain.model.emoji.CustomEmoji
|
||||
import dev.usbharu.hideout.core.domain.model.media.toMediaAttachments
|
||||
import dev.usbharu.hideout.core.infrastructure.exposedrepository.*
|
||||
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.springframework.stereotype.Repository
|
||||
import java.time.Instant
|
||||
import dev.usbharu.hideout.domain.mastodon.model.generated.CustomEmoji as MastodonEmoji
|
||||
|
||||
@Suppress("IncompleteDestructuring")
|
||||
@Repository
|
||||
|
@ -23,6 +25,10 @@ class StatusQueryServiceImpl : StatusQueryService {
|
|||
postIdSet.addAll(statusQueries.flatMap { listOfNotNull(it.postId, it.replyId, it.repostId) })
|
||||
val mediaIdSet = mutableSetOf<Long>()
|
||||
mediaIdSet.addAll(statusQueries.flatMap { it.mediaIds })
|
||||
|
||||
val emojiIdSet = mutableSetOf<Long>()
|
||||
emojiIdSet.addAll(statusQueries.flatMap { it.emojiIds })
|
||||
|
||||
val postMap = Posts
|
||||
.leftJoin(Actors)
|
||||
.select { Posts.id inList postIdSet }
|
||||
|
@ -32,12 +38,16 @@ class StatusQueryServiceImpl : StatusQueryService {
|
|||
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 ->
|
||||
postMap[statusQuery.postId]?.copy(
|
||||
inReplyToId = statusQuery.replyId?.toString(),
|
||||
inReplyToAccountId = postMap[statusQuery.replyId]?.account?.id,
|
||||
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)
|
||||
}
|
||||
|
||||
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> {
|
||||
val statuses = pairs.map { it.first }
|
||||
return pairs
|
||||
|
@ -120,6 +149,8 @@ class StatusQueryServiceImpl : StatusQueryService {
|
|||
private suspend fun findByPostIdsWithMedia(ids: List<Long>): List<Status> {
|
||||
val pairs = Posts
|
||||
.leftJoin(PostsMedia)
|
||||
.leftJoin(PostsEmojis)
|
||||
.leftJoin(CustomEmojis)
|
||||
.leftJoin(Actors)
|
||||
.leftJoin(Media)
|
||||
.select { Posts.id inList ids }
|
||||
|
@ -129,13 +160,22 @@ class StatusQueryServiceImpl : StatusQueryService {
|
|||
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(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(
|
||||
id = it[Posts.id].toString(),
|
||||
uri = it[Posts.apId],
|
||||
|
|
|
@ -24,4 +24,20 @@ class MastodonStatusesApiContoller(private val statusesApiService: StatusesApiSe
|
|||
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 replyId: 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,
|
||||
includeFollowers: Boolean = false
|
||||
): List<Status>
|
||||
|
||||
suspend fun findByPostId(id: Long): Status
|
||||
}
|
||||
|
|
|
@ -1,17 +1,24 @@
|
|||
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.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.toMediaAttachments
|
||||
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.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.Visibility.*
|
||||
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.toStatusVisibility
|
||||
import dev.usbharu.hideout.mastodon.query.StatusQueryService
|
||||
import dev.usbharu.hideout.mastodon.service.account.AccountService
|
||||
import dev.usbharu.hideout.util.EmojiUtil
|
||||
import org.slf4j.LoggerFactory
|
||||
import org.springframework.stereotype.Service
|
||||
import java.time.Instant
|
||||
|
@ -22,16 +29,38 @@ interface StatusesApiService {
|
|||
statusesRequest: StatusesRequest,
|
||||
userId: Long
|
||||
): 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
|
||||
@Suppress("LongParameterList")
|
||||
class StatsesApiServiceImpl(
|
||||
private val postService: PostService,
|
||||
private val accountService: AccountService,
|
||||
private val mediaRepository: MediaRepository,
|
||||
private val transaction: Transaction,
|
||||
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 {
|
||||
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 {
|
||||
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
|
||||
}
|
||||
|
||||
override fun hashCode(): Int {
|
||||
return path?.hashCode() ?: 0
|
||||
}
|
||||
override fun hashCode(): Int = 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
|
||||
(
|
||||
id bigint primary key,
|
||||
|
@ -13,6 +25,10 @@ create table if not exists instance
|
|||
moderation_note varchar(10000) 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
|
||||
(
|
||||
id bigint primary key,
|
||||
|
@ -34,7 +50,8 @@ create table if not exists actors
|
|||
following_count int not null,
|
||||
followers_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"),
|
||||
constraint fk_actors_instance__id foreign key ("instance") references instance (id) on delete restrict on update restrict
|
||||
);
|
||||
|
@ -99,24 +116,42 @@ 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;
|
||||
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;
|
||||
|
||||
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
|
||||
(
|
||||
id bigserial primary key,
|
||||
emoji_id bigint not null,
|
||||
post_id bigint not null,
|
||||
actor_id bigint not null
|
||||
id bigint primary key,
|
||||
unicode_emoji varchar(255) null default null,
|
||||
custom_emoji_id bigint null default null,
|
||||
post_id bigint not null,
|
||||
actor_id bigint not null,
|
||||
unique (post_id, actor_id)
|
||||
);
|
||||
alter table reactions
|
||||
add constraint fk_reactions_post_id__id foreign key (post_id) references posts (id) on delete restrict on update restrict;
|
||||
alter table reactions
|
||||
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
|
||||
(
|
||||
id bigint primary key,
|
||||
user_id bigint not null,
|
||||
timeline_id bigint not null,
|
||||
post_id bigint not null,
|
||||
post_actor_id bigint not null,
|
||||
post_actor_id bigint not null,
|
||||
created_at bigint not null,
|
||||
reply_id bigint null,
|
||||
repost_id bigint null,
|
||||
|
@ -124,7 +159,8 @@ create table if not exists timelines
|
|||
"sensitive" boolean not null,
|
||||
is_local 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
|
||||
|
|
|
@ -152,6 +152,79 @@ paths:
|
|||
schema:
|
||||
$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:
|
||||
post:
|
||||
tags:
|
||||
|
|
|
@ -3,6 +3,7 @@ package dev.usbharu.hideout
|
|||
import com.fasterxml.jackson.module.kotlin.isKotlinClass
|
||||
import com.jparams.verifier.tostring.ToStringVerifier
|
||||
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.Warning
|
||||
import nl.jqno.equalsverifier.internal.reflection.PackageScanner
|
||||
|
@ -95,6 +96,7 @@ class EqualsAndToStringTest {
|
|||
.filter {
|
||||
it.superclass == Any::class.java || it.superclass?.packageName?.startsWith("dev.usbharu") ?: true
|
||||
}
|
||||
.filterNot { it == UnicodeEmoji::class.java }
|
||||
.map {
|
||||
|
||||
dynamicTest(it.name) {
|
||||
|
|
|
@ -3,6 +3,7 @@ package dev.usbharu.hideout.activitypub.domain.model
|
|||
import com.fasterxml.jackson.module.kotlin.readValue
|
||||
import dev.usbharu.hideout.activitypub.service.objects.note.APNoteServiceImpl.Companion.public
|
||||
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.Test
|
||||
|
||||
|
@ -51,11 +52,7 @@ class NoteSerializeTest {
|
|||
"attachment": [],
|
||||
"sensitive": false,
|
||||
"tag": [
|
||||
{
|
||||
"type": "Mention",
|
||||
"href": "https://calckey.jp/users/9bu1xzwjyb",
|
||||
"name": "@trapezial@calckey.jp"
|
||||
}
|
||||
|
||||
]
|
||||
}"""
|
||||
|
||||
|
@ -77,4 +74,105 @@ class NoteSerializeTest {
|
|||
)
|
||||
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.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.reaction.Reaction
|
||||
import dev.usbharu.hideout.core.external.job.DeliverReactionJob
|
||||
|
@ -48,7 +49,7 @@ class APReactionServiceImplTest {
|
|||
apReactionServiceImpl.reaction(
|
||||
Reaction(
|
||||
id = TwitterSnowflakeIdGenerateService.generateId(),
|
||||
emojiId = 0,
|
||||
emoji = UnicodeEmoji("❤"),
|
||||
postId = post.id,
|
||||
actorId = user.id
|
||||
)
|
||||
|
@ -88,7 +89,7 @@ class APReactionServiceImplTest {
|
|||
apReactionServiceImpl.removeReaction(
|
||||
Reaction(
|
||||
id = TwitterSnowflakeIdGenerateService.generateId(),
|
||||
emojiId = 0,
|
||||
emoji = UnicodeEmoji("❤"),
|
||||
postId = post.id,
|
||||
actorId = user.id
|
||||
)
|
||||
|
|
|
@ -73,6 +73,7 @@ class APNoteServiceImplTest {
|
|||
apResourceResolveService = mock(),
|
||||
postBuilder = Post.PostBuilder(CharacterLimit()),
|
||||
noteQueryService = noteQueryService,
|
||||
mock(),
|
||||
mock()
|
||||
)
|
||||
|
||||
|
@ -142,7 +143,8 @@ class APNoteServiceImplTest {
|
|||
apResourceResolveService = apResourceResolveService,
|
||||
postBuilder = Post.PostBuilder(CharacterLimit()),
|
||||
noteQueryService = noteQueryService,
|
||||
mock()
|
||||
mock(),
|
||||
mock { }
|
||||
)
|
||||
|
||||
val actual = apNoteServiceImpl.fetchNote(url)
|
||||
|
@ -190,6 +192,7 @@ class APNoteServiceImplTest {
|
|||
apResourceResolveService = apResourceResolveService,
|
||||
postBuilder = Post.PostBuilder(CharacterLimit()),
|
||||
noteQueryService = noteQueryService,
|
||||
mock(),
|
||||
mock()
|
||||
)
|
||||
|
||||
|
@ -240,6 +243,7 @@ class APNoteServiceImplTest {
|
|||
apResourceResolveService = mock(),
|
||||
postBuilder = postBuilder,
|
||||
noteQueryService = noteQueryService,
|
||||
mock(),
|
||||
mock()
|
||||
)
|
||||
|
||||
|
@ -292,6 +296,7 @@ class APNoteServiceImplTest {
|
|||
apResourceResolveService = mock(),
|
||||
postBuilder = postBuilder,
|
||||
noteQueryService = noteQueryService,
|
||||
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.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.ReactionRepository
|
||||
import kotlinx.coroutines.test.runTest
|
||||
import org.jetbrains.exposed.exceptions.ExposedSQLException
|
||||
import org.junit.jupiter.api.Test
|
||||
import org.junit.jupiter.api.extension.ExtendWith
|
||||
import org.mockito.InjectMocks
|
||||
|
@ -32,59 +32,33 @@ class ReactionServiceImplTest {
|
|||
|
||||
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
|
||||
)
|
||||
val generateId = TwitterSnowflakeIdGenerateService.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
|
||||
fun `receiveReaction リアクションが既に作成されていることを検知出来ずに例外が発生した場合は何もしない`() = runTest {
|
||||
val post = PostBuilder.of()
|
||||
|
||||
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() {
|
||||
fun `receiveReaction リアクションが既に作成されている場合削除して新しく作成`() = runTest() {
|
||||
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(
|
||||
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
|
||||
|
@ -96,10 +70,10 @@ class ReactionServiceImplTest {
|
|||
val generateId = TwitterSnowflakeIdGenerateService.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(apReactionService, times(1)).reaction(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, UnicodeEmoji("❤"), post.id, post.actorId)))
|
||||
}
|
||||
|
||||
@Test
|
||||
|
@ -107,30 +81,30 @@ class ReactionServiceImplTest {
|
|||
val post = PostBuilder.of()
|
||||
val id = TwitterSnowflakeIdGenerateService.generateId()
|
||||
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()
|
||||
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)).save(eq(Reaction(generateId, 0, post.id, post.actorId)))
|
||||
verify(apReactionService, times(1)).removeReaction(eq(Reaction(id, 0, post.id, post.actorId)))
|
||||
verify(apReactionService, times(1)).reaction(eq(Reaction(generateId, 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, UnicodeEmoji("❤"), 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, UnicodeEmoji("❤"), post.id, post.actorId)))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `removeReaction リアクションが存在する場合削除して配送`() = runTest {
|
||||
val post = PostBuilder.of()
|
||||
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)
|
||||
|
||||
verify(reactionRepository, times(1)).delete(eq(Reaction(0, 0, post.id, post.actorId)))
|
||||
verify(apReactionService, times(1)).removeReaction(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, 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