mirror of https://github.com/usbharu/Hideout.git
Merge pull request #132 from usbharu/feature/remote-media
Feature/remote media
This commit is contained in:
commit
ce7024849a
|
@ -142,6 +142,7 @@ dependencies {
|
||||||
implementation("dev.usbharu:http-signature:1.0.0")
|
implementation("dev.usbharu:http-signature:1.0.0")
|
||||||
|
|
||||||
implementation("org.postgresql:postgresql:42.6.0")
|
implementation("org.postgresql:postgresql:42.6.0")
|
||||||
|
implementation("com.twelvemonkeys.imageio:imageio-webp:3.10.0")
|
||||||
|
|
||||||
|
|
||||||
implementation("io.ktor:ktor-client-logging-jvm:$ktor_version")
|
implementation("io.ktor:ktor-client-logging-jvm:$ktor_version")
|
||||||
|
|
|
@ -96,6 +96,7 @@ class APRequestServiceImpl(
|
||||||
return objectMapper.readValue(bodyAsText, responseClass)
|
return objectMapper.readValue(bodyAsText, responseClass)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Suppress("LongMethod")
|
||||||
override suspend fun <T : Object> apPost(url: String, body: T?, signer: User?): String {
|
override suspend fun <T : Object> apPost(url: String, body: T?, signer: User?): String {
|
||||||
logger.debug("START ActivityPub Request POST url: {}, signer: {}", url, signer?.url)
|
logger.debug("START ActivityPub Request POST url: {}, signer: {}", url, signer?.url)
|
||||||
val requestBody = if (body != null) {
|
val requestBody = if (body != null) {
|
||||||
|
|
|
@ -13,6 +13,8 @@ import dev.usbharu.hideout.core.domain.model.post.Post
|
||||||
import dev.usbharu.hideout.core.domain.model.post.PostRepository
|
import dev.usbharu.hideout.core.domain.model.post.PostRepository
|
||||||
import dev.usbharu.hideout.core.domain.model.post.Visibility
|
import dev.usbharu.hideout.core.domain.model.post.Visibility
|
||||||
import dev.usbharu.hideout.core.query.PostQueryService
|
import dev.usbharu.hideout.core.query.PostQueryService
|
||||||
|
import dev.usbharu.hideout.core.service.media.MediaService
|
||||||
|
import dev.usbharu.hideout.core.service.media.RemoteMedia
|
||||||
import dev.usbharu.hideout.core.service.post.PostService
|
import dev.usbharu.hideout.core.service.post.PostService
|
||||||
import io.ktor.client.plugins.*
|
import io.ktor.client.plugins.*
|
||||||
import kotlinx.coroutines.CoroutineScope
|
import kotlinx.coroutines.CoroutineScope
|
||||||
|
@ -52,7 +54,8 @@ class APNoteServiceImpl(
|
||||||
private val postService: PostService,
|
private val postService: PostService,
|
||||||
private val apResourceResolveService: APResourceResolveService,
|
private val apResourceResolveService: APResourceResolveService,
|
||||||
private val postBuilder: Post.PostBuilder,
|
private val postBuilder: Post.PostBuilder,
|
||||||
private val noteQueryService: NoteQueryService
|
private val noteQueryService: NoteQueryService,
|
||||||
|
private val mediaService: MediaService
|
||||||
|
|
||||||
) : APNoteService {
|
) : APNoteService {
|
||||||
|
|
||||||
|
@ -123,6 +126,19 @@ class APNoteServiceImpl(
|
||||||
postQueryService.findByUrl(it)
|
postQueryService.findByUrl(it)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
val mediaList = note.attachment
|
||||||
|
.filter { it.url != null }
|
||||||
|
.map {
|
||||||
|
mediaService.uploadRemoteMedia(
|
||||||
|
RemoteMedia(
|
||||||
|
(it.name ?: it.url)!!,
|
||||||
|
it.url!!,
|
||||||
|
it.mediaType ?: "application/octet-stream"
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
.map { it.id }
|
||||||
|
|
||||||
// TODO: リモートのメディア処理を追加
|
// TODO: リモートのメディア処理を追加
|
||||||
postService.createRemote(
|
postService.createRemote(
|
||||||
postBuilder.of(
|
postBuilder.of(
|
||||||
|
@ -135,6 +151,7 @@ class APNoteServiceImpl(
|
||||||
replyId = reply?.id,
|
replyId = reply?.id,
|
||||||
sensitive = note.sensitive,
|
sensitive = note.sensitive,
|
||||||
apId = note.id ?: url,
|
apId = note.id ?: url,
|
||||||
|
mediaIds = mediaList
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
return note
|
return note
|
||||||
|
|
|
@ -1,8 +1,9 @@
|
||||||
package dev.usbharu.hideout.core.service.media
|
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
|
import dev.usbharu.hideout.mastodon.interfaces.api.media.MediaRequest
|
||||||
|
|
||||||
interface MediaService {
|
interface MediaService {
|
||||||
suspend fun uploadLocalMedia(mediaRequest: MediaRequest): dev.usbharu.hideout.core.domain.model.media.Media
|
suspend fun uploadLocalMedia(mediaRequest: MediaRequest): Media
|
||||||
suspend fun uploadRemoteMedia(remoteMedia: RemoteMedia)
|
suspend fun uploadRemoteMedia(remoteMedia: RemoteMedia): Media
|
||||||
}
|
}
|
||||||
|
|
|
@ -3,9 +3,15 @@ package dev.usbharu.hideout.core.service.media
|
||||||
import dev.usbharu.hideout.core.domain.exception.media.MediaFileSizeIsZeroException
|
import dev.usbharu.hideout.core.domain.exception.media.MediaFileSizeIsZeroException
|
||||||
import dev.usbharu.hideout.core.domain.exception.media.MediaSaveException
|
import dev.usbharu.hideout.core.domain.exception.media.MediaSaveException
|
||||||
import dev.usbharu.hideout.core.domain.exception.media.UnsupportedMediaException
|
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.domain.model.media.MediaRepository
|
||||||
import dev.usbharu.hideout.core.service.media.converter.MediaProcessService
|
import dev.usbharu.hideout.core.service.media.converter.MediaProcessService
|
||||||
import dev.usbharu.hideout.mastodon.interfaces.api.media.MediaRequest
|
import dev.usbharu.hideout.mastodon.interfaces.api.media.MediaRequest
|
||||||
|
import io.ktor.client.*
|
||||||
|
import io.ktor.client.request.*
|
||||||
|
import io.ktor.client.statement.*
|
||||||
|
import io.ktor.http.*
|
||||||
|
import io.ktor.util.*
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
import kotlinx.coroutines.withContext
|
import kotlinx.coroutines.withContext
|
||||||
import org.slf4j.LoggerFactory
|
import org.slf4j.LoggerFactory
|
||||||
|
@ -21,7 +27,8 @@ class MediaServiceImpl(
|
||||||
private val fileTypeDeterminationService: FileTypeDeterminationService,
|
private val fileTypeDeterminationService: FileTypeDeterminationService,
|
||||||
private val mediaBlurhashService: MediaBlurhashService,
|
private val mediaBlurhashService: MediaBlurhashService,
|
||||||
private val mediaRepository: MediaRepository,
|
private val mediaRepository: MediaRepository,
|
||||||
private val mediaProcessService: MediaProcessService
|
private val mediaProcessService: MediaProcessService,
|
||||||
|
private val httpClient: HttpClient
|
||||||
) : MediaService {
|
) : MediaService {
|
||||||
override suspend fun uploadLocalMedia(mediaRequest: MediaRequest): EntityMedia {
|
override suspend fun uploadLocalMedia(mediaRequest: MediaRequest): EntityMedia {
|
||||||
logger.info(
|
logger.info(
|
||||||
|
@ -88,7 +95,67 @@ class MediaServiceImpl(
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
override suspend fun uploadRemoteMedia(remoteMedia: RemoteMedia) = Unit
|
// TODO: 仮の処理として保存したように動かす
|
||||||
|
override suspend fun uploadRemoteMedia(remoteMedia: RemoteMedia): Media {
|
||||||
|
logger.info("MEDIA Remote media. filename:${remoteMedia.name} url:${remoteMedia.url}")
|
||||||
|
|
||||||
|
val httpResponse = httpClient.get(remoteMedia.url)
|
||||||
|
val bytes = httpResponse.bodyAsChannel().toByteArray()
|
||||||
|
|
||||||
|
val contentType = httpResponse.contentType()?.toString()
|
||||||
|
val fileType =
|
||||||
|
fileTypeDeterminationService.fileType(bytes, remoteMedia.name, contentType)
|
||||||
|
|
||||||
|
if (fileType != FileType.Image) {
|
||||||
|
throw UnsupportedMediaException("FileType: $fileType is not supported.")
|
||||||
|
}
|
||||||
|
|
||||||
|
val processedMedia = mediaProcessService.process(
|
||||||
|
fileType = fileType,
|
||||||
|
contentType = contentType.orEmpty(),
|
||||||
|
fileName = remoteMedia.name,
|
||||||
|
file = bytes,
|
||||||
|
thumbnail = null
|
||||||
|
)
|
||||||
|
|
||||||
|
val mediaSave = MediaSave(
|
||||||
|
"${UUID.randomUUID()}.${processedMedia.file.extension}",
|
||||||
|
"",
|
||||||
|
processedMedia.file.byteArray,
|
||||||
|
processedMedia.thumbnail?.byteArray
|
||||||
|
)
|
||||||
|
|
||||||
|
val save = try {
|
||||||
|
mediaDataStore.save(mediaSave)
|
||||||
|
} 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(bytes.inputStream()))
|
||||||
|
}
|
||||||
|
|
||||||
|
return mediaRepository.save(
|
||||||
|
EntityMedia(
|
||||||
|
id = mediaRepository.generateId(),
|
||||||
|
name = remoteMedia.name,
|
||||||
|
url = save.url,
|
||||||
|
remoteUrl = remoteMedia.url,
|
||||||
|
thumbnailUrl = save.thumbnailUrl,
|
||||||
|
type = fileType,
|
||||||
|
blurHash = blurhash
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
private val logger = LoggerFactory.getLogger(MediaServiceImpl::class.java)
|
private val logger = LoggerFactory.getLogger(MediaServiceImpl::class.java)
|
||||||
|
|
|
@ -48,12 +48,12 @@ class StatusQueryServiceImpl : StatusQueryService {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return statusQueries.mapNotNull {
|
return statusQueries.mapNotNull { statusQuery ->
|
||||||
postMap[it.postId]?.copy(
|
postMap[statusQuery.postId]?.copy(
|
||||||
inReplyToId = it.replyId?.toString(),
|
inReplyToId = statusQuery.replyId?.toString(),
|
||||||
inReplyToAccountId = postMap[it.replyId]?.account?.id,
|
inReplyToAccountId = postMap[statusQuery.replyId]?.account?.id,
|
||||||
reblog = postMap[it.repostId],
|
reblog = postMap[statusQuery.repostId],
|
||||||
mediaAttachments = it.mediaIds.mapNotNull { mediaMap[it] }
|
mediaAttachments = statusQuery.mediaIds.mapNotNull { mediaMap[it] }
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -78,7 +78,8 @@ class APNoteServiceImplTest {
|
||||||
postService = mock(),
|
postService = mock(),
|
||||||
apResourceResolveService = mock(),
|
apResourceResolveService = mock(),
|
||||||
postBuilder = Post.PostBuilder(CharacterLimit()),
|
postBuilder = Post.PostBuilder(CharacterLimit()),
|
||||||
noteQueryService = noteQueryService
|
noteQueryService = noteQueryService,
|
||||||
|
mock()
|
||||||
)
|
)
|
||||||
|
|
||||||
val actual = apNoteServiceImpl.fetchNote(url)
|
val actual = apNoteServiceImpl.fetchNote(url)
|
||||||
|
@ -155,7 +156,8 @@ class APNoteServiceImplTest {
|
||||||
postService = mock(),
|
postService = mock(),
|
||||||
apResourceResolveService = apResourceResolveService,
|
apResourceResolveService = apResourceResolveService,
|
||||||
postBuilder = Post.PostBuilder(CharacterLimit()),
|
postBuilder = Post.PostBuilder(CharacterLimit()),
|
||||||
noteQueryService = noteQueryService
|
noteQueryService = noteQueryService,
|
||||||
|
mock()
|
||||||
)
|
)
|
||||||
|
|
||||||
val actual = apNoteServiceImpl.fetchNote(url)
|
val actual = apNoteServiceImpl.fetchNote(url)
|
||||||
|
@ -223,7 +225,8 @@ class APNoteServiceImplTest {
|
||||||
postService = mock(),
|
postService = mock(),
|
||||||
apResourceResolveService = apResourceResolveService,
|
apResourceResolveService = apResourceResolveService,
|
||||||
postBuilder = Post.PostBuilder(CharacterLimit()),
|
postBuilder = Post.PostBuilder(CharacterLimit()),
|
||||||
noteQueryService = noteQueryService
|
noteQueryService = noteQueryService,
|
||||||
|
mock()
|
||||||
)
|
)
|
||||||
|
|
||||||
assertThrows<FailedToGetActivityPubResourceException> { apNoteServiceImpl.fetchNote(url) }
|
assertThrows<FailedToGetActivityPubResourceException> { apNoteServiceImpl.fetchNote(url) }
|
||||||
|
@ -275,7 +278,8 @@ class APNoteServiceImplTest {
|
||||||
postService = postService,
|
postService = postService,
|
||||||
apResourceResolveService = mock(),
|
apResourceResolveService = mock(),
|
||||||
postBuilder = postBuilder,
|
postBuilder = postBuilder,
|
||||||
noteQueryService = noteQueryService
|
noteQueryService = noteQueryService,
|
||||||
|
mock()
|
||||||
)
|
)
|
||||||
|
|
||||||
val note = Note(
|
val note = Note(
|
||||||
|
@ -333,7 +337,8 @@ class APNoteServiceImplTest {
|
||||||
postService = mock(),
|
postService = mock(),
|
||||||
apResourceResolveService = mock(),
|
apResourceResolveService = mock(),
|
||||||
postBuilder = postBuilder,
|
postBuilder = postBuilder,
|
||||||
noteQueryService = noteQueryService
|
noteQueryService = noteQueryService,
|
||||||
|
mock()
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
|
Loading…
Reference in New Issue