From 86daf1041babf492133d7149bbb0a1fc4c3134ad Mon Sep 17 00:00:00 2001 From: usbharu <64310155+usbharu@users.noreply.github.com> Date: Fri, 7 Jun 2024 20:44:12 +0900 Subject: [PATCH] =?UTF-8?q?feat:=20=E6=8A=95=E7=A8=BF=E3=82=92=E5=8F=96?= =?UTF-8?q?=E5=BE=97=E3=81=A7=E3=81=8D=E3=82=8B=E3=82=88=E3=81=86=E3=81=AB?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../hideout/core/application/post/GetPost.kt | 21 ++ .../post/GetPostApplicationService.kt | 40 +++ .../hideout/core/application/post/Post.kt | 58 ++++ .../RegisterLocalPostApplicationService.kt | 7 +- .../DelegateCommandExecutorFactory.kt | 36 +++ .../resources/db/migration/V1__Init_DB.sql | 2 +- hideout-mastodon/build.gradle.kts | 1 + .../mastodon/application/status/GetStatus.kt | 21 ++ .../status/GetStatusApplicationService.kt | 42 +++ .../ExposedAccountQueryService.kt | 73 +++++ .../exposedquery/ExposedStatusQueryService.kt | 291 ++++++++++++++++++ .../interfaces/api/SpringStatusApi.kt | 27 +- .../mastodon/query/AccountQueryService.kt | 24 ++ .../mastodon/query/StatusQueryService.kt | 45 +++ .../src/main/resources/openapi/mastodon.yaml | 2 - 15 files changed, 678 insertions(+), 12 deletions(-) create mode 100644 hideout-core/src/main/kotlin/dev/usbharu/hideout/core/application/post/GetPost.kt create mode 100644 hideout-core/src/main/kotlin/dev/usbharu/hideout/core/application/post/GetPostApplicationService.kt create mode 100644 hideout-core/src/main/kotlin/dev/usbharu/hideout/core/application/post/Post.kt create mode 100644 hideout-core/src/main/kotlin/dev/usbharu/hideout/core/infrastructure/springframework/DelegateCommandExecutorFactory.kt create mode 100644 hideout-mastodon/src/main/kotlin/dev/usbharu/hideout/mastodon/application/status/GetStatus.kt create mode 100644 hideout-mastodon/src/main/kotlin/dev/usbharu/hideout/mastodon/application/status/GetStatusApplicationService.kt create mode 100644 hideout-mastodon/src/main/kotlin/dev/usbharu/hideout/mastodon/infrastructure/exposedquery/ExposedAccountQueryService.kt create mode 100644 hideout-mastodon/src/main/kotlin/dev/usbharu/hideout/mastodon/infrastructure/exposedquery/ExposedStatusQueryService.kt create mode 100644 hideout-mastodon/src/main/kotlin/dev/usbharu/hideout/mastodon/query/AccountQueryService.kt create mode 100644 hideout-mastodon/src/main/kotlin/dev/usbharu/hideout/mastodon/query/StatusQueryService.kt diff --git a/hideout-core/src/main/kotlin/dev/usbharu/hideout/core/application/post/GetPost.kt b/hideout-core/src/main/kotlin/dev/usbharu/hideout/core/application/post/GetPost.kt new file mode 100644 index 00000000..90ef8560 --- /dev/null +++ b/hideout-core/src/main/kotlin/dev/usbharu/hideout/core/application/post/GetPost.kt @@ -0,0 +1,21 @@ +/* + * 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.post + +data class GetPost( + val postId: Long, +) diff --git a/hideout-core/src/main/kotlin/dev/usbharu/hideout/core/application/post/GetPostApplicationService.kt b/hideout-core/src/main/kotlin/dev/usbharu/hideout/core/application/post/GetPostApplicationService.kt new file mode 100644 index 00000000..6c8bf98b --- /dev/null +++ b/hideout-core/src/main/kotlin/dev/usbharu/hideout/core/application/post/GetPostApplicationService.kt @@ -0,0 +1,40 @@ +/* + * 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.post + +import dev.usbharu.hideout.core.application.shared.AbstractApplicationService +import dev.usbharu.hideout.core.application.shared.CommandExecutor +import dev.usbharu.hideout.core.application.shared.Transaction +import dev.usbharu.hideout.core.domain.model.post.PostId +import dev.usbharu.hideout.core.domain.model.post.PostRepository +import org.slf4j.LoggerFactory +import org.springframework.stereotype.Service + +@Service +class GetPostApplicationService(private val postRepository: PostRepository, transaction: Transaction) : + AbstractApplicationService(transaction, logger) { + + override suspend fun internalExecute(command: GetPost, executor: CommandExecutor): Post { + val post = postRepository.findById(PostId(command.postId)) ?: throw Exception("Post not found") + + return Post.of(post) + } + + companion object { + private val logger = LoggerFactory.getLogger(GetPostApplicationService::class.java) + } +} \ No newline at end of file diff --git a/hideout-core/src/main/kotlin/dev/usbharu/hideout/core/application/post/Post.kt b/hideout-core/src/main/kotlin/dev/usbharu/hideout/core/application/post/Post.kt new file mode 100644 index 00000000..538b8911 --- /dev/null +++ b/hideout-core/src/main/kotlin/dev/usbharu/hideout/core/application/post/Post.kt @@ -0,0 +1,58 @@ +/* + * 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.post + +import dev.usbharu.hideout.core.domain.model.post.Post +import dev.usbharu.hideout.core.domain.model.post.Visibility +import java.net.URI +import java.time.Instant + +data class Post( + val id: Long, + val actorId: Long, + val overview: String?, + val text: String, + val content: String, + val createdAt: Instant, + val visibility: Visibility, + val url: URI, + val repostId: Long?, + val replyId: Long?, + val sensitive: Boolean, + val mediaIds: List, + val moveTo: Long?, +) { + companion object { + fun of(post: Post): dev.usbharu.hideout.core.application.post.Post { + return Post( + post.id.id, + post.actorId.id, + post.overview?.overview, + post.text, + post.content.content, + post.createdAt, + post.visibility, + post.url, + post.repostId?.id, + post.replyId?.id, + post.sensitive, + post.mediaIds.map { it.id }, + post.moveTo?.id + ) + } + } +} diff --git a/hideout-core/src/main/kotlin/dev/usbharu/hideout/core/application/post/RegisterLocalPostApplicationService.kt b/hideout-core/src/main/kotlin/dev/usbharu/hideout/core/application/post/RegisterLocalPostApplicationService.kt index 8b81c8de..bb67bad9 100644 --- a/hideout-core/src/main/kotlin/dev/usbharu/hideout/core/application/post/RegisterLocalPostApplicationService.kt +++ b/hideout-core/src/main/kotlin/dev/usbharu/hideout/core/application/post/RegisterLocalPostApplicationService.kt @@ -19,7 +19,6 @@ package dev.usbharu.hideout.core.application.post import dev.usbharu.hideout.core.application.shared.AbstractApplicationService import dev.usbharu.hideout.core.application.shared.CommandExecutor 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.media.MediaId import dev.usbharu.hideout.core.domain.model.post.PostId @@ -38,13 +37,13 @@ class RegisterLocalPostApplicationService( private val postRepository: PostRepository, private val userDetailRepository: UserDetailRepository, transaction: Transaction, -) : AbstractApplicationService(transaction, Companion.logger) { +) : AbstractApplicationService(transaction, Companion.logger) { companion object { val logger: Logger = LoggerFactory.getLogger(RegisterLocalPostApplicationService::class.java) } - override suspend fun internalExecute(command: RegisterLocalPost, executor: CommandExecutor) { + override suspend fun internalExecute(command: RegisterLocalPost, executor: CommandExecutor): Long { val actorId = (userDetailRepository.findById(command.userDetailId) ?: throw IllegalStateException("actor not found")).actorId @@ -59,5 +58,7 @@ class RegisterLocalPostApplicationService( command.mediaIds.map { MediaId(it) }) postRepository.save(post) + + return post.id.id } } diff --git a/hideout-core/src/main/kotlin/dev/usbharu/hideout/core/infrastructure/springframework/DelegateCommandExecutorFactory.kt b/hideout-core/src/main/kotlin/dev/usbharu/hideout/core/infrastructure/springframework/DelegateCommandExecutorFactory.kt new file mode 100644 index 00000000..6312b05b --- /dev/null +++ b/hideout-core/src/main/kotlin/dev/usbharu/hideout/core/infrastructure/springframework/DelegateCommandExecutorFactory.kt @@ -0,0 +1,36 @@ +/* + * 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.infrastructure.springframework + +import dev.usbharu.hideout.core.application.shared.CommandExecutor +import dev.usbharu.hideout.core.infrastructure.springframework.oauth2.Oauth2CommandExecutorFactory +import org.springframework.security.core.context.SecurityContextHolder +import org.springframework.security.oauth2.jwt.Jwt +import org.springframework.stereotype.Component + +@Component +class DelegateCommandExecutorFactory( + private val oauth2CommandExecutorFactory: Oauth2CommandExecutorFactory, + private val mvcCommandExecutorFactory: SpringMvcCommandExecutorFactory, +) { + fun getCommandExecutor(): CommandExecutor { + if (SecurityContextHolder.getContext().authentication.principal is Jwt) { + return oauth2CommandExecutorFactory.getCommandExecutor() + } + return mvcCommandExecutorFactory.getCommandExecutor() + } +} \ No newline at end of file diff --git a/hideout-core/src/main/resources/db/migration/V1__Init_DB.sql b/hideout-core/src/main/resources/db/migration/V1__Init_DB.sql index c8dd74bd..dc757618 100644 --- a/hideout-core/src/main/resources/db/migration/V1__Init_DB.sql +++ b/hideout-core/src/main/resources/db/migration/V1__Init_DB.sql @@ -86,7 +86,7 @@ create table if not exists media url varchar(255) not null unique, remote_url varchar(255) null unique, thumbnail_url varchar(255) null unique, - "type" int not null, + "type" varchar(100) not null, blurhash varchar(255) null, mime_type varchar(255) not null, description varchar(4000) null diff --git a/hideout-mastodon/build.gradle.kts b/hideout-mastodon/build.gradle.kts index 2907614d..54fea879 100644 --- a/hideout-mastodon/build.gradle.kts +++ b/hideout-mastodon/build.gradle.kts @@ -31,6 +31,7 @@ dependencies { implementation(libs.jakarta.annotation) implementation(libs.jakarta.validation) + implementation(libs.bundles.exposed) implementation(libs.bundles.openapi) implementation(libs.bundles.coroutines) } diff --git a/hideout-mastodon/src/main/kotlin/dev/usbharu/hideout/mastodon/application/status/GetStatus.kt b/hideout-mastodon/src/main/kotlin/dev/usbharu/hideout/mastodon/application/status/GetStatus.kt new file mode 100644 index 00000000..37b6882e --- /dev/null +++ b/hideout-mastodon/src/main/kotlin/dev/usbharu/hideout/mastodon/application/status/GetStatus.kt @@ -0,0 +1,21 @@ +/* + * 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.mastodon.application.status + +data class GetStatus( + val id: String, +) diff --git a/hideout-mastodon/src/main/kotlin/dev/usbharu/hideout/mastodon/application/status/GetStatusApplicationService.kt b/hideout-mastodon/src/main/kotlin/dev/usbharu/hideout/mastodon/application/status/GetStatusApplicationService.kt new file mode 100644 index 00000000..545bca34 --- /dev/null +++ b/hideout-mastodon/src/main/kotlin/dev/usbharu/hideout/mastodon/application/status/GetStatusApplicationService.kt @@ -0,0 +1,42 @@ +/* + * 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.mastodon.application.status + +import dev.usbharu.hideout.core.application.shared.AbstractApplicationService +import dev.usbharu.hideout.core.application.shared.CommandExecutor +import dev.usbharu.hideout.core.application.shared.Transaction +import dev.usbharu.hideout.mastodon.interfaces.api.generated.model.Status +import dev.usbharu.hideout.mastodon.query.StatusQueryService +import org.slf4j.LoggerFactory +import org.springframework.stereotype.Service + +@Service +class GetStatusApplicationService( + private val statusQueryService: StatusQueryService, + transaction: Transaction, +) : AbstractApplicationService( + transaction, + logger +) { + companion object { + val logger = LoggerFactory.getLogger(GetStatusApplicationService::class.java)!! + } + + override suspend fun internalExecute(command: GetStatus, executor: CommandExecutor): Status { + return statusQueryService.findByPostId(command.id.toLong()) ?: throw Exception("Not fount") + } +} \ No newline at end of file diff --git a/hideout-mastodon/src/main/kotlin/dev/usbharu/hideout/mastodon/infrastructure/exposedquery/ExposedAccountQueryService.kt b/hideout-mastodon/src/main/kotlin/dev/usbharu/hideout/mastodon/infrastructure/exposedquery/ExposedAccountQueryService.kt new file mode 100644 index 00000000..41ca4ab2 --- /dev/null +++ b/hideout-mastodon/src/main/kotlin/dev/usbharu/hideout/mastodon/infrastructure/exposedquery/ExposedAccountQueryService.kt @@ -0,0 +1,73 @@ +/* + * 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.mastodon.infrastructure.exposedquery + +import dev.usbharu.hideout.core.config.ApplicationConfig +import dev.usbharu.hideout.core.infrastructure.exposedrepository.Actors +import dev.usbharu.hideout.mastodon.interfaces.api.generated.model.Account +import dev.usbharu.hideout.mastodon.query.AccountQueryService +import org.jetbrains.exposed.sql.ResultRow +import org.jetbrains.exposed.sql.selectAll +import org.springframework.stereotype.Repository + +@Repository +class AccountQueryServiceImpl(private val applicationConfig: ApplicationConfig) : AccountQueryService { + override suspend fun findById(accountId: Long): Account? { + val query = Actors.selectAll().where { Actors.id eq accountId } + + return query + .singleOrNull() + ?.let { toAccount(it) } + } + + override suspend fun findByIds(accountIds: List): List { + val query = Actors.selectAll().where { Actors.id inList accountIds } + + return query + .map { toAccount(it) } + } + + private fun toAccount( + resultRow: ResultRow, + ): Account { + val userUrl = "${applicationConfig.url}/users/${resultRow[Actors.id]}" + + return Account( + id = resultRow[Actors.id].toString(), + username = resultRow[Actors.name], + acct = "${resultRow[Actors.name]}@${resultRow[Actors.domain]}", + url = resultRow[Actors.url], + displayName = resultRow[Actors.screenName], + note = resultRow[Actors.description], + avatar = userUrl + "/icon.jpg", + avatarStatic = userUrl + "/icon.jpg", + header = userUrl + "/header.jpg", + headerStatic = userUrl + "/header.jpg", + locked = resultRow[Actors.locked], + fields = emptyList(), + emojis = emptyList(), + bot = false, + group = false, + discoverable = true, + createdAt = resultRow[Actors.createdAt].toString(), + lastStatusAt = resultRow[Actors.lastPostAt]?.toString(), + statusesCount = resultRow[Actors.postsCount], + followersCount = resultRow[Actors.followersCount], + followingCount = resultRow[Actors.followingCount], + ) + } +} \ No newline at end of file diff --git a/hideout-mastodon/src/main/kotlin/dev/usbharu/hideout/mastodon/infrastructure/exposedquery/ExposedStatusQueryService.kt b/hideout-mastodon/src/main/kotlin/dev/usbharu/hideout/mastodon/infrastructure/exposedquery/ExposedStatusQueryService.kt new file mode 100644 index 00000000..bfeca74c --- /dev/null +++ b/hideout-mastodon/src/main/kotlin/dev/usbharu/hideout/mastodon/infrastructure/exposedquery/ExposedStatusQueryService.kt @@ -0,0 +1,291 @@ +/* + * 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.mastodon.infrastructure.exposedquery + +import dev.usbharu.hideout.core.domain.model.emoji.CustomEmoji +import dev.usbharu.hideout.core.domain.model.media.* +import dev.usbharu.hideout.core.domain.model.post.Visibility +import dev.usbharu.hideout.core.infrastructure.exposedrepository.* +import dev.usbharu.hideout.mastodon.interfaces.api.generated.model.Account +import dev.usbharu.hideout.mastodon.interfaces.api.generated.model.MediaAttachment +import dev.usbharu.hideout.mastodon.interfaces.api.generated.model.Status +import dev.usbharu.hideout.mastodon.interfaces.api.generated.model.Status.Visibility.* +import dev.usbharu.hideout.mastodon.query.StatusQuery +import dev.usbharu.hideout.mastodon.query.StatusQueryService +import org.jetbrains.exposed.sql.ResultRow +import org.jetbrains.exposed.sql.andWhere +import org.jetbrains.exposed.sql.selectAll +import org.springframework.stereotype.Repository +import java.net.URI +import dev.usbharu.hideout.core.domain.model.media.Media as EntityMedia +import dev.usbharu.hideout.mastodon.interfaces.api.generated.model.CustomEmoji as MastodonEmoji + +@Suppress("IncompleteDestructuring") +@Repository +class StatusQueryServiceImpl : StatusQueryService { + override suspend fun findByPostIds(ids: List): List = findByPostIdsWithMedia(ids) + + override suspend fun findByPostIdsWithMediaIds(statusQueries: List): List { + val postIdSet = mutableSetOf() + postIdSet.addAll(statusQueries.flatMap { listOfNotNull(it.postId, it.replyId, it.repostId) }) + val mediaIdSet = mutableSetOf() + mediaIdSet.addAll(statusQueries.flatMap { it.mediaIds }) + + val emojiIdSet = mutableSetOf() + emojiIdSet.addAll(statusQueries.flatMap { it.emojiIds }) + + val postMap = Posts + .leftJoin(Actors) + .selectAll().where { Posts.id inList postIdSet } + .associate { it[Posts.id] to toStatus(it) } + val mediaMap = Media.selectAll().where { Media.id inList mediaIdSet } + .associate { + it[Media.id] to it.toMedia().toMediaAttachments() + } + + val emojiMap = CustomEmojis.selectAll().where { CustomEmojis.id inList emojiIdSet }.associate { + it[CustomEmojis.id] to it.toCustomEmoji().toMastodonEmoji() + } + return statusQueries.mapNotNull { statusQuery -> + postMap[statusQuery.postId]?.copy( + inReplyToId = statusQuery.replyId?.toString(), + inReplyToAccountId = postMap[statusQuery.replyId]?.account?.id, + reblog = postMap[statusQuery.repostId], + mediaAttachments = statusQuery.mediaIds.mapNotNull { mediaMap[it] }, + emojis = statusQuery.emojiIds.mapNotNull { emojiMap[it] } + ) + } + } + + override suspend fun accountsStatus( + accountId: Long, + onlyMedia: Boolean, + excludeReplies: Boolean, + excludeReblogs: Boolean, + pinned: Boolean, + tagged: String?, + includeFollowers: Boolean, + ): List { + val query = Posts + .leftJoin(PostsMedia) + .leftJoin(Actors) + .leftJoin(Media) + .selectAll().where { Posts.actorId eq accountId } + + if (onlyMedia) { + query.andWhere { PostsMedia.mediaId.isNotNull() } + } + if (excludeReplies) { + query.andWhere { Posts.replyId.isNotNull() } + } + if (excludeReblogs) { + query.andWhere { Posts.repostId.isNotNull() } + } + if (includeFollowers) { + query.andWhere { Posts.visibility inList listOf(public.name, unlisted.name, private.name) } + } else { + query.andWhere { Posts.visibility inList listOf(public.name, unlisted.name) } + } + + val pairs = query + .groupBy { it[Posts.id] } + .map { it.value } + .map { + toStatus(it.first()).copy( + mediaAttachments = it.mapNotNull { resultRow -> + resultRow.toMediaOrNull()?.toMediaAttachments() + } + ) to it.first()[Posts.repostId] + } + + val statuses = resolveReplyAndRepost(pairs) + return statuses + } + + override suspend fun findByPostId(id: Long): Status? { + val map = Posts + .leftJoin(PostsMedia) + .leftJoin(Actors) + .leftJoin(Media) + .selectAll() + .where { Posts.id eq id } + .groupBy { it[Posts.id] } + .map { it.value } + .map { + toStatus(it.first()).copy( + mediaAttachments = it.mapNotNull { resultRow -> + resultRow.toMediaOrNull()?.toMediaAttachments() + }, + emojis = it.mapNotNull { resultRow -> resultRow.toCustomEmojiOrNull()?.toMastodonEmoji() } + ) to it.first()[Posts.repostId] + } + return resolveReplyAndRepost(map).singleOrNull() + } + + private fun resolveReplyAndRepost(pairs: List>): List { + val statuses = pairs.map { it.first } + return pairs + .map { + if (it.second != null) { + it.first.copy(reblog = statuses.find { (id) -> id == it.second.toString() }) + } else { + it.first + } + } + .map { + if (it.inReplyToId != null) { + println("statuses trace: $statuses") + println("inReplyToId trace: ${it.inReplyToId}") + it.copy(inReplyToAccountId = statuses.find { (id) -> id == it.inReplyToId }?.account?.id) + } else { + it + } + } + } + + private suspend fun findByPostIdsWithMedia(ids: List): List { + val pairs = Posts + .leftJoin(PostsMedia) + .leftJoin(PostsEmojis) + .leftJoin(CustomEmojis) + .leftJoin(Actors) + .leftJoin(Media) + .selectAll().where { Posts.id inList ids } + .groupBy { it[Posts.id] } + .map { it.value } + .map { + toStatus(it.first()).copy( + mediaAttachments = it.mapNotNull { resultRow -> + resultRow.toMediaOrNull()?.toMediaAttachments() + }, + emojis = it.mapNotNull { resultRow -> resultRow.toCustomEmojiOrNull()?.toMastodonEmoji() } + ) to it.first()[Posts.repostId] + } + return resolveReplyAndRepost(pairs) + } +} + +private fun CustomEmoji.toMastodonEmoji(): MastodonEmoji = MastodonEmoji( + shortcode = this.name, + url = this.url.toString(), + staticUrl = this.url.toString(), + visibleInPicker = true, + category = this.category.orEmpty() +) + +private fun toStatus(it: ResultRow) = Status( + id = it[Posts.id].toString(), + uri = it[Posts.apId], + createdAt = it[Posts.createdAt].toString(), + account = Account( + id = it[Actors.id].toString(), + username = it[Actors.name], + acct = "${it[Actors.name]}@${it[Actors.domain]}", + url = it[Actors.url], + displayName = it[Actors.screenName], + note = it[Actors.description], + avatar = it[Actors.url] + "/icon.jpg", + avatarStatic = it[Actors.url] + "/icon.jpg", + header = it[Actors.url] + "/header.jpg", + headerStatic = it[Actors.url] + "/header.jpg", + locked = it[Actors.locked], + fields = emptyList(), + emojis = emptyList(), + bot = false, + group = false, + discoverable = true, + createdAt = it[Actors.createdAt].toString(), + lastStatusAt = it[Actors.lastPostAt]?.toString(), + statusesCount = it[Actors.postsCount], + followersCount = it[Actors.followersCount], + followingCount = it[Actors.followingCount], + noindex = false, + moved = false, + suspendex = false, + limited = false + ), + content = it[Posts.text], + visibility = when (Visibility.valueOf(it[Posts.visibility])) { + Visibility.PUBLIC -> public + Visibility.UNLISTED -> unlisted + Visibility.FOLLOWERS -> private + Visibility.DIRECT -> direct + }, + sensitive = it[Posts.sensitive], + spoilerText = it[Posts.overview].orEmpty(), + mediaAttachments = emptyList(), + mentions = emptyList(), + tags = emptyList(), + emojis = emptyList(), + reblogsCount = 0, + favouritesCount = 0, + repliesCount = 0, + url = it[Posts.apId], + inReplyToId = it[Posts.replyId]?.toString(), + inReplyToAccountId = null, + language = null, + text = it[Posts.text], + editedAt = null +) + +fun ResultRow.toMedia(): EntityMedia { + val fileType = FileType.valueOf(this[Media.type]) + val mimeType = this[Media.mimeType] + return EntityMedia( + id = MediaId(this[Media.id]), + name = MediaName(this[Media.name]), + url = URI.create(this[Media.url]), + remoteUrl = this[Media.remoteUrl]?.let { URI.create(it) }, + thumbnailUrl = this[Media.thumbnailUrl]?.let { URI.create(it) }, + type = fileType, + blurHash = this[Media.blurhash]?.let { MediaBlurHash(it) }, + mimeType = MimeType(mimeType.substringBefore("/"), mimeType.substringAfter("/"), fileType), + description = this[Media.description]?.let { MediaDescription(it) } + ) +} + +fun ResultRow.toMediaOrNull(): EntityMedia? { + val fileType = FileType.valueOf(this.getOrNull(Media.type) ?: return null) + val mimeType = this.getOrNull(Media.mimeType) ?: return null + return EntityMedia( + id = MediaId(this.getOrNull(Media.id) ?: return null), + name = MediaName(this.getOrNull(Media.name) ?: return null), + url = URI.create(this.getOrNull(Media.url) ?: return null), + remoteUrl = this[Media.remoteUrl]?.let { URI.create(it) }, + thumbnailUrl = this[Media.thumbnailUrl]?.let { URI.create(it) }, + type = FileType.valueOf(this[Media.type]), + blurHash = this[Media.blurhash]?.let { MediaBlurHash(it) }, + mimeType = MimeType(mimeType.substringBefore("/"), mimeType.substringAfter("/"), fileType), + description = MediaDescription(this[Media.description] ?: return null) + ) +} + +fun EntityMedia.toMediaAttachments(): MediaAttachment = MediaAttachment( + id = id.toString(), + type = when (type) { + FileType.Image -> MediaAttachment.Type.image + FileType.Video -> MediaAttachment.Type.video + FileType.Audio -> MediaAttachment.Type.audio + FileType.Unknown -> MediaAttachment.Type.unknown + }, + url = url.toString(), + previewUrl = thumbnailUrl?.toString(), + remoteUrl = remoteUrl?.toString(), + description = description?.description, + blurhash = blurHash?.hash, + textUrl = url.toString() +) \ No newline at end of file diff --git a/hideout-mastodon/src/main/kotlin/dev/usbharu/hideout/mastodon/interfaces/api/SpringStatusApi.kt b/hideout-mastodon/src/main/kotlin/dev/usbharu/hideout/mastodon/interfaces/api/SpringStatusApi.kt index e1885640..322d3f68 100644 --- a/hideout-mastodon/src/main/kotlin/dev/usbharu/hideout/mastodon/interfaces/api/SpringStatusApi.kt +++ b/hideout-mastodon/src/main/kotlin/dev/usbharu/hideout/mastodon/interfaces/api/SpringStatusApi.kt @@ -19,7 +19,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.domain.model.post.Visibility -import dev.usbharu.hideout.core.infrastructure.springframework.oauth2.Oauth2CommandExecutorFactory +import dev.usbharu.hideout.core.infrastructure.springframework.DelegateCommandExecutorFactory +import dev.usbharu.hideout.core.infrastructure.springframework.oauth2.Oauth2CommandExecutor +import dev.usbharu.hideout.mastodon.application.status.GetStatus +import dev.usbharu.hideout.mastodon.application.status.GetStatusApplicationService import dev.usbharu.hideout.mastodon.interfaces.api.generated.StatusApi import dev.usbharu.hideout.mastodon.interfaces.api.generated.model.Status import dev.usbharu.hideout.mastodon.interfaces.api.generated.model.StatusesRequest @@ -29,8 +32,9 @@ import org.springframework.stereotype.Controller @Controller class SpringStatusApi( - private val oauth2CommandExecutorFactory: Oauth2CommandExecutorFactory, + private val delegateCommandExecutorFactory: DelegateCommandExecutorFactory, private val registerLocalPostApplicationService: RegisterLocalPostApplicationService, + private val getStatusApplicationService: GetStatusApplicationService, ) : StatusApi { override suspend fun apiV1StatusesIdEmojiReactionsEmojiDelete(id: String, emoji: String): ResponseEntity { return super.apiV1StatusesIdEmojiReactionsEmojiDelete(id, emoji) @@ -41,12 +45,18 @@ class SpringStatusApi( } override suspend fun apiV1StatusesIdGet(id: String): ResponseEntity { - return super.apiV1StatusesIdGet(id) + + return ResponseEntity.ok( + getStatusApplicationService.execute( + GetStatus(id), + delegateCommandExecutorFactory.getCommandExecutor() + ) + ) } override suspend fun apiV1StatusesPost(statusesRequest: StatusesRequest): ResponseEntity { - val executor = oauth2CommandExecutorFactory.getCommandExecutor() - registerLocalPostApplicationService.execute( + val executor = delegateCommandExecutorFactory.getCommandExecutor() as Oauth2CommandExecutor + val execute = registerLocalPostApplicationService.execute( RegisterLocalPost( userDetailId = executor.userDetailId, content = statusesRequest.status.orEmpty(), @@ -65,6 +75,11 @@ class SpringStatusApi( ), executor ) - return ResponseEntity.ok().build() + + + val status = getStatusApplicationService.execute(GetStatus(execute.toString()), executor) + return ResponseEntity.ok( + status + ) } } \ No newline at end of file diff --git a/hideout-mastodon/src/main/kotlin/dev/usbharu/hideout/mastodon/query/AccountQueryService.kt b/hideout-mastodon/src/main/kotlin/dev/usbharu/hideout/mastodon/query/AccountQueryService.kt new file mode 100644 index 00000000..61de616c --- /dev/null +++ b/hideout-mastodon/src/main/kotlin/dev/usbharu/hideout/mastodon/query/AccountQueryService.kt @@ -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.mastodon.query + +import dev.usbharu.hideout.mastodon.interfaces.api.generated.model.Account + +interface AccountQueryService { + suspend fun findById(accountId: Long): Account? + suspend fun findByIds(accountIds: List): List +} \ No newline at end of file diff --git a/hideout-mastodon/src/main/kotlin/dev/usbharu/hideout/mastodon/query/StatusQueryService.kt b/hideout-mastodon/src/main/kotlin/dev/usbharu/hideout/mastodon/query/StatusQueryService.kt new file mode 100644 index 00000000..dc7cc88e --- /dev/null +++ b/hideout-mastodon/src/main/kotlin/dev/usbharu/hideout/mastodon/query/StatusQueryService.kt @@ -0,0 +1,45 @@ +/* + * 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.mastodon.query + +import dev.usbharu.hideout.mastodon.interfaces.api.generated.model.Status + +interface StatusQueryService { + suspend fun findByPostIds(ids: List): List + suspend fun findByPostIdsWithMediaIds(statusQueries: List): List + + @Suppress("LongParameterList") + suspend fun accountsStatus( + accountId: Long, + onlyMedia: Boolean = false, + excludeReplies: Boolean = false, + excludeReblogs: Boolean = false, + pinned: Boolean = false, + tagged: String?, + includeFollowers: Boolean = false, + ): List + + suspend fun findByPostId(id: Long): Status? +} + +data class StatusQuery( + val postId: Long, + val replyId: Long?, + val repostId: Long?, + val mediaIds: List, + val emojiIds: List, +) \ No newline at end of file diff --git a/hideout-mastodon/src/main/resources/openapi/mastodon.yaml b/hideout-mastodon/src/main/resources/openapi/mastodon.yaml index 3d78d56c..91c75422 100644 --- a/hideout-mastodon/src/main/resources/openapi/mastodon.yaml +++ b/hideout-mastodon/src/main/resources/openapi/mastodon.yaml @@ -1577,8 +1577,6 @@ components: - discoverable - created_at - statuses_count - - followers_count - - followers_count CredentialAccount: type: object