mirror of https://github.com/usbharu/Hideout.git
parent
e40e600547
commit
e722ddd910
|
@ -4,9 +4,11 @@ import dev.usbharu.hideout.core.domain.model.media.toMediaAttachments
|
|||
import dev.usbharu.hideout.core.infrastructure.exposedrepository.*
|
||||
import dev.usbharu.hideout.domain.mastodon.model.generated.Account
|
||||
import dev.usbharu.hideout.domain.mastodon.model.generated.Status
|
||||
import dev.usbharu.hideout.domain.mastodon.model.generated.Status.Visibility.*
|
||||
import dev.usbharu.hideout.mastodon.interfaces.api.status.StatusQuery
|
||||
import dev.usbharu.hideout.mastodon.query.StatusQueryService
|
||||
import org.jetbrains.exposed.sql.ResultRow
|
||||
import org.jetbrains.exposed.sql.andWhere
|
||||
import org.jetbrains.exposed.sql.select
|
||||
import org.springframework.stereotype.Repository
|
||||
import java.time.Instant
|
||||
|
@ -40,6 +42,62 @@ class StatusQueryServiceImpl : StatusQueryService {
|
|||
}
|
||||
}
|
||||
|
||||
override suspend fun accountsStatus(
|
||||
accountId: Long,
|
||||
maxId: Long?,
|
||||
sinceId: Long?,
|
||||
minId: Long?,
|
||||
limit: Int,
|
||||
onlyMedia: Boolean,
|
||||
excludeReplies: Boolean,
|
||||
excludeReblogs: Boolean,
|
||||
pinned: Boolean,
|
||||
tagged: String?,
|
||||
includeFollowers: Boolean
|
||||
): List<Status> {
|
||||
val query = Posts
|
||||
.leftJoin(PostsMedia)
|
||||
.leftJoin(Users)
|
||||
.leftJoin(Media)
|
||||
.select { Posts.userId eq accountId }.limit(20)
|
||||
|
||||
if (maxId != null) {
|
||||
query.andWhere { Posts.id eq maxId }
|
||||
}
|
||||
if (sinceId != null) {
|
||||
query.andWhere { Posts.id eq sinceId }
|
||||
}
|
||||
if (minId != null) {
|
||||
query.andWhere { Posts.id eq minId }
|
||||
}
|
||||
if (onlyMedia) {
|
||||
query.andWhere { PostsMedia.mediaId.isNotNull() }
|
||||
}
|
||||
if (excludeReplies) {
|
||||
query.andWhere { Posts.replyId.isNotNull() }
|
||||
}
|
||||
if (excludeReblogs) {
|
||||
query.andWhere { Posts.repostId.isNotNull() }
|
||||
}
|
||||
if (includeFollowers) {
|
||||
query.andWhere { Posts.visibility inList listOf(public.ordinal, unlisted.ordinal, private.ordinal) }
|
||||
} else {
|
||||
query.andWhere { Posts.visibility inList listOf(public.ordinal, unlisted.ordinal) }
|
||||
}
|
||||
|
||||
val pairs = query.groupBy { it[Posts.id] }
|
||||
.map { it.value }
|
||||
.map {
|
||||
toStatus(it.first()).copy(
|
||||
mediaAttachments = it.mapNotNull { resultRow ->
|
||||
resultRow.toMediaOrNull()?.toMediaAttachments()
|
||||
}
|
||||
) to it.first()[Posts.repostId]
|
||||
}
|
||||
|
||||
return resolveReplyAndRepost(pairs)
|
||||
}
|
||||
|
||||
private fun resolveReplyAndRepost(pairs: List<Pair<Status, Long?>>): List<Status> {
|
||||
val statuses = pairs.map { it.first }
|
||||
return pairs
|
||||
|
@ -111,11 +169,11 @@ private fun toStatus(it: ResultRow) = Status(
|
|||
),
|
||||
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
|
||||
0 -> public
|
||||
1 -> unlisted
|
||||
2 -> private
|
||||
3 -> direct
|
||||
else -> public
|
||||
},
|
||||
sensitive = it[Posts.sensitive],
|
||||
spoilerText = it[Posts.overview].orEmpty(),
|
||||
|
|
|
@ -3,11 +3,11 @@ package dev.usbharu.hideout.mastodon.interfaces.api.account
|
|||
import dev.usbharu.hideout.application.external.Transaction
|
||||
import dev.usbharu.hideout.controller.mastodon.generated.AccountApi
|
||||
import dev.usbharu.hideout.core.service.user.UserCreateDto
|
||||
import dev.usbharu.hideout.domain.mastodon.model.generated.Account
|
||||
import dev.usbharu.hideout.domain.mastodon.model.generated.CredentialAccount
|
||||
import dev.usbharu.hideout.domain.mastodon.model.generated.FollowRequestBody
|
||||
import dev.usbharu.hideout.domain.mastodon.model.generated.Relationship
|
||||
import dev.usbharu.hideout.domain.mastodon.model.generated.*
|
||||
import dev.usbharu.hideout.mastodon.service.account.AccountApiService
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.coroutines.flow.asFlow
|
||||
import kotlinx.coroutines.runBlocking
|
||||
import org.springframework.http.HttpHeaders
|
||||
import org.springframework.http.HttpStatus
|
||||
import org.springframework.http.ResponseEntity
|
||||
|
@ -58,4 +58,49 @@ class MastodonAccountApiController(
|
|||
httpHeaders.location = URI("/users/$username")
|
||||
return ResponseEntity(Unit, httpHeaders, HttpStatus.FOUND)
|
||||
}
|
||||
|
||||
override fun apiV1AccountsIdStatusesGet(
|
||||
id: String,
|
||||
maxId: String?,
|
||||
sinceId: String?,
|
||||
minId: String?,
|
||||
limit: Int,
|
||||
onlyMedia: Boolean,
|
||||
excludeReplies: Boolean,
|
||||
excludeReblogs: Boolean,
|
||||
pinned: Boolean,
|
||||
tagged: String?
|
||||
): ResponseEntity<Flow<Status>> = runBlocking {
|
||||
val principal = SecurityContextHolder.getContext().getAuthentication().principal as Jwt
|
||||
|
||||
val userid = principal.getClaim<String>("uid").toLong()
|
||||
val statusFlow = accountApiService.accountsStatuses(
|
||||
id.toLong(),
|
||||
maxId?.toLongOrNull(),
|
||||
sinceId?.toLongOrNull(),
|
||||
minId?.toLongOrNull(),
|
||||
limit,
|
||||
onlyMedia,
|
||||
excludeReplies,
|
||||
excludeReblogs,
|
||||
pinned,
|
||||
tagged,
|
||||
userid
|
||||
).asFlow()
|
||||
ResponseEntity.ok(statusFlow)
|
||||
}
|
||||
|
||||
override fun apiV1AccountsRelationshipsGet(
|
||||
id: List<String>?,
|
||||
withSuspended: Boolean
|
||||
): ResponseEntity<Flow<Relationship>> = runBlocking {
|
||||
val principal = SecurityContextHolder.getContext().getAuthentication().principal as Jwt
|
||||
|
||||
val userid = principal.getClaim<String>("uid").toLong()
|
||||
|
||||
ResponseEntity.ok(
|
||||
accountApiService.relationships(userid, id.orEmpty().mapNotNull { it.toLongOrNull() }, withSuspended)
|
||||
.asFlow()
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -6,4 +6,34 @@ import dev.usbharu.hideout.mastodon.interfaces.api.status.StatusQuery
|
|||
interface StatusQueryService {
|
||||
suspend fun findByPostIds(ids: List<Long>): List<Status>
|
||||
suspend fun findByPostIdsWithMediaIds(statusQueries: List<StatusQuery>): List<Status>
|
||||
|
||||
/**
|
||||
* アカウントの投稿一覧を取得します
|
||||
*
|
||||
* @param accountId 対象アカウントのid
|
||||
* @param maxId 投稿の最大id
|
||||
* @param sinceId 投稿の最小id
|
||||
* @param minId 不明
|
||||
* @param limit 投稿の最大件数
|
||||
* @param onlyMedia メディア付き投稿のみ
|
||||
* @param excludeReplies 返信を除外
|
||||
* @param excludeReblogs リブログを除外
|
||||
* @param pinned ピン止め投稿のみ
|
||||
* @param tagged タグ付き?
|
||||
* @param includeFollowers フォロワー限定投稿を含める
|
||||
*/
|
||||
@Suppress("LongParameterList")
|
||||
suspend fun accountsStatus(
|
||||
accountId: Long,
|
||||
maxId: Long? = null,
|
||||
sinceId: Long? = null,
|
||||
minId: Long? = null,
|
||||
limit: Int,
|
||||
onlyMedia: Boolean = false,
|
||||
excludeReplies: Boolean = false,
|
||||
excludeReblogs: Boolean = false,
|
||||
pinned: Boolean = false,
|
||||
tagged: String? = null,
|
||||
includeFollowers: Boolean = false
|
||||
): List<Status>
|
||||
}
|
||||
|
|
|
@ -6,14 +6,31 @@ import dev.usbharu.hideout.core.query.FollowerQueryService
|
|||
import dev.usbharu.hideout.core.service.user.UserCreateDto
|
||||
import dev.usbharu.hideout.core.service.user.UserService
|
||||
import dev.usbharu.hideout.domain.mastodon.model.generated.*
|
||||
import dev.usbharu.hideout.mastodon.query.StatusQueryService
|
||||
import org.slf4j.LoggerFactory
|
||||
import org.springframework.stereotype.Service
|
||||
|
||||
@Service
|
||||
interface AccountApiService {
|
||||
suspend fun accountsStatuses(
|
||||
userid: Long,
|
||||
maxId: Long?,
|
||||
sinceId: Long?,
|
||||
minId: Long?,
|
||||
limit: Int,
|
||||
onlyMedia: Boolean,
|
||||
excludeReplies: Boolean,
|
||||
excludeReblogs: Boolean,
|
||||
pinned: Boolean,
|
||||
tagged: String?,
|
||||
loginUser: Long?
|
||||
): List<Status>
|
||||
|
||||
suspend fun verifyCredentials(userid: Long): CredentialAccount
|
||||
suspend fun registerAccount(userCreateDto: UserCreateDto): Unit
|
||||
suspend fun follow(userid: Long, followeeId: Long): Relationship
|
||||
suspend fun account(id: Long): Account
|
||||
suspend fun relationships(userid: Long, id: List<Long>, withSuspended: Boolean): List<Relationship>
|
||||
}
|
||||
|
||||
@Service
|
||||
|
@ -22,9 +39,48 @@ class AccountApiServiceImpl(
|
|||
private val transaction: Transaction,
|
||||
private val userService: UserService,
|
||||
private val followerQueryService: FollowerQueryService,
|
||||
private val userRepository: UserRepository
|
||||
private val userRepository: UserRepository,
|
||||
private val statusQueryService: StatusQueryService
|
||||
) :
|
||||
AccountApiService {
|
||||
override suspend fun accountsStatuses(
|
||||
userid: Long,
|
||||
maxId: Long?,
|
||||
sinceId: Long?,
|
||||
minId: Long?,
|
||||
limit: Int,
|
||||
onlyMedia: Boolean,
|
||||
excludeReplies: Boolean,
|
||||
excludeReblogs: Boolean,
|
||||
pinned: Boolean,
|
||||
tagged: String?,
|
||||
loginUser: Long?
|
||||
): List<Status> {
|
||||
val canViewFollowers = if (loginUser == null) {
|
||||
false
|
||||
} else {
|
||||
transaction.transaction {
|
||||
followerQueryService.alreadyFollow(userid, loginUser)
|
||||
}
|
||||
}
|
||||
|
||||
return transaction.transaction {
|
||||
statusQueryService.accountsStatus(
|
||||
userid,
|
||||
maxId,
|
||||
sinceId,
|
||||
minId,
|
||||
limit,
|
||||
onlyMedia,
|
||||
excludeReplies,
|
||||
excludeReblogs,
|
||||
pinned,
|
||||
tagged,
|
||||
canViewFollowers
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
override suspend fun verifyCredentials(userid: Long): CredentialAccount = transaction.transaction {
|
||||
val account = accountService.findById(userid)
|
||||
from(account)
|
||||
|
@ -70,6 +126,42 @@ class AccountApiServiceImpl(
|
|||
return@transaction accountService.findById(id)
|
||||
}
|
||||
|
||||
override suspend fun relationships(userid: Long, id: List<Long>, withSuspended: Boolean): List<Relationship> {
|
||||
if (id.isEmpty()) {
|
||||
return emptyList()
|
||||
}
|
||||
|
||||
|
||||
logger.warn("id is too long! ({}) truncate to 20", id.size)
|
||||
|
||||
val subList = id.subList(0, 20)
|
||||
|
||||
return subList.map {
|
||||
|
||||
val alreadyFollow = followerQueryService.alreadyFollow(userid, it)
|
||||
|
||||
val followed = followerQueryService.alreadyFollow(it, userid)
|
||||
|
||||
val requested = userRepository.findFollowRequestsById(it, userid)
|
||||
|
||||
Relationship(
|
||||
id = it.toString(),
|
||||
following = alreadyFollow,
|
||||
showingReblogs = true,
|
||||
notifying = false,
|
||||
followedBy = followed,
|
||||
blocking = false,
|
||||
blockedBy = false,
|
||||
muting = false,
|
||||
mutingNotifications = false,
|
||||
requested = requested,
|
||||
domainBlocking = false,
|
||||
endorsed = false,
|
||||
note = ""
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private fun from(account: Account): CredentialAccount {
|
||||
return CredentialAccount(
|
||||
id = account.id,
|
||||
|
@ -107,4 +199,8 @@ class AccountApiServiceImpl(
|
|||
role = Role(0, "Admin", "", 32)
|
||||
)
|
||||
}
|
||||
|
||||
companion object {
|
||||
private val logger = LoggerFactory.getLogger(AccountApiServiceImpl::class.java)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -206,6 +206,37 @@ paths:
|
|||
200:
|
||||
description: 成功
|
||||
|
||||
/api/v1/accounts/relationships:
|
||||
get:
|
||||
tags:
|
||||
- account
|
||||
security:
|
||||
- OAuth2:
|
||||
- "read:follows"
|
||||
parameters:
|
||||
- in: query
|
||||
name: id
|
||||
required: false
|
||||
schema:
|
||||
type: array
|
||||
items:
|
||||
type: string
|
||||
- in: query
|
||||
name: with_suspended
|
||||
required: false
|
||||
schema:
|
||||
type: boolean
|
||||
default: false
|
||||
responses:
|
||||
200:
|
||||
description: 成功
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
type: array
|
||||
items:
|
||||
$ref: "#/components/schemas/Relationship"
|
||||
|
||||
/api/v1/accounts/{id}:
|
||||
get:
|
||||
tags:
|
||||
|
@ -255,6 +286,81 @@ paths:
|
|||
application/json:
|
||||
schema:
|
||||
$ref: "#/components/schemas/Relationship"
|
||||
|
||||
/api/v1/accounts/{id}/statuses:
|
||||
get:
|
||||
tags:
|
||||
- account
|
||||
security:
|
||||
- OAuth2:
|
||||
- "read:statuses"
|
||||
parameters:
|
||||
- in: path
|
||||
name: id
|
||||
required: true
|
||||
schema:
|
||||
type: string
|
||||
- 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
|
||||
default: 20
|
||||
- in: query
|
||||
name: only_media
|
||||
required: false
|
||||
schema:
|
||||
type: boolean
|
||||
default: false
|
||||
- in: query
|
||||
name: exclude_replies
|
||||
required: false
|
||||
schema:
|
||||
type: boolean
|
||||
default: false
|
||||
- in: query
|
||||
name: exclude_reblogs
|
||||
required: false
|
||||
schema:
|
||||
type: boolean
|
||||
default: false
|
||||
- in: query
|
||||
name: pinned
|
||||
required: false
|
||||
schema:
|
||||
type: boolean
|
||||
default: false
|
||||
- in: query
|
||||
required: false
|
||||
name: tagged
|
||||
schema:
|
||||
type: string
|
||||
|
||||
responses:
|
||||
200:
|
||||
description: 成功
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
type: array
|
||||
items:
|
||||
$ref: "#/components/schemas/Status"
|
||||
|
||||
/api/v1/timelines/public:
|
||||
get:
|
||||
tags:
|
||||
|
|
Loading…
Reference in New Issue