mirror of https://github.com/usbharu/Hideout.git
feat: GenerateTimelineServiceを作成
This commit is contained in:
parent
5b89c681b0
commit
d9fed726fb
|
@ -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>> {
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
|
@ -13,5 +13,6 @@ data class Timeline(
|
||||||
val replyId: Long?,
|
val replyId: Long?,
|
||||||
val repostId: Long?,
|
val repostId: Long?,
|
||||||
val visibility: Visibility,
|
val visibility: Visibility,
|
||||||
val sensitive: Boolean
|
val sensitive: Boolean,
|
||||||
|
val isLocal: Boolean
|
||||||
)
|
)
|
||||||
|
|
|
@ -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>
|
||||||
|
}
|
|
@ -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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,11 +1,18 @@
|
||||||
package dev.usbharu.hideout.repository
|
package dev.usbharu.hideout.repository
|
||||||
|
|
||||||
import dev.usbharu.hideout.domain.model.hideout.entity.Timeline
|
import dev.usbharu.hideout.domain.model.hideout.entity.Timeline
|
||||||
|
import org.springframework.data.domain.Pageable
|
||||||
import org.springframework.data.mongodb.repository.MongoRepository
|
import org.springframework.data.mongodb.repository.MongoRepository
|
||||||
|
|
||||||
interface MongoTimelineRepository : MongoRepository<Timeline, Long> {
|
interface MongoTimelineRepository : MongoRepository<Timeline, Long> {
|
||||||
|
|
||||||
|
|
||||||
fun findByUserId(id: Long): List<Timeline>
|
fun findByUserId(id: Long): List<Timeline>
|
||||||
fun findByUserIdAndTimelineId(userId: Long, timelineId: 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>
|
||||||
}
|
}
|
||||||
|
|
|
@ -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>
|
||||||
|
|
||||||
|
}
|
|
@ -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) })
|
||||||
|
}
|
||||||
|
}
|
|
@ -18,25 +18,25 @@ class PostServiceImpl(
|
||||||
private val interceptors = Collections.synchronizedList(mutableListOf<PostCreateInterceptor>())
|
private val interceptors = Collections.synchronizedList(mutableListOf<PostCreateInterceptor>())
|
||||||
|
|
||||||
override suspend fun createLocal(post: PostCreateDto): Post {
|
override suspend fun createLocal(post: PostCreateDto): Post {
|
||||||
val create = internalCreate(post)
|
val create = internalCreate(post, true)
|
||||||
interceptors.forEach { it.run(create) }
|
interceptors.forEach { it.run(create) }
|
||||||
return create
|
return create
|
||||||
}
|
}
|
||||||
|
|
||||||
override suspend fun createRemote(post: Post): Post {
|
override suspend fun createRemote(post: Post): Post {
|
||||||
return internalCreate(post)
|
return internalCreate(post, false)
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun addInterceptor(postCreateInterceptor: PostCreateInterceptor) {
|
override fun addInterceptor(postCreateInterceptor: PostCreateInterceptor) {
|
||||||
interceptors.add(postCreateInterceptor)
|
interceptors.add(postCreateInterceptor)
|
||||||
}
|
}
|
||||||
|
|
||||||
private suspend fun internalCreate(post: Post): Post {
|
private suspend fun internalCreate(post: Post, isLocal: Boolean): Post {
|
||||||
timelineService.publishTimeline(post)
|
timelineService.publishTimeline(post, isLocal)
|
||||||
return postRepository.save(post)
|
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 user = userRepository.findById(post.userId) ?: throw UserNotFoundException("${post.userId} was not found")
|
||||||
val id = postRepository.generateId()
|
val id = postRepository.generateId()
|
||||||
val createPost = Post.of(
|
val createPost = Post.of(
|
||||||
|
@ -50,6 +50,6 @@ class PostServiceImpl(
|
||||||
repostId = null,
|
repostId = null,
|
||||||
replyId = null
|
replyId = null
|
||||||
)
|
)
|
||||||
return internalCreate(createPost)
|
return internalCreate(createPost, isLocal)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -11,7 +11,7 @@ class TimelineService(
|
||||||
private val followerQueryService: FollowerQueryService,
|
private val followerQueryService: FollowerQueryService,
|
||||||
private val timelineRepository: TimelineRepository
|
private val timelineRepository: TimelineRepository
|
||||||
) {
|
) {
|
||||||
suspend fun publishTimeline(post: Post) {
|
suspend fun publishTimeline(post: Post, isLocal: Boolean) {
|
||||||
val findFollowersById = followerQueryService.findFollowersById(post.userId)
|
val findFollowersById = followerQueryService.findFollowersById(post.userId)
|
||||||
timelineRepository.saveAll(findFollowersById.map {
|
timelineRepository.saveAll(findFollowersById.map {
|
||||||
Timeline(
|
Timeline(
|
||||||
|
@ -24,7 +24,8 @@ class TimelineService(
|
||||||
replyId = post.replyId,
|
replyId = post.replyId,
|
||||||
repostId = post.repostId,
|
repostId = post.repostId,
|
||||||
visibility = post.visibility,
|
visibility = post.visibility,
|
||||||
sensitive = post.sensitive
|
sensitive = post.sensitive,
|
||||||
|
isLocal = isLocal
|
||||||
)
|
)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
|
@ -15,6 +15,8 @@ tags:
|
||||||
description: app
|
description: app
|
||||||
- name: instance
|
- name: instance
|
||||||
description: instance
|
description: instance
|
||||||
|
- name: timeline
|
||||||
|
description: timeline
|
||||||
|
|
||||||
paths:
|
paths:
|
||||||
/api/v2/instance:
|
/api/v2/instance:
|
||||||
|
@ -202,6 +204,94 @@ paths:
|
||||||
200:
|
200:
|
||||||
description: 成功
|
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:
|
components:
|
||||||
schemas:
|
schemas:
|
||||||
AccountsCreateRequest:
|
AccountsCreateRequest:
|
||||||
|
|
Loading…
Reference in New Issue