Merge pull request #132 from usbharu/feature/remote-media

Feature/remote media
This commit is contained in:
usbharu 2023-11-02 20:13:06 +09:00 committed by GitHub
commit ce7024849a
7 changed files with 108 additions and 16 deletions

View File

@ -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")

View File

@ -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) {

View File

@ -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

View File

@ -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
} }

View File

@ -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)

View File

@ -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] }
) )
} }
} }

View File

@ -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()
) )