feat: 投稿APIを追加

This commit is contained in:
usbharu 2023-09-20 16:25:13 +09:00
parent 19e44c4675
commit 0e00e9526d
5 changed files with 311 additions and 5 deletions

View File

@ -1,15 +1,30 @@
package dev.usbharu.hideout.controller.mastodon
import dev.usbharu.hideout.controller.mastodon.generated.DefaultApi
import dev.usbharu.hideout.domain.mastodon.model.generated.Status
import dev.usbharu.hideout.domain.mastodon.model.generated.StatusesRequest
import dev.usbharu.hideout.domain.mastodon.model.generated.V1Instance
import dev.usbharu.hideout.domain.model.UserDetailsImpl
import dev.usbharu.hideout.service.api.mastodon.InstanceApiService
import dev.usbharu.hideout.service.api.mastodon.StatusesApiService
import org.springframework.http.HttpStatus
import org.springframework.http.ResponseEntity
import org.springframework.security.core.context.SecurityContextHolder
import org.springframework.stereotype.Controller
@Controller
class MastodonApiController(private val instanceApiService: InstanceApiService) : DefaultApi {
class MastodonApiController(
private val instanceApiService: InstanceApiService,
private val statusesApiService: StatusesApiService
) : DefaultApi {
override suspend fun apiV1InstanceGet(): ResponseEntity<V1Instance> {
return ResponseEntity(instanceApiService.v1Instance(), HttpStatus.OK)
}
override suspend fun apiV1StatusesPost(statusesRequest: StatusesRequest): ResponseEntity<Status> {
val principal = SecurityContextHolder.getContext().getAuthentication().getPrincipal()
require(principal is UserDetailsImpl)
return ResponseEntity(statusesApiService.postStatus(statusesRequest, principal), HttpStatus.OK)
}
}

View File

@ -0,0 +1,21 @@
package dev.usbharu.hideout.domain.model
import org.springframework.security.core.GrantedAuthority
import org.springframework.security.core.userdetails.User
import java.io.Serial
class UserDetailsImpl(
val id: Long,
username: String?,
password: String?,
enabled: Boolean,
accountNonExpired: Boolean,
credentialsNonExpired: Boolean,
accountNonLocked: Boolean,
authorities: MutableCollection<out GrantedAuthority>?
) : User(username, password, enabled, accountNonExpired, credentialsNonExpired, accountNonLocked, authorities) {
companion object {
@Serial
private const val serialVersionUID: Long = -899168205656607781L
}
}

View File

@ -0,0 +1,105 @@
package dev.usbharu.hideout.service.api.mastodon
import dev.usbharu.hideout.domain.mastodon.model.generated.Status
import dev.usbharu.hideout.domain.mastodon.model.generated.StatusesRequest
import dev.usbharu.hideout.domain.model.UserDetailsImpl
import dev.usbharu.hideout.domain.model.hideout.dto.PostCreateDto
import dev.usbharu.hideout.domain.model.hideout.entity.Visibility
import dev.usbharu.hideout.exception.FailedToGetResourcesException
import dev.usbharu.hideout.query.PostQueryService
import dev.usbharu.hideout.query.UserQueryService
import dev.usbharu.hideout.service.mastodon.AccountService
import dev.usbharu.hideout.service.post.PostService
import org.springframework.stereotype.Service
import java.time.Instant
@Service
interface StatusesApiService {
suspend fun postStatus(statusesRequest: StatusesRequest, user: UserDetailsImpl): Status
}
@Service
class StatsesApiServiceImpl(
private val postService: PostService,
private val accountService: AccountService,
private val postQueryService: PostQueryService,
private val userQueryService: UserQueryService
) :
StatusesApiService {
override suspend fun postStatus(statusesRequest: StatusesRequest, user: UserDetailsImpl): Status {
val visibility = when (statusesRequest.visibility) {
StatusesRequest.Visibility.public -> Visibility.PUBLIC
StatusesRequest.Visibility.unlisted -> Visibility.UNLISTED
StatusesRequest.Visibility.private -> Visibility.FOLLOWERS
StatusesRequest.Visibility.direct -> Visibility.DIRECT
null -> Visibility.PUBLIC
}
val post = postService.createLocal(
PostCreateDto(
statusesRequest.status.orEmpty(),
statusesRequest.spoilerText,
visibility,
null,
statusesRequest.inReplyToId?.toLongOrNull(),
user.id
)
)
val account = accountService.findById(user.id)
val postVisibility = when (statusesRequest.visibility) {
StatusesRequest.Visibility.public -> Status.Visibility.public
StatusesRequest.Visibility.unlisted -> Status.Visibility.unlisted
StatusesRequest.Visibility.private -> Status.Visibility.private
StatusesRequest.Visibility.direct -> Status.Visibility.direct
null -> Status.Visibility.public
}
val replyUser = if (post.replyId != null) {
try {
userQueryService.findById(postQueryService.findById(post.replyId).userId).id
} catch (e: FailedToGetResourcesException) {
null
}
} else {
null
}
return Status(
id = post.id.toString(),
uri = post.apId,
createdAt = Instant.ofEpochMilli(post.createdAt).toString(),
account = account,
content = post.text,
visibility = postVisibility,
sensitive = post.sensitive,
spoilerText = post.overview.orEmpty(),
mediaAttachments = emptyList(),
mentions = emptyList(),
tags = emptyList(),
emojis = emptyList(),
reblogsCount = 0,
favouritesCount = 0,
repliesCount = 0,
url = post.url,
post.replyId?.toString(),
inReplyToAccountId = replyUser?.toString(),
reblog = null,
language = null,
text = post.text,
editedAt = null,
application = null,
poll = null,
card = null,
favourited = null,
reblogged = null,
muted = null,
bookmarked = null,
pinned = null,
filtered = null
)
}
}

View File

@ -1,8 +1,39 @@
package dev.usbharu.hideout.service.mastodon
import dev.usbharu.hideout.domain.mastodon.model.generated.Account
import dev.usbharu.hideout.query.UserQueryService
import org.springframework.stereotype.Service
@Service
interface AccountService {
suspend fun findById()
suspend fun findById(id: Long): Account
}
@Service
class AccountServiceImpl(private val userQueryService: UserQueryService) : AccountService {
override suspend fun findById(id: Long): Account {
val findById = userQueryService.findById(id)
return Account(
id = findById.id.toString(),
username = findById.name,
acct = "${findById.name}@${findById.domain}",
url = findById.url,
displayName = findById.screenName,
note = findById.description,
avatar = findById.url + "/icon.jpg",
avatarStatic = findById.url + "/icon.jpg",
header = findById.url + "/header.jpg",
headerStatic = findById.url + "/header.jpg",
locked = false,
emptyList(),
emptyList(),
false,
false,
false,
findById.createdAt.toString(),
findById.createdAt.toString(),
0,
0,
)
}
}

View File

@ -97,6 +97,27 @@ paths:
application/json:
schema:
$ref: "#/components/schemas/V1Instance"
/api/v1/statuses:
post:
security:
- OAuth2:
- "write:statuses"
requestBody:
description: 投稿する内容
required: true
content:
application/json:
schema:
$ref: "#/components/schemas/StatusesRequest"
responses:
200:
description: 成功
content:
application/json:
schema:
$ref: "#/components/schemas/Status"
components:
schemas:
Account:
@ -250,7 +271,7 @@ components:
type: array
items:
$ref: "#/components/schemas/StatusMention"
tag:
tags:
type: array
items:
$ref: "#/components/schemas/StatusTag"
@ -277,7 +298,7 @@ components:
$ref: "#/components/schemas/Status"
poll:
$ref: "#/components/schemas/Poll"
PreviewCard:
card:
$ref: "#/components/schemas/PreviewCard"
language:
type: string
@ -302,7 +323,28 @@ components:
type: array
items:
$ref: "#/components/schemas/FilterResult"
required:
- id
- uri
- created_at
- account
- content
- visibility
- sensitive
- spoiler_text
- media_attachments
- mentions
- tags
- emojis
- reblogs_count
- favourites_count
- replies_count
- url
- in_reply_to_id
- in_reply_to_account_id
- language
- text
- edited_at
MediaAttachment:
type: object
@ -844,3 +886,95 @@ components:
type: string
content:
type: string
StatusesRequest:
type: object
properties:
status:
type: string
nullable: true
media_ids:
type: array
items:
type: string
poll:
$ref: "#/components/schemas/StatusesRequestPoll"
in_reply_to_id:
type: string
sensitive:
type: boolean
spoiler_text:
type: string
visibility:
type: string
enum:
- public
- unlisted
- private
- direct
language:
type: string
scheduled_at:
type: string
StatusesRequestPoll:
type: object
properties:
options:
type: array
items:
type: string
expires_in:
type: integer
multiple:
type: boolean
hide_totals:
type: boolean
securitySchemes:
OAuth2:
type: oauth2
description: Mastodon Oauth
flows:
authorizationCode:
authorizationUrl: /oauth/authorize
tokenUrl: /oauth/token
scopes:
read:accounts: ""
read:blocks: ""
read:bookmarks: ""
read:favourites: ""
read:filters: ""
read:follows: ""
read:lists: ""
read:mutes: ""
read:notifications: ""
read:search: ""
read:statuses: ""
write:accounts: ""
write:blocks: ""
write:bookmarks: ""
write:conversations: ""
write:favourites: ""
write:filters: ""
write:follows: ""
write:lists: ""
write:media: ""
write:mutes: ""
write:notifications: ""
write:reports: ""
write:statuses: ""
admin:read:accounts: ""
admin:read:reports: ""
admin:read:domain_allows: ""
admin:read:domain_blocks: ""
admin:read:ip_blocks: ""
admin:read:email_domain_blocks: ""
admin:read:canonical_email_blocks: ""
admin:write:accounts: ""
admin:write:reports: ""
admin:write:domain_allows: ""
admin:write:domain_blocks: ""
admin:write:ip_blocks: ""
admin:write:email_domain_blocks: ""
admin:write:canonical_email_blocks: ""