fix: メディア付き投稿に失敗する問題を修正

This commit is contained in:
usbharu 2024-08-12 13:28:03 +09:00
parent 42b9d4e64b
commit ced41e64fd
Signed by: usbharu
GPG Key ID: 6556747BF94EEBC8
6 changed files with 136 additions and 30 deletions

View File

@ -7,6 +7,7 @@ import org.slf4j.LoggerFactory
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty
import org.springframework.stereotype.Component import org.springframework.stereotype.Component
import java.net.URI import java.net.URI
import java.nio.file.Files
import java.nio.file.Path import java.nio.file.Path
import kotlin.io.path.copyTo import kotlin.io.path.copyTo
@ -19,9 +20,16 @@ class LocalFileSystemMediaStore(
MediaStore { MediaStore {
private val publicUrl = localStorageConfig.publicUrl ?: "${applicationConfig.url}/files/" private val publicUrl = localStorageConfig.publicUrl ?: "${applicationConfig.url}/files/"
private val savePath = Path.of(localStorageConfig.path)
init {
Files.createDirectories(savePath)
}
override suspend fun upload(path: Path, id: String): URI { override suspend fun upload(path: Path, id: String): URI {
logger.info("START Media upload. {}", id) logger.info("START Media upload. {}", id)
val fileSavePath = buildSavePath(path, id) val fileSavePath = buildSavePath(savePath, id)
val fileSavePathString = fileSavePath.toAbsolutePath().toString() val fileSavePathString = fileSavePath.toAbsolutePath().toString()
logger.info("MEDIA save. path: {}", fileSavePathString) logger.info("MEDIA save. path: {}", fileSavePathString)

View File

@ -97,6 +97,10 @@ tasks {
importMappings.put("org.springframework.core.io.Resource", "org.springframework.web.multipart.MultipartFile") importMappings.put("org.springframework.core.io.Resource", "org.springframework.web.multipart.MultipartFile")
typeMappings.put("org.springframework.core.io.Resource", "org.springframework.web.multipart.MultipartFile") typeMappings.put("org.springframework.core.io.Resource", "org.springframework.web.multipart.MultipartFile")
schemaMappings.put(
"StatusesRequest",
"dev.usbharu.hideout.mastodon.interfaces.api.StatusesRequest"
)
templateDir.set("$rootDir/templates") templateDir.set("$rootDir/templates")
} }
} }

View File

@ -16,9 +16,9 @@
package dev.usbharu.hideout.mastodon.application.status package dev.usbharu.hideout.mastodon.application.status
import dev.usbharu.hideout.core.application.shared.LocalUserAbstractApplicationService import dev.usbharu.hideout.core.application.shared.AbstractApplicationService
import dev.usbharu.hideout.core.application.shared.Transaction import dev.usbharu.hideout.core.application.shared.Transaction
import dev.usbharu.hideout.core.domain.model.support.principal.FromApi import dev.usbharu.hideout.core.domain.model.support.principal.Principal
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.query.StatusQueryService import dev.usbharu.hideout.mastodon.query.StatusQueryService
import org.slf4j.LoggerFactory import org.slf4j.LoggerFactory
@ -28,7 +28,7 @@ import org.springframework.stereotype.Service
class GetStatusApplicationService( class GetStatusApplicationService(
private val statusQueryService: StatusQueryService, private val statusQueryService: StatusQueryService,
transaction: Transaction, transaction: Transaction,
) : LocalUserAbstractApplicationService<GetStatus, Status>( ) : AbstractApplicationService<GetStatus, Status>(
transaction, transaction,
logger logger
) { ) {
@ -36,7 +36,7 @@ class GetStatusApplicationService(
val logger = LoggerFactory.getLogger(GetStatusApplicationService::class.java)!! val logger = LoggerFactory.getLogger(GetStatusApplicationService::class.java)!!
} }
override suspend fun internalExecute(command: GetStatus, principal: FromApi): Status { override suspend fun internalExecute(command: GetStatus, principal: Principal): Status {
return statusQueryService.findByPostId(command.id.toLong(), principal) return statusQueryService.findByPostId(command.id.toLong(), principal)
?: throw IllegalArgumentException("Post ${command.id} not found.") ?: throw IllegalArgumentException("Post ${command.id} not found.")

View File

@ -79,11 +79,11 @@ class StatusQueryServiceImpl : StatusQueryService {
emojiIdSet.addAll(statusQueries.flatMap { it.emojiIds }) emojiIdSet.addAll(statusQueries.flatMap { it.emojiIds })
val qa = authorizedQuery() val qa = authorizedQuery()
val replyToAlias = Posts.alias("reply_to")
val postMap = Posts val postMap = qa
.leftJoin(Actors) .leftJoin(Actors)
.selectAll().where { Posts.id inList postIdSet } .selectAll().where { Posts.id inList postIdSet }
.associate { it[Posts.id] to toStatus(it, qa) } .associate { it[Posts.id] to toStatus(it, qa, replyToAlias) }
val mediaMap = Media.selectAll().where { Media.id inList mediaIdSet } val mediaMap = Media.selectAll().where { Media.id inList mediaIdSet }
.associate { .associate {
it[Media.id] to it.toMedia().toMediaAttachments() it[Media.id] to it.toMedia().toMediaAttachments()
@ -112,6 +112,7 @@ class StatusQueryServiceImpl : StatusQueryService {
tagged: String?, tagged: String?,
includeFollowers: Boolean, includeFollowers: Boolean,
): List<Status> { ): List<Status> {
val inReplyToAlias = Posts.alias("reply_to")
val qa = authorizedQuery() val qa = authorizedQuery()
val query = qa val query = qa
.leftJoin(PostsMedia) .leftJoin(PostsMedia)
@ -138,7 +139,7 @@ class StatusQueryServiceImpl : StatusQueryService {
.groupBy { it[Posts.id] } .groupBy { it[Posts.id] }
.map { it.value } .map { it.value }
.map { .map {
toStatus(it.first(), qa).copy( toStatus(it.first(), qa, inReplyToAlias).copy(
mediaAttachments = it.mapNotNull { resultRow -> mediaAttachments = it.mapNotNull { resultRow ->
resultRow.toMediaOrNull()?.toMediaAttachments() resultRow.toMediaOrNull()?.toMediaAttachments()
} }
@ -151,16 +152,18 @@ class StatusQueryServiceImpl : StatusQueryService {
override suspend fun findByPostId(id: Long, principal: Principal?): Status? { override suspend fun findByPostId(id: Long, principal: Principal?): Status? {
val aq = authorizedQuery(principal) val aq = authorizedQuery(principal)
val inReplyTo = Posts.alias("reply_to")
val map = aq val map = aq
.leftJoin(PostsMedia, { aq[Posts.id] }, { PostsMedia.postId }) .leftJoin(PostsMedia, { aq[Posts.id] }, { PostsMedia.postId })
.leftJoin(Actors, { aq[Posts.actorId] }, { Actors.id }) .leftJoin(Actors, { aq[Posts.actorId] }, { Actors.id })
.leftJoin(Media, { PostsMedia.mediaId }, { Media.id }) .leftJoin(Media, { PostsMedia.mediaId }, { Media.id })
.leftJoin(inReplyTo, { aq[Posts.replyId] }, { inReplyTo[Posts.id] })
.selectAll() .selectAll()
.where { aq[Posts.id] eq id } .where { aq[Posts.id] eq id }
.groupBy { it[aq[Posts.id]] } .groupBy { it[aq[Posts.id]] }
.map { it.value } .map { it.value }
.map { .map {
toStatus(it.first(), aq).copy( toStatus(it.first(), aq, inReplyTo).copy(
mediaAttachments = it.mapNotNull { resultRow -> mediaAttachments = it.mapNotNull { resultRow ->
resultRow.toMediaOrNull()?.toMediaAttachments() resultRow.toMediaOrNull()?.toMediaAttachments()
}, },
@ -180,18 +183,10 @@ class StatusQueryServiceImpl : StatusQueryService {
it.first 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> { private suspend fun findByPostIdsWithMedia(ids: List<Long>): List<Status> {
val inReplyToAlias = Posts.alias("reply_to")
val qa = authorizedQuery() val qa = authorizedQuery()
val pairs = Posts val pairs = Posts
.leftJoin(PostsMedia) .leftJoin(PostsMedia)
@ -203,7 +198,7 @@ class StatusQueryServiceImpl : StatusQueryService {
.groupBy { it[Posts.id] } .groupBy { it[Posts.id] }
.map { it.value } .map { it.value }
.map { .map {
toStatus(it.first(), qa).copy( toStatus(it.first(), qa, inReplyToAlias).copy(
mediaAttachments = it.mapNotNull { resultRow -> mediaAttachments = it.mapNotNull { resultRow ->
resultRow.toMediaOrNull()?.toMediaAttachments() resultRow.toMediaOrNull()?.toMediaAttachments()
}, },
@ -222,7 +217,7 @@ private fun CustomEmoji.toMastodonEmoji(): MastodonEmoji = MastodonEmoji(
category = this.category.orEmpty() category = this.category.orEmpty()
) )
private fun toStatus(it: ResultRow, queryAlias: QueryAlias) = Status( private fun toStatus(it: ResultRow, queryAlias: QueryAlias, inReplyToAlias: Alias<Posts>) = Status(
id = it[queryAlias[Posts.id]].toString(), id = it[queryAlias[Posts.id]].toString(),
uri = it[queryAlias[Posts.apId]], uri = it[queryAlias[Posts.apId]],
createdAt = it[queryAlias[Posts.createdAt]].toString(), createdAt = it[queryAlias[Posts.createdAt]].toString(),
@ -271,7 +266,7 @@ private fun toStatus(it: ResultRow, queryAlias: QueryAlias) = Status(
repliesCount = 0, repliesCount = 0,
url = it[queryAlias[Posts.apId]], url = it[queryAlias[Posts.apId]],
inReplyToId = it[queryAlias[Posts.replyId]]?.toString(), inReplyToId = it[queryAlias[Posts.replyId]]?.toString(),
inReplyToAccountId = null, inReplyToAccountId = it.getOrNull(inReplyToAlias[Posts.actorId])?.toString(),
language = null, language = null,
text = it[queryAlias[Posts.text]], text = it[queryAlias[Posts.text]],
editedAt = null editedAt = null
@ -305,12 +300,12 @@ fun ResultRow.toMediaOrNull(): EntityMedia? {
type = FileType.valueOf(this[Media.type]), type = FileType.valueOf(this[Media.type]),
blurHash = this[Media.blurhash]?.let { MediaBlurHash(it) }, blurHash = this[Media.blurhash]?.let { MediaBlurHash(it) },
mimeType = MimeType(mimeType.substringBefore("/"), mimeType.substringAfter("/"), fileType), mimeType = MimeType(mimeType.substringBefore("/"), mimeType.substringAfter("/"), fileType),
description = MediaDescription(this[Media.description] ?: return null) description = this[Media.description]?.let { MediaDescription(it) }
) )
} }
fun EntityMedia.toMediaAttachments(): MediaAttachment = MediaAttachment( fun EntityMedia.toMediaAttachments(): MediaAttachment = MediaAttachment(
id = id.toString(), id = id.id.toString(),
type = when (type) { type = when (type) {
FileType.Image -> MediaAttachment.Type.image FileType.Image -> MediaAttachment.Type.image
FileType.Video -> MediaAttachment.Type.video FileType.Video -> MediaAttachment.Type.video

View File

@ -22,10 +22,9 @@ import dev.usbharu.hideout.core.domain.model.post.Visibility
import dev.usbharu.hideout.core.domain.model.support.principal.PrincipalContextHolder import dev.usbharu.hideout.core.domain.model.support.principal.PrincipalContextHolder
import dev.usbharu.hideout.mastodon.application.status.GetStatus import dev.usbharu.hideout.mastodon.application.status.GetStatus
import dev.usbharu.hideout.mastodon.application.status.GetStatusApplicationService import dev.usbharu.hideout.mastodon.application.status.GetStatusApplicationService
import dev.usbharu.hideout.mastodon.interfaces.api.StatusesRequest.Visibility.*
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.Visibility.*
import org.springframework.http.ResponseEntity import org.springframework.http.ResponseEntity
import org.springframework.stereotype.Controller import org.springframework.stereotype.Controller
@ -52,13 +51,13 @@ class SpringStatusApi(
) )
} }
override suspend fun apiV1StatusesPost(statusesRequest: StatusesRequest): ResponseEntity<Status> { override suspend fun apiV1StatusesPost(statusesRequest: dev.usbharu.hideout.mastodon.interfaces.api.StatusesRequest): ResponseEntity<Status> {
val principal = principalContextHolder.getPrincipal() val principal = principalContextHolder.getPrincipal()
val execute = registerLocalPostApplicationService.execute( val execute = registerLocalPostApplicationService.execute(
RegisterLocalPost( RegisterLocalPost(
content = statusesRequest.status.orEmpty(), content = statusesRequest.status.orEmpty(),
overview = statusesRequest.spoilerText, overview = statusesRequest.spoiler_text,
visibility = when (statusesRequest.visibility) { visibility = when (statusesRequest.visibility) {
public -> Visibility.PUBLIC public -> Visibility.PUBLIC
unlisted -> Visibility.UNLISTED unlisted -> Visibility.UNLISTED
@ -67,9 +66,9 @@ class SpringStatusApi(
null -> Visibility.PUBLIC null -> Visibility.PUBLIC
}, },
repostId = null, repostId = null,
replyId = statusesRequest.inReplyToId?.toLong(), replyId = statusesRequest.in_reply_to_id?.toLong(),
sensitive = statusesRequest.sensitive == true, sensitive = statusesRequest.sensitive == true,
mediaIds = statusesRequest.mediaIds.orEmpty().map { it.toLong() } mediaIds = statusesRequest.media_ids.map { it.toLong() }
), principal ), principal
) )

View File

@ -0,0 +1,100 @@
package dev.usbharu.hideout.mastodon.interfaces.api
import com.fasterxml.jackson.annotation.JsonProperty
import dev.usbharu.hideout.core.domain.model.post.Visibility
import dev.usbharu.hideout.mastodon.interfaces.api.StatusesRequest.Visibility.*
import dev.usbharu.hideout.mastodon.interfaces.api.generated.model.Status
import dev.usbharu.hideout.mastodon.interfaces.api.generated.model.StatusesRequestPoll
@Suppress("VariableNaming", "EnumEntryName")
class StatusesRequest {
@JsonProperty("status")
var status: String? = null
@JsonProperty("media_ids")
var media_ids: List<String> = emptyList()
@JsonProperty("poll")
var poll: StatusesRequestPoll? = null
@JsonProperty("in_reply_to_id")
var in_reply_to_id: String? = null
@JsonProperty("sensitive")
var sensitive: Boolean? = null
@JsonProperty("spoiler_text")
var spoiler_text: String? = null
@JsonProperty("visibility")
var visibility: Visibility? = null
@JsonProperty("language")
var language: String? = null
@JsonProperty("scheduled_at")
var scheduled_at: String? = null
override fun equals(other: Any?): Boolean {
if (this === other) return true
if (other !is StatusesRequest) return false
if (status != other.status) return false
if (media_ids != other.media_ids) return false
if (poll != other.poll) return false
if (in_reply_to_id != other.in_reply_to_id) return false
if (sensitive != other.sensitive) return false
if (spoiler_text != other.spoiler_text) return false
if (visibility != other.visibility) return false
if (language != other.language) return false
if (scheduled_at != other.scheduled_at) return false
return true
}
override fun hashCode(): Int {
var result = status?.hashCode() ?: 0
result = 31 * result + media_ids.hashCode()
result = 31 * result + (poll?.hashCode() ?: 0)
result = 31 * result + (in_reply_to_id?.hashCode() ?: 0)
result = 31 * result + (sensitive?.hashCode() ?: 0)
result = 31 * result + (spoiler_text?.hashCode() ?: 0)
result = 31 * result + (visibility?.hashCode() ?: 0)
result = 31 * result + (language?.hashCode() ?: 0)
result = 31 * result + (scheduled_at?.hashCode() ?: 0)
return result
}
override fun toString(): String {
return "StatusesRequest(status=$status, mediaIds=$media_ids, poll=$poll, inReplyToId=$in_reply_to_id, " +
"sensitive=$sensitive, spoilerText=$spoiler_text, visibility=$visibility, language=$language," +
" scheduledAt=$scheduled_at)"
}
@Suppress("EnumNaming", "EnumEntryNameCase")
enum class Visibility {
`public`,
unlisted,
private,
direct
}
}
fun StatusesRequest.Visibility?.toPostVisibility(): Visibility {
return when (this) {
public -> Visibility.PUBLIC
unlisted -> Visibility.UNLISTED
private -> Visibility.FOLLOWERS
direct -> Visibility.DIRECT
null -> Visibility.PUBLIC
}
}
fun StatusesRequest.Visibility?.toStatusVisibility(): Status.Visibility {
return when (this) {
public -> Status.Visibility.public
unlisted -> Status.Visibility.unlisted
private -> Status.Visibility.private
direct -> Status.Visibility.direct
null -> Status.Visibility.public
}
}