From 9de04f944f0df8a20b890db62474f0cdabf3f575 Mon Sep 17 00:00:00 2001 From: usbharu <64310155+usbharu@users.noreply.github.com> Date: Wed, 17 May 2023 21:56:06 +0900 Subject: [PATCH] =?UTF-8?q?feat:=20=E6=8A=95=E7=A8=BF=E5=8F=96=E5=BE=97?= =?UTF-8?q?=E3=81=AEAPI=E3=82=92=E8=BF=BD=E5=8A=A0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../dev/usbharu/hideout/domain/model/Acct.kt | 3 ++ .../domain/model/hideout/dto/PostCreateDto.kt | 11 +++- .../exception/PostNotFoundException.kt | 8 +++ .../dev/usbharu/hideout/plugins/Routing.kt | 4 ++ .../hideout/routing/api/internal/v1/Posts.kt | 54 ++++++++++++++++++- .../usbharu/hideout/service/IPostService.kt | 39 ++++++++++++++ .../hideout/service/impl/PostService.kt | 21 ++++++++ .../dev/usbharu/hideout/util/AcctUtil.kt | 41 ++++++++++++++ 8 files changed, 178 insertions(+), 3 deletions(-) create mode 100644 src/main/kotlin/dev/usbharu/hideout/domain/model/Acct.kt create mode 100644 src/main/kotlin/dev/usbharu/hideout/exception/PostNotFoundException.kt create mode 100644 src/main/kotlin/dev/usbharu/hideout/util/AcctUtil.kt diff --git a/src/main/kotlin/dev/usbharu/hideout/domain/model/Acct.kt b/src/main/kotlin/dev/usbharu/hideout/domain/model/Acct.kt new file mode 100644 index 00000000..af771e1b --- /dev/null +++ b/src/main/kotlin/dev/usbharu/hideout/domain/model/Acct.kt @@ -0,0 +1,3 @@ +package dev.usbharu.hideout.domain.model + +data class Acct(val username: String, val domain: String? = null, val isRemote: Boolean = domain == null) diff --git a/src/main/kotlin/dev/usbharu/hideout/domain/model/hideout/dto/PostCreateDto.kt b/src/main/kotlin/dev/usbharu/hideout/domain/model/hideout/dto/PostCreateDto.kt index 9d699fb7..272215a2 100644 --- a/src/main/kotlin/dev/usbharu/hideout/domain/model/hideout/dto/PostCreateDto.kt +++ b/src/main/kotlin/dev/usbharu/hideout/domain/model/hideout/dto/PostCreateDto.kt @@ -1,3 +1,12 @@ package dev.usbharu.hideout.domain.model.hideout.dto -data class PostCreateDto(val text: String, val userId: Long) +import dev.usbharu.hideout.domain.model.hideout.entity.Visibility + +data class PostCreateDto( + val text: String, + val overview: String? = null, + val visibility: Visibility, + val repostId: Long? = null, + val repolyId: Long? = null, + val userId: Long +) diff --git a/src/main/kotlin/dev/usbharu/hideout/exception/PostNotFoundException.kt b/src/main/kotlin/dev/usbharu/hideout/exception/PostNotFoundException.kt new file mode 100644 index 00000000..9fe3cf78 --- /dev/null +++ b/src/main/kotlin/dev/usbharu/hideout/exception/PostNotFoundException.kt @@ -0,0 +1,8 @@ +package dev.usbharu.hideout.exception + +class PostNotFoundException : IllegalArgumentException { + constructor() : super() + constructor(s: String?) : super(s) + constructor(message: String?, cause: Throwable?) : super(message, cause) + constructor(cause: Throwable?) : super(cause) +} diff --git a/src/main/kotlin/dev/usbharu/hideout/plugins/Routing.kt b/src/main/kotlin/dev/usbharu/hideout/plugins/Routing.kt index 24f94a6a..1d7cce98 100644 --- a/src/main/kotlin/dev/usbharu/hideout/plugins/Routing.kt +++ b/src/main/kotlin/dev/usbharu/hideout/plugins/Routing.kt @@ -3,6 +3,7 @@ package dev.usbharu.hideout.plugins import dev.usbharu.hideout.routing.activitypub.inbox import dev.usbharu.hideout.routing.activitypub.outbox import dev.usbharu.hideout.routing.activitypub.usersAP +import dev.usbharu.hideout.routing.api.internal.v1.posts import dev.usbharu.hideout.routing.api.mastodon.v1.statuses import dev.usbharu.hideout.routing.wellknown.webfinger import dev.usbharu.hideout.service.IPostService @@ -31,5 +32,8 @@ fun Application.configureRouting( route("/api/v1") { statuses(postService) } + route("/api/internal/v1") { + posts(postService) + } } } diff --git a/src/main/kotlin/dev/usbharu/hideout/routing/api/internal/v1/Posts.kt b/src/main/kotlin/dev/usbharu/hideout/routing/api/internal/v1/Posts.kt index 7477cf6c..db1b3f70 100644 --- a/src/main/kotlin/dev/usbharu/hideout/routing/api/internal/v1/Posts.kt +++ b/src/main/kotlin/dev/usbharu/hideout/routing/api/internal/v1/Posts.kt @@ -1,9 +1,13 @@ package dev.usbharu.hideout.routing.api.internal.v1 +import dev.usbharu.hideout.config.Config import dev.usbharu.hideout.domain.model.hideout.dto.PostCreateDto import dev.usbharu.hideout.domain.model.hideout.form.Post +import dev.usbharu.hideout.exception.ParameterNotExistException +import dev.usbharu.hideout.exception.PostNotFoundException import dev.usbharu.hideout.plugins.TOKEN_AUTH import dev.usbharu.hideout.service.IPostService +import dev.usbharu.hideout.util.AcctUtil import dev.usbharu.hideout.util.InstantParseUtil import io.ktor.http.* import io.ktor.server.application.* @@ -21,7 +25,14 @@ fun Route.posts(postService: IPostService) { val userId = principal.payload.getClaim("uid").asLong() val receive = call.receive() - val postCreateDto = PostCreateDto(receive.text, userId) + val postCreateDto = PostCreateDto( + receive.text, + receive.overview, + receive.visibility, + receive.repostId, + receive.replyId, + userId + ) val create = postService.create(postCreateDto) call.response.header("Location", create.url) call.respond(HttpStatusCode.OK) @@ -35,7 +46,46 @@ fun Route.posts(postService: IPostService) { val minId = call.request.queryParameters["minId"]?.toLong() val maxId = call.request.queryParameters["maxId"]?.toLong() val limit = call.request.queryParameters["limit"]?.toInt() - postService.findAll(since, until, minId, maxId, limit, userId) + call.respond(postService.findAll(since, until, minId, maxId, limit, userId)) + } + get("/{id}") { + val userId = call.principal()?.payload?.getClaim("uid")?.asLong() + val id = call.parameters["id"]?.toLong() + ?: throw ParameterNotExistException("Parameter(id='postsId') does not exist.") + val post = (postService.findByIdForUser(id, userId) + ?: throw PostNotFoundException("$id was not found or is not authorized.")) + call.respond(post) + } + } + } + route("/users/{name}/posts") { + authenticate(TOKEN_AUTH, optional = true) { + get { + val userId = call.principal()?.payload?.getClaim("uid")?.asLong() + val targetUserName = call.parameters["name"] + ?: throw ParameterNotExistException("Parameter(name='userName@domain') does not exist.") + val targetUserId = targetUserName.toLongOrNull() + val posts = if (targetUserId == null) { + val acct = AcctUtil.parse(targetUserName) + postService.findByUserNameAndDomainForUser( + acct.username, + acct.domain ?: Config.configData.domain, + forUserId = userId + ) + } else { + postService.findByUserIdForUser(targetUserId, forUserId = userId) + } + call.respond(posts) + + } + get("/{id}") { + val userId = call.principal()?.payload?.getClaim("uid")?.asLong() + val id = call.parameters["id"]?.toLong() + ?: throw ParameterNotExistException("Parameter(name='postsId' does not exist.") + val post = (postService.findByIdForUser(id, userId) + ?: throw PostNotFoundException("$id was not found or is not authorized.")) + call.response.header("Content-Location", post.url) + call.respond(post) } } } diff --git a/src/main/kotlin/dev/usbharu/hideout/service/IPostService.kt b/src/main/kotlin/dev/usbharu/hideout/service/IPostService.kt index 0b4b4d59..f2da7a53 100644 --- a/src/main/kotlin/dev/usbharu/hideout/service/IPostService.kt +++ b/src/main/kotlin/dev/usbharu/hideout/service/IPostService.kt @@ -1,5 +1,6 @@ package dev.usbharu.hideout.service +import dev.usbharu.hideout.config.Config import dev.usbharu.hideout.domain.model.hideout.dto.PostCreateDto import dev.usbharu.hideout.domain.model.hideout.entity.Post import java.time.Instant @@ -17,5 +18,43 @@ interface IPostService { ): List suspend fun findById(id: String): Post + + /** + * 権限を考慮して投稿を取得します。 + * + * @param id + * @param userId + * @return + */ + suspend fun findByIdForUser(id: Long, userId: Long?): Post? + + /** + * 権限を考慮してユーザーの投稿を取得します。 + * + * @param userId + * @param forUserId + * @return + */ + suspend fun findByUserIdForUser( + userId: Long, + since: Instant? = null, + until: Instant? = null, + minId: Long? = null, + maxId: Long? = null, + limit: Int? = null, + forUserId: Long? = null + ): List + + suspend fun findByUserNameAndDomainForUser( + userName: String, + domain: String = Config.configData.domain, + since: Instant? = null, + until: Instant? = null, + minId: Long? = null, + maxId: Long? = null, + limit: Int? = null, + forUserId: Long? = null + ): List + suspend fun delete(id: String) } diff --git a/src/main/kotlin/dev/usbharu/hideout/service/impl/PostService.kt b/src/main/kotlin/dev/usbharu/hideout/service/impl/PostService.kt index 137075b1..e017610d 100644 --- a/src/main/kotlin/dev/usbharu/hideout/service/impl/PostService.kt +++ b/src/main/kotlin/dev/usbharu/hideout/service/impl/PostService.kt @@ -9,6 +9,10 @@ import dev.usbharu.hideout.repository.UsersFollowers import dev.usbharu.hideout.repository.toPost import dev.usbharu.hideout.service.IPostService import dev.usbharu.hideout.service.activitypub.ActivityPubNoteService +import org.jetbrains.exposed.sql.SqlExpressionBuilder.eq +import org.jetbrains.exposed.sql.SqlExpressionBuilder.inSubQuery +import org.jetbrains.exposed.sql.and +import org.jetbrains.exposed.sql.orIfNotNull import org.jetbrains.exposed.sql.orWhere import org.jetbrains.exposed.sql.select import org.jetbrains.exposed.sql.transactions.transaction @@ -71,6 +75,23 @@ class PostService( TODO("Not yet implemented") } + override suspend fun findByIdForUser(id: Long, userId: Long?): Post? { + return transaction { + val select = Posts.select( + Posts.id.eq(id).and( + Posts.visibility.eq(Visibility.PUBLIC.ordinal).orIfNotNull( + userId?.let { + Posts.userId.inSubQuery( + UsersFollowers.slice(UsersFollowers.userId).select(UsersFollowers.followerId.eq(userId)) + ) + } + ) + ) + ) + select.singleOrNull()?.toPost() + } + } + override suspend fun delete(id: String) { TODO("Not yet implemented") } diff --git a/src/main/kotlin/dev/usbharu/hideout/util/AcctUtil.kt b/src/main/kotlin/dev/usbharu/hideout/util/AcctUtil.kt new file mode 100644 index 00000000..70ce1ff2 --- /dev/null +++ b/src/main/kotlin/dev/usbharu/hideout/util/AcctUtil.kt @@ -0,0 +1,41 @@ +package dev.usbharu.hideout.util + +import dev.usbharu.hideout.domain.model.Acct + +object AcctUtil { + fun parse(string: String): Acct { + if (string.isBlank()) { + throw IllegalArgumentException("Invalid acct.(Blank)") + } + return when (string.count { c -> c == '@' }) { + 0 -> { + Acct(string) + } + + 1 -> { + if (string.startsWith("@")) { + Acct(string.substring(1 until string.length)) + } else { + Acct(string.substringBefore("@"), string.substringAfter("@")) + } + } + + 2 -> { + if (string.startsWith("@")) { + val substring = string.substring(1 until string.length) + val userName = substring.substringBefore("@") + val domain = substring.substringAfter("@") + Acct( + userName, domain.ifBlank { null } + ) + } else { + throw IllegalArgumentException("Invalid acct.(@ are in the wrong position)") + } + } + + else -> { + throw IllegalArgumentException("Invalid acct.(Too many @)") + } + } + } +}