diff --git a/build.gradle.kts b/build.gradle.kts index 47ebb8b3..ec03a89e 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -142,6 +142,7 @@ dependencies { implementation("dev.usbharu:http-signature:1.0.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") diff --git a/src/main/kotlin/dev/usbharu/hideout/activitypub/service/common/APRequestServiceImpl.kt b/src/main/kotlin/dev/usbharu/hideout/activitypub/service/common/APRequestServiceImpl.kt index 6e8edef9..6e87d402 100644 --- a/src/main/kotlin/dev/usbharu/hideout/activitypub/service/common/APRequestServiceImpl.kt +++ b/src/main/kotlin/dev/usbharu/hideout/activitypub/service/common/APRequestServiceImpl.kt @@ -96,6 +96,7 @@ class APRequestServiceImpl( return objectMapper.readValue(bodyAsText, responseClass) } + @Suppress("LongMethod") override suspend fun apPost(url: String, body: T?, signer: User?): String { logger.debug("START ActivityPub Request POST url: {}, signer: {}", url, signer?.url) val requestBody = if (body != null) { diff --git a/src/main/kotlin/dev/usbharu/hideout/activitypub/service/objects/note/APNoteService.kt b/src/main/kotlin/dev/usbharu/hideout/activitypub/service/objects/note/APNoteService.kt index c221e9fb..0fdee80b 100644 --- a/src/main/kotlin/dev/usbharu/hideout/activitypub/service/objects/note/APNoteService.kt +++ b/src/main/kotlin/dev/usbharu/hideout/activitypub/service/objects/note/APNoteService.kt @@ -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.Visibility 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 io.ktor.client.plugins.* import kotlinx.coroutines.CoroutineScope @@ -52,7 +54,8 @@ class APNoteServiceImpl( private val postService: PostService, private val apResourceResolveService: APResourceResolveService, private val postBuilder: Post.PostBuilder, - private val noteQueryService: NoteQueryService + private val noteQueryService: NoteQueryService, + private val mediaService: MediaService ) : APNoteService { @@ -123,6 +126,19 @@ class APNoteServiceImpl( 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: リモートのメディア処理を追加 postService.createRemote( postBuilder.of( @@ -135,6 +151,7 @@ class APNoteServiceImpl( replyId = reply?.id, sensitive = note.sensitive, apId = note.id ?: url, + mediaIds = mediaList ) ) return note diff --git a/src/main/kotlin/dev/usbharu/hideout/core/service/media/MediaService.kt b/src/main/kotlin/dev/usbharu/hideout/core/service/media/MediaService.kt index 75d928e8..b85c497a 100644 --- a/src/main/kotlin/dev/usbharu/hideout/core/service/media/MediaService.kt +++ b/src/main/kotlin/dev/usbharu/hideout/core/service/media/MediaService.kt @@ -1,8 +1,9 @@ package dev.usbharu.hideout.core.service.media +import dev.usbharu.hideout.core.domain.model.media.Media import dev.usbharu.hideout.mastodon.interfaces.api.media.MediaRequest interface MediaService { - suspend fun uploadLocalMedia(mediaRequest: MediaRequest): dev.usbharu.hideout.core.domain.model.media.Media - suspend fun uploadRemoteMedia(remoteMedia: RemoteMedia) + suspend fun uploadLocalMedia(mediaRequest: MediaRequest): Media + suspend fun uploadRemoteMedia(remoteMedia: RemoteMedia): Media } diff --git a/src/main/kotlin/dev/usbharu/hideout/core/service/media/MediaServiceImpl.kt b/src/main/kotlin/dev/usbharu/hideout/core/service/media/MediaServiceImpl.kt index ee507aed..ca0f1387 100644 --- a/src/main/kotlin/dev/usbharu/hideout/core/service/media/MediaServiceImpl.kt +++ b/src/main/kotlin/dev/usbharu/hideout/core/service/media/MediaServiceImpl.kt @@ -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.MediaSaveException import dev.usbharu.hideout.core.domain.exception.media.UnsupportedMediaException +import dev.usbharu.hideout.core.domain.model.media.Media import dev.usbharu.hideout.core.domain.model.media.MediaRepository import dev.usbharu.hideout.core.service.media.converter.MediaProcessService import dev.usbharu.hideout.mastodon.interfaces.api.media.MediaRequest +import 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.withContext import org.slf4j.LoggerFactory @@ -21,7 +27,8 @@ class MediaServiceImpl( private val fileTypeDeterminationService: FileTypeDeterminationService, private val mediaBlurhashService: MediaBlurhashService, private val mediaRepository: MediaRepository, - private val mediaProcessService: MediaProcessService + private val mediaProcessService: MediaProcessService, + private val httpClient: HttpClient ) : MediaService { override suspend fun uploadLocalMedia(mediaRequest: MediaRequest): EntityMedia { 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 { private val logger = LoggerFactory.getLogger(MediaServiceImpl::class.java) diff --git a/src/main/kotlin/dev/usbharu/hideout/mastodon/infrastructure/exposedquery/StatusQueryServiceImpl.kt b/src/main/kotlin/dev/usbharu/hideout/mastodon/infrastructure/exposedquery/StatusQueryServiceImpl.kt index 7589fcb6..c300375a 100644 --- a/src/main/kotlin/dev/usbharu/hideout/mastodon/infrastructure/exposedquery/StatusQueryServiceImpl.kt +++ b/src/main/kotlin/dev/usbharu/hideout/mastodon/infrastructure/exposedquery/StatusQueryServiceImpl.kt @@ -48,12 +48,12 @@ class StatusQueryServiceImpl : StatusQueryService { } } - return statusQueries.mapNotNull { - postMap[it.postId]?.copy( - inReplyToId = it.replyId?.toString(), - inReplyToAccountId = postMap[it.replyId]?.account?.id, - reblog = postMap[it.repostId], - mediaAttachments = it.mediaIds.mapNotNull { mediaMap[it] } + return statusQueries.mapNotNull { statusQuery -> + postMap[statusQuery.postId]?.copy( + inReplyToId = statusQuery.replyId?.toString(), + inReplyToAccountId = postMap[statusQuery.replyId]?.account?.id, + reblog = postMap[statusQuery.repostId], + mediaAttachments = statusQuery.mediaIds.mapNotNull { mediaMap[it] } ) } } diff --git a/src/test/kotlin/dev/usbharu/hideout/activitypub/service/objects/note/APNoteServiceImplTest.kt b/src/test/kotlin/dev/usbharu/hideout/activitypub/service/objects/note/APNoteServiceImplTest.kt index 5c0d6363..c7e047ed 100644 --- a/src/test/kotlin/dev/usbharu/hideout/activitypub/service/objects/note/APNoteServiceImplTest.kt +++ b/src/test/kotlin/dev/usbharu/hideout/activitypub/service/objects/note/APNoteServiceImplTest.kt @@ -78,7 +78,8 @@ class APNoteServiceImplTest { postService = mock(), apResourceResolveService = mock(), postBuilder = Post.PostBuilder(CharacterLimit()), - noteQueryService = noteQueryService + noteQueryService = noteQueryService, + mock() ) val actual = apNoteServiceImpl.fetchNote(url) @@ -155,7 +156,8 @@ class APNoteServiceImplTest { postService = mock(), apResourceResolveService = apResourceResolveService, postBuilder = Post.PostBuilder(CharacterLimit()), - noteQueryService = noteQueryService + noteQueryService = noteQueryService, + mock() ) val actual = apNoteServiceImpl.fetchNote(url) @@ -223,7 +225,8 @@ class APNoteServiceImplTest { postService = mock(), apResourceResolveService = apResourceResolveService, postBuilder = Post.PostBuilder(CharacterLimit()), - noteQueryService = noteQueryService + noteQueryService = noteQueryService, + mock() ) assertThrows { apNoteServiceImpl.fetchNote(url) } @@ -275,7 +278,8 @@ class APNoteServiceImplTest { postService = postService, apResourceResolveService = mock(), postBuilder = postBuilder, - noteQueryService = noteQueryService + noteQueryService = noteQueryService, + mock() ) val note = Note( @@ -333,7 +337,8 @@ class APNoteServiceImplTest { postService = mock(), apResourceResolveService = mock(), postBuilder = postBuilder, - noteQueryService = noteQueryService + noteQueryService = noteQueryService, + mock() )