Merge pull request #287 from usbharu/feature/error-response

Mastodon互換APIに異常系レスポンスを追加
This commit is contained in:
usbharu 2024-02-24 14:31:02 +09:00 committed by GitHub
commit a242748908
15 changed files with 538 additions and 33 deletions

View File

@ -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

View File

@ -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"))

View File

@ -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)
}

View File

@ -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
}
}

View File

@ -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)

View File

@ -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
),
)
}
}

View File

@ -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)

View File

@ -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> {

View File

@ -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)
}
}

View File

@ -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))
}
} }

View File

@ -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?
} }

View File

@ -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 {

View File

@ -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

View File

@ -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}}

View File

@ -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}}