Compare commits

...

12 Commits

93 changed files with 1036 additions and 680 deletions

View File

@ -60,6 +60,8 @@ tasks.create<GenerateTask>("openApiGenerateMastodonCompatibleApi", GenerateTask:
configOptions.put("interfaceOnly", "true")
configOptions.put("useSpringBoot3", "true")
additionalProperties.put("useTags", "true")
importMappings.put("org.springframework.core.io.Resource", "org.springframework.web.multipart.MultipartFile")
typeMappings.put("org.springframework.core.io.Resource", "org.springframework.web.multipart.MultipartFile")
}
repositories {
@ -116,6 +118,9 @@ dependencies {
implementation("org.springframework.security:spring-security-oauth2-jose")
implementation("org.springframework.boot:spring-boot-starter-data-mongodb")
implementation("org.jetbrains.exposed:exposed-spring-boot-starter:0.44.0")
implementation("io.trbl:blurhash:1.0.0")
implementation("software.amazon.awssdk:s3:2.20.157")
implementation("io.ktor:ktor-client-logging-jvm:$ktor_version")

View File

@ -3,6 +3,7 @@ build:
weights:
Indentation: 0
MagicNumber: 0
InjectDispatcher: 0
style:
ClassOrdering:

View File

@ -4,6 +4,7 @@ import dev.usbharu.hideout.domain.model.job.HideoutJob
import dev.usbharu.hideout.service.ap.APService
import dev.usbharu.hideout.service.job.JobQueueParentService
import dev.usbharu.hideout.service.job.JobQueueWorkerService
import org.slf4j.Logger
import org.slf4j.LoggerFactory
import org.springframework.boot.ApplicationArguments
import org.springframework.boot.ApplicationRunner
@ -18,7 +19,7 @@ class JobQueueRunner(private val jobQueueParentService: JobQueueParentService, p
}
companion object {
val LOGGER = LoggerFactory.getLogger(JobQueueRunner::class.java)
val LOGGER: Logger = LoggerFactory.getLogger(JobQueueRunner::class.java)
}
}
@ -46,6 +47,6 @@ class JobQueueWorkerRunner(
}
companion object {
val LOGGER = LoggerFactory.getLogger(JobQueueWorkerRunner::class.java)
val LOGGER: Logger = LoggerFactory.getLogger(JobQueueWorkerRunner::class.java)
}
}

View File

@ -0,0 +1,20 @@
package dev.usbharu.hideout.config
import org.springframework.context.annotation.Bean
import org.springframework.context.annotation.Configuration
import software.amazon.awssdk.auth.credentials.AwsBasicCredentials
import software.amazon.awssdk.regions.Region
import software.amazon.awssdk.services.s3.S3Client
import java.net.URI
@Configuration
class AwsConfig {
@Bean
fun s3Client(awsConfig: StorageConfig): S3Client {
return S3Client.builder()
.endpointOverride(URI.create(awsConfig.endpoint))
.region(Region.of(awsConfig.region))
.credentialsProvider { AwsBasicCredentials.create(awsConfig.accessKey, awsConfig.secretKey) }
.build()
}
}

View File

@ -10,9 +10,23 @@ class SpringConfig {
@Autowired
lateinit var config: ApplicationConfig
@Autowired
lateinit var storageConfig: StorageConfig
}
@ConfigurationProperties("hideout")
data class ApplicationConfig(
val url: URL
)
@ConfigurationProperties("hideout.storage")
data class StorageConfig(
val useS3: Boolean,
val endpoint: String,
val publicUrl: String,
val bucket: String,
val region: String,
val accessKey: String,
val secretKey: String
)

View File

@ -0,0 +1,31 @@
package dev.usbharu.hideout.controller.mastodon
import dev.usbharu.hideout.controller.mastodon.generated.MediaApi
import dev.usbharu.hideout.domain.mastodon.model.generated.MediaAttachment
import dev.usbharu.hideout.domain.model.hideout.form.Media
import dev.usbharu.hideout.service.api.mastodon.MediaApiService
import kotlinx.coroutines.runBlocking
import org.springframework.http.ResponseEntity
import org.springframework.stereotype.Controller
import org.springframework.web.multipart.MultipartFile
@Controller
class MastodonMediaApiController(private val mediaApiService: MediaApiService) : MediaApi {
override fun apiV1MediaPost(
file: MultipartFile,
thumbnail: MultipartFile?,
description: String?,
focus: String?
): ResponseEntity<MediaAttachment> = runBlocking {
ResponseEntity.ok(
mediaApiService.postMedia(
Media(
file,
thumbnail,
description,
focus
)
)
)
}
}

View File

@ -0,0 +1,8 @@
package dev.usbharu.hideout.domain.model
data class MediaSave(
val name: String,
val prefix: String,
val fileInputStream: ByteArray,
val thumbnailInputStream: ByteArray?
)

View File

@ -1,6 +0,0 @@
package dev.usbharu.hideout.domain.model.api.mastodon
data class StatusForPost(
val status: String,
val userId: Long
)

View File

@ -0,0 +1,8 @@
package dev.usbharu.hideout.domain.model.hideout.dto
enum class FileType {
Image,
Video,
Audio,
Unknown
}

View File

@ -1,31 +0,0 @@
package dev.usbharu.hideout.domain.model.hideout.dto
import dev.usbharu.hideout.domain.model.hideout.entity.Post
import dev.usbharu.hideout.domain.model.hideout.entity.User
import dev.usbharu.hideout.domain.model.hideout.entity.Visibility
data class PostResponse(
val id: String,
val user: UserResponse,
val overview: String? = null,
val text: String? = null,
val createdAt: Long,
val visibility: Visibility,
val url: String,
val sensitive: Boolean = false,
) {
companion object {
fun from(post: Post, user: User): PostResponse {
return PostResponse(
id = post.id.toString(),
user = UserResponse.from(user),
overview = post.overview,
text = post.text,
createdAt = post.createdAt,
visibility = post.visibility,
url = post.url,
sensitive = post.sensitive
)
}
}
}

View File

@ -0,0 +1,6 @@
package dev.usbharu.hideout.domain.model.hideout.dto
data class ProcessedFile(
val byteArray: ByteArray,
val extension: String
)

View File

@ -0,0 +1,6 @@
package dev.usbharu.hideout.domain.model.hideout.dto
data class ProcessedMedia(
val file: ProcessedFile,
val thumbnail: ProcessedFile?
)

View File

@ -0,0 +1,7 @@
package dev.usbharu.hideout.domain.model.hideout.dto
data class RemoteMedia(
val name: String,
val url: String,
val mediaType: String
)

View File

@ -0,0 +1,16 @@
package dev.usbharu.hideout.domain.model.hideout.dto
sealed class SavedMedia(val success: Boolean)
class SuccessSavedMedia(
val name: String,
val url: String,
val thumbnailUrl: String,
) :
SavedMedia(true)
class FaildSavedMedia(
val reason: String,
val description: String,
val trace: Throwable? = null
) : SavedMedia(false)

View File

@ -1,27 +0,0 @@
package dev.usbharu.hideout.domain.model.hideout.dto
import dev.usbharu.hideout.domain.model.hideout.entity.User
data class UserResponse(
val id: String,
val name: String,
val domain: String,
val screenName: String,
val description: String = "",
val url: String,
val createdAt: Long
) {
companion object {
fun from(user: User): UserResponse {
return UserResponse(
id = user.id.toString(),
name = user.name,
domain = user.domain,
screenName = user.screenName,
description = user.description,
url = user.url,
createdAt = user.createdAt.toEpochMilli()
)
}
}
}

View File

@ -0,0 +1,13 @@
package dev.usbharu.hideout.domain.model.hideout.entity
import dev.usbharu.hideout.domain.model.hideout.dto.FileType
data class Media(
val id: Long,
val name: String,
val url: String,
val remoteUrl: String?,
val thumbnailUrl: String?,
val type: FileType,
val blurHash: String?
)

View File

@ -0,0 +1,10 @@
package dev.usbharu.hideout.domain.model.hideout.form
import org.springframework.web.multipart.MultipartFile
data class Media(
val file: MultipartFile,
val thumbnail: MultipartFile?,
val description: String?,
val focus: String?
)

View File

@ -1,11 +0,0 @@
package dev.usbharu.hideout.domain.model.hideout.form
import dev.usbharu.hideout.domain.model.hideout.entity.Visibility
data class Post(
val text: String,
val overview: String? = null,
val visibility: Visibility = Visibility.PUBLIC,
val repostId: Long? = null,
val replyId: Long? = null
)

View File

@ -1,3 +0,0 @@
package dev.usbharu.hideout.domain.model.hideout.form
data class Reaction(val reaction: String?)

View File

@ -1,3 +0,0 @@
package dev.usbharu.hideout.domain.model.hideout.form
data class RefreshToken(val refreshToken: String)

View File

@ -1,3 +0,0 @@
package dev.usbharu.hideout.domain.model.hideout.form
data class UserCreate(val username: String, val password: String)

View File

@ -1,3 +0,0 @@
package dev.usbharu.hideout.domain.model.hideout.form
data class UserLogin(val username: String, val password: String)

View File

@ -1,37 +1,38 @@
package dev.usbharu.hideout.domain.model.job
import kjob.core.Job
import kjob.core.Prop
import org.springframework.stereotype.Component
sealed class HideoutJob(name: String = "") : Job(name)
@Component
object ReceiveFollowJob : HideoutJob("ReceiveFollowJob") {
val actor = string("actor")
val follow = string("follow")
val targetActor = string("targetActor")
val actor: Prop<ReceiveFollowJob, String> = string("actor")
val follow: Prop<ReceiveFollowJob, String> = string("follow")
val targetActor: Prop<ReceiveFollowJob, String> = string("targetActor")
}
@Component
object DeliverPostJob : HideoutJob("DeliverPostJob") {
val post = string("post")
val actor = string("actor")
val inbox = string("inbox")
val post: Prop<DeliverPostJob, String> = string("post")
val actor: Prop<DeliverPostJob, String> = string("actor")
val inbox: Prop<DeliverPostJob, String> = string("inbox")
}
@Component
object DeliverReactionJob : HideoutJob("DeliverReactionJob") {
val reaction = string("reaction")
val postUrl = string("postUrl")
val actor = string("actor")
val inbox = string("inbox")
val id = string("id")
val reaction: Prop<DeliverReactionJob, String> = string("reaction")
val postUrl: Prop<DeliverReactionJob, String> = string("postUrl")
val actor: Prop<DeliverReactionJob, String> = string("actor")
val inbox: Prop<DeliverReactionJob, String> = string("inbox")
val id: Prop<DeliverReactionJob, String> = string("id")
}
@Component
object DeliverRemoveReactionJob : HideoutJob("DeliverRemoveReactionJob") {
val id = string("id")
val inbox = string("inbox")
val actor = string("actor")
val like = string("like")
val id: Prop<DeliverRemoveReactionJob, String> = string("id")
val inbox: Prop<DeliverRemoveReactionJob, String> = string("inbox")
val actor: Prop<DeliverRemoveReactionJob, String> = string("actor")
val like: Prop<DeliverRemoveReactionJob, String> = string("like")
}

View File

@ -1,3 +1,5 @@
@file:Suppress("ClassName")
package dev.usbharu.hideout.domain.model.wellknown
@Suppress("ClassNaming")

View File

@ -1,8 +1,15 @@
package dev.usbharu.hideout.exception
import java.io.Serial
open class FailedToGetResourcesException : IllegalArgumentException {
constructor() : super()
constructor(s: String?) : super(s)
constructor(message: String?, cause: Throwable?) : super(message, cause)
constructor(cause: Throwable?) : super(cause)
companion object {
@Serial
private const val serialVersionUID: Long = -3117221954866309059L
}
}

View File

@ -1,8 +1,15 @@
package dev.usbharu.hideout.exception
import java.io.Serial
class HttpSignatureVerifyException : IllegalArgumentException {
constructor() : super()
constructor(s: String?) : super(s)
constructor(message: String?, cause: Throwable?) : super(message, cause)
constructor(cause: Throwable?) : super(cause)
companion object {
@Serial
private const val serialVersionUID: Long = 1484943321770741944L
}
}

View File

@ -1,8 +1,15 @@
package dev.usbharu.hideout.exception
import java.io.Serial
class IllegalParameterException : IllegalArgumentException {
constructor() : super()
constructor(s: String?) : super(s)
constructor(message: String?, cause: Throwable?) : super(message, cause)
constructor(cause: Throwable?) : super(cause)
companion object {
@Serial
private const val serialVersionUID: Long = -4641102874061252642L
}
}

View File

@ -1,8 +1,15 @@
package dev.usbharu.hideout.exception
import java.io.Serial
class InvalidRefreshTokenException : IllegalArgumentException {
constructor() : super()
constructor(s: String?) : super(s)
constructor(message: String?, cause: Throwable?) : super(message, cause)
constructor(cause: Throwable?) : super(cause)
companion object {
@Serial
private const val serialVersionUID: Long = -3779633753651907145L
}
}

View File

@ -1,8 +1,15 @@
package dev.usbharu.hideout.exception
import java.io.Serial
class InvalidUsernameOrPasswordException : IllegalArgumentException {
constructor() : super()
constructor(s: String?) : super(s)
constructor(message: String?, cause: Throwable?) : super(message, cause)
constructor(cause: Throwable?) : super(cause)
companion object {
@Serial
private const val serialVersionUID: Long = -3638699928983322003L
}
}

View File

@ -1,8 +1,15 @@
package dev.usbharu.hideout.exception
import java.io.Serial
class JsonParseException : IllegalArgumentException {
constructor() : super()
constructor(s: String?) : super(s)
constructor(message: String?, cause: Throwable?) : super(message, cause)
constructor(cause: Throwable?) : super(cause)
companion object {
@Serial
private const val serialVersionUID: Long = 7975567796830950692L
}
}

View File

@ -1,8 +1,15 @@
package dev.usbharu.hideout.exception
import java.io.Serial
class NotInitException : IllegalStateException {
constructor() : super()
constructor(s: String?) : super(s)
constructor(message: String?, cause: Throwable?) : super(message, cause)
constructor(cause: Throwable?) : super(cause)
companion object {
@Serial
private const val serialVersionUID: Long = -5859046179473905716L
}
}

View File

@ -1,8 +1,15 @@
package dev.usbharu.hideout.exception
import java.io.Serial
class ParameterNotExistException : IllegalArgumentException {
constructor() : super()
constructor(s: String?) : super(s)
constructor(message: String?, cause: Throwable?) : super(message, cause)
constructor(cause: Throwable?) : super(cause)
companion object {
@Serial
private const val serialVersionUID: Long = -8845602757225726432L
}
}

View File

@ -1,8 +1,15 @@
package dev.usbharu.hideout.exception
import java.io.Serial
class PostNotFoundException : IllegalArgumentException {
constructor() : super()
constructor(s: String?) : super(s)
constructor(message: String?, cause: Throwable?) : super(message, cause)
constructor(cause: Throwable?) : super(cause)
companion object {
@Serial
private const val serialVersionUID: Long = 7133035286017262876L
}
}

View File

@ -1,8 +1,15 @@
package dev.usbharu.hideout.exception
import java.io.Serial
class UserNotFoundException : IllegalArgumentException {
constructor() : super()
constructor(s: String?) : super(s)
constructor(message: String?, cause: Throwable?) : super(message, cause)
constructor(cause: Throwable?) : super(cause)
companion object {
@Serial
private const val serialVersionUID: Long = 6343548635914580823L
}
}

View File

@ -1,8 +1,15 @@
package dev.usbharu.hideout.exception
import java.io.Serial
class UsernameAlreadyExistException : IllegalArgumentException {
constructor() : super()
constructor(s: String?) : super(s)
constructor(message: String?, cause: Throwable?) : super(message, cause)
constructor(cause: Throwable?) : super(cause)
companion object {
@Serial
private const val serialVersionUID: Long = -4635016576575533883L
}
}

View File

@ -1,8 +1,15 @@
package dev.usbharu.hideout.exception.ap
import java.io.Serial
class IllegalActivityPubObjectException : IllegalArgumentException {
constructor() : super()
constructor(s: String?) : super(s)
constructor(message: String?, cause: Throwable?) : super(message, cause)
constructor(cause: Throwable?) : super(cause)
companion object {
@Serial
private const val serialVersionUID: Long = 7216998115771415263L
}
}

View File

@ -0,0 +1,14 @@
package dev.usbharu.hideout.exception.media
open class MediaConvertException : MediaException {
constructor() : super()
constructor(message: String?) : super(message)
constructor(message: String?, cause: Throwable?) : super(message, cause)
constructor(cause: Throwable?) : super(cause)
constructor(message: String?, cause: Throwable?, enableSuppression: Boolean, writableStackTrace: Boolean) : super(
message,
cause,
enableSuppression,
writableStackTrace
)
}

View File

@ -0,0 +1,14 @@
package dev.usbharu.hideout.exception.media
abstract class MediaException : RuntimeException {
constructor() : super()
constructor(message: String?) : super(message)
constructor(message: String?, cause: Throwable?) : super(message, cause)
constructor(cause: Throwable?) : super(cause)
constructor(message: String?, cause: Throwable?, enableSuppression: Boolean, writableStackTrace: Boolean) : super(
message,
cause,
enableSuppression,
writableStackTrace
)
}

View File

@ -0,0 +1,14 @@
package dev.usbharu.hideout.exception.media
open class MediaFileSizeException : MediaException {
constructor() : super()
constructor(message: String?) : super(message)
constructor(message: String?, cause: Throwable?) : super(message, cause)
constructor(cause: Throwable?) : super(cause)
constructor(message: String?, cause: Throwable?, enableSuppression: Boolean, writableStackTrace: Boolean) : super(
message,
cause,
enableSuppression,
writableStackTrace
)
}

View File

@ -0,0 +1,14 @@
package dev.usbharu.hideout.exception.media
class MediaFileSizeIsZeroException : MediaFileSizeException {
constructor() : super()
constructor(message: String?) : super(message)
constructor(message: String?, cause: Throwable?) : super(message, cause)
constructor(cause: Throwable?) : super(cause)
constructor(message: String?, cause: Throwable?, enableSuppression: Boolean, writableStackTrace: Boolean) : super(
message,
cause,
enableSuppression,
writableStackTrace
)
}

View File

@ -0,0 +1,14 @@
package dev.usbharu.hideout.exception.media
open class MediaSaveException : MediaException {
constructor() : super()
constructor(message: String?) : super(message)
constructor(message: String?, cause: Throwable?) : super(message, cause)
constructor(cause: Throwable?) : super(cause)
constructor(message: String?, cause: Throwable?, enableSuppression: Boolean, writableStackTrace: Boolean) : super(
message,
cause,
enableSuppression,
writableStackTrace
)
}

View File

@ -0,0 +1,14 @@
package dev.usbharu.hideout.exception.media
class UnsupportedMediaException : MediaException {
constructor() : super()
constructor(message: String?) : super(message)
constructor(message: String?, cause: Throwable?) : super(message, cause)
constructor(cause: Throwable?) : super(cause)
constructor(message: String?, cause: Throwable?, enableSuppression: Boolean, writableStackTrace: Boolean) : super(
message,
cause,
enableSuppression,
writableStackTrace
)
}

View File

@ -56,7 +56,10 @@ class HttpSignaturePluginConfig {
lateinit var keyMap: KeyMap
}
val httpSignaturePlugin = createClientPlugin("HttpSign", ::HttpSignaturePluginConfig) {
val httpSignaturePlugin: ClientPlugin<HttpSignaturePluginConfig> = createClientPlugin(
"HttpSign",
::HttpSignaturePluginConfig
) {
val keyMap = pluginConfig.keyMap
val format = SimpleDateFormat("EEE, dd MMM yyyy HH:mm:ss zzz", Locale.US)
format.timeZone = TimeZone.getTimeZone("GMT")
@ -141,13 +144,13 @@ val httpSignaturePlugin = createClientPlugin("HttpSign", ::HttpSignaturePluginCo
request.headers.remove("Signature")
signer!!.sign(object : HttpMessage, HttpRequest {
(signer ?: return@onRequest).sign(object : HttpMessage, HttpRequest {
override fun headerValues(name: String?): MutableList<String> =
name?.let { request.headers.getAll(it) }?.toMutableList() ?: mutableListOf()
override fun addHeader(name: String?, value: String?) {
val split = value?.split("=").orEmpty()
name?.let { request.header(it, split.get(0) + "=\"" + split.get(1).trim('"') + "\"") }
name?.let { request.header(it, split[0] + "=\"" + split[1].trim('"') + "\"") }
}
override fun method(): String = request.method.value

View File

@ -59,7 +59,7 @@ class FollowerQueryServiceImpl : FollowerQueryService {
val followers = Users.alias("FOLLOWERS")
return Users.innerJoin(
otherTable = UsersFollowers,
onColumn = { Users.id },
onColumn = { id },
otherColumn = { userId }
)
.innerJoin(
@ -149,7 +149,7 @@ class FollowerQueryServiceImpl : FollowerQueryService {
val followers = Users.alias("FOLLOWERS")
return Users.innerJoin(
otherTable = UsersFollowers,
onColumn = { Users.id },
onColumn = { id },
otherColumn = { userId }
)
.innerJoin(

View File

@ -1,39 +0,0 @@
package dev.usbharu.hideout.query
import dev.usbharu.hideout.domain.model.hideout.dto.PostResponse
import org.springframework.stereotype.Repository
@Suppress("LongParameterList")
@Repository
interface PostResponseQueryService {
suspend fun findById(id: Long, userId: Long?): PostResponse
suspend fun findAll(
since: Long? = null,
until: Long? = null,
minId: Long? = null,
maxId: Long? = null,
limit: Int? = null,
userId: Long? = null
): List<PostResponse>
suspend fun findByUserId(
userId: Long,
since: Long? = null,
until: Long? = null,
minId: Long? = null,
maxId: Long? = null,
limit: Int? = null,
userId2: Long? = null
): List<PostResponse>
suspend fun findByUserNameAndUserDomain(
name: String,
domain: String,
since: Long? = null,
until: Long? = null,
minId: Long? = null,
maxId: Long? = null,
limit: Int? = null,
userId: Long? = null
): List<PostResponse>
}

View File

@ -1,70 +0,0 @@
package dev.usbharu.hideout.query
import dev.usbharu.hideout.domain.model.hideout.dto.PostResponse
import dev.usbharu.hideout.exception.FailedToGetResourcesException
import dev.usbharu.hideout.repository.Posts
import dev.usbharu.hideout.repository.Users
import dev.usbharu.hideout.repository.toPost
import dev.usbharu.hideout.repository.toUser
import dev.usbharu.hideout.util.singleOr
import org.jetbrains.exposed.sql.and
import org.jetbrains.exposed.sql.innerJoin
import org.jetbrains.exposed.sql.select
import org.jetbrains.exposed.sql.selectAll
import org.springframework.stereotype.Repository
@Repository
class PostResponseQueryServiceImpl : PostResponseQueryService {
override suspend fun findById(id: Long, userId: Long?): PostResponse {
return Posts
.innerJoin(Users, onColumn = { Posts.userId }, otherColumn = { Users.id })
.select { Posts.id eq id }
.singleOr { FailedToGetResourcesException("id: $id,userId: $userId is a duplicate or does not exist.", it) }
.let { PostResponse.from(it.toPost(), it.toUser()) }
}
override suspend fun findAll(
since: Long?,
until: Long?,
minId: Long?,
maxId: Long?,
limit: Int?,
userId: Long?
): List<PostResponse> {
return Posts
.innerJoin(Users, onColumn = { Posts.userId }, otherColumn = { id })
.selectAll()
.map { PostResponse.from(it.toPost(), it.toUser()) }
}
override suspend fun findByUserId(
userId: Long,
since: Long?,
until: Long?,
minId: Long?,
maxId: Long?,
limit: Int?,
userId2: Long?
): List<PostResponse> {
return Posts
.innerJoin(Users, onColumn = { Posts.userId }, otherColumn = { id })
.select { Posts.userId eq userId }
.map { PostResponse.from(it.toPost(), it.toUser()) }
}
override suspend fun findByUserNameAndUserDomain(
name: String,
domain: String,
since: Long?,
until: Long?,
minId: Long?,
maxId: Long?,
limit: Int?,
userId: Long?
): List<PostResponse> {
return Posts
.innerJoin(Users, onColumn = { Posts.userId }, otherColumn = { id })
.select { Users.name eq name and (Users.domain eq domain) }
.map { PostResponse.from(it.toPost(), it.toUser()) }
}
}

View File

@ -1,61 +0,0 @@
package dev.usbharu.hideout.query
import dev.usbharu.hideout.domain.model.hideout.dto.Account
import dev.usbharu.hideout.domain.model.hideout.dto.ReactionResponse
import dev.usbharu.hideout.domain.model.hideout.entity.Reaction
import dev.usbharu.hideout.exception.FailedToGetResourcesException
import dev.usbharu.hideout.repository.Reactions
import dev.usbharu.hideout.repository.Users
import dev.usbharu.hideout.repository.toReaction
import dev.usbharu.hideout.util.singleOr
import org.jetbrains.exposed.sql.*
import org.jetbrains.exposed.sql.SqlExpressionBuilder.eq
import org.springframework.stereotype.Repository
@Repository
class ReactionQueryServiceImpl : ReactionQueryService {
override suspend fun findByPostId(postId: Long, userId: Long?): List<Reaction> {
return Reactions.select {
Reactions.postId.eq(postId)
}.map { it.toReaction() }
}
@Suppress("FunctionMaxLength")
override suspend fun findByPostIdAndUserIdAndEmojiId(postId: Long, userId: Long, emojiId: Long): Reaction {
return Reactions
.select {
Reactions.postId.eq(postId).and(Reactions.userId.eq(userId)).and(
Reactions.emojiId.eq(emojiId)
)
}
.singleOr {
FailedToGetResourcesException(
"postId: $postId,userId: $userId,emojiId: $emojiId is duplicate or does not exist.",
it
)
}
.toReaction()
}
override suspend fun reactionAlreadyExist(postId: Long, userId: Long, emojiId: Long): Boolean {
return Reactions.select {
Reactions.postId.eq(postId).and(Reactions.userId.eq(userId)).and(
Reactions.emojiId.eq(emojiId)
)
}.empty().not()
}
override suspend fun deleteByPostIdAndUserId(postId: Long, userId: Long) {
Reactions.deleteWhere { Reactions.postId.eq(postId).and(Reactions.userId.eq(userId)) }
}
override suspend fun findByPostIdWithUsers(postId: Long, userId: Long?): List<ReactionResponse> {
return Reactions
.leftJoin(Users, onColumn = { Reactions.userId }, otherColumn = { id })
.select { Reactions.postId.eq(postId) }
.groupBy { _: ResultRow -> ReactionResponse("", true, "", emptyList()) }
.map { entry: Map.Entry<ReactionResponse, List<ResultRow>> ->
entry.key.copy(accounts = entry.value.map { Account(it[Users.screenName], "", it[Users.url]) })
}
}
}

View File

@ -69,16 +69,7 @@ class StatusQueryServiceImpl : StatusQueryService {
inReplyToAccountId = null,
language = null,
text = it[Posts.text],
editedAt = null,
application = null,
poll = null,
card = null,
favourited = null,
reblogged = null,
muted = null,
bookmarked = null,
pinned = null,
filtered = null
editedAt = null
) to it[Posts.repostId]
}
@ -86,14 +77,14 @@ class StatusQueryServiceImpl : StatusQueryService {
return pairs
.map {
if (it.second != null) {
it.first.copy(reblog = statuses.find { status -> status.id == it.second.toString() })
it.first.copy(reblog = statuses.find { (id) -> id == it.second.toString() })
} else {
it.first
}
}
.map {
if (it.inReplyToId != null) {
it.copy(inReplyToAccountId = statuses.find { status -> status.id == it.inReplyToId }?.id)
it.copy(inReplyToAccountId = statuses.find { (id) -> id == it.inReplyToId }?.id)
} else {
it
}

View File

@ -0,0 +1,10 @@
package dev.usbharu.hideout.repository
import dev.usbharu.hideout.domain.model.hideout.entity.Media
interface MediaRepository {
suspend fun generateId(): Long
suspend fun save(media: Media): Media
suspend fun findById(id: Long): Media
suspend fun delete(id: Long)
}

View File

@ -0,0 +1,80 @@
package dev.usbharu.hideout.repository
import dev.usbharu.hideout.domain.model.hideout.dto.FileType
import dev.usbharu.hideout.exception.FailedToGetResourcesException
import dev.usbharu.hideout.service.core.IdGenerateService
import dev.usbharu.hideout.util.singleOr
import org.jetbrains.exposed.sql.*
import org.jetbrains.exposed.sql.SqlExpressionBuilder.eq
import org.springframework.stereotype.Repository
import dev.usbharu.hideout.domain.model.hideout.entity.Media as EntityMedia
@Repository
class MediaRepositoryImpl(private val idGenerateService: IdGenerateService) : MediaRepository {
override suspend fun generateId(): Long = idGenerateService.generateId()
override suspend fun save(media: EntityMedia): EntityMedia {
if (Media.select {
Media.id eq media.id
}.singleOrNull() != null
) {
Media.update({ Media.id eq media.id }) {
it[Media.name] = media.name
it[Media.url] = media.url
it[Media.remoteUrl] = media.remoteUrl
it[Media.thumbnailUrl] = media.thumbnailUrl
it[Media.type] = media.type.ordinal
it[Media.blurhash] = media.blurHash
}
} else {
Media.insert {
it[Media.id] = media.id
it[Media.name] = media.name
it[Media.url] = media.url
it[Media.remoteUrl] = media.remoteUrl
it[Media.thumbnailUrl] = media.thumbnailUrl
it[Media.type] = media.type.ordinal
it[Media.blurhash] = media.blurHash
}
}
return media
}
override suspend fun findById(id: Long): EntityMedia {
return Media
.select {
Media.id eq id
}
.singleOr {
FailedToGetResourcesException("id: $id was not found.")
}.toMedia()
}
override suspend fun delete(id: Long) {
Media.deleteWhere {
Media.id eq id
}
}
fun ResultRow.toMedia(): EntityMedia {
return EntityMedia(
id = this[Media.id],
name = this[Media.name],
url = this[Media.url],
remoteUrl = this[Media.remoteUrl],
thumbnailUrl = this[Media.thumbnailUrl],
type = FileType.values().first { it.ordinal == this[Media.type] },
blurHash = this[Media.blurhash],
)
}
}
object Media : Table("media") {
val id = long("id")
val name = varchar("name", 255)
val url = varchar("url", 255)
val remoteUrl = varchar("remote_url", 255).nullable()
val thumbnailUrl = varchar("thumbnail_url", 255).nullable()
val type = integer("type")
val blurhash = varchar("blurhash", 255).nullable()
}

View File

@ -1,10 +1,7 @@
package dev.usbharu.hideout.repository
import dev.usbharu.hideout.domain.model.hideout.entity.Jwt
import org.jetbrains.exposed.sql.Table
import org.jetbrains.exposed.sql.insert
import org.jetbrains.exposed.sql.select
import org.jetbrains.exposed.sql.update
import org.jetbrains.exposed.sql.*
import org.springframework.stereotype.Repository
import java.util.*
@ -41,10 +38,10 @@ class MetaRepositoryImpl : MetaRepository {
}
object Meta : Table("meta_info") {
val id = long("id")
val version = varchar("version", 1000)
val kid = varchar("kid", 1000)
val jwtPrivateKey = varchar("jwt_private_key", 100000)
val jwtPublicKey = varchar("jwt_public_key", 100000)
val id: Column<Long> = long("id")
val version: Column<String> = varchar("version", 1000)
val kid: Column<String> = varchar("kid", 1000)
val jwtPrivateKey: Column<String> = varchar("jwt_private_key", 100000)
val jwtPublicKey: Column<String> = varchar("jwt_public_key", 100000)
override val primaryKey: PrimaryKey = PrimaryKey(id)
}

View File

@ -55,17 +55,17 @@ class PostRepositoryImpl(private val idGenerateService: IdGenerateService) : Pos
}
object Posts : Table() {
val id = long("id")
val userId = long("userId").references(Users.id)
val overview = varchar("overview", 100).nullable()
val text = varchar("text", 3000)
val createdAt = long("createdAt")
val visibility = integer("visibility").default(0)
val url = varchar("url", 500)
val repostId = long("repostId").references(id).nullable()
val replyId = long("replyId").references(id).nullable()
val sensitive = bool("sensitive").default(false)
val apId = varchar("ap_id", 100).uniqueIndex()
val id: Column<Long> = long("id")
val userId: Column<Long> = long("userId").references(Users.id)
val overview: Column<String?> = varchar("overview", 100).nullable()
val text: Column<String> = varchar("text", 3000)
val createdAt: Column<Long> = long("createdAt")
val visibility: Column<Int> = integer("visibility").default(0)
val url: Column<String> = varchar("url", 500)
val repostId: Column<Long?> = long("repostId").references(id).nullable()
val replyId: Column<Long?> = long("replyId").references(id).nullable()
val sensitive: Column<Boolean> = bool("sensitive").default(false)
val apId: Column<String> = varchar("ap_id", 100).uniqueIndex()
override val primaryKey: PrimaryKey = PrimaryKey(id)
}

View File

@ -12,7 +12,6 @@ class ReactionRepositoryImpl(
private val idGenerateService: IdGenerateService
) : ReactionRepository {
override suspend fun generateId(): Long = idGenerateService.generateId()
override suspend fun save(reaction: Reaction): Reaction {
@ -54,9 +53,9 @@ fun ResultRow.toReaction(): Reaction {
}
object Reactions : LongIdTable("reactions") {
val emojiId = long("emoji_id")
val postId = long("post_id").references(Posts.id)
val userId = long("user_id").references(Users.id)
val emojiId: Column<Long> = long("emoji_id")
val postId: Column<Long> = long("post_id").references(Posts.id)
val userId: Column<Long> = long("user_id").references(Users.id)
init {
uniqueIndex(emojiId, postId, userId)

View File

@ -10,6 +10,7 @@ import dev.usbharu.hideout.service.auth.ExposedOAuth2AuthorizationService
import org.jetbrains.exposed.sql.*
import org.jetbrains.exposed.sql.javatime.CurrentTimestamp
import org.jetbrains.exposed.sql.javatime.timestamp
import org.slf4j.Logger
import org.slf4j.LoggerFactory
import org.springframework.security.jackson2.SecurityJackson2Modules
import org.springframework.security.oauth2.core.AuthorizationGrantType
@ -40,9 +41,9 @@ class RegisteredClientRepositoryImpl : RegisteredClientRepository {
it[clientSecretExpiresAt] = registeredClient.clientSecretExpiresAt
it[clientName] = registeredClient.clientName
it[clientAuthenticationMethods] =
registeredClient.clientAuthenticationMethods.map { method -> method.value }.joinToString(",")
registeredClient.clientAuthenticationMethods.joinToString(",") { method -> method.value }
it[authorizationGrantTypes] =
registeredClient.authorizationGrantTypes.map { type -> type.value }.joinToString(",")
registeredClient.authorizationGrantTypes.joinToString(",") { type -> type.value }
it[redirectUris] = registeredClient.redirectUris.joinToString(",")
it[postLogoutRedirectUris] = registeredClient.postLogoutRedirectUris.joinToString(",")
it[scopes] = registeredClient.scopes.joinToString(",")
@ -84,7 +85,7 @@ class RegisteredClientRepositoryImpl : RegisteredClientRepository {
val toRegisteredClient = RegisteredClient.select {
RegisteredClient.clientId eq clientId
}.singleOrNull()?.toRegisteredClient()
LOGGER.trace("findByClientId: $toRegisteredClient")
LOGGER.trace("findByClientId: {}", toRegisteredClient)
return toRegisteredClient
}
@ -157,7 +158,7 @@ class RegisteredClientRepositoryImpl : RegisteredClientRepository {
companion object {
val objectMapper: ObjectMapper = ObjectMapper()
val LOGGER = LoggerFactory.getLogger(RegisteredClientRepositoryImpl::class.java)
val LOGGER: Logger = LoggerFactory.getLogger(RegisteredClientRepositoryImpl::class.java)
init {
@ -172,19 +173,19 @@ class RegisteredClientRepositoryImpl : RegisteredClientRepository {
// org/springframework/security/oauth2/server/authorization/client/oauth2-registered-client-schema.sql
object RegisteredClient : Table("registered_client") {
val id = varchar("id", 100)
val clientId = varchar("client_id", 100)
val clientIdIssuedAt = timestamp("client_id_issued_at").defaultExpression(CurrentTimestamp())
val clientSecret = varchar("client_secret", 200).nullable().default(null)
val clientSecretExpiresAt = timestamp("client_secret_expires_at").nullable().default(null)
val clientName = varchar("client_name", 200)
val clientAuthenticationMethods = varchar("client_authentication_methods", 1000)
val authorizationGrantTypes = varchar("authorization_grant_types", 1000)
val redirectUris = varchar("redirect_uris", 1000).nullable().default(null)
val postLogoutRedirectUris = varchar("post_logout_redirect_uris", 1000).nullable().default(null)
val scopes = varchar("scopes", 1000)
val clientSettings = varchar("client_settings", 2000)
val tokenSettings = varchar("token_settings", 2000)
val id: Column<String> = varchar("id", 100)
val clientId: Column<String> = varchar("client_id", 100)
val clientIdIssuedAt: Column<Instant> = timestamp("client_id_issued_at").defaultExpression(CurrentTimestamp())
val clientSecret: Column<String?> = varchar("client_secret", 200).nullable().default(null)
val clientSecretExpiresAt: Column<Instant?> = timestamp("client_secret_expires_at").nullable().default(null)
val clientName: Column<String> = varchar("client_name", 200)
val clientAuthenticationMethods: Column<String> = varchar("client_authentication_methods", 1000)
val authorizationGrantTypes: Column<String> = varchar("authorization_grant_types", 1000)
val redirectUris: Column<String?> = varchar("redirect_uris", 1000).nullable().default(null)
val postLogoutRedirectUris: Column<String?> = varchar("post_logout_redirect_uris", 1000).nullable().default(null)
val scopes: Column<String> = varchar("scopes", 1000)
val clientSettings: Column<String> = varchar("client_settings", 2000)
val tokenSettings: Column<String> = varchar("token_settings", 2000)
override val primaryKey = PrimaryKey(id)
override val primaryKey: PrimaryKey = PrimaryKey(id)
}

View File

@ -71,18 +71,24 @@ class UserRepositoryImpl(private val idGenerateService: IdGenerateService) :
}
object Users : Table("users") {
val id = long("id")
val name = varchar("name", length = Config.configData.characterLimit.account.id)
val domain = varchar("domain", length = Config.configData.characterLimit.general.domain)
val screenName = varchar("screen_name", length = Config.configData.characterLimit.account.name)
val description = varchar("description", length = Config.configData.characterLimit.account.description)
val password = varchar("password", length = 255).nullable()
val inbox = varchar("inbox", length = Config.configData.characterLimit.general.url).uniqueIndex()
val outbox = varchar("outbox", length = Config.configData.characterLimit.general.url).uniqueIndex()
val url = varchar("url", length = Config.configData.characterLimit.general.url).uniqueIndex()
val publicKey = varchar("public_key", length = Config.configData.characterLimit.general.publicKey)
val privateKey = varchar("private_key", length = Config.configData.characterLimit.general.privateKey).nullable()
val createdAt = long("created_at")
val id: Column<Long> = long("id")
val name: Column<String> = varchar("name", length = Config.configData.characterLimit.account.id)
val domain: Column<String> = varchar("domain", length = Config.configData.characterLimit.general.domain)
val screenName: Column<String> = varchar("screen_name", length = Config.configData.characterLimit.account.name)
val description: Column<String> = varchar(
"description",
length = Config.configData.characterLimit.account.description
)
val password: Column<String?> = varchar("password", length = 255).nullable()
val inbox: Column<String> = varchar("inbox", length = Config.configData.characterLimit.general.url).uniqueIndex()
val outbox: Column<String> = varchar("outbox", length = Config.configData.characterLimit.general.url).uniqueIndex()
val url: Column<String> = varchar("url", length = Config.configData.characterLimit.general.url).uniqueIndex()
val publicKey: Column<String> = varchar("public_key", length = Config.configData.characterLimit.general.publicKey)
val privateKey: Column<String?> = varchar(
"private_key",
length = Config.configData.characterLimit.general.privateKey
).nullable()
val createdAt: Column<Long> = long("created_at")
override val primaryKey: PrimaryKey = PrimaryKey(id)
@ -109,8 +115,8 @@ fun ResultRow.toUser(): User {
}
object UsersFollowers : LongIdTable("users_followers") {
val userId = long("user_id").references(Users.id).index()
val followerId = long("follower_id").references(Users.id)
val userId: Column<Long> = long("user_id").references(Users.id).index()
val followerId: Column<Long> = long("follower_id").references(Users.id)
init {
uniqueIndex(userId, followerId)
@ -118,8 +124,8 @@ object UsersFollowers : LongIdTable("users_followers") {
}
object FollowRequests : LongIdTable("follow_requests") {
val userId = long("user_id").references(Users.id)
val followerId = long("follower_id").references(Users.id)
val userId: Column<Long> = long("user_id").references(Users.id)
val followerId: Column<Long> = long("follower_id").references(Users.id)
init {
uniqueIndex(userId, followerId)

View File

@ -12,7 +12,6 @@ import dev.usbharu.hideout.service.user.UserService
import io.ktor.http.*
import org.springframework.stereotype.Service
@Service
interface APAcceptService {
suspend fun receiveAccept(accept: Accept): ActivityPubResponse
}

View File

@ -9,7 +9,6 @@ import dev.usbharu.hideout.service.core.Transaction
import io.ktor.http.*
import org.springframework.stereotype.Service
@Service
interface APCreateService {
suspend fun receiveCreate(create: Create): ActivityPubResponse
}

View File

@ -10,7 +10,6 @@ import dev.usbharu.hideout.service.reaction.ReactionService
import io.ktor.http.*
import org.springframework.stereotype.Service
@Service
interface APLikeService {
suspend fun receiveLike(like: Like): ActivityPubResponse
}
@ -29,9 +28,9 @@ class APLikeServiceImpl(
like.`object` ?: throw IllegalActivityPubObjectException("object is null")
transaction.transaction(java.sql.Connection.TRANSACTION_SERIALIZABLE) {
val person = apUserService.fetchPersonWithEntity(actor)
apNoteService.fetchNote(like.`object`!!)
apNoteService.fetchNote(like.`object` ?: return@transaction)
val post = postQueryService.findByUrl(like.`object`!!)
val post = postQueryService.findByUrl(like.`object` ?: return@transaction)
reactionService.receiveReaction(
content,

View File

@ -27,7 +27,6 @@ import org.springframework.beans.factory.annotation.Qualifier
import org.springframework.stereotype.Service
import java.time.Instant
@Service
interface APNoteService {
suspend fun createNote(post: Post)
@ -57,7 +56,7 @@ class APNoteServiceImpl(
postService.addInterceptor(this)
}
private val logger = LoggerFactory.getLogger(this::class.java)
private val logger = LoggerFactory.getLogger(APNoteServiceImpl::class.java)
override suspend fun createNote(post: Post) {
val followers = followerQueryService.findFollowersById(post.userId)
@ -81,7 +80,7 @@ class APNoteServiceImpl(
attributedTo = actor,
content = postEntity.text,
published = Instant.ofEpochMilli(postEntity.createdAt).toString(),
to = listOf(public, actor + "/follower")
to = listOf(public, "$actor/follower")
)
val inbox = props[DeliverPostJob.inbox]
logger.debug("createNoteJob: actor={}, note={}, inbox={}", actor, postEntity, inbox)
@ -173,12 +172,10 @@ class APNoteServiceImpl(
Post.of(
id = postRepository.generateId(),
userId = person.second.id,
overview = null,
text = note.content.orEmpty(),
createdAt = Instant.parse(note.published).toEpochMilli(),
visibility = visibility,
url = note.id ?: url,
repostId = null,
replyId = reply?.id,
sensitive = note.sensitive,
apId = note.id ?: url,

View File

@ -19,7 +19,6 @@ import org.springframework.beans.factory.annotation.Qualifier
import org.springframework.stereotype.Service
import java.time.Instant
@Service
interface APReactionService {
suspend fun reaction(like: Reaction)
suspend fun removeReaction(like: Reaction)

View File

@ -8,7 +8,6 @@ import io.ktor.client.*
import org.springframework.beans.factory.annotation.Qualifier
import org.springframework.stereotype.Service
@Service
interface APSendFollowService {
suspend fun sendFollow(sendFollowDto: SendFollowDto)
}

View File

@ -14,7 +14,6 @@ import org.slf4j.LoggerFactory
import org.springframework.beans.factory.annotation.Qualifier
import org.springframework.stereotype.Service
@Service
interface APService {
fun parseActivity(json: String): ActivityType
@ -186,7 +185,7 @@ class APServiceImpl(
@Qualifier("activitypub") private val objectMapper: ObjectMapper
) : APService {
val logger: Logger = LoggerFactory.getLogger(this::class.java)
val logger: Logger = LoggerFactory.getLogger(APServiceImpl::class.java)
override fun parseActivity(json: String): ActivityType {
val readTree = objectMapper.readTree(json)
logger.trace("readTree: {}", readTree)
@ -229,16 +228,16 @@ class APServiceImpl(
// println(apReceiveFollowService::class.java)
// apReceiveFollowService.receiveFollowJob(job.props as JobProps<ReceiveFollowJob>)
when {
hideoutJob is ReceiveFollowJob -> {
when (hideoutJob) {
is ReceiveFollowJob -> {
apReceiveFollowService.receiveFollowJob(
job.props as JobProps<ReceiveFollowJob>
)
}
hideoutJob is DeliverPostJob -> apNoteService.createNoteJob(job.props as JobProps<DeliverPostJob>)
hideoutJob is DeliverReactionJob -> apReactionService.reactionJob(job.props as JobProps<DeliverReactionJob>)
hideoutJob is DeliverRemoveReactionJob -> apReactionService.removeReactionJob(
is DeliverPostJob -> apNoteService.createNoteJob(job.props as JobProps<DeliverPostJob>)
is DeliverReactionJob -> apReactionService.reactionJob(job.props as JobProps<DeliverReactionJob>)
is DeliverRemoveReactionJob -> apReactionService.removeReactionJob(
job.props as JobProps<DeliverRemoveReactionJob>
)

View File

@ -10,7 +10,6 @@ import dev.usbharu.hideout.service.user.UserService
import io.ktor.http.*
import org.springframework.stereotype.Service
@Service
interface APUndoService {
suspend fun receiveUndo(undo: Undo): ActivityPubResponse
}

View File

@ -22,7 +22,6 @@ import io.ktor.http.*
import org.springframework.beans.factory.annotation.Qualifier
import org.springframework.stereotype.Service
@Service
interface APUserService {
suspend fun getPersonByName(name: String): Person

View File

@ -1,129 +0,0 @@
package dev.usbharu.hideout.service.api
import dev.usbharu.hideout.config.ApplicationConfig
import dev.usbharu.hideout.domain.model.hideout.dto.PostCreateDto
import dev.usbharu.hideout.domain.model.hideout.dto.PostResponse
import dev.usbharu.hideout.domain.model.hideout.dto.ReactionResponse
import dev.usbharu.hideout.domain.model.hideout.form.Post
import dev.usbharu.hideout.query.PostResponseQueryService
import dev.usbharu.hideout.query.ReactionQueryService
import dev.usbharu.hideout.repository.UserRepository
import dev.usbharu.hideout.service.core.Transaction
import dev.usbharu.hideout.service.post.PostService
import dev.usbharu.hideout.service.reaction.ReactionService
import dev.usbharu.hideout.util.AcctUtil
import org.springframework.stereotype.Service
import java.time.Instant
@Suppress("LongParameterList")
@Service
interface PostApiService {
suspend fun createPost(postForm: dev.usbharu.hideout.domain.model.hideout.form.Post, userId: Long): PostResponse
suspend fun getById(id: Long, userId: Long?): PostResponse
suspend fun getAll(
since: Instant? = null,
until: Instant? = null,
minId: Long? = null,
maxId: Long? = null,
limit: Int? = null,
userId: Long? = null
): List<PostResponse>
suspend fun getByUser(
nameOrId: String,
since: Instant? = null,
until: Instant? = null,
minId: Long? = null,
maxId: Long? = null,
limit: Int? = null,
userId: Long? = null
): List<PostResponse>
suspend fun getReactionByPostId(postId: Long, userId: Long? = null): List<ReactionResponse>
suspend fun appendReaction(reaction: String, userId: Long, postId: Long)
suspend fun removeReaction(userId: Long, postId: Long)
}
@Service
class PostApiServiceImpl(
private val postService: PostService,
private val userRepository: UserRepository,
private val postResponseQueryService: PostResponseQueryService,
private val reactionQueryService: ReactionQueryService,
private val reactionService: ReactionService,
private val transaction: Transaction,
private val applicationConfig: ApplicationConfig
) : PostApiService {
override suspend fun createPost(postForm: Post, userId: Long): PostResponse {
return transaction.transaction {
val createdPost = postService.createLocal(
PostCreateDto(
text = postForm.text,
overview = postForm.overview,
visibility = postForm.visibility,
repostId = postForm.repostId,
repolyId = postForm.replyId,
userId = userId
)
)
val creator = userRepository.findById(userId)
PostResponse.from(createdPost, creator!!)
}
}
override suspend fun getById(id: Long, userId: Long?): PostResponse = postResponseQueryService.findById(id, userId)
override suspend fun getAll(
since: Instant?,
until: Instant?,
minId: Long?,
maxId: Long?,
limit: Int?,
userId: Long?
): List<PostResponse> = transaction.transaction {
postResponseQueryService.findAll(
since = since?.toEpochMilli(),
until = until?.toEpochMilli(),
minId = minId,
maxId = maxId,
limit = limit,
userId = userId
)
}
override suspend fun getByUser(
nameOrId: String,
since: Instant?,
until: Instant?,
minId: Long?,
maxId: Long?,
limit: Int?,
userId: Long?
): List<PostResponse> {
val idOrNull = nameOrId.toLongOrNull()
return if (idOrNull == null) {
val acct = AcctUtil.parse(nameOrId)
postResponseQueryService.findByUserNameAndUserDomain(
acct.username,
acct.domain ?: applicationConfig.url.host
)
} else {
postResponseQueryService.findByUserId(idOrNull)
}
}
override suspend fun getReactionByPostId(postId: Long, userId: Long?): List<ReactionResponse> =
transaction.transaction { reactionQueryService.findByPostIdWithUsers(postId, userId) }
override suspend fun appendReaction(reaction: String, userId: Long, postId: Long) {
transaction.transaction {
reactionService.sendReaction(reaction, userId, postId)
}
}
override suspend fun removeReaction(userId: Long, postId: Long) {
transaction.transaction {
reactionService.removeReaction(userId, postId)
}
}
}

View File

@ -1,116 +0,0 @@
package dev.usbharu.hideout.service.api
import dev.usbharu.hideout.config.ApplicationConfig
import dev.usbharu.hideout.domain.model.Acct
import dev.usbharu.hideout.domain.model.hideout.dto.UserCreateDto
import dev.usbharu.hideout.domain.model.hideout.dto.UserResponse
import dev.usbharu.hideout.exception.UsernameAlreadyExistException
import dev.usbharu.hideout.query.FollowerQueryService
import dev.usbharu.hideout.query.UserQueryService
import dev.usbharu.hideout.service.core.Transaction
import dev.usbharu.hideout.service.user.UserService
import org.springframework.stereotype.Service
import kotlin.math.min
@Suppress("TooManyFunctions")
@Service
interface UserApiService {
suspend fun findAll(limit: Int? = 100, offset: Long = 0): List<UserResponse>
suspend fun findById(id: Long): UserResponse
suspend fun findByIds(ids: List<Long>): List<UserResponse>
suspend fun findByAcct(acct: Acct): UserResponse
suspend fun findFollowers(userId: Long): List<UserResponse>
suspend fun findFollowings(userId: Long): List<UserResponse>
suspend fun findFollowersByAcct(acct: Acct): List<UserResponse>
suspend fun findFollowingsByAcct(acct: Acct): List<UserResponse>
suspend fun createUser(username: String, password: String): UserResponse
suspend fun follow(targetId: Long, sourceId: Long): Boolean
suspend fun follow(targetAcct: Acct, sourceId: Long): Boolean
}
@Service
class UserApiServiceImpl(
private val userQueryService: UserQueryService,
private val followerQueryService: FollowerQueryService,
private val userService: UserService,
private val transaction: Transaction,
private val applicationConfig: ApplicationConfig
) : UserApiService {
override suspend fun findAll(limit: Int?, offset: Long): List<UserResponse> = transaction.transaction {
userQueryService.findAll(min(limit ?: 100, 100), offset).map { UserResponse.from(it) }
}
override suspend fun findById(id: Long): UserResponse =
transaction.transaction { UserResponse.from(userQueryService.findById(id)) }
override suspend fun findByIds(ids: List<Long>): List<UserResponse> {
return transaction.transaction {
userQueryService.findByIds(ids).map { UserResponse.from(it) }
}
}
override suspend fun findByAcct(acct: Acct): UserResponse {
return transaction.transaction {
UserResponse.from(
userQueryService.findByNameAndDomain(
acct.username,
acct.domain ?: applicationConfig.url.host
)
)
}
}
override suspend fun findFollowers(userId: Long): List<UserResponse> = transaction.transaction {
followerQueryService.findFollowersById(userId).map { UserResponse.from(it) }
}
override suspend fun findFollowings(userId: Long): List<UserResponse> = transaction.transaction {
followerQueryService.findFollowingById(userId).map { UserResponse.from(it) }
}
override suspend fun findFollowersByAcct(acct: Acct): List<UserResponse> = transaction.transaction {
followerQueryService.findFollowersByNameAndDomain(acct.username, acct.domain ?: applicationConfig.url.host)
.map { UserResponse.from(it) }
}
override suspend fun findFollowingsByAcct(acct: Acct): List<UserResponse> = transaction.transaction {
followerQueryService.findFollowingByNameAndDomain(acct.username, acct.domain ?: applicationConfig.url.host)
.map { UserResponse.from(it) }
}
override suspend fun createUser(username: String, password: String): UserResponse {
return transaction.transaction {
if (userQueryService.existByNameAndDomain(username, applicationConfig.url.host)) {
throw UsernameAlreadyExistException()
}
UserResponse.from(userService.createLocalUser(UserCreateDto(username, username, "", password)))
}
}
override suspend fun follow(targetId: Long, sourceId: Long): Boolean {
return transaction.transaction {
userService.followRequest(targetId, sourceId)
}
}
override suspend fun follow(targetAcct: Acct, sourceId: Long): Boolean {
return transaction.transaction {
userService.followRequest(
userQueryService.findByNameAndDomain(
targetAcct.username,
targetAcct.domain ?: applicationConfig.url.host
).id,
sourceId
)
}
}
}

View File

@ -0,0 +1,10 @@
package dev.usbharu.hideout.service.api.mastodon
import dev.usbharu.hideout.domain.mastodon.model.generated.MediaAttachment
import dev.usbharu.hideout.domain.model.hideout.form.Media
import org.springframework.stereotype.Service
@Service
interface MediaApiService {
suspend fun postMedia(media: Media): MediaAttachment
}

View File

@ -0,0 +1,35 @@
package dev.usbharu.hideout.service.api.mastodon
import dev.usbharu.hideout.domain.mastodon.model.generated.MediaAttachment
import dev.usbharu.hideout.domain.model.hideout.dto.FileType
import dev.usbharu.hideout.domain.model.hideout.form.Media
import dev.usbharu.hideout.service.core.Transaction
import dev.usbharu.hideout.service.media.MediaService
import org.springframework.stereotype.Service
@Service
class MediaApiServiceImpl(private val mediaService: MediaService, private val transaction: Transaction) :
MediaApiService {
override suspend fun postMedia(media: Media): MediaAttachment {
return transaction.transaction {
val uploadLocalMedia = mediaService.uploadLocalMedia(media)
val type = when (uploadLocalMedia.type) {
FileType.Image -> MediaAttachment.Type.image
FileType.Video -> MediaAttachment.Type.video
FileType.Audio -> MediaAttachment.Type.audio
FileType.Unknown -> MediaAttachment.Type.unknown
}
return@transaction MediaAttachment(
id = uploadLocalMedia.id.toString(),
type = type,
url = uploadLocalMedia.url,
previewUrl = uploadLocalMedia.thumbnailUrl,
remoteUrl = null,
description = media.description,
blurhash = uploadLocalMedia.blurHash,
textUrl = uploadLocalMedia.url
)
}
}
}

View File

@ -42,7 +42,6 @@ class StatsesApiServiceImpl(
text = statusesRequest.status.orEmpty(),
overview = statusesRequest.spoilerText,
visibility = visibility,
repostId = null,
repolyId = statusesRequest.inReplyToId?.toLongOrNull(),
userId = userId
)
@ -86,19 +85,9 @@ class StatsesApiServiceImpl(
url = post.url,
inReplyToId = post.replyId?.toString(),
inReplyToAccountId = replyUser?.toString(),
reblog = null,
language = null,
text = post.text,
editedAt = null,
application = null,
poll = null,
card = null,
favourited = null,
reblogged = null,
muted = null,
bookmarked = null,
pinned = null,
filtered = null
editedAt = null
)
}
}

View File

@ -60,8 +60,6 @@ class TimelineApiServiceImpl(
): List<Status> = transaction.transaction {
generateTimelineService.getTimeline(
forUserId = userId,
localOnly = false,
mediaOnly = false,
maxId = maxId,
minId = minId,
sinceId = sinceId,

View File

@ -17,7 +17,7 @@ class ExposedOAuth2AuthorizationConsentService(
) :
OAuth2AuthorizationConsentService {
override fun save(authorizationConsent: AuthorizationConsent?) = runBlocking {
override fun save(authorizationConsent: AuthorizationConsent?): Unit = runBlocking {
requireNotNull(authorizationConsent)
transaction.transaction {
val singleOrNull =
@ -74,8 +74,8 @@ class ExposedOAuth2AuthorizationConsentService(
}
object OAuth2AuthorizationConsent : Table("oauth2_authorization_consent") {
val registeredClientId = varchar("registered_client_id", 100)
val principalName = varchar("principal_name", 200)
val authorities = varchar("authorities", 1000)
override val primaryKey = PrimaryKey(registeredClientId, principalName)
val registeredClientId: Column<String> = varchar("registered_client_id", 100)
val principalName: Column<String> = varchar("principal_name", 200)
val authorities: Column<String> = varchar("authorities", 1000)
override val primaryKey: PrimaryKey = PrimaryKey(registeredClientId, principalName)
}

View File

@ -23,6 +23,7 @@ import org.springframework.security.oauth2.server.authorization.OAuth2TokenType
import org.springframework.security.oauth2.server.authorization.client.RegisteredClientRepository
import org.springframework.security.oauth2.server.authorization.jackson2.OAuth2AuthorizationServerJackson2Module
import org.springframework.stereotype.Service
import java.time.Instant
@Service
class ExposedOAuth2AuthorizationService(
@ -133,7 +134,7 @@ class ExposedOAuth2AuthorizationService(
if (authorization == null) {
return
}
Authorization.deleteWhere { Authorization.id eq authorization.id }
Authorization.deleteWhere { id eq authorization.id }
}
override fun findById(id: String?): OAuth2Authorization? {
@ -336,40 +337,44 @@ class ExposedOAuth2AuthorizationService(
}
object Authorization : Table("application_authorization") {
val id = varchar("id", 255)
val registeredClientId = varchar("registered_client_id", 255)
val principalName = varchar("principal_name", 255)
val authorizationGrantType = varchar("authorization_grant_type", 255)
val authorizedScopes = varchar("authorized_scopes", 1000).nullable().default(null)
val attributes = varchar("attributes", 4000).nullable().default(null)
val state = varchar("state", 500).nullable().default(null)
val authorizationCodeValue = varchar("authorization_code_value", 4000).nullable().default(null)
val authorizationCodeIssuedAt = timestamp("authorization_code_issued_at").nullable().default(null)
val authorizationCodeExpiresAt = timestamp("authorization_code_expires_at").nullable().default(null)
val authorizationCodeMetadata = varchar("authorization_code_metadata", 2000).nullable().default(null)
val accessTokenValue = varchar("access_token_value", 4000).nullable().default(null)
val accessTokenIssuedAt = timestamp("access_token_issued_at").nullable().default(null)
val accessTokenExpiresAt = timestamp("access_token_expires_at").nullable().default(null)
val accessTokenMetadata = varchar("access_token_metadata", 2000).nullable().default(null)
val accessTokenType = varchar("access_token_type", 255).nullable().default(null)
val accessTokenScopes = varchar("access_token_scopes", 1000).nullable().default(null)
val refreshTokenValue = varchar("refresh_token_value", 4000).nullable().default(null)
val refreshTokenIssuedAt = timestamp("refresh_token_issued_at").nullable().default(null)
val refreshTokenExpiresAt = timestamp("refresh_token_expires_at").nullable().default(null)
val refreshTokenMetadata = varchar("refresh_token_metadata", 2000).nullable().default(null)
val oidcIdTokenValue = varchar("oidc_id_token_value", 4000).nullable().default(null)
val oidcIdTokenIssuedAt = timestamp("oidc_id_token_issued_at").nullable().default(null)
val oidcIdTokenExpiresAt = timestamp("oidc_id_token_expires_at").nullable().default(null)
val oidcIdTokenMetadata = varchar("oidc_id_token_metadata", 2000).nullable().default(null)
val oidcIdTokenClaims = varchar("oidc_id_token_claims", 2000).nullable().default(null)
val userCodeValue = varchar("user_code_value", 4000).nullable().default(null)
val userCodeIssuedAt = timestamp("user_code_issued_at").nullable().default(null)
val userCodeExpiresAt = timestamp("user_code_expires_at").nullable().default(null)
val userCodeMetadata = varchar("user_code_metadata", 2000).nullable().default(null)
val deviceCodeValue = varchar("device_code_value", 4000).nullable().default(null)
val deviceCodeIssuedAt = timestamp("device_code_issued_at").nullable().default(null)
val deviceCodeExpiresAt = timestamp("device_code_expires_at").nullable().default(null)
val deviceCodeMetadata = varchar("device_code_metadata", 2000).nullable().default(null)
val id: Column<String> = varchar("id", 255)
val registeredClientId: Column<String> = varchar("registered_client_id", 255)
val principalName: Column<String> = varchar("principal_name", 255)
val authorizationGrantType: Column<String> = varchar("authorization_grant_type", 255)
val authorizedScopes: Column<String?> = varchar("authorized_scopes", 1000).nullable().default(null)
val attributes: Column<String?> = varchar("attributes", 4000).nullable().default(null)
val state: Column<String?> = varchar("state", 500).nullable().default(null)
val authorizationCodeValue: Column<String?> = varchar("authorization_code_value", 4000).nullable().default(null)
val authorizationCodeIssuedAt: Column<Instant?> = timestamp("authorization_code_issued_at").nullable().default(null)
val authorizationCodeExpiresAt: Column<Instant?> = timestamp("authorization_code_expires_at").nullable().default(
null
)
val authorizationCodeMetadata: Column<String?> = varchar("authorization_code_metadata", 2000).nullable().default(
null
)
val accessTokenValue: Column<String?> = varchar("access_token_value", 4000).nullable().default(null)
val accessTokenIssuedAt: Column<Instant?> = timestamp("access_token_issued_at").nullable().default(null)
val accessTokenExpiresAt: Column<Instant?> = timestamp("access_token_expires_at").nullable().default(null)
val accessTokenMetadata: Column<String?> = varchar("access_token_metadata", 2000).nullable().default(null)
val accessTokenType: Column<String?> = varchar("access_token_type", 255).nullable().default(null)
val accessTokenScopes: Column<String?> = varchar("access_token_scopes", 1000).nullable().default(null)
val refreshTokenValue: Column<String?> = varchar("refresh_token_value", 4000).nullable().default(null)
val refreshTokenIssuedAt: Column<Instant?> = timestamp("refresh_token_issued_at").nullable().default(null)
val refreshTokenExpiresAt: Column<Instant?> = timestamp("refresh_token_expires_at").nullable().default(null)
val refreshTokenMetadata: Column<String?> = varchar("refresh_token_metadata", 2000).nullable().default(null)
val oidcIdTokenValue: Column<String?> = varchar("oidc_id_token_value", 4000).nullable().default(null)
val oidcIdTokenIssuedAt: Column<Instant?> = timestamp("oidc_id_token_issued_at").nullable().default(null)
val oidcIdTokenExpiresAt: Column<Instant?> = timestamp("oidc_id_token_expires_at").nullable().default(null)
val oidcIdTokenMetadata: Column<String?> = varchar("oidc_id_token_metadata", 2000).nullable().default(null)
val oidcIdTokenClaims: Column<String?> = varchar("oidc_id_token_claims", 2000).nullable().default(null)
val userCodeValue: Column<String?> = varchar("user_code_value", 4000).nullable().default(null)
val userCodeIssuedAt: Column<Instant?> = timestamp("user_code_issued_at").nullable().default(null)
val userCodeExpiresAt: Column<Instant?> = timestamp("user_code_expires_at").nullable().default(null)
val userCodeMetadata: Column<String?> = varchar("user_code_metadata", 2000).nullable().default(null)
val deviceCodeValue: Column<String?> = varchar("device_code_value", 4000).nullable().default(null)
val deviceCodeIssuedAt: Column<Instant?> = timestamp("device_code_issued_at").nullable().default(null)
val deviceCodeExpiresAt: Column<Instant?> = timestamp("device_code_expires_at").nullable().default(null)
val deviceCodeMetadata: Column<String?> = varchar("device_code_metadata", 2000).nullable().default(null)
override val primaryKey = PrimaryKey(id)
override val primaryKey: PrimaryKey = PrimaryKey(id)
}

View File

@ -12,7 +12,7 @@ class MetaServiceImpl(private val metaRepository: MetaRepository, private val tr
override suspend fun getMeta(): Meta =
transaction.transaction { metaRepository.get() ?: throw NotInitException("Meta is null") }
override suspend fun updateMeta(meta: Meta) = transaction.transaction {
override suspend fun updateMeta(meta: Meta): Unit = transaction.transaction {
metaRepository.save(meta)
}

View File

@ -0,0 +1,7 @@
package dev.usbharu.hideout.service.media
import dev.usbharu.hideout.domain.model.hideout.dto.FileType
interface FileTypeDeterminationService {
fun fileType(byteArray: ByteArray, filename: String, contentType: String?): FileType
}

View File

@ -0,0 +1,27 @@
package dev.usbharu.hideout.service.media
import dev.usbharu.hideout.domain.model.hideout.dto.FileType
import org.springframework.stereotype.Component
@Component
class FileTypeDeterminationServiceImpl : FileTypeDeterminationService {
override fun fileType(
byteArray: ByteArray,
filename: String,
contentType: String?
): FileType {
if (contentType == null) {
return FileType.Unknown
}
if (contentType.startsWith("image")) {
return FileType.Image
}
if (contentType.startsWith("video")) {
return FileType.Video
}
if (contentType.startsWith("audio")) {
return FileType.Audio
}
return FileType.Unknown
}
}

View File

@ -0,0 +1,7 @@
package dev.usbharu.hideout.service.media
import java.awt.image.BufferedImage
interface MediaBlurhashService {
fun generateBlurhash(bufferedImage: BufferedImage): String
}

View File

@ -0,0 +1,10 @@
package dev.usbharu.hideout.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

@ -0,0 +1,9 @@
package dev.usbharu.hideout.service.media
import dev.usbharu.hideout.domain.model.MediaSave
import dev.usbharu.hideout.domain.model.hideout.dto.SavedMedia
interface MediaDataStore {
suspend fun save(dataMediaSave: MediaSave): SavedMedia
suspend fun delete(id: String)
}

View File

@ -0,0 +1,9 @@
package dev.usbharu.hideout.service.media
import dev.usbharu.hideout.domain.model.hideout.dto.RemoteMedia
import dev.usbharu.hideout.domain.model.hideout.form.Media
interface MediaService {
suspend fun uploadLocalMedia(media: Media): dev.usbharu.hideout.domain.model.hideout.entity.Media
suspend fun uploadRemoteMedia(remoteMedia: RemoteMedia)
}

View File

@ -0,0 +1,95 @@
package dev.usbharu.hideout.service.media
import dev.usbharu.hideout.domain.model.MediaSave
import dev.usbharu.hideout.domain.model.hideout.dto.FaildSavedMedia
import dev.usbharu.hideout.domain.model.hideout.dto.FileType
import dev.usbharu.hideout.domain.model.hideout.dto.RemoteMedia
import dev.usbharu.hideout.domain.model.hideout.dto.SuccessSavedMedia
import dev.usbharu.hideout.domain.model.hideout.form.Media
import dev.usbharu.hideout.exception.media.MediaFileSizeIsZeroException
import dev.usbharu.hideout.exception.media.MediaSaveException
import dev.usbharu.hideout.exception.media.UnsupportedMediaException
import dev.usbharu.hideout.repository.MediaRepository
import dev.usbharu.hideout.service.media.converter.MediaProcessService
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext
import org.slf4j.LoggerFactory
import org.springframework.stereotype.Service
import java.util.*
import javax.imageio.ImageIO
import dev.usbharu.hideout.domain.model.hideout.entity.Media as EntityMedia
@Service
class MediaServiceImpl(
private val mediaDataStore: MediaDataStore,
private val fileTypeDeterminationService: FileTypeDeterminationService,
private val mediaBlurhashService: MediaBlurhashService,
private val mediaRepository: MediaRepository,
private val mediaProcessService: MediaProcessService
) : MediaService {
override suspend fun uploadLocalMedia(media: Media): EntityMedia {
logger.info(
"Media upload. filename:${media.file.name} size:${media.file.size} contentType:${media.file.contentType}"
)
if (media.file.size == 0L) {
throw MediaFileSizeIsZeroException("Media file size is zero.")
}
val fileType = fileTypeDeterminationService.fileType(media.file.bytes, media.file.name, media.file.contentType)
if (fileType != FileType.Image) {
throw UnsupportedMediaException("FileType: $fileType is not supported.")
}
val process = mediaProcessService.process(
fileType,
media.file.contentType.orEmpty(),
media.file.name,
media.file.bytes,
media.thumbnail?.bytes
)
val dataMediaSave = MediaSave(
"${UUID.randomUUID()}.${process.file.extension}",
"",
process.file.byteArray,
process.thumbnail?.byteArray
)
val save = try {
mediaDataStore.save(dataMediaSave)
} catch (e: Exception) {
logger.warn("Failed save media", e)
throw MediaSaveException("Failed save media.", e)
}
if (save.success.not()) {
save as FaildSavedMedia
logger.warn("Failed save media. reason: ${save.reason}")
logger.warn(save.description, save.trace)
throw MediaSaveException("Failed save media.")
}
save as SuccessSavedMedia
val blurHash = withContext(Dispatchers.IO) {
mediaBlurhashService.generateBlurhash(ImageIO.read(media.file.bytes.inputStream()))
}
return mediaRepository.save(
EntityMedia(
id = mediaRepository.generateId(),
name = media.file.name,
url = save.url,
remoteUrl = null,
thumbnailUrl = save.thumbnailUrl,
type = fileType,
blurHash = blurHash
)
)
}
override suspend fun uploadRemoteMedia(remoteMedia: RemoteMedia) = Unit
companion object {
private val logger = LoggerFactory.getLogger(MediaServiceImpl::class.java)
}
}

View File

@ -0,0 +1,67 @@
package dev.usbharu.hideout.service.media
import dev.usbharu.hideout.config.StorageConfig
import dev.usbharu.hideout.domain.model.MediaSave
import dev.usbharu.hideout.domain.model.hideout.dto.SavedMedia
import dev.usbharu.hideout.domain.model.hideout.dto.SuccessSavedMedia
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.async
import kotlinx.coroutines.awaitAll
import kotlinx.coroutines.withContext
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
class S3MediaDataStore(private val s3Client: S3Client, private val storageConfig: StorageConfig) : MediaDataStore {
override suspend fun save(dataMediaSave: MediaSave): SavedMedia {
val fileUploadRequest = PutObjectRequest.builder()
.bucket(storageConfig.bucket)
.key(dataMediaSave.name)
.build()
val thumbnailKey = "thumbnail-${dataMediaSave.name}"
val thumbnailUploadRequest = PutObjectRequest.builder()
.bucket(storageConfig.bucket)
.key(thumbnailKey)
.build()
val pairList = withContext(Dispatchers.IO) {
awaitAll(
async {
if (dataMediaSave.thumbnailInputStream != null) {
s3Client.putObject(
thumbnailUploadRequest,
RequestBody.fromBytes(dataMediaSave.thumbnailInputStream)
)
"thumbnail" to s3Client.utilities()
.getUrl(GetUrlRequest.builder().bucket(storageConfig.bucket).key(thumbnailKey).build())
} else {
"thumbnail" to null
}
},
async {
s3Client.putObject(fileUploadRequest, RequestBody.fromBytes(dataMediaSave.fileInputStream))
"file" to s3Client.utilities()
.getUrl(GetUrlRequest.builder().bucket(storageConfig.bucket).key(dataMediaSave.name).build())
}
)
}.toMap()
return SuccessSavedMedia(
dataMediaSave.name,
pairList.getValue("file").toString(),
pairList.getValue("thumbnail").toString()
)
}
override suspend fun delete(id: String) {
val fileDeleteRequest = DeleteObjectRequest.builder().bucket(storageConfig.bucket).key(id).build()
val thumbnailDeleteRequest =
DeleteObjectRequest.builder().bucket(storageConfig.bucket).key("thumbnail-$id").build()
s3Client.deleteObject(fileDeleteRequest)
s3Client.deleteObject(thumbnailDeleteRequest)
}
}

View File

@ -0,0 +1,9 @@
package dev.usbharu.hideout.service.media
import dev.usbharu.hideout.domain.model.hideout.dto.ProcessedFile
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

@ -0,0 +1,27 @@
package dev.usbharu.hideout.service.media
import dev.usbharu.hideout.domain.model.hideout.dto.ProcessedFile
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

@ -0,0 +1,10 @@
package dev.usbharu.hideout.service.media.converter
import dev.usbharu.hideout.domain.model.hideout.dto.FileType
import dev.usbharu.hideout.domain.model.hideout.dto.ProcessedFile
import java.io.InputStream
interface MediaConverter {
fun isSupport(fileType: FileType): Boolean
fun convert(inputStream: InputStream): ProcessedFile
}

View File

@ -0,0 +1,14 @@
package dev.usbharu.hideout.service.media.converter
import dev.usbharu.hideout.domain.model.hideout.dto.FileType
import dev.usbharu.hideout.domain.model.hideout.dto.ProcessedFile
import java.io.InputStream
interface MediaConverterRoot {
suspend fun convert(
fileType: FileType,
contentType: String,
filename: String,
inputStream: InputStream
): ProcessedFile
}

View File

@ -0,0 +1,32 @@
package dev.usbharu.hideout.service.media.converter
import dev.usbharu.hideout.domain.model.hideout.dto.FileType
import dev.usbharu.hideout.domain.model.hideout.dto.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

@ -0,0 +1,14 @@
package dev.usbharu.hideout.service.media.converter
import dev.usbharu.hideout.domain.model.hideout.dto.FileType
import dev.usbharu.hideout.domain.model.hideout.dto.ProcessedMedia
interface MediaProcessService {
suspend fun process(
fileType: FileType,
contentType: String,
fileName: String,
file: ByteArray,
thumbnail: ByteArray?
): ProcessedMedia
}

View File

@ -0,0 +1,47 @@
package dev.usbharu.hideout.service.media.converter
import dev.usbharu.hideout.domain.model.hideout.dto.FileType
import dev.usbharu.hideout.domain.model.hideout.dto.ProcessedMedia
import dev.usbharu.hideout.exception.media.MediaConvertException
import dev.usbharu.hideout.service.media.ThumbnailGenerateService
import org.slf4j.LoggerFactory
import org.springframework.stereotype.Service
@Service
class MediaProcessServiceImpl(
private val mediaConverterRoot: MediaConverterRoot,
private val thumbnailGenerateService: ThumbnailGenerateService
) : MediaProcessService {
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
)
)
}
companion object {
private val logger = LoggerFactory.getLogger(MediaProcessServiceImpl::class.java)
}
}

View File

@ -44,9 +44,7 @@ class PostServiceImpl(
text = post.text,
createdAt = Instant.now().toEpochMilli(),
visibility = post.visibility,
url = "${user.url}/posts/$id",
repostId = null,
replyId = null
url = "${user.url}/posts/$id"
)
return internalCreate(createPost, isLocal)
}

View File

@ -26,8 +26,8 @@ class UserAuthServiceImpl(
companion object {
val sha256: MessageDigest = MessageDigest.getInstance("SHA-256")
const val keySize = 2048
const val pemSize = 64
const val keySize: Int = 2048
const val pemSize: Int = 64
}
}

View File

@ -26,7 +26,7 @@ class ExposedKJob(config: Configuration) : BaseKJob<ExposedKJob.Configuration>(c
return super.start()
}
override fun shutdown() = runBlocking {
override fun shutdown(): Unit = runBlocking {
super.shutdown()
lockRepository.clearExpired()
}
@ -40,10 +40,10 @@ class ExposedKJob(config: Configuration) : BaseKJob<ExposedKJob.Configuration>(c
var driverClassName: String? = null
var connectionDatabase: Database? = null
var jobTableName = "kjobJobs"
var jobTableName: String = "kjobJobs"
var lockTableName = "kjobLocks"
var lockTableName: String = "kjobLocks"
var expireLockInMinutes = 5L
var expireLockInMinutes: Long = 5L
}
}

View File

@ -7,6 +7,11 @@ hideout:
key-id: a
private-key: "MIIEvwIBADANBgkqhkiG9w0BAQEFAASCBKkwggSlAgEAAoIBAQC7VJTUt9Us8cKjMzEfYyjiWA4R4/M2bS1GB4t7NXp98C3SC6dVMvDuictGeurT8jNbvJZHtCSuYEvuNMoSfm76oqFvAp8Gy0iz5sxjZmSnXyCdPEovGhLa0VzMaQ8s+CLOyS56YyCFGeJZqgtzJ6GR3eqoYSW9b9UMvkBpZODSctWSNGj3P7jRFDO5VoTwCQAWbFnOjDfH5Ulgp2PKSQnSJP3AJLQNFNe7br1XbrhV//eO+t51mIpGSDCUv3E0DDFcWDTH9cXDTTlRZVEiR2BwpZOOkE/Z0/BVnhZYL71oZV34bKfWjQIt6V/isSMahdsAASACp4ZTGtwiVuNd9tybAgMBAAECggEBAKTmjaS6tkK8BlPXClTQ2vpz/N6uxDeS35mXpqasqskVlaAidgg/sWqpjXDbXr93otIMLlWsM+X0CqMDgSXKejLS2jx4GDjI1ZTXg++0AMJ8sJ74pWzVDOfmCEQ/7wXs3+cbnXhKriO8Z036q92Qc1+N87SI38nkGa0ABH9CN83HmQqt4fB7UdHzuIRe/me2PGhIq5ZBzj6h3BpoPGzEP+x3l9YmK8t/1cN0pqI+dQwYdgfGjackLu/2qH80MCF7IyQaseZUOJyKrCLtSD/Iixv/hzDEUPfOCjFDgTpzf3cwta8+oE4wHCo1iI1/4TlPkwmXx4qSXtmw4aQPz7IDQvECgYEA8KNThCO2gsC2I9PQDM/8Cw0O983WCDY+oi+7JPiNAJwv5DYBqEZB1QYdj06YD16XlC/HAZMsMku1na2TN0driwenQQWzoev3g2S7gRDoS/FCJSI3jJ+kjgtaA7Qmzlgk1TxODN+G1H91HW7t0l7VnL27IWyYo2qRRK3jzxqUiPUCgYEAx0oQs2reBQGMVZnApD1jeq7n4MvNLcPvt8b/eU9iUv6Y4Mj0Suo/AU8lYZXm8ubbqAlwz2VSVunD2tOplHyMUrtCtObAfVDUAhCndKaA9gApgfb3xw1IKbuQ1u4IF1FJl3VtumfQn//LiH1B3rXhcdyo3/vIttEk48RakUKClU8CgYEAzV7W3COOlDDcQd935DdtKBFRAPRPAlspQUnzMi5eSHMD/ISLDY5IiQHbIH83D4bvXq0X7qQoSBSNP7Dvv3HYuqMhf0DaegrlBuJllFVVq9qPVRnKxt1Il2HgxOBvbhOT+9in1BzA+YJ99UzC85O0Qz06A+CmtHEy4aZ2kj5hHjECgYEAmNS4+A8Fkss8Js1RieK2LniBxMgmYml3pfVLKGnzmng7H2+cwPLhPIzIuwytXywh2bzbsYEfYx3EoEVgMEpPhoarQnYPukrJO4gwE2o5Te6T5mJSZGlQJQj9q4ZB2Dfzet6INsK0oG8XVGXSpQvQh3RUYekCZQkBBFcpqWpbIEsCgYAnM3DQf3FJoSnXaMhrVBIovic5l0xFkEHskAjFTevO86Fsz1C2aSeRKSqGFoOQ0tmJzBEs1R6KqnHInicDTQrKhArgLXX4v3CddjfTRJkFWDbE/CkvKZNOrcf1nhaGCPspRJj2KUkj1Fhl9Cncdn/RsYEONbwQSjIfMPkvxF+8HQ=="
public-key: "MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAu1SU1LfVLPHCozMxH2Mo4lgOEePzNm0tRgeLezV6ffAt0gunVTLw7onLRnrq0/IzW7yWR7QkrmBL7jTKEn5u+qKhbwKfBstIs+bMY2Zkp18gnTxKLxoS2tFczGkPLPgizskuemMghRniWaoLcyehkd3qqGElvW/VDL5AaWTg0nLVkjRo9z+40RQzuVaE8AkAFmxZzow3x+VJYKdjykkJ0iT9wCS0DRTXu269V264Vf/3jvredZiKRkgwlL9xNAwxXFg0x/XFw005UWVRIkdgcKWTjpBP2dPwVZ4WWC+9aGVd+Gyn1o0CLelf4rEjGoXbAAEgAqeGUxrcIlbjXfbcmwIDAQAB"
spring:
jackson:
serialization:

View File

@ -17,6 +17,8 @@ tags:
description: instance
- name: timeline
description: timeline
- name: media
description: media
paths:
/api/v2/instance:
@ -291,9 +293,48 @@ paths:
type: array
items:
$ref: "#/components/schemas/Status"
/api/v1/media:
post:
tags:
- media
security:
- OAuth2:
- "write:media"
requestBody:
required: true
content:
multipart/form-data:
schema:
$ref: "#/components/schemas/V1MediaRequest"
encoding:
file:
contentType: image/jpeg, image/png
responses:
200:
description: 成功
content:
application/json:
schema:
$ref: "#/components/schemas/MediaAttachment"
components:
schemas:
V1MediaRequest:
type: object
properties:
file:
type: string
format: binary
thumbnail:
type: string
format: binary
description:
type: string
focus:
type: string
required:
- file
AccountsCreateRequest:
type: object
properties: