From 3b577869840b1082147345a3da326bb309306a4b Mon Sep 17 00:00:00 2001 From: usbharu <64310155+usbharu@users.noreply.github.com> Date: Mon, 9 Sep 2024 16:15:20 +0900 Subject: [PATCH] =?UTF-8?q?feat:=20Mastodon=E3=81=A7=E3=83=9B=E3=83=BC?= =?UTF-8?q?=E3=83=A0=E3=82=BF=E3=82=A4=E3=83=A0=E3=83=A9=E3=82=A4=E3=83=B3?= =?UTF-8?q?=E3=82=92=E8=AA=AD=E3=82=81=E3=82=8B=E3=82=88=E3=81=86=E3=81=AB?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../MongoInternalTimelineObjectRepository.kt | 3 + .../src/main/resources/templates/index.html | 6 +- .../timeline/MastodonReadTimeline.kt | 13 ++ .../MastodonReadTimelineApplicationService.kt | 113 ++++++++++++++++++ .../interfaces/api/SpringTimelineApi.kt | 53 +++++++- .../src/main/resources/openapi/mastodon.yaml | 2 +- 6 files changed, 185 insertions(+), 5 deletions(-) create mode 100644 hideout-mastodon/src/main/kotlin/dev/usbharu/hideout/mastodon/application/timeline/MastodonReadTimeline.kt create mode 100644 hideout-mastodon/src/main/kotlin/dev/usbharu/hideout/mastodon/application/timeline/MastodonReadTimelineApplicationService.kt diff --git a/hideout-core/src/main/kotlin/dev/usbharu/hideout/core/infrastructure/mongorepository/MongoInternalTimelineObjectRepository.kt b/hideout-core/src/main/kotlin/dev/usbharu/hideout/core/infrastructure/mongorepository/MongoInternalTimelineObjectRepository.kt index 2e66bbbd..c6dec873 100644 --- a/hideout-core/src/main/kotlin/dev/usbharu/hideout/core/infrastructure/mongorepository/MongoInternalTimelineObjectRepository.kt +++ b/hideout-core/src/main/kotlin/dev/usbharu/hideout/core/infrastructure/mongorepository/MongoInternalTimelineObjectRepository.kt @@ -25,6 +25,7 @@ import org.springframework.data.mongodb.core.MongoTemplate import org.springframework.data.mongodb.core.mapping.Document import org.springframework.data.mongodb.core.query.Criteria import org.springframework.data.mongodb.core.query.Query +import org.springframework.data.mongodb.core.query.isEqualTo import org.springframework.data.repository.kotlin.CoroutineCrudRepository import org.springframework.stereotype.Repository import java.time.Instant @@ -84,6 +85,8 @@ class MongoInternalTimelineObjectRepository( ): PaginationList { val query = Query() + query.addCriteria(Criteria.where("timelineId").isEqualTo(timelineId.value)) + if (page?.minId != null) { query.with(Sort.by(Sort.Direction.ASC, "postCreatedAt")) page.minId?.let { query.addCriteria(Criteria.where("postId").gt(it)) } diff --git a/hideout-core/src/main/resources/templates/index.html b/hideout-core/src/main/resources/templates/index.html index 06321a91..f739ba1a 100644 --- a/hideout-core/src/main/resources/templates/index.html +++ b/hideout-core/src/main/resources/templates/index.html @@ -14,8 +14,8 @@ - + +No Script + \ No newline at end of file diff --git a/hideout-mastodon/src/main/kotlin/dev/usbharu/hideout/mastodon/application/timeline/MastodonReadTimeline.kt b/hideout-mastodon/src/main/kotlin/dev/usbharu/hideout/mastodon/application/timeline/MastodonReadTimeline.kt new file mode 100644 index 00000000..4d76bda5 --- /dev/null +++ b/hideout-mastodon/src/main/kotlin/dev/usbharu/hideout/mastodon/application/timeline/MastodonReadTimeline.kt @@ -0,0 +1,13 @@ +package dev.usbharu.hideout.mastodon.application.timeline + +import dev.usbharu.hideout.core.domain.model.support.page.Page + +class MastodonReadTimeline( + val timelineId: Long, + val mediaOnly: Boolean, + val localOnly: Boolean, + val remoteOnly: Boolean, + val page: Page +) { + +} diff --git a/hideout-mastodon/src/main/kotlin/dev/usbharu/hideout/mastodon/application/timeline/MastodonReadTimelineApplicationService.kt b/hideout-mastodon/src/main/kotlin/dev/usbharu/hideout/mastodon/application/timeline/MastodonReadTimelineApplicationService.kt new file mode 100644 index 00000000..9858228b --- /dev/null +++ b/hideout-mastodon/src/main/kotlin/dev/usbharu/hideout/mastodon/application/timeline/MastodonReadTimelineApplicationService.kt @@ -0,0 +1,113 @@ +package dev.usbharu.hideout.mastodon.application.timeline + +import dev.usbharu.hideout.core.application.shared.AbstractApplicationService +import dev.usbharu.hideout.core.application.shared.Transaction +import dev.usbharu.hideout.core.domain.model.post.Visibility.* +import dev.usbharu.hideout.core.domain.model.support.acct.Acct +import dev.usbharu.hideout.core.domain.model.support.page.PaginationList +import dev.usbharu.hideout.core.domain.model.support.principal.Principal +import dev.usbharu.hideout.core.domain.model.timeline.TimelineId +import dev.usbharu.hideout.core.domain.model.timeline.TimelineRepository +import dev.usbharu.hideout.core.external.timeline.ReadTimelineOption +import dev.usbharu.hideout.core.external.timeline.TimelineStore +import dev.usbharu.hideout.mastodon.interfaces.api.generated.model.Account +import dev.usbharu.hideout.mastodon.interfaces.api.generated.model.MediaAttachment +import dev.usbharu.hideout.mastodon.interfaces.api.generated.model.Status +import org.slf4j.LoggerFactory +import org.springframework.stereotype.Service + +@Service +class MastodonReadTimelineApplicationService( + transaction: Transaction, + private val timelineRepository: TimelineRepository, + private val timelineStore: TimelineStore +) : + AbstractApplicationService>(transaction, logger) { + override suspend fun internalExecute( + command: MastodonReadTimeline, + principal: Principal + ): PaginationList { + val timeline = timelineRepository.findById(TimelineId(command.timelineId)) + ?: throw IllegalArgumentException("Timeline ${command.timelineId} not found.") + + val readTimelineOption = ReadTimelineOption( + command.mediaOnly, + command.localOnly, + command.remoteOnly + ) + + val readTimeline = timelineStore.readTimeline(timeline, readTimelineOption, command.page, principal) + + return PaginationList(readTimeline.map { + Status( + it.postId.id.toString(), + it.post.url.toString(), + it.post.createdAt.toString(), + account = Account( + id = it.postActor.id.id.toString(), + username = it.postActor.name.name, + acct = Acct(it.postActor.name.name, it.postActor.domain.domain).toString(), + url = it.postActor.url.toString(), + displayName = it.postActor.screenName.screenName, + note = it.postActor.description.description, + avatar = it.postActorIconMedia?.url.toString(), + avatarStatic = it.postActorIconMedia?.thumbnailUrl.toString(), + header = "", + headerStatic = "", + locked = false, + fields = emptyList(), + emojis = emptyList(), + bot = false, + group = false, + discoverable = true, + createdAt = it.postActor.createdAt.toString(), + statusesCount = it.postActor.postsCount.postsCount, + noindex = true, + moved = it.postActor.moveTo != null, + suspended = it.postActor.suspend, + limited = false, + lastStatusAt = it.postActor.lastPostAt?.toString(), + followersCount = it.postActor.followersCount?.relationshipCount, + followingCount = it.postActor.followingCount?.relationshipCount, + source = null + ), + content = it.post.content.content, + visibility = when (it.post.visibility) { + PUBLIC -> Status.Visibility.public + UNLISTED -> Status.Visibility.unlisted + FOLLOWERS -> Status.Visibility.private + DIRECT -> Status.Visibility.direct + }, + sensitive = it.post.sensitive, + spoilerText = it.post.overview?.overview.orEmpty(), + mediaAttachments = it.postMedias.map { MediaAttachment(it.id.id.toString()) }, + mentions = emptyList(), + tags = emptyList(), + emojis = emptyList(), + reblogsCount = 0, + favouritesCount = it.reactionsList.sumOf { it.count }, + repliesCount = 0, + url = it.post.url.toString(), + text = it.post.content.text, + application = null, + inReplyToId = it.replyPost?.id?.toString(), + inReplyToAccountId = it.replyPostActor?.id?.toString(), + reblog = null, + poll = null, + card = null, + language = null, + editedAt = null, + favourited = it.favourited, + reblogged = false, + muted = false, + bookmarked = false, + pinned = false, + filtered = emptyList(), + ) + }, readTimeline.next?.id, readTimeline.prev?.id) + } + + companion object { + private val logger = LoggerFactory.getLogger(MastodonReadTimelineApplicationService::class.java) + } +} \ No newline at end of file diff --git a/hideout-mastodon/src/main/kotlin/dev/usbharu/hideout/mastodon/interfaces/api/SpringTimelineApi.kt b/hideout-mastodon/src/main/kotlin/dev/usbharu/hideout/mastodon/interfaces/api/SpringTimelineApi.kt index 2a27d263..9f259736 100644 --- a/hideout-mastodon/src/main/kotlin/dev/usbharu/hideout/mastodon/interfaces/api/SpringTimelineApi.kt +++ b/hideout-mastodon/src/main/kotlin/dev/usbharu/hideout/mastodon/interfaces/api/SpringTimelineApi.kt @@ -16,8 +16,59 @@ package dev.usbharu.hideout.mastodon.interfaces.api +import dev.usbharu.hideout.core.application.exception.InternalServerException +import dev.usbharu.hideout.core.application.shared.Transaction +import dev.usbharu.hideout.core.domain.model.support.page.Page +import dev.usbharu.hideout.core.domain.model.userdetails.UserDetailRepository +import dev.usbharu.hideout.core.infrastructure.springframework.oauth2.SpringSecurityOauth2PrincipalContextHolder +import dev.usbharu.hideout.mastodon.application.timeline.MastodonReadTimeline +import dev.usbharu.hideout.mastodon.application.timeline.MastodonReadTimelineApplicationService import dev.usbharu.hideout.mastodon.interfaces.api.generated.TimelineApi +import dev.usbharu.hideout.mastodon.interfaces.api.generated.model.Status +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.asFlow +import kotlinx.coroutines.runBlocking +import org.springframework.http.ResponseEntity import org.springframework.stereotype.Controller @Controller -class SpringTimelineApi : TimelineApi +class SpringTimelineApi( + private val mastodonReadTimelineApplicationService: MastodonReadTimelineApplicationService, + + private val principalContextHolder: SpringSecurityOauth2PrincipalContextHolder, + private val userDetailRepository: UserDetailRepository, + private val transaction: Transaction, +) : TimelineApi { + override fun apiV1TimelinesHomeGet( + maxId: String?, + sinceId: String?, + minId: String?, + limit: Int? + ): ResponseEntity> = runBlocking { + val principal = principalContextHolder.getPrincipal() + val userDetail = transaction.transaction { + userDetailRepository.findByActorId(principal.actorId.id) + ?: throw InternalServerException("UserDetail not found.") + } + + val homeTimelineId = + userDetail.homeTimelineId ?: throw InternalServerException("HomeTimeline ${userDetail.id} is null.") + + ResponseEntity.ok( + mastodonReadTimelineApplicationService.execute( + MastodonReadTimeline( + timelineId = homeTimelineId.value, + mediaOnly = false, + localOnly = false, + remoteOnly = false, + page = Page.of( + maxId?.toLongOrNull(), + sinceId?.toLongOrNull(), + minId?.toLongOrNull(), + limit + ) + ), principal + ).asFlow() + ) + } +} diff --git a/hideout-mastodon/src/main/resources/openapi/mastodon.yaml b/hideout-mastodon/src/main/resources/openapi/mastodon.yaml index 76d8f5aa..1fb96267 100644 --- a/hideout-mastodon/src/main/resources/openapi/mastodon.yaml +++ b/hideout-mastodon/src/main/resources/openapi/mastodon.yaml @@ -2020,7 +2020,7 @@ components: type: object properties: filter: - $ref: "#/components/schemas/FilterResult" + $ref: "#/components/schemas/Filter" keyword_matches: type: array items: