mirror of https://github.com/usbharu/Hideout.git
Merge pull request #254 from usbharu/feature/mute
feat: ユーザーのミュートのMastodon互換APIを実装
This commit is contained in:
commit
a60b58daef
|
@ -281,6 +281,149 @@ class AccountApiTest {
|
||||||
assertThat(alreadyFollow).isTrue()
|
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 {
|
companion object {
|
||||||
@JvmStatic
|
@JvmStatic
|
||||||
@AfterAll
|
@AfterAll
|
||||||
|
|
|
@ -200,6 +200,9 @@ class SecurityConfig {
|
||||||
authorize(POST, "/api/v1/accounts/*/unfollow", hasAnyScope("write", "write:follows"))
|
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/*/block", hasAnyScope("write", "write:blocks"))
|
||||||
authorize(POST, "/api/v1/accounts/*/unblock", 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/media", hasAnyScope("write", "write:media"))
|
||||||
authorize(POST, "/api/v1/statuses", hasAnyScope("write", "write:statuses"))
|
authorize(POST, "/api/v1/statuses", hasAnyScope("write", "write:statuses"))
|
||||||
|
|
|
@ -42,4 +42,12 @@ interface RelationshipRepository {
|
||||||
followRequest: Boolean,
|
followRequest: Boolean,
|
||||||
ignoreFollowRequest: Boolean
|
ignoreFollowRequest: Boolean
|
||||||
): List<Relationship>
|
): List<Relationship>
|
||||||
|
|
||||||
|
suspend fun findByActorIdAntMutingAndMaxIdAndSinceId(
|
||||||
|
actorId: Long,
|
||||||
|
muting: Boolean,
|
||||||
|
maxId: Long?,
|
||||||
|
sinceId: Long?,
|
||||||
|
limit: Int
|
||||||
|
): List<Relationship>
|
||||||
}
|
}
|
||||||
|
|
|
@ -97,6 +97,28 @@ class RelationshipRepositoryImpl : RelationshipRepository, AbstractRepository()
|
||||||
return@query query.map { it.toRelationships() }
|
return@query query.map { it.toRelationships() }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
override suspend fun findByActorIdAntMutingAndMaxIdAndSinceId(
|
||||||
|
actorId: Long,
|
||||||
|
muting: Boolean,
|
||||||
|
maxId: Long?,
|
||||||
|
sinceId: Long?,
|
||||||
|
limit: Int
|
||||||
|
): List<Relationship> = 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 {
|
companion object {
|
||||||
private val logger = LoggerFactory.getLogger(RelationshipRepositoryImpl::class.java)
|
private val logger = LoggerFactory.getLogger(RelationshipRepositoryImpl::class.java)
|
||||||
}
|
}
|
||||||
|
|
|
@ -186,4 +186,36 @@ class MastodonAccountApiController(
|
||||||
.asFlow()
|
.asFlow()
|
||||||
ResponseEntity.ok(accountFlow)
|
ResponseEntity.ok(accountFlow)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
override suspend fun apiV1AccountsIdMutePost(id: String): ResponseEntity<Relationship> {
|
||||||
|
val principal = SecurityContextHolder.getContext().getAuthentication().principal as Jwt
|
||||||
|
|
||||||
|
val userid = principal.getClaim<String>("uid").toLong()
|
||||||
|
|
||||||
|
val mute = accountApiService.mute(userid, id.toLong())
|
||||||
|
|
||||||
|
return ResponseEntity.ok(mute)
|
||||||
|
}
|
||||||
|
|
||||||
|
override suspend fun apiV1AccountsIdUnmutePost(id: String): ResponseEntity<Relationship> {
|
||||||
|
val principal = SecurityContextHolder.getContext().getAuthentication().principal as Jwt
|
||||||
|
|
||||||
|
val userid = principal.getClaim<String>("uid").toLong()
|
||||||
|
|
||||||
|
val unmute = accountApiService.unmute(userid, id.toLong())
|
||||||
|
|
||||||
|
return ResponseEntity.ok(unmute)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun apiV1MutesGet(maxId: String?, sinceId: String?, limit: Int?): ResponseEntity<Flow<Account>> =
|
||||||
|
runBlocking {
|
||||||
|
val principal = SecurityContextHolder.getContext().getAuthentication().principal as Jwt
|
||||||
|
|
||||||
|
val userid = principal.getClaim<String>("uid").toLong()
|
||||||
|
|
||||||
|
val unmute =
|
||||||
|
accountApiService.mutesAccount(userid, maxId?.toLong(), sinceId?.toLong(), limit ?: 20).asFlow()
|
||||||
|
|
||||||
|
return@runBlocking ResponseEntity.ok(unmute)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -60,6 +60,9 @@ interface AccountApiService {
|
||||||
|
|
||||||
suspend fun acceptFollowRequest(loginUser: Long, target: Long): Relationship
|
suspend fun acceptFollowRequest(loginUser: Long, target: Long): Relationship
|
||||||
suspend fun rejectFollowRequest(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<Account>
|
||||||
}
|
}
|
||||||
|
|
||||||
@Service
|
@Service
|
||||||
|
@ -245,6 +248,25 @@ class AccountApiServiceImpl(
|
||||||
return@transaction fetchRelationship(loginUser, target)
|
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<Account> {
|
||||||
|
val mutedAccounts =
|
||||||
|
relationshipRepository.findByActorIdAntMutingAndMaxIdAndSinceId(userid, true, maxId, sinceId, limit)
|
||||||
|
|
||||||
|
return accountService.findByIds(mutedAccounts.map { it.targetActorId })
|
||||||
|
}
|
||||||
|
|
||||||
private fun from(account: Account): CredentialAccount {
|
private fun from(account: Account): CredentialAccount {
|
||||||
return CredentialAccount(
|
return CredentialAccount(
|
||||||
id = account.id,
|
id = account.id,
|
||||||
|
|
|
@ -464,6 +464,80 @@ paths:
|
||||||
schema:
|
schema:
|
||||||
$ref: "#/components/schemas/Relationship"
|
$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:
|
/api/v1/accounts/{id}/statuses:
|
||||||
get:
|
get:
|
||||||
tags:
|
tags:
|
||||||
|
|
Loading…
Reference in New Issue