From d204cfc37f598389950985da91f33bfac5ab4efc Mon Sep 17 00:00:00 2001 From: usbharu <64310155+usbharu@users.noreply.github.com> Date: Thu, 25 Jan 2024 22:53:43 +0900 Subject: [PATCH 1/3] =?UTF-8?q?feat:=20=E3=83=A6=E3=83=BC=E3=82=B6?= =?UTF-8?q?=E3=83=BC=E3=81=AE=E3=83=9F=E3=83=A5=E3=83=BC=E3=83=88=E3=81=AE?= =?UTF-8?q?Mastodon=E4=BA=92=E6=8F=9BAPI=E3=82=92=E5=AE=9F=E8=A3=85?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../relationship/RelationshipRepository.kt | 8 ++ .../RelationshipRepositoryImpl.kt | 22 ++++++ .../account/MastodonAccountApiController.kt | 32 ++++++++ .../service/account/AccountApiService.kt | 22 ++++++ src/main/resources/openapi/mastodon.yaml | 74 +++++++++++++++++++ 5 files changed, 158 insertions(+) diff --git a/src/main/kotlin/dev/usbharu/hideout/core/domain/model/relationship/RelationshipRepository.kt b/src/main/kotlin/dev/usbharu/hideout/core/domain/model/relationship/RelationshipRepository.kt index ac96db6b..7dca9785 100644 --- a/src/main/kotlin/dev/usbharu/hideout/core/domain/model/relationship/RelationshipRepository.kt +++ b/src/main/kotlin/dev/usbharu/hideout/core/domain/model/relationship/RelationshipRepository.kt @@ -42,4 +42,12 @@ interface RelationshipRepository { followRequest: Boolean, ignoreFollowRequest: Boolean ): List + + suspend fun findByActorIdAntMutingAndMaxIdAndSinceId( + actorId: Long, + muting: Boolean, + maxId: Long?, + sinceId: Long?, + limit: Int + ): List } diff --git a/src/main/kotlin/dev/usbharu/hideout/core/domain/model/relationship/RelationshipRepositoryImpl.kt b/src/main/kotlin/dev/usbharu/hideout/core/domain/model/relationship/RelationshipRepositoryImpl.kt index 9cea8aaa..6aa2fd70 100644 --- a/src/main/kotlin/dev/usbharu/hideout/core/domain/model/relationship/RelationshipRepositoryImpl.kt +++ b/src/main/kotlin/dev/usbharu/hideout/core/domain/model/relationship/RelationshipRepositoryImpl.kt @@ -97,6 +97,28 @@ class RelationshipRepositoryImpl : RelationshipRepository, AbstractRepository() return@query query.map { it.toRelationships() } } + override suspend fun findByActorIdAntMutingAndMaxIdAndSinceId( + actorId: Long, + muting: Boolean, + maxId: Long?, + sinceId: Long?, + limit: Int + ): List = query { + val query = Relationships.select { + Relationships.actorId.eq(actorId).and(Relationships.muting.eq(muting)) + }.limit(limit) + + if (maxId != null) { + query.andWhere { Relationships.id lessEq maxId } + } + + if (sinceId != null) { + query.andWhere { Relationships.id greaterEq sinceId } + } + + return@query query.map { it.toRelationships() } + } + companion object { private val logger = LoggerFactory.getLogger(RelationshipRepositoryImpl::class.java) } diff --git a/src/main/kotlin/dev/usbharu/hideout/mastodon/interfaces/api/account/MastodonAccountApiController.kt b/src/main/kotlin/dev/usbharu/hideout/mastodon/interfaces/api/account/MastodonAccountApiController.kt index e8e65008..d2066b68 100644 --- a/src/main/kotlin/dev/usbharu/hideout/mastodon/interfaces/api/account/MastodonAccountApiController.kt +++ b/src/main/kotlin/dev/usbharu/hideout/mastodon/interfaces/api/account/MastodonAccountApiController.kt @@ -186,4 +186,36 @@ class MastodonAccountApiController( .asFlow() ResponseEntity.ok(accountFlow) } + + override suspend fun apiV1AccountsIdMutePost(id: String): ResponseEntity { + val principal = SecurityContextHolder.getContext().getAuthentication().principal as Jwt + + val userid = principal.getClaim("uid").toLong() + + val mute = accountApiService.mute(userid, id.toLong()) + + return ResponseEntity.ok(mute) + } + + override suspend fun apiV1AccountsIdUnmutePost(id: String): ResponseEntity { + val principal = SecurityContextHolder.getContext().getAuthentication().principal as Jwt + + val userid = principal.getClaim("uid").toLong() + + val unmute = accountApiService.unmute(userid, id.toLong()) + + return ResponseEntity.ok(unmute) + } + + override fun apiV1MutesGet(maxId: String?, sinceId: String?, limit: Int?): ResponseEntity> = + runBlocking { + val principal = SecurityContextHolder.getContext().getAuthentication().principal as Jwt + + val userid = principal.getClaim("uid").toLong() + + val unmute = + accountApiService.mutesAccount(userid, maxId?.toLong(), sinceId?.toLong(), limit ?: 20).asFlow() + + return@runBlocking ResponseEntity.ok(unmute) + } } diff --git a/src/main/kotlin/dev/usbharu/hideout/mastodon/service/account/AccountApiService.kt b/src/main/kotlin/dev/usbharu/hideout/mastodon/service/account/AccountApiService.kt index 0cefb38c..a69e0474 100644 --- a/src/main/kotlin/dev/usbharu/hideout/mastodon/service/account/AccountApiService.kt +++ b/src/main/kotlin/dev/usbharu/hideout/mastodon/service/account/AccountApiService.kt @@ -60,6 +60,9 @@ interface AccountApiService { suspend fun acceptFollowRequest(loginUser: Long, target: Long): Relationship suspend fun rejectFollowRequest(loginUser: Long, target: Long): Relationship + suspend fun mute(userid: Long, target: Long): Relationship + suspend fun unmute(userid: Long, target: Long): Relationship + suspend fun mutesAccount(userid: Long, maxId: Long?, sinceId: Long?, limit: Int): List } @Service @@ -245,6 +248,25 @@ class AccountApiServiceImpl( return@transaction fetchRelationship(loginUser, target) } + override suspend fun mute(userid: Long, target: Long): Relationship = transaction.transaction { + relationshipService.mute(userid, target) + + return@transaction fetchRelationship(userid, target) + } + + override suspend fun unmute(userid: Long, target: Long): Relationship = transaction.transaction { + relationshipService.mute(userid, target) + + return@transaction fetchRelationship(userid, target) + } + + override suspend fun mutesAccount(userid: Long, maxId: Long?, sinceId: Long?, limit: Int): List { + val mutedAccounts = + relationshipRepository.findByActorIdAntMutingAndMaxIdAndSinceId(userid, true, maxId, sinceId, limit) + + return accountService.findByIds(mutedAccounts.map { it.targetActorId }) + } + private fun from(account: Account): CredentialAccount { return CredentialAccount( id = account.id, diff --git a/src/main/resources/openapi/mastodon.yaml b/src/main/resources/openapi/mastodon.yaml index 3a6ce32c..829da25c 100644 --- a/src/main/resources/openapi/mastodon.yaml +++ b/src/main/resources/openapi/mastodon.yaml @@ -464,6 +464,80 @@ paths: schema: $ref: "#/components/schemas/Relationship" + /api/v1/accounts/{id}/mute: + post: + tags: + - account + security: + - OAuth2: + - "write:mutes" + parameters: + - in: path + name: id + required: true + schema: + type: string + responses: + 200: + description: 成功 + content: + application/json: + schema: + $ref: "#/components/schemas/Relationship" + + /api/v1/accounts/{id}/unmute: + post: + tags: + - account + security: + - OAuth2: + - "write:mutes" + parameters: + - in: path + name: id + required: true + schema: + type: string + responses: + 200: + description: 成功 + content: + application/json: + schema: + $ref: "#/components/schemas/Relationship" + + /api/v1/mutes: + get: + tags: + - account + security: + - OAuth2: + - "read:mutes" + parameters: + - in: query + name: max_id + required: false + schema: + type: string + - in: query + name: since_id + required: false + schema: + type: string + - in: query + name: limit + schema: + type: integer + required: false + responses: + 200: + description: 成功 + content: + application/json: + schema: + items: + $ref: "#/components/schemas/Account" + /api/v1/accounts/{id}/statuses: get: tags: From 94cc109dce886e39eef37468f9b350efb4bed006 Mon Sep 17 00:00:00 2001 From: usbharu <64310155+usbharu@users.noreply.github.com> Date: Fri, 26 Jan 2024 11:50:02 +0900 Subject: [PATCH 2/3] =?UTF-8?q?feat:=20=E3=83=9F=E3=83=A5=E3=83=BC?= =?UTF-8?q?=E3=83=88API=E3=81=AE=E6=A8=A9=E9=99=90=E3=81=AE=E8=A8=AD?= =?UTF-8?q?=E5=AE=9A=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/application/config/SecurityConfig.kt | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/main/kotlin/dev/usbharu/hideout/application/config/SecurityConfig.kt b/src/main/kotlin/dev/usbharu/hideout/application/config/SecurityConfig.kt index 870c1159..b79bd78f 100644 --- a/src/main/kotlin/dev/usbharu/hideout/application/config/SecurityConfig.kt +++ b/src/main/kotlin/dev/usbharu/hideout/application/config/SecurityConfig.kt @@ -200,6 +200,9 @@ class SecurityConfig { authorize(POST, "/api/v1/accounts/*/unfollow", hasAnyScope("write", "write:follows")) authorize(POST, "/api/v1/accounts/*/block", hasAnyScope("write", "write:blocks")) authorize(POST, "/api/v1/accounts/*/unblock", hasAnyScope("write", "write:blocks")) + authorize(POST, "/api/v1/accounts/*/mute", hasAnyScope("write", "write:mutes")) + authorize(POST, "/api/v1/accounts/*/unmute", hasAnyScope("write", "write:mutes")) + authorize(GET, "/api/v1/mutes", hasAnyScope("read", "read:mutes")) authorize(POST, "/api/v1/media", hasAnyScope("write", "write:media")) authorize(POST, "/api/v1/statuses", hasAnyScope("write", "write:statuses")) From 9e912c1bb4c12f2a40d2622927dc2820d4606a48 Mon Sep 17 00:00:00 2001 From: usbharu <64310155+usbharu@users.noreply.github.com> Date: Fri, 26 Jan 2024 11:50:28 +0900 Subject: [PATCH 3/3] =?UTF-8?q?test:=20=E3=83=9F=E3=83=A5=E3=83=BC?= =?UTF-8?q?=E3=83=88=E3=81=AE=E3=83=86=E3=82=B9=E3=83=88=E3=82=92=E8=BF=BD?= =?UTF-8?q?=E5=8A=A0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../kotlin/mastodon/account/AccountApiTest.kt | 143 ++++++++++++++++++ 1 file changed, 143 insertions(+) diff --git a/src/intTest/kotlin/mastodon/account/AccountApiTest.kt b/src/intTest/kotlin/mastodon/account/AccountApiTest.kt index f2f9ffc7..666d3d57 100644 --- a/src/intTest/kotlin/mastodon/account/AccountApiTest.kt +++ b/src/intTest/kotlin/mastodon/account/AccountApiTest.kt @@ -281,6 +281,149 @@ class AccountApiTest { assertThat(alreadyFollow).isTrue() } + @Test + fun `apiV1AccountsIdMutePost write権限でミュートできる`() { + mockMvc + .post("/api/v1/accounts/2/mute") { + contentType = MediaType.APPLICATION_JSON + with(jwt().jwt { it.claim("uid", "1") }.authorities(SimpleGrantedAuthority("SCOPE_write"))) + } + .asyncDispatch() + .andExpect { status { isOk() } } + } + + @Test + fun `apiV1AccountsIdMutePost write_mutes権限でミュートできる`() { + mockMvc + .post("/api/v1/accounts/2/mute") { + contentType = MediaType.APPLICATION_JSON + with(jwt().jwt { it.claim("uid", "1") }.authorities(SimpleGrantedAuthority("SCOPE_write:mutes"))) + } + .asyncDispatch() + .andExpect { status { isOk() } } + } + + @Test + fun `apiV1AccountsIdMutePost read権限だと403`() = runTest { + mockMvc + .post("/api/v1/accounts/2/mute") { + contentType = MediaType.APPLICATION_JSON + with(jwt().jwt { it.claim("uid", "1") }.authorities(SimpleGrantedAuthority("SCOPE_read"))) + } + .andExpect { status { isForbidden() } } + } + + @Test + @WithAnonymousUser + fun `apiV1AccountsIdMutePost 匿名だと401`() = runTest { + mockMvc + .post("/api/v1/accounts/2/mute") { + contentType = MediaType.APPLICATION_JSON + with(csrf()) + } + .andExpect { status { isUnauthorized() } } + } + + @Test + @WithAnonymousUser + fun `apiV1AccountsIdMutePost csrfトークンがないと403`() = runTest { + mockMvc + .post("/api/v1/accounts/2/mute") { + contentType = MediaType.APPLICATION_JSON + } + .andExpect { status { isForbidden() } } + } + + @Test + fun `apiV1AccountsIdUnmutePost write権限でアンミュートできる`() { + mockMvc + .post("/api/v1/accounts/2/unmute") { + contentType = MediaType.APPLICATION_JSON + with(jwt().jwt { it.claim("uid", "1") }.authorities(SimpleGrantedAuthority("SCOPE_write"))) + } + .asyncDispatch() + .andExpect { status { isOk() } } + } + + @Test + fun `apiV1AccountsIdUnmutePost write_mutes権限でアンミュートできる`() { + mockMvc + .post("/api/v1/accounts/2/unmute") { + contentType = MediaType.APPLICATION_JSON + with(jwt().jwt { it.claim("uid", "1") }.authorities(SimpleGrantedAuthority("SCOPE_write:mutes"))) + } + .asyncDispatch() + .andExpect { status { isOk() } } + } + + @Test + fun `apiV1AccountsIdUnmutePost read権限だと403`() = runTest { + mockMvc + .post("/api/v1/accounts/2/unmute") { + contentType = MediaType.APPLICATION_JSON + with(jwt().jwt { it.claim("uid", "1") }.authorities(SimpleGrantedAuthority("SCOPE_read"))) + } + .andExpect { status { isForbidden() } } + } + + @Test + @WithAnonymousUser + fun `apiV1AccountsIdUnmutePost 匿名だと401`() = runTest { + mockMvc + .post("/api/v1/accounts/2/unmute") { + contentType = MediaType.APPLICATION_JSON + with(csrf()) + } + .andExpect { status { isUnauthorized() } } + } + + @Test + @WithAnonymousUser + fun `apiV1AccountsIdUnmutePost csrfトークンがないと403`() = runTest { + mockMvc + .post("/api/v1/accounts/2/unmute") { + contentType = MediaType.APPLICATION_JSON + } + .andExpect { status { isForbidden() } } + } + + @Test + fun `apiV1MutesGet read権限でミュートしているアカウント一覧を取得できる`() { + mockMvc + .get("/api/v1/mutes") { + with(jwt().jwt { it.claim("uid", "1") }.authorities(SimpleGrantedAuthority("SCOPE_read"))) + } + .asyncDispatch() + .andExpect { status { isOk() } } + } + + @Test + fun `apiV1MutesGet read_mutes権限でミュートしているアカウント一覧を取得できる`() { + mockMvc + .get("/api/v1/mutes") { + with(jwt().jwt { it.claim("uid", "1") }.authorities(SimpleGrantedAuthority("SCOPE_read:mutes"))) + } + .asyncDispatch() + .andExpect { status { isOk() } } + } + + @Test + fun `apiV1MutesGet write権限だと403`() { + mockMvc + .get("/api/v1/mutes") { + with(jwt().jwt { it.claim("uid", "1") }.authorities(SimpleGrantedAuthority("SCOPE_write"))) + } + .andExpect { status { isForbidden() } } + } + + @Test + @WithAnonymousUser + fun `apiV1MutesGet 匿名だと401`() { + mockMvc + .get("/api/v1/mutes") + .andExpect { status { isUnauthorized() } } + } + companion object { @JvmStatic @AfterAll