This commit is contained in:
usbharu 2024-06-02 16:04:30 +09:00
parent e13858e068
commit 922bdb4991
142 changed files with 72 additions and 4891 deletions

View File

@ -17,7 +17,7 @@
package activitypub.webfinger
import dev.usbharu.hideout.SpringApplication
import dev.usbharu.hideout.application.external.Transaction
import dev.usbharu.hideout.core.application.shared.Transaction
import dev.usbharu.owl.producer.api.OwlProducer
import kotlinx.coroutines.runBlocking
import org.flywaydb.core.Flyway

View File

@ -20,8 +20,6 @@ import dev.usbharu.hideout.SpringApplication
import dev.usbharu.hideout.core.domain.model.emoji.CustomEmoji
import dev.usbharu.hideout.core.domain.model.emoji.UnicodeEmoji
import dev.usbharu.hideout.core.infrastructure.exposedrepository.CustomEmojis
import dev.usbharu.hideout.core.infrastructure.exposedrepository.Reactions
import dev.usbharu.hideout.core.infrastructure.exposedrepository.toReaction
import dev.usbharu.owl.producer.api.OwlProducer
import kotlinx.coroutines.runBlocking
import org.assertj.core.api.Assertions.assertThat

View File

@ -16,7 +16,7 @@
package util
import dev.usbharu.hideout.application.external.Transaction
import dev.usbharu.hideout.core.application.shared.Transaction
object TestTransaction : Transaction {
override suspend fun <T> transaction(block: suspend () -> T): T = block()

View File

@ -16,7 +16,7 @@
package util
import dev.usbharu.hideout.application.external.Transaction
import dev.usbharu.hideout.core.application.shared.Transaction
import dev.usbharu.hideout.core.domain.exception.resource.UserNotFoundException
import dev.usbharu.hideout.core.infrastructure.springframework.httpsignature.HttpSignatureUser
import dev.usbharu.httpsignature.common.HttpHeaders

View File

@ -25,7 +25,7 @@ import com.nimbusds.jose.jwk.source.JWKSource
import com.nimbusds.jose.proc.SecurityContext
import dev.usbharu.hideout.activitypub.domain.model.StringORObjectSerializer
import dev.usbharu.hideout.activitypub.domain.model.StringOrObject
import dev.usbharu.hideout.application.external.Transaction
import dev.usbharu.hideout.core.application.shared.Transaction
import dev.usbharu.hideout.core.infrastructure.springframework.httpsignature.HttpSignatureFilter
import dev.usbharu.hideout.core.infrastructure.springframework.httpsignature.HttpSignatureHeaderChecker
import dev.usbharu.hideout.core.infrastructure.springframework.httpsignature.HttpSignatureUserDetailsService

View File

@ -35,7 +35,6 @@ class OwlProducerRunner(private val owlProducer: OwlProducer, private val taskDe
}
override fun destroy() {
System.err.println("destroy aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa")
runBlocking {
owlProducer.stop()
}

View File

@ -16,7 +16,7 @@
package dev.usbharu.hideout.application.infrastructure.exposed
import dev.usbharu.hideout.application.external.Transaction
import dev.usbharu.hideout.core.application.shared.Transaction
import kotlinx.coroutines.runBlocking
import kotlinx.coroutines.slf4j.MDCContext
import org.jetbrains.exposed.sql.Slf4jSqlDebugLogger

View File

@ -16,8 +16,6 @@
package dev.usbharu.hideout.application.service.init
import dev.usbharu.hideout.core.domain.model.meta.Jwt
import dev.usbharu.hideout.core.domain.model.meta.Meta
import org.springframework.stereotype.Service
@Service

View File

@ -16,11 +16,8 @@
package dev.usbharu.hideout.application.service.init
import dev.usbharu.hideout.application.external.Transaction
import dev.usbharu.hideout.core.application.shared.Transaction
import dev.usbharu.hideout.core.domain.exception.NotInitException
import dev.usbharu.hideout.core.domain.model.meta.Jwt
import dev.usbharu.hideout.core.domain.model.meta.Meta
import dev.usbharu.hideout.core.domain.model.meta.MetaRepository
import org.springframework.stereotype.Service
@Service

View File

@ -1,80 +0,0 @@
/*
* 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.application.service.init
import dev.usbharu.hideout.application.external.Transaction
import dev.usbharu.hideout.core.domain.model.meta.Jwt
import dev.usbharu.hideout.core.domain.model.meta.Meta
import dev.usbharu.hideout.core.domain.model.meta.MetaRepository
import dev.usbharu.hideout.util.ServerUtil
import org.slf4j.Logger
import org.slf4j.LoggerFactory
import org.springframework.stereotype.Service
import java.security.KeyPairGenerator
import java.util.*
@Service
class ServerInitialiseServiceImpl(
private val metaRepository: MetaRepository,
private val transaction: Transaction
) :
ServerInitialiseService {
val logger: Logger = LoggerFactory.getLogger(ServerInitialiseServiceImpl::class.java)
override suspend fun init() {
transaction.transaction {
val savedMeta = metaRepository.get()
val implementationVersion = ServerUtil.getImplementationVersion()
if (wasInitialised(savedMeta).not()) {
logger.info("Start Initialise")
initialise(implementationVersion)
logger.info("Finish Initialise")
return@transaction
}
if (isVersionChanged(requireNotNull(savedMeta))) {
logger.info("Version changed!! (${savedMeta.version} -> $implementationVersion)")
updateVersion(savedMeta, implementationVersion)
}
}
}
private fun wasInitialised(meta: Meta?): Boolean {
logger.debug("Initialise checking...")
return meta != null
}
private fun isVersionChanged(meta: Meta): Boolean = meta.version != ServerUtil.getImplementationVersion()
private suspend fun initialise(implementationVersion: String) {
val keyPairGenerator = KeyPairGenerator.getInstance("RSA")
keyPairGenerator.initialize(2048)
val generateKeyPair = keyPairGenerator.generateKeyPair()
val jwt = Jwt(
UUID.randomUUID(),
Base64.getEncoder().encodeToString(generateKeyPair.private.encoded),
Base64.getEncoder().encodeToString(generateKeyPair.public.encoded)
)
val meta = Meta(implementationVersion, jwt)
metaRepository.save(meta)
}
private suspend fun updateVersion(meta: Meta, version: String) {
metaRepository.save(meta.copy(version = version))
}
}

View File

@ -14,9 +14,9 @@
* limitations under the License.
*/
package dev.usbharu.hideout.core.usecase.actor
package dev.usbharu.hideout.core.application.actor
import dev.usbharu.hideout.application.external.Transaction
import dev.usbharu.hideout.core.application.shared.Transaction
import dev.usbharu.hideout.core.domain.model.actor.Actor2Repository
import dev.usbharu.hideout.core.domain.model.actor.ActorId
import org.springframework.stereotype.Service

View File

@ -14,9 +14,9 @@
* limitations under the License.
*/
package dev.usbharu.hideout.core.usecase.actor
package dev.usbharu.hideout.core.application.actor
import dev.usbharu.hideout.application.external.Transaction
import dev.usbharu.hideout.core.application.shared.Transaction
import dev.usbharu.hideout.core.domain.model.actor.Actor2Repository
import dev.usbharu.hideout.core.domain.model.actor.ActorId
import dev.usbharu.hideout.core.domain.service.actor.local.AccountMigrationCheck.*

View File

@ -14,7 +14,7 @@
* limitations under the License.
*/
package dev.usbharu.hideout.core.usecase.actor
package dev.usbharu.hideout.core.application.actor
data class RegisterLocalActor(
val name: String,

View File

@ -14,11 +14,11 @@
* limitations under the License.
*/
package dev.usbharu.hideout.core.usecase.actor
package dev.usbharu.hideout.core.application.actor
import dev.usbharu.hideout.application.config.ApplicationConfig
import dev.usbharu.hideout.application.external.Transaction
import dev.usbharu.hideout.application.service.id.IdGenerateService
import dev.usbharu.hideout.core.application.shared.Transaction
import dev.usbharu.hideout.core.domain.model.actor.Actor2Repository
import dev.usbharu.hideout.core.domain.model.instance.InstanceRepository
import dev.usbharu.hideout.core.domain.model.userdetails.UserDetail

View File

@ -1,4 +1,4 @@
package dev.usbharu.hideout.core.usecase.actor
package dev.usbharu.hideout.core.application.actor
import org.springframework.stereotype.Service

View File

@ -14,9 +14,9 @@
* limitations under the License.
*/
package dev.usbharu.hideout.core.usecase.actor
package dev.usbharu.hideout.core.application.actor
import dev.usbharu.hideout.application.external.Transaction
import dev.usbharu.hideout.core.application.shared.Transaction
import dev.usbharu.hideout.core.domain.model.actor.Actor2Repository
import dev.usbharu.hideout.core.domain.model.actor.ActorId
import org.springframework.stereotype.Service

View File

@ -14,9 +14,9 @@
* limitations under the License.
*/
package dev.usbharu.hideout.core.usecase.actor
package dev.usbharu.hideout.core.application.actor
import dev.usbharu.hideout.application.external.Transaction
import dev.usbharu.hideout.core.application.shared.Transaction
import dev.usbharu.hideout.core.domain.model.actor.Actor2Repository
import dev.usbharu.hideout.core.domain.model.actor.ActorId
import org.springframework.stereotype.Service

View File

@ -14,7 +14,7 @@
* limitations under the License.
*/
package dev.usbharu.hideout.core.usecase.post
package dev.usbharu.hideout.core.application.post
import dev.usbharu.hideout.core.domain.model.post.Post2Repository
import dev.usbharu.hideout.core.domain.model.post.PostId

View File

@ -14,7 +14,7 @@
* limitations under the License.
*/
package dev.usbharu.hideout.core.usecase.post
package dev.usbharu.hideout.core.application.post
import dev.usbharu.hideout.core.domain.model.post.Visibility

View File

@ -14,7 +14,7 @@
* limitations under the License.
*/
package dev.usbharu.hideout.core.usecase.post
package dev.usbharu.hideout.core.application.post
import dev.usbharu.hideout.core.domain.model.actor.Actor2Repository
import dev.usbharu.hideout.core.domain.model.actor.ActorId

View File

@ -14,7 +14,7 @@
* limitations under the License.
*/
package dev.usbharu.hideout.core.usecase.post
package dev.usbharu.hideout.core.application.post
data class UpdateLocalNote(
val postId: Long,

View File

@ -14,9 +14,9 @@
* limitations under the License.
*/
package dev.usbharu.hideout.core.usecase.post
package dev.usbharu.hideout.core.application.post
import dev.usbharu.hideout.application.external.Transaction
import dev.usbharu.hideout.core.application.shared.Transaction
import dev.usbharu.hideout.core.domain.model.media.MediaId
import dev.usbharu.hideout.core.domain.model.post.Post2Repository
import dev.usbharu.hideout.core.domain.model.post.PostId

View File

@ -14,7 +14,7 @@
* limitations under the License.
*/
package dev.usbharu.hideout.application.external
package dev.usbharu.hideout.core.application.shared
import org.springframework.stereotype.Service

View File

@ -16,10 +16,13 @@
package dev.usbharu.hideout.core.domain.model.emoji
import dev.usbharu.hideout.core.domain.model.instance.InstanceId
import dev.usbharu.hideout.core.domain.model.shared.Domain
import java.net.URI
import java.time.Instant
sealed class Emoji {
abstract val domain: String
abstract val domain: Domain
abstract val name: String
@Suppress("FunctionMinLength")
@ -33,13 +36,13 @@ sealed class Emoji {
}
data class CustomEmoji(
val id: Long,
val id: EmojiId,
override val name: String,
override val domain: String,
val instanceId: Long?,
val url: String,
override val domain: Domain,
val instanceId: InstanceId,
val url: URI,
val category: String?,
val createdAt: Instant
val createdAt: Instant,
) : Emoji() {
override fun id(): String = id.toString()
}
@ -47,6 +50,6 @@ data class CustomEmoji(
data class UnicodeEmoji(
override val name: String
) : Emoji() {
override val domain: String = "unicode.org"
override val domain: Domain = Domain("unicode.org")
override fun id(): String = name
}

View File

@ -1,25 +0,0 @@
/*
* Copyright (C) 2024 usbharu
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package dev.usbharu.hideout.core.domain.model.filter
data class Filter(
val id: Long,
val userId: Long,
val name: String,
val context: List<FilterType>,
val filterAction: FilterAction,
)

View File

@ -1,23 +0,0 @@
/*
* Copyright (C) 2024 usbharu
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package dev.usbharu.hideout.core.domain.model.filter
@Suppress("EnumEntryNameCase", "EnumNaming")
enum class FilterAction {
warn,
hide
}

View File

@ -1,30 +0,0 @@
/*
* Copyright (C) 2024 usbharu
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package dev.usbharu.hideout.core.domain.model.filter
interface FilterRepository {
suspend fun generateId(): Long
suspend fun save(filter: Filter): Filter
suspend fun findById(id: Long): Filter?
suspend fun findByUserIdAndId(userId: Long, id: Long): Filter?
suspend fun findByUserIdAndType(userId: Long, types: List<FilterType>): List<Filter>
suspend fun deleteById(id: Long)
suspend fun deleteByUserIdAndId(userId: Long, id: Long)
}

View File

@ -1,26 +0,0 @@
/*
* Copyright (C) 2024 usbharu
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package dev.usbharu.hideout.core.domain.model.filter
@Suppress("EnumEntryNameCase", "EnumNaming")
enum class FilterType {
home,
notifications,
public,
thread,
account
}

View File

@ -1,26 +0,0 @@
/*
* Copyright (C) 2024 usbharu
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package dev.usbharu.hideout.core.domain.model.filterkeyword
import dev.usbharu.hideout.core.domain.model.filter.FilterMode
data class FilterKeyword(
val id: Long,
val filterId: Long,
val keyword: String,
val mode: FilterMode
)

View File

@ -1,26 +0,0 @@
/*
* Copyright (C) 2024 usbharu
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package dev.usbharu.hideout.core.domain.model.filterkeyword
interface FilterKeywordRepository {
suspend fun generateId(): Long
suspend fun save(filterKeyword: FilterKeyword): FilterKeyword
suspend fun saveAll(filterKeywordList: List<FilterKeyword>)
suspend fun findById(id: Long): FilterKeyword?
suspend fun deleteById(id: Long)
suspend fun deleteByFilterId(filterId: Long)
}

View File

@ -1,54 +0,0 @@
/*
* Copyright (C) 2024 usbharu
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package dev.usbharu.hideout.core.domain.model.instance
class Nodeinfo private constructor() {
var links: List<Links> = emptyList()
override fun equals(other: Any?): Boolean {
if (this === other) return true
if (javaClass != other?.javaClass) return false
other as Nodeinfo
return links == other.links
}
override fun hashCode(): Int = links.hashCode()
}
class Links private constructor() {
var rel: String? = null
var href: String? = null
override fun equals(other: Any?): Boolean {
if (this === other) return true
if (javaClass != other?.javaClass) return false
other as Links
if (rel != other.rel) return false
if (href != other.href) return false
return true
}
override fun hashCode(): Int {
var result = rel?.hashCode() ?: 0
result = 31 * result + (href?.hashCode() ?: 0)
return result
}
}

View File

@ -1,87 +0,0 @@
/*
* 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.
*/
@file:Suppress("Filename")
package dev.usbharu.hideout.core.domain.model.instance
@Suppress("ClassNaming")
class Nodeinfo2_0 {
var metadata: Metadata? = null
var software: Software? = null
override fun equals(other: Any?): Boolean {
if (this === other) return true
if (javaClass != other?.javaClass) return false
other as Nodeinfo2_0
if (metadata != other.metadata) return false
if (software != other.software) return false
return true
}
override fun hashCode(): Int {
var result = metadata?.hashCode() ?: 0
result = 31 * result + (software?.hashCode() ?: 0)
return result
}
}
class Metadata {
var nodeName: String? = null
var nodeDescription: String? = null
override fun equals(other: Any?): Boolean {
if (this === other) return true
if (javaClass != other?.javaClass) return false
other as Metadata
if (nodeName != other.nodeName) return false
if (nodeDescription != other.nodeDescription) return false
return true
}
override fun hashCode(): Int {
var result = nodeName?.hashCode() ?: 0
result = 31 * result + (nodeDescription?.hashCode() ?: 0)
return result
}
}
class Software {
var name: String? = null
var version: String? = null
override fun equals(other: Any?): Boolean {
if (this === other) return true
if (javaClass != other?.javaClass) return false
other as Software
if (name != other.name) return false
if (version != other.version) return false
return true
}
override fun hashCode(): Int {
var result = name?.hashCode() ?: 0
result = 31 * result + (version?.hashCode() ?: 0)
return result
}
}

View File

@ -14,7 +14,7 @@
* limitations under the License.
*/
package dev.usbharu.hideout.core.service.media
package dev.usbharu.hideout.core.domain.model.media
enum class FileType {
Image,

View File

@ -16,34 +16,17 @@
package dev.usbharu.hideout.core.domain.model.media
import dev.usbharu.hideout.core.service.media.FileType
import dev.usbharu.hideout.core.service.media.MimeType
import dev.usbharu.hideout.domain.mastodon.model.generated.MediaAttachment
import java.net.URI
data class Media(
val id: Long,
val name: String,
val url: String,
val remoteUrl: String?,
val thumbnailUrl: String?,
val id: MediaId,
val name: MediaName,
val url: URI,
val remoteUrl: URI?,
val thumbnailUrl: URI?,
val type: FileType,
val mimeType: MimeType,
val blurHash: String?,
val description: String? = null
val blurHash: MediaBlurHash?,
val description: MediaDescription? = null,
)
fun Media.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,
previewUrl = thumbnailUrl,
remoteUrl = remoteUrl,
description = description,
blurhash = blurHash,
textUrl = url
)

View File

@ -14,10 +14,7 @@
* limitations under the License.
*/
package dev.usbharu.hideout.core.domain.model.filter
package dev.usbharu.hideout.core.domain.model.media
enum class FilterMode {
WHOLE_WORD,
REGEX,
NONE
}
@JvmInline
value class MediaBlurHash(val hash: String)

View File

@ -14,9 +14,7 @@
* limitations under the License.
*/
package dev.usbharu.hideout.core.service.post
package dev.usbharu.hideout.core.domain.model.media
data class FormattedPostContent(
val html: String,
val content: String
)
@JvmInline
value class MediaDescription(val description: String)

View File

@ -14,6 +14,7 @@
* limitations under the License.
*/
package dev.usbharu.hideout.core.domain.model.meta
package dev.usbharu.hideout.core.domain.model.media
data class Meta(val version: String, val jwt: Jwt)
@JvmInline
value class MediaName(val name: String)

View File

@ -14,6 +14,6 @@
* limitations under the License.
*/
package dev.usbharu.hideout.core.service.media
package dev.usbharu.hideout.core.domain.model.media
data class MimeType(val type: String, val subtype: String, val fileType: FileType)

View File

@ -1,21 +0,0 @@
/*
* Copyright (C) 2024 usbharu
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package dev.usbharu.hideout.core.domain.model.meta
import java.util.*
data class Jwt(val kid: UUID, val privateKey: String, val publicKey: String)

View File

@ -1,27 +0,0 @@
/*
* Copyright (C) 2024 usbharu
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package dev.usbharu.hideout.core.domain.model.meta
import org.springframework.stereotype.Repository
@Repository
interface MetaRepository {
suspend fun save(meta: Meta)
suspend fun get(): Meta?
}

View File

@ -1,30 +0,0 @@
/*
* Copyright (C) 2024 usbharu
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package dev.usbharu.hideout.core.domain.model.notification
import java.time.Instant
data class Notification(
val id: Long,
val type: String,
val userId: Long,
val sourceActorId: Long?,
val postId: Long?,
val text: String?,
val reactionId: Long?,
val createdAt: Instant
)

View File

@ -1,24 +0,0 @@
/*
* Copyright (C) 2024 usbharu
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package dev.usbharu.hideout.core.domain.model.notification
interface NotificationRepository {
suspend fun generateId(): Long
suspend fun save(notification: Notification): Notification
suspend fun findById(id: Long): Notification?
suspend fun deleteById(id: Long)
}

View File

@ -1,21 +0,0 @@
/*
* Copyright (C) 2024 usbharu
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package dev.usbharu.hideout.core.domain.model.reaction
import dev.usbharu.hideout.core.domain.model.emoji.Emoji
data class Reaction(val id: Long, val emoji: Emoji, val postId: Long, val actorId: Long)

View File

@ -1,40 +0,0 @@
/*
* Copyright (C) 2024 usbharu
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package dev.usbharu.hideout.core.domain.model.reaction
import dev.usbharu.hideout.core.domain.model.emoji.Emoji
import org.springframework.stereotype.Repository
@Repository
@Suppress("FunctionMaxLength", "TooManyFunctions")
interface ReactionRepository {
suspend fun generateId(): Long
suspend fun save(reaction: Reaction): Reaction
suspend fun delete(reaction: Reaction): Reaction
suspend fun deleteByPostId(postId: Long): Int
suspend fun deleteByActorId(actorId: Long): Int
suspend fun deleteByPostIdAndActorId(postId: Long, actorId: Long)
suspend fun deleteByPostIdAndActorIdAndEmoji(postId: Long, actorId: Long, emoji: Emoji)
suspend fun findById(id: Long): Reaction?
suspend fun findByPostId(postId: Long): List<Reaction>
suspend fun findByPostIdAndActorIdAndEmojiId(postId: Long, actorId: Long, emojiId: Long): Reaction?
suspend fun existByPostIdAndActorIdAndEmojiId(postId: Long, actorId: Long, emojiId: Long): Boolean
suspend fun existByPostIdAndActorIdAndUnicodeEmoji(postId: Long, actorId: Long, unicodeEmoji: String): Boolean
suspend fun existByPostIdAndActorIdAndEmoji(postId: Long, actorId: Long, emoji: Emoji): Boolean
suspend fun existByPostIdAndActor(postId: Long, actorId: Long): Boolean
suspend fun findByPostIdAndActorId(postId: Long, actorId: Long): List<Reaction>
}

View File

@ -1,38 +0,0 @@
/*
* Copyright (C) 2024 usbharu
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package dev.usbharu.hideout.core.domain.model.relationship
/**
* ユーザーとの関係を表します
*
* @property actorId ユーザー
* @property targetActorId 相手ユーザー
* @property following フォローしているか
* @property blocking ブロックしているか
* @property muting ミュートしているか
* @property followRequest フォローリクエストを送っているか
* @property ignoreFollowRequestToTarget フォローリクエストを無視しているか
*/
data class Relationship(
val actorId: Long,
val targetActorId: Long,
val following: Boolean,
val blocking: Boolean,
val muting: Boolean,
val followRequest: Boolean,
val ignoreFollowRequestToTarget: Boolean
)

View File

@ -1,178 +0,0 @@
/*
* Copyright (C) 2024 usbharu
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package dev.usbharu.hideout.core.domain.model.relationship
import dev.usbharu.hideout.application.infrastructure.exposed.Page
import dev.usbharu.hideout.application.infrastructure.exposed.PaginationList
import dev.usbharu.hideout.application.infrastructure.exposed.withPagination
import dev.usbharu.hideout.core.infrastructure.exposedrepository.AbstractRepository
import org.jetbrains.exposed.dao.id.LongIdTable
import org.jetbrains.exposed.sql.*
import org.jetbrains.exposed.sql.SqlExpressionBuilder.eq
import org.slf4j.Logger
import org.slf4j.LoggerFactory
import org.springframework.stereotype.Service
@Service
class RelationshipRepositoryImpl : RelationshipRepository, AbstractRepository() {
override val logger: Logger
get() = Companion.logger
override suspend fun save(relationship: Relationship): Relationship = query {
val singleOrNull = Relationships.selectAll().where {
(Relationships.actorId eq relationship.actorId).and(
Relationships.targetActorId eq relationship.targetActorId
)
}.forUpdate().singleOrNull()
if (singleOrNull == null) {
Relationships.insert {
it[actorId] = relationship.actorId
it[targetActorId] = relationship.targetActorId
it[following] = relationship.following
it[blocking] = relationship.blocking
it[muting] = relationship.muting
it[followRequest] = relationship.followRequest
it[ignoreFollowRequestFromTarget] = relationship.ignoreFollowRequestToTarget
}
} else {
Relationships.update({
(Relationships.actorId eq relationship.actorId).and(
Relationships.targetActorId eq relationship.targetActorId
)
}) {
it[following] = relationship.following
it[blocking] = relationship.blocking
it[muting] = relationship.muting
it[followRequest] = relationship.followRequest
it[ignoreFollowRequestFromTarget] = relationship.ignoreFollowRequestToTarget
}
}
return@query relationship
}
override suspend fun delete(relationship: Relationship): Unit = query {
Relationships.deleteWhere {
(actorId eq relationship.actorId).and(
targetActorId eq relationship.targetActorId
)
}
}
override suspend fun findByUserIdAndTargetUserId(actorId: Long, targetActorId: Long): Relationship? = query {
return@query Relationships.selectAll()
.where { (Relationships.actorId eq actorId).and(Relationships.targetActorId eq targetActorId) }
.singleOrNull()?.toRelationships()
}
override suspend fun deleteByActorIdOrTargetActorId(actorId: Long, targetActorId: Long): Unit = query {
Relationships.deleteWhere {
Relationships.actorId.eq(actorId).or(Relationships.targetActorId.eq(targetActorId))
}
}
override suspend fun findByTargetIdAndFollowing(targetId: Long, following: Boolean): List<Relationship> = query {
return@query Relationships
.selectAll().where { Relationships.targetActorId eq targetId and (Relationships.following eq following) }
.map { it.toRelationships() }
}
override suspend fun countByTargetIdAndFollowing(targetId: Long, following: Boolean): Int = query {
return@query Relationships
.selectAll()
.where {
Relationships.targetActorId eq targetId and (Relationships.following eq following)
}
.count()
.toInt()
}
override suspend fun countByUserIdAndFollowing(userId: Long, following: Boolean): Int = query {
return@query Relationships
.selectAll()
.where {
Relationships.actorId eq userId and (Relationships.following eq following)
}
.count()
.toInt()
}
override suspend fun findByTargetIdAndFollowRequestAndIgnoreFollowRequest(
targetId: Long,
followRequest: Boolean,
ignoreFollowRequest: Boolean,
page: Page.PageByMaxId,
): PaginationList<Relationship, Long> = query {
val query = Relationships.selectAll().where {
Relationships.targetActorId.eq(targetId).and(Relationships.followRequest.eq(followRequest))
.and(Relationships.ignoreFollowRequestFromTarget.eq(ignoreFollowRequest))
}
val resultRowList = query.withPagination(page, Relationships.id)
return@query PaginationList(
resultRowList.map { it.toRelationships() },
resultRowList.next?.value,
resultRowList.prev?.value
)
}
override suspend fun findByActorIdAndMuting(
actorId: Long,
muting: Boolean,
page: Page.PageByMaxId,
): PaginationList<Relationship, Long> = query {
val query =
Relationships.selectAll().where { Relationships.actorId.eq(actorId).and(Relationships.muting.eq(muting)) }
val resultRowList = query.withPagination(page, Relationships.id)
return@query PaginationList(
resultRowList.map { it.toRelationships() },
resultRowList.next?.value,
resultRowList.prev?.value
)
}
companion object {
private val logger = LoggerFactory.getLogger(RelationshipRepositoryImpl::class.java)
}
}
fun ResultRow.toRelationships(): Relationship = Relationship(
actorId = this[Relationships.actorId],
targetActorId = this[Relationships.targetActorId],
following = this[Relationships.following],
blocking = this[Relationships.blocking],
muting = this[Relationships.muting],
followRequest = this[Relationships.followRequest],
ignoreFollowRequestToTarget = this[Relationships.ignoreFollowRequestFromTarget]
)
object Relationships : LongIdTable("relationships") {
val actorId = long("actor_id").references(Actors.id)
val targetActorId = long("target_actor_id").references(Actors.id)
val following = bool("following")
val blocking = bool("blocking")
val muting = bool("muting")
val followRequest = bool("follow_request")
val ignoreFollowRequestFromTarget = bool("ignore_follow_request")
init {
uniqueIndex(actorId, targetActorId)
}
}

View File

@ -1,37 +0,0 @@
/*
* Copyright (C) 2024 usbharu
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package dev.usbharu.hideout.core.domain.model.shared.domainevent
import java.time.Instant
import java.util.*
data class DomainEvent(
private val id: String,
private val name: String,
private val occurredOn: Instant,
private val body: DomainEventBody,
) {
companion object {
fun create(name: String, body: DomainEventBody): DomainEvent {
return DomainEvent(UUID.randomUUID().toString(), name, Instant.now(), body)
}
fun reconstruct(id: String, name: String, occurredOn: Instant, body: DomainEventBody): DomainEvent {
return DomainEvent(id, name, occurredOn, body)
}
}
}

View File

@ -1,23 +0,0 @@
/*
* Copyright (C) 2024 usbharu
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package dev.usbharu.hideout.core.domain.model.shared.domainevent
abstract class DomainEventBody(val map: Map<String, Any>) {
fun toMap(): Map<String, Any> {
return map
}
}

View File

@ -1,42 +0,0 @@
/*
* Copyright (C) 2024 usbharu
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package dev.usbharu.hideout.core.domain.model.timeline
import dev.usbharu.hideout.core.domain.model.post.Visibility
import org.springframework.data.annotation.Id
import org.springframework.data.mongodb.core.index.CompoundIndex
import org.springframework.data.mongodb.core.mapping.Document
@Document
@CompoundIndex(def = "{'userId':1,'timelineId':1,'postId':1}", unique = true)
data class Timeline(
@Id
val id: Long,
val userId: Long,
val timelineId: Long,
val postId: Long,
val postActorId: Long,
val createdAt: Long,
val replyId: Long?,
val repostId: Long?,
val visibility: Visibility,
val sensitive: Boolean,
val isLocal: Boolean,
val isPureRepost: Boolean = false,
val mediaIds: List<Long> = emptyList(),
val emojiIds: List<Long> = emptyList()
)

View File

@ -1,25 +0,0 @@
/*
* Copyright (C) 2024 usbharu
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package dev.usbharu.hideout.core.domain.model.timeline
interface TimelineRepository {
suspend fun generateId(): Long
suspend fun save(timeline: Timeline): Timeline
suspend fun saveAll(timelines: List<Timeline>): List<Timeline>
suspend fun findByUserId(id: Long): List<Timeline>
suspend fun findByUserIdAndTimelineId(userId: Long, timelineId: Long): List<Timeline>
}

View File

@ -18,8 +18,6 @@ package dev.usbharu.hideout.core.infrastructure.exposedrepository
import dev.usbharu.hideout.application.service.id.IdGenerateService
import dev.usbharu.hideout.core.domain.model.post.Visibility
import dev.usbharu.hideout.core.domain.model.timeline.Timeline
import dev.usbharu.hideout.core.domain.model.timeline.TimelineRepository
import org.jetbrains.exposed.sql.*
import org.slf4j.Logger
import org.slf4j.LoggerFactory

View File

@ -17,10 +17,9 @@
package dev.usbharu.hideout.core.infrastructure.exposedrepository
import dev.usbharu.hideout.application.service.id.IdGenerateService
import dev.usbharu.hideout.core.domain.model.media.FileType
import dev.usbharu.hideout.core.domain.model.media.MediaRepository
import dev.usbharu.hideout.core.infrastructure.exposedrepository.Media.mimeType
import dev.usbharu.hideout.core.service.media.FileType
import dev.usbharu.hideout.core.service.media.MimeType
import dev.usbharu.hideout.core.domain.model.media.MimeType
import org.jetbrains.exposed.sql.*
import org.jetbrains.exposed.sql.SqlExpressionBuilder.eq
import org.slf4j.Logger

View File

@ -16,8 +16,6 @@
package dev.usbharu.hideout.core.infrastructure.exposedrepository
import dev.usbharu.hideout.core.domain.model.meta.Jwt
import dev.usbharu.hideout.core.domain.model.meta.MetaRepository
import org.jetbrains.exposed.sql.*
import org.springframework.stereotype.Repository
import java.util.*

View File

@ -1,232 +0,0 @@
/*
* 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.exposedrepository
import dev.usbharu.hideout.application.service.id.IdGenerateService
import dev.usbharu.hideout.core.domain.model.emoji.CustomEmoji
import dev.usbharu.hideout.core.domain.model.emoji.Emoji
import dev.usbharu.hideout.core.domain.model.emoji.UnicodeEmoji
import dev.usbharu.hideout.core.domain.model.reaction.Reaction
import dev.usbharu.hideout.core.domain.model.reaction.ReactionRepository
import org.jetbrains.exposed.dao.id.LongIdTable
import org.jetbrains.exposed.sql.*
import org.jetbrains.exposed.sql.SqlExpressionBuilder.eq
import org.slf4j.Logger
import org.slf4j.LoggerFactory
import org.springframework.stereotype.Repository
@Repository
class ReactionRepositoryImpl(
private val idGenerateService: IdGenerateService
) : ReactionRepository, AbstractRepository() {
override val logger: Logger
get() = Companion.logger
override suspend fun generateId(): Long = idGenerateService.generateId()
override suspend fun save(reaction: Reaction): Reaction = query {
if (Reactions.selectAll().where { Reactions.id eq reaction.id }.forUpdate().empty()) {
Reactions.insert {
it[id] = reaction.id
if (reaction.emoji is CustomEmoji) {
it[customEmojiId] = reaction.emoji.id
it[unicodeEmoji] = null
} else {
it[customEmojiId] = null
it[unicodeEmoji] = reaction.emoji.name
}
it[postId] = reaction.postId
it[actorId] = reaction.actorId
}
} else {
Reactions.update({ Reactions.id eq reaction.id }) {
if (reaction.emoji is CustomEmoji) {
it[customEmojiId] = reaction.emoji.id
it[unicodeEmoji] = null
} else {
it[customEmojiId] = null
it[unicodeEmoji] = reaction.emoji.name
}
it[postId] = reaction.postId
it[actorId] = reaction.actorId
}
}
return@query reaction
}
override suspend fun delete(reaction: Reaction): Reaction = query {
if (reaction.emoji is CustomEmoji) {
Reactions.deleteWhere {
id.eq(reaction.id).and(postId.eq(reaction.postId)).and(actorId.eq(reaction.actorId))
.and(customEmojiId.eq(reaction.emoji.id))
}
} else {
Reactions.deleteWhere {
id.eq(reaction.id).and(postId.eq(reaction.postId)).and(actorId.eq(reaction.actorId))
.and(unicodeEmoji.eq(reaction.emoji.name))
}
}
return@query reaction
}
override suspend fun deleteByPostId(postId: Long): Int = query {
return@query Reactions.deleteWhere {
Reactions.postId eq postId
}
}
override suspend fun deleteByActorId(actorId: Long): Int = query {
return@query Reactions.deleteWhere {
Reactions.actorId eq actorId
}
}
override suspend fun deleteByPostIdAndActorId(postId: Long, actorId: Long): Unit = query {
Reactions.deleteWhere {
Reactions.postId eq postId and (Reactions.actorId eq actorId)
}
}
override suspend fun deleteByPostIdAndActorIdAndEmoji(postId: Long, actorId: Long, emoji: Emoji): Unit = query {
if (emoji is CustomEmoji) {
Reactions.deleteWhere {
Reactions.postId.eq(postId)
.and(Reactions.actorId.eq(actorId))
.and(customEmojiId.eq(emoji.id))
}
} else {
Reactions.deleteWhere {
Reactions.postId.eq(postId)
.and(Reactions.actorId.eq(actorId))
.and(unicodeEmoji.eq(emoji.name))
}
}
}
override suspend fun findById(id: Long): Reaction? = query {
return@query Reactions.leftJoin(CustomEmojis).selectAll().where { Reactions.id eq id }.singleOrNull()
?.toReaction()
}
override suspend fun findByPostId(postId: Long): List<Reaction> = query {
return@query Reactions.leftJoin(CustomEmojis).selectAll().where { Reactions.postId eq postId }
.map { it.toReaction() }
}
override suspend fun findByPostIdAndActorIdAndEmojiId(postId: Long, actorId: Long, emojiId: Long): Reaction? =
query {
return@query Reactions.leftJoin(CustomEmojis).selectAll().where {
Reactions.postId eq postId and (Reactions.actorId eq actorId).and(
Reactions.customEmojiId.eq<Long?>(
emojiId
)
)
}.singleOrNull()?.toReaction()
}
override suspend fun existByPostIdAndActorIdAndEmojiId(postId: Long, actorId: Long, emojiId: Long): Boolean =
query {
return@query Reactions.selectAll().where {
Reactions.postId
.eq<Long>(postId)
.and(Reactions.actorId.eq<Long>(actorId))
.and(Reactions.customEmojiId.eq<Long?>(emojiId))
}.empty().not()
}
override suspend fun existByPostIdAndActorIdAndUnicodeEmoji(
postId: Long,
actorId: Long,
unicodeEmoji: String
): Boolean = query {
return@query Reactions.selectAll().where {
Reactions.postId
.eq<Long>(postId)
.and(Reactions.actorId.eq<Long>(actorId))
.and(Reactions.unicodeEmoji.eq<String?>(unicodeEmoji))
}.empty().not()
}
override suspend fun existByPostIdAndActorIdAndEmoji(postId: Long, actorId: Long, emoji: Emoji): Boolean = query {
val query = Reactions.selectAll().where {
Reactions.postId
.eq(postId)
.and(Reactions.actorId.eq(actorId))
}
if (emoji is UnicodeEmoji) {
query.andWhere { Reactions.unicodeEmoji eq emoji.name }
} else {
emoji as CustomEmoji
query.andWhere { Reactions.customEmojiId eq emoji.id }
}
return@query query.empty().not()
}
override suspend fun existByPostIdAndActor(postId: Long, actorId: Long): Boolean = query {
Reactions.selectAll().where { Reactions.postId.eq(postId).and(Reactions.actorId.eq(actorId)) }.empty().not()
}
override suspend fun findByPostIdAndActorId(postId: Long, actorId: Long): List<Reaction> = query {
return@query Reactions.leftJoin(CustomEmojis)
.selectAll().where { Reactions.postId eq postId and (Reactions.actorId eq actorId) }
.map { it.toReaction() }
}
companion object {
private val logger = LoggerFactory.getLogger(ReactionRepositoryImpl::class.java)
}
}
fun ResultRow.toReaction(): Reaction {
val emoji = if (this[Reactions.customEmojiId] != null) {
CustomEmoji(
id = this[Reactions.customEmojiId]!!,
name = this[CustomEmojis.name],
domain = this[CustomEmojis.domain],
instanceId = this[CustomEmojis.instanceId],
url = this[CustomEmojis.url],
category = this[CustomEmojis.category],
createdAt = this[CustomEmojis.createdAt]
)
} else if (this[Reactions.unicodeEmoji] != null) {
UnicodeEmoji(this[Reactions.unicodeEmoji]!!)
} else {
throw IllegalStateException("customEmojiId and unicodeEmoji is null.")
}
return Reaction(
this[Reactions.id].value,
emoji,
this[Reactions.postId],
this[Reactions.actorId]
)
}
object Reactions : LongIdTable("reactions") {
val customEmojiId = long("custom_emoji_id").references(CustomEmojis.id).nullable()
val unicodeEmoji = varchar("unicode_emoji", 255).nullable()
val postId: Column<Long> =
long("post_id").references(Posts.id, onDelete = ReferenceOption.CASCADE, onUpdate = ReferenceOption.CASCADE)
val actorId: Column<Long> =
long("actor_id").references(Actors.id, onDelete = ReferenceOption.CASCADE, onUpdate = ReferenceOption.CASCADE)
init {
uniqueIndex(customEmojiId, postId, actorId)
}
}

View File

@ -1,35 +0,0 @@
/*
* 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.mongorepository
import dev.usbharu.hideout.core.domain.model.timeline.Timeline
import org.springframework.data.domain.Pageable
import org.springframework.data.mongodb.repository.MongoRepository
@Suppress("LongParameterList", "FunctionMaxLength")
interface MongoTimelineRepository : MongoRepository<Timeline, Long> {
fun findByUserId(id: Long): List<Timeline>
fun findByUserIdAndTimelineId(userId: Long, timelineId: Long): List<Timeline>
fun findByUserIdAndTimelineIdAndPostIdBetweenAndIsLocal(
userId: Long?,
timelineId: Long?,
postIdMin: Long?,
postIdMax: Long?,
isLocal: Boolean?,
pageable: Pageable
): List<Timeline>
}

View File

@ -1,67 +0,0 @@
/*
* 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.mongorepository
import dev.usbharu.hideout.application.service.id.IdGenerateService
import dev.usbharu.hideout.core.domain.exception.resource.DuplicateException
import dev.usbharu.hideout.core.domain.exception.resource.ResourceAccessException
import dev.usbharu.hideout.core.domain.model.timeline.Timeline
import dev.usbharu.hideout.core.domain.model.timeline.TimelineRepository
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty
import org.springframework.dao.DataAccessException
import org.springframework.dao.DuplicateKeyException
import org.springframework.stereotype.Repository
@Repository
@ConditionalOnProperty("hideout.use-mongodb", havingValue = "true", matchIfMissing = false)
class MongoTimelineRepositoryWrapper(
private val mongoTimelineRepository: MongoTimelineRepository,
private val idGenerateService: IdGenerateService
) :
TimelineRepository {
override suspend fun generateId(): Long = idGenerateService.generateId()
override suspend fun save(timeline: Timeline): Timeline {
return withContext(Dispatchers.IO) {
mongoTimelineRepository.save(timeline)
}
}
override suspend fun saveAll(timelines: List<Timeline>): List<Timeline> {
try {
return mongoTimelineRepository.saveAll(timelines)
} catch (e: DuplicateKeyException) {
throw DuplicateException("Timeline duplicate.", e)
} catch (e: DataAccessException) {
throw ResourceAccessException(e)
}
}
override suspend fun findByUserId(id: Long): List<Timeline> {
return withContext(Dispatchers.IO) {
mongoTimelineRepository.findByUserId(id)
}
}
override suspend fun findByUserIdAndTimelineId(userId: Long, timelineId: Long): List<Timeline> {
return withContext(Dispatchers.IO) {
mongoTimelineRepository.findByUserIdAndTimelineId(userId, timelineId)
}
}
}

View File

@ -16,7 +16,7 @@
package dev.usbharu.hideout.core.infrastructure.springframework.httpsignature
import dev.usbharu.hideout.application.external.Transaction
import dev.usbharu.hideout.core.application.shared.Transaction
import dev.usbharu.hideout.core.domain.exception.HttpSignatureVerifyException
import dev.usbharu.hideout.util.RsaUtil
import dev.usbharu.httpsignature.common.HttpMethod

View File

@ -16,7 +16,7 @@
package dev.usbharu.hideout.core.infrastructure.springframework.oauth2
import dev.usbharu.hideout.application.external.Transaction
import dev.usbharu.hideout.core.application.shared.Transaction
import kotlinx.coroutines.runBlocking
import org.jetbrains.exposed.sql.*
import org.jetbrains.exposed.sql.SqlExpressionBuilder.eq

View File

@ -19,7 +19,7 @@ package dev.usbharu.hideout.core.infrastructure.springframework.oauth2
import com.fasterxml.jackson.databind.ObjectMapper
import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule
import com.fasterxml.jackson.module.kotlin.readValue
import dev.usbharu.hideout.application.external.Transaction
import dev.usbharu.hideout.core.application.shared.Transaction
import kotlinx.coroutines.runBlocking
import org.jetbrains.exposed.sql.*
import org.jetbrains.exposed.sql.SqlExpressionBuilder.eq

View File

@ -17,7 +17,7 @@
package dev.usbharu.hideout.core.infrastructure.springframework.oauth2
import dev.usbharu.hideout.application.config.ApplicationConfig
import dev.usbharu.hideout.application.external.Transaction
import dev.usbharu.hideout.core.application.shared.Transaction
import dev.usbharu.hideout.core.domain.exception.resource.UserNotFoundException
import dev.usbharu.hideout.core.domain.model.userdetails.UserDetailRepository
import kotlinx.coroutines.runBlocking

View File

@ -40,13 +40,7 @@ class AuthController(
@PostMapping("/auth/sign_up")
suspend fun signUp(@Validated @ModelAttribute signUpForm: SignUpForm): String {
val registerAccount = authApiService.registerAccount(
RegisterAccountDto(
signUpForm.username,
signUpForm.password,
signUpForm.recaptchaResponse
)
)
return "redirect:" + registerAccount.url
}

View File

@ -1,76 +0,0 @@
/*
* 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.query.model
import dev.usbharu.hideout.core.domain.model.filter.FilterType
import dev.usbharu.hideout.core.infrastructure.exposedrepository.FilterKeywords
import dev.usbharu.hideout.core.infrastructure.exposedrepository.Filters
import dev.usbharu.hideout.core.infrastructure.exposedrepository.toFilter
import dev.usbharu.hideout.core.infrastructure.exposedrepository.toFilterKeyword
import org.jetbrains.exposed.sql.Query
import org.jetbrains.exposed.sql.and
import org.jetbrains.exposed.sql.selectAll
import org.springframework.stereotype.Repository
@Repository
class ExposedFilterQueryService : FilterQueryService {
override suspend fun findByUserIdAndType(userId: Long, types: List<FilterType>): List<FilterQueryModel> {
return Filters
.rightJoin(FilterKeywords)
.selectAll()
.where { Filters.userId eq userId }
.toFilterQueryModel()
}
override suspend fun findByUserId(userId: Long): List<FilterQueryModel> {
return Filters
.rightJoin(FilterKeywords)
.selectAll()
.where { Filters.userId eq userId }
.toFilterQueryModel()
}
override suspend fun findByUserIdAndId(userId: Long, id: Long): FilterQueryModel? {
return Filters
.leftJoin(FilterKeywords)
.selectAll()
.where { Filters.userId eq userId and (Filters.id eq id) }
.toFilterQueryModel()
.firstOrNull()
}
override suspend fun findByUserIdAndKeywordId(userId: Long, keywordId: Long): FilterQueryModel? {
return Filters
.leftJoin(FilterKeywords)
.selectAll()
.where { Filters.userId eq userId and (FilterKeywords.id eq keywordId) }
.toFilterQueryModel()
.firstOrNull()
}
private fun Query.toFilterQueryModel(): List<FilterQueryModel> {
return this
.groupBy { it[Filters.id] }
.map { it.value }
.map {
FilterQueryModel.of(
it.first().toFilter(),
it.map { resultRow -> resultRow.toFilterKeyword() }
)
}
}
}

View File

@ -1,43 +0,0 @@
/*
* 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.query.model
import dev.usbharu.hideout.core.domain.model.filter.Filter
import dev.usbharu.hideout.core.domain.model.filter.FilterAction
import dev.usbharu.hideout.core.domain.model.filter.FilterType
import dev.usbharu.hideout.core.domain.model.filterkeyword.FilterKeyword
data class FilterQueryModel(
val id: Long,
val userId: Long,
val name: String,
val context: List<FilterType>,
val filterAction: FilterAction,
val keywords: List<FilterKeyword>
) {
companion object {
@Suppress("FunctionMinLength")
fun of(filter: Filter, keywords: List<FilterKeyword>): FilterQueryModel = FilterQueryModel(
id = filter.id,
userId = filter.userId,
name = filter.name,
context = filter.context,
filterAction = filter.filterAction,
keywords = keywords
)
}
}

View File

@ -1,26 +0,0 @@
/*
* 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.query.model
import dev.usbharu.hideout.core.domain.model.filter.FilterType
interface FilterQueryService {
suspend fun findByUserIdAndType(userId: Long, types: List<FilterType>): List<FilterQueryModel>
suspend fun findByUserId(userId: Long): List<FilterQueryModel>
suspend fun findByUserIdAndId(userId: Long, id: Long): FilterQueryModel?
suspend fun findByUserIdAndKeywordId(userId: Long, keywordId: Long): FilterQueryModel?
}

View File

@ -1,24 +0,0 @@
/*
* 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.service.filter
import dev.usbharu.hideout.core.domain.model.filter.FilterMode
data class FilterKeyword(
val keyword: String,
val mode: FilterMode
)

View File

@ -1,24 +0,0 @@
/*
* 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.service.filter
import dev.usbharu.hideout.core.query.model.FilterQueryModel
data class FilterResult(
val filter: FilterQueryModel,
val keyword: String,
)

View File

@ -1,33 +0,0 @@
/*
* 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.service.filter
import dev.usbharu.hideout.core.domain.model.filter.FilterAction
import dev.usbharu.hideout.core.domain.model.filter.FilterType
import dev.usbharu.hideout.core.query.model.FilterQueryModel
interface MuteService {
suspend fun createFilter(
title: String,
context: List<FilterType>,
action: FilterAction,
keywords: List<FilterKeyword>,
loginUser: Long
): FilterQueryModel
suspend fun getFilters(userId: Long, types: List<FilterType> = emptyList()): List<FilterQueryModel>
}

View File

@ -1,66 +0,0 @@
/*
* 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.service.filter
import dev.usbharu.hideout.core.domain.model.filter.Filter
import dev.usbharu.hideout.core.domain.model.filter.FilterAction
import dev.usbharu.hideout.core.domain.model.filter.FilterRepository
import dev.usbharu.hideout.core.domain.model.filter.FilterType
import dev.usbharu.hideout.core.domain.model.filterkeyword.FilterKeywordRepository
import dev.usbharu.hideout.core.query.model.FilterQueryModel
import dev.usbharu.hideout.core.query.model.FilterQueryService
import org.springframework.stereotype.Service
@Service
class MuteServiceImpl(
private val filterRepository: FilterRepository,
private val filterKeywordRepository: FilterKeywordRepository,
private val filterQueryService: FilterQueryService
) : MuteService {
override suspend fun createFilter(
title: String,
context: List<FilterType>,
action: FilterAction,
keywords: List<FilterKeyword>,
loginUser: Long
): FilterQueryModel {
val filter = Filter(
filterRepository.generateId(),
loginUser,
title,
context,
action
)
val filterKeywordList = keywords.map {
dev.usbharu.hideout.core.domain.model.filterkeyword.FilterKeyword(
filterKeywordRepository.generateId(),
filter.id,
it.keyword,
it.mode
)
}
val savedFilter = filterRepository.save(filter)
filterKeywordRepository.saveAll(filterKeywordList)
return FilterQueryModel.of(savedFilter, filterKeywordList)
}
override suspend fun getFilters(userId: Long, types: List<FilterType>): List<FilterQueryModel> =
filterQueryService.findByUserIdAndType(userId, types)
}

View File

@ -1,27 +0,0 @@
/*
* 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.service.instance
data class InstanceCreateDto(
val name: String?,
val description: String?,
val url: String,
val iconUrl: String,
val sharedInbox: String?,
val software: String?,
val version: String?,
)

View File

@ -1,124 +0,0 @@
/*
* 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.service.instance
import com.fasterxml.jackson.databind.ObjectMapper
import dev.usbharu.hideout.core.domain.model.instance.Instance
import dev.usbharu.hideout.core.domain.model.instance.InstanceRepository
import dev.usbharu.hideout.core.domain.model.instance.Nodeinfo
import dev.usbharu.hideout.core.domain.model.instance.Nodeinfo2_0
import dev.usbharu.hideout.core.service.resource.ResourceResolveService
import org.slf4j.LoggerFactory
import org.springframework.beans.factory.annotation.Qualifier
import org.springframework.stereotype.Service
import java.net.URL
import java.time.Instant
interface InstanceService {
suspend fun fetchInstance(url: String, sharedInbox: String? = null): Instance
suspend fun createNewInstance(instanceCreateDto: InstanceCreateDto): Instance
}
@Service
class InstanceServiceImpl(
private val instanceRepository: InstanceRepository,
private val resourceResolveService: ResourceResolveService,
@Qualifier("activitypub") private val objectMapper: ObjectMapper,
) : InstanceService {
override suspend fun fetchInstance(url: String, sharedInbox: String?): Instance {
val u = URL(url)
val resolveInstanceUrl = u.protocol + "://" + u.host
val instance = instanceRepository.findByUrl(resolveInstanceUrl)
if (instance != null) {
return instance
}
logger.info("Instance not found. try fetch instance info. url: {}", resolveInstanceUrl)
@Suppress("TooGenericExceptionCaught")
try {
val nodeinfoJson = resourceResolveService.resolve("$resolveInstanceUrl/.well-known/nodeinfo").bodyAsText()
val nodeinfo = objectMapper.readValue(nodeinfoJson, Nodeinfo::class.java)
val nodeinfoPathMap = nodeinfo.links.associate { it.rel to it.href }
for ((key, value) in nodeinfoPathMap) {
when (key) {
"http://nodeinfo.diaspora.software/ns/schema/2.0",
"http://nodeinfo.diaspora.software/ns/schema/2.1",
-> {
val nodeinfo20 = objectMapper.readValue(
resourceResolveService.resolve(value!!).bodyAsText(),
Nodeinfo2_0::class.java
)
val instanceCreateDto = InstanceCreateDto(
name = nodeinfo20.metadata?.nodeName,
description = nodeinfo20.metadata?.nodeDescription,
url = resolveInstanceUrl,
iconUrl = "$resolveInstanceUrl/favicon.ico",
sharedInbox = sharedInbox,
software = nodeinfo20.software?.name,
version = nodeinfo20.software?.version
)
return createNewInstance(instanceCreateDto)
}
else -> {
throw IllegalStateException("Unknown nodeinfo versions: $key url: $value")
}
}
}
} catch (e: Exception) {
logger.warn("FAILED Fetch Instance", e)
}
return createNewInstance(
InstanceCreateDto(
name = null,
description = null,
url = resolveInstanceUrl,
iconUrl = "$resolveInstanceUrl/favicon.ico",
sharedInbox = null,
software = null,
version = null
)
)
}
override suspend fun createNewInstance(instanceCreateDto: InstanceCreateDto): Instance {
val instance = Instance(
id = instanceRepository.generateId(),
name = instanceCreateDto.name ?: instanceCreateDto.url,
description = instanceCreateDto.description.orEmpty(),
url = instanceCreateDto.url,
iconUrl = instanceCreateDto.iconUrl,
sharedInbox = instanceCreateDto.sharedInbox,
software = instanceCreateDto.software ?: "unknown",
version = instanceCreateDto.version ?: "unknown",
isBlocked = false,
isMuted = false,
moderationNote = "",
createdAt = Instant.now()
)
instanceRepository.save(instance)
return instance
}
companion object {
private val logger = LoggerFactory.getLogger(InstanceServiceImpl::class.java)
}
}

View File

@ -1,105 +0,0 @@
/*
* 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.service.media
import org.apache.tika.Tika
import org.slf4j.LoggerFactory
import org.springframework.stereotype.Component
import java.nio.file.Path
@Component
class ApatcheTikaFileTypeDeterminationService : FileTypeDeterminationService {
override fun fileType(
byteArray: ByteArray,
filename: String,
contentType: String?
): MimeType {
logger.info("START Detect file type name: {}", filename)
val tika = Tika()
val detect = try {
tika.detect(byteArray, filename)
} catch (e: IllegalStateException) {
logger.warn("FAILED Detect file type", e)
"application/octet-stream"
}
val type = detect.substringBefore("/")
val fileType = when (type) {
"image" -> {
FileType.Image
}
"video" -> {
FileType.Video
}
"audio" -> {
FileType.Audio
}
else -> {
FileType.Unknown
}
}
val mimeType = MimeType(type, detect.substringAfter("/"), fileType)
logger.info("SUCCESS Detect file type name: {},MimeType: {}", filename, mimeType)
return mimeType
}
override fun fileType(path: Path, filename: String): MimeType {
logger.info("START Detect file type name: {}", filename)
val tika = Tika()
val detect = try {
tika.detect(path)
} catch (e: IllegalStateException) {
logger.warn("FAILED Detect file type", e)
"application/octet-stream"
}
val type = detect.substringBefore("/")
val fileType = when (type) {
"image" -> {
FileType.Image
}
"video" -> {
FileType.Video
}
"audio" -> {
FileType.Audio
}
else -> {
FileType.Unknown
}
}
val mimeType = MimeType(type, detect.substringAfter("/"), fileType)
logger.info("SUCCESS Detect file type name: {},MimeType: {}", filename, mimeType)
return mimeType
}
companion object {
private val logger = LoggerFactory.getLogger(ApatcheTikaFileTypeDeterminationService::class.java)
}
}

View File

@ -1,24 +0,0 @@
/*
* 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.service.media
import java.nio.file.Path
interface FileTypeDeterminationService {
fun fileType(byteArray: ByteArray, filename: String, contentType: String?): MimeType
fun fileType(path: Path, filename: String): MimeType
}

View File

@ -1,131 +0,0 @@
/*
* 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.service.media
import dev.usbharu.hideout.application.config.ApplicationConfig
import dev.usbharu.hideout.application.config.LocalStorageConfig
import org.slf4j.LoggerFactory
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty
import org.springframework.stereotype.Service
import java.nio.file.Path
import java.nio.file.StandardOpenOption
import kotlin.io.path.copyTo
import kotlin.io.path.createDirectories
import kotlin.io.path.deleteIfExists
import kotlin.io.path.outputStream
/**
* ローカルファイルシステムにメディアを保存します
*
* @constructor
* ApplicationConfigとLocalStorageConfigをもとに作成
*
* @param applicationConfig ApplicationConfig
* @param localStorageConfig LocalStorageConfig
*/
@Service
@ConditionalOnProperty("hideout.storage.type", havingValue = "local", matchIfMissing = true)
class LocalFileSystemMediaDataStore(
applicationConfig: ApplicationConfig,
localStorageConfig: LocalStorageConfig
) : MediaDataStore {
private val savePath: Path = Path.of(localStorageConfig.path).toAbsolutePath()
private val publicUrl = localStorageConfig.publicUrl ?: "${applicationConfig.url}/files/"
init {
savePath.createDirectories()
}
@Suppress("NestedBlockDepth")
override suspend fun save(dataMediaSave: MediaSave): SavedMedia {
val fileSavePath = buildSavePath(savePath, dataMediaSave.name)
val thumbnailSavePath = buildSavePath(savePath, "thumbnail-" + dataMediaSave.name)
dataMediaSave.thumbnailInputStream?.inputStream()?.use {
it.buffered().use { bufferedInputStream ->
thumbnailSavePath.outputStream(StandardOpenOption.CREATE_NEW, StandardOpenOption.WRITE)
.use { outputStream ->
outputStream.buffered().use {
bufferedInputStream.transferTo(it)
}
}
}
}
dataMediaSave.fileInputStream.inputStream().use {
it.buffered().use { bufferedInputStream ->
fileSavePath.outputStream(StandardOpenOption.CREATE_NEW, StandardOpenOption.WRITE)
.use { outputStream -> outputStream.buffered().use { bufferedInputStream.transferTo(it) } }
}
}
return SuccessSavedMedia(
dataMediaSave.name,
publicUrl + dataMediaSave.name,
publicUrl + "thumbnail-" + dataMediaSave.name
)
}
override suspend fun save(dataSaveRequest: MediaSaveRequest): SavedMedia {
logger.info("START Media upload. {}", dataSaveRequest.name)
val fileSavePath = buildSavePath(savePath, dataSaveRequest.name)
val thumbnailSavePath = buildSavePath(savePath, "thumbnail-" + dataSaveRequest.name)
val fileSavePathString = fileSavePath.toAbsolutePath().toString()
logger.info("MEDIA save. path: {}", fileSavePathString)
@Suppress("TooGenericExceptionCaught")
try {
dataSaveRequest.filePath.copyTo(fileSavePath)
dataSaveRequest.thumbnailPath?.copyTo(thumbnailSavePath)
} catch (e: Exception) {
logger.warn("FAILED to Save the media.", e)
return FaildSavedMedia("FAILED to Save the media.", "Failed copy to path: $fileSavePathString", e)
}
logger.info("SUCCESS Media upload. {}", dataSaveRequest.name)
return SuccessSavedMedia(
dataSaveRequest.name,
publicUrl + dataSaveRequest.name,
publicUrl + "thumbnail-" + dataSaveRequest.name
)
}
/**
* メディアを削除しますサムネイルも削除されます
*
* @param id 削除するメディアのid [SuccessSavedMedia.name]を指定します
*/
override suspend fun delete(id: String) {
logger.info("START Media delete. id: {}", id)
@Suppress("TooGenericExceptionCaught")
try {
buildSavePath(savePath, id).deleteIfExists()
buildSavePath(savePath, "thumbnail-$id").deleteIfExists()
} catch (e: Exception) {
logger.warn("FAILED Media delete. id: {}", id, e)
}
}
private fun buildSavePath(savePath: Path, name: String): Path = savePath.resolve(name)
companion object {
private val logger = LoggerFactory.getLogger(LocalFileSystemMediaDataStore::class.java)
}
}

View File

@ -1,23 +0,0 @@
/*
* 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.service.media
import java.awt.image.BufferedImage
interface MediaBlurhashService {
fun generateBlurhash(bufferedImage: BufferedImage): String
}

View File

@ -1,26 +0,0 @@
/*
* 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.service.media
import io.trbl.blurhash.BlurHash
import org.springframework.stereotype.Service
import java.awt.image.BufferedImage
@Service
class MediaBlurhashServiceImpl : MediaBlurhashService {
override fun generateBlurhash(bufferedImage: BufferedImage): String = BlurHash.encode(bufferedImage)
}

View File

@ -1,47 +0,0 @@
/*
* 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.service.media
/**
* メディアを保存するインタフェース
*
*/
interface MediaDataStore {
/**
* InputStreamを使用してメディアを保存します
*
* @param dataMediaSave FileとThumbnailのinputStream
* @return 保存されたメディア
*/
suspend fun save(dataMediaSave: MediaSave): SavedMedia
/**
* 一時ファイルのパスを使用してメディアを保存します
*
* @param dataSaveRequest FileとThumbnailのパス
* @return 保存されたメディア
*/
suspend fun save(dataSaveRequest: MediaSaveRequest): SavedMedia
/**
* メディアを削除します
* 実装はサムネイルメタデータなども削除するべきです
*
* @param id 削除するメディアのid 通常は[SuccessSavedMedia.name]を指定します
*/
suspend fun delete(id: String)
}

View File

@ -1,30 +0,0 @@
/*
* 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.service.media
interface MediaFileRenameService {
/**
* メディアをリネームします
*
* @param uploadName アップロードされた時点でのファイル名
* @param uploadMimeType アップロードされた時点でのMimeType
* @param processedName 処理後のファイル名
* @param processedMimeType 処理後のMimeType
* @return リネーム後のファイル名
*/
fun rename(uploadName: String, uploadMimeType: MimeType, processedName: String, processedMimeType: MimeType): String
}

View File

@ -1,49 +0,0 @@
/*
* 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.service.media
data class MediaSave(
val name: String,
val prefix: String,
val fileInputStream: ByteArray,
val thumbnailInputStream: ByteArray?
) {
override fun equals(other: Any?): Boolean {
if (this === other) return true
if (javaClass != other?.javaClass) return false
other as MediaSave
if (name != other.name) return false
if (prefix != other.prefix) return false
if (!fileInputStream.contentEquals(other.fileInputStream)) return false
if (thumbnailInputStream != null) {
if (other.thumbnailInputStream == null) return false
if (!thumbnailInputStream.contentEquals(other.thumbnailInputStream)) return false
} else if (other.thumbnailInputStream != null) return false
return true
}
override fun hashCode(): Int {
var result = name.hashCode()
result = 31 * result + prefix.hashCode()
result = 31 * result + fileInputStream.contentHashCode()
result = 31 * result + (thumbnailInputStream?.contentHashCode() ?: 0)
return result
}
}

View File

@ -1,26 +0,0 @@
/*
* 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.service.media
import java.nio.file.Path
data class MediaSaveRequest(
val name: String,
val preffix: String,
val filePath: Path,
val thumbnailPath: Path?
)

View File

@ -1,25 +0,0 @@
/*
* 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.service.media
import dev.usbharu.hideout.core.domain.model.media.Media
import dev.usbharu.hideout.mastodon.interfaces.api.media.MediaRequest
interface MediaService {
suspend fun uploadLocalMedia(mediaRequest: MediaRequest): Media
suspend fun uploadRemoteMedia(remoteMedia: RemoteMedia): Media
}

View File

@ -1,218 +0,0 @@
/*
* 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.service.media
import dev.usbharu.hideout.core.domain.exception.media.MediaSaveException
import dev.usbharu.hideout.core.domain.exception.media.UnsupportedMediaException
import dev.usbharu.hideout.core.domain.model.media.Media
import dev.usbharu.hideout.core.domain.model.media.MediaRepository
import dev.usbharu.hideout.core.service.media.converter.MediaProcessService
import dev.usbharu.hideout.mastodon.interfaces.api.media.MediaRequest
import dev.usbharu.hideout.util.withDelete
import org.slf4j.LoggerFactory
import org.springframework.stereotype.Service
import java.nio.file.Files
import javax.imageio.ImageIO
import dev.usbharu.hideout.core.domain.model.media.Media as EntityMedia
@Service
@Suppress("TooGenericExceptionCaught")
class MediaServiceImpl(
private val mediaDataStore: MediaDataStore,
private val fileTypeDeterminationService: FileTypeDeterminationService,
private val mediaBlurhashService: MediaBlurhashService,
private val mediaRepository: MediaRepository,
private val mediaProcessServices: List<MediaProcessService>,
private val remoteMediaDownloadService: RemoteMediaDownloadService,
private val renameService: MediaFileRenameService
) : MediaService {
@Suppress("LongMethod", "NestedBlockDepth")
override suspend fun uploadLocalMedia(mediaRequest: MediaRequest): EntityMedia {
val fileName = mediaRequest.file.name
logger.info(
"Media upload. filename:$fileName " +
"contentType:${mediaRequest.file.contentType}"
)
val tempFile = Files.createTempFile("hideout-tmp-file", ".tmp")
tempFile.withDelete().use {
Files.newOutputStream(tempFile).use { outputStream ->
mediaRequest.file.inputStream.use {
it.transferTo(outputStream)
}
}
val mimeType = fileTypeDeterminationService.fileType(tempFile, fileName)
val process = findMediaProcessor(mimeType).process(
mimeType,
fileName,
tempFile,
null
)
val dataMediaSave = MediaSaveRequest(
renameService.rename(
mediaRequest.file.name,
mimeType,
process.filePath.fileName.toString(),
process.fileMimeType
),
"",
process.filePath,
process.thumbnailPath
)
dataMediaSave.filePath.withDelete().use {
dataMediaSave.thumbnailPath.withDelete().use {
val save = try {
mediaDataStore.save(dataMediaSave)
} catch (e: Exception) {
logger.warn("Failed to save the media", e)
throw MediaSaveException("Failed to save the media.", e)
}
if (save.success.not()) {
save as FaildSavedMedia
logger.warn("Failed to save the media. reason: ${save.reason}")
logger.warn(save.description, save.trace)
throw MediaSaveException("Failed to save the media.")
}
save as SuccessSavedMedia
val blurHash = generateBlurhash(process)
return mediaRepository.save(
EntityMedia(
id = mediaRepository.generateId(),
name = fileName,
url = save.url,
remoteUrl = null,
thumbnailUrl = save.thumbnailUrl,
type = process.fileMimeType.fileType,
mimeType = process.fileMimeType,
blurHash = blurHash,
description = mediaRequest.description
)
)
}
}
}
}
// TODO: 仮の処理として保存したように動かす
@Suppress("LongMethod", "NestedBlockDepth")
override suspend fun uploadRemoteMedia(remoteMedia: RemoteMedia): Media {
logger.info("MEDIA Remote media. filename:${remoteMedia.name} url:${remoteMedia.url}")
val findByRemoteUrl = mediaRepository.findByRemoteUrl(remoteMedia.url)
if (findByRemoteUrl != null) {
logger.warn("DUPLICATED Remote media is duplicated. url: {}", remoteMedia.url)
return findByRemoteUrl
}
remoteMediaDownloadService.download(remoteMedia.url).withDelete().use {
val mimeType = fileTypeDeterminationService.fileType(it.path, remoteMedia.name)
val process = findMediaProcessor(mimeType).process(mimeType, remoteMedia.name, it.path, null)
val mediaSaveRequest = MediaSaveRequest(
renameService.rename(
remoteMedia.name,
mimeType,
process.filePath.fileName.toString(),
process.fileMimeType
),
"",
process.filePath,
process.thumbnailPath
)
mediaSaveRequest.filePath.withDelete().use {
mediaSaveRequest.filePath.withDelete().use {
val save = try {
mediaDataStore.save(mediaSaveRequest)
} catch (e: Exception) {
logger.warn("Failed to save the media", e)
throw MediaSaveException("Failed to save the media.", e)
}
if (save is FaildSavedMedia) {
logger.warn("Failed to save the media. reason: ${save.reason}")
logger.warn(save.description, save.trace)
throw MediaSaveException("Failed to save the media.")
}
save as SuccessSavedMedia
val blurhash = generateBlurhash(process)
return mediaRepository.save(
EntityMedia(
id = mediaRepository.generateId(),
name = remoteMedia.name,
url = save.url,
remoteUrl = remoteMedia.url,
thumbnailUrl = save.thumbnailUrl,
type = process.fileMimeType.fileType,
mimeType = process.fileMimeType,
blurHash = blurhash
)
)
}
}
}
}
private fun findMediaProcessor(mimeType: MimeType): MediaProcessService {
try {
return mediaProcessServices.first {
try {
it.isSupport(mimeType)
} catch (_: Exception) {
false
}
}
} catch (_: NoSuchElementException) {
throw UnsupportedMediaException("MediaType: $mimeType isn't supported.")
}
}
private fun generateBlurhash(process: ProcessedMediaPath): String {
val path = if (process.thumbnailPath != null && process.thumbnailMimeType != null) {
process.thumbnailPath
} else {
process.filePath
}
val mimeType = if (process.thumbnailPath != null && process.thumbnailMimeType != null) {
process.thumbnailMimeType
} else {
process.fileMimeType
}
val imageReadersByMIMEType = ImageIO.getImageReadersByMIMEType(mimeType.type + "/" + mimeType.subtype)
for (imageReader in imageReadersByMIMEType) {
try {
val bufferedImage = ImageIO.createImageInputStream(path.toFile()).use {
imageReader.input = it
imageReader.read(0)
}
return mediaBlurhashService.generateBlurhash(bufferedImage)
} catch (e: Exception) {
logger.warn("Failed to read thumbnail", e)
}
}
return ""
}
companion object {
private val logger = LoggerFactory.getLogger(MediaServiceImpl::class.java)
}
}

View File

@ -1,40 +0,0 @@
/*
* 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.service.media
data class ProcessedFile(
val byteArray: ByteArray,
val extension: String
) {
override fun equals(other: Any?): Boolean {
if (this === other) return true
if (javaClass != other?.javaClass) return false
other as ProcessedFile
if (!byteArray.contentEquals(other.byteArray)) return false
if (extension != other.extension) return false
return true
}
override fun hashCode(): Int {
var result = byteArray.contentHashCode()
result = 31 * result + extension.hashCode()
return result
}
}

View File

@ -1,22 +0,0 @@
/*
* 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.service.media
data class ProcessedMedia(
val file: ProcessedFile,
val thumbnail: ProcessedFile?
)

View File

@ -1,26 +0,0 @@
/*
* 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.service.media
import java.nio.file.Path
data class ProcessedMediaPath(
val filePath: Path,
val thumbnailPath: Path?,
val fileMimeType: MimeType,
val thumbnailMimeType: MimeType?
)

View File

@ -1,24 +0,0 @@
/*
* 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.service.media
data class RemoteMedia(
val name: String,
val url: String,
val mediaType: String,
val description: String? = null
)

View File

@ -1,23 +0,0 @@
/*
* 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.service.media
import java.nio.file.Path
interface RemoteMediaDownloadService {
suspend fun download(url: String): Path
}

View File

@ -1,61 +0,0 @@
/*
* 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.service.media
import dev.usbharu.hideout.application.config.MediaConfig
import dev.usbharu.hideout.core.domain.exception.media.RemoteMediaFileSizeException
import dev.usbharu.hideout.core.service.resource.KtorResourceResolveService
import org.slf4j.LoggerFactory
import org.springframework.stereotype.Service
import java.nio.file.Files
import java.nio.file.Path
import kotlin.io.path.outputStream
@Service
class RemoteMediaDownloadServiceImpl(
private val resourceResolveService: KtorResourceResolveService,
private val mediaConfig: MediaConfig
) :
RemoteMediaDownloadService {
override suspend fun download(url: String): Path {
logger.info("START Download remote file. url: {}", url)
val httpResponse = resourceResolveService.resolve(url).body()
val createTempFile = Files.createTempFile("hideout-remote-download", ".tmp")
logger.debug("Save to {} url: {} ", createTempFile, url)
httpResponse.use { inputStream ->
createTempFile.outputStream().use {
inputStream.transferTo(it)
}
}
val contentLength = createTempFile.toFile().length()
if (contentLength >= mediaConfig.remoteMediaFileSizeLimit) {
throw RemoteMediaFileSizeException(
"File size is too large. $contentLength >= ${mediaConfig.remoteMediaFileSizeLimit}"
)
}
logger.info("SUCCESS Download remote file. url: {}", url)
return createTempFile
}
companion object {
private val logger = LoggerFactory.getLogger(RemoteMediaDownloadServiceImpl::class.java)
}
}

View File

@ -1,139 +0,0 @@
/*
* 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.service.media
import dev.usbharu.hideout.application.config.S3StorageConfig
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.async
import kotlinx.coroutines.awaitAll
import kotlinx.coroutines.withContext
import org.slf4j.LoggerFactory
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty
import org.springframework.stereotype.Service
import software.amazon.awssdk.core.sync.RequestBody
import software.amazon.awssdk.services.s3.S3Client
import software.amazon.awssdk.services.s3.model.DeleteObjectRequest
import software.amazon.awssdk.services.s3.model.GetUrlRequest
import software.amazon.awssdk.services.s3.model.PutObjectRequest
@Service
@ConditionalOnProperty("hideout.storage.type", havingValue = "s3")
class S3MediaDataStore(private val s3Client: S3Client, private val s3StorageConfig: S3StorageConfig) : MediaDataStore {
override suspend fun save(dataMediaSave: MediaSave): SavedMedia {
val fileUploadRequest = PutObjectRequest.builder()
.bucket(s3StorageConfig.bucket)
.key(dataMediaSave.name)
.build()
val thumbnailKey = "thumbnail-${dataMediaSave.name}"
val thumbnailUploadRequest = PutObjectRequest.builder()
.bucket(s3StorageConfig.bucket)
.key(thumbnailKey)
.build()
withContext(Dispatchers.IO) {
awaitAll(
async {
if (dataMediaSave.thumbnailInputStream != null) {
s3Client.putObject(
thumbnailUploadRequest,
RequestBody.fromBytes(dataMediaSave.thumbnailInputStream)
)
s3Client.utilities()
.getUrl(GetUrlRequest.builder().bucket(s3StorageConfig.bucket).key(thumbnailKey).build())
} else {
null
}
},
async {
s3Client.putObject(fileUploadRequest, RequestBody.fromBytes(dataMediaSave.fileInputStream))
s3Client.utilities()
.getUrl(GetUrlRequest.builder().bucket(s3StorageConfig.bucket).key(dataMediaSave.name).build())
}
)
}
return SuccessSavedMedia(
name = dataMediaSave.name,
url = "${s3StorageConfig.publicUrl}/${s3StorageConfig.bucket}/${dataMediaSave.name}",
thumbnailUrl = "${s3StorageConfig.publicUrl}/${s3StorageConfig.bucket}/$thumbnailKey"
)
}
override suspend fun save(dataSaveRequest: MediaSaveRequest): SavedMedia {
logger.info("MEDIA upload. {}", dataSaveRequest.name)
val fileUploadRequest = PutObjectRequest.builder()
.bucket(s3StorageConfig.bucket)
.key(dataSaveRequest.name)
.build()
logger.info("MEDIA upload. bucket: {} key: {}", s3StorageConfig.bucket, dataSaveRequest.name)
val thumbnailKey = "thumbnail-${dataSaveRequest.name}"
val thumbnailUploadRequest = PutObjectRequest.builder()
.bucket(s3StorageConfig.bucket)
.key(thumbnailKey)
.build()
logger.info("MEDIA upload. bucket: {} key: {}", s3StorageConfig.bucket, thumbnailKey)
withContext(Dispatchers.IO) {
awaitAll(
async {
if (dataSaveRequest.thumbnailPath != null) {
s3Client.putObject(
thumbnailUploadRequest,
RequestBody.fromFile(dataSaveRequest.thumbnailPath)
)
} else {
null
}
},
async {
s3Client.putObject(fileUploadRequest, RequestBody.fromFile(dataSaveRequest.filePath))
}
)
}
val successSavedMedia = SuccessSavedMedia(
name = dataSaveRequest.name,
url = "${s3StorageConfig.publicUrl}/${s3StorageConfig.bucket}/${dataSaveRequest.name}",
thumbnailUrl = "${s3StorageConfig.publicUrl}/${s3StorageConfig.bucket}/$thumbnailKey"
)
logger.info("SUCCESS Media upload. {}", dataSaveRequest.name)
logger.debug(
"name: {} url: {} thumbnail url: {}",
successSavedMedia.name,
successSavedMedia.url,
successSavedMedia.thumbnailUrl
)
return successSavedMedia
}
override suspend fun delete(id: String) {
val fileDeleteRequest = DeleteObjectRequest.builder().bucket(s3StorageConfig.bucket).key(id).build()
val thumbnailDeleteRequest =
DeleteObjectRequest.builder().bucket(s3StorageConfig.bucket).key("thumbnail-$id").build()
s3Client.deleteObject(fileDeleteRequest)
s3Client.deleteObject(thumbnailDeleteRequest)
}
companion object {
private val logger = LoggerFactory.getLogger(S3MediaDataStore::class.java)
}
}

View File

@ -1,87 +0,0 @@
/*
* 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.service.media
sealed class SavedMedia(val success: Boolean) {
override fun equals(other: Any?): Boolean {
if (this === other) return true
if (javaClass != other?.javaClass) return false
other as SavedMedia
return success == other.success
}
override fun hashCode(): Int = success.hashCode()
}
class SuccessSavedMedia(
val name: String,
val url: String,
val thumbnailUrl: String,
) :
SavedMedia(true) {
override fun equals(other: Any?): Boolean {
if (this === other) return true
if (javaClass != other?.javaClass) return false
if (!super.equals(other)) return false
other as SuccessSavedMedia
if (name != other.name) return false
if (url != other.url) return false
if (thumbnailUrl != other.thumbnailUrl) return false
return true
}
override fun hashCode(): Int {
var result = super.hashCode()
result = 31 * result + name.hashCode()
result = 31 * result + url.hashCode()
result = 31 * result + thumbnailUrl.hashCode()
return result
}
}
class FaildSavedMedia(
val reason: String,
val description: String,
val trace: Throwable? = null
) : SavedMedia(false) {
override fun equals(other: Any?): Boolean {
if (this === other) return true
if (javaClass != other?.javaClass) return false
if (!super.equals(other)) return false
other as FaildSavedMedia
if (reason != other.reason) return false
if (description != other.description) return false
if (trace != other.trace) return false
return true
}
override fun hashCode(): Int {
var result = super.hashCode()
result = 31 * result + reason.hashCode()
result = 31 * result + description.hashCode()
result = 31 * result + (trace?.hashCode() ?: 0)
return result
}
}

View File

@ -1,24 +0,0 @@
/*
* 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.service.media
import java.io.InputStream
interface ThumbnailGenerateService {
fun generate(bufferedImage: InputStream, width: Int, height: Int): ProcessedFile?
fun generate(outputStream: ByteArray, width: Int, height: Int): ProcessedFile?
}

View File

@ -1,42 +0,0 @@
/*
* 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.service.media
import org.springframework.stereotype.Service
import java.awt.image.BufferedImage
import java.io.ByteArrayOutputStream
import java.io.InputStream
import javax.imageio.ImageIO
@Service
class ThumbnailGenerateServiceImpl : ThumbnailGenerateService {
override fun generate(bufferedImage: InputStream, width: Int, height: Int): ProcessedFile? {
val image = ImageIO.read(bufferedImage)
return internalGenerate(image)
}
override fun generate(outputStream: ByteArray, width: Int, height: Int): ProcessedFile? {
val image = ImageIO.read(outputStream.inputStream())
return internalGenerate(image)
}
private fun internalGenerate(image: BufferedImage): ProcessedFile {
val byteArrayOutputStream = ByteArrayOutputStream()
ImageIO.write(image, "jpeg", byteArrayOutputStream)
return ProcessedFile(byteArrayOutputStream.toByteArray(), "jpg")
}
}

View File

@ -1,32 +0,0 @@
/*
* 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.service.media
import org.springframework.beans.factory.annotation.Qualifier
import org.springframework.stereotype.Service
import java.util.*
@Qualifier("uuid")
@Service
class UUIDMediaFileRenameService : MediaFileRenameService {
override fun rename(
uploadName: String,
uploadMimeType: MimeType,
processedName: String,
processedMimeType: MimeType
): String = "${UUID.randomUUID()}.${uploadMimeType.subtype}.${processedMimeType.subtype}"
}

View File

@ -1,26 +0,0 @@
/*
* 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.service.media.converter
import dev.usbharu.hideout.core.service.media.FileType
import dev.usbharu.hideout.core.service.media.ProcessedFile
import java.io.InputStream
interface MediaConverter {
fun isSupport(fileType: FileType): Boolean
fun convert(inputStream: InputStream): ProcessedFile
}

View File

@ -1,30 +0,0 @@
/*
* 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.service.media.converter
import dev.usbharu.hideout.core.service.media.FileType
import dev.usbharu.hideout.core.service.media.ProcessedFile
import java.io.InputStream
interface MediaConverterRoot {
suspend fun convert(
fileType: FileType,
contentType: String,
filename: String,
inputStream: InputStream
): ProcessedFile
}

View File

@ -1,48 +0,0 @@
/*
* 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.service.media.converter
import dev.usbharu.hideout.core.service.media.FileType
import dev.usbharu.hideout.core.service.media.ProcessedFile
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext
import org.springframework.stereotype.Service
import java.io.InputStream
@Service
class MediaConverterRootImpl(private val converters: List<MediaConverter>) : MediaConverterRoot {
override suspend fun convert(
fileType: FileType,
contentType: String,
filename: String,
inputStream: InputStream
): ProcessedFile {
val convert = converters.find {
it.isSupport(fileType)
}?.convert(inputStream)
if (convert != null) {
return convert
}
return withContext(Dispatchers.IO) {
if (filename.contains('.')) {
ProcessedFile(inputStream.readAllBytes(), filename.substringAfterLast("."))
} else {
ProcessedFile(inputStream.readAllBytes(), contentType.substringAfterLast("/"))
}
}
}
}

View File

@ -1,42 +0,0 @@
/*
* 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.service.media.converter
import dev.usbharu.hideout.core.service.media.FileType
import dev.usbharu.hideout.core.service.media.MimeType
import dev.usbharu.hideout.core.service.media.ProcessedMedia
import dev.usbharu.hideout.core.service.media.ProcessedMediaPath
import java.nio.file.Path
interface MediaProcessService {
fun isSupport(mimeType: MimeType): Boolean
suspend fun process(
fileType: FileType,
contentType: String,
fileName: String,
file: ByteArray,
thumbnail: ByteArray?
): ProcessedMedia
suspend fun process(
mimeType: MimeType,
fileName: String,
filePath: Path,
thumbnails: Path?
): ProcessedMediaPath
}

View File

@ -1,74 +0,0 @@
/*
* 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.service.media.converter
import dev.usbharu.hideout.core.domain.exception.media.MediaConvertException
import dev.usbharu.hideout.core.service.media.*
import org.slf4j.LoggerFactory
import org.springframework.stereotype.Service
import java.nio.file.Path
@Service
@Suppress("TooGenericExceptionCaught")
class MediaProcessServiceImpl(
private val mediaConverterRoot: MediaConverterRoot,
private val thumbnailGenerateService: ThumbnailGenerateService
) : MediaProcessService {
override fun isSupport(mimeType: MimeType): Boolean = false
override suspend fun process(
fileType: FileType,
contentType: String,
filename: String,
file: ByteArray,
thumbnail: ByteArray?
): ProcessedMedia {
val fileInputStream = try {
mediaConverterRoot.convert(fileType, contentType, filename, file.inputStream().buffered())
} catch (e: Exception) {
logger.warn("Failed convert media.", e)
throw MediaConvertException("Failed convert media.", e)
}
val thumbnailInputStream = try {
thumbnail?.let { mediaConverterRoot.convert(fileType, contentType, filename, it.inputStream().buffered()) }
} catch (e: Exception) {
logger.warn("Failed convert thumbnail media.", e)
null
}
return ProcessedMedia(
fileInputStream,
thumbnailGenerateService.generate(
thumbnailInputStream?.byteArray ?: file,
2048,
2048
)
)
}
override suspend fun process(
mimeType: MimeType,
fileName: String,
filePath: Path,
thumbnails: Path?
): ProcessedMediaPath {
TODO("Not yet implemented")
}
companion object {
private val logger = LoggerFactory.getLogger(MediaProcessServiceImpl::class.java)
}
}

View File

@ -1,131 +0,0 @@
/*
* 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.service.media.converter.image
import dev.usbharu.hideout.core.domain.exception.media.MediaProcessException
import dev.usbharu.hideout.core.service.media.FileType
import dev.usbharu.hideout.core.service.media.MimeType
import dev.usbharu.hideout.core.service.media.ProcessedMedia
import dev.usbharu.hideout.core.service.media.ProcessedMediaPath
import dev.usbharu.hideout.core.service.media.converter.MediaProcessService
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.slf4j.MDCContext
import kotlinx.coroutines.withContext
import net.coobird.thumbnailator.Thumbnails
import org.slf4j.LoggerFactory
import org.springframework.beans.factory.annotation.Qualifier
import org.springframework.stereotype.Service
import java.awt.Color
import java.awt.image.BufferedImage
import java.nio.file.Files
import java.nio.file.Path
import java.util.*
import javax.imageio.ImageIO
import kotlin.io.path.inputStream
import kotlin.io.path.outputStream
@Service
@Qualifier("image")
class ImageMediaProcessService(private val imageMediaProcessorConfiguration: ImageMediaProcessorConfiguration?) :
MediaProcessService {
private val convertType = imageMediaProcessorConfiguration?.convert ?: "jpeg"
private val supportedTypes = imageMediaProcessorConfiguration?.supportedType ?: listOf("webp", "jpeg", "png")
private val genThumbnail = imageMediaProcessorConfiguration?.thubnail?.generate ?: true
private val width = imageMediaProcessorConfiguration?.thubnail?.width ?: 1000
private val height = imageMediaProcessorConfiguration?.thubnail?.height ?: 1000
override fun isSupport(mimeType: MimeType): Boolean {
if (mimeType.type != "image") {
return false
}
return supportedTypes.contains(mimeType.subtype)
}
override suspend fun process(
fileType: FileType,
contentType: String,
fileName: String,
file: ByteArray,
thumbnail: ByteArray?
): ProcessedMedia {
TODO("Not yet implemented")
}
override suspend fun process(
mimeType: MimeType,
fileName: String,
filePath: Path,
thumbnails: Path?
): ProcessedMediaPath = withContext(Dispatchers.IO + MDCContext()) {
val read = ImageIO.read(filePath.inputStream())
val bufferedImage = BufferedImage(read.width, read.height, BufferedImage.TYPE_INT_RGB)
val graphics = bufferedImage.createGraphics()
graphics.drawImage(read, 0, 0, Color.BLACK, null)
val tempFileName = UUID.randomUUID().toString()
val tempFile = Files.createTempFile(tempFileName, "tmp")
val thumbnailPath = if (genThumbnail) {
val tempThumbnailFile = Files.createTempFile("thumbnail-$tempFileName", ".tmp")
tempThumbnailFile.outputStream().use {
val write = ImageIO.write(
if (thumbnails != null) {
Thumbnails.of(thumbnails.toFile())
.size(width, height)
.imageType(BufferedImage.TYPE_INT_RGB)
.asBufferedImage()
} else {
Thumbnails.of(bufferedImage)
.size(width, height)
.imageType(BufferedImage.TYPE_INT_RGB)
.asBufferedImage()
},
convertType,
it
)
tempThumbnailFile.takeIf { write }
}
} else {
null
}
tempFile.outputStream().use {
if (ImageIO.write(bufferedImage, convertType, it).not()) {
logger.warn("Failed to save a temporary file. type: {} ,path: {}", convertType, tempFile)
throw MediaProcessException("Failed to save a temporary file.")
}
}
ProcessedMediaPath(
tempFile,
thumbnailPath,
MimeType("image", convertType, FileType.Image),
MimeType("image", convertType, FileType.Image).takeIf { genThumbnail }
)
}
companion object {
private val logger = LoggerFactory.getLogger(ImageMediaProcessService::class.java)
}
}

View File

@ -1,33 +0,0 @@
/*
* 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.service.media.converter.image
import org.springframework.boot.context.properties.ConfigurationProperties
@ConfigurationProperties("hideout.media.image")
data class ImageMediaProcessorConfiguration(
val convert: String?,
val thubnail: ImageMediaProcessorThumbnailConfiguration?,
val supportedType: List<String>?,
)
data class ImageMediaProcessorThumbnailConfiguration(
val generate: Boolean,
val width: Int,
val height: Int
)

View File

@ -1,143 +0,0 @@
/*
* 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.service.media.converter.movie
import dev.usbharu.hideout.core.service.media.FileType
import dev.usbharu.hideout.core.service.media.MimeType
import dev.usbharu.hideout.core.service.media.ProcessedMedia
import dev.usbharu.hideout.core.service.media.ProcessedMediaPath
import dev.usbharu.hideout.core.service.media.converter.MediaProcessService
import org.bytedeco.ffmpeg.global.avcodec
import org.bytedeco.javacv.FFmpegFrameFilter
import org.bytedeco.javacv.FFmpegFrameGrabber
import org.bytedeco.javacv.FFmpegFrameRecorder
import org.bytedeco.javacv.Java2DFrameConverter
import org.slf4j.LoggerFactory
import org.springframework.beans.factory.annotation.Qualifier
import org.springframework.stereotype.Service
import java.awt.image.BufferedImage
import java.nio.file.Files
import java.nio.file.Path
import javax.imageio.ImageIO
import kotlin.math.min
@Service
@Qualifier("video")
class MovieMediaProcessService : MediaProcessService {
override fun isSupport(mimeType: MimeType): Boolean = mimeType.type == "video"
override suspend fun process(
fileType: FileType,
contentType: String,
fileName: String,
file: ByteArray,
thumbnail: ByteArray?
): ProcessedMedia {
TODO("Not yet implemented")
}
@Suppress("LongMethod", "NestedBlockDepth", "CognitiveComplexMethod")
override suspend fun process(
mimeType: MimeType,
fileName: String,
filePath: Path,
thumbnails: Path?
): ProcessedMediaPath {
val tempFile = Files.createTempFile("hideout-movie-processor-", ".tmp")
val thumbnailFile = Files.createTempFile("hideout-movie-thumbnail-generate-", ".tmp")
logger.info("START Convert Movie Media {}", fileName)
FFmpegFrameGrabber(filePath.toFile()).use { grabber ->
grabber.start()
val width = grabber.imageWidth
val height = grabber.imageHeight
val frameRate = 60.0
logger.debug("Movie Media Width {}, Height {}", width, height)
FFmpegFrameFilter(
"fps=fps=${frameRate.toInt()}",
"anull",
width,
height,
grabber.audioChannels
).use { filter ->
filter.sampleFormat = grabber.sampleFormat
filter.sampleRate = grabber.sampleRate
filter.pixelFormat = grabber.pixelFormat
filter.frameRate = grabber.frameRate
filter.start()
val videoBitRate = min(1300000, (width * height * frameRate * 1 * 0.07).toInt())
logger.debug("Movie Media BitRate {}", videoBitRate)
FFmpegFrameRecorder(tempFile.toFile(), width, height, grabber.audioChannels).use {
it.sampleRate = grabber.sampleRate
it.format = "mp4"
it.videoCodec = avcodec.AV_CODEC_ID_H264
it.audioCodec = avcodec.AV_CODEC_ID_AAC
it.audioChannels = grabber.audioChannels
it.videoQuality = 1.0
it.frameRate = frameRate
it.setVideoOption("preset", "ultrafast")
it.timestamp = 0
it.gopSize = frameRate.toInt()
it.videoBitrate = videoBitRate
it.start()
var bufferedImage: BufferedImage? = null
val frameConverter = Java2DFrameConverter()
while (true) {
val grab = grabber.grab() ?: break
if (bufferedImage == null) {
bufferedImage = frameConverter.convert(grab)
}
if (grab.image != null || grab.samples != null) {
filter.push(grab)
}
while (true) {
val frame = filter.pull() ?: break
it.record(frame)
}
}
if (bufferedImage != null) {
ImageIO.write(bufferedImage, "jpeg", thumbnailFile.toFile())
}
}
}
}
logger.info("SUCCESS Convert Movie Media {}", fileName)
return ProcessedMediaPath(
tempFile,
thumbnailFile,
MimeType("video", "mp4", FileType.Video),
MimeType("image", "jpeg", FileType.Image)
)
}
companion object {
private val logger = LoggerFactory.getLogger(MovieMediaProcessService::class.java)
}
}

Some files were not shown because too many files have changed in this diff Show More