feat: GenerateTimelineServiceを作成

This commit is contained in:
usbharu 2023-09-29 17:49:34 +09:00
parent 5b89c681b0
commit d9fed726fb
10 changed files with 299 additions and 11 deletions

View File

@ -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<List<Status>> {
}
override fun apiV1TimelinesPublicGet(
local: Boolean?,
remote: Boolean?,
onlyMedia: Boolean?,
maxId: String?,
sinceId: String?,
minId: String?,
limit: Int?
): ResponseEntity<List<Status>> {
}
}

View File

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

View File

@ -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<Long>): List<Status>
}

View File

@ -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<Long>): List<Status> {
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
}
}
}
}

View File

@ -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<Timeline, Long> {
fun findByUserId(id: Long): List<Timeline>
fun findByUserIdAndTimelineId(userId: Long, timelineId: Long): List<Timeline>
fun findByUserIdAndTimelineIdAndPostIdBetweenAndLocal(
userId: Long?,
timelineId: Long?,
postIdMin: Long?,
postIdMax: Long?,
isLocal: Boolean?,
pageable: Pageable
): List<Timeline>
}

View File

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

View File

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

View File

@ -18,25 +18,25 @@ class PostServiceImpl(
private val interceptors = Collections.synchronizedList(mutableListOf<PostCreateInterceptor>())
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)
}
}

View File

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

View File

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