mirror of https://github.com/usbharu/Hideout.git
Merge pull request #599 from usbharu/mastodon-timeline-api
feat: Mastodonでホームタイムラインを読めるように
This commit is contained in:
commit
2e0f0c77bf
|
@ -29,10 +29,10 @@ class UserCreateReactionApplicationService(
|
||||||
private val unicodeEmojiService: UnicodeEmojiService
|
private val unicodeEmojiService: UnicodeEmojiService
|
||||||
) :
|
) :
|
||||||
LocalUserAbstractApplicationService<CreateReaction, Unit>(
|
LocalUserAbstractApplicationService<CreateReaction, Unit>(
|
||||||
transaction, logger
|
transaction,
|
||||||
|
logger
|
||||||
) {
|
) {
|
||||||
override suspend fun internalExecute(command: CreateReaction, principal: LocalUser) {
|
override suspend fun internalExecute(command: CreateReaction, principal: LocalUser) {
|
||||||
|
|
||||||
val postId = PostId(command.postId)
|
val postId = PostId(command.postId)
|
||||||
val post = postRepository.findById(postId) ?: throw IllegalArgumentException("Post $postId not found.")
|
val post = postRepository.findById(postId) ?: throw IllegalArgumentException("Post $postId not found.")
|
||||||
if (postReadAccessControl.isAllow(post, principal).not()) {
|
if (postReadAccessControl.isAllow(post, principal).not()) {
|
||||||
|
@ -62,4 +62,4 @@ class UserCreateReactionApplicationService(
|
||||||
companion object {
|
companion object {
|
||||||
private val logger = LoggerFactory.getLogger(UserCreateReactionApplicationService::class.java)
|
private val logger = LoggerFactory.getLogger(UserCreateReactionApplicationService::class.java)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -18,7 +18,8 @@ class UserRemoveReactionApplicationService(
|
||||||
private val unicodeEmojiService: UnicodeEmojiService
|
private val unicodeEmojiService: UnicodeEmojiService
|
||||||
) :
|
) :
|
||||||
LocalUserAbstractApplicationService<RemoveReaction, Unit>(
|
LocalUserAbstractApplicationService<RemoveReaction, Unit>(
|
||||||
transaction, logger
|
transaction,
|
||||||
|
logger
|
||||||
) {
|
) {
|
||||||
override suspend fun internalExecute(command: RemoveReaction, principal: LocalUser) {
|
override suspend fun internalExecute(command: RemoveReaction, principal: LocalUser) {
|
||||||
val postId = PostId(command.postId)
|
val postId = PostId(command.postId)
|
||||||
|
@ -47,4 +48,4 @@ class UserRemoveReactionApplicationService(
|
||||||
companion object {
|
companion object {
|
||||||
private val logger = LoggerFactory.getLogger(UserRemoveReactionApplicationService::class.java)
|
private val logger = LoggerFactory.getLogger(UserRemoveReactionApplicationService::class.java)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -19,4 +19,4 @@ class ReactionEventBody(
|
||||||
enum class ReactionEvent(val eventName: String) {
|
enum class ReactionEvent(val eventName: String) {
|
||||||
CREATE("ReactionCreate"),
|
CREATE("ReactionCreate"),
|
||||||
DELETE("ReactionDelete"),
|
DELETE("ReactionDelete"),
|
||||||
}
|
}
|
||||||
|
|
|
@ -29,9 +29,9 @@ sealed class Emoji {
|
||||||
abstract fun id(): String
|
abstract fun id(): String
|
||||||
override fun toString(): String {
|
override fun toString(): String {
|
||||||
return "Emoji(" +
|
return "Emoji(" +
|
||||||
"domain='$domain', " +
|
"domain='$domain', " +
|
||||||
"name='$name'" +
|
"name='$name'" +
|
||||||
")"
|
")"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -18,7 +18,6 @@ class Reaction(
|
||||||
val createdAt: Instant
|
val createdAt: Instant
|
||||||
) : DomainEventStorable() {
|
) : DomainEventStorable() {
|
||||||
|
|
||||||
|
|
||||||
override fun equals(other: Any?): Boolean {
|
override fun equals(other: Any?): Boolean {
|
||||||
if (this === other) return true
|
if (this === other) return true
|
||||||
if (javaClass != other?.javaClass) return false
|
if (javaClass != other?.javaClass) return false
|
||||||
|
@ -46,8 +45,13 @@ class Reaction(
|
||||||
createdAt: Instant
|
createdAt: Instant
|
||||||
): Reaction {
|
): Reaction {
|
||||||
return Reaction(
|
return Reaction(
|
||||||
id, postId, actorId, customEmojiId, unicodeEmoji, createdAt
|
id,
|
||||||
|
postId,
|
||||||
|
actorId,
|
||||||
|
customEmojiId,
|
||||||
|
unicodeEmoji,
|
||||||
|
createdAt
|
||||||
).apply { addDomainEvent(ReactionEventFactory(this).createEvent(ReactionEvent.CREATE)) }
|
).apply { addDomainEvent(ReactionEventFactory(this).createEvent(ReactionEvent.CREATE)) }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
package dev.usbharu.hideout.core.domain.model.reaction
|
package dev.usbharu.hideout.core.domain.model.reaction
|
||||||
|
|
||||||
@JvmInline
|
@JvmInline
|
||||||
value class ReactionId(val value: Long)
|
value class ReactionId(val value: Long)
|
||||||
|
|
|
@ -23,4 +23,4 @@ interface ReactionRepository {
|
||||||
): Reaction?
|
): Reaction?
|
||||||
|
|
||||||
suspend fun delete(reaction: Reaction)
|
suspend fun delete(reaction: Reaction)
|
||||||
}
|
}
|
||||||
|
|
|
@ -61,9 +61,9 @@ class TimelineObject(
|
||||||
lastUpdatedAt = Instant.now()
|
lastUpdatedAt = Instant.now()
|
||||||
isPureRepost =
|
isPureRepost =
|
||||||
post.repostId != null &&
|
post.repostId != null &&
|
||||||
post.replyId == null &&
|
post.replyId == null &&
|
||||||
post.text.isEmpty() &&
|
post.text.isEmpty() &&
|
||||||
post.overview?.overview.isNullOrEmpty()
|
post.overview?.overview.isNullOrEmpty()
|
||||||
warnFilters = filterResults.map { TimelineObjectWarnFilter(it.filter.id, it.matchedKeyword) }
|
warnFilters = filterResults.map { TimelineObjectWarnFilter(it.filter.id, it.matchedKeyword) }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -133,9 +133,9 @@ class TimelineObject(
|
||||||
repostActorId = repost.actorId,
|
repostActorId = repost.actorId,
|
||||||
visibility = post.visibility,
|
visibility = post.visibility,
|
||||||
isPureRepost = repost.mediaIds.isEmpty() &&
|
isPureRepost = repost.mediaIds.isEmpty() &&
|
||||||
repost.overview == null &&
|
repost.overview == null &&
|
||||||
repost.content == PostContent.empty &&
|
repost.content == PostContent.empty &&
|
||||||
repost.replyId == null,
|
repost.replyId == null,
|
||||||
mediaIds = post.mediaIds,
|
mediaIds = post.mediaIds,
|
||||||
emojiIds = post.emojiIds,
|
emojiIds = post.emojiIds,
|
||||||
visibleActors = post.visibleActors.toList(),
|
visibleActors = post.visibleActors.toList(),
|
||||||
|
|
|
@ -2,4 +2,4 @@ package dev.usbharu.hideout.core.domain.service.emoji
|
||||||
|
|
||||||
interface UnicodeEmojiService {
|
interface UnicodeEmojiService {
|
||||||
fun isUnicodeEmoji(emoji: String): Boolean
|
fun isUnicodeEmoji(emoji: String): Boolean
|
||||||
}
|
}
|
||||||
|
|
|
@ -9,4 +9,4 @@ class EmojiKtUnicodeEmojiService : UnicodeEmojiService {
|
||||||
override fun isUnicodeEmoji(emoji: String): Boolean {
|
override fun isUnicodeEmoji(emoji: String): Boolean {
|
||||||
return Emojis.allEmojis.singleOrNull { it.char == emoji } != null
|
return Emojis.allEmojis.singleOrNull { it.char == emoji } != null
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -37,7 +37,6 @@ class ExposedReactionsQueryService : ReactionsQueryService, AbstractRepository()
|
||||||
|
|
||||||
override suspend fun findAllByPostIdIn(postIds: List<PostId>): List<Reactions> {
|
override suspend fun findAllByPostIdIn(postIds: List<PostId>): List<Reactions> {
|
||||||
return query {
|
return query {
|
||||||
|
|
||||||
val actorIdsQuery =
|
val actorIdsQuery =
|
||||||
ExposedrepositoryReactions.actorId.castTo<String>(VarCharColumnType()).groupConcat(",", true)
|
ExposedrepositoryReactions.actorId.castTo<String>(VarCharColumnType()).groupConcat(",", true)
|
||||||
|
|
||||||
|
@ -71,4 +70,4 @@ class ExposedReactionsQueryService : ReactionsQueryService, AbstractRepository()
|
||||||
companion object {
|
companion object {
|
||||||
private val logger = LoggerFactory.getLogger(ExposedReactionsQueryService::class.java)
|
private val logger = LoggerFactory.getLogger(ExposedReactionsQueryService::class.java)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -42,10 +42,10 @@ class ExposedUserTimelineQueryService : UserTimelineQueryService, AbstractReposi
|
||||||
.select(Posts.columns)
|
.select(Posts.columns)
|
||||||
.where {
|
.where {
|
||||||
Posts.visibility eq Visibility.PUBLIC.name or
|
Posts.visibility eq Visibility.PUBLIC.name or
|
||||||
(Posts.visibility eq Visibility.UNLISTED.name) or
|
(Posts.visibility eq Visibility.UNLISTED.name) or
|
||||||
(Posts.visibility eq Visibility.DIRECT.name and (PostsVisibleActors.actorId eq principal.actorId.id)) or
|
(Posts.visibility eq Visibility.DIRECT.name and (PostsVisibleActors.actorId eq principal.actorId.id)) or
|
||||||
(Posts.visibility eq Visibility.FOLLOWERS.name and (Relationships.blocking eq false and (relationshipsAlias[Relationships.following] eq true))) or
|
(Posts.visibility eq Visibility.FOLLOWERS.name and (Relationships.blocking eq false and (relationshipsAlias[Relationships.following] eq true))) or
|
||||||
(Posts.actorId eq principal.actorId.id)
|
(Posts.actorId eq principal.actorId.id)
|
||||||
}
|
}
|
||||||
.alias("authorized_table")
|
.alias("authorized_table")
|
||||||
}
|
}
|
||||||
|
@ -61,10 +61,12 @@ class ExposedUserTimelineQueryService : UserTimelineQueryService, AbstractReposi
|
||||||
.leftJoin(iconMedia, { Actors.icon }, { iconMedia[Media.id] })
|
.leftJoin(iconMedia, { Actors.icon }, { iconMedia[Media.id] })
|
||||||
.leftJoin(PostsMedia, { authorizedQuery[Posts.id] }, { PostsMedia.postId })
|
.leftJoin(PostsMedia, { authorizedQuery[Posts.id] }, { PostsMedia.postId })
|
||||||
.leftJoin(Media, { PostsMedia.mediaId }, { Media.id })
|
.leftJoin(Media, { PostsMedia.mediaId }, { Media.id })
|
||||||
.leftJoin(Reactions,
|
.leftJoin(
|
||||||
|
Reactions,
|
||||||
{ authorizedQuery[Posts.id] },
|
{ authorizedQuery[Posts.id] },
|
||||||
{ Reactions.postId },
|
{ Reactions.postId },
|
||||||
{ Reactions.id isDistinctFrom principal.actorId.id })
|
{ Reactions.id isDistinctFrom principal.actorId.id }
|
||||||
|
)
|
||||||
.selectAll()
|
.selectAll()
|
||||||
.where { authorizedQuery[Posts.id] inList idList.map { it.id } }
|
.where { authorizedQuery[Posts.id] inList idList.map { it.id } }
|
||||||
.groupBy { it[authorizedQuery[Posts.id]] }
|
.groupBy { it[authorizedQuery[Posts.id]] }
|
||||||
|
@ -73,7 +75,8 @@ class ExposedUserTimelineQueryService : UserTimelineQueryService, AbstractReposi
|
||||||
toPostDetail(it.first(), authorizedQuery, iconMedia).copy(
|
toPostDetail(it.first(), authorizedQuery, iconMedia).copy(
|
||||||
mediaDetailList = it.mapNotNull { resultRow ->
|
mediaDetailList = it.mapNotNull { resultRow ->
|
||||||
resultRow.toMediaOrNull()?.let { it1 -> MediaDetail.of(it1) }
|
resultRow.toMediaOrNull()?.let { it1 -> MediaDetail.of(it1) }
|
||||||
}, favourited = it.any { it.getOrNull(Reactions.actorId) != null }
|
},
|
||||||
|
favourited = it.any { it.getOrNull(Reactions.actorId) != null }
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -17,13 +17,14 @@ import org.slf4j.LoggerFactory
|
||||||
import org.springframework.stereotype.Repository
|
import org.springframework.stereotype.Repository
|
||||||
|
|
||||||
@Repository
|
@Repository
|
||||||
class ExposedReactionRepository(override val domainEventPublisher: DomainEventPublisher) : ReactionRepository,
|
class ExposedReactionRepository(override val domainEventPublisher: DomainEventPublisher) :
|
||||||
AbstractRepository(), DomainEventPublishableRepository<Reaction> {
|
ReactionRepository,
|
||||||
|
AbstractRepository(),
|
||||||
|
DomainEventPublishableRepository<Reaction> {
|
||||||
|
|
||||||
override val logger: Logger
|
override val logger: Logger
|
||||||
get() = Companion.logger
|
get() = Companion.logger
|
||||||
|
|
||||||
|
|
||||||
override suspend fun save(reaction: Reaction): Reaction {
|
override suspend fun save(reaction: Reaction): Reaction {
|
||||||
return query {
|
return query {
|
||||||
Reactions.upsert {
|
Reactions.upsert {
|
||||||
|
@ -66,7 +67,9 @@ class ExposedReactionRepository(override val domainEventPublisher: DomainEventPu
|
||||||
return query {
|
return query {
|
||||||
Reactions.selectAll().where {
|
Reactions.selectAll().where {
|
||||||
Reactions.postId.eq(postId.id).and(Reactions.actorId eq actorId.id)
|
Reactions.postId.eq(postId.id).and(Reactions.actorId eq actorId.id)
|
||||||
.and((Reactions.customEmojiId eq customEmojiId?.emojiId or (Reactions.unicodeEmoji eq unicodeEmoji)))
|
.and(
|
||||||
|
(Reactions.customEmojiId eq customEmojiId?.emojiId or (Reactions.unicodeEmoji eq unicodeEmoji))
|
||||||
|
)
|
||||||
}.empty().not()
|
}.empty().not()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -89,10 +92,11 @@ class ExposedReactionRepository(override val domainEventPublisher: DomainEventPu
|
||||||
unicodeEmoji: String
|
unicodeEmoji: String
|
||||||
): Reaction? {
|
): Reaction? {
|
||||||
return query {
|
return query {
|
||||||
|
|
||||||
Reactions.selectAll().where {
|
Reactions.selectAll().where {
|
||||||
Reactions.postId.eq(postId.id).and(Reactions.actorId eq actorId.id)
|
Reactions.postId.eq(postId.id).and(Reactions.actorId eq actorId.id)
|
||||||
.and((Reactions.customEmojiId eq customEmojiId?.emojiId or (Reactions.unicodeEmoji eq unicodeEmoji)))
|
.and(
|
||||||
|
(Reactions.customEmojiId eq customEmojiId?.emojiId or (Reactions.unicodeEmoji eq unicodeEmoji))
|
||||||
|
)
|
||||||
}.limit(1).singleOrNull()?.toReaction()
|
}.limit(1).singleOrNull()?.toReaction()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -111,7 +115,6 @@ fun ResultRow.toReaction(): Reaction {
|
||||||
UnicodeEmoji(this[Reactions.unicodeEmoji]),
|
UnicodeEmoji(this[Reactions.unicodeEmoji]),
|
||||||
this[Reactions.createdAt]
|
this[Reactions.createdAt]
|
||||||
)
|
)
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
object Reactions : Table("reactions") {
|
object Reactions : Table("reactions") {
|
||||||
|
@ -122,4 +125,4 @@ object Reactions : Table("reactions") {
|
||||||
val unicodeEmoji = varchar("unicode_emoji", 100)
|
val unicodeEmoji = varchar("unicode_emoji", 100)
|
||||||
val createdAt = timestamp("created_at")
|
val createdAt = timestamp("created_at")
|
||||||
override val primaryKey: PrimaryKey = PrimaryKey(id)
|
override val primaryKey: PrimaryKey = PrimaryKey(id)
|
||||||
}
|
}
|
||||||
|
|
|
@ -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.mapping.Document
|
||||||
import org.springframework.data.mongodb.core.query.Criteria
|
import org.springframework.data.mongodb.core.query.Criteria
|
||||||
import org.springframework.data.mongodb.core.query.Query
|
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.data.repository.kotlin.CoroutineCrudRepository
|
||||||
import org.springframework.stereotype.Repository
|
import org.springframework.stereotype.Repository
|
||||||
import java.time.Instant
|
import java.time.Instant
|
||||||
|
@ -84,6 +85,8 @@ class MongoInternalTimelineObjectRepository(
|
||||||
): PaginationList<TimelineObject, PostId> {
|
): PaginationList<TimelineObject, PostId> {
|
||||||
val query = Query()
|
val query = Query()
|
||||||
|
|
||||||
|
query.addCriteria(Criteria.where("timelineId").isEqualTo(timelineId.value))
|
||||||
|
|
||||||
if (page?.minId != null) {
|
if (page?.minId != null) {
|
||||||
query.with(Sort.by(Sort.Direction.ASC, "postCreatedAt"))
|
query.with(Sort.by(Sort.Direction.ASC, "postCreatedAt"))
|
||||||
page.minId?.let { query.addCriteria(Criteria.where("postId").gt(it)) }
|
page.minId?.let { query.addCriteria(Criteria.where("postId").gt(it)) }
|
||||||
|
|
|
@ -246,8 +246,8 @@ abstract class AbstractTimelineStore(private val idGenerateService: IdGenerateSe
|
||||||
val actors =
|
val actors =
|
||||||
getActors(
|
getActors(
|
||||||
timelineObjectList.map { it.postActorId } +
|
timelineObjectList.map { it.postActorId } +
|
||||||
timelineObjectList.mapNotNull { it.repostActorId } +
|
timelineObjectList.mapNotNull { it.repostActorId } +
|
||||||
timelineObjectList.mapNotNull { it.replyActorId }
|
timelineObjectList.mapNotNull { it.replyActorId }
|
||||||
)
|
)
|
||||||
|
|
||||||
val postMap = posts.associate { post ->
|
val postMap = posts.associate { post ->
|
||||||
|
@ -256,7 +256,7 @@ abstract class AbstractTimelineStore(private val idGenerateService: IdGenerateSe
|
||||||
|
|
||||||
val mediaMap = getMedias(
|
val mediaMap = getMedias(
|
||||||
posts.flatMap { it.mediaIds } +
|
posts.flatMap { it.mediaIds } +
|
||||||
actors.mapNotNull { it.value.icon }
|
actors.mapNotNull { it.value.icon }
|
||||||
)
|
)
|
||||||
|
|
||||||
val reactions = getReactions(posts.map { it.id })
|
val reactions = getReactions(posts.map { it.id })
|
||||||
|
|
|
@ -47,7 +47,8 @@ class PostsController(
|
||||||
id,
|
id,
|
||||||
null,
|
null,
|
||||||
"❤"
|
"❤"
|
||||||
), principal
|
),
|
||||||
|
principal
|
||||||
)
|
)
|
||||||
return "redirect:/users/$name/posts/$id"
|
return "redirect:/users/$name/posts/$id"
|
||||||
}
|
}
|
||||||
|
@ -60,7 +61,8 @@ class PostsController(
|
||||||
id,
|
id,
|
||||||
null,
|
null,
|
||||||
"❤"
|
"❤"
|
||||||
), principal
|
),
|
||||||
|
principal
|
||||||
)
|
)
|
||||||
return "redirect:/users/$name/posts/$id"
|
return "redirect:/users/$name/posts/$id"
|
||||||
}
|
}
|
||||||
|
|
|
@ -6,4 +6,4 @@ import dev.usbharu.hideout.core.domain.model.post.PostId
|
||||||
interface ReactionsQueryService {
|
interface ReactionsQueryService {
|
||||||
suspend fun findAllByPostId(postId: PostId): List<Reactions>
|
suspend fun findAllByPostId(postId: PostId): List<Reactions>
|
||||||
suspend fun findAllByPostIdIn(postIds: List<PostId>): List<Reactions>
|
suspend fun findAllByPostIdIn(postIds: List<PostId>): List<Reactions>
|
||||||
}
|
}
|
||||||
|
|
|
@ -14,8 +14,8 @@
|
||||||
</noscript>
|
</noscript>
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<noscript>
|
|
||||||
<a th:href="${nsUrl}">No Script</a>
|
<a th:href="${nsUrl}">No Script</a>
|
||||||
</noscript>
|
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
|
@ -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
|
||||||
|
) {
|
||||||
|
|
||||||
|
}
|
|
@ -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<MastodonReadTimeline, PaginationList<Status, Long>>(transaction, logger) {
|
||||||
|
override suspend fun internalExecute(
|
||||||
|
command: MastodonReadTimeline,
|
||||||
|
principal: Principal
|
||||||
|
): PaginationList<Status, Long> {
|
||||||
|
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)
|
||||||
|
}
|
||||||
|
}
|
|
@ -16,8 +16,59 @@
|
||||||
|
|
||||||
package dev.usbharu.hideout.mastodon.interfaces.api
|
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.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
|
import org.springframework.stereotype.Controller
|
||||||
|
|
||||||
@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<Flow<Status>> = 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()
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -2020,7 +2020,7 @@ components:
|
||||||
type: object
|
type: object
|
||||||
properties:
|
properties:
|
||||||
filter:
|
filter:
|
||||||
$ref: "#/components/schemas/FilterResult"
|
$ref: "#/components/schemas/Filter"
|
||||||
keyword_matches:
|
keyword_matches:
|
||||||
type: array
|
type: array
|
||||||
items:
|
items:
|
||||||
|
|
Loading…
Reference in New Issue