mirror of https://github.com/usbharu/Hideout.git
Merge pull request #197 from usbharu/feature/mastodon-follow-api
Feature/mastodon follow api
This commit is contained in:
commit
d05e8cf68b
|
@ -1,8 +1,10 @@
|
||||||
package mastodon.account
|
package mastodon.account
|
||||||
|
|
||||||
import dev.usbharu.hideout.SpringApplication
|
import dev.usbharu.hideout.SpringApplication
|
||||||
|
import dev.usbharu.hideout.core.infrastructure.exposedquery.FollowerQueryServiceImpl
|
||||||
import dev.usbharu.hideout.core.infrastructure.exposedquery.UserQueryServiceImpl
|
import dev.usbharu.hideout.core.infrastructure.exposedquery.UserQueryServiceImpl
|
||||||
import kotlinx.coroutines.test.runTest
|
import kotlinx.coroutines.test.runTest
|
||||||
|
import org.assertj.core.api.Assertions.assertThat
|
||||||
import org.flywaydb.core.Flyway
|
import org.flywaydb.core.Flyway
|
||||||
import org.junit.jupiter.api.AfterAll
|
import org.junit.jupiter.api.AfterAll
|
||||||
import org.junit.jupiter.api.BeforeEach
|
import org.junit.jupiter.api.BeforeEach
|
||||||
|
@ -14,6 +16,7 @@ import org.springframework.http.MediaType
|
||||||
import org.springframework.security.core.authority.SimpleGrantedAuthority
|
import org.springframework.security.core.authority.SimpleGrantedAuthority
|
||||||
import org.springframework.security.test.context.support.WithAnonymousUser
|
import org.springframework.security.test.context.support.WithAnonymousUser
|
||||||
import org.springframework.security.test.web.servlet.request.SecurityMockMvcRequestPostProcessors
|
import org.springframework.security.test.web.servlet.request.SecurityMockMvcRequestPostProcessors
|
||||||
|
import org.springframework.security.test.web.servlet.request.SecurityMockMvcRequestPostProcessors.csrf
|
||||||
import org.springframework.security.test.web.servlet.request.SecurityMockMvcRequestPostProcessors.jwt
|
import org.springframework.security.test.web.servlet.request.SecurityMockMvcRequestPostProcessors.jwt
|
||||||
import org.springframework.security.test.web.servlet.setup.SecurityMockMvcConfigurers.springSecurity
|
import org.springframework.security.test.web.servlet.setup.SecurityMockMvcConfigurers.springSecurity
|
||||||
import org.springframework.test.context.jdbc.Sql
|
import org.springframework.test.context.jdbc.Sql
|
||||||
|
@ -29,11 +32,16 @@ import org.springframework.web.context.WebApplicationContext
|
||||||
@AutoConfigureMockMvc
|
@AutoConfigureMockMvc
|
||||||
@Transactional
|
@Transactional
|
||||||
@Sql("/sql/test-user.sql", executionPhase = Sql.ExecutionPhase.BEFORE_TEST_CLASS)
|
@Sql("/sql/test-user.sql", executionPhase = Sql.ExecutionPhase.BEFORE_TEST_CLASS)
|
||||||
|
@Sql("/sql/test-user2.sql", executionPhase = Sql.ExecutionPhase.BEFORE_TEST_CLASS)
|
||||||
class AccountApiTest {
|
class AccountApiTest {
|
||||||
|
|
||||||
|
@Autowired
|
||||||
|
private lateinit var followerQueryServiceImpl: FollowerQueryServiceImpl
|
||||||
|
|
||||||
@Autowired
|
@Autowired
|
||||||
private lateinit var userQueryServiceImpl: UserQueryServiceImpl
|
private lateinit var userQueryServiceImpl: UserQueryServiceImpl
|
||||||
|
|
||||||
|
|
||||||
@Autowired
|
@Autowired
|
||||||
private lateinit var context: WebApplicationContext
|
private lateinit var context: WebApplicationContext
|
||||||
|
|
||||||
|
@ -92,7 +100,7 @@ class AccountApiTest {
|
||||||
.asyncDispatch()
|
.asyncDispatch()
|
||||||
.andExpect { status { isFound() } }
|
.andExpect { status { isFound() } }
|
||||||
|
|
||||||
userQueryServiceImpl.findByNameAndDomain("api-test-user-1", "localhost")
|
userQueryServiceImpl.findByNameAndDomain("api-test-user-1", "example.com")
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
|
@ -108,7 +116,7 @@ class AccountApiTest {
|
||||||
.asyncDispatch()
|
.asyncDispatch()
|
||||||
.andExpect { status { isFound() } }
|
.andExpect { status { isFound() } }
|
||||||
|
|
||||||
userQueryServiceImpl.findByNameAndDomain("api-test-user-2", "localhost")
|
userQueryServiceImpl.findByNameAndDomain("api-test-user-2", "example.com")
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
|
@ -159,6 +167,120 @@ class AccountApiTest {
|
||||||
.andExpect { status { isForbidden() } }
|
.andExpect { status { isForbidden() } }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
@WithAnonymousUser
|
||||||
|
fun `apiV1AccountsIdGet 匿名でアカウント情報を取得できる`() {
|
||||||
|
mockMvc
|
||||||
|
.get("/api/v1/accounts/1")
|
||||||
|
.asyncDispatch()
|
||||||
|
.andExpect { status { isOk() } }
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `apiV1AccountsIdFollowPost write_follows権限でPOSTでフォローできる`() {
|
||||||
|
mockMvc
|
||||||
|
.post("/api/v1/accounts/2/follow") {
|
||||||
|
contentType = MediaType.APPLICATION_JSON
|
||||||
|
with(jwt().jwt { it.claim("uid", "1") }.authorities(SimpleGrantedAuthority("SCOPE_write:follows")))
|
||||||
|
}
|
||||||
|
.asyncDispatch()
|
||||||
|
.andExpect { status { isOk() } }
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `apiV1AccountsIdFollowPost write権限でPOSTでフォローできる`() {
|
||||||
|
mockMvc
|
||||||
|
.post("/api/v1/accounts/2/follow") {
|
||||||
|
contentType = MediaType.APPLICATION_JSON
|
||||||
|
with(jwt().jwt { it.claim("uid", "1") }.authorities(SimpleGrantedAuthority("SCOPE_write")))
|
||||||
|
}
|
||||||
|
.asyncDispatch()
|
||||||
|
.andExpect { status { isOk() } }
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `apiV1AccountsIdFollowPost read権限でだと403`() {
|
||||||
|
mockMvc
|
||||||
|
.post("/api/v1/accounts/2/follow") {
|
||||||
|
contentType = MediaType.APPLICATION_JSON
|
||||||
|
with(jwt().jwt { it.claim("uid", "1") }.authorities(SimpleGrantedAuthority("SCOPE_read")))
|
||||||
|
}
|
||||||
|
.andExpect { status { isForbidden() } }
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
@WithAnonymousUser
|
||||||
|
fun `apiV1AAccountsIdFollowPost 匿名だと401`() {
|
||||||
|
mockMvc
|
||||||
|
.post("/api/v1/accounts/2/follow") {
|
||||||
|
contentType = MediaType.APPLICATION_JSON
|
||||||
|
with(csrf())
|
||||||
|
}
|
||||||
|
.andExpect { status { isUnauthorized() } }
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
@WithAnonymousUser
|
||||||
|
fun `apiV1AAccountsIdFollowPost 匿名の場合通常csrfトークンは持ってないので403`() {
|
||||||
|
mockMvc
|
||||||
|
.post("/api/v1/accounts/2/follow") {
|
||||||
|
contentType = MediaType.APPLICATION_JSON
|
||||||
|
}
|
||||||
|
.andExpect { status { isForbidden() } }
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `apiV1AccountsRelationshipsGet 匿名だと401`() {
|
||||||
|
mockMvc
|
||||||
|
.get("/api/v1/accounts/relationships")
|
||||||
|
.andExpect { status { isUnauthorized() } }
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `apiV1AccountsRelationshipsGet read_follows権限を持っていたら取得できる`() {
|
||||||
|
mockMvc
|
||||||
|
.get("/api/v1/accounts/relationships") {
|
||||||
|
with(jwt().jwt { it.claim("uid", "1") }.authorities(SimpleGrantedAuthority("SCOPE_read:follows")))
|
||||||
|
}
|
||||||
|
.asyncDispatch()
|
||||||
|
.andExpect { status { isOk() } }
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `apiV1AccountsRelationshipsGet read権限を持っていたら取得できる`() {
|
||||||
|
mockMvc
|
||||||
|
.get("/api/v1/accounts/relationships") {
|
||||||
|
with(jwt().jwt { it.claim("uid", "1") }.authorities(SimpleGrantedAuthority("SCOPE_read")))
|
||||||
|
}
|
||||||
|
.asyncDispatch()
|
||||||
|
.andExpect { status { isOk() } }
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `apiV1AccountsRelationshipsGet write権限だと403`() {
|
||||||
|
mockMvc
|
||||||
|
.get("/api/v1/accounts/relationships") {
|
||||||
|
with(jwt().jwt { it.claim("uid", "1") }.authorities(SimpleGrantedAuthority("SCOPE_write")))
|
||||||
|
}
|
||||||
|
.andExpect { status { isForbidden() } }
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
@Sql("/sql/accounts/apiV1AccountsIdFollowPost フォローできる.sql")
|
||||||
|
fun `apiV1AccountsIdFollowPost フォローできる`() = runTest {
|
||||||
|
mockMvc
|
||||||
|
.post("/api/v1/accounts/3733363/follow") {
|
||||||
|
contentType = MediaType.APPLICATION_JSON
|
||||||
|
with(jwt().jwt { it.claim("uid", "37335363") }.authorities(SimpleGrantedAuthority("SCOPE_write")))
|
||||||
|
}
|
||||||
|
.asyncDispatch()
|
||||||
|
.andExpect { status { isOk() } }
|
||||||
|
|
||||||
|
val alreadyFollow = followerQueryServiceImpl.alreadyFollow(3733363, 37335363)
|
||||||
|
|
||||||
|
assertThat(alreadyFollow).isTrue()
|
||||||
|
}
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
@JvmStatic
|
@JvmStatic
|
||||||
@AfterAll
|
@AfterAll
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
hideout:
|
hideout:
|
||||||
url: "https://localhost:8080"
|
url: "https://example.com"
|
||||||
use-mongodb: true
|
use-mongodb: true
|
||||||
security:
|
security:
|
||||||
jwt:
|
jwt:
|
||||||
|
|
|
@ -0,0 +1,18 @@
|
||||||
|
insert into "USERS" (id, name, domain, screen_name, description, password, inbox, outbox, url, public_key, private_key,
|
||||||
|
created_at, key_id, following, followers, instance)
|
||||||
|
VALUES (3733363, 'follow-test-user-1', 'example.com', 'follow-test-user-1-name', '',
|
||||||
|
'5e884898da28047151d0e56f8dc6292773603d0d6aabbdd62a11ef721d1542d8',
|
||||||
|
'https://example.com/users/follow-test-user-1/inbox',
|
||||||
|
'https://example.com/users/follow-test-user-1/outbox', 'https://example.com/users/follow-test-user-1',
|
||||||
|
'-----BEGIN PUBLIC KEY-----...-----END PUBLIC KEY-----',
|
||||||
|
'-----BEGIN PRIVATE KEY-----...-----END PRIVATE KEY-----', 12345678,
|
||||||
|
'https://example.com/users/follow-test-user-1#pubkey', 'https://example.com/users/follow-test-user-1/following',
|
||||||
|
'https://example.com/users/follow-test-user-1/followers', null),
|
||||||
|
(37335363, 'follow-test-user-2', 'example.com', 'follow-test-user-2-name', '',
|
||||||
|
'5e884898da28047151d0e56f8dc6292773603d0d6aabbdd62a11ef721d1542d8',
|
||||||
|
'https://example.com/users/follow-test-user-2/inbox',
|
||||||
|
'https://example.com/users/follow-test-user-2/outbox', 'https://example.com/users/follow-test-user-2',
|
||||||
|
'-----BEGIN PUBLIC KEY-----...-----END PUBLIC KEY-----',
|
||||||
|
'-----BEGIN PRIVATE KEY-----...-----END PRIVATE KEY-----', 12345678,
|
||||||
|
'https://example.com/users/follow-test-user-2#pubkey', 'https://example.com/users/follow-test-user-2/following',
|
||||||
|
'https://example.com/users/follow-test-user-2/followers', null);
|
|
@ -0,0 +1,10 @@
|
||||||
|
insert into "USERS" (ID, NAME, DOMAIN, SCREEN_NAME, DESCRIPTION, PASSWORD, INBOX, OUTBOX, URL, PUBLIC_KEY, PRIVATE_KEY,
|
||||||
|
CREATED_AT, KEY_ID, FOLLOWING, FOLLOWERS, INSTANCE)
|
||||||
|
VALUES (2, 'test-user2', 'example.com', 'Im test user.', 'THis account is test user.',
|
||||||
|
'5e884898da28047151d0e56f8dc6292773603d0d6aabbdd62a11ef721d1542d8',
|
||||||
|
'https://example.com/users/test-user2/inbox',
|
||||||
|
'https://example.com/users/test-user2/outbox', 'https://example.com/users/test-user2',
|
||||||
|
'-----BEGIN PUBLIC KEY-----...-----END PUBLIC KEY-----',
|
||||||
|
'-----BEGIN PRIVATE KEY-----...-----END PRIVATE KEY-----', 12345678,
|
||||||
|
'https://example.com/users/test-user2#pubkey', 'https://example.com/users/test-user2/following',
|
||||||
|
'https://example.com/users/test-user2s/followers', null);
|
|
@ -1,9 +1,12 @@
|
||||||
package dev.usbharu.hideout.activitypub.domain.model
|
package dev.usbharu.hideout.activitypub.domain.model
|
||||||
|
|
||||||
|
import com.fasterxml.jackson.annotation.JsonSetter
|
||||||
|
import com.fasterxml.jackson.annotation.Nulls
|
||||||
import dev.usbharu.hideout.activitypub.domain.model.objects.Object
|
import dev.usbharu.hideout.activitypub.domain.model.objects.Object
|
||||||
|
|
||||||
open class Document(
|
open class Document(
|
||||||
type: List<String> = emptyList(),
|
type: List<String> = emptyList(),
|
||||||
|
@JsonSetter(nulls = Nulls.AS_EMPTY)
|
||||||
override val name: String = "",
|
override val name: String = "",
|
||||||
val mediaType: String,
|
val mediaType: String,
|
||||||
val url: String
|
val url: String
|
||||||
|
|
|
@ -4,12 +4,11 @@ import dev.usbharu.hideout.activitypub.domain.model.objects.Object
|
||||||
|
|
||||||
open class Image(
|
open class Image(
|
||||||
type: List<String> = emptyList(),
|
type: List<String> = emptyList(),
|
||||||
val mediaType: String,
|
val mediaType: String? = null,
|
||||||
val url: String
|
val url: String
|
||||||
) : Object(
|
) : Object(
|
||||||
add(type, "Image")
|
add(type, "Image")
|
||||||
) {
|
) {
|
||||||
|
|
||||||
override fun equals(other: Any?): Boolean {
|
override fun equals(other: Any?): Boolean {
|
||||||
if (this === other) return true
|
if (this === other) return true
|
||||||
if (javaClass != other?.javaClass) return false
|
if (javaClass != other?.javaClass) return false
|
||||||
|
@ -25,10 +24,16 @@ open class Image(
|
||||||
|
|
||||||
override fun hashCode(): Int {
|
override fun hashCode(): Int {
|
||||||
var result = super.hashCode()
|
var result = super.hashCode()
|
||||||
result = 31 * result + mediaType.hashCode()
|
result = 31 * result + (mediaType?.hashCode() ?: 0)
|
||||||
result = 31 * result + url.hashCode()
|
result = 31 * result + url.hashCode()
|
||||||
return result
|
return result
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun toString(): String = "Image(mediaType=$mediaType, url=$url) ${super.toString()}"
|
override fun toString(): String {
|
||||||
|
return "Image(" +
|
||||||
|
"mediaType=$mediaType, " +
|
||||||
|
"url='$url'" +
|
||||||
|
")" +
|
||||||
|
" ${super.toString()}"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -84,7 +84,7 @@ class APRequestServiceImpl(
|
||||||
headers {
|
headers {
|
||||||
appendAll(headers)
|
appendAll(headers)
|
||||||
append("Signature", sign.signatureHeader)
|
append("Signature", sign.signatureHeader)
|
||||||
remove("Host")
|
// remove("Host")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
contentType(Activity)
|
contentType(Activity)
|
||||||
|
@ -173,7 +173,7 @@ class APRequestServiceImpl(
|
||||||
append("Accept", Activity)
|
append("Accept", Activity)
|
||||||
append("Date", date)
|
append("Date", date)
|
||||||
append("Host", u.host)
|
append("Host", u.host)
|
||||||
append("Digest", "sha-256=$digest")
|
append("Digest", "SHA-256=$digest")
|
||||||
}
|
}
|
||||||
|
|
||||||
val sign = httpSignatureSigner.sign(
|
val sign = httpSignatureSigner.sign(
|
||||||
|
@ -193,7 +193,7 @@ class APRequestServiceImpl(
|
||||||
headers {
|
headers {
|
||||||
appendAll(headers)
|
appendAll(headers)
|
||||||
append("Signature", sign.signatureHeader)
|
append("Signature", sign.signatureHeader)
|
||||||
remove("Host")
|
// remove("Host")
|
||||||
}
|
}
|
||||||
setBody(requestBody)
|
setBody(requestBody)
|
||||||
contentType(Activity)
|
contentType(Activity)
|
||||||
|
|
|
@ -99,7 +99,6 @@ class InboxJobProcessor(
|
||||||
|
|
||||||
val verify = signature?.let { verifyHttpSignature(httpRequest, it, transaction) } ?: false
|
val verify = signature?.let { verifyHttpSignature(httpRequest, it, transaction) } ?: false
|
||||||
|
|
||||||
transaction.transaction {
|
|
||||||
logger.debug("Is verifying success? {}", verify)
|
logger.debug("Is verifying success? {}", verify)
|
||||||
|
|
||||||
val activityPubProcessor =
|
val activityPubProcessor =
|
||||||
|
@ -115,7 +114,6 @@ class InboxJobProcessor(
|
||||||
|
|
||||||
logger.info("SUCCESS Process inbox. type: {}", param.type)
|
logger.info("SUCCESS Process inbox. type: {}", param.type)
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
override fun job(): InboxJob = InboxJob
|
override fun job(): InboxJob = InboxJob
|
||||||
|
|
||||||
|
|
|
@ -58,8 +58,8 @@ class APUserServiceImpl(
|
||||||
url = userUrl,
|
url = userUrl,
|
||||||
icon = Image(
|
icon = Image(
|
||||||
type = emptyList(),
|
type = emptyList(),
|
||||||
mediaType = "image/png",
|
mediaType = "image/jpeg",
|
||||||
url = "$userUrl/icon.png"
|
url = "$userUrl/icon.jpg"
|
||||||
),
|
),
|
||||||
publicKey = Key(
|
publicKey = Key(
|
||||||
id = userEntity.keyId,
|
id = userEntity.keyId,
|
||||||
|
@ -124,8 +124,8 @@ class APUserServiceImpl(
|
||||||
url = id,
|
url = id,
|
||||||
icon = Image(
|
icon = Image(
|
||||||
type = emptyList(),
|
type = emptyList(),
|
||||||
mediaType = "image/png",
|
mediaType = "image/jpeg",
|
||||||
url = "$id/icon.png"
|
url = "$id/icon.jpg"
|
||||||
),
|
),
|
||||||
publicKey = Key(
|
publicKey = Key(
|
||||||
id = userEntity.keyId,
|
id = userEntity.keyId,
|
||||||
|
|
|
@ -180,17 +180,23 @@ class SecurityConfig {
|
||||||
|
|
||||||
authorize(POST, "/inbox", permitAll)
|
authorize(POST, "/inbox", permitAll)
|
||||||
authorize(POST, "/users/*/inbox", permitAll)
|
authorize(POST, "/users/*/inbox", permitAll)
|
||||||
|
authorize(GET, "/users/*", permitAll)
|
||||||
|
authorize(GET, "/users/*/posts/*", permitAll)
|
||||||
|
|
||||||
authorize(POST, "/api/v1/apps", permitAll)
|
authorize(POST, "/api/v1/apps", permitAll)
|
||||||
authorize(GET, "/api/v1/instance/**", permitAll)
|
authorize(GET, "/api/v1/instance/**", permitAll)
|
||||||
authorize(POST, "/api/v1/accounts", permitAll)
|
authorize(POST, "/api/v1/accounts", permitAll)
|
||||||
|
|
||||||
authorize("/auth/sign_up", hasRole("ANONYMOUS"))
|
authorize("/auth/sign_up", hasRole("ANONYMOUS"))
|
||||||
authorize(GET, "/files", permitAll)
|
authorize(GET, "/files/*", permitAll)
|
||||||
authorize(GET, "/users/*/icon.jpg", permitAll)
|
authorize(GET, "/users/*/icon.jpg", permitAll)
|
||||||
authorize(GET, "/users/*/header.jpg", permitAll)
|
authorize(GET, "/users/*/header.jpg", permitAll)
|
||||||
|
|
||||||
authorize(GET, "/api/v1/accounts/verify_credentials", hasAnyScope("read", "read:accounts"))
|
authorize(GET, "/api/v1/accounts/verify_credentials", hasAnyScope("read", "read:accounts"))
|
||||||
|
authorize(GET, "/api/v1/accounts/relationships", hasAnyScope("read", "read:follows"))
|
||||||
|
authorize(GET, "/api/v1/accounts/*", permitAll)
|
||||||
|
authorize(GET, "/api/v1/accounts/*/statuses", permitAll)
|
||||||
|
authorize(POST, "/api/v1/accounts/*/follow", hasAnyScope("write", "write:follows"))
|
||||||
|
|
||||||
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"))
|
||||||
|
|
|
@ -2,19 +2,24 @@ package dev.usbharu.hideout.application.infrastructure.exposed
|
||||||
|
|
||||||
import dev.usbharu.hideout.application.external.Transaction
|
import dev.usbharu.hideout.application.external.Transaction
|
||||||
import kotlinx.coroutines.slf4j.MDCContext
|
import kotlinx.coroutines.slf4j.MDCContext
|
||||||
|
import org.jetbrains.exposed.sql.StdOutSqlLogger
|
||||||
|
import org.jetbrains.exposed.sql.addLogger
|
||||||
import org.jetbrains.exposed.sql.transactions.experimental.newSuspendedTransaction
|
import org.jetbrains.exposed.sql.transactions.experimental.newSuspendedTransaction
|
||||||
import org.springframework.stereotype.Service
|
import org.springframework.stereotype.Service
|
||||||
|
import java.sql.Connection
|
||||||
|
|
||||||
@Service
|
@Service
|
||||||
class ExposedTransaction : Transaction {
|
class ExposedTransaction : Transaction {
|
||||||
override suspend fun <T> transaction(block: suspend () -> T): T {
|
override suspend fun <T> transaction(block: suspend () -> T): T {
|
||||||
return newSuspendedTransaction(MDCContext()) {
|
return newSuspendedTransaction(MDCContext(), transactionIsolation = Connection.TRANSACTION_SERIALIZABLE) {
|
||||||
|
addLogger(StdOutSqlLogger)
|
||||||
block()
|
block()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
override suspend fun <T> transaction(transactionLevel: Int, block: suspend () -> T): T {
|
override suspend fun <T> transaction(transactionLevel: Int, block: suspend () -> T): T {
|
||||||
return newSuspendedTransaction(MDCContext(), transactionIsolation = transactionLevel) {
|
return newSuspendedTransaction(MDCContext(), transactionIsolation = transactionLevel) {
|
||||||
|
addLogger(StdOutSqlLogger)
|
||||||
block()
|
block()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -8,6 +8,7 @@ import kjob.core.dsl.JobRegisterContext
|
||||||
import kjob.core.dsl.KJobFunctions
|
import kjob.core.dsl.KJobFunctions
|
||||||
import kjob.core.kjob
|
import kjob.core.kjob
|
||||||
import org.jetbrains.exposed.sql.transactions.TransactionManager
|
import org.jetbrains.exposed.sql.transactions.TransactionManager
|
||||||
|
import org.slf4j.MDC
|
||||||
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty
|
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty
|
||||||
import org.springframework.stereotype.Service
|
import org.springframework.stereotype.Service
|
||||||
|
|
||||||
|
@ -35,8 +36,13 @@ class KJobJobQueueWorkerService(private val jobQueueProcessorList: List<JobProce
|
||||||
for (jobProcessor in jobQueueProcessorList) {
|
for (jobProcessor in jobQueueProcessorList) {
|
||||||
kjob.register(jobProcessor.job()) {
|
kjob.register(jobProcessor.job()) {
|
||||||
execute {
|
execute {
|
||||||
|
try {
|
||||||
|
MDC.put("x-job-id", this.jobId)
|
||||||
val param = it.convertUnsafe(props)
|
val param = it.convertUnsafe(props)
|
||||||
jobProcessor.process(param)
|
jobProcessor.process(param)
|
||||||
|
} finally {
|
||||||
|
MDC.remove("x-job-id")
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -8,9 +8,7 @@ import jakarta.servlet.http.HttpServletRequest
|
||||||
import org.springframework.security.web.authentication.preauth.AbstractPreAuthenticatedProcessingFilter
|
import org.springframework.security.web.authentication.preauth.AbstractPreAuthenticatedProcessingFilter
|
||||||
import java.net.URL
|
import java.net.URL
|
||||||
|
|
||||||
class HttpSignatureFilter(
|
class HttpSignatureFilter(private val httpSignatureHeaderParser: SignatureHeaderParser) :
|
||||||
private val httpSignatureHeaderParser: SignatureHeaderParser
|
|
||||||
) :
|
|
||||||
AbstractPreAuthenticatedProcessingFilter() {
|
AbstractPreAuthenticatedProcessingFilter() {
|
||||||
override fun getPreAuthenticatedPrincipal(request: HttpServletRequest?): Any? {
|
override fun getPreAuthenticatedPrincipal(request: HttpServletRequest?): Any? {
|
||||||
val headersList = request?.headerNames?.toList().orEmpty()
|
val headersList = request?.headerNames?.toList().orEmpty()
|
||||||
|
|
|
@ -114,10 +114,13 @@ class UserServiceImpl(
|
||||||
}
|
}
|
||||||
|
|
||||||
override suspend fun follow(id: Long, followerId: Long) {
|
override suspend fun follow(id: Long, followerId: Long) {
|
||||||
|
logger.debug("START Follow id: {} → target: {}", followerId, id)
|
||||||
followerQueryService.appendFollower(id, followerId)
|
followerQueryService.appendFollower(id, followerId)
|
||||||
if (userRepository.findFollowRequestsById(id, followerId)) {
|
if (userRepository.findFollowRequestsById(id, followerId)) {
|
||||||
|
logger.debug("Follow request is accepted! ")
|
||||||
userRepository.deleteFollowRequest(id, followerId)
|
userRepository.deleteFollowRequest(id, followerId)
|
||||||
}
|
}
|
||||||
|
logger.debug("SUCCESS Follow id: {} → target: {}", followerId, id)
|
||||||
}
|
}
|
||||||
|
|
||||||
override suspend fun unfollow(id: Long, followerId: Long): Boolean {
|
override suspend fun unfollow(id: Long, followerId: Long): Boolean {
|
||||||
|
|
|
@ -4,9 +4,11 @@ import dev.usbharu.hideout.core.domain.model.media.toMediaAttachments
|
||||||
import dev.usbharu.hideout.core.infrastructure.exposedrepository.*
|
import dev.usbharu.hideout.core.infrastructure.exposedrepository.*
|
||||||
import dev.usbharu.hideout.domain.mastodon.model.generated.Account
|
import dev.usbharu.hideout.domain.mastodon.model.generated.Account
|
||||||
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.mastodon.interfaces.api.status.StatusQuery
|
import dev.usbharu.hideout.mastodon.interfaces.api.status.StatusQuery
|
||||||
import dev.usbharu.hideout.mastodon.query.StatusQueryService
|
import dev.usbharu.hideout.mastodon.query.StatusQueryService
|
||||||
import org.jetbrains.exposed.sql.ResultRow
|
import org.jetbrains.exposed.sql.ResultRow
|
||||||
|
import org.jetbrains.exposed.sql.andWhere
|
||||||
import org.jetbrains.exposed.sql.select
|
import org.jetbrains.exposed.sql.select
|
||||||
import org.springframework.stereotype.Repository
|
import org.springframework.stereotype.Repository
|
||||||
import java.time.Instant
|
import java.time.Instant
|
||||||
|
@ -40,6 +42,62 @@ class StatusQueryServiceImpl : StatusQueryService {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
override suspend fun accountsStatus(
|
||||||
|
accountId: Long,
|
||||||
|
maxId: Long?,
|
||||||
|
sinceId: Long?,
|
||||||
|
minId: Long?,
|
||||||
|
limit: Int,
|
||||||
|
onlyMedia: Boolean,
|
||||||
|
excludeReplies: Boolean,
|
||||||
|
excludeReblogs: Boolean,
|
||||||
|
pinned: Boolean,
|
||||||
|
tagged: String?,
|
||||||
|
includeFollowers: Boolean
|
||||||
|
): List<Status> {
|
||||||
|
val query = Posts
|
||||||
|
.leftJoin(PostsMedia)
|
||||||
|
.leftJoin(Users)
|
||||||
|
.leftJoin(Media)
|
||||||
|
.select { Posts.userId eq accountId }.limit(20)
|
||||||
|
|
||||||
|
if (maxId != null) {
|
||||||
|
query.andWhere { Posts.id eq maxId }
|
||||||
|
}
|
||||||
|
if (sinceId != null) {
|
||||||
|
query.andWhere { Posts.id eq sinceId }
|
||||||
|
}
|
||||||
|
if (minId != null) {
|
||||||
|
query.andWhere { Posts.id eq minId }
|
||||||
|
}
|
||||||
|
if (onlyMedia) {
|
||||||
|
query.andWhere { PostsMedia.mediaId.isNotNull() }
|
||||||
|
}
|
||||||
|
if (excludeReplies) {
|
||||||
|
query.andWhere { Posts.replyId.isNotNull() }
|
||||||
|
}
|
||||||
|
if (excludeReblogs) {
|
||||||
|
query.andWhere { Posts.repostId.isNotNull() }
|
||||||
|
}
|
||||||
|
if (includeFollowers) {
|
||||||
|
query.andWhere { Posts.visibility inList listOf(public.ordinal, unlisted.ordinal, private.ordinal) }
|
||||||
|
} else {
|
||||||
|
query.andWhere { Posts.visibility inList listOf(public.ordinal, unlisted.ordinal) }
|
||||||
|
}
|
||||||
|
|
||||||
|
val pairs = query.groupBy { it[Posts.id] }
|
||||||
|
.map { it.value }
|
||||||
|
.map {
|
||||||
|
toStatus(it.first()).copy(
|
||||||
|
mediaAttachments = it.mapNotNull { resultRow ->
|
||||||
|
resultRow.toMediaOrNull()?.toMediaAttachments()
|
||||||
|
}
|
||||||
|
) to it.first()[Posts.repostId]
|
||||||
|
}
|
||||||
|
|
||||||
|
return resolveReplyAndRepost(pairs)
|
||||||
|
}
|
||||||
|
|
||||||
private fun resolveReplyAndRepost(pairs: List<Pair<Status, Long?>>): List<Status> {
|
private fun resolveReplyAndRepost(pairs: List<Pair<Status, Long?>>): List<Status> {
|
||||||
val statuses = pairs.map { it.first }
|
val statuses = pairs.map { it.first }
|
||||||
return pairs
|
return pairs
|
||||||
|
@ -111,11 +169,11 @@ private fun toStatus(it: ResultRow) = Status(
|
||||||
),
|
),
|
||||||
content = it[Posts.text],
|
content = it[Posts.text],
|
||||||
visibility = when (it[Posts.visibility]) {
|
visibility = when (it[Posts.visibility]) {
|
||||||
0 -> Status.Visibility.public
|
0 -> public
|
||||||
1 -> Status.Visibility.unlisted
|
1 -> unlisted
|
||||||
2 -> Status.Visibility.private
|
2 -> private
|
||||||
3 -> Status.Visibility.direct
|
3 -> direct
|
||||||
else -> Status.Visibility.public
|
else -> public
|
||||||
},
|
},
|
||||||
sensitive = it[Posts.sensitive],
|
sensitive = it[Posts.sensitive],
|
||||||
spoilerText = it[Posts.overview].orEmpty(),
|
spoilerText = it[Posts.overview].orEmpty(),
|
||||||
|
|
|
@ -3,8 +3,11 @@ package dev.usbharu.hideout.mastodon.interfaces.api.account
|
||||||
import dev.usbharu.hideout.application.external.Transaction
|
import dev.usbharu.hideout.application.external.Transaction
|
||||||
import dev.usbharu.hideout.controller.mastodon.generated.AccountApi
|
import dev.usbharu.hideout.controller.mastodon.generated.AccountApi
|
||||||
import dev.usbharu.hideout.core.service.user.UserCreateDto
|
import dev.usbharu.hideout.core.service.user.UserCreateDto
|
||||||
import dev.usbharu.hideout.domain.mastodon.model.generated.CredentialAccount
|
import dev.usbharu.hideout.domain.mastodon.model.generated.*
|
||||||
import dev.usbharu.hideout.mastodon.service.account.AccountApiService
|
import dev.usbharu.hideout.mastodon.service.account.AccountApiService
|
||||||
|
import kotlinx.coroutines.flow.Flow
|
||||||
|
import kotlinx.coroutines.flow.asFlow
|
||||||
|
import kotlinx.coroutines.runBlocking
|
||||||
import org.springframework.http.HttpHeaders
|
import org.springframework.http.HttpHeaders
|
||||||
import org.springframework.http.HttpStatus
|
import org.springframework.http.HttpStatus
|
||||||
import org.springframework.http.ResponseEntity
|
import org.springframework.http.ResponseEntity
|
||||||
|
@ -18,6 +21,19 @@ class MastodonAccountApiController(
|
||||||
private val accountApiService: AccountApiService,
|
private val accountApiService: AccountApiService,
|
||||||
private val transaction: Transaction
|
private val transaction: Transaction
|
||||||
) : AccountApi {
|
) : AccountApi {
|
||||||
|
|
||||||
|
override suspend fun apiV1AccountsIdFollowPost(
|
||||||
|
id: String,
|
||||||
|
followRequestBody: FollowRequestBody?
|
||||||
|
): ResponseEntity<Relationship> {
|
||||||
|
val principal = SecurityContextHolder.getContext().getAuthentication().principal as Jwt
|
||||||
|
|
||||||
|
return ResponseEntity.ok(accountApiService.follow(principal.getClaim<String>("uid").toLong(), id.toLong()))
|
||||||
|
}
|
||||||
|
|
||||||
|
override suspend fun apiV1AccountsIdGet(id: String): ResponseEntity<Account> =
|
||||||
|
ResponseEntity.ok(accountApiService.account(id.toLong()))
|
||||||
|
|
||||||
override suspend fun apiV1AccountsVerifyCredentialsGet(): ResponseEntity<CredentialAccount> {
|
override suspend fun apiV1AccountsVerifyCredentialsGet(): ResponseEntity<CredentialAccount> {
|
||||||
val principal = SecurityContextHolder.getContext().getAuthentication().principal as Jwt
|
val principal = SecurityContextHolder.getContext().getAuthentication().principal as Jwt
|
||||||
|
|
||||||
|
@ -42,4 +58,49 @@ class MastodonAccountApiController(
|
||||||
httpHeaders.location = URI("/users/$username")
|
httpHeaders.location = URI("/users/$username")
|
||||||
return ResponseEntity(Unit, httpHeaders, HttpStatus.FOUND)
|
return ResponseEntity(Unit, httpHeaders, HttpStatus.FOUND)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
override fun apiV1AccountsIdStatusesGet(
|
||||||
|
id: String,
|
||||||
|
maxId: String?,
|
||||||
|
sinceId: String?,
|
||||||
|
minId: String?,
|
||||||
|
limit: Int,
|
||||||
|
onlyMedia: Boolean,
|
||||||
|
excludeReplies: Boolean,
|
||||||
|
excludeReblogs: Boolean,
|
||||||
|
pinned: Boolean,
|
||||||
|
tagged: String?
|
||||||
|
): ResponseEntity<Flow<Status>> = runBlocking {
|
||||||
|
val principal = SecurityContextHolder.getContext().getAuthentication().principal as Jwt
|
||||||
|
|
||||||
|
val userid = principal.getClaim<String>("uid").toLong()
|
||||||
|
val statusFlow = accountApiService.accountsStatuses(
|
||||||
|
id.toLong(),
|
||||||
|
maxId?.toLongOrNull(),
|
||||||
|
sinceId?.toLongOrNull(),
|
||||||
|
minId?.toLongOrNull(),
|
||||||
|
limit,
|
||||||
|
onlyMedia,
|
||||||
|
excludeReplies,
|
||||||
|
excludeReblogs,
|
||||||
|
pinned,
|
||||||
|
tagged,
|
||||||
|
userid
|
||||||
|
).asFlow()
|
||||||
|
ResponseEntity.ok(statusFlow)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun apiV1AccountsRelationshipsGet(
|
||||||
|
id: List<String>?,
|
||||||
|
withSuspended: Boolean
|
||||||
|
): ResponseEntity<Flow<Relationship>> = runBlocking {
|
||||||
|
val principal = SecurityContextHolder.getContext().getAuthentication().principal as Jwt
|
||||||
|
|
||||||
|
val userid = principal.getClaim<String>("uid").toLong()
|
||||||
|
|
||||||
|
ResponseEntity.ok(
|
||||||
|
accountApiService.relationships(userid, id.orEmpty().mapNotNull { it.toLongOrNull() }, withSuspended)
|
||||||
|
.asFlow()
|
||||||
|
)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -6,4 +6,34 @@ import dev.usbharu.hideout.mastodon.interfaces.api.status.StatusQuery
|
||||||
interface StatusQueryService {
|
interface StatusQueryService {
|
||||||
suspend fun findByPostIds(ids: List<Long>): List<Status>
|
suspend fun findByPostIds(ids: List<Long>): List<Status>
|
||||||
suspend fun findByPostIdsWithMediaIds(statusQueries: List<StatusQuery>): List<Status>
|
suspend fun findByPostIdsWithMediaIds(statusQueries: List<StatusQuery>): List<Status>
|
||||||
|
|
||||||
|
/**
|
||||||
|
* アカウントの投稿一覧を取得します
|
||||||
|
*
|
||||||
|
* @param accountId 対象アカウントのid
|
||||||
|
* @param maxId 投稿の最大id
|
||||||
|
* @param sinceId 投稿の最小id
|
||||||
|
* @param minId 不明
|
||||||
|
* @param limit 投稿の最大件数
|
||||||
|
* @param onlyMedia メディア付き投稿のみ
|
||||||
|
* @param excludeReplies 返信を除外
|
||||||
|
* @param excludeReblogs リブログを除外
|
||||||
|
* @param pinned ピン止め投稿のみ
|
||||||
|
* @param tagged タグ付き?
|
||||||
|
* @param includeFollowers フォロワー限定投稿を含める
|
||||||
|
*/
|
||||||
|
@Suppress("LongParameterList")
|
||||||
|
suspend fun accountsStatus(
|
||||||
|
accountId: Long,
|
||||||
|
maxId: Long? = null,
|
||||||
|
sinceId: Long? = null,
|
||||||
|
minId: Long? = null,
|
||||||
|
limit: Int,
|
||||||
|
onlyMedia: Boolean = false,
|
||||||
|
excludeReplies: Boolean = false,
|
||||||
|
excludeReblogs: Boolean = false,
|
||||||
|
pinned: Boolean = false,
|
||||||
|
tagged: String? = null,
|
||||||
|
includeFollowers: Boolean = false
|
||||||
|
): List<Status>
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,27 +1,87 @@
|
||||||
package dev.usbharu.hideout.mastodon.service.account
|
package dev.usbharu.hideout.mastodon.service.account
|
||||||
|
|
||||||
import dev.usbharu.hideout.application.external.Transaction
|
import dev.usbharu.hideout.application.external.Transaction
|
||||||
|
import dev.usbharu.hideout.core.domain.model.user.UserRepository
|
||||||
|
import dev.usbharu.hideout.core.query.FollowerQueryService
|
||||||
import dev.usbharu.hideout.core.service.user.UserCreateDto
|
import dev.usbharu.hideout.core.service.user.UserCreateDto
|
||||||
import dev.usbharu.hideout.core.service.user.UserService
|
import dev.usbharu.hideout.core.service.user.UserService
|
||||||
import dev.usbharu.hideout.domain.mastodon.model.generated.Account
|
import dev.usbharu.hideout.domain.mastodon.model.generated.*
|
||||||
import dev.usbharu.hideout.domain.mastodon.model.generated.CredentialAccount
|
import dev.usbharu.hideout.mastodon.query.StatusQueryService
|
||||||
import dev.usbharu.hideout.domain.mastodon.model.generated.CredentialAccountSource
|
import org.slf4j.LoggerFactory
|
||||||
import dev.usbharu.hideout.domain.mastodon.model.generated.Role
|
|
||||||
import org.springframework.stereotype.Service
|
import org.springframework.stereotype.Service
|
||||||
|
import kotlin.math.min
|
||||||
|
|
||||||
@Service
|
@Service
|
||||||
interface AccountApiService {
|
interface AccountApiService {
|
||||||
|
suspend fun accountsStatuses(
|
||||||
|
userid: Long,
|
||||||
|
maxId: Long?,
|
||||||
|
sinceId: Long?,
|
||||||
|
minId: Long?,
|
||||||
|
limit: Int,
|
||||||
|
onlyMedia: Boolean,
|
||||||
|
excludeReplies: Boolean,
|
||||||
|
excludeReblogs: Boolean,
|
||||||
|
pinned: Boolean,
|
||||||
|
tagged: String?,
|
||||||
|
loginUser: Long?
|
||||||
|
): List<Status>
|
||||||
|
|
||||||
suspend fun verifyCredentials(userid: Long): CredentialAccount
|
suspend fun verifyCredentials(userid: Long): CredentialAccount
|
||||||
suspend fun registerAccount(userCreateDto: UserCreateDto): Unit
|
suspend fun registerAccount(userCreateDto: UserCreateDto): Unit
|
||||||
|
suspend fun follow(userid: Long, followeeId: Long): Relationship
|
||||||
|
suspend fun account(id: Long): Account
|
||||||
|
suspend fun relationships(userid: Long, id: List<Long>, withSuspended: Boolean): List<Relationship>
|
||||||
}
|
}
|
||||||
|
|
||||||
@Service
|
@Service
|
||||||
class AccountApiServiceImpl(
|
class AccountApiServiceImpl(
|
||||||
private val accountService: AccountService,
|
private val accountService: AccountService,
|
||||||
private val transaction: Transaction,
|
private val transaction: Transaction,
|
||||||
private val userService: UserService
|
private val userService: UserService,
|
||||||
|
private val followerQueryService: FollowerQueryService,
|
||||||
|
private val userRepository: UserRepository,
|
||||||
|
private val statusQueryService: StatusQueryService
|
||||||
) :
|
) :
|
||||||
AccountApiService {
|
AccountApiService {
|
||||||
|
override suspend fun accountsStatuses(
|
||||||
|
userid: Long,
|
||||||
|
maxId: Long?,
|
||||||
|
sinceId: Long?,
|
||||||
|
minId: Long?,
|
||||||
|
limit: Int,
|
||||||
|
onlyMedia: Boolean,
|
||||||
|
excludeReplies: Boolean,
|
||||||
|
excludeReblogs: Boolean,
|
||||||
|
pinned: Boolean,
|
||||||
|
tagged: String?,
|
||||||
|
loginUser: Long?
|
||||||
|
): List<Status> {
|
||||||
|
val canViewFollowers = if (loginUser == null) {
|
||||||
|
false
|
||||||
|
} else {
|
||||||
|
transaction.transaction {
|
||||||
|
followerQueryService.alreadyFollow(userid, loginUser)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return transaction.transaction {
|
||||||
|
statusQueryService.accountsStatus(
|
||||||
|
userid,
|
||||||
|
maxId,
|
||||||
|
sinceId,
|
||||||
|
minId,
|
||||||
|
limit,
|
||||||
|
onlyMedia,
|
||||||
|
excludeReplies,
|
||||||
|
excludeReblogs,
|
||||||
|
pinned,
|
||||||
|
tagged,
|
||||||
|
canViewFollowers
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
override suspend fun verifyCredentials(userid: Long): CredentialAccount = transaction.transaction {
|
override suspend fun verifyCredentials(userid: Long): CredentialAccount = transaction.transaction {
|
||||||
val account = accountService.findById(userid)
|
val account = accountService.findById(userid)
|
||||||
from(account)
|
from(account)
|
||||||
|
@ -31,6 +91,75 @@ class AccountApiServiceImpl(
|
||||||
userService.createLocalUser(UserCreateDto(userCreateDto.name, userCreateDto.name, "", userCreateDto.password))
|
userService.createLocalUser(UserCreateDto(userCreateDto.name, userCreateDto.name, "", userCreateDto.password))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
override suspend fun follow(userid: Long, followeeId: Long): Relationship = transaction.transaction {
|
||||||
|
val alreadyFollow = followerQueryService.alreadyFollow(followeeId, userid)
|
||||||
|
|
||||||
|
val followRequest = if (alreadyFollow) {
|
||||||
|
true
|
||||||
|
} else {
|
||||||
|
userService.followRequest(followeeId, userid)
|
||||||
|
}
|
||||||
|
|
||||||
|
val alreadyFollow1 = followerQueryService.alreadyFollow(userid, followeeId)
|
||||||
|
|
||||||
|
val followRequestsById = userRepository.findFollowRequestsById(followeeId, userid)
|
||||||
|
|
||||||
|
return@transaction Relationship(
|
||||||
|
followeeId.toString(),
|
||||||
|
followRequest,
|
||||||
|
true,
|
||||||
|
false,
|
||||||
|
alreadyFollow1,
|
||||||
|
false,
|
||||||
|
false,
|
||||||
|
false,
|
||||||
|
false,
|
||||||
|
followRequestsById,
|
||||||
|
false,
|
||||||
|
false,
|
||||||
|
""
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
override suspend fun account(id: Long): Account = transaction.transaction {
|
||||||
|
return@transaction accountService.findById(id)
|
||||||
|
}
|
||||||
|
|
||||||
|
override suspend fun relationships(userid: Long, id: List<Long>, withSuspended: Boolean): List<Relationship> =
|
||||||
|
transaction.transaction {
|
||||||
|
if (id.isEmpty()) {
|
||||||
|
return@transaction emptyList()
|
||||||
|
}
|
||||||
|
|
||||||
|
logger.warn("id is too long! ({}) truncate to 20", id.size)
|
||||||
|
|
||||||
|
val subList = id.subList(0, min(id.size, 20))
|
||||||
|
|
||||||
|
return@transaction subList.map {
|
||||||
|
val alreadyFollow = followerQueryService.alreadyFollow(userid, it)
|
||||||
|
|
||||||
|
val followed = followerQueryService.alreadyFollow(it, userid)
|
||||||
|
|
||||||
|
val requested = userRepository.findFollowRequestsById(it, userid)
|
||||||
|
|
||||||
|
Relationship(
|
||||||
|
id = it.toString(),
|
||||||
|
following = alreadyFollow,
|
||||||
|
showingReblogs = true,
|
||||||
|
notifying = false,
|
||||||
|
followedBy = followed,
|
||||||
|
blocking = false,
|
||||||
|
blockedBy = false,
|
||||||
|
muting = false,
|
||||||
|
mutingNotifications = false,
|
||||||
|
requested = requested,
|
||||||
|
domainBlocking = false,
|
||||||
|
endorsed = false,
|
||||||
|
note = ""
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private fun from(account: Account): CredentialAccount {
|
private fun from(account: Account): CredentialAccount {
|
||||||
return CredentialAccount(
|
return CredentialAccount(
|
||||||
id = account.id,
|
id = account.id,
|
||||||
|
@ -68,4 +197,8 @@ class AccountApiServiceImpl(
|
||||||
role = Role(0, "Admin", "", 32)
|
role = Role(0, "Admin", "", 32)
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
private val logger = LoggerFactory.getLogger(AccountApiServiceImpl::class.java)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,5 +1,6 @@
|
||||||
package dev.usbharu.hideout.mastodon.service.account
|
package dev.usbharu.hideout.mastodon.service.account
|
||||||
|
|
||||||
|
import dev.usbharu.hideout.application.config.ApplicationConfig
|
||||||
import dev.usbharu.hideout.core.query.UserQueryService
|
import dev.usbharu.hideout.core.query.UserQueryService
|
||||||
import dev.usbharu.hideout.domain.mastodon.model.generated.Account
|
import dev.usbharu.hideout.domain.mastodon.model.generated.Account
|
||||||
import org.springframework.stereotype.Service
|
import org.springframework.stereotype.Service
|
||||||
|
@ -10,9 +11,14 @@ interface AccountService {
|
||||||
}
|
}
|
||||||
|
|
||||||
@Service
|
@Service
|
||||||
class AccountServiceImpl(private val userQueryService: UserQueryService) : AccountService {
|
class AccountServiceImpl(
|
||||||
|
private val userQueryService: UserQueryService,
|
||||||
|
private val applicationConfig: ApplicationConfig
|
||||||
|
) : AccountService {
|
||||||
override suspend fun findById(id: Long): Account {
|
override suspend fun findById(id: Long): Account {
|
||||||
val findById = userQueryService.findById(id)
|
val findById = userQueryService.findById(id)
|
||||||
|
val userUrl = applicationConfig.url.toString() + "/users/" + findById.id.toString()
|
||||||
|
|
||||||
return Account(
|
return Account(
|
||||||
id = findById.id.toString(),
|
id = findById.id.toString(),
|
||||||
username = findById.name,
|
username = findById.name,
|
||||||
|
@ -20,10 +26,10 @@ class AccountServiceImpl(private val userQueryService: UserQueryService) : Accou
|
||||||
url = findById.url,
|
url = findById.url,
|
||||||
displayName = findById.screenName,
|
displayName = findById.screenName,
|
||||||
note = findById.description,
|
note = findById.description,
|
||||||
avatar = findById.url + "/icon.jpg",
|
avatar = "$userUrl/icon.jpg",
|
||||||
avatarStatic = findById.url + "/icon.jpg",
|
avatarStatic = "$userUrl/icon.jpg",
|
||||||
header = findById.url + "/header.jpg",
|
header = "$userUrl/header.jpg",
|
||||||
headerStatic = findById.url + "/header.jpg",
|
headerStatic = "$userUrl/header.jpg",
|
||||||
locked = false,
|
locked = false,
|
||||||
fields = emptyList(),
|
fields = emptyList(),
|
||||||
emojis = emptyList(),
|
emojis = emptyList(),
|
||||||
|
|
|
@ -13,7 +13,6 @@ import org.springframework.security.oauth2.server.authorization.settings.ClientS
|
||||||
import org.springframework.security.oauth2.server.authorization.settings.TokenSettings
|
import org.springframework.security.oauth2.server.authorization.settings.TokenSettings
|
||||||
import org.springframework.stereotype.Service
|
import org.springframework.stereotype.Service
|
||||||
import java.time.Duration
|
import java.time.Duration
|
||||||
import java.time.Instant
|
|
||||||
import java.util.*
|
import java.util.*
|
||||||
|
|
||||||
@Service
|
@Service
|
||||||
|
@ -46,7 +45,7 @@ class AppApiServiceImpl(
|
||||||
.tokenSettings(
|
.tokenSettings(
|
||||||
TokenSettings.builder()
|
TokenSettings.builder()
|
||||||
.accessTokenTimeToLive(
|
.accessTokenTimeToLive(
|
||||||
Duration.ofSeconds((Instant.MAX.epochSecond - Instant.now().epochSecond - 10000) / 1000)
|
Duration.ofSeconds(31536000000)
|
||||||
)
|
)
|
||||||
.build()
|
.build()
|
||||||
)
|
)
|
||||||
|
@ -60,7 +59,8 @@ class AppApiServiceImpl(
|
||||||
"invalid-vapid-key",
|
"invalid-vapid-key",
|
||||||
appsRequest.website,
|
appsRequest.website,
|
||||||
id,
|
id,
|
||||||
clientSecret
|
clientSecret,
|
||||||
|
appsRequest.redirectUris
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -36,7 +36,7 @@ spring:
|
||||||
max-request-size: 40MB
|
max-request-size: 40MB
|
||||||
h2:
|
h2:
|
||||||
console:
|
console:
|
||||||
enabled: false
|
enabled: true
|
||||||
server:
|
server:
|
||||||
tomcat:
|
tomcat:
|
||||||
basedir: tomcat
|
basedir: tomcat
|
||||||
|
|
|
@ -31,7 +31,7 @@ create table if not exists users
|
||||||
"following" varchar(1000) null,
|
"following" varchar(1000) null,
|
||||||
followers varchar(1000) null,
|
followers varchar(1000) null,
|
||||||
"instance" bigint null,
|
"instance" bigint null,
|
||||||
unique (name, domain),
|
unique ("name", "domain"),
|
||||||
constraint fk_users_instance__id foreign key ("instance") references instance (id) on delete restrict on update restrict
|
constraint fk_users_instance__id foreign key ("instance") references instance (id) on delete restrict on update restrict
|
||||||
);
|
);
|
||||||
create table if not exists follow_requests
|
create table if not exists follow_requests
|
||||||
|
|
|
@ -4,7 +4,7 @@
|
||||||
<pattern>%d{YYYY-MM-dd HH:mm:ss.SSS} [%thread] %-5level [%X{x-request-id}] %logger{36} - %msg%n</pattern>
|
<pattern>%d{YYYY-MM-dd HH:mm:ss.SSS} [%thread] %-5level [%X{x-request-id}] %logger{36} - %msg%n</pattern>
|
||||||
</encoder>
|
</encoder>
|
||||||
</appender>
|
</appender>
|
||||||
<root level="INFO">
|
<root level="DEBUG">
|
||||||
<appender-ref ref="STDOUT"/>
|
<appender-ref ref="STDOUT"/>
|
||||||
</root>
|
</root>
|
||||||
<logger name="org.eclipse.jetty" level="INFO"/>
|
<logger name="org.eclipse.jetty" level="INFO"/>
|
||||||
|
@ -12,7 +12,7 @@
|
||||||
<logger name="kjob.core.internal.scheduler.JobServiceImpl" level="INFO"/>
|
<logger name="kjob.core.internal.scheduler.JobServiceImpl" level="INFO"/>
|
||||||
<logger name="Exposed" level="INFO"/>
|
<logger name="Exposed" level="INFO"/>
|
||||||
<logger name="io.ktor.server.plugins.contentnegotiation" level="INFO"/>
|
<logger name="io.ktor.server.plugins.contentnegotiation" level="INFO"/>
|
||||||
<logger name="org.springframework.web.filter.CommonsRequestLoggingFilter" level="INFO"/>
|
<logger name="org.springframework.web.filter.CommonsRequestLoggingFilter" level="DEBUG"/>
|
||||||
<logger name="org.mongodb.driver.protocol.command" level="INFO"/>
|
<logger name="org.mongodb.driver.protocol.command" level="INFO"/>
|
||||||
<logger name="dev.usbharu" level="TRACE"/>
|
<logger name="dev.usbharu" level="TRACE"/>
|
||||||
</configuration>
|
</configuration>
|
||||||
|
|
|
@ -206,6 +206,161 @@ paths:
|
||||||
200:
|
200:
|
||||||
description: 成功
|
description: 成功
|
||||||
|
|
||||||
|
/api/v1/accounts/relationships:
|
||||||
|
get:
|
||||||
|
tags:
|
||||||
|
- account
|
||||||
|
security:
|
||||||
|
- OAuth2:
|
||||||
|
- "read:follows"
|
||||||
|
parameters:
|
||||||
|
- in: query
|
||||||
|
name: id[]
|
||||||
|
required: false
|
||||||
|
schema:
|
||||||
|
type: array
|
||||||
|
items:
|
||||||
|
type: string
|
||||||
|
- in: query
|
||||||
|
name: with_suspended
|
||||||
|
required: false
|
||||||
|
schema:
|
||||||
|
type: boolean
|
||||||
|
default: false
|
||||||
|
responses:
|
||||||
|
200:
|
||||||
|
description: 成功
|
||||||
|
content:
|
||||||
|
application/json:
|
||||||
|
schema:
|
||||||
|
type: array
|
||||||
|
items:
|
||||||
|
$ref: "#/components/schemas/Relationship"
|
||||||
|
|
||||||
|
/api/v1/accounts/{id}:
|
||||||
|
get:
|
||||||
|
tags:
|
||||||
|
- account
|
||||||
|
security:
|
||||||
|
- { }
|
||||||
|
parameters:
|
||||||
|
- in: path
|
||||||
|
name: id
|
||||||
|
required: true
|
||||||
|
schema:
|
||||||
|
type: string
|
||||||
|
responses:
|
||||||
|
200:
|
||||||
|
description: 成功
|
||||||
|
content:
|
||||||
|
application/json:
|
||||||
|
schema:
|
||||||
|
$ref: "#/components/schemas/Account"
|
||||||
|
|
||||||
|
/api/v1/accounts/{id}/follow:
|
||||||
|
post:
|
||||||
|
tags:
|
||||||
|
- account
|
||||||
|
security:
|
||||||
|
- OAuth2:
|
||||||
|
- "write:follows"
|
||||||
|
parameters:
|
||||||
|
- in: path
|
||||||
|
name: id
|
||||||
|
required: true
|
||||||
|
schema:
|
||||||
|
type: string
|
||||||
|
requestBody:
|
||||||
|
required: false
|
||||||
|
content:
|
||||||
|
application/json:
|
||||||
|
schema:
|
||||||
|
$ref: "#/components/schemas/FollowRequestBody"
|
||||||
|
application/x-www-form-urlencoded:
|
||||||
|
schema:
|
||||||
|
$ref: "#/components/schemas/FollowRequestBody"
|
||||||
|
responses:
|
||||||
|
200:
|
||||||
|
description: 成功
|
||||||
|
content:
|
||||||
|
application/json:
|
||||||
|
schema:
|
||||||
|
$ref: "#/components/schemas/Relationship"
|
||||||
|
|
||||||
|
/api/v1/accounts/{id}/statuses:
|
||||||
|
get:
|
||||||
|
tags:
|
||||||
|
- account
|
||||||
|
security:
|
||||||
|
- OAuth2:
|
||||||
|
- "read:statuses"
|
||||||
|
parameters:
|
||||||
|
- in: path
|
||||||
|
name: id
|
||||||
|
required: true
|
||||||
|
schema:
|
||||||
|
type: string
|
||||||
|
- in: query
|
||||||
|
name: max_id
|
||||||
|
required: false
|
||||||
|
schema:
|
||||||
|
type: string
|
||||||
|
- in: query
|
||||||
|
name: since_id
|
||||||
|
required: false
|
||||||
|
schema:
|
||||||
|
type: string
|
||||||
|
- in: query
|
||||||
|
name: min_id
|
||||||
|
required: false
|
||||||
|
schema:
|
||||||
|
type: string
|
||||||
|
- in: query
|
||||||
|
name: limit
|
||||||
|
required: false
|
||||||
|
schema:
|
||||||
|
type: integer
|
||||||
|
default: 20
|
||||||
|
- in: query
|
||||||
|
name: only_media
|
||||||
|
required: false
|
||||||
|
schema:
|
||||||
|
type: boolean
|
||||||
|
default: false
|
||||||
|
- in: query
|
||||||
|
name: exclude_replies
|
||||||
|
required: false
|
||||||
|
schema:
|
||||||
|
type: boolean
|
||||||
|
default: false
|
||||||
|
- in: query
|
||||||
|
name: exclude_reblogs
|
||||||
|
required: false
|
||||||
|
schema:
|
||||||
|
type: boolean
|
||||||
|
default: false
|
||||||
|
- in: query
|
||||||
|
name: pinned
|
||||||
|
required: false
|
||||||
|
schema:
|
||||||
|
type: boolean
|
||||||
|
default: false
|
||||||
|
- in: query
|
||||||
|
required: false
|
||||||
|
name: tagged
|
||||||
|
schema:
|
||||||
|
type: string
|
||||||
|
|
||||||
|
responses:
|
||||||
|
200:
|
||||||
|
description: 成功
|
||||||
|
content:
|
||||||
|
application/json:
|
||||||
|
schema:
|
||||||
|
type: array
|
||||||
|
items:
|
||||||
|
$ref: "#/components/schemas/Status"
|
||||||
|
|
||||||
/api/v1/timelines/public:
|
/api/v1/timelines/public:
|
||||||
get:
|
get:
|
||||||
tags:
|
tags:
|
||||||
|
@ -1314,6 +1469,8 @@ components:
|
||||||
type: string
|
type: string
|
||||||
client_secret:
|
client_secret:
|
||||||
type: string
|
type: string
|
||||||
|
redirect_uri:
|
||||||
|
type: string
|
||||||
required:
|
required:
|
||||||
- name
|
- name
|
||||||
- vapid_key
|
- vapid_key
|
||||||
|
@ -1333,6 +1490,65 @@ components:
|
||||||
- client_name
|
- client_name
|
||||||
- redirect_uris
|
- redirect_uris
|
||||||
|
|
||||||
|
Relationship:
|
||||||
|
type: object
|
||||||
|
properties:
|
||||||
|
id:
|
||||||
|
type: string
|
||||||
|
following:
|
||||||
|
type: boolean
|
||||||
|
showing_reblogs:
|
||||||
|
type: boolean
|
||||||
|
notifying:
|
||||||
|
type: boolean
|
||||||
|
followed_by:
|
||||||
|
type: boolean
|
||||||
|
blocking:
|
||||||
|
type: boolean
|
||||||
|
blocked_by:
|
||||||
|
type: boolean
|
||||||
|
muting:
|
||||||
|
type: boolean
|
||||||
|
muting_notifications:
|
||||||
|
type: boolean
|
||||||
|
requested:
|
||||||
|
type: boolean
|
||||||
|
domain_blocking:
|
||||||
|
type: boolean
|
||||||
|
endorsed:
|
||||||
|
type: boolean
|
||||||
|
note:
|
||||||
|
type: string
|
||||||
|
required:
|
||||||
|
- id
|
||||||
|
- following
|
||||||
|
- showing_reblogs
|
||||||
|
- notifying
|
||||||
|
- followed_by
|
||||||
|
- blocking
|
||||||
|
- blocked_by
|
||||||
|
- muting
|
||||||
|
- muting_notifications
|
||||||
|
- requested
|
||||||
|
- domain_blocking
|
||||||
|
- endorsed
|
||||||
|
- note
|
||||||
|
|
||||||
|
|
||||||
|
FollowRequestBody:
|
||||||
|
type: object
|
||||||
|
properties:
|
||||||
|
reblogs:
|
||||||
|
type: boolean
|
||||||
|
default: true
|
||||||
|
notify:
|
||||||
|
type: boolean
|
||||||
|
default: false
|
||||||
|
languages:
|
||||||
|
type: array
|
||||||
|
items:
|
||||||
|
type: string
|
||||||
|
|
||||||
securitySchemes:
|
securitySchemes:
|
||||||
OAuth2:
|
OAuth2:
|
||||||
type: oauth2
|
type: oauth2
|
||||||
|
|
|
@ -125,4 +125,20 @@ class MastodonAccountApiControllerTest {
|
||||||
.andExpect { header { string("location", "/users/hoge") } }
|
.andExpect { header { string("location", "/users/hoge") } }
|
||||||
.andExpect { status { isFound() } }
|
.andExpect { status { isFound() } }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `apiV1AccountsIdFollowPost フォロー成功時は200が返ってくる`() {
|
||||||
|
val createEmptyContext = SecurityContextHolder.createEmptyContext()
|
||||||
|
createEmptyContext.authentication = JwtAuthenticationToken(
|
||||||
|
Jwt.withTokenValue("a").header("alg", "RS236").claim("uid", "1234").build()
|
||||||
|
)
|
||||||
|
SecurityContextHolder.setContext(createEmptyContext)
|
||||||
|
mockMvc
|
||||||
|
.post("/api/v1/accounts/1/follow") {
|
||||||
|
contentType = MediaType.APPLICATION_JSON
|
||||||
|
}
|
||||||
|
.asyncDispatch()
|
||||||
|
.andExpect { status { isOk() } }
|
||||||
|
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,330 @@
|
||||||
|
package dev.usbharu.hideout.mastodon.service.account
|
||||||
|
|
||||||
|
import dev.usbharu.hideout.application.external.Transaction
|
||||||
|
import dev.usbharu.hideout.core.domain.model.user.UserRepository
|
||||||
|
import dev.usbharu.hideout.core.query.FollowerQueryService
|
||||||
|
import dev.usbharu.hideout.core.service.user.UserService
|
||||||
|
import dev.usbharu.hideout.domain.mastodon.model.generated.Account
|
||||||
|
import dev.usbharu.hideout.domain.mastodon.model.generated.Relationship
|
||||||
|
import dev.usbharu.hideout.domain.mastodon.model.generated.Status
|
||||||
|
import dev.usbharu.hideout.mastodon.query.StatusQueryService
|
||||||
|
import kotlinx.coroutines.test.runTest
|
||||||
|
import org.assertj.core.api.Assertions.assertThat
|
||||||
|
import org.junit.jupiter.api.Test
|
||||||
|
import org.junit.jupiter.api.extension.ExtendWith
|
||||||
|
import org.mockito.InjectMocks
|
||||||
|
import org.mockito.Mock
|
||||||
|
import org.mockito.Spy
|
||||||
|
import org.mockito.junit.jupiter.MockitoExtension
|
||||||
|
import org.mockito.kotlin.*
|
||||||
|
import utils.TestTransaction
|
||||||
|
|
||||||
|
@ExtendWith(MockitoExtension::class)
|
||||||
|
class AccountApiServiceImplTest {
|
||||||
|
|
||||||
|
@Mock
|
||||||
|
private lateinit var accountService: AccountService
|
||||||
|
|
||||||
|
@Mock
|
||||||
|
private lateinit var userService: UserService
|
||||||
|
|
||||||
|
@Mock
|
||||||
|
private lateinit var userRepository: UserRepository
|
||||||
|
|
||||||
|
@Mock
|
||||||
|
private lateinit var followerQueryService: FollowerQueryService
|
||||||
|
|
||||||
|
@Mock
|
||||||
|
private lateinit var statusQueryService: StatusQueryService
|
||||||
|
|
||||||
|
@Spy
|
||||||
|
private val transaction: Transaction = TestTransaction
|
||||||
|
|
||||||
|
@InjectMocks
|
||||||
|
private lateinit var accountApiServiceImpl: AccountApiServiceImpl
|
||||||
|
|
||||||
|
private val statusList = listOf(
|
||||||
|
Status(
|
||||||
|
id = "",
|
||||||
|
uri = "",
|
||||||
|
createdAt = "",
|
||||||
|
account = Account(
|
||||||
|
id = "",
|
||||||
|
username = "",
|
||||||
|
acct = "",
|
||||||
|
url = "",
|
||||||
|
displayName = "",
|
||||||
|
note = "",
|
||||||
|
avatar = "",
|
||||||
|
avatarStatic = "",
|
||||||
|
header = "",
|
||||||
|
headerStatic = "",
|
||||||
|
locked = false,
|
||||||
|
fields = emptyList(),
|
||||||
|
emojis = emptyList(),
|
||||||
|
bot = false,
|
||||||
|
group = false,
|
||||||
|
discoverable = true,
|
||||||
|
createdAt = "",
|
||||||
|
lastStatusAt = "",
|
||||||
|
statusesCount = 0,
|
||||||
|
followersCount = 0,
|
||||||
|
noindex = false,
|
||||||
|
moved = false,
|
||||||
|
suspendex = false,
|
||||||
|
limited = false,
|
||||||
|
followingCount = 0
|
||||||
|
),
|
||||||
|
content = "",
|
||||||
|
visibility = Status.Visibility.public,
|
||||||
|
sensitive = false,
|
||||||
|
spoilerText = "",
|
||||||
|
mediaAttachments = emptyList(),
|
||||||
|
mentions = emptyList(),
|
||||||
|
tags = emptyList(),
|
||||||
|
emojis = emptyList(),
|
||||||
|
reblogsCount = 0,
|
||||||
|
favouritesCount = 0,
|
||||||
|
repliesCount = 0,
|
||||||
|
url = "https://example.com",
|
||||||
|
inReplyToId = null,
|
||||||
|
inReplyToAccountId = null,
|
||||||
|
language = "ja_JP",
|
||||||
|
text = "Test",
|
||||||
|
editedAt = null
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `accountsStatuses 非ログイン時は非公開投稿を見れない`() = runTest {
|
||||||
|
val userId = 1234L
|
||||||
|
|
||||||
|
whenever(
|
||||||
|
statusQueryService.accountsStatus(
|
||||||
|
accountId = eq(userId),
|
||||||
|
maxId = isNull(),
|
||||||
|
sinceId = isNull(),
|
||||||
|
minId = isNull(),
|
||||||
|
limit = eq(20),
|
||||||
|
onlyMedia = eq(false),
|
||||||
|
excludeReplies = eq(false),
|
||||||
|
excludeReblogs = eq(false),
|
||||||
|
pinned = eq(false),
|
||||||
|
tagged = isNull(),
|
||||||
|
includeFollowers = eq(false)
|
||||||
|
)
|
||||||
|
).doReturn(
|
||||||
|
statusList
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
val accountsStatuses = accountApiServiceImpl.accountsStatuses(
|
||||||
|
userid = userId,
|
||||||
|
maxId = null,
|
||||||
|
sinceId = null,
|
||||||
|
minId = null,
|
||||||
|
limit = 20,
|
||||||
|
onlyMedia = false,
|
||||||
|
excludeReplies = false,
|
||||||
|
excludeReblogs = false,
|
||||||
|
pinned = false,
|
||||||
|
tagged = null,
|
||||||
|
loginUser = null
|
||||||
|
)
|
||||||
|
|
||||||
|
assertThat(accountsStatuses).hasSize(1)
|
||||||
|
|
||||||
|
verify(followerQueryService, never()).alreadyFollow(any(), any())
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `accountsStatuses ログイン時フォロワーじゃない場合は非公開投稿を見れない`() = runTest {
|
||||||
|
val userId = 1234L
|
||||||
|
val loginUser = 1L
|
||||||
|
whenever(
|
||||||
|
statusQueryService.accountsStatus(
|
||||||
|
accountId = eq(userId),
|
||||||
|
maxId = isNull(),
|
||||||
|
sinceId = isNull(),
|
||||||
|
minId = isNull(),
|
||||||
|
limit = eq(20),
|
||||||
|
onlyMedia = eq(false),
|
||||||
|
excludeReplies = eq(false),
|
||||||
|
excludeReblogs = eq(false),
|
||||||
|
pinned = eq(false),
|
||||||
|
tagged = isNull(),
|
||||||
|
includeFollowers = eq(false)
|
||||||
|
)
|
||||||
|
).doReturn(statusList)
|
||||||
|
|
||||||
|
whenever(followerQueryService.alreadyFollow(eq(userId), eq(loginUser))).doReturn(false)
|
||||||
|
|
||||||
|
|
||||||
|
val accountsStatuses = accountApiServiceImpl.accountsStatuses(
|
||||||
|
userid = userId,
|
||||||
|
maxId = null,
|
||||||
|
sinceId = null,
|
||||||
|
minId = null,
|
||||||
|
limit = 20,
|
||||||
|
onlyMedia = false,
|
||||||
|
excludeReplies = false,
|
||||||
|
excludeReblogs = false,
|
||||||
|
pinned = false,
|
||||||
|
tagged = null,
|
||||||
|
loginUser = loginUser
|
||||||
|
)
|
||||||
|
|
||||||
|
assertThat(accountsStatuses).hasSize(1)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `accountsStatuses ログイン時フォロワーの場合は非公開投稿を見れる`() = runTest {
|
||||||
|
val userId = 1234L
|
||||||
|
val loginUser = 2L
|
||||||
|
whenever(
|
||||||
|
statusQueryService.accountsStatus(
|
||||||
|
accountId = eq(userId),
|
||||||
|
maxId = isNull(),
|
||||||
|
sinceId = isNull(),
|
||||||
|
minId = isNull(),
|
||||||
|
limit = eq(20),
|
||||||
|
onlyMedia = eq(false),
|
||||||
|
excludeReplies = eq(false),
|
||||||
|
excludeReblogs = eq(false),
|
||||||
|
pinned = eq(false),
|
||||||
|
tagged = isNull(),
|
||||||
|
includeFollowers = eq(true)
|
||||||
|
)
|
||||||
|
).doReturn(statusList)
|
||||||
|
|
||||||
|
whenever(followerQueryService.alreadyFollow(eq(userId), eq(loginUser))).doReturn(true)
|
||||||
|
|
||||||
|
|
||||||
|
val accountsStatuses = accountApiServiceImpl.accountsStatuses(
|
||||||
|
userid = userId,
|
||||||
|
maxId = null,
|
||||||
|
sinceId = null,
|
||||||
|
minId = null,
|
||||||
|
limit = 20,
|
||||||
|
onlyMedia = false,
|
||||||
|
excludeReplies = false,
|
||||||
|
excludeReblogs = false,
|
||||||
|
pinned = false,
|
||||||
|
tagged = null,
|
||||||
|
loginUser = loginUser
|
||||||
|
)
|
||||||
|
|
||||||
|
assertThat(accountsStatuses).hasSize(1)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `follow 既にフォローしている場合は何もしない`() = runTest {
|
||||||
|
val userId = 1234L
|
||||||
|
val followeeId = 1L
|
||||||
|
|
||||||
|
whenever(followerQueryService.alreadyFollow(eq(followeeId), eq(userId))).doReturn(true)
|
||||||
|
|
||||||
|
whenever(followerQueryService.alreadyFollow(eq(userId), eq(followeeId))).doReturn(true)
|
||||||
|
|
||||||
|
whenever(userRepository.findFollowRequestsById(eq(followeeId), eq(userId))).doReturn(false)
|
||||||
|
|
||||||
|
val follow = accountApiServiceImpl.follow(userId, followeeId)
|
||||||
|
|
||||||
|
val expected = Relationship(
|
||||||
|
id = followeeId.toString(),
|
||||||
|
following = true,
|
||||||
|
showingReblogs = true,
|
||||||
|
notifying = false,
|
||||||
|
followedBy = true,
|
||||||
|
blocking = false,
|
||||||
|
blockedBy = false,
|
||||||
|
muting = false,
|
||||||
|
mutingNotifications = false,
|
||||||
|
requested = false,
|
||||||
|
domainBlocking = false,
|
||||||
|
endorsed = false,
|
||||||
|
note = ""
|
||||||
|
)
|
||||||
|
assertThat(follow).isEqualTo(expected)
|
||||||
|
|
||||||
|
verify(userService, never()).followRequest(any(), any())
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `follow 未フォローの場合フォローリクエストが発生する`() = runTest {
|
||||||
|
val userId = 1234L
|
||||||
|
val followeeId = 1L
|
||||||
|
|
||||||
|
whenever(followerQueryService.alreadyFollow(eq(followeeId), eq(userId))).doReturn(false)
|
||||||
|
|
||||||
|
whenever(userService.followRequest(eq(followeeId), eq(userId))).doReturn(true)
|
||||||
|
|
||||||
|
whenever(followerQueryService.alreadyFollow(eq(userId), eq(followeeId))).doReturn(true)
|
||||||
|
|
||||||
|
whenever(userRepository.findFollowRequestsById(eq(followeeId), eq(userId))).doReturn(false)
|
||||||
|
|
||||||
|
val follow = accountApiServiceImpl.follow(userId, followeeId)
|
||||||
|
|
||||||
|
val expected = Relationship(
|
||||||
|
id = followeeId.toString(),
|
||||||
|
following = true,
|
||||||
|
showingReblogs = true,
|
||||||
|
notifying = false,
|
||||||
|
followedBy = true,
|
||||||
|
blocking = false,
|
||||||
|
blockedBy = false,
|
||||||
|
muting = false,
|
||||||
|
mutingNotifications = false,
|
||||||
|
requested = false,
|
||||||
|
domainBlocking = false,
|
||||||
|
endorsed = false,
|
||||||
|
note = ""
|
||||||
|
)
|
||||||
|
assertThat(follow).isEqualTo(expected)
|
||||||
|
|
||||||
|
verify(userService, times(1)).followRequest(eq(followeeId), eq(userId))
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `relationships idが長すぎたら省略する`() = runTest {
|
||||||
|
whenever(followerQueryService.alreadyFollow(any(), any())).doReturn(true)
|
||||||
|
|
||||||
|
whenever(userRepository.findFollowRequestsById(any(), any())).doReturn(true)
|
||||||
|
|
||||||
|
val relationships = accountApiServiceImpl.relationships(
|
||||||
|
userid = 1234L,
|
||||||
|
id = (1..30L).toList(),
|
||||||
|
withSuspended = false
|
||||||
|
)
|
||||||
|
|
||||||
|
assertThat(relationships).hasSizeLessThanOrEqualTo(20)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `relationships id0の場合即時return`() = runTest {
|
||||||
|
val relationships = accountApiServiceImpl.relationships(
|
||||||
|
userid = 1234L,
|
||||||
|
id = emptyList(),
|
||||||
|
withSuspended = false
|
||||||
|
)
|
||||||
|
|
||||||
|
assertThat(relationships).hasSize(0)
|
||||||
|
verify(followerQueryService, never()).alreadyFollow(any(), any())
|
||||||
|
verify(userRepository, never()).findFollowRequestsById(any(), any())
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `relationships idに指定されたアカウントの関係を取得する`() = runTest {
|
||||||
|
whenever(followerQueryService.alreadyFollow(any(), any())).doReturn(true)
|
||||||
|
|
||||||
|
whenever(userRepository.findFollowRequestsById(any(), any())).doReturn(true)
|
||||||
|
|
||||||
|
val relationships = accountApiServiceImpl.relationships(
|
||||||
|
userid = 1234L,
|
||||||
|
id = (1..15L).toList(),
|
||||||
|
withSuspended = false
|
||||||
|
)
|
||||||
|
|
||||||
|
assertThat(relationships).hasSize(15)
|
||||||
|
}
|
||||||
|
}
|
Loading…
Reference in New Issue