mirror of https://github.com/usbharu/Hideout.git
Merge pull request #287 from usbharu/feature/error-response
Mastodon互換APIに異常系レスポンスを追加
This commit is contained in:
commit
a242748908
|
@ -141,10 +141,11 @@ class AccountApiTest {
|
||||||
mockMvc
|
mockMvc
|
||||||
.post("/api/v1/accounts") {
|
.post("/api/v1/accounts") {
|
||||||
contentType = MediaType.APPLICATION_FORM_URLENCODED
|
contentType = MediaType.APPLICATION_FORM_URLENCODED
|
||||||
param("username", "api-test-user-3")
|
param("password", "api-test-user-3")
|
||||||
with(csrf())
|
with(csrf())
|
||||||
}
|
}
|
||||||
.andExpect { status { isBadRequest() } }
|
.andDo { print() }
|
||||||
|
.andExpect { status { isUnprocessableEntity() } }
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
|
@ -156,7 +157,7 @@ class AccountApiTest {
|
||||||
param("username", "api-test-user-4")
|
param("username", "api-test-user-4")
|
||||||
with(csrf())
|
with(csrf())
|
||||||
}
|
}
|
||||||
.andExpect { status { isBadRequest() } }
|
.andExpect { status { isUnprocessableEntity() } }
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
|
|
|
@ -239,6 +239,7 @@ class SecurityConfig {
|
||||||
|
|
||||||
authorize(POST, "/api/v1/media", rf.hasScope("write:media"))
|
authorize(POST, "/api/v1/media", rf.hasScope("write:media"))
|
||||||
authorize(POST, "/api/v1/statuses", rf.hasScope("write:statuses"))
|
authorize(POST, "/api/v1/statuses", rf.hasScope("write:statuses"))
|
||||||
|
authorize(GET, "/api/v1/statuses/*", permitAll)
|
||||||
|
|
||||||
authorize(GET, "/api/v1/timelines/public", permitAll)
|
authorize(GET, "/api/v1/timelines/public", permitAll)
|
||||||
authorize(GET, "/api/v1/timelines/home", rf.hasScope("read:statuses"))
|
authorize(GET, "/api/v1/timelines/home", rf.hasScope("read:statuses"))
|
||||||
|
|
|
@ -0,0 +1,38 @@
|
||||||
|
/*
|
||||||
|
* Copyright (C) 2024 usbharu
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package dev.usbharu.hideout.mastodon.domain.exception
|
||||||
|
|
||||||
|
import dev.usbharu.hideout.mastodon.domain.model.MastodonApiErrorResponse
|
||||||
|
|
||||||
|
open class ClientException : MastodonApiException {
|
||||||
|
constructor(response: MastodonApiErrorResponse<*>) : super(response)
|
||||||
|
constructor(message: String?, response: MastodonApiErrorResponse<*>) : super(message, response)
|
||||||
|
constructor(message: String?, cause: Throwable?, response: MastodonApiErrorResponse<*>) : super(
|
||||||
|
message,
|
||||||
|
cause,
|
||||||
|
response
|
||||||
|
)
|
||||||
|
|
||||||
|
constructor(cause: Throwable?, response: MastodonApiErrorResponse<*>) : super(cause, response)
|
||||||
|
constructor(
|
||||||
|
message: String?,
|
||||||
|
cause: Throwable?,
|
||||||
|
enableSuppression: Boolean,
|
||||||
|
writableStackTrace: Boolean,
|
||||||
|
response: MastodonApiErrorResponse<*>,
|
||||||
|
) : super(message, cause, enableSuppression, writableStackTrace, response)
|
||||||
|
}
|
|
@ -0,0 +1,55 @@
|
||||||
|
/*
|
||||||
|
* Copyright (C) 2024 usbharu
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package dev.usbharu.hideout.mastodon.domain.exception
|
||||||
|
|
||||||
|
import dev.usbharu.hideout.mastodon.domain.model.MastodonApiErrorResponse
|
||||||
|
|
||||||
|
abstract class MastodonApiException : RuntimeException {
|
||||||
|
|
||||||
|
val response: MastodonApiErrorResponse<*>
|
||||||
|
|
||||||
|
constructor(response: MastodonApiErrorResponse<*>) : super() {
|
||||||
|
this.response = response
|
||||||
|
}
|
||||||
|
|
||||||
|
constructor(message: String?, response: MastodonApiErrorResponse<*>) : super(message) {
|
||||||
|
this.response = response
|
||||||
|
}
|
||||||
|
|
||||||
|
constructor(message: String?, cause: Throwable?, response: MastodonApiErrorResponse<*>) : super(message, cause) {
|
||||||
|
this.response = response
|
||||||
|
}
|
||||||
|
|
||||||
|
constructor(cause: Throwable?, response: MastodonApiErrorResponse<*>) : super(cause) {
|
||||||
|
this.response = response
|
||||||
|
}
|
||||||
|
|
||||||
|
constructor(
|
||||||
|
message: String?,
|
||||||
|
cause: Throwable?,
|
||||||
|
enableSuppression: Boolean,
|
||||||
|
writableStackTrace: Boolean,
|
||||||
|
response: MastodonApiErrorResponse<*>,
|
||||||
|
) : super(
|
||||||
|
message,
|
||||||
|
cause,
|
||||||
|
enableSuppression,
|
||||||
|
writableStackTrace
|
||||||
|
) {
|
||||||
|
this.response = response
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,21 @@
|
||||||
|
/*
|
||||||
|
* Copyright (C) 2024 usbharu
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package dev.usbharu.hideout.mastodon.domain.exception
|
||||||
|
|
||||||
|
import dev.usbharu.hideout.mastodon.domain.model.MastodonApiErrorResponse
|
||||||
|
|
||||||
|
open class ServerException(response: MastodonApiErrorResponse<*>) : MastodonApiException(response)
|
|
@ -0,0 +1,56 @@
|
||||||
|
/*
|
||||||
|
* Copyright (C) 2024 usbharu
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package dev.usbharu.hideout.mastodon.domain.exception
|
||||||
|
|
||||||
|
import dev.usbharu.hideout.domain.mastodon.model.generated.NotFoundResponse
|
||||||
|
import dev.usbharu.hideout.mastodon.domain.model.MastodonApiErrorResponse
|
||||||
|
|
||||||
|
class StatusNotFoundException : ClientException {
|
||||||
|
|
||||||
|
constructor(response: MastodonApiErrorResponse<NotFoundResponse>) : super(response)
|
||||||
|
|
||||||
|
constructor(message: String?, response: MastodonApiErrorResponse<NotFoundResponse>) : super(message, response)
|
||||||
|
constructor(message: String?, cause: Throwable?, response: MastodonApiErrorResponse<NotFoundResponse>) : super(
|
||||||
|
message,
|
||||||
|
cause,
|
||||||
|
response
|
||||||
|
)
|
||||||
|
constructor(cause: Throwable?, response: MastodonApiErrorResponse<NotFoundResponse>) : super(cause, response)
|
||||||
|
|
||||||
|
constructor(
|
||||||
|
message: String?,
|
||||||
|
cause: Throwable?,
|
||||||
|
enableSuppression: Boolean,
|
||||||
|
writableStackTrace: Boolean,
|
||||||
|
response: MastodonApiErrorResponse<NotFoundResponse>,
|
||||||
|
) : super(message, cause, enableSuppression, writableStackTrace, response)
|
||||||
|
|
||||||
|
fun getTypedResponse(): MastodonApiErrorResponse<NotFoundResponse> =
|
||||||
|
response as MastodonApiErrorResponse<NotFoundResponse>
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
fun ofId(id: Long): StatusNotFoundException = StatusNotFoundException(
|
||||||
|
"id: $id was not found.",
|
||||||
|
MastodonApiErrorResponse(
|
||||||
|
NotFoundResponse(
|
||||||
|
"Record not found"
|
||||||
|
),
|
||||||
|
404
|
||||||
|
),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,19 @@
|
||||||
|
/*
|
||||||
|
* Copyright (C) 2024 usbharu
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package dev.usbharu.hideout.mastodon.domain.model
|
||||||
|
|
||||||
|
data class MastodonApiErrorResponse<R>(val response: R, val statusCode: Int)
|
|
@ -122,12 +122,13 @@ class StatusQueryServiceImpl : StatusQueryService {
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
override suspend fun findByPostId(id: Long): Status {
|
override suspend fun findByPostId(id: Long): Status? {
|
||||||
val map = Posts
|
val map = Posts
|
||||||
.leftJoin(PostsMedia)
|
.leftJoin(PostsMedia)
|
||||||
.leftJoin(Actors)
|
.leftJoin(Actors)
|
||||||
.leftJoin(Media)
|
.leftJoin(Media)
|
||||||
.selectAll().where { Posts.id eq id }
|
.selectAll()
|
||||||
|
.where { Posts.id eq id }
|
||||||
.groupBy { it[Posts.id] }
|
.groupBy { it[Posts.id] }
|
||||||
.map { it.value }
|
.map { it.value }
|
||||||
.map {
|
.map {
|
||||||
|
@ -138,7 +139,7 @@ class StatusQueryServiceImpl : StatusQueryService {
|
||||||
emojis = it.mapNotNull { resultRow -> resultRow.toCustomEmojiOrNull()?.toMastodonEmoji() }
|
emojis = it.mapNotNull { resultRow -> resultRow.toCustomEmojiOrNull()?.toMastodonEmoji() }
|
||||||
) to it.first()[Posts.repostId]
|
) to it.first()[Posts.repostId]
|
||||||
}
|
}
|
||||||
return resolveReplyAndRepost(map).single()
|
return resolveReplyAndRepost(map).singleOrNull()
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun resolveReplyAndRepost(pairs: List<Pair<Status, Long?>>): List<Status> {
|
private fun resolveReplyAndRepost(pairs: List<Pair<Status, Long?>>): List<Status> {
|
||||||
|
|
|
@ -0,0 +1,104 @@
|
||||||
|
/*
|
||||||
|
* Copyright (C) 2024 usbharu
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package dev.usbharu.hideout.mastodon.infrastructure.springweb
|
||||||
|
|
||||||
|
import dev.usbharu.hideout.domain.mastodon.model.generated.NotFoundResponse
|
||||||
|
import dev.usbharu.hideout.domain.mastodon.model.generated.UnprocessableEntityResponse
|
||||||
|
import dev.usbharu.hideout.domain.mastodon.model.generated.UnprocessableEntityResponseDetails
|
||||||
|
import dev.usbharu.hideout.mastodon.domain.exception.StatusNotFoundException
|
||||||
|
import dev.usbharu.hideout.mastodon.interfaces.api.account.MastodonAccountApiController
|
||||||
|
import dev.usbharu.hideout.mastodon.interfaces.api.apps.MastodonAppsApiController
|
||||||
|
import dev.usbharu.hideout.mastodon.interfaces.api.filter.MastodonFilterApiController
|
||||||
|
import dev.usbharu.hideout.mastodon.interfaces.api.instance.MastodonInstanceApiController
|
||||||
|
import dev.usbharu.hideout.mastodon.interfaces.api.media.MastodonMediaApiController
|
||||||
|
import dev.usbharu.hideout.mastodon.interfaces.api.notification.MastodonNotificationApiController
|
||||||
|
import dev.usbharu.hideout.mastodon.interfaces.api.status.MastodonStatusesApiContoller
|
||||||
|
import dev.usbharu.hideout.mastodon.interfaces.api.timeline.MastodonTimelineApiController
|
||||||
|
import org.slf4j.LoggerFactory
|
||||||
|
import org.springframework.http.HttpStatus
|
||||||
|
import org.springframework.http.ResponseEntity
|
||||||
|
import org.springframework.validation.BindException
|
||||||
|
import org.springframework.validation.FieldError
|
||||||
|
import org.springframework.web.bind.annotation.ControllerAdvice
|
||||||
|
import org.springframework.web.bind.annotation.ExceptionHandler
|
||||||
|
|
||||||
|
@ControllerAdvice(
|
||||||
|
assignableTypes = [
|
||||||
|
MastodonAccountApiController::class,
|
||||||
|
MastodonAppsApiController::class,
|
||||||
|
MastodonFilterApiController::class,
|
||||||
|
MastodonInstanceApiController::class,
|
||||||
|
MastodonMediaApiController::class,
|
||||||
|
MastodonNotificationApiController::class,
|
||||||
|
MastodonStatusesApiContoller::class,
|
||||||
|
MastodonTimelineApiController::class
|
||||||
|
]
|
||||||
|
)
|
||||||
|
class MastodonApiControllerAdvice {
|
||||||
|
|
||||||
|
@ExceptionHandler(BindException::class)
|
||||||
|
fun handleException(ex: BindException): ResponseEntity<UnprocessableEntityResponse> {
|
||||||
|
logger.debug("Failed bind entity.", ex)
|
||||||
|
|
||||||
|
val details = mutableMapOf<String, MutableList<UnprocessableEntityResponseDetails>>()
|
||||||
|
|
||||||
|
ex.allErrors.forEach {
|
||||||
|
val defaultMessage = it.defaultMessage
|
||||||
|
when {
|
||||||
|
it is FieldError -> {
|
||||||
|
val code = when (it.code) {
|
||||||
|
"Email" -> "ERR_INVALID"
|
||||||
|
"Pattern" -> "ERR_INVALID"
|
||||||
|
else -> "ERR_INVALID"
|
||||||
|
}
|
||||||
|
details.getOrPut(it.field) {
|
||||||
|
mutableListOf()
|
||||||
|
}.add(UnprocessableEntityResponseDetails(code, defaultMessage.orEmpty()))
|
||||||
|
}
|
||||||
|
|
||||||
|
defaultMessage?.startsWith("Parameter specified as non-null is null:") == true -> {
|
||||||
|
val parameter = defaultMessage.substringAfterLast("parameter ")
|
||||||
|
|
||||||
|
details.getOrPut(parameter) {
|
||||||
|
mutableListOf()
|
||||||
|
}.add(UnprocessableEntityResponseDetails("ERR_BLANK", "can't be blank"))
|
||||||
|
}
|
||||||
|
|
||||||
|
else -> {
|
||||||
|
logger.warn("Unknown validation error", ex)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
val message = details.map {
|
||||||
|
it.key + " " + it.value.joinToString { responseDetails -> responseDetails.description }
|
||||||
|
}.joinToString()
|
||||||
|
|
||||||
|
return ResponseEntity.unprocessableEntity()
|
||||||
|
.body(UnprocessableEntityResponse(message, details))
|
||||||
|
}
|
||||||
|
|
||||||
|
@ExceptionHandler(StatusNotFoundException::class)
|
||||||
|
fun handleException(ex: StatusNotFoundException): ResponseEntity<NotFoundResponse> {
|
||||||
|
logger.warn("Status not found.", ex)
|
||||||
|
return ResponseEntity.status(HttpStatus.NOT_FOUND).body(ex.getTypedResponse().response)
|
||||||
|
}
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
private val logger = LoggerFactory.getLogger(MastodonApiControllerAdvice::class.java)
|
||||||
|
}
|
||||||
|
}
|
|
@ -57,5 +57,9 @@ class MastodonStatusesApiContoller(
|
||||||
return ResponseEntity.ok(statusesApiService.emojiReactions(id.toLong(), uid, emoji))
|
return ResponseEntity.ok(statusesApiService.emojiReactions(id.toLong(), uid, emoji))
|
||||||
}
|
}
|
||||||
|
|
||||||
override suspend fun apiV1StatusesIdGet(id: String): ResponseEntity<Status> = super.apiV1StatusesIdGet(id)
|
override suspend fun apiV1StatusesIdGet(id: String): ResponseEntity<Status> {
|
||||||
|
val uid = loginUserContextHolder.getLoginUserIdOrNull()
|
||||||
|
|
||||||
|
return ResponseEntity.ok(statusesApiService.findById(id.toLong(), uid))
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -37,5 +37,5 @@ interface StatusQueryService {
|
||||||
page: Page
|
page: Page
|
||||||
): PaginationList<Status, Long>
|
): PaginationList<Status, Long>
|
||||||
|
|
||||||
suspend fun findByPostId(id: Long): Status
|
suspend fun findByPostId(id: Long): Status?
|
||||||
}
|
}
|
||||||
|
|
|
@ -29,6 +29,7 @@ import dev.usbharu.hideout.core.service.post.PostService
|
||||||
import dev.usbharu.hideout.core.service.reaction.ReactionService
|
import dev.usbharu.hideout.core.service.reaction.ReactionService
|
||||||
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.domain.mastodon.model.generated.Status.Visibility.*
|
||||||
|
import dev.usbharu.hideout.mastodon.domain.exception.StatusNotFoundException
|
||||||
import dev.usbharu.hideout.mastodon.interfaces.api.status.StatusesRequest
|
import dev.usbharu.hideout.mastodon.interfaces.api.status.StatusesRequest
|
||||||
import dev.usbharu.hideout.mastodon.interfaces.api.status.toPostVisibility
|
import dev.usbharu.hideout.mastodon.interfaces.api.status.toPostVisibility
|
||||||
import dev.usbharu.hideout.mastodon.interfaces.api.status.toStatusVisibility
|
import dev.usbharu.hideout.mastodon.interfaces.api.status.toStatusVisibility
|
||||||
|
@ -43,25 +44,25 @@ import java.time.Instant
|
||||||
interface StatusesApiService {
|
interface StatusesApiService {
|
||||||
suspend fun postStatus(
|
suspend fun postStatus(
|
||||||
statusesRequest: StatusesRequest,
|
statusesRequest: StatusesRequest,
|
||||||
userId: Long
|
userId: Long,
|
||||||
): Status
|
): Status
|
||||||
|
|
||||||
suspend fun findById(
|
suspend fun findById(
|
||||||
id: Long,
|
id: Long,
|
||||||
userId: Long?
|
userId: Long?,
|
||||||
): Status?
|
): Status
|
||||||
|
|
||||||
suspend fun emojiReactions(
|
suspend fun emojiReactions(
|
||||||
postId: Long,
|
postId: Long,
|
||||||
userId: Long,
|
userId: Long,
|
||||||
emojiName: String
|
emojiName: String,
|
||||||
): Status?
|
): Status
|
||||||
|
|
||||||
suspend fun removeEmojiReactions(
|
suspend fun removeEmojiReactions(
|
||||||
postId: Long,
|
postId: Long,
|
||||||
userId: Long,
|
userId: Long,
|
||||||
emojiName: String
|
emojiName: String,
|
||||||
): Status?
|
): Status
|
||||||
}
|
}
|
||||||
|
|
||||||
@Service
|
@Service
|
||||||
|
@ -76,12 +77,12 @@ class StatsesApiServiceImpl(
|
||||||
private val statusQueryService: StatusQueryService,
|
private val statusQueryService: StatusQueryService,
|
||||||
private val relationshipRepository: RelationshipRepository,
|
private val relationshipRepository: RelationshipRepository,
|
||||||
private val reactionService: ReactionService,
|
private val reactionService: ReactionService,
|
||||||
private val emojiService: EmojiService
|
private val emojiService: EmojiService,
|
||||||
) :
|
) :
|
||||||
StatusesApiService {
|
StatusesApiService {
|
||||||
override suspend fun postStatus(
|
override suspend fun postStatus(
|
||||||
statusesRequest: StatusesRequest,
|
statusesRequest: StatusesRequest,
|
||||||
userId: Long
|
userId: Long,
|
||||||
): Status = transaction.transaction {
|
): Status = transaction.transaction {
|
||||||
logger.debug("START create post by mastodon api. {}", statusesRequest)
|
logger.debug("START create post by mastodon api. {}", statusesRequest)
|
||||||
|
|
||||||
|
@ -140,39 +141,49 @@ class StatsesApiServiceImpl(
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
override suspend fun findById(id: Long, userId: Long?): Status? {
|
override suspend fun findById(id: Long, userId: Long?): Status = transaction.transaction {
|
||||||
val status = statusQueryService.findByPostId(id)
|
val status = statusQueryService.findByPostId(id) ?: statusNotFound(id)
|
||||||
|
|
||||||
return status(status, userId)
|
return@transaction status(status, userId)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun accessDenied(id: String): Nothing {
|
||||||
|
logger.debug("Access Denied $id")
|
||||||
|
throw StatusNotFoundException.ofId(id.toLong())
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun statusNotFound(id: Long): Nothing {
|
||||||
|
logger.debug("Status Not Found $id")
|
||||||
|
throw StatusNotFoundException.ofId(id)
|
||||||
}
|
}
|
||||||
|
|
||||||
private suspend fun status(
|
private suspend fun status(
|
||||||
status: Status,
|
status: Status,
|
||||||
userId: Long?
|
userId: Long?,
|
||||||
): Status? {
|
): Status {
|
||||||
return when (status.visibility) {
|
return when (status.visibility) {
|
||||||
public -> status
|
public -> status
|
||||||
unlisted -> status
|
unlisted -> status
|
||||||
private -> {
|
private -> {
|
||||||
if (userId == null) {
|
if (userId == null) {
|
||||||
return null
|
accessDenied(status.id)
|
||||||
}
|
}
|
||||||
|
|
||||||
val relationship =
|
val relationship =
|
||||||
relationshipRepository.findByUserIdAndTargetUserId(userId, status.account.id.toLong())
|
relationshipRepository.findByUserIdAndTargetUserId(userId, status.account.id.toLong())
|
||||||
?: return null
|
?: accessDenied(status.id)
|
||||||
if (relationship.following) {
|
if (relationship.following) {
|
||||||
return status
|
return status
|
||||||
}
|
}
|
||||||
return null
|
accessDenied(status.id)
|
||||||
}
|
}
|
||||||
|
|
||||||
direct -> null
|
direct -> accessDenied(status.id)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
override suspend fun emojiReactions(postId: Long, userId: Long, emojiName: String): Status? {
|
override suspend fun emojiReactions(postId: Long, userId: Long, emojiName: String): Status {
|
||||||
status(statusQueryService.findByPostId(postId), userId) ?: return null
|
status(statusQueryService.findByPostId(postId) ?: statusNotFound(postId), userId)
|
||||||
|
|
||||||
val emoji = try {
|
val emoji = try {
|
||||||
if (EmojiUtil.isEmoji(emojiName)) {
|
if (EmojiUtil.isEmoji(emojiName)) {
|
||||||
|
@ -186,13 +197,13 @@ class StatsesApiServiceImpl(
|
||||||
UnicodeEmoji("❤")
|
UnicodeEmoji("❤")
|
||||||
}
|
}
|
||||||
reactionService.sendReaction(emoji, userId, postId)
|
reactionService.sendReaction(emoji, userId, postId)
|
||||||
return statusQueryService.findByPostId(postId)
|
return statusQueryService.findByPostId(postId) ?: statusNotFound(postId)
|
||||||
}
|
}
|
||||||
|
|
||||||
override suspend fun removeEmojiReactions(postId: Long, userId: Long, emojiName: String): Status? {
|
override suspend fun removeEmojiReactions(postId: Long, userId: Long, emojiName: String): Status {
|
||||||
reactionService.removeReaction(userId, postId)
|
reactionService.removeReaction(userId, postId)
|
||||||
|
|
||||||
return status(statusQueryService.findByPostId(postId), userId)
|
return status(statusQueryService.findByPostId(postId) ?: statusNotFound(postId), userId)
|
||||||
}
|
}
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
|
|
|
@ -250,6 +250,8 @@ paths:
|
||||||
application/json:
|
application/json:
|
||||||
schema:
|
schema:
|
||||||
$ref: "#/components/schemas/Application"
|
$ref: "#/components/schemas/Application"
|
||||||
|
422:
|
||||||
|
$ref: "#/components/responses/unprocessableEntity"
|
||||||
|
|
||||||
/api/v1/accounts/verify_credentials:
|
/api/v1/accounts/verify_credentials:
|
||||||
get:
|
get:
|
||||||
|
@ -265,6 +267,12 @@ paths:
|
||||||
application/json:
|
application/json:
|
||||||
schema:
|
schema:
|
||||||
$ref: "#/components/schemas/CredentialAccount"
|
$ref: "#/components/schemas/CredentialAccount"
|
||||||
|
401:
|
||||||
|
$ref: "#/components/responses/unauthorized"
|
||||||
|
403:
|
||||||
|
$ref: "#/components/responses/forbidden"
|
||||||
|
422:
|
||||||
|
$ref: "#/components/responses/unprocessableEntity"
|
||||||
|
|
||||||
/api/v1/accounts:
|
/api/v1/accounts:
|
||||||
post:
|
post:
|
||||||
|
@ -285,6 +293,10 @@ paths:
|
||||||
responses:
|
responses:
|
||||||
200:
|
200:
|
||||||
description: 成功
|
description: 成功
|
||||||
|
401:
|
||||||
|
$ref: "#/components/responses/unauthorized"
|
||||||
|
429:
|
||||||
|
$ref: "#/components/responses/rateLimited"
|
||||||
|
|
||||||
/api/v1/accounts/relationships:
|
/api/v1/accounts/relationships:
|
||||||
get:
|
get:
|
||||||
|
@ -316,6 +328,10 @@ paths:
|
||||||
type: array
|
type: array
|
||||||
items:
|
items:
|
||||||
$ref: "#/components/schemas/Relationship"
|
$ref: "#/components/schemas/Relationship"
|
||||||
|
401:
|
||||||
|
$ref: "#/components/responses/unauthorized"
|
||||||
|
422:
|
||||||
|
$ref: "#/components/responses/unprocessableEntity"
|
||||||
|
|
||||||
/api/v1/accounts/update_credentials:
|
/api/v1/accounts/update_credentials:
|
||||||
patch:
|
patch:
|
||||||
|
@ -337,6 +353,10 @@ paths:
|
||||||
application/json:
|
application/json:
|
||||||
schema:
|
schema:
|
||||||
$ref: "#/components/schemas/Account"
|
$ref: "#/components/schemas/Account"
|
||||||
|
401:
|
||||||
|
$ref: "#/components/responses/unauthorized"
|
||||||
|
422:
|
||||||
|
$ref: "#/components/responses/unprocessableEntity"
|
||||||
|
|
||||||
/api/v1/accounts/{id}:
|
/api/v1/accounts/{id}:
|
||||||
get:
|
get:
|
||||||
|
@ -357,6 +377,10 @@ paths:
|
||||||
application/json:
|
application/json:
|
||||||
schema:
|
schema:
|
||||||
$ref: "#/components/schemas/Account"
|
$ref: "#/components/schemas/Account"
|
||||||
|
401:
|
||||||
|
$ref: "#/components/responses/unauthorized"
|
||||||
|
404:
|
||||||
|
$ref: "#/components/responses/notfound"
|
||||||
|
|
||||||
/api/v1/accounts/{id}/follow:
|
/api/v1/accounts/{id}/follow:
|
||||||
post:
|
post:
|
||||||
|
@ -387,6 +411,12 @@ paths:
|
||||||
application/json:
|
application/json:
|
||||||
schema:
|
schema:
|
||||||
$ref: "#/components/schemas/Relationship"
|
$ref: "#/components/schemas/Relationship"
|
||||||
|
401:
|
||||||
|
$ref: "#/components/responses/unauthorized"
|
||||||
|
403:
|
||||||
|
$ref: "#/components/responses/forbidden"
|
||||||
|
422:
|
||||||
|
$ref: "#/components/responses/unprocessableEntity"
|
||||||
|
|
||||||
/api/v1/accounts/{id}/block:
|
/api/v1/accounts/{id}/block:
|
||||||
post:
|
post:
|
||||||
|
@ -408,6 +438,12 @@ paths:
|
||||||
application/json:
|
application/json:
|
||||||
schema:
|
schema:
|
||||||
$ref: "#/components/schemas/Relationship"
|
$ref: "#/components/schemas/Relationship"
|
||||||
|
401:
|
||||||
|
$ref: "#/components/responses/unauthorized"
|
||||||
|
403:
|
||||||
|
$ref: "#/components/responses/forbidden"
|
||||||
|
422:
|
||||||
|
$ref: "#/components/responses/unprocessableEntity"
|
||||||
|
|
||||||
/api/v1/accounts/{id}/unfollow:
|
/api/v1/accounts/{id}/unfollow:
|
||||||
post:
|
post:
|
||||||
|
@ -429,6 +465,12 @@ paths:
|
||||||
application/json:
|
application/json:
|
||||||
schema:
|
schema:
|
||||||
$ref: "#/components/schemas/Relationship"
|
$ref: "#/components/schemas/Relationship"
|
||||||
|
401:
|
||||||
|
$ref: "#/components/responses/unauthorized"
|
||||||
|
403:
|
||||||
|
$ref: "#/components/responses/forbidden"
|
||||||
|
422:
|
||||||
|
$ref: "#/components/responses/unprocessableEntity"
|
||||||
|
|
||||||
/api/v1/accounts/{id}/unblock:
|
/api/v1/accounts/{id}/unblock:
|
||||||
post:
|
post:
|
||||||
|
@ -450,6 +492,13 @@ paths:
|
||||||
application/json:
|
application/json:
|
||||||
schema:
|
schema:
|
||||||
$ref: "#/components/schemas/Relationship"
|
$ref: "#/components/schemas/Relationship"
|
||||||
|
401:
|
||||||
|
$ref: "#/components/responses/unauthorized"
|
||||||
|
403:
|
||||||
|
$ref: "#/components/responses/forbidden"
|
||||||
|
422:
|
||||||
|
$ref: "#/components/responses/unprocessableEntity"
|
||||||
|
|
||||||
/api/v1/accounts/{id}/remove_from_followers:
|
/api/v1/accounts/{id}/remove_from_followers:
|
||||||
post:
|
post:
|
||||||
tags:
|
tags:
|
||||||
|
@ -470,6 +519,12 @@ paths:
|
||||||
application/json:
|
application/json:
|
||||||
schema:
|
schema:
|
||||||
$ref: "#/components/schemas/Relationship"
|
$ref: "#/components/schemas/Relationship"
|
||||||
|
401:
|
||||||
|
$ref: "#/components/responses/unauthorized"
|
||||||
|
403:
|
||||||
|
$ref: "#/components/responses/forbidden"
|
||||||
|
422:
|
||||||
|
$ref: "#/components/responses/unprocessableEntity"
|
||||||
|
|
||||||
/api/v1/accounts/{id}/mute:
|
/api/v1/accounts/{id}/mute:
|
||||||
post:
|
post:
|
||||||
|
@ -491,6 +546,12 @@ paths:
|
||||||
application/json:
|
application/json:
|
||||||
schema:
|
schema:
|
||||||
$ref: "#/components/schemas/Relationship"
|
$ref: "#/components/schemas/Relationship"
|
||||||
|
401:
|
||||||
|
$ref: "#/components/responses/unauthorized"
|
||||||
|
403:
|
||||||
|
$ref: "#/components/responses/forbidden"
|
||||||
|
422:
|
||||||
|
$ref: "#/components/responses/unprocessableEntity"
|
||||||
|
|
||||||
/api/v1/accounts/{id}/unmute:
|
/api/v1/accounts/{id}/unmute:
|
||||||
post:
|
post:
|
||||||
|
@ -512,6 +573,12 @@ paths:
|
||||||
application/json:
|
application/json:
|
||||||
schema:
|
schema:
|
||||||
$ref: "#/components/schemas/Relationship"
|
$ref: "#/components/schemas/Relationship"
|
||||||
|
401:
|
||||||
|
$ref: "#/components/responses/unauthorized"
|
||||||
|
403:
|
||||||
|
$ref: "#/components/responses/forbidden"
|
||||||
|
422:
|
||||||
|
$ref: "#/components/responses/unprocessableEntity"
|
||||||
|
|
||||||
/api/v1/mutes:
|
/api/v1/mutes:
|
||||||
get:
|
get:
|
||||||
|
@ -544,6 +611,12 @@ paths:
|
||||||
schema:
|
schema:
|
||||||
items:
|
items:
|
||||||
$ref: "#/components/schemas/Account"
|
$ref: "#/components/schemas/Account"
|
||||||
|
401:
|
||||||
|
$ref: "#/components/responses/unauthorized"
|
||||||
|
403:
|
||||||
|
$ref: "#/components/responses/forbidden"
|
||||||
|
422:
|
||||||
|
$ref: "#/components/responses/unprocessableEntity"
|
||||||
|
|
||||||
/api/v1/accounts/{id}/statuses:
|
/api/v1/accounts/{id}/statuses:
|
||||||
get:
|
get:
|
||||||
|
@ -620,6 +693,11 @@ paths:
|
||||||
items:
|
items:
|
||||||
$ref: "#/components/schemas/Status"
|
$ref: "#/components/schemas/Status"
|
||||||
|
|
||||||
|
401:
|
||||||
|
$ref: "#/components/responses/unauthorized"
|
||||||
|
404:
|
||||||
|
$ref: "#/components/responses/notfound"
|
||||||
|
|
||||||
/api/v1/timelines/public:
|
/api/v1/timelines/public:
|
||||||
get:
|
get:
|
||||||
tags:
|
tags:
|
||||||
|
@ -707,6 +785,11 @@ paths:
|
||||||
type: array
|
type: array
|
||||||
items:
|
items:
|
||||||
$ref: "#/components/schemas/Status"
|
$ref: "#/components/schemas/Status"
|
||||||
|
206:
|
||||||
|
$ref: "#/components/responses/partialContent"
|
||||||
|
401:
|
||||||
|
$ref: "#/components/responses/unauthorized"
|
||||||
|
|
||||||
/api/v1/media:
|
/api/v1/media:
|
||||||
post:
|
post:
|
||||||
tags:
|
tags:
|
||||||
|
@ -730,6 +813,10 @@ paths:
|
||||||
application/json:
|
application/json:
|
||||||
schema:
|
schema:
|
||||||
$ref: "#/components/schemas/MediaAttachment"
|
$ref: "#/components/schemas/MediaAttachment"
|
||||||
|
401:
|
||||||
|
$ref: "#/components/responses/unauthorized"
|
||||||
|
422:
|
||||||
|
$ref: "#/components/responses/unprocessableEntity"
|
||||||
|
|
||||||
/api/v1/follow_requests:
|
/api/v1/follow_requests:
|
||||||
get:
|
get:
|
||||||
|
@ -1368,6 +1455,8 @@ components:
|
||||||
type: object
|
type: object
|
||||||
properties:
|
properties:
|
||||||
username:
|
username:
|
||||||
|
|
||||||
|
nullable: false
|
||||||
type: string
|
type: string
|
||||||
minLength: 1
|
minLength: 1
|
||||||
maxLength: 300
|
maxLength: 300
|
||||||
|
@ -2748,6 +2837,110 @@ components:
|
||||||
- created_at
|
- created_at
|
||||||
- account
|
- account
|
||||||
|
|
||||||
|
UnprocessableEntityResponse:
|
||||||
|
type: object
|
||||||
|
properties:
|
||||||
|
error:
|
||||||
|
type: string
|
||||||
|
details:
|
||||||
|
type: array
|
||||||
|
additionalProperties:
|
||||||
|
type: array
|
||||||
|
items:
|
||||||
|
$ref: "#/components/schemas/UnprocessableEntityResponseDetails"
|
||||||
|
|
||||||
|
UnprocessableEntityResponseDetails:
|
||||||
|
type: object
|
||||||
|
properties:
|
||||||
|
error:
|
||||||
|
type: string
|
||||||
|
description:
|
||||||
|
type: string
|
||||||
|
nullable: false
|
||||||
|
required:
|
||||||
|
- error
|
||||||
|
- description
|
||||||
|
|
||||||
|
UnauthorizedResponse:
|
||||||
|
type: object
|
||||||
|
properties:
|
||||||
|
error:
|
||||||
|
type: string
|
||||||
|
default: "The access token is invalid"
|
||||||
|
required:
|
||||||
|
- error
|
||||||
|
|
||||||
|
RateLimitedResponse:
|
||||||
|
type: object
|
||||||
|
properties:
|
||||||
|
error:
|
||||||
|
type: string
|
||||||
|
default: "Too many requests"
|
||||||
|
required:
|
||||||
|
- error
|
||||||
|
|
||||||
|
ForbiddenResponse:
|
||||||
|
type: object
|
||||||
|
properties:
|
||||||
|
error:
|
||||||
|
type: string
|
||||||
|
required:
|
||||||
|
- error
|
||||||
|
|
||||||
|
NotFoundResponse:
|
||||||
|
type: object
|
||||||
|
properties:
|
||||||
|
error:
|
||||||
|
type: string
|
||||||
|
required:
|
||||||
|
- error
|
||||||
|
|
||||||
|
PartialContentResponse:
|
||||||
|
type: object
|
||||||
|
|
||||||
|
|
||||||
|
responses:
|
||||||
|
forbidden:
|
||||||
|
description: forbidden
|
||||||
|
content:
|
||||||
|
application/json:
|
||||||
|
schema:
|
||||||
|
$ref: "#/components/schemas/ForbiddenResponse"
|
||||||
|
unprocessableEntity:
|
||||||
|
description: Unprocessable entity
|
||||||
|
content:
|
||||||
|
application/json:
|
||||||
|
schema:
|
||||||
|
$ref: "#/components/schemas/UnprocessableEntityResponse"
|
||||||
|
|
||||||
|
unauthorized:
|
||||||
|
description: Unauthorized
|
||||||
|
content:
|
||||||
|
application/json:
|
||||||
|
schema:
|
||||||
|
$ref: "#/components/schemas/UnauthorizedResponse"
|
||||||
|
|
||||||
|
rateLimited:
|
||||||
|
description: Too many requests
|
||||||
|
content:
|
||||||
|
application/json:
|
||||||
|
schema:
|
||||||
|
$ref: "#/components/schemas/RateLimitedResponse"
|
||||||
|
|
||||||
|
notfound:
|
||||||
|
description: Not found
|
||||||
|
content:
|
||||||
|
application/json:
|
||||||
|
schema:
|
||||||
|
$ref: "#/components/schemas/NotFoundResponse"
|
||||||
|
|
||||||
|
partialContent:
|
||||||
|
description: Partial content
|
||||||
|
content:
|
||||||
|
application/json:
|
||||||
|
schema:
|
||||||
|
$ref: "#/components/schemas/PartialContentResponse"
|
||||||
|
|
||||||
securitySchemes:
|
securitySchemes:
|
||||||
OAuth2:
|
OAuth2:
|
||||||
type: oauth2
|
type: oauth2
|
||||||
|
|
|
@ -36,3 +36,4 @@ Not Integer, not Long => we have a decimal value!
|
||||||
}}{{^isInteger}}{{^isLong}}{{#minimum}}
|
}}{{^isInteger}}{{^isLong}}{{#minimum}}
|
||||||
@get:DecimalMin("{{.}}"){{/minimum}}{{#maximum}}
|
@get:DecimalMin("{{.}}"){{/minimum}}{{#maximum}}
|
||||||
@get:DecimalMax("{{.}}"){{/maximum}}{{/isLong}}{{/isInteger}}
|
@get:DecimalMax("{{.}}"){{/maximum}}{{/isLong}}{{/isInteger}}
|
||||||
|
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
{{#useBeanValidation}}{{>beanValidation}}{{>beanValidationModel}}{{/useBeanValidation}}{{#swagger2AnnotationLibrary}}
|
{{#useBeanValidation}}{{>beanValidation}}{{>beanValidationModel}}{{/useBeanValidation}}{{#swagger2AnnotationLibrary}}
|
||||||
@Schema({{#example}}example = "{{#lambdaRemoveLineBreak}}{{#lambdaEscapeDoubleQuote}}{{{.}}}{{/lambdaEscapeDoubleQuote}}{{/lambdaRemoveLineBreak}}", {{/example}}required = true, {{#isReadOnly}}readOnly = {{{isReadOnly}}}, {{/isReadOnly}}description = "{{{description}}}"){{/swagger2AnnotationLibrary}}{{#swagger1AnnotationLibrary}}
|
@Schema({{#example}}example = "{{#lambdaRemoveLineBreak}}{{#lambdaEscapeDoubleQuote}}{{{.}}}{{/lambdaEscapeDoubleQuote}}{{/lambdaRemoveLineBreak}}", {{/example}}required = true, {{#isReadOnly}}readOnly = {{{isReadOnly}}}, {{/isReadOnly}}description = "{{{description}}}"){{/swagger2AnnotationLibrary}}{{#swagger1AnnotationLibrary}}
|
||||||
@ApiModelProperty({{#example}}example = "{{#lambdaRemoveLineBreak}}{{#lambdaEscapeDoubleQuote}}{{{.}}}{{/lambdaEscapeDoubleQuote}}{{/lambdaRemoveLineBreak}}", {{/example}}required = true, {{#isReadOnly}}readOnly = {{{isReadOnly}}}, {{/isReadOnly}}value = "{{{description}}}"){{/swagger1AnnotationLibrary}}
|
@ApiModelProperty({{#example}}example = "{{#lambdaRemoveLineBreak}}{{#lambdaEscapeDoubleQuote}}{{{.}}}{{/lambdaEscapeDoubleQuote}}{{/lambdaRemoveLineBreak}}", {{/example}}required = true, {{#isReadOnly}}readOnly = {{{isReadOnly}}}, {{/isReadOnly}}value = "{{{description}}}"){{/swagger1AnnotationLibrary}}{{^isNullable}}@get:NotNull{{/isNullable}}
|
||||||
@get:JsonProperty("{{{baseName}}}", required = true){{#isInherited}} override{{/isInherited}} {{>modelMutable}} {{{name}}}: {{#isEnum}}{{#isArray}}{{baseType}}<{{/isArray}}{{classname}}.{{{nameInCamelCase}}}{{#isArray}}>{{/isArray}}{{/isEnum}}{{^isEnum}}{{{dataType}}}{{/isEnum}}{{#isNullable}}?{{/isNullable}}{{#defaultValue}} = {{{.}}}{{/defaultValue}}
|
@get:JsonProperty("{{{baseName}}}", required = true){{#isInherited}} override{{/isInherited}} {{>modelMutable}} {{{name}}}: {{#isEnum}}{{#isArray}}{{baseType}}<{{/isArray}}{{classname}}.{{{nameInCamelCase}}}{{#isArray}}>{{/isArray}}{{/isEnum}}{{^isEnum}}{{{dataType}}}{{/isEnum}}{{#isNullable}}?{{/isNullable}}{{#defaultValue}} = {{{.}}}{{/defaultValue}}
|
||||||
|
|
Loading…
Reference in New Issue