diff --git a/hideout-core/src/main/kotlin/dev/usbharu/hideout/core/infrastructure/localfilesystem/LocalFileSystemMediaStore.kt b/hideout-core/src/main/kotlin/dev/usbharu/hideout/core/infrastructure/localfilesystem/LocalFileSystemMediaStore.kt index 607ab397..da95574a 100644 --- a/hideout-core/src/main/kotlin/dev/usbharu/hideout/core/infrastructure/localfilesystem/LocalFileSystemMediaStore.kt +++ b/hideout-core/src/main/kotlin/dev/usbharu/hideout/core/infrastructure/localfilesystem/LocalFileSystemMediaStore.kt @@ -7,6 +7,7 @@ import org.slf4j.LoggerFactory import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty import org.springframework.stereotype.Component import java.net.URI +import java.nio.file.Files import java.nio.file.Path import kotlin.io.path.copyTo @@ -19,9 +20,16 @@ class LocalFileSystemMediaStore( MediaStore { 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 { logger.info("START Media upload. {}", id) - val fileSavePath = buildSavePath(path, id) + val fileSavePath = buildSavePath(savePath, id) val fileSavePathString = fileSavePath.toAbsolutePath().toString() logger.info("MEDIA save. path: {}", fileSavePathString) diff --git a/hideout-mastodon/build.gradle.kts b/hideout-mastodon/build.gradle.kts index 5eafc93e..f9dbba9d 100644 --- a/hideout-mastodon/build.gradle.kts +++ b/hideout-mastodon/build.gradle.kts @@ -97,6 +97,10 @@ tasks { importMappings.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") } } 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 index 48f71791..f5139192 100644 --- 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 @@ -16,9 +16,9 @@ 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.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.query.StatusQueryService import org.slf4j.LoggerFactory @@ -28,7 +28,7 @@ import org.springframework.stereotype.Service class GetStatusApplicationService( private val statusQueryService: StatusQueryService, transaction: Transaction, -) : LocalUserAbstractApplicationService( +) : AbstractApplicationService( transaction, logger ) { @@ -36,7 +36,7 @@ class GetStatusApplicationService( 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) ?: throw IllegalArgumentException("Post ${command.id} not found.") 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 index 2e172a33..8208256d 100644 --- 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 @@ -79,11 +79,11 @@ class StatusQueryServiceImpl : StatusQueryService { emojiIdSet.addAll(statusQueries.flatMap { it.emojiIds }) val qa = authorizedQuery() - - val postMap = Posts + val replyToAlias = Posts.alias("reply_to") + val postMap = qa .leftJoin(Actors) .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 } .associate { it[Media.id] to it.toMedia().toMediaAttachments() @@ -112,6 +112,7 @@ class StatusQueryServiceImpl : StatusQueryService { tagged: String?, includeFollowers: Boolean, ): List { + val inReplyToAlias = Posts.alias("reply_to") val qa = authorizedQuery() val query = qa .leftJoin(PostsMedia) @@ -138,7 +139,7 @@ class StatusQueryServiceImpl : StatusQueryService { .groupBy { it[Posts.id] } .map { it.value } .map { - toStatus(it.first(), qa).copy( + toStatus(it.first(), qa, inReplyToAlias).copy( mediaAttachments = it.mapNotNull { resultRow -> resultRow.toMediaOrNull()?.toMediaAttachments() } @@ -151,16 +152,18 @@ class StatusQueryServiceImpl : StatusQueryService { override suspend fun findByPostId(id: Long, principal: Principal?): Status? { val aq = authorizedQuery(principal) + val inReplyTo = Posts.alias("reply_to") val map = aq .leftJoin(PostsMedia, { aq[Posts.id] }, { PostsMedia.postId }) .leftJoin(Actors, { aq[Posts.actorId] }, { Actors.id }) .leftJoin(Media, { PostsMedia.mediaId }, { Media.id }) + .leftJoin(inReplyTo, { aq[Posts.replyId] }, { inReplyTo[Posts.id] }) .selectAll() .where { aq[Posts.id] eq id } .groupBy { it[aq[Posts.id]] } .map { it.value } .map { - toStatus(it.first(), aq).copy( + toStatus(it.first(), aq, inReplyTo).copy( mediaAttachments = it.mapNotNull { resultRow -> resultRow.toMediaOrNull()?.toMediaAttachments() }, @@ -180,18 +183,10 @@ class StatusQueryServiceImpl : StatusQueryService { 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 inReplyToAlias = Posts.alias("reply_to") val qa = authorizedQuery() val pairs = Posts .leftJoin(PostsMedia) @@ -203,7 +198,7 @@ class StatusQueryServiceImpl : StatusQueryService { .groupBy { it[Posts.id] } .map { it.value } .map { - toStatus(it.first(), qa).copy( + toStatus(it.first(), qa, inReplyToAlias).copy( mediaAttachments = it.mapNotNull { resultRow -> resultRow.toMediaOrNull()?.toMediaAttachments() }, @@ -222,7 +217,7 @@ private fun CustomEmoji.toMastodonEmoji(): MastodonEmoji = MastodonEmoji( category = this.category.orEmpty() ) -private fun toStatus(it: ResultRow, queryAlias: QueryAlias) = Status( +private fun toStatus(it: ResultRow, queryAlias: QueryAlias, inReplyToAlias: Alias) = Status( id = it[queryAlias[Posts.id]].toString(), uri = it[queryAlias[Posts.apId]], createdAt = it[queryAlias[Posts.createdAt]].toString(), @@ -271,7 +266,7 @@ private fun toStatus(it: ResultRow, queryAlias: QueryAlias) = Status( repliesCount = 0, url = it[queryAlias[Posts.apId]], inReplyToId = it[queryAlias[Posts.replyId]]?.toString(), - inReplyToAccountId = null, + inReplyToAccountId = it.getOrNull(inReplyToAlias[Posts.actorId])?.toString(), language = null, text = it[queryAlias[Posts.text]], editedAt = null @@ -305,12 +300,12 @@ fun ResultRow.toMediaOrNull(): EntityMedia? { 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) + description = this[Media.description]?.let { MediaDescription(it) } ) } fun EntityMedia.toMediaAttachments(): MediaAttachment = MediaAttachment( - id = id.toString(), + id = id.id.toString(), type = when (type) { FileType.Image -> MediaAttachment.Type.image FileType.Video -> MediaAttachment.Type.video 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 6c2f468a..3f981012 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 @@ -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.mastodon.application.status.GetStatus 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.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.stereotype.Controller @@ -52,13 +51,13 @@ class SpringStatusApi( ) } - override suspend fun apiV1StatusesPost(statusesRequest: StatusesRequest): ResponseEntity { + override suspend fun apiV1StatusesPost(statusesRequest: dev.usbharu.hideout.mastodon.interfaces.api.StatusesRequest): ResponseEntity { val principal = principalContextHolder.getPrincipal() val execute = registerLocalPostApplicationService.execute( RegisterLocalPost( content = statusesRequest.status.orEmpty(), - overview = statusesRequest.spoilerText, + overview = statusesRequest.spoiler_text, visibility = when (statusesRequest.visibility) { public -> Visibility.PUBLIC unlisted -> Visibility.UNLISTED @@ -67,9 +66,9 @@ class SpringStatusApi( null -> Visibility.PUBLIC }, repostId = null, - replyId = statusesRequest.inReplyToId?.toLong(), + replyId = statusesRequest.in_reply_to_id?.toLong(), sensitive = statusesRequest.sensitive == true, - mediaIds = statusesRequest.mediaIds.orEmpty().map { it.toLong() } + mediaIds = statusesRequest.media_ids.map { it.toLong() } ), principal ) diff --git a/hideout-mastodon/src/main/kotlin/dev/usbharu/hideout/mastodon/interfaces/api/StatusesRequest.kt b/hideout-mastodon/src/main/kotlin/dev/usbharu/hideout/mastodon/interfaces/api/StatusesRequest.kt new file mode 100644 index 00000000..74c6cbd1 --- /dev/null +++ b/hideout-mastodon/src/main/kotlin/dev/usbharu/hideout/mastodon/interfaces/api/StatusesRequest.kt @@ -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 = 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 + } +} \ No newline at end of file