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.core.infrastructure.exposedrepository.*
|
||||||
import dev.usbharu.hideout.domain.mastodon.model.generated.Account
|
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
|
||||||
|
import dev.usbharu.hideout.domain.mastodon.model.generated.Status.Visibility.*
|
||||||
import dev.usbharu.hideout.mastodon.interfaces.api.status.StatusQuery
|
import dev.usbharu.hideout.mastodon.interfaces.api.status.StatusQuery
|
||||||
import dev.usbharu.hideout.mastodon.query.StatusQueryService
|
import dev.usbharu.hideout.mastodon.query.StatusQueryService
|
||||||
import org.jetbrains.exposed.sql.ResultRow
|
import org.jetbrains.exposed.sql.ResultRow
|
||||||
|
import org.jetbrains.exposed.sql.andWhere
|
||||||
import org.jetbrains.exposed.sql.select
|
import org.jetbrains.exposed.sql.select
|
||||||
import org.springframework.stereotype.Repository
|
import org.springframework.stereotype.Repository
|
||||||
import java.time.Instant
|
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> {
|
private fun resolveReplyAndRepost(pairs: List<Pair<Status, Long?>>): List<Status> {
|
||||||
val statuses = pairs.map { it.first }
|
val statuses = pairs.map { it.first }
|
||||||
return pairs
|
return pairs
|
||||||
|
@ -111,11 +169,11 @@ private fun toStatus(it: ResultRow) = Status(
|
||||||
),
|
),
|
||||||
content = it[Posts.text],
|
content = it[Posts.text],
|
||||||
visibility = when (it[Posts.visibility]) {
|
visibility = when (it[Posts.visibility]) {
|
||||||
0 -> Status.Visibility.public
|
0 -> public
|
||||||
1 -> Status.Visibility.unlisted
|
1 -> unlisted
|
||||||
2 -> Status.Visibility.private
|
2 -> private
|
||||||
3 -> Status.Visibility.direct
|
3 -> direct
|
||||||
else -> Status.Visibility.public
|
else -> public
|
||||||
},
|
},
|
||||||
sensitive = it[Posts.sensitive],
|
sensitive = it[Posts.sensitive],
|
||||||
spoilerText = it[Posts.overview].orEmpty(),
|
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.application.external.Transaction
|
||||||
import dev.usbharu.hideout.controller.mastodon.generated.AccountApi
|
import dev.usbharu.hideout.controller.mastodon.generated.AccountApi
|
||||||
import dev.usbharu.hideout.core.service.user.UserCreateDto
|
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.*
|
||||||
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.mastodon.service.account.AccountApiService
|
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.HttpHeaders
|
||||||
import org.springframework.http.HttpStatus
|
import org.springframework.http.HttpStatus
|
||||||
import org.springframework.http.ResponseEntity
|
import org.springframework.http.ResponseEntity
|
||||||
|
@ -58,4 +58,49 @@ class MastodonAccountApiController(
|
||||||
httpHeaders.location = URI("/users/$username")
|
httpHeaders.location = URI("/users/$username")
|
||||||
return ResponseEntity(Unit, httpHeaders, HttpStatus.FOUND)
|
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 {
|
interface StatusQueryService {
|
||||||
suspend fun findByPostIds(ids: List<Long>): List<Status>
|
suspend fun findByPostIds(ids: List<Long>): List<Status>
|
||||||
suspend fun findByPostIdsWithMediaIds(statusQueries: List<StatusQuery>): 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.UserCreateDto
|
||||||
import dev.usbharu.hideout.core.service.user.UserService
|
import dev.usbharu.hideout.core.service.user.UserService
|
||||||
import dev.usbharu.hideout.domain.mastodon.model.generated.*
|
import dev.usbharu.hideout.domain.mastodon.model.generated.*
|
||||||
|
import dev.usbharu.hideout.mastodon.query.StatusQueryService
|
||||||
|
import org.slf4j.LoggerFactory
|
||||||
import org.springframework.stereotype.Service
|
import org.springframework.stereotype.Service
|
||||||
|
|
||||||
@Service
|
@Service
|
||||||
interface AccountApiService {
|
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 verifyCredentials(userid: Long): CredentialAccount
|
||||||
suspend fun registerAccount(userCreateDto: UserCreateDto): Unit
|
suspend fun registerAccount(userCreateDto: UserCreateDto): Unit
|
||||||
suspend fun follow(userid: Long, followeeId: Long): Relationship
|
suspend fun follow(userid: Long, followeeId: Long): Relationship
|
||||||
suspend fun account(id: Long): Account
|
suspend fun account(id: Long): Account
|
||||||
|
suspend fun relationships(userid: Long, id: List<Long>, withSuspended: Boolean): List<Relationship>
|
||||||
}
|
}
|
||||||
|
|
||||||
@Service
|
@Service
|
||||||
|
@ -22,9 +39,48 @@ class AccountApiServiceImpl(
|
||||||
private val transaction: Transaction,
|
private val transaction: Transaction,
|
||||||
private val userService: UserService,
|
private val userService: UserService,
|
||||||
private val followerQueryService: FollowerQueryService,
|
private val followerQueryService: FollowerQueryService,
|
||||||
private val userRepository: UserRepository
|
private val userRepository: UserRepository,
|
||||||
|
private val statusQueryService: StatusQueryService
|
||||||
) :
|
) :
|
||||||
AccountApiService {
|
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 {
|
override suspend fun verifyCredentials(userid: Long): CredentialAccount = transaction.transaction {
|
||||||
val account = accountService.findById(userid)
|
val account = accountService.findById(userid)
|
||||||
from(account)
|
from(account)
|
||||||
|
@ -70,6 +126,42 @@ class AccountApiServiceImpl(
|
||||||
return@transaction accountService.findById(id)
|
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 {
|
private fun from(account: Account): CredentialAccount {
|
||||||
return CredentialAccount(
|
return CredentialAccount(
|
||||||
id = account.id,
|
id = account.id,
|
||||||
|
@ -107,4 +199,8 @@ class AccountApiServiceImpl(
|
||||||
role = Role(0, "Admin", "", 32)
|
role = Role(0, "Admin", "", 32)
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
private val logger = LoggerFactory.getLogger(AccountApiServiceImpl::class.java)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -206,6 +206,37 @@ paths:
|
||||||
200:
|
200:
|
||||||
description: 成功
|
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}:
|
/api/v1/accounts/{id}:
|
||||||
get:
|
get:
|
||||||
tags:
|
tags:
|
||||||
|
@ -255,6 +286,81 @@ paths:
|
||||||
application/json:
|
application/json:
|
||||||
schema:
|
schema:
|
||||||
$ref: "#/components/schemas/Relationship"
|
$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:
|
/api/v1/timelines/public:
|
||||||
get:
|
get:
|
||||||
tags:
|
tags:
|
||||||
|
|
Loading…
Reference in New Issue