31 Commits

Author SHA1 Message Date
usbharu 6b7cacd83f test: 失敗するテストを修正 2024-09-09 14:49:52 +09:00
usbharu 587e87b353 style: fix lint (CI) 2024-09-09 05:28:32 +00:00
usbharu fef08cd6ae Merge branch 'develop' into reaction 2024-09-09 14:24:19 +09:00
renovate[bot] 452cc5be7b chore(deps): update detekt to v1.23.7 2024-09-08 15:27:50 +00:00
usbharu 021f6b0d45 feat: mastodon 互換APIを追加 2024-09-08 23:32:01 +09:00
usbharu 7665286270 fix: セッションが存在しない時にエラーが出る問題を修正 2024-09-08 23:01:19 +09:00
usbharu 5eb3bc3704 feat: リアクションを取り消せるように 2024-09-08 23:00:59 +09:00
usbharu 8c3ff077d8 fix: セッションが存在しない時に作成しないように 2024-09-08 22:27:40 +09:00
usbharu d4aaad3fb6 feat: リアクションしたかを確認できるように 2024-09-08 22:26:50 +09:00
renovate[bot] c768749490 fix(deps): update dependency dev.usbharu:emoji-kt to v2.0.1 2024-09-08 10:00:49 +00:00
usbharu e42919ce3c feat: タイムラインにリアクションを表示するように 2024-09-08 16:40:51 +09:00
usbharu 2b567bb1d5 feat: Post詳細ページでリアクションを表示できるように 2024-09-08 15:58:05 +09:00
usbharu 1b4dbc8566 feat: reactionのテーブル定義を追加 2024-09-08 01:17:58 +09:00
usbharu daf676503d feat: ReactionRepositoryを追加 2024-09-08 01:10:12 +09:00
usbharu 72c9b8b7c5 feat: Reactionを追加 2024-09-08 00:58:18 +09:00
usbharu 6c83306f0d Merge pull request #591 from usbharu/follow
簡易クライアントからフォローできるように
2024-09-07 23:33:42 +09:00
usbharu cf1327eae4 style: fix lint (CI) 2024-09-07 14:29:44 +00:00
usbharu 0c3e69a7e4 feat: unfollow時にフォローリクエストも破棄するように
不整合等の解消を容易にするためのため
2024-09-07 23:23:57 +09:00
usbharu b3a5749b54 feat: ユーザー詳細からフォロー/アンフォローできるように 2024-09-07 18:46:49 +09:00
usbharu cf0b0017f6 feat: 所有しているタイムラインの一覧を取得できるように 2024-09-07 16:44:30 +09:00
usbharu 8c5c2abb3f feat: フォローを外したときにホームタイムラインから自動で外れるように 2024-09-07 16:04:19 +09:00
usbharu 2e836c228f Merge pull request #590 from usbharu/structured-logging
構造化ログを出力するように
2024-09-07 14:51:14 +09:00
usbharu cc0e14f00b Merge branch 'develop' into structured-logging 2024-09-07 14:48:26 +09:00
usbharu c26e8aea4b chore: version catalogを使用するように 2024-09-07 13:30:46 +09:00
usbharu e8724f333d chore: ロガーをlogbackに変更して構造化ログを出力するように 2024-09-07 13:27:02 +09:00
renovate[bot] 7e535ebcf9 fix(deps): update dependency software.amazon.awssdk:s3 to v2.27.21 2024-09-07 02:25:33 +00:00
renovate[bot] 96dd11292f fix(deps): update dependency org.apache.logging.log4j:log4j-slf4j2-impl to v2.24.0 2024-09-06 22:51:21 +00:00
usbharu ebef329120 Merge pull request #587 from usbharu/use-upsert
feat: insertとupdateを使用していた部分をupsertに置き換え
2024-09-07 00:06:09 +09:00
usbharu c7742914ef style: fix lint (CI) 2024-09-06 08:58:52 +00:00
usbharu 4bc0dc6927 Merge branch 'develop' into use-upsert 2024-09-06 17:54:28 +09:00
usbharu 96af8fa144 feat: insertとupdateを使用していた部分をupsertに置き換え 2024-09-06 17:53:58 +09:00
81 changed files with 1302 additions and 452 deletions
+1
View File
@@ -49,3 +49,4 @@ out/
/hideout-core/.kotlin/sessions/
/hideout-mastodon/.kotlin/sessions/
/http-client.private.env.json
/logs/
-6
View File
@@ -48,12 +48,6 @@ repositories {
}
}
}
configurations {
all {
exclude("org.springframework.boot", "spring-boot-starter-logging")
exclude("ch.qos.logback", "logback-classic")
}
}
dependencies {
implementation("dev.usbharu:hideout-core:0.0.1")
+11 -15
View File
@@ -90,7 +90,6 @@ dependencies {
implementation("org.springframework.boot:spring-boot-starter-web")
implementation("org.springframework.boot:spring-boot-starter-security")
implementation("org.springframework.boot:spring-boot-starter-thymeleaf")
implementation("org.springframework.boot:spring-boot-starter-log4j2")
implementation("org.springframework.boot:spring-boot-starter-validation")
annotationProcessor("org.springframework:spring-context-indexer")
@@ -125,8 +124,7 @@ dependencies {
implementation(libs.http.signature)
implementation(libs.emoji.kt)
implementation(libs.logback.ecs.encoder)
testImplementation("org.springframework.boot:spring-boot-starter-test")
testImplementation(libs.kotlin.junit)
@@ -134,7 +132,6 @@ dependencies {
testImplementation(libs.ktor.client.mock)
testImplementation(libs.h2db)
testImplementation(libs.mockito.kotlin)
}
detekt {
@@ -145,14 +142,20 @@ detekt {
autoCorrect = true
}
configurations.matching { it.name == "detekt" }.all {
resolutionStrategy.eachDependency {
if (requested.group == "org.jetbrains.kotlin") {
useVersion(io.gitlab.arturbosch.detekt.getSupportedKotlinVersion())
configurations {
matching { it.name == "detekt" }.all {
resolutionStrategy.eachDependency {
if (requested.group == "org.jetbrains.kotlin") {
useVersion(io.gitlab.arturbosch.detekt.getSupportedKotlinVersion())
}
}
}
all {
exclude("org.apache.logging.log4j", "log4j-slf4j2-impl")
}
}
//tasks{
// bootRun {
// sourceResources(sourceSets.main.get())
@@ -176,13 +179,6 @@ tasks.withType<io.gitlab.arturbosch.detekt.DetektCreateBaselineTask>().configure
exclude("**/org/koin/ksp/generated/**", "**/generated/**")
}
configurations {
all {
exclude("org.springframework.boot", "spring-boot-starter-logging")
exclude("ch.qos.logback", "logback-classic")
}
}
project.gradle.taskGraph.whenReady {
println(this.allTasks)
this.allTasks.map { println(it.name) }
@@ -26,8 +26,10 @@ class GetActorDetailApplicationService(
actorRepository.findById(ActorId(command.id))
?: throw IllegalArgumentException("Actor ${command.id} not found.")
} else if (command.actorName != null) {
val host = command.actorName.host.ifEmpty {
val host = if (command.actorName.host.isEmpty()) {
applicationConfig.url.host
} else {
command.actorName.host
}
actorRepository.findByNameAndDomain(command.actorName.userpart, host)
?: throw IllegalArgumentException("Actor ${command.actorName} not found.")
@@ -0,0 +1,8 @@
package dev.usbharu.hideout.core.application.actor
import org.springframework.stereotype.Service
@Service
interface SetAlsoKnownAsLocalActorApplicationService {
suspend fun setAlsoKnownAs(actorId: Long, alsoKnownAs: List<Long>)
}
@@ -0,0 +1,3 @@
package dev.usbharu.hideout.core.application.actor
data class SuspendLocalActor(val actorId: Long)
@@ -0,0 +1,44 @@
/*
* Copyright (C) 2024 usbharu
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package dev.usbharu.hideout.core.application.actor
import dev.usbharu.hideout.core.application.shared.LocalUserAbstractApplicationService
import dev.usbharu.hideout.core.application.shared.Transaction
import dev.usbharu.hideout.core.domain.model.actor.ActorId
import dev.usbharu.hideout.core.domain.model.actor.ActorRepository
import dev.usbharu.hideout.core.domain.model.support.principal.LocalUser
import org.slf4j.LoggerFactory
import org.springframework.stereotype.Service
@Service
class SuspendLocalActorApplicationService(
transaction: Transaction,
private val actorRepository: ActorRepository,
) : LocalUserAbstractApplicationService<SuspendLocalActor, Unit>(transaction, logger) {
override suspend fun internalExecute(command: SuspendLocalActor, principal: LocalUser) {
val id = ActorId(command.actorId)
val actor =
actorRepository.findById(id) ?: throw IllegalArgumentException("Actor ${command.actorId} not found.")
actor.suspend = true
actorRepository.save(actor)
}
companion object {
private val logger = LoggerFactory.getLogger(SuspendLocalActorApplicationService::class.java)
}
}
@@ -0,0 +1,3 @@
package dev.usbharu.hideout.core.application.actor
data class UnsuspendLocalActor(val actorId: Long)
@@ -0,0 +1,44 @@
/*
* Copyright (C) 2024 usbharu
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package dev.usbharu.hideout.core.application.actor
import dev.usbharu.hideout.core.application.shared.LocalUserAbstractApplicationService
import dev.usbharu.hideout.core.application.shared.Transaction
import dev.usbharu.hideout.core.domain.model.actor.ActorId
import dev.usbharu.hideout.core.domain.model.actor.ActorRepository
import dev.usbharu.hideout.core.domain.model.support.principal.LocalUser
import org.slf4j.LoggerFactory
import org.springframework.stereotype.Service
@Service
class UnsuspendLocalActorApplicationService(
transaction: Transaction,
private val actorRepository: ActorRepository,
) : LocalUserAbstractApplicationService<UnsuspendLocalActor, Unit>(transaction, logger) {
override suspend fun internalExecute(command: UnsuspendLocalActor, principal: LocalUser) {
val findById = actorRepository.findById(ActorId(command.actorId))
?: throw IllegalArgumentException("Actor ${command.actorId} not found.")
findById.suspend = false
actorRepository.save(findById)
}
companion object {
private val logger = LoggerFactory.getLogger(UnsuspendLocalActorApplicationService::class.java)
}
}
@@ -5,24 +5,20 @@ import dev.usbharu.hideout.core.application.timeline.AddTimelineRelationship
import dev.usbharu.hideout.core.application.timeline.UserAddTimelineRelationshipApplicationService
import dev.usbharu.hideout.core.domain.event.relationship.RelationshipEvent
import dev.usbharu.hideout.core.domain.event.relationship.RelationshipEventBody
import dev.usbharu.hideout.core.domain.model.timelinerelationship.TimelineRelationship
import dev.usbharu.hideout.core.domain.model.timelinerelationship.TimelineRelationshipId
import dev.usbharu.hideout.core.domain.model.timelinerelationship.Visible
import dev.usbharu.hideout.core.domain.model.userdetails.UserDetailRepository
import dev.usbharu.hideout.core.domain.shared.id.IdGenerateService
import org.slf4j.LoggerFactory
import org.springframework.stereotype.Component
@Component
class TimelineRelationshipFollowSubscriber(
private val userAddTimelineRelationshipApplicationService: UserAddTimelineRelationshipApplicationService,
private val idGenerateService: IdGenerateService,
private val userDetailRepository: UserDetailRepository,
private val domainEventSubscriber: DomainEventSubscriber
) : Subscriber {
override fun init() {
domainEventSubscriber.subscribe<RelationshipEventBody>(RelationshipEvent.FOLLOW.eventName) {
domainEventSubscriber.subscribe<RelationshipEventBody>(RelationshipEvent.ACCEPT_FOLLOW.eventName) {
val relationship = it.body.getRelationship()
val userDetail = userDetailRepository.findByActorId(relationship.actorId.id)
?: throw InternalServerException("Userdetail ${relationship.actorId} not found by actorid.")
@@ -34,12 +30,9 @@ class TimelineRelationshipFollowSubscriber(
@Suppress("UnsafeCallOnNullableType")
userAddTimelineRelationshipApplicationService.execute(
AddTimelineRelationship(
TimelineRelationship(
TimelineRelationshipId(idGenerateService.generateId()),
userDetail.homeTimelineId!!,
relationship.targetActorId,
Visible.FOLLOWERS
)
userDetail.homeTimelineId!!,
relationship.targetActorId,
Visible.FOLLOWERS
),
it.body.principal
)
@@ -0,0 +1,46 @@
package dev.usbharu.hideout.core.application.domainevent.subscribers
import dev.usbharu.hideout.core.application.timeline.RemoveTimelineRelationship
import dev.usbharu.hideout.core.application.timeline.UserRemoveTimelineRelationshipApplicationService
import dev.usbharu.hideout.core.domain.event.relationship.RelationshipEvent
import dev.usbharu.hideout.core.domain.event.relationship.RelationshipEventBody
import dev.usbharu.hideout.core.domain.model.timelinerelationship.TimelineRelationshipRepository
import dev.usbharu.hideout.core.domain.model.userdetails.UserDetailRepository
import org.slf4j.LoggerFactory
import org.springframework.stereotype.Component
@Component
class TimelineRelationshipUnfollowSubscriber(
private val domainEventSubscriber: DomainEventSubscriber,
private val userRemoveTimelineRelationshipApplicationService: UserRemoveTimelineRelationshipApplicationService,
private val userDetailRepository: UserDetailRepository,
private val timelineRelationshipRepository: TimelineRelationshipRepository,
) : Subscriber {
override fun init() {
domainEventSubscriber.subscribe<RelationshipEventBody>(RelationshipEvent.UNFOLLOW.eventName) {
val relationship = it.body.getRelationship()
val userDetail = userDetailRepository.findByActorId(relationship.actorId.id) ?: throw IllegalStateException(
"UserDetail ${relationship.actorId} not found by actorId."
)
if (userDetail.homeTimelineId == null) {
logger.warn("HomeTimeline for ${userDetail.id} not found.")
return@subscribe
}
val timelineRelationship = timelineRelationshipRepository.findByTimelineIdAndActorId(
userDetail.homeTimelineId!!,
relationship.targetActorId
)
?: throw IllegalStateException("TimelineRelationship ${userDetail.homeTimelineId} to ${relationship.targetActorId} not found by timelineId and ActorId")
userRemoveTimelineRelationshipApplicationService.execute(
RemoveTimelineRelationship(timelineRelationship.id),
it.body.principal
)
}
}
companion object {
private val logger = LoggerFactory.getLogger(TimelineRelationshipUnfollowSubscriber::class.java)
}
}
@@ -0,0 +1,28 @@
package dev.usbharu.hideout.core.application.model
import dev.usbharu.hideout.core.domain.model.emoji.CustomEmoji
import dev.usbharu.hideout.core.domain.model.reaction.Reaction
import java.net.URI
data class Reactions(
val postId: Long,
val count: Int,
val name: String,
val domain: String,
val url: URI?,
val actorIds: List<Long>,
) {
companion object {
fun of(reactionList: List<Reaction>, customEmoji: CustomEmoji?): Reactions {
val first = reactionList.first()
return Reactions(
first.id.value,
reactionList.size,
customEmoji?.name ?: first.unicodeEmoji.name,
customEmoji?.domain?.domain ?: first.unicodeEmoji.domain.domain,
customEmoji?.url,
reactionList.map { it.actorId.id }
)
}
}
}
@@ -0,0 +1,23 @@
package dev.usbharu.hideout.core.application.model
import dev.usbharu.hideout.core.domain.model.timeline.TimelineVisibility
data class Timeline(
val id: Long,
val userDetailId: Long,
val name: String,
val visibility: TimelineVisibility,
val isSystem: Boolean
) {
companion object {
fun of(timeline: dev.usbharu.hideout.core.domain.model.timeline.Timeline): Timeline {
return Timeline(
timeline.id.value,
timeline.userDetailId.id,
timeline.name.value,
timeline.visibility,
timeline.isSystem
)
}
}
}
@@ -9,8 +9,10 @@ import dev.usbharu.hideout.core.domain.model.media.Media
import dev.usbharu.hideout.core.domain.model.media.MediaRepository
import dev.usbharu.hideout.core.domain.model.post.PostId
import dev.usbharu.hideout.core.domain.model.post.PostRepository
import dev.usbharu.hideout.core.domain.model.reaction.ReactionRepository
import dev.usbharu.hideout.core.domain.model.support.principal.Principal
import dev.usbharu.hideout.core.domain.service.post.IPostReadAccessControl
import dev.usbharu.hideout.core.query.reactions.ReactionsQueryService
import org.slf4j.LoggerFactory
import org.springframework.stereotype.Service
@@ -20,7 +22,9 @@ class GetPostDetailApplicationService(
private val postRepository: PostRepository,
private val actorRepository: ActorRepository,
private val mediaRepository: MediaRepository,
private val iPostReadAccessControl: IPostReadAccessControl
private val iPostReadAccessControl: IPostReadAccessControl,
private val reactionsQueryService: ReactionsQueryService,
private val reactionRepository: ReactionRepository,
) : AbstractApplicationService<GetPostDetail, PostDetail>(
transaction,
logger
@@ -38,6 +42,15 @@ class GetPostDetailApplicationService(
val mediaList = mediaRepository.findByIds(post.mediaIds)
val reactions = reactionsQueryService.findAllByPostId(post.id)
val favourited = reactionRepository.existsByPostIdAndActorIdAndCustomEmojiIdOrUnicodeEmoji(
post.id,
principal.actorId,
null,
""
)
return PostDetail.of(
post = post,
actor = actor,
@@ -46,6 +59,8 @@ class GetPostDetailApplicationService(
reply = post.replyId?.let { fetchChild(it, actor, iconMedia, principal) },
repost = post.repostId?.let { fetchChild(it, actor, iconMedia, principal) },
moveTo = post.moveTo?.let { fetchChild(it, actor, iconMedia, principal) },
reactionsList = reactions,
favourited
)
}
@@ -69,10 +84,12 @@ class GetPostDetailApplicationService(
val mediaList = mediaRepository.findByIds(post.mediaIds)
return PostDetail.of(
post,
first,
third,
mediaList
post = post,
actor = first,
iconMedia = third,
mediaList = mediaList,
reactionsList = emptyList(),
favourited = false
)
}
@@ -1,5 +1,6 @@
package dev.usbharu.hideout.core.application.post
import dev.usbharu.hideout.core.application.model.Reactions
import dev.usbharu.hideout.core.domain.model.actor.Actor
import dev.usbharu.hideout.core.domain.model.media.Media
import dev.usbharu.hideout.core.domain.model.post.Post
@@ -23,7 +24,9 @@ data class PostDetail(
val sensitive: Boolean,
val deleted: Boolean,
val mediaDetailList: List<MediaDetail>,
val moveTo: PostDetail?
val moveTo: PostDetail?,
val reactionsList: List<Reactions>,
val favourited: Boolean
) {
companion object {
@Suppress("LongParameterList")
@@ -35,6 +38,8 @@ data class PostDetail(
reply: PostDetail? = null,
repost: PostDetail? = null,
moveTo: PostDetail? = null,
reactionsList: List<Reactions>,
favourited: Boolean
): PostDetail {
return PostDetail(
id = post.id.id,
@@ -52,7 +57,9 @@ data class PostDetail(
sensitive = post.sensitive,
deleted = false,
mediaDetailList = mediaList.map { MediaDetail.of(it) },
moveTo = moveTo
moveTo = moveTo,
reactionsList = reactionsList,
favourited = favourited
)
}
}
@@ -0,0 +1,3 @@
package dev.usbharu.hideout.core.application.reaction
data class CreateReaction(val postId: Long, val customEmojiId: Long?, val unicodeEmoji: String)
@@ -0,0 +1,7 @@
package dev.usbharu.hideout.core.application.reaction
data class RemoveReaction(
val postId: Long,
val customEmojiId: Long?,
val unicodeEmoji: String
)
@@ -0,0 +1,65 @@
package dev.usbharu.hideout.core.application.reaction
import dev.usbharu.hideout.core.application.exception.PermissionDeniedException
import dev.usbharu.hideout.core.application.shared.LocalUserAbstractApplicationService
import dev.usbharu.hideout.core.application.shared.Transaction
import dev.usbharu.hideout.core.domain.model.emoji.CustomEmojiRepository
import dev.usbharu.hideout.core.domain.model.emoji.UnicodeEmoji
import dev.usbharu.hideout.core.domain.model.post.PostId
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.ReactionId
import dev.usbharu.hideout.core.domain.model.reaction.ReactionRepository
import dev.usbharu.hideout.core.domain.model.support.principal.LocalUser
import dev.usbharu.hideout.core.domain.service.emoji.UnicodeEmojiService
import dev.usbharu.hideout.core.domain.service.post.IPostReadAccessControl
import dev.usbharu.hideout.core.domain.shared.id.IdGenerateService
import org.slf4j.LoggerFactory
import org.springframework.stereotype.Service
import java.time.Instant
@Service
class UserCreateReactionApplicationService(
transaction: Transaction,
private val idGenerateService: IdGenerateService,
private val reactionRepository: ReactionRepository,
private val postReadAccessControl: IPostReadAccessControl,
private val postRepository: PostRepository,
private val customEmojiRepository: CustomEmojiRepository,
private val unicodeEmojiService: UnicodeEmojiService
) :
LocalUserAbstractApplicationService<CreateReaction, Unit>(
transaction,
logger
) {
override suspend fun internalExecute(command: CreateReaction, principal: LocalUser) {
val postId = PostId(command.postId)
val post = postRepository.findById(postId) ?: throw IllegalArgumentException("Post $postId not found.")
if (postReadAccessControl.isAllow(post, principal).not()) {
throw PermissionDeniedException()
}
val customEmoji = command.customEmojiId?.let { customEmojiRepository.findById(it) }
val unicodeEmoji = if (unicodeEmojiService.isUnicodeEmoji(command.unicodeEmoji)) {
command.unicodeEmoji
} else {
""
}
val reaction = Reaction.create(
id = ReactionId(idGenerateService.generateId()),
postId = postId,
actorId = principal.actorId,
customEmojiId = customEmoji?.id,
unicodeEmoji = UnicodeEmoji(unicodeEmoji),
createdAt = Instant.now()
)
reactionRepository.save(reaction)
}
companion object {
private val logger = LoggerFactory.getLogger(UserCreateReactionApplicationService::class.java)
}
}
@@ -0,0 +1,51 @@
package dev.usbharu.hideout.core.application.reaction
import dev.usbharu.hideout.core.application.shared.LocalUserAbstractApplicationService
import dev.usbharu.hideout.core.application.shared.Transaction
import dev.usbharu.hideout.core.domain.model.emoji.CustomEmojiRepository
import dev.usbharu.hideout.core.domain.model.post.PostId
import dev.usbharu.hideout.core.domain.model.reaction.ReactionRepository
import dev.usbharu.hideout.core.domain.model.support.principal.LocalUser
import dev.usbharu.hideout.core.domain.service.emoji.UnicodeEmojiService
import org.slf4j.LoggerFactory
import org.springframework.stereotype.Service
@Service
class UserRemoveReactionApplicationService(
transaction: Transaction,
private val customEmojiRepository: CustomEmojiRepository,
private val reactionRepository: ReactionRepository,
private val unicodeEmojiService: UnicodeEmojiService
) :
LocalUserAbstractApplicationService<RemoveReaction, Unit>(
transaction,
logger
) {
override suspend fun internalExecute(command: RemoveReaction, principal: LocalUser) {
val postId = PostId(command.postId)
val customEmoji = command.customEmojiId?.let { customEmojiRepository.findById(it) }
val unicodeEmoji = if (unicodeEmojiService.isUnicodeEmoji(command.unicodeEmoji)) {
command.unicodeEmoji
} else {
""
}
val reaction =
reactionRepository.findByPostIdAndActorIdAndCustomEmojiIdOrUnicodeEmoji(
postId,
principal.actorId,
customEmoji?.id,
unicodeEmoji
)
?: throw IllegalArgumentException("Reaction $postId ${principal.actorId} ${customEmoji?.id} $unicodeEmoji not found.")
reaction.delete()
reactionRepository.delete(reaction)
}
companion object {
private val logger = LoggerFactory.getLogger(UserRemoveReactionApplicationService::class.java)
}
}
@@ -1,7 +1,11 @@
package dev.usbharu.hideout.core.application.timeline
import dev.usbharu.hideout.core.domain.model.timelinerelationship.TimelineRelationship
import dev.usbharu.hideout.core.domain.model.actor.ActorId
import dev.usbharu.hideout.core.domain.model.timeline.TimelineId
import dev.usbharu.hideout.core.domain.model.timelinerelationship.Visible
data class AddTimelineRelationship(
val timelineRelationship: TimelineRelationship
val timelineId: TimelineId,
val actorId: ActorId,
val visible: Visible
)
@@ -0,0 +1,3 @@
package dev.usbharu.hideout.core.application.timeline
data class GetTimelines(val userDetailId: Long)
@@ -47,7 +47,9 @@ class ReadTimelineApplicationService(
it.replyPost,
it.replyPostActor!!,
it.replyPostActorIconMedia,
it.replyPostMedias.orEmpty()
it.replyPostMedias.orEmpty(),
reactionsList = emptyList(),
favourited = false,
)
} else {
null
@@ -56,10 +58,12 @@ class ReadTimelineApplicationService(
val repost = if (it.repostPost != null) {
@Suppress("UnsafeCallOnNullableType")
PostDetail.of(
it.repostPost,
it.repostPostActor!!,
it.repostPostActorIconMedia,
it.repostPostMedias.orEmpty()
post = it.repostPost,
actor = it.repostPostActor!!,
iconMedia = it.repostPostActorIconMedia,
mediaList = it.repostPostMedias.orEmpty(),
reactionsList = emptyList(),
favourited = false
)
} else {
null
@@ -71,7 +75,9 @@ class ReadTimelineApplicationService(
iconMedia = it.postActorIconMedia,
mediaList = it.postMedias,
reply = reply,
repost = repost
repost = repost,
reactionsList = emptyList(),
favourited = it.favourited
)
}
@@ -0,0 +1,5 @@
package dev.usbharu.hideout.core.application.timeline
import dev.usbharu.hideout.core.domain.model.timelinerelationship.TimelineRelationshipId
data class RemoveTimelineRelationship(val timelineRelationshipId: TimelineRelationshipId)
@@ -1,15 +1,22 @@
package dev.usbharu.hideout.core.application.timeline
import dev.usbharu.hideout.core.application.exception.PermissionDeniedException
import dev.usbharu.hideout.core.application.shared.LocalUserAbstractApplicationService
import dev.usbharu.hideout.core.application.shared.Transaction
import dev.usbharu.hideout.core.domain.model.support.principal.LocalUser
import dev.usbharu.hideout.core.domain.model.timeline.TimelineRepository
import dev.usbharu.hideout.core.domain.model.timelinerelationship.TimelineRelationship
import dev.usbharu.hideout.core.domain.model.timelinerelationship.TimelineRelationshipId
import dev.usbharu.hideout.core.domain.model.timelinerelationship.TimelineRelationshipRepository
import dev.usbharu.hideout.core.domain.shared.id.IdGenerateService
import org.slf4j.LoggerFactory
import org.springframework.stereotype.Service
@Service
class UserAddTimelineRelationshipApplicationService(
private val timelineRelationshipRepository: TimelineRelationshipRepository,
private val timelineRepository: TimelineRepository,
private val idGenerateService: IdGenerateService,
transaction: Transaction
) :
LocalUserAbstractApplicationService<AddTimelineRelationship, Unit>(
@@ -17,7 +24,21 @@ class UserAddTimelineRelationshipApplicationService(
logger
) {
override suspend fun internalExecute(command: AddTimelineRelationship, principal: LocalUser) {
timelineRelationshipRepository.save(command.timelineRelationship)
val timeline = timelineRepository.findById(command.timelineId)
?: throw IllegalArgumentException("Timeline ${command.timelineId} not found.")
if (timeline.userDetailId != principal.userDetailId) {
throw PermissionDeniedException()
}
timelineRelationshipRepository.save(
TimelineRelationship(
TimelineRelationshipId(idGenerateService.generateId()),
command.timelineId,
command.actorId,
command.visible
)
)
}
companion object {
@@ -0,0 +1,37 @@
package dev.usbharu.hideout.core.application.timeline
import dev.usbharu.hideout.core.application.model.Timeline
import dev.usbharu.hideout.core.application.shared.AbstractApplicationService
import dev.usbharu.hideout.core.application.shared.Transaction
import dev.usbharu.hideout.core.domain.model.support.principal.Principal
import dev.usbharu.hideout.core.domain.model.timeline.TimelineRepository
import dev.usbharu.hideout.core.domain.model.timeline.TimelineVisibility
import dev.usbharu.hideout.core.domain.model.userdetails.UserDetailId
import org.slf4j.LoggerFactory
import org.springframework.stereotype.Service
@Service
class UserGetTimelinesApplicationService(transaction: Transaction, private val timelineRepository: TimelineRepository) :
AbstractApplicationService<GetTimelines, List<Timeline>>(
transaction,
logger
) {
override suspend fun internalExecute(command: GetTimelines, principal: Principal): List<Timeline> {
val userDetailId = UserDetailId(command.userDetailId)
val timelineVisibility = if (userDetailId == principal.userDetailId) {
listOf(TimelineVisibility.PUBLIC, TimelineVisibility.UNLISTED, TimelineVisibility.PRIVATE)
} else {
listOf(TimelineVisibility.PUBLIC)
}
val timelineList =
timelineRepository.findAllByUserDetailIdAndVisibilityIn(userDetailId, timelineVisibility)
return timelineList.map { Timeline.of(it) }
}
companion object {
private val logger = LoggerFactory.getLogger(UserGetTimelinesApplicationService::class.java)
}
}
@@ -0,0 +1,44 @@
package dev.usbharu.hideout.core.application.timeline
import dev.usbharu.hideout.core.application.exception.PermissionDeniedException
import dev.usbharu.hideout.core.application.shared.LocalUserAbstractApplicationService
import dev.usbharu.hideout.core.application.shared.Transaction
import dev.usbharu.hideout.core.domain.model.support.principal.LocalUser
import dev.usbharu.hideout.core.domain.model.timeline.TimelineRepository
import dev.usbharu.hideout.core.domain.model.timelinerelationship.TimelineRelationshipRepository
import org.slf4j.LoggerFactory
import org.springframework.stereotype.Service
@Service
class UserRemoveTimelineRelationshipApplicationService(
transaction: Transaction,
private val timelineRelationshipRepository: TimelineRelationshipRepository,
private val timelineRepository: TimelineRepository
) :
LocalUserAbstractApplicationService<RemoveTimelineRelationship, Unit>(
transaction,
logger
) {
override suspend fun internalExecute(command: RemoveTimelineRelationship, principal: LocalUser) {
val timelineRelationship = (
timelineRelationshipRepository.findById(command.timelineRelationshipId)
?: throw IllegalArgumentException("TimelineRelationship ${command.timelineRelationshipId} not found.")
)
val timeline = (
timelineRepository.findById(timelineRelationship.timelineId)
?: throw IllegalArgumentException("Timeline ${timelineRelationship.timelineId} not found.")
)
if (timeline.userDetailId != principal.userDetailId) {
throw PermissionDeniedException()
}
timelineRelationshipRepository.delete(timelineRelationship)
}
companion object {
private val logger = LoggerFactory.getLogger(UserRemoveTimelineRelationshipApplicationService::class.java)
}
}
@@ -0,0 +1,22 @@
package dev.usbharu.hideout.core.domain.event.reaction
import dev.usbharu.hideout.core.domain.model.reaction.Reaction
import dev.usbharu.hideout.core.domain.model.reaction.ReactionId
import dev.usbharu.hideout.core.domain.shared.domainevent.DomainEvent
import dev.usbharu.hideout.core.domain.shared.domainevent.DomainEventBody
class ReactionEventFactory(private val reaction: Reaction) {
fun createEvent(reactionEvent: ReactionEvent): DomainEvent<ReactionEventBody> =
DomainEvent.create(reactionEvent.eventName, ReactionEventBody(reaction))
}
class ReactionEventBody(
reaction: Reaction
) : DomainEventBody(mapOf("reactionId" to reaction.id)) {
fun getReactionId(): ReactionId = toMap()["reactionId"] as ReactionId
}
enum class ReactionEvent(val eventName: String) {
CREATE("ReactionCreate"),
DELETE("ReactionDelete"),
}
@@ -35,7 +35,8 @@ class RelationshipEventBody(
}
enum class RelationshipEvent(val eventName: String) {
FOLLOW("RelationshipFollow"),
ACCEPT_FOLLOW("RelationshipFollow"),
REJECT_FOLLOW("RelationshipRejectFollow"),
UNFOLLOW("RelationshipUnfollow"),
BLOCK("RelationshipBlock"),
UNBLOCK("RelationshipUnblock"),
@@ -18,7 +18,7 @@ package dev.usbharu.hideout.core.domain.model.actor
import dev.usbharu.hideout.core.domain.event.actor.ActorDomainEventFactory
import dev.usbharu.hideout.core.domain.event.actor.ActorEvent.*
import dev.usbharu.hideout.core.domain.model.emoji.EmojiId
import dev.usbharu.hideout.core.domain.model.emoji.CustomEmojiId
import dev.usbharu.hideout.core.domain.model.instance.InstanceId
import dev.usbharu.hideout.core.domain.model.media.MediaId
import dev.usbharu.hideout.core.domain.model.support.domain.Domain
@@ -52,7 +52,7 @@ class Actor(
var lastUpdateAt: Instant = createdAt,
alsoKnownAs: Set<ActorId> = emptySet(),
moveTo: ActorId? = null,
emojiIds: Set<EmojiId>,
emojiIds: Set<CustomEmojiId>,
deleted: Boolean,
icon: MediaId?,
banner: MediaId?,
@@ -0,0 +1,24 @@
/*
* Copyright (C) 2024 usbharu
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package dev.usbharu.hideout.core.domain.model.actor
enum class Role {
LOCAL,
MODERATOR,
ADMINISTRATOR,
REMOTE
}
@@ -36,7 +36,7 @@ sealed class Emoji {
}
data class CustomEmoji(
val id: EmojiId,
val id: CustomEmojiId,
override val name: String,
override val domain: Domain,
val instanceId: InstanceId,
@@ -50,6 +50,10 @@ data class CustomEmoji(
data class UnicodeEmoji(
override val name: String
) : Emoji() {
override val domain: Domain = Domain("unicode.org")
override val domain: Domain = Companion.domain
override fun id(): String = name
companion object {
val domain = Domain("unicode.org")
}
}
@@ -17,7 +17,7 @@
package dev.usbharu.hideout.core.domain.model.emoji
@JvmInline
value class EmojiId(val emojiId: Long) {
value class CustomEmojiId(val emojiId: Long) {
init {
require(0 <= emojiId)
}
@@ -20,7 +20,7 @@ import dev.usbharu.hideout.core.domain.event.post.PostDomainEventFactory
import dev.usbharu.hideout.core.domain.event.post.PostEvent
import dev.usbharu.hideout.core.domain.model.actor.Actor
import dev.usbharu.hideout.core.domain.model.actor.ActorId
import dev.usbharu.hideout.core.domain.model.emoji.EmojiId
import dev.usbharu.hideout.core.domain.model.emoji.CustomEmojiId
import dev.usbharu.hideout.core.domain.model.instance.InstanceId
import dev.usbharu.hideout.core.domain.model.media.MediaId
import dev.usbharu.hideout.core.domain.shared.domainevent.DomainEventStorable
@@ -138,7 +138,7 @@ class Post(
return content.text
}
val emojiIds: List<EmojiId>
val emojiIds: List<CustomEmojiId>
get() {
if (hide) {
return PostContent.empty.emojiIds
@@ -217,7 +217,7 @@ class Post(
override fun hashCode(): Int = id.hashCode()
fun reconstructWith(mediaIds: List<MediaId>, emojis: List<EmojiId>, visibleActors: Set<ActorId>): Post {
fun reconstructWith(mediaIds: List<MediaId>, emojis: List<CustomEmojiId>, visibleActors: Set<ActorId>): Post {
return Post(
id = id,
actorId = actorId,
@@ -16,9 +16,9 @@
package dev.usbharu.hideout.core.domain.model.post
import dev.usbharu.hideout.core.domain.model.emoji.EmojiId
import dev.usbharu.hideout.core.domain.model.emoji.CustomEmojiId
data class PostContent(val text: String, val content: String, val emojiIds: List<EmojiId>) {
data class PostContent(val text: String, val content: String, val emojiIds: List<CustomEmojiId>) {
companion object {
val empty = PostContent("", "", emptyList())
@@ -0,0 +1,57 @@
package dev.usbharu.hideout.core.domain.model.reaction
import dev.usbharu.hideout.core.domain.event.reaction.ReactionEvent
import dev.usbharu.hideout.core.domain.event.reaction.ReactionEventFactory
import dev.usbharu.hideout.core.domain.model.actor.ActorId
import dev.usbharu.hideout.core.domain.model.emoji.CustomEmojiId
import dev.usbharu.hideout.core.domain.model.emoji.UnicodeEmoji
import dev.usbharu.hideout.core.domain.model.post.PostId
import dev.usbharu.hideout.core.domain.shared.domainevent.DomainEventStorable
import java.time.Instant
class Reaction(
val id: ReactionId,
val postId: PostId,
val actorId: ActorId,
val customEmojiId: CustomEmojiId?,
val unicodeEmoji: UnicodeEmoji,
val createdAt: Instant
) : DomainEventStorable() {
override fun equals(other: Any?): Boolean {
if (this === other) return true
if (javaClass != other?.javaClass) return false
other as Reaction
return id == other.id
}
override fun hashCode(): Int {
return id.hashCode()
}
fun delete() {
addDomainEvent(ReactionEventFactory(this).createEvent(ReactionEvent.DELETE))
}
companion object {
fun create(
id: ReactionId,
postId: PostId,
actorId: ActorId,
customEmojiId: CustomEmojiId?,
unicodeEmoji: UnicodeEmoji,
createdAt: Instant
): Reaction {
return Reaction(
id,
postId,
actorId,
customEmojiId,
unicodeEmoji,
createdAt
).apply { addDomainEvent(ReactionEventFactory(this).createEvent(ReactionEvent.CREATE)) }
}
}
}
@@ -0,0 +1,4 @@
package dev.usbharu.hideout.core.domain.model.reaction
@JvmInline
value class ReactionId(val value: Long)
@@ -0,0 +1,26 @@
package dev.usbharu.hideout.core.domain.model.reaction
import dev.usbharu.hideout.core.domain.model.actor.ActorId
import dev.usbharu.hideout.core.domain.model.emoji.CustomEmojiId
import dev.usbharu.hideout.core.domain.model.post.PostId
interface ReactionRepository {
suspend fun save(reaction: Reaction): Reaction
suspend fun findById(reactionId: ReactionId): Reaction?
suspend fun findByPostId(postId: PostId): List<Reaction>
suspend fun existsByPostIdAndActorIdAndCustomEmojiIdOrUnicodeEmoji(
postId: PostId,
actorId: ActorId,
customEmojiId: CustomEmojiId?,
unicodeEmoji: String
): Boolean
suspend fun findByPostIdAndActorIdAndCustomEmojiIdOrUnicodeEmoji(
postId: PostId,
actorId: ActorId,
customEmojiId: CustomEmojiId?,
unicodeEmoji: String
): Reaction?
suspend fun delete(reaction: Reaction)
}
@@ -44,15 +44,12 @@ class Relationship(
var mutingFollowRequest: Boolean = mutingFollowRequest
private set
fun follow() {
require(blocking.not())
following = true
addDomainEvent(RelationshipEventFactory(this).createEvent(RelationshipEvent.FOLLOW))
}
fun unfollow() {
following = false
addDomainEvent(RelationshipEventFactory(this).createEvent(RelationshipEvent.UNFOLLOW))
followRequesting = false
val relationshipEventFactory = RelationshipEventFactory(this)
addDomainEvent(relationshipEventFactory.createEvent(RelationshipEvent.UNFOLLOW))
addDomainEvent(relationshipEventFactory.createEvent(RelationshipEvent.UNFOLLOW_REQUEST))
}
fun block() {
@@ -96,11 +93,15 @@ class Relationship(
}
fun acceptFollowRequest() {
follow()
require(blocking.not())
following = true
addDomainEvent(RelationshipEventFactory(this).createEvent(RelationshipEvent.ACCEPT_FOLLOW))
followRequesting = false
}
fun rejectFollowRequest() {
require(followRequesting)
addDomainEvent(RelationshipEventFactory(this).createEvent(RelationshipEvent.REJECT_FOLLOW))
followRequesting = false
}
@@ -1,5 +1,6 @@
package dev.usbharu.hideout.core.domain.model.support.timelineobjectdetail
import dev.usbharu.hideout.core.application.model.Reactions
import dev.usbharu.hideout.core.domain.model.actor.Actor
import dev.usbharu.hideout.core.domain.model.media.Media
import dev.usbharu.hideout.core.domain.model.post.Post
@@ -29,7 +30,9 @@ data class TimelineObjectDetail(
val isPureRepost: Boolean,
val lastUpdateAt: Instant,
val hasMediaInRepost: Boolean,
val warnFilter: List<TimelineObjectWarnFilter>
val warnFilter: List<TimelineObjectWarnFilter>,
val reactionsList: List<Reactions>,
val favourited: Boolean
) {
companion object {
@Suppress("LongParameterList")
@@ -48,7 +51,9 @@ data class TimelineObjectDetail(
repostPostMedias: List<Media>?,
repostPostActor: Actor?,
repostPostActorIconMedia: Media?,
warnFilter: List<TimelineObjectWarnFilter>
warnFilter: List<TimelineObjectWarnFilter>,
reactionsList: List<Reactions>,
favourited: Boolean
): TimelineObjectDetail {
return TimelineObjectDetail(
id = timelineObject.id,
@@ -69,7 +74,9 @@ data class TimelineObjectDetail(
isPureRepost = timelineObject.isPureRepost,
lastUpdateAt = timelineObject.lastUpdatedAt,
hasMediaInRepost = timelineObject.hasMediaInRepost,
warnFilter = warnFilter
warnFilter = warnFilter,
reactionsList = reactionsList,
favourited = favourited
)
}
}
@@ -1,5 +1,7 @@
package dev.usbharu.hideout.core.domain.model.timeline
import dev.usbharu.hideout.core.domain.model.userdetails.UserDetailId
interface TimelineRepository {
suspend fun save(timeline: Timeline): Timeline
suspend fun delete(timeline: Timeline)
@@ -7,4 +9,8 @@ interface TimelineRepository {
suspend fun findByIds(ids: List<TimelineId>): List<Timeline>
suspend fun findById(id: TimelineId): Timeline?
suspend fun findAllByUserDetailIdAndVisibilityIn(
userDetailId: UserDetailId,
visibility: List<TimelineVisibility>
): List<Timeline>
}
@@ -1,7 +1,7 @@
package dev.usbharu.hideout.core.domain.model.timelineobject
import dev.usbharu.hideout.core.domain.model.actor.ActorId
import dev.usbharu.hideout.core.domain.model.emoji.EmojiId
import dev.usbharu.hideout.core.domain.model.emoji.CustomEmojiId
import dev.usbharu.hideout.core.domain.model.filter.FilterResult
import dev.usbharu.hideout.core.domain.model.media.MediaId
import dev.usbharu.hideout.core.domain.model.post.Post
@@ -28,12 +28,12 @@ class TimelineObject(
visibility: Visibility,
isPureRepost: Boolean,
mediaIds: List<MediaId>,
emojiIds: List<EmojiId>,
emojiIds: List<CustomEmojiId>,
visibleActors: List<ActorId>,
hasMediaInRepost: Boolean,
lastUpdatedAt: Instant,
var warnFilters: List<TimelineObjectWarnFilter>,
var favourited: Boolean
) {
var isPureRepost = isPureRepost
private set
@@ -82,7 +82,8 @@ class TimelineObject(
timeline: Timeline,
post: Post,
replyActorId: ActorId?,
filterResults: List<FilterResult>
filterResults: List<FilterResult>,
favourited: Boolean
): TimelineObject {
return TimelineObject(
id = timelineObjectId,
@@ -102,7 +103,8 @@ class TimelineObject(
visibleActors = post.visibleActors.toList(),
hasMediaInRepost = false,
lastUpdatedAt = Instant.now(),
warnFilters = filterResults.map { TimelineObjectWarnFilter(it.filter.id, it.matchedKeyword) }
warnFilters = filterResults.map { TimelineObjectWarnFilter(it.filter.id, it.matchedKeyword) },
favourited = favourited
)
}
@@ -113,7 +115,8 @@ class TimelineObject(
post: Post,
replyActorId: ActorId?,
repost: Post,
filterResults: List<FilterResult>
filterResults: List<FilterResult>,
favourited: Boolean
): TimelineObject {
require(post.repostId == repost.id)
@@ -138,7 +141,8 @@ class TimelineObject(
visibleActors = post.visibleActors.toList(),
hasMediaInRepost = repost.mediaIds.isNotEmpty(),
lastUpdatedAt = Instant.now(),
warnFilters = filterResults.map { TimelineObjectWarnFilter(it.filter.id, it.matchedKeyword) }
warnFilters = filterResults.map { TimelineObjectWarnFilter(it.filter.id, it.matchedKeyword) },
favourited = favourited
)
}
}
@@ -8,7 +8,20 @@ class TimelineRelationship(
val timelineId: TimelineId,
val actorId: ActorId,
val visible: Visible
)
) {
override fun equals(other: Any?): Boolean {
if (this === other) return true
if (javaClass != other?.javaClass) return false
other as TimelineRelationship
return id == other.id
}
override fun hashCode(): Int {
return id.hashCode()
}
}
enum class Visible {
PUBLIC,
@@ -1,10 +1,13 @@
package dev.usbharu.hideout.core.domain.model.timelinerelationship
import dev.usbharu.hideout.core.domain.model.actor.ActorId
import dev.usbharu.hideout.core.domain.model.timeline.TimelineId
interface TimelineRelationshipRepository {
suspend fun save(timelineRelationship: TimelineRelationship): TimelineRelationship
suspend fun delete(timelineRelationship: TimelineRelationship)
suspend fun findByActorId(actorId: ActorId): List<TimelineRelationship>
suspend fun findById(timelineRelationshipId: TimelineRelationshipId): TimelineRelationship?
suspend fun findByTimelineIdAndActorId(timelineId: TimelineId, actorId: ActorId): TimelineRelationship?
}
@@ -0,0 +1,5 @@
package dev.usbharu.hideout.core.domain.service.emoji
interface UnicodeEmojiService {
fun isUnicodeEmoji(emoji: String): Boolean
}
@@ -0,0 +1,12 @@
package dev.usbharu.hideout.core.infrastructure.emojikt
import Emojis
import dev.usbharu.hideout.core.domain.service.emoji.UnicodeEmojiService
import org.springframework.stereotype.Service
@Service
class EmojiKtUnicodeEmojiService : UnicodeEmojiService {
override fun isUnicodeEmoji(emoji: String): Boolean {
return Emojis.allEmojis.singleOrNull { it.char == emoji } != null
}
}
@@ -17,7 +17,7 @@
package dev.usbharu.hideout.core.infrastructure.exposed
import dev.usbharu.hideout.core.domain.model.actor.*
import dev.usbharu.hideout.core.domain.model.emoji.EmojiId
import dev.usbharu.hideout.core.domain.model.emoji.CustomEmojiId
import dev.usbharu.hideout.core.domain.model.instance.InstanceId
import dev.usbharu.hideout.core.domain.model.media.MediaId
import dev.usbharu.hideout.core.domain.model.support.domain.Domain
@@ -57,7 +57,7 @@ class ActorResultRowMapper : ResultRowMapper<Actor> {
emojiIds = resultRow[Actors.emojis]
.split(",")
.filter { it.isNotEmpty() }
.map { EmojiId(it.toLong()) }
.map { CustomEmojiId(it.toLong()) }
.toSet(),
deleted = resultRow[Actors.deleted],
icon = resultRow[Actors.icon]?.let { MediaId(it) },
@@ -17,7 +17,7 @@
package dev.usbharu.hideout.core.infrastructure.exposed
import dev.usbharu.hideout.core.domain.model.actor.ActorId
import dev.usbharu.hideout.core.domain.model.emoji.EmojiId
import dev.usbharu.hideout.core.domain.model.emoji.CustomEmojiId
import dev.usbharu.hideout.core.domain.model.media.MediaId
import dev.usbharu.hideout.core.domain.model.post.Post
import dev.usbharu.hideout.core.infrastructure.exposedrepository.Posts
@@ -55,7 +55,7 @@ class PostQueryMapper(private val postResultRowMapper: ResultRowMapper<Post>) :
.mapNotNull { resultRow: ResultRow ->
resultRow
.getOrNull(PostsEmojis.emojiId)
?.let { emojiId -> EmojiId(emojiId) }
?.let { emojiId -> CustomEmojiId(emojiId) }
},
visibleActors = it.mapNotNull { resultRow: ResultRow ->
resultRow
@@ -0,0 +1,73 @@
package dev.usbharu.hideout.core.infrastructure.exposedquery
import dev.usbharu.hideout.core.application.model.Reactions
import dev.usbharu.hideout.core.domain.model.emoji.UnicodeEmoji
import dev.usbharu.hideout.core.domain.model.post.PostId
import dev.usbharu.hideout.core.infrastructure.exposedrepository.AbstractRepository
import dev.usbharu.hideout.core.infrastructure.exposedrepository.CustomEmojis
import dev.usbharu.hideout.core.infrastructure.exposedrepository.toCustomEmojiOrNull
import dev.usbharu.hideout.core.infrastructure.exposedrepository.toReaction
import dev.usbharu.hideout.core.query.reactions.ReactionsQueryService
import org.jetbrains.exposed.sql.*
import org.slf4j.Logger
import org.slf4j.LoggerFactory
import org.springframework.stereotype.Repository
import java.net.URI
import dev.usbharu.hideout.core.infrastructure.exposedrepository.Reactions as ExposedrepositoryReactions
@Repository
class ExposedReactionsQueryService : ReactionsQueryService, AbstractRepository() {
override suspend fun findAllByPostId(postId: PostId): List<Reactions> {
return query {
ExposedrepositoryReactions.leftJoin(CustomEmojis).selectAll()
.where { ExposedrepositoryReactions.postId eq postId.id }
.groupBy {
it[ExposedrepositoryReactions.customEmojiId]?.toString()
?: it[ExposedrepositoryReactions.unicodeEmoji]
}
.map { it.value }
.map {
Reactions.of(
it.map { resultRow -> resultRow.toReaction() },
it.first().toCustomEmojiOrNull()
)
}
}
}
override suspend fun findAllByPostIdIn(postIds: List<PostId>): List<Reactions> {
return query {
val actorIdsQuery =
ExposedrepositoryReactions.actorId.castTo<String>(VarCharColumnType()).groupConcat(",", true)
ExposedrepositoryReactions.leftJoin(CustomEmojis)
.select(
ExposedrepositoryReactions.postId,
ExposedrepositoryReactions.postId.count(),
ExposedrepositoryReactions.customEmojiId.max(),
ExposedrepositoryReactions.unicodeEmoji.max<String, String>(),
actorIdsQuery
)
.where { ExposedrepositoryReactions.postId inList postIds.map { it.id } }
.groupBy(ExposedrepositoryReactions.postId)
.map {
Reactions(
it[ExposedrepositoryReactions.postId],
it[ExposedrepositoryReactions.postId.count()].toInt(),
it.getOrNull(CustomEmojis.name)
?: it[ExposedrepositoryReactions.unicodeEmoji.max<String, String>()]!!,
it.getOrNull(CustomEmojis.domain) ?: UnicodeEmoji.domain.domain,
it.getOrNull(CustomEmojis.url)?.let { it1 -> URI.create(it1) },
it[actorIdsQuery].split(",").mapNotNull { it.toLongOrNull() }
)
}
}
}
override val logger: Logger
get() = Companion.logger
companion object {
private val logger = LoggerFactory.getLogger(ExposedReactionsQueryService::class.java)
}
}
@@ -61,6 +61,12 @@ class ExposedUserTimelineQueryService : UserTimelineQueryService, AbstractReposi
.leftJoin(iconMedia, { Actors.icon }, { iconMedia[Media.id] })
.leftJoin(PostsMedia, { authorizedQuery[Posts.id] }, { PostsMedia.postId })
.leftJoin(Media, { PostsMedia.mediaId }, { Media.id })
.leftJoin(
Reactions,
{ authorizedQuery[Posts.id] },
{ Reactions.postId },
{ Reactions.id isDistinctFrom principal.actorId.id }
)
.selectAll()
.where { authorizedQuery[Posts.id] inList idList.map { it.id } }
.groupBy { it[authorizedQuery[Posts.id]] }
@@ -69,7 +75,8 @@ class ExposedUserTimelineQueryService : UserTimelineQueryService, AbstractReposi
toPostDetail(it.first(), authorizedQuery, iconMedia).copy(
mediaDetailList = it.mapNotNull { resultRow ->
resultRow.toMediaOrNull()?.let { it1 -> MediaDetail.of(it1) }
}
},
favourited = it.any { it.getOrNull(Reactions.actorId) != null }
)
}
}
@@ -100,7 +107,9 @@ class ExposedUserTimelineQueryService : UserTimelineQueryService, AbstractReposi
sensitive = it[authorizedQuery[Posts.sensitive]],
deleted = it[authorizedQuery[Posts.deleted]],
mediaDetailList = emptyList(),
moveTo = null
moveTo = null,
emptyList(),
favourited = false
)
}
@@ -17,8 +17,8 @@
package dev.usbharu.hideout.core.infrastructure.exposedrepository
import dev.usbharu.hideout.core.domain.model.emoji.CustomEmoji
import dev.usbharu.hideout.core.domain.model.emoji.CustomEmojiId
import dev.usbharu.hideout.core.domain.model.emoji.CustomEmojiRepository
import dev.usbharu.hideout.core.domain.model.emoji.EmojiId
import dev.usbharu.hideout.core.domain.model.instance.InstanceId
import dev.usbharu.hideout.core.domain.model.support.domain.Domain
import org.jetbrains.exposed.sql.*
@@ -37,27 +37,14 @@ class CustomEmojiRepositoryImpl : CustomEmojiRepository,
get() = Companion.logger
override suspend fun save(customEmoji: CustomEmoji): CustomEmoji = query {
val singleOrNull =
CustomEmojis.selectAll().where { CustomEmojis.id eq customEmoji.id.emojiId }.forUpdate().singleOrNull()
if (singleOrNull == null) {
CustomEmojis.insert {
it[id] = customEmoji.id.emojiId
it[name] = customEmoji.name
it[domain] = customEmoji.domain.domain
it[instanceId] = customEmoji.instanceId.instanceId
it[url] = customEmoji.url.toString()
it[category] = customEmoji.category
it[createdAt] = customEmoji.createdAt
}
} else {
CustomEmojis.update({ CustomEmojis.id eq customEmoji.id.emojiId }) {
it[name] = customEmoji.name
it[domain] = customEmoji.domain.domain
it[instanceId] = customEmoji.instanceId.instanceId
it[url] = customEmoji.url.toString()
it[category] = customEmoji.category
it[createdAt] = customEmoji.createdAt
}
CustomEmojis.upsert {
it[id] = customEmoji.id.emojiId
it[name] = customEmoji.name
it[domain] = customEmoji.domain.domain
it[instanceId] = customEmoji.instanceId.instanceId
it[url] = customEmoji.url.toString()
it[category] = customEmoji.category
it[createdAt] = customEmoji.createdAt
}
return@query customEmoji
}
@@ -94,7 +81,7 @@ class CustomEmojiRepositoryImpl : CustomEmojiRepository,
}
fun ResultRow.toCustomEmoji(): CustomEmoji = CustomEmoji(
id = EmojiId(this[CustomEmojis.id]),
id = CustomEmojiId(this[CustomEmojis.id]),
name = this[CustomEmojis.name],
domain = Domain(this[CustomEmojis.domain]),
instanceId = InstanceId(this[CustomEmojis.instanceId]),
@@ -105,7 +92,7 @@ fun ResultRow.toCustomEmoji(): CustomEmoji = CustomEmoji(
fun ResultRow.toCustomEmojiOrNull(): CustomEmoji? {
return CustomEmoji(
id = EmojiId(this.getOrNull(CustomEmojis.id) ?: return null),
id = CustomEmojiId(this.getOrNull(CustomEmojis.id) ?: return null),
name = this.getOrNull(CustomEmojis.name) ?: return null,
domain = Domain(this.getOrNull(CustomEmojis.domain) ?: return null),
instanceId = InstanceId(this.getOrNull(CustomEmojis.instanceId) ?: return null),
@@ -168,7 +168,7 @@ class ExposedPostRepository(
Posts.id eq id.id
}
.let(postQueryMapper::map)
.first()
.firstOrNull()
}
override suspend fun findAllById(ids: List<PostId>): List<Post> {
@@ -0,0 +1,128 @@
package dev.usbharu.hideout.core.infrastructure.exposedrepository
import dev.usbharu.hideout.core.domain.model.actor.ActorId
import dev.usbharu.hideout.core.domain.model.emoji.CustomEmojiId
import dev.usbharu.hideout.core.domain.model.emoji.UnicodeEmoji
import dev.usbharu.hideout.core.domain.model.post.PostId
import dev.usbharu.hideout.core.domain.model.reaction.Reaction
import dev.usbharu.hideout.core.domain.model.reaction.ReactionId
import dev.usbharu.hideout.core.domain.model.reaction.ReactionRepository
import dev.usbharu.hideout.core.domain.shared.domainevent.DomainEventPublisher
import dev.usbharu.hideout.core.domain.shared.repository.DomainEventPublishableRepository
import org.jetbrains.exposed.sql.*
import org.jetbrains.exposed.sql.SqlExpressionBuilder.eq
import org.jetbrains.exposed.sql.javatime.timestamp
import org.slf4j.Logger
import org.slf4j.LoggerFactory
import org.springframework.stereotype.Repository
@Repository
class ExposedReactionRepository(override val domainEventPublisher: DomainEventPublisher) :
ReactionRepository,
AbstractRepository(),
DomainEventPublishableRepository<Reaction> {
override val logger: Logger
get() = Companion.logger
override suspend fun save(reaction: Reaction): Reaction {
return query {
Reactions.upsert {
it[Reactions.id] = reaction.id.value
it[Reactions.postId] = reaction.postId.id
it[Reactions.actorId] = reaction.actorId.id
it[Reactions.customEmojiId] = reaction.customEmojiId?.emojiId
it[Reactions.unicodeEmoji] = reaction.unicodeEmoji.name
it[Reactions.createdAt] = reaction.createdAt
}
onComplete {
update(reaction)
}
reaction
}
}
override suspend fun findById(reactionId: ReactionId): Reaction? {
return query {
Reactions.selectAll().where {
Reactions.id eq reactionId.value
}.singleOrNull()?.toReaction()
}
}
override suspend fun findByPostId(postId: PostId): List<Reaction> {
return query {
Reactions.selectAll().where {
Reactions.postId eq postId.id
}.map { it.toReaction() }
}
}
override suspend fun existsByPostIdAndActorIdAndCustomEmojiIdOrUnicodeEmoji(
postId: PostId,
actorId: ActorId,
customEmojiId: CustomEmojiId?,
unicodeEmoji: String
): Boolean {
return query {
Reactions.selectAll().where {
Reactions.postId.eq(postId.id).and(Reactions.actorId eq actorId.id)
.and(
(Reactions.customEmojiId eq customEmojiId?.emojiId or (Reactions.unicodeEmoji eq unicodeEmoji))
)
}.empty().not()
}
}
override suspend fun delete(reaction: Reaction) {
return query {
Reactions.deleteWhere {
Reactions.id eq reaction.id.value
}
onComplete {
update(reaction)
}
}
}
override suspend fun findByPostIdAndActorIdAndCustomEmojiIdOrUnicodeEmoji(
postId: PostId,
actorId: ActorId,
customEmojiId: CustomEmojiId?,
unicodeEmoji: String
): Reaction? {
return query {
Reactions.selectAll().where {
Reactions.postId.eq(postId.id).and(Reactions.actorId eq actorId.id)
.and(
(Reactions.customEmojiId eq customEmojiId?.emojiId or (Reactions.unicodeEmoji eq unicodeEmoji))
)
}.limit(1).singleOrNull()?.toReaction()
}
}
companion object {
private val logger = LoggerFactory.getLogger(ExposedReactionRepository::class.java)
}
}
fun ResultRow.toReaction(): Reaction {
return Reaction(
ReactionId(this[Reactions.id]),
PostId(this[Reactions.postId]),
ActorId(this[Reactions.actorId]),
this[Reactions.customEmojiId]?.let { CustomEmojiId(it) },
UnicodeEmoji(this[Reactions.unicodeEmoji]),
this[Reactions.createdAt]
)
}
object Reactions : Table("reactions") {
val id = long("id")
val postId = long("post_id").references(Posts.id)
val actorId = long("actor_id").references(Actors.id)
val customEmojiId = long("custom_emoji_id").references(CustomEmojis.id).nullable()
val unicodeEmoji = varchar("unicode_emoji", 100)
val createdAt = timestamp("created_at")
override val primaryKey: PrimaryKey = PrimaryKey(id)
}
@@ -19,7 +19,7 @@ class ExposedTimelineRelationshipRepository : AbstractRepository(), TimelineRela
override suspend fun save(timelineRelationship: TimelineRelationship): TimelineRelationship {
query {
TimelineRelationships.insert {
TimelineRelationships.upsert {
it[id] = timelineRelationship.id.value
it[timelineId] = timelineRelationship.timelineId.value
it[actorId] = timelineRelationship.actorId.id
@@ -45,6 +45,22 @@ class ExposedTimelineRelationshipRepository : AbstractRepository(), TimelineRela
}
}
override suspend fun findById(timelineRelationshipId: TimelineRelationshipId): TimelineRelationship? {
return query {
TimelineRelationships.selectAll().where {
TimelineRelationships.id eq timelineRelationshipId.value
}.singleOrNull()?.toTimelineRelationship()
}
}
override suspend fun findByTimelineIdAndActorId(timelineId: TimelineId, actorId: ActorId): TimelineRelationship? {
return query {
TimelineRelationships.selectAll().where {
TimelineRelationships.timelineId eq timelineId.value and (TimelineRelationships.actorId eq actorId.id)
}.singleOrNull()?.toTimelineRelationship()
}
}
companion object {
private val logger = LoggerFactory.getLogger(ExposedTimelineRelationshipRepository::class.java)
}
@@ -20,7 +20,7 @@ class ExposedTimelineRepository(override val domainEventPublisher: DomainEventPu
override suspend fun save(timeline: Timeline): Timeline {
query {
Timelines.insert {
Timelines.upsert {
it[id] = timeline.id.value
it[userDetailId] = timeline.userDetailId.id
it[name] = timeline.name.value
@@ -58,6 +58,17 @@ class ExposedTimelineRepository(override val domainEventPublisher: DomainEventPu
}
}
override suspend fun findAllByUserDetailIdAndVisibilityIn(
userDetailId: UserDetailId,
visibility: List<TimelineVisibility>
): List<Timeline> {
return query {
Timelines.selectAll().where {
Timelines.userDetailId eq userDetailId.id and (Timelines.visibility inList visibility.map { it.name })
}.map { it.toTimeline() }
}
}
companion object {
private val logger = LoggerFactory.getLogger(ExposedTimelineRepository::class.java.name)
}
@@ -33,36 +33,21 @@ class InstanceRepositoryImpl : InstanceRepository,
get() = Companion.logger
override suspend fun save(instance: InstanceEntity): InstanceEntity = query {
if (Instance.selectAll().where { Instance.id.eq(instance.id.instanceId) }.forUpdate().empty()) {
Instance.insert {
it[id] = instance.id.instanceId
it[name] = instance.name.name
it[description] = instance.description.description
it[url] = instance.url.toString()
it[iconUrl] = instance.iconUrl.toString()
it[sharedInbox] = instance.sharedInbox?.toString()
it[software] = instance.software.software
it[version] = instance.version.version
it[isBlocked] = instance.isBlocked
it[isMuted] = instance.isMuted
it[moderationNote] = instance.moderationNote.note
it[createdAt] = instance.createdAt
}
} else {
Instance.update({ Instance.id eq instance.id.instanceId }) {
it[name] = instance.name.name
it[description] = instance.description.description
it[url] = instance.url.toString()
it[iconUrl] = instance.iconUrl.toString()
it[sharedInbox] = instance.sharedInbox?.toString()
it[software] = instance.software.software
it[version] = instance.version.version
it[isBlocked] = instance.isBlocked
it[isMuted] = instance.isMuted
it[moderationNote] = instance.moderationNote.note
it[createdAt] = instance.createdAt
}
Instance.upsert {
it[id] = instance.id.instanceId
it[name] = instance.name.name
it[description] = instance.description.description
it[url] = instance.url.toString()
it[iconUrl] = instance.iconUrl.toString()
it[sharedInbox] = instance.sharedInbox?.toString()
it[software] = instance.software.software
it[version] = instance.version.version
it[isBlocked] = instance.isBlocked
it[isMuted] = instance.isMuted
it[moderationNote] = instance.moderationNote.note
it[createdAt] = instance.createdAt
}
return@query instance
}
@@ -31,6 +31,22 @@ class MediaRepositoryImpl : MediaRepository, AbstractRepository() {
override val logger: Logger
get() = Companion.logger
override suspend fun save(media: EntityMedia): EntityMedia = query {
Media.upsert {
it[id] = media.id.id
it[name] = media.name.name
it[url] = media.url.toString()
it[remoteUrl] = media.remoteUrl?.toString()
it[thumbnailUrl] = media.thumbnailUrl?.toString()
it[type] = media.type.name
it[blurhash] = media.blurHash?.hash
it[mimeType] = media.mimeType.type + "/" + media.mimeType.subtype
it[description] = media.description?.description
it[actorId] = media.actorId.id
}
return@query media
}
override suspend fun findById(id: MediaId): dev.usbharu.hideout.core.domain.model.media.Media? {
return query {
return@query Media
@@ -55,37 +71,6 @@ class MediaRepositoryImpl : MediaRepository, AbstractRepository() {
}
}
override suspend fun save(media: EntityMedia): EntityMedia = query {
if (Media.selectAll().where { Media.id eq media.id.id }.forUpdate().singleOrNull() != null
) {
Media.update({ Media.id eq media.id.id }) {
it[name] = media.name.name
it[url] = media.url.toString()
it[remoteUrl] = media.remoteUrl?.toString()
it[thumbnailUrl] = media.thumbnailUrl?.toString()
it[type] = media.type.name
it[blurhash] = media.blurHash?.hash
it[mimeType] = media.mimeType.type + "/" + media.mimeType.subtype
it[description] = media.description?.description
it[actorId] = media.actorId.id
}
} else {
Media.insert {
it[id] = media.id.id
it[name] = media.name.name
it[url] = media.url.toString()
it[remoteUrl] = media.remoteUrl?.toString()
it[thumbnailUrl] = media.thumbnailUrl?.toString()
it[type] = media.type.name
it[blurhash] = media.blurHash?.hash
it[mimeType] = media.mimeType.type + "/" + media.mimeType.subtype
it[description] = media.description?.description
it[actorId] = media.actorId.id
}
}
return@query media
}
companion object {
private val logger = LoggerFactory.getLogger(MediaRepositoryImpl::class.java)
}
@@ -41,26 +41,15 @@ class UserDetailRepositoryImpl(override val domainEventPublisher: DomainEventPub
override suspend fun save(userDetail: UserDetail): UserDetail {
val userDetail1 = query {
val singleOrNull =
UserDetails.selectAll().where { UserDetails.id eq userDetail.id.id }.forUpdate().singleOrNull()
if (singleOrNull == null) {
UserDetails.insert {
it[id] = userDetail.id.id
it[actorId] = userDetail.actorId.id
it[password] = userDetail.password.password
it[autoAcceptFolloweeFollowRequest] = userDetail.autoAcceptFolloweeFollowRequest
it[lastMigration] = userDetail.lastMigration
it[homeTimelineId] = userDetail.homeTimelineId?.value
}
} else {
UserDetails.update({ UserDetails.id eq userDetail.id.id }) {
it[actorId] = userDetail.actorId.id
it[password] = userDetail.password.password
it[autoAcceptFolloweeFollowRequest] = userDetail.autoAcceptFolloweeFollowRequest
it[lastMigration] = userDetail.lastMigration
it[homeTimelineId] = userDetail.homeTimelineId?.value
}
UserDetails.upsert {
it[id] = userDetail.id.id
it[actorId] = userDetail.actorId.id
it[password] = userDetail.password.password
it[autoAcceptFolloweeFollowRequest] = userDetail.autoAcceptFolloweeFollowRequest
it[lastMigration] = userDetail.lastMigration
it[homeTimelineId] = userDetail.homeTimelineId?.value
}
onComplete {
update(userDetail)
}
@@ -1,7 +1,7 @@
package dev.usbharu.hideout.core.infrastructure.mongorepository
import dev.usbharu.hideout.core.domain.model.actor.ActorId
import dev.usbharu.hideout.core.domain.model.emoji.EmojiId
import dev.usbharu.hideout.core.domain.model.emoji.CustomEmojiId
import dev.usbharu.hideout.core.domain.model.filter.FilterId
import dev.usbharu.hideout.core.domain.model.media.MediaId
import dev.usbharu.hideout.core.domain.model.post.PostId
@@ -133,7 +133,8 @@ data class SpringDataMongoTimelineObject(
val visibleActors: List<Long>,
val hasMediaInRepost: Boolean,
val lastUpdatedAt: Long,
val warnFilters: List<SpringDataMongoTimelineObjectWarnFilter>
val warnFilters: List<SpringDataMongoTimelineObjectWarnFilter>,
val favourited: Boolean
) {
fun toTimelineObject(): TimelineObject {
@@ -151,11 +152,12 @@ data class SpringDataMongoTimelineObject(
visibility = visibility,
isPureRepost = isPureRepost,
mediaIds = mediaIds.map { MediaId(it) },
emojiIds = emojiIds.map { EmojiId(it) },
emojiIds = emojiIds.map { CustomEmojiId(it) },
visibleActors = visibleActors.map { ActorId(it) },
hasMediaInRepost = hasMediaInRepost,
lastUpdatedAt = Instant.ofEpochSecond(lastUpdatedAt),
warnFilters = warnFilters.map { it.toTimelineObjectWarnFilter() }
warnFilters = warnFilters.map { it.toTimelineObjectWarnFilter() },
favourited = favourited
)
}
@@ -179,7 +181,8 @@ data class SpringDataMongoTimelineObject(
visibleActors = timelineObject.visibleActors.map { it.id },
hasMediaInRepost = timelineObject.hasMediaInRepost,
lastUpdatedAt = timelineObject.lastUpdatedAt.epochSecond,
warnFilters = timelineObject.warnFilters.map { SpringDataMongoTimelineObjectWarnFilter.of(it) }
warnFilters = timelineObject.warnFilters.map { SpringDataMongoTimelineObjectWarnFilter.of(it) },
favourited = timelineObject.favourited
)
}
}
@@ -29,7 +29,7 @@ class SPAInterceptor : HandlerInterceptor {
return
}
if (request.session.getAttribute("s") == "f") {
if (request.getSession(false)?.getAttribute("s") == "f") {
return
}
@@ -1,5 +1,6 @@
package dev.usbharu.hideout.core.infrastructure.timeline
import dev.usbharu.hideout.core.application.model.Reactions
import dev.usbharu.hideout.core.domain.model.actor.Actor
import dev.usbharu.hideout.core.domain.model.actor.ActorId
import dev.usbharu.hideout.core.domain.model.filter.Filter
@@ -75,7 +76,8 @@ abstract class AbstractTimelineStore(private val idGenerateService: IdGenerateSe
post = post,
replyActorId = replyActorId,
repost = repost,
filterResults = applyFilters.filterResults
filterResults = applyFilters.filterResults,
favourited = false
)
}
@@ -84,7 +86,8 @@ abstract class AbstractTimelineStore(private val idGenerateService: IdGenerateSe
timeline = timeline,
post = post,
replyActorId = replyActorId,
filterResults = applyFilters.filterResults
filterResults = applyFilters.filterResults,
favourited = false
)
}
@@ -256,6 +259,8 @@ abstract class AbstractTimelineStore(private val idGenerateService: IdGenerateSe
actors.mapNotNull { it.value.icon }
)
val reactions = getReactions(posts.map { it.id })
return PaginationList(
timelineObjectList.mapNotNull<TimelineObject, TimelineObjectDetail> {
val timelineUserDetail = userDetails[it.userDetailId] ?: return@mapNotNull null
@@ -268,6 +273,7 @@ abstract class AbstractTimelineStore(private val idGenerateService: IdGenerateSe
val repost = postMap[it.repostId]
val repostMedias = repost?.post?.mediaIds?.mapNotNull { mediaId -> mediaMap[mediaId] }
val repostActor = actors[it.repostActorId]
val reactionsList = reactions[it.postId].orEmpty()
TimelineObjectDetail.of(
timelineObject = it,
timelineUserDetail = timelineUserDetail,
@@ -288,7 +294,9 @@ abstract class AbstractTimelineStore(private val idGenerateService: IdGenerateSe
filterResult.filter.id,
filterResult.matchedKeyword
)
}
},
reactionsList = reactionsList,
favourited = it.favourited
)
},
timelineObjectList.lastOrNull()?.postId,
@@ -300,5 +308,7 @@ abstract class AbstractTimelineStore(private val idGenerateService: IdGenerateSe
protected abstract suspend fun getMedias(mediaIds: List<MediaId>): Map<MediaId, Media>
protected abstract suspend fun getReactions(postIds: List<PostId>): Map<PostId, List<Reactions>>
protected abstract suspend fun getUserDetails(userDetailIdList: List<UserDetailId>): Map<UserDetailId, UserDetail>
}
@@ -1,5 +1,6 @@
package dev.usbharu.hideout.core.infrastructure.timeline
import dev.usbharu.hideout.core.application.model.Reactions
import dev.usbharu.hideout.core.config.DefaultTimelineStoreConfig
import dev.usbharu.hideout.core.domain.model.actor.Actor
import dev.usbharu.hideout.core.domain.model.actor.ActorId
@@ -32,6 +33,7 @@ import dev.usbharu.hideout.core.domain.service.filter.FilterDomainService
import dev.usbharu.hideout.core.domain.service.post.IPostReadAccessControl
import dev.usbharu.hideout.core.domain.shared.id.IdGenerateService
import dev.usbharu.hideout.core.external.timeline.ReadTimelineOption
import dev.usbharu.hideout.core.query.reactions.ReactionsQueryService
import org.springframework.stereotype.Component
import java.time.Instant
@@ -49,7 +51,8 @@ open class DefaultTimelineStore(
private val userDetailRepository: UserDetailRepository,
private val actorRepository: ActorRepository,
private val mediaRepository: MediaRepository,
private val postIPostReadAccessControl: IPostReadAccessControl
private val postIPostReadAccessControl: IPostReadAccessControl,
private val reactionsQueryService: ReactionsQueryService,
) : AbstractTimelineStore(idGenerateService) {
override suspend fun getTimelines(actorId: ActorId): List<Timeline> {
return timelineRepository.findByIds(
@@ -157,6 +160,10 @@ open class DefaultTimelineStore(
override suspend fun getMedias(mediaIds: List<MediaId>): Map<MediaId, Media> =
mediaRepository.findByIds(mediaIds).associateBy { it.id }
override suspend fun getReactions(postIds: List<PostId>): Map<PostId, List<Reactions>> {
return reactionsQueryService.findAllByPostIdIn(postIds).groupBy { PostId(it.postId) }
}
override suspend fun getUserDetails(userDetailIdList: List<UserDetailId>): Map<UserDetailId, UserDetail> =
userDetailRepository.findAllById(userDetailIdList).associateBy { it.id }
}
@@ -4,18 +4,25 @@ import dev.usbharu.hideout.core.application.exception.PermissionDeniedException
import dev.usbharu.hideout.core.application.instance.GetLocalInstanceApplicationService
import dev.usbharu.hideout.core.application.post.GetPostDetail
import dev.usbharu.hideout.core.application.post.GetPostDetailApplicationService
import dev.usbharu.hideout.core.application.reaction.CreateReaction
import dev.usbharu.hideout.core.application.reaction.RemoveReaction
import dev.usbharu.hideout.core.application.reaction.UserCreateReactionApplicationService
import dev.usbharu.hideout.core.application.reaction.UserRemoveReactionApplicationService
import dev.usbharu.hideout.core.infrastructure.springframework.SpringSecurityFormLoginPrincipalContextHolder
import org.springframework.security.access.AccessDeniedException
import org.springframework.stereotype.Controller
import org.springframework.ui.Model
import org.springframework.web.bind.annotation.GetMapping
import org.springframework.web.bind.annotation.PathVariable
import org.springframework.web.bind.annotation.PostMapping
@Controller
class PostsController(
private val getPostDetailApplicationService: GetPostDetailApplicationService,
private val springSecurityFormLoginPrincipalContextHolder: SpringSecurityFormLoginPrincipalContextHolder,
private val getLocalInstanceApplicationService: GetLocalInstanceApplicationService
private val getLocalInstanceApplicationService: GetLocalInstanceApplicationService,
private val userCreateReactionApplicationService: UserCreateReactionApplicationService,
private val userRemoveReactionApplicationService: UserRemoveReactionApplicationService
) {
@GetMapping("/users/{name}/posts/{id}")
suspend fun postById(@PathVariable id: Long, model: Model): String {
@@ -31,4 +38,32 @@ class PostsController(
return "postById"
}
@PostMapping("/users/{name}/posts/{id}/favourite")
suspend fun favourite(@PathVariable id: Long, @PathVariable name: String): String {
val principal = springSecurityFormLoginPrincipalContextHolder.getPrincipal()
userCreateReactionApplicationService.execute(
CreateReaction(
id,
null,
""
),
principal
)
return "redirect:/users/$name/posts/$id"
}
@PostMapping("/users/{name}/posts/{id}/unfavourite")
suspend fun unfavourite(@PathVariable id: Long, @PathVariable name: String): String {
val principal = springSecurityFormLoginPrincipalContextHolder.getPrincipal()
userRemoveReactionApplicationService.execute(
RemoveReaction(
id,
null,
""
),
principal
)
return "redirect:/users/$name/posts/$id"
}
}
@@ -3,16 +3,24 @@ package dev.usbharu.hideout.core.interfaces.web.user
import dev.usbharu.hideout.core.application.actor.GetActorDetail
import dev.usbharu.hideout.core.application.actor.GetActorDetailApplicationService
import dev.usbharu.hideout.core.application.instance.GetLocalInstanceApplicationService
import dev.usbharu.hideout.core.application.relationship.followrequest.FollowRequest
import dev.usbharu.hideout.core.application.relationship.followrequest.UserFollowRequestApplicationService
import dev.usbharu.hideout.core.application.relationship.get.GetRelationship
import dev.usbharu.hideout.core.application.relationship.get.GetRelationshipApplicationService
import dev.usbharu.hideout.core.application.relationship.unfollow.Unfollow
import dev.usbharu.hideout.core.application.relationship.unfollow.UserUnfollowApplicationService
import dev.usbharu.hideout.core.application.timeline.GetUserTimeline
import dev.usbharu.hideout.core.application.timeline.GetUserTimelineApplicationService
import dev.usbharu.hideout.core.domain.model.support.acct.Acct
import dev.usbharu.hideout.core.domain.model.support.page.Page
import dev.usbharu.hideout.core.domain.model.support.principal.Anonymous
import dev.usbharu.hideout.core.domain.model.support.principal.LocalUser
import dev.usbharu.hideout.core.infrastructure.springframework.SpringSecurityFormLoginPrincipalContextHolder
import org.springframework.stereotype.Controller
import org.springframework.ui.Model
import org.springframework.web.bind.annotation.GetMapping
import org.springframework.web.bind.annotation.PathVariable
import org.springframework.web.bind.annotation.PostMapping
import org.springframework.web.bind.annotation.RequestParam
@Controller
@@ -20,7 +28,11 @@ class UserController(
private val getLocalInstanceApplicationService: GetLocalInstanceApplicationService,
private val getUserDetailApplicationService: GetActorDetailApplicationService,
private val springSecurityFormLoginPrincipalContextHolder: SpringSecurityFormLoginPrincipalContextHolder,
private val getUserTimelineApplicationService: GetUserTimelineApplicationService
private val getUserTimelineApplicationService: GetUserTimelineApplicationService,
private val userFollowRequestApplicationService: UserFollowRequestApplicationService,
private val getActorDetailApplicationService: GetActorDetailApplicationService,
private val userUnfollowApplicationService: UserUnfollowApplicationService,
private val userGetRelationshipApplicationService: GetRelationshipApplicationService
) {
@GetMapping("/users/{name}")
suspend fun userById(
@@ -38,6 +50,14 @@ class UserController(
"user",
actorDetail
)
val relationship =
if (principal is LocalUser) {
userGetRelationshipApplicationService.execute(GetRelationship(actorDetail.id), principal)
} else {
null
}
model.addAttribute("relationship", relationship)
model.addAttribute(
"userTimeline",
getUserTimelineApplicationService.execute(
@@ -50,4 +70,24 @@ class UserController(
)
return "userById"
}
@PostMapping("/users/{name}/follow")
suspend fun follow(@PathVariable name: String): String {
val principal = springSecurityFormLoginPrincipalContextHolder.getPrincipal()
val actorDetail = getActorDetailApplicationService.execute(GetActorDetail(Acct.of(name), null), principal)
userFollowRequestApplicationService.execute(FollowRequest((actorDetail.id)), principal)
return "redirect:/users/$name"
}
@PostMapping("/users/{name}/unfollow")
suspend fun unfollow(@PathVariable name: String): String {
val principal = springSecurityFormLoginPrincipalContextHolder.getPrincipal()
val actorDetail = getActorDetailApplicationService.execute(GetActorDetail(Acct.of(name), null), principal)
userUnfollowApplicationService.execute(Unfollow((actorDetail.id)), principal)
return "redirect:/users/$name"
}
}
@@ -0,0 +1,9 @@
package dev.usbharu.hideout.core.query.reactions
import dev.usbharu.hideout.core.application.model.Reactions
import dev.usbharu.hideout.core.domain.model.post.PostId
interface ReactionsQueryService {
suspend fun findAllByPostId(postId: PostId): List<Reactions>
suspend fun findAllByPostIdIn(postIds: List<PostId>): List<Reactions>
}
@@ -4,6 +4,8 @@ hideout:
private: true
spring:
application:
name: hideout
data:
mongodb:
auto-index-creation: true
@@ -318,4 +318,17 @@ create table if not exists filter_keywords
keyword varchar(1000) not null,
mode varchar(100) not null,
constraint fk_filter_keywords_filter_id__id foreign key (filter_id) references filters (id) on delete cascade on update cascade
)
);
create table if not exists reactions
(
id bigint primary key,
post_id bigint not null,
actor_id bigint not null,
custom_emoji_id bigint null,
unicode_emoji varchar(100) not null,
created_at timestamp not null,
constraint fk_reactions_post_id__id foreign key (post_id) references posts (id) on delete cascade on update cascade,
constraint fk_reactions_actor_id__id foreign key (actor_id) references actors (id) on delete cascade on update cascade,
unique (post_id, actor_id, created_at, unicode_emoji)
);
@@ -1,19 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<Configuration status="WARN">
<Appenders>
<Console name="Console" target="SYSTEM_OUT">
<PatternLayout pattern="%d{HH:mm:ss.SSS} [%.15t] %highlight{%-5level} %logger{36} - %msg%n"/>
</Console>
</Appenders>
<Loggers>
<Root level="INFO">
<AppenderRef ref="Console"/>
</Root>
<Logger name="dev.usbharu.owl.broker.service.QueuedTaskAssignerImpl" level="TRACE"/>
<Logger name="org.mongodb.driver.cluster" level="INFO"/>
<Logger name="org.apache.tomcat.util.net.NioEndpoint" level="INFO"/>
<!-- <Logger name="Exposed" level="DEBUG"/>-->
<Logger name="sun.rmi" level="INFO"/>
<Logger name="javax.management.remote.rmi" level="INFO"/>
</Loggers>
</Configuration>
@@ -0,0 +1,15 @@
<?xml version="1.0" encoding="UTF-8"?>
<configuration>
<property name="LOG_FILE" value="logs/logFile.log"/>
<property name="CONSOLE_LOG_THRESHOLD" value="${CONSOLE_LOG_THRESHOLD:-INFO}"/>
<include resource="org/springframework/boot/logging/logback/defaults.xml"/>
<include resource="org/springframework/boot/logging/logback/console-appender.xml"/>
<include resource="org/springframework/boot/logging/logback/file-appender.xml"/>
<include resource="co/elastic/logging/logback/boot/ecs-console-appender.xml"/>
<include resource="co/elastic/logging/logback/boot/ecs-file-appender.xml"/>
<root level="TRACE">
<appender-ref ref="CONSOLE"/>
<appender-ref ref="ECS_JSON_FILE"/>
<appender-ref ref="FILE"/>
</root>
</configuration>
@@ -1,35 +0,0 @@
<configuration>
<appender name="FILE" class="ch.qos.logback.core.rolling.RollingFileAppender">
<file>logFile.log</file>
<encoder>
<charset>UTF-8</charset>
<pattern>%d{YYYY-MM-dd HH:mm:ss.SSS} [%thread] %-5level [%X{x-request-id}] %logger{36} - %msg%n</pattern>
</encoder>
<rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy">
<!-- daily rollover -->
<fileNamePattern>logFile.%d{yyyy-MM-dd_HH}.log</fileNamePattern>
<!-- keep 30 days worth of history -->
<maxHistory>30</maxHistory>
</rollingPolicy>
</appender>
<appender name="STDOUT" class="ch.qos.logback.core.ConsoleAppender">
<encoder>
<pattern>%d{YYYY-MM-dd HH:mm:ss.SSS} [%thread] %-5level [%X{x-request-id},%X{x-job-id}] %logger{36} -
%msg%n
</pattern>
</encoder>
</appender>
<root level="DEBUG">
<appender-ref ref="STDOUT"/>
<appender-ref ref="FILE"/>
</root>
<logger name="org.eclipse.jetty" level="INFO"/>
<logger name="io.netty" level="INFO"/>
<logger name="kjob.core.internal.scheduler.JobServiceImpl" level="INFO"/>
<logger name="Exposed" level="DEBUG"/>
<logger name="io.ktor.server.plugins.contentnegotiation" level="INFO"/>
<logger name="org.springframework.web.filter.CommonsRequestLoggingFilter" level="DEBUG"/>
<logger name="org.mongodb.driver.protocol.command" level="INFO"/>
<logger name="dev.usbharu" level="TRACE"/>
</configuration>
@@ -44,10 +44,27 @@
<div class="post-controller" th:fragment="single-post-controller(post)">
<!--/*@thymesVar id="post" type="dev.usbharu.hideout.core.application.post.PostDetail"*/-->
<a th:href="${'/publish?reply_to=' + post.id}">Reply</a>
<a th:href="${post.apId}">
<time th:datetime="${post.createdAt}" th:text="${#temporals.format(post.createdAt, 'yyyy-MM-dd HH:mm')}"></time>
</a>
<th:block th:if="${post.favourited}">
<form method="post" th:action="@{/users/a/posts/{id}/unfavourite(id=${post.id})}">
<a th:href="${'/publish?reply_to=' + post.id}">Reply</a>
<input type="submit" value="[❤]">
<a th:href="${post.apId}">
<time th:datetime="${post.createdAt}"
th:text="${#temporals.format(post.createdAt, 'yyyy-MM-dd HH:mm')}"></time>
</a>
</form>
</th:block>
<th:block th:unless="${post.favourited}">
<form method="post" th:action="@{/users/a/posts/{id}/favourite(id=${post.id})}">
<a th:href="${'/publish?reply_to=' + post.id}">Reply</a>
<input type="submit" value="❤">
<a th:href="${post.apId}">
<time th:datetime="${post.createdAt}"
th:text="${#temporals.format(post.createdAt, 'yyyy-MM-dd HH:mm')}"></time>
</a>
</form>
</th:block>
</div>
@@ -19,6 +19,17 @@
<h2 th:text="${user.screenName}"></h2>
</th:block>
<p th:text="'@'+${user.name} + '@' + ${user.host}"></p>
<th:block th:if="${relationship != null}">
<form method="post" th:action="@{/users/{name}/unfollow(name=${user.name+'@'+user.host})}"
th:if="${relationship.following}">
<input type="submit" value="Unfollow">
</form>
<form method="post" th:action="@{/users/{name}/follow(name=${user.name+'@'+user.host})}"
th:unless="${relationship.following}">
<input type="submit" value="Follow">
</form>
</th:block>
</div>
<div>
<p th:text="${user.description}"></p>
@@ -1,160 +0,0 @@
package dev.usbharu.hideout.core.application.actor
import dev.usbharu.hideout.core.config.ApplicationConfig
import dev.usbharu.hideout.core.domain.model.actor.ActorId
import dev.usbharu.hideout.core.domain.model.actor.ActorRepository
import dev.usbharu.hideout.core.domain.model.actor.TestActorFactory
import dev.usbharu.hideout.core.domain.model.media.*
import dev.usbharu.hideout.core.domain.model.support.acct.Acct
import dev.usbharu.hideout.core.domain.model.support.principal.Anonymous
import kotlinx.coroutines.test.runTest
import org.junit.jupiter.api.Assertions.assertEquals
import org.junit.jupiter.api.Test
import org.junit.jupiter.api.assertThrows
import org.junit.jupiter.api.extension.ExtendWith
import org.mockito.InjectMocks
import org.mockito.Mock
import org.mockito.Spy
import org.mockito.junit.jupiter.MockitoExtension
import org.mockito.kotlin.*
import utils.TestTransaction
import java.net.URI
@ExtendWith(MockitoExtension::class)
class GetActorDetailApplicationServiceTest {
@InjectMocks
lateinit var service: GetActorDetailApplicationService
@Mock
lateinit var actorRepository: ActorRepository
@Mock
lateinit var mediaRepository: MediaRepository
@Spy
val applicationConfig = ApplicationConfig(URI.create("http://localhost:8081").toURL())
@Spy
val transaction = TestTransaction
@Test
fun idとname両方nullならエラー() = runTest {
assertThrows<IllegalArgumentException> {
service.execute(GetActorDetail(null, null), Anonymous)
}
}
@Test
fun idがnullじゃない場合idから取得() = runTest {
val actor = TestActorFactory.create(id = 1)
whenever(actorRepository.findById(ActorId(1))).doReturn(actor)
val actual = service.execute(GetActorDetail(null, 1), Anonymous)
assertEquals(actual, ActorDetail.of(actor, null, null))
}
@Test
fun idが存在しないとエラー() = runTest {
assertThrows<IllegalArgumentException> {
service.execute(GetActorDetail(null, 2), Anonymous)
}
}
@Test
fun idがnullでacctがnullじゃない場合acctから取得() = runTest {
val actor = TestActorFactory.create(actorName = "test", domain = "example.com")
whenever(actorRepository.findByNameAndDomain("test", "example.com")).thenReturn(actor)
val actual = service.execute(GetActorDetail(Acct("test", "example.com"), null), Anonymous)
assertEquals(actual, ActorDetail.of(actor, null, null))
}
@Test
fun acctのhostが空ならローカルのhostが使われる() = runTest {
val actor = TestActorFactory.create(actorName = "test", domain = "localhost")
whenever(actorRepository.findByNameAndDomain("test", "localhost")).thenReturn(actor)
val actual = service.execute(GetActorDetail(Acct("test", ""), null), Anonymous)
assertEquals(actual, ActorDetail.of(actor, null, null))
}
@Test
fun acctが存在しないとエラー() = runTest {
assertThrows<IllegalArgumentException> {
service.execute(GetActorDetail(Acct("test", "example.com"), null), Anonymous)
}
}
@Test
fun idとacctがnullじゃない場合idが優先される() = runTest {
val actor = TestActorFactory.create(id = 1)
whenever(actorRepository.findById(ActorId(1))).doReturn(actor)
val actual = service.execute(GetActorDetail(null, 1), Anonymous)
assertEquals(actual, ActorDetail.of(actor, null, null))
verify(actorRepository, never()).findByNameAndDomain(any(), any())
}
@Test
fun iconがnullじゃない時取得する() = runTest {
val actor = TestActorFactory.create(id = 1, icon = 1)
whenever(actorRepository.findById(ActorId(1))).doReturn(actor)
whenever(mediaRepository.findById(MediaId(1))).doReturn(
Media(
id = MediaId(1),
name = MediaName(""),
url = URI.create("http://example.com"),
remoteUrl = null,
thumbnailUrl = null,
type = FileType.Image,
mimeType = MimeType("image", "jpeg", FileType.Image),
blurHash = null,
description = null,
actorId = ActorId(1)
)
)
val actual = service.execute(GetActorDetail(null, 1), Anonymous)
assertEquals(actual, ActorDetail.of(actor, URI.create("http://example.com"), null))
}
@Test
fun bannerがnullじゃない時取得する() = runTest {
val actor = TestActorFactory.create(id = 1, banner = 1)
whenever(actorRepository.findById(ActorId(1))).doReturn(actor)
whenever(mediaRepository.findById(MediaId(1))).doReturn(
Media(
id = MediaId(1),
name = MediaName(""),
url = URI.create("http://example.com"),
remoteUrl = null,
thumbnailUrl = null,
type = FileType.Image,
mimeType = MimeType("image", "jpeg", FileType.Image),
blurHash = null,
description = null,
actorId = ActorId(1)
)
)
val actual = service.execute(GetActorDetail(null, 1), Anonymous)
assertEquals(actual, ActorDetail.of(actor, null, URI.create("http://example.com")))
}
@Test
fun iconとbannerが見つからなかった場合null() = runTest {
val actor = TestActorFactory.create(id = 1, icon = 1, banner = 1)
whenever(actorRepository.findById(ActorId(1))).doReturn(actor)
val actual = service.execute(GetActorDetail(null, 1), Anonymous)
assertEquals(actual, ActorDetail.of(actor, null, null))
}
}
@@ -1,8 +1,7 @@
package dev.usbharu.hideout.core.domain.model.actor
import dev.usbharu.hideout.core.domain.model.emoji.EmojiId
import dev.usbharu.hideout.core.domain.model.emoji.CustomEmojiId
import dev.usbharu.hideout.core.domain.model.instance.InstanceId
import dev.usbharu.hideout.core.domain.model.media.MediaId
import dev.usbharu.hideout.core.domain.model.support.domain.Domain
import dev.usbharu.hideout.core.infrastructure.other.TwitterSnowflakeIdGenerateService
import kotlinx.coroutines.runBlocking
@@ -36,10 +35,9 @@ object TestActorFactory {
suspend: Boolean = false,
alsoKnownAs: Set<ActorId> = emptySet(),
moveTo: Long? = null,
emojiIds: Set<EmojiId> = emptySet(),
emojiIds: Set<CustomEmojiId> = emptySet(),
deleted: Boolean = false,
icon: Long? = null,
banner: Long? = null
roles: Set<Role> = emptySet(),
): Actor {
return runBlocking {
Actor(
@@ -68,8 +66,8 @@ object TestActorFactory {
moveTo = moveTo?.let { ActorId(it) },
emojiIds = emojiIds,
deleted = deleted,
icon = icon?.let { MediaId(it) },
banner = banner?.let { MediaId(it) },
icon = null,
banner = null,
)
}
@@ -3,18 +3,18 @@ package dev.usbharu.hideout.core.domain.model.emoji
import org.junit.jupiter.api.Assertions.assertDoesNotThrow
import org.junit.jupiter.api.Test
class EmojiIdTest {
class CustomEmojiIdTest {
@Test
fun emojiIdは0以上である必要がある() {
org.junit.jupiter.api.assertThrows<IllegalArgumentException> {
EmojiId(-1)
CustomEmojiId(-1)
}
}
@Test
fun emojiIdは0以上なら設定できる() {
assertDoesNotThrow {
EmojiId(1)
CustomEmojiId(1)
}
}
}
@@ -4,7 +4,7 @@ import dev.usbharu.hideout.core.domain.event.post.PostEvent
import dev.usbharu.hideout.core.domain.model.actor.ActorId
import dev.usbharu.hideout.core.domain.model.actor.ActorPublicKey
import dev.usbharu.hideout.core.domain.model.actor.TestActorFactory
import dev.usbharu.hideout.core.domain.model.emoji.EmojiId
import dev.usbharu.hideout.core.domain.model.emoji.CustomEmojiId
import dev.usbharu.hideout.core.domain.model.media.MediaId
import org.junit.jupiter.api.Assertions.assertTrue
import org.junit.jupiter.api.Test
@@ -447,7 +447,7 @@ class PostTest {
@Test
fun `emojiIds hideがtrueの時empty`() {
val actor = TestActorFactory.create()
val emojiIds = listOf(EmojiId(1), EmojiId(2))
val emojiIds = listOf(CustomEmojiId(1), CustomEmojiId(2))
val post = Post.create(
id = PostId(1),
actorId = actor.id,
@@ -473,7 +473,7 @@ class PostTest {
@Test
fun `emojiIds hideがfalseの時中身が返される`() {
val actor = TestActorFactory.create()
val emojiIds = listOf(EmojiId(1), EmojiId(2))
val emojiIds = listOf(CustomEmojiId(1), CustomEmojiId(2))
val post = Post.create(
id = PostId(1),
actorId = actor.id,
@@ -500,7 +500,7 @@ class PostTest {
val post = TestPostFactory.create()
val mediaIds = listOf<MediaId>(MediaId(1))
val visibleActors = setOf<ActorId>((ActorId(2)))
val emojis = listOf<EmojiId>(EmojiId(3))
val emojis = listOf<CustomEmojiId>(CustomEmojiId(3))
val reconstructWith = post.reconstructWith(mediaIds, emojis, visibleActors)
assertEquals(mediaIds, reconstructWith.mediaIds)
@@ -511,7 +511,7 @@ class PostTest {
@Test
fun `mediaIds hideがtrueの時emptyが返される`() {
val actor = TestActorFactory.create()
val emojiIds = listOf(EmojiId(1), EmojiId(2))
val emojiIds = listOf(CustomEmojiId(1), CustomEmojiId(2))
val mediaIds = listOf(MediaId(1))
val post = Post.create(
id = PostId(1),
@@ -538,7 +538,7 @@ class PostTest {
@Test
fun `mediaIds hideがfalseの時中身が返される`() {
val actor = TestActorFactory.create()
val emojiIds = listOf(EmojiId(1), EmojiId(2))
val emojiIds = listOf(CustomEmojiId(1), CustomEmojiId(2))
val mediaIds = listOf(MediaId(2))
val post = Post.create(
id = PostId(1),
@@ -603,7 +603,7 @@ class PostTest {
fun `restore 指定された引数で再構成されCHECKUPDATEイベントが発生する`() {
val post = TestPostFactory.create(deleted = true)
val postContent = PostContent("aiueo", "aiueo", listOf(EmojiId(1)))
val postContent = PostContent("aiueo", "aiueo", listOf(CustomEmojiId(1)))
val overview = PostOverview("overview")
val mediaIds = listOf(MediaId(1))
post.restore(
@@ -622,7 +622,7 @@ class PostTest {
fun deletedがfalseの時失敗する() {
val post = TestPostFactory.create(deleted = false)
val postContent = PostContent("aiueo", "aiueo", listOf(EmojiId(1)))
val postContent = PostContent("aiueo", "aiueo", listOf(CustomEmojiId(1)))
val overview = PostOverview("overview")
val mediaIds = listOf(MediaId(1))
assertThrows<IllegalArgumentException> {
@@ -0,0 +1,16 @@
package dev.usbharu.hideout.core.infrastructure.emojikt
import org.junit.jupiter.api.Assertions.assertTrue
import org.junit.jupiter.params.ParameterizedTest
import org.junit.jupiter.params.provider.ValueSource
class EmojiKtUnicodeEmojiServiceTest {
@ParameterizedTest
@ValueSource(strings = ["", "👱", "☠️", "⁉️"])
fun 絵文字の判定ができる(s: String) {
assertTrue(EmojiKtUnicodeEmojiService().isUnicodeEmoji(s))
}
}
@@ -19,6 +19,7 @@ import dev.usbharu.hideout.core.domain.model.userdetails.UserDetailRepository
import dev.usbharu.hideout.core.domain.service.filter.FilterDomainService
import dev.usbharu.hideout.core.domain.service.post.IPostReadAccessControl
import dev.usbharu.hideout.core.infrastructure.other.TwitterSnowflakeIdGenerateService
import dev.usbharu.hideout.core.query.reactions.ReactionsQueryService
import kotlinx.coroutines.test.runTest
import org.assertj.core.api.Assertions.assertThat
import org.junit.jupiter.api.Test
@@ -64,6 +65,9 @@ class DefaultTimelineStoreTest {
@Mock
lateinit var iPostReadAccessControl: IPostReadAccessControl
@Mock
lateinit var reactionsQueryService: ReactionsQueryService
@Spy
val defaultTimelineStoreConfig = DefaultTimelineStoreConfig(500)
-7
View File
@@ -41,19 +41,12 @@ repositories {
}
}
configurations {
all {
exclude("org.springframework.boot", "spring-boot-starter-logging")
exclude("ch.qos.logback", "logback-classic")
}
}
dependencies {
detektPlugins(libs.detekt.formatting)
implementation("org.springframework.boot:spring-boot-starter-web")
implementation("org.springframework.boot:spring-boot-starter-security")
implementation("com.fasterxml.jackson.datatype:jackson-datatype-jsr310")
implementation("org.springframework.boot:spring-boot-starter-log4j2")
implementation("dev.usbharu:hideout-core:0.0.1")
@@ -18,6 +18,10 @@ package dev.usbharu.hideout.mastodon.interfaces.api
import dev.usbharu.hideout.core.application.post.RegisterLocalPost
import dev.usbharu.hideout.core.application.post.RegisterLocalPostApplicationService
import dev.usbharu.hideout.core.application.reaction.CreateReaction
import dev.usbharu.hideout.core.application.reaction.RemoveReaction
import dev.usbharu.hideout.core.application.reaction.UserCreateReactionApplicationService
import dev.usbharu.hideout.core.application.reaction.UserRemoveReactionApplicationService
import dev.usbharu.hideout.core.domain.model.post.Visibility
import dev.usbharu.hideout.core.infrastructure.springframework.oauth2.SpringSecurityOauth2PrincipalContextHolder
import dev.usbharu.hideout.mastodon.application.status.GetStatus
@@ -32,7 +36,9 @@ import org.springframework.stereotype.Controller
class SpringStatusApi(
private val registerLocalPostApplicationService: RegisterLocalPostApplicationService,
private val getStatusApplicationService: GetStatusApplicationService,
private val principalContextHolder: SpringSecurityOauth2PrincipalContextHolder
private val principalContextHolder: SpringSecurityOauth2PrincipalContextHolder,
private val userCreateReactionApplicationService: UserCreateReactionApplicationService,
private val userRemoveReactionApplicationService: UserRemoveReactionApplicationService
) : StatusApi {
override suspend fun apiV1StatusesIdEmojiReactionsEmojiDelete(id: String, emoji: String): ResponseEntity<Status> =
super.apiV1StatusesIdEmojiReactionsEmojiDelete(id, emoji)
@@ -49,6 +55,20 @@ class SpringStatusApi(
)
}
override suspend fun apiV1StatusesIdFavouritePost(id: String): ResponseEntity<Status> {
val principal = principalContextHolder.getPrincipal()
userCreateReactionApplicationService.execute(CreateReaction(postId = id.toLong(), null, ""), principal)
return ResponseEntity.ok(getStatusApplicationService.execute(GetStatus(id), principal))
}
override suspend fun apiV1StatusesIdUnfavouritePost(id: String): ResponseEntity<Status> {
val principal = principalContextHolder.getPrincipal()
userRemoveReactionApplicationService.execute(RemoveReaction(postId = id.toLong(), null, ""), principal)
return ResponseEntity.ok(getStatusApplicationService.execute(GetStatus(id), principal))
}
override suspend fun apiV1StatusesPost(statusesRequest: StatusesRequest): ResponseEntity<Status> {
val principal = principalContextHolder.getPrincipal()
val execute = registerLocalPostApplicationService.execute(
@@ -177,6 +177,48 @@ paths:
schema:
$ref: "#/components/schemas/Status"
/api/v1/statuses/{id}/favourite:
post:
tags:
- status
security:
- OAuth2:
- "write:favourites"
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}/unfavourite:
post:
tags:
- status
security:
- OAuth2:
- "write:favourites"
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:
+6 -4
View File
@@ -4,7 +4,7 @@ kotlin = "2.0.20"
ktor = "2.3.12"
exposed = "0.54.0"
javacv-ffmpeg = "6.1.1-1.5.10"
detekt = "1.23.6"
detekt = "1.23.7"
coroutines = "1.8.1"
swagger = "2.2.23"
tika = "2.9.2"
@@ -65,7 +65,7 @@ jackson-module-kotlin = { module = "com.fasterxml.jackson.module:jackson-module-
blurhash = { module = "io.trbl:blurhash", version = "1.0.0" }
aws-s3 = { module = "software.amazon.awssdk:s3", version = "2.27.20" }
aws-s3 = { module = "software.amazon.awssdk:s3", version = "2.27.21" }
jsoup = { module = "org.jsoup:jsoup", version = "1.18.1" }
@@ -96,14 +96,16 @@ protoc = { module = "com.google.protobuf:protoc", version.ref = "protobuf" }
koin-bom = { module = "io.insert-koin:koin-bom", version = "3.5.6" }
koin-core = { module = "io.insert-koin:koin-core" }
log4j2-slf4j = { module = "org.apache.logging.log4j:log4j-slf4j2-impl", version = "2.23.1" }
log4j2-slf4j = { module = "org.apache.logging.log4j:log4j-slf4j2-impl", version = "2.24.0" }
mongodb-kotlin-coroutine = { module = "org.mongodb:mongodb-driver-kotlin-coroutine", version = "5.1.4" }
mockito-kotlin = { module = "org.mockito.kotlin:mockito-kotlin", version = "5.4.0" }
http-signature = { module = "dev.usbharu:http-signature", version = "1.0.0" }
emoji-kt = { module = "dev.usbharu:emoji-kt", version = "2.0.0" }
emoji-kt = { module = "dev.usbharu:emoji-kt", version = "2.0.1" }
logback-ecs-encoder = { module = "co.elastic.logging:logback-ecs-encoder", version = "1.6.0" }
[bundles]