From 4268f90fa5b8e205059950d9e48c999544e223c0 Mon Sep 17 00:00:00 2001 From: usbharu <64310155+usbharu@users.noreply.github.com> Date: Fri, 29 Sep 2023 17:49:34 +0900 Subject: [PATCH] =?UTF-8?q?feat:=20GenerateTimelineService=E3=82=92?= =?UTF-8?q?=E4=BD=9C=E6=88=90?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../mastodon/MastodonTimelineApiController.kt | 30 ++++++ .../domain/model/hideout/entity/Timeline.kt | 3 +- .../query/mastodon/StatusQueryService.kt | 7 ++ .../query/mastodon/StatusQueryServiceImpl.kt | 102 ++++++++++++++++++ .../repository/MongoTimelineRepository.kt | 11 +- .../service/post/GenerateTimelineService.kt | 18 ++++ .../post/MongoGenerateTimelineService.kt | 32 ++++++ .../hideout/service/post/PostServiceImpl.kt | 12 +-- .../hideout/service/post/TimelineService.kt | 5 +- src/main/resources/openapi/mastodon.yaml | 90 ++++++++++++++++ 10 files changed, 299 insertions(+), 11 deletions(-) create mode 100644 src/main/kotlin/dev/usbharu/hideout/controller/mastodon/MastodonTimelineApiController.kt create mode 100644 src/main/kotlin/dev/usbharu/hideout/query/mastodon/StatusQueryService.kt create mode 100644 src/main/kotlin/dev/usbharu/hideout/query/mastodon/StatusQueryServiceImpl.kt create mode 100644 src/main/kotlin/dev/usbharu/hideout/service/post/GenerateTimelineService.kt create mode 100644 src/main/kotlin/dev/usbharu/hideout/service/post/MongoGenerateTimelineService.kt diff --git a/src/main/kotlin/dev/usbharu/hideout/controller/mastodon/MastodonTimelineApiController.kt b/src/main/kotlin/dev/usbharu/hideout/controller/mastodon/MastodonTimelineApiController.kt new file mode 100644 index 00000000..03f3ace8 --- /dev/null +++ b/src/main/kotlin/dev/usbharu/hideout/controller/mastodon/MastodonTimelineApiController.kt @@ -0,0 +1,30 @@ +package dev.usbharu.hideout.controller.mastodon + +import dev.usbharu.hideout.controller.mastodon.generated.TimelineApi +import dev.usbharu.hideout.domain.mastodon.model.generated.Status +import org.springframework.http.ResponseEntity +import org.springframework.stereotype.Controller + +@Controller +class MastodonTimelineApiController : TimelineApi { + override fun apiV1TimelinesHomeGet( + maxId: String?, + sinceId: String?, + minId: String?, + limit: Int? + ): ResponseEntity> { + + } + + override fun apiV1TimelinesPublicGet( + local: Boolean?, + remote: Boolean?, + onlyMedia: Boolean?, + maxId: String?, + sinceId: String?, + minId: String?, + limit: Int? + ): ResponseEntity> { + + } +} diff --git a/src/main/kotlin/dev/usbharu/hideout/domain/model/hideout/entity/Timeline.kt b/src/main/kotlin/dev/usbharu/hideout/domain/model/hideout/entity/Timeline.kt index e8e591ea..64f450a8 100644 --- a/src/main/kotlin/dev/usbharu/hideout/domain/model/hideout/entity/Timeline.kt +++ b/src/main/kotlin/dev/usbharu/hideout/domain/model/hideout/entity/Timeline.kt @@ -13,5 +13,6 @@ data class Timeline( val replyId: Long?, val repostId: Long?, val visibility: Visibility, - val sensitive: Boolean + val sensitive: Boolean, + val isLocal: Boolean ) diff --git a/src/main/kotlin/dev/usbharu/hideout/query/mastodon/StatusQueryService.kt b/src/main/kotlin/dev/usbharu/hideout/query/mastodon/StatusQueryService.kt new file mode 100644 index 00000000..a4ed048c --- /dev/null +++ b/src/main/kotlin/dev/usbharu/hideout/query/mastodon/StatusQueryService.kt @@ -0,0 +1,7 @@ +package dev.usbharu.hideout.query.mastodon + +import dev.usbharu.hideout.domain.mastodon.model.generated.Status + +interface StatusQueryService { + suspend fun findByPostIds(ids: List): List +} diff --git a/src/main/kotlin/dev/usbharu/hideout/query/mastodon/StatusQueryServiceImpl.kt b/src/main/kotlin/dev/usbharu/hideout/query/mastodon/StatusQueryServiceImpl.kt new file mode 100644 index 00000000..fb0cf012 --- /dev/null +++ b/src/main/kotlin/dev/usbharu/hideout/query/mastodon/StatusQueryServiceImpl.kt @@ -0,0 +1,102 @@ +package dev.usbharu.hideout.query.mastodon + +import dev.usbharu.hideout.domain.mastodon.model.generated.Account +import dev.usbharu.hideout.domain.mastodon.model.generated.Status +import dev.usbharu.hideout.repository.Posts +import dev.usbharu.hideout.repository.Users +import org.jetbrains.exposed.sql.innerJoin +import org.jetbrains.exposed.sql.select +import java.time.Instant + +class StatusQueryServiceImpl : StatusQueryService { + override suspend fun findByPostIds(ids: List): List { + + val pairs = Posts.innerJoin(Users, onColumn = { userId }, otherColumn = { id }) + .select { Posts.id inList ids } + .map { + Status( + id = it[Posts.id].toString(), + uri = it[Posts.apId], + createdAt = Instant.ofEpochMilli(it[Posts.createdAt]).toString(), + account = Account( + id = it[Users.id].toString(), + username = it[Users.name], + acct = "${it[Users.name]}@${it[Users.domain]}", + url = it[Users.url], + displayName = it[Users.screenName], + note = it[Users.description], + avatar = it[Users.url] + "/icon.jpg", + avatarStatic = it[Users.url] + "/icon.jpg", + header = it[Users.url] + "/header.jpg", + headerStatic = it[Users.url] + "/header.jpg", + locked = false, + fields = emptyList(), + emojis = emptyList(), + bot = false, + group = false, + discoverable = true, + createdAt = Instant.ofEpochMilli(it[Users.createdAt]).toString(), + lastStatusAt = Instant.ofEpochMilli(it[Users.createdAt]).toString(), + statusesCount = 0, + followersCount = 0, + followingCount = 0, + noindex = false, + moved = false, + suspendex = false, + limited = false + ), + content = it[Posts.text], + visibility = when (it[Posts.visibility]) { + 0 -> Status.Visibility.public + 1 -> Status.Visibility.unlisted + 2 -> Status.Visibility.private + 3 -> Status.Visibility.direct + else -> Status.Visibility.public + }, + sensitive = it[Posts.sensitive], + spoilerText = it[Posts.overview].orEmpty(), + mediaAttachments = emptyList(), + mentions = emptyList(), + tags = emptyList(), + emojis = emptyList(), + reblogsCount = 0, + favouritesCount = 0, + repliesCount = 0, + url = it[Posts.apId], + inReplyToId = it[Posts.replyId].toString(), + 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 + ) to it[Posts.repostId] + } + + val statuses = pairs.map { it.first } + return pairs + .map { + if (it.second != null) { + it.first.copy(reblog = statuses.find { status -> status.id == it.second.toString() }) + } else { + it.first + } + } + .map { + if (it.inReplyToId != null) { + it.copy(inReplyToAccountId = statuses.find { status -> status.id == it.inReplyToId }?.id) + } else { + it + } + } + + + } +} diff --git a/src/main/kotlin/dev/usbharu/hideout/repository/MongoTimelineRepository.kt b/src/main/kotlin/dev/usbharu/hideout/repository/MongoTimelineRepository.kt index 716609f5..2e92c7f4 100644 --- a/src/main/kotlin/dev/usbharu/hideout/repository/MongoTimelineRepository.kt +++ b/src/main/kotlin/dev/usbharu/hideout/repository/MongoTimelineRepository.kt @@ -1,11 +1,18 @@ package dev.usbharu.hideout.repository import dev.usbharu.hideout.domain.model.hideout.entity.Timeline +import org.springframework.data.domain.Pageable import org.springframework.data.mongodb.repository.MongoRepository interface MongoTimelineRepository : MongoRepository { - - fun findByUserId(id: Long): List fun findByUserIdAndTimelineId(userId: Long, timelineId: Long): List + fun findByUserIdAndTimelineIdAndPostIdBetweenAndLocal( + userId: Long?, + timelineId: Long?, + postIdMin: Long?, + postIdMax: Long?, + isLocal: Boolean?, + pageable: Pageable + ): List } diff --git a/src/main/kotlin/dev/usbharu/hideout/service/post/GenerateTimelineService.kt b/src/main/kotlin/dev/usbharu/hideout/service/post/GenerateTimelineService.kt new file mode 100644 index 00000000..ea1a26a4 --- /dev/null +++ b/src/main/kotlin/dev/usbharu/hideout/service/post/GenerateTimelineService.kt @@ -0,0 +1,18 @@ +package dev.usbharu.hideout.service.post + +import dev.usbharu.hideout.domain.mastodon.model.generated.Status +import org.springframework.stereotype.Service + +@Service +interface GenerateTimelineService { + suspend fun getTimeline( + forUserId: Long? = null, + localOnly: Boolean = false, + mediaOnly: Boolean = false, + maxId: Long? = null, + minId: Long? = null, + sinceId: Long? = null, + limit: Int = 20 + ): List + +} diff --git a/src/main/kotlin/dev/usbharu/hideout/service/post/MongoGenerateTimelineService.kt b/src/main/kotlin/dev/usbharu/hideout/service/post/MongoGenerateTimelineService.kt new file mode 100644 index 00000000..8f1b7b33 --- /dev/null +++ b/src/main/kotlin/dev/usbharu/hideout/service/post/MongoGenerateTimelineService.kt @@ -0,0 +1,32 @@ +package dev.usbharu.hideout.service.post + +import dev.usbharu.hideout.domain.mastodon.model.generated.Status +import dev.usbharu.hideout.query.mastodon.StatusQueryService +import dev.usbharu.hideout.repository.MongoTimelineRepository +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.withContext +import org.springframework.data.domain.Pageable + +class MongoGenerateTimelineService( + private val mongoTimelineRepository: MongoTimelineRepository, + private val statusQueryService: StatusQueryService +) : + GenerateTimelineService { + override suspend fun getTimeline( + forUserId: Long?, + localOnly: Boolean, + mediaOnly: Boolean, + maxId: Long?, + minId: Long?, + sinceId: Long?, + limit: Int + ): List { + val timelines = + withContext(Dispatchers.IO) { + mongoTimelineRepository.findByUserIdAndTimelineIdAndPostIdBetweenAndLocal( + forUserId, 0, maxId, minId, localOnly, Pageable.ofSize(limit) + ) + } + return statusQueryService.findByPostIds(timelines.flatMap { setOfNotNull(it.postId, it.replyId, it.repostId) }) + } +} diff --git a/src/main/kotlin/dev/usbharu/hideout/service/post/PostServiceImpl.kt b/src/main/kotlin/dev/usbharu/hideout/service/post/PostServiceImpl.kt index f2531442..2fb9a1e4 100644 --- a/src/main/kotlin/dev/usbharu/hideout/service/post/PostServiceImpl.kt +++ b/src/main/kotlin/dev/usbharu/hideout/service/post/PostServiceImpl.kt @@ -18,25 +18,25 @@ class PostServiceImpl( private val interceptors = Collections.synchronizedList(mutableListOf()) override suspend fun createLocal(post: PostCreateDto): Post { - val create = internalCreate(post) + val create = internalCreate(post, true) interceptors.forEach { it.run(create) } return create } override suspend fun createRemote(post: Post): Post { - return internalCreate(post) + return internalCreate(post, false) } override fun addInterceptor(postCreateInterceptor: PostCreateInterceptor) { interceptors.add(postCreateInterceptor) } - private suspend fun internalCreate(post: Post): Post { - timelineService.publishTimeline(post) + private suspend fun internalCreate(post: Post, isLocal: Boolean): Post { + timelineService.publishTimeline(post, isLocal) return postRepository.save(post) } - private suspend fun internalCreate(post: PostCreateDto): Post { + private suspend fun internalCreate(post: PostCreateDto, isLocal: Boolean): Post { val user = userRepository.findById(post.userId) ?: throw UserNotFoundException("${post.userId} was not found") val id = postRepository.generateId() val createPost = Post.of( @@ -50,6 +50,6 @@ class PostServiceImpl( repostId = null, replyId = null ) - return internalCreate(createPost) + return internalCreate(createPost, isLocal) } } diff --git a/src/main/kotlin/dev/usbharu/hideout/service/post/TimelineService.kt b/src/main/kotlin/dev/usbharu/hideout/service/post/TimelineService.kt index 681893f7..d0aeefbb 100644 --- a/src/main/kotlin/dev/usbharu/hideout/service/post/TimelineService.kt +++ b/src/main/kotlin/dev/usbharu/hideout/service/post/TimelineService.kt @@ -11,7 +11,7 @@ class TimelineService( private val followerQueryService: FollowerQueryService, private val timelineRepository: TimelineRepository ) { - suspend fun publishTimeline(post: Post) { + suspend fun publishTimeline(post: Post, isLocal: Boolean) { val findFollowersById = followerQueryService.findFollowersById(post.userId) timelineRepository.saveAll(findFollowersById.map { Timeline( @@ -24,7 +24,8 @@ class TimelineService( replyId = post.replyId, repostId = post.repostId, visibility = post.visibility, - sensitive = post.sensitive + sensitive = post.sensitive, + isLocal = isLocal ) }) } diff --git a/src/main/resources/openapi/mastodon.yaml b/src/main/resources/openapi/mastodon.yaml index 94b34dd6..5464a28a 100644 --- a/src/main/resources/openapi/mastodon.yaml +++ b/src/main/resources/openapi/mastodon.yaml @@ -15,6 +15,8 @@ tags: description: app - name: instance description: instance + - name: timeline + description: timeline paths: /api/v2/instance: @@ -202,6 +204,94 @@ paths: 200: description: 成功 + /api/v1/timelines/public: + get: + tags: + - timeline + parameters: + - in: query + name: local + required: false + schema: + type: boolean + - in: query + name: remote + required: false + schema: + type: boolean + - in: query + name: only_media + required: false + schema: + type: boolean + - in: query + name: max_id + required: false + schema: + type: string + - in: query + name: since_id + required: false + schema: + type: string + - in: query + name: min_id + required: false + schema: + type: string + - in: query + name: limit + required: false + schema: + type: integer + responses: + 200: + description: 成功 + content: + application/json: + schema: + type: array + items: + $ref: "#/components/schemas/Status" + + /api/v1/timelines/home: + get: + tags: + - timeline + security: + - OAuth2: + - "read:statuses" + parameters: + - in: query + name: max_id + required: false + schema: + type: string + - in: query + name: since_id + required: false + schema: + type: string + - in: query + name: min_id + required: false + schema: + type: string + - in: query + name: limit + required: false + schema: + type: integer + responses: + 200: + description: 成功 + content: + application/json: + schema: + type: array + items: + $ref: "#/components/schemas/Status" + components: schemas: AccountsCreateRequest: