mirror of https://github.com/usbharu/Hideout.git
feat: 投稿を取得できるように
This commit is contained in:
parent
91867d6b83
commit
86daf1041b
|
@ -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,
|
||||||
|
)
|
|
@ -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<GetPost, Post>(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)
|
||||||
|
}
|
||||||
|
}
|
|
@ -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<Long>,
|
||||||
|
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
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -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.AbstractApplicationService
|
||||||
import dev.usbharu.hideout.core.application.shared.CommandExecutor
|
import dev.usbharu.hideout.core.application.shared.CommandExecutor
|
||||||
import dev.usbharu.hideout.core.application.shared.Transaction
|
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.actor.ActorRepository
|
||||||
import dev.usbharu.hideout.core.domain.model.media.MediaId
|
import dev.usbharu.hideout.core.domain.model.media.MediaId
|
||||||
import dev.usbharu.hideout.core.domain.model.post.PostId
|
import dev.usbharu.hideout.core.domain.model.post.PostId
|
||||||
|
@ -38,13 +37,13 @@ class RegisterLocalPostApplicationService(
|
||||||
private val postRepository: PostRepository,
|
private val postRepository: PostRepository,
|
||||||
private val userDetailRepository: UserDetailRepository,
|
private val userDetailRepository: UserDetailRepository,
|
||||||
transaction: Transaction,
|
transaction: Transaction,
|
||||||
) : AbstractApplicationService<RegisterLocalPost, Unit>(transaction, Companion.logger) {
|
) : AbstractApplicationService<RegisterLocalPost, Long>(transaction, Companion.logger) {
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
val logger: Logger = LoggerFactory.getLogger(RegisterLocalPostApplicationService::class.java)
|
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)
|
val actorId = (userDetailRepository.findById(command.userDetailId)
|
||||||
?: throw IllegalStateException("actor not found")).actorId
|
?: throw IllegalStateException("actor not found")).actorId
|
||||||
|
|
||||||
|
@ -59,5 +58,7 @@ class RegisterLocalPostApplicationService(
|
||||||
command.mediaIds.map { MediaId(it) })
|
command.mediaIds.map { MediaId(it) })
|
||||||
|
|
||||||
postRepository.save(post)
|
postRepository.save(post)
|
||||||
|
|
||||||
|
return post.id.id
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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()
|
||||||
|
}
|
||||||
|
}
|
|
@ -86,7 +86,7 @@ create table if not exists media
|
||||||
url varchar(255) not null unique,
|
url varchar(255) not null unique,
|
||||||
remote_url varchar(255) null unique,
|
remote_url varchar(255) null unique,
|
||||||
thumbnail_url varchar(255) null unique,
|
thumbnail_url varchar(255) null unique,
|
||||||
"type" int not null,
|
"type" varchar(100) not null,
|
||||||
blurhash varchar(255) null,
|
blurhash varchar(255) null,
|
||||||
mime_type varchar(255) not null,
|
mime_type varchar(255) not null,
|
||||||
description varchar(4000) null
|
description varchar(4000) null
|
||||||
|
|
|
@ -31,6 +31,7 @@ dependencies {
|
||||||
implementation(libs.jakarta.annotation)
|
implementation(libs.jakarta.annotation)
|
||||||
implementation(libs.jakarta.validation)
|
implementation(libs.jakarta.validation)
|
||||||
|
|
||||||
|
implementation(libs.bundles.exposed)
|
||||||
implementation(libs.bundles.openapi)
|
implementation(libs.bundles.openapi)
|
||||||
implementation(libs.bundles.coroutines)
|
implementation(libs.bundles.coroutines)
|
||||||
}
|
}
|
||||||
|
|
|
@ -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,
|
||||||
|
)
|
|
@ -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<GetStatus, Status>(
|
||||||
|
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")
|
||||||
|
}
|
||||||
|
}
|
|
@ -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<Long>): List<Account> {
|
||||||
|
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],
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
|
@ -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<Long>): List<Status> = findByPostIdsWithMedia(ids)
|
||||||
|
|
||||||
|
override suspend fun findByPostIdsWithMediaIds(statusQueries: List<StatusQuery>): List<Status> {
|
||||||
|
val postIdSet = mutableSetOf<Long>()
|
||||||
|
postIdSet.addAll(statusQueries.flatMap { listOfNotNull(it.postId, it.replyId, it.repostId) })
|
||||||
|
val mediaIdSet = mutableSetOf<Long>()
|
||||||
|
mediaIdSet.addAll(statusQueries.flatMap { it.mediaIds })
|
||||||
|
|
||||||
|
val emojiIdSet = mutableSetOf<Long>()
|
||||||
|
emojiIdSet.addAll(statusQueries.flatMap { it.emojiIds })
|
||||||
|
|
||||||
|
val postMap = Posts
|
||||||
|
.leftJoin(Actors)
|
||||||
|
.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<Status> {
|
||||||
|
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<Pair<Status, Long?>>): List<Status> {
|
||||||
|
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<Long>): List<Status> {
|
||||||
|
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()
|
||||||
|
)
|
|
@ -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.RegisterLocalPost
|
||||||
import dev.usbharu.hideout.core.application.post.RegisterLocalPostApplicationService
|
import dev.usbharu.hideout.core.application.post.RegisterLocalPostApplicationService
|
||||||
import dev.usbharu.hideout.core.domain.model.post.Visibility
|
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.StatusApi
|
||||||
import dev.usbharu.hideout.mastodon.interfaces.api.generated.model.Status
|
import dev.usbharu.hideout.mastodon.interfaces.api.generated.model.Status
|
||||||
import dev.usbharu.hideout.mastodon.interfaces.api.generated.model.StatusesRequest
|
import dev.usbharu.hideout.mastodon.interfaces.api.generated.model.StatusesRequest
|
||||||
|
@ -29,8 +32,9 @@ import org.springframework.stereotype.Controller
|
||||||
|
|
||||||
@Controller
|
@Controller
|
||||||
class SpringStatusApi(
|
class SpringStatusApi(
|
||||||
private val oauth2CommandExecutorFactory: Oauth2CommandExecutorFactory,
|
private val delegateCommandExecutorFactory: DelegateCommandExecutorFactory,
|
||||||
private val registerLocalPostApplicationService: RegisterLocalPostApplicationService,
|
private val registerLocalPostApplicationService: RegisterLocalPostApplicationService,
|
||||||
|
private val getStatusApplicationService: GetStatusApplicationService,
|
||||||
) : StatusApi {
|
) : StatusApi {
|
||||||
override suspend fun apiV1StatusesIdEmojiReactionsEmojiDelete(id: String, emoji: String): ResponseEntity<Status> {
|
override suspend fun apiV1StatusesIdEmojiReactionsEmojiDelete(id: String, emoji: String): ResponseEntity<Status> {
|
||||||
return super.apiV1StatusesIdEmojiReactionsEmojiDelete(id, emoji)
|
return super.apiV1StatusesIdEmojiReactionsEmojiDelete(id, emoji)
|
||||||
|
@ -41,12 +45,18 @@ class SpringStatusApi(
|
||||||
}
|
}
|
||||||
|
|
||||||
override suspend fun apiV1StatusesIdGet(id: String): ResponseEntity<Status> {
|
override suspend fun apiV1StatusesIdGet(id: String): ResponseEntity<Status> {
|
||||||
return super.apiV1StatusesIdGet(id)
|
|
||||||
|
return ResponseEntity.ok(
|
||||||
|
getStatusApplicationService.execute(
|
||||||
|
GetStatus(id),
|
||||||
|
delegateCommandExecutorFactory.getCommandExecutor()
|
||||||
|
)
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
override suspend fun apiV1StatusesPost(statusesRequest: StatusesRequest): ResponseEntity<Status> {
|
override suspend fun apiV1StatusesPost(statusesRequest: StatusesRequest): ResponseEntity<Status> {
|
||||||
val executor = oauth2CommandExecutorFactory.getCommandExecutor()
|
val executor = delegateCommandExecutorFactory.getCommandExecutor() as Oauth2CommandExecutor
|
||||||
registerLocalPostApplicationService.execute(
|
val execute = registerLocalPostApplicationService.execute(
|
||||||
RegisterLocalPost(
|
RegisterLocalPost(
|
||||||
userDetailId = executor.userDetailId,
|
userDetailId = executor.userDetailId,
|
||||||
content = statusesRequest.status.orEmpty(),
|
content = statusesRequest.status.orEmpty(),
|
||||||
|
@ -65,6 +75,11 @@ class SpringStatusApi(
|
||||||
),
|
),
|
||||||
executor
|
executor
|
||||||
)
|
)
|
||||||
return ResponseEntity.ok().build()
|
|
||||||
|
|
||||||
|
val status = getStatusApplicationService.execute(GetStatus(execute.toString()), executor)
|
||||||
|
return ResponseEntity.ok(
|
||||||
|
status
|
||||||
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
|
@ -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<Long>): List<Account>
|
||||||
|
}
|
|
@ -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<Long>): List<Status>
|
||||||
|
suspend fun findByPostIdsWithMediaIds(statusQueries: List<StatusQuery>): List<Status>
|
||||||
|
|
||||||
|
@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<Status>
|
||||||
|
|
||||||
|
suspend fun findByPostId(id: Long): Status?
|
||||||
|
}
|
||||||
|
|
||||||
|
data class StatusQuery(
|
||||||
|
val postId: Long,
|
||||||
|
val replyId: Long?,
|
||||||
|
val repostId: Long?,
|
||||||
|
val mediaIds: List<Long>,
|
||||||
|
val emojiIds: List<Long>,
|
||||||
|
)
|
|
@ -1577,8 +1577,6 @@ components:
|
||||||
- discoverable
|
- discoverable
|
||||||
- created_at
|
- created_at
|
||||||
- statuses_count
|
- statuses_count
|
||||||
- followers_count
|
|
||||||
- followers_count
|
|
||||||
|
|
||||||
CredentialAccount:
|
CredentialAccount:
|
||||||
type: object
|
type: object
|
||||||
|
|
Loading…
Reference in New Issue