From e2bb2f6c2aff82f5611292bb931ffe20397c0d62 Mon Sep 17 00:00:00 2001 From: usbharu <64310155+usbharu@users.noreply.github.com> Date: Tue, 21 Nov 2023 12:38:06 +0900 Subject: [PATCH 01/19] =?UTF-8?q?feat:=20=E3=83=95=E3=82=A9=E3=83=AD?= =?UTF-8?q?=E3=83=BC=E3=81=AEMastodon=E4=BA=92=E6=8F=9BAPI=E3=82=92?= =?UTF-8?q?=E4=BD=9C=E6=88=90?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../account/MastodonAccountApiController.kt | 16 +++ .../service/account/AccountApiService.kt | 49 +++++++- src/main/resources/openapi/mastodon.yaml | 110 ++++++++++++++++++ 3 files changed, 170 insertions(+), 5 deletions(-) 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 46a03fbe..f3c05765 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 @@ -3,7 +3,10 @@ package dev.usbharu.hideout.mastodon.interfaces.api.account import dev.usbharu.hideout.application.external.Transaction import dev.usbharu.hideout.controller.mastodon.generated.AccountApi import dev.usbharu.hideout.core.service.user.UserCreateDto +import dev.usbharu.hideout.domain.mastodon.model.generated.Account import dev.usbharu.hideout.domain.mastodon.model.generated.CredentialAccount +import dev.usbharu.hideout.domain.mastodon.model.generated.FollowRequestBody +import dev.usbharu.hideout.domain.mastodon.model.generated.Relationship import dev.usbharu.hideout.mastodon.service.account.AccountApiService import org.springframework.http.HttpHeaders import org.springframework.http.HttpStatus @@ -18,6 +21,19 @@ class MastodonAccountApiController( private val accountApiService: AccountApiService, private val transaction: Transaction ) : AccountApi { + + override suspend fun apiV1AccountsIdFollowPost( + id: String, + followRequestBody: FollowRequestBody? + ): ResponseEntity { + val principal = SecurityContextHolder.getContext().getAuthentication().principal as Jwt + + return ResponseEntity.ok(accountApiService.follow(principal.getClaim("uid").toLong(), id.toLong())) + } + + override suspend fun apiV1AccountsIdGet(id: String): ResponseEntity = + ResponseEntity.ok(accountApiService.account(id.toLong())) + override suspend fun apiV1AccountsVerifyCredentialsGet(): ResponseEntity { val principal = SecurityContextHolder.getContext().getAuthentication().principal as Jwt 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 9fc44262..b2e2792a 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 @@ -1,25 +1,28 @@ 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.UserCreateDto 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.CredentialAccount -import dev.usbharu.hideout.domain.mastodon.model.generated.CredentialAccountSource -import dev.usbharu.hideout.domain.mastodon.model.generated.Role +import dev.usbharu.hideout.domain.mastodon.model.generated.* import org.springframework.stereotype.Service @Service interface AccountApiService { suspend fun verifyCredentials(userid: Long): CredentialAccount suspend fun registerAccount(userCreateDto: UserCreateDto): Unit + suspend fun follow(userid: Long, followeeId: Long): Relationship + suspend fun account(id: Long): Account } @Service class AccountApiServiceImpl( private val accountService: AccountService, private val transaction: Transaction, - private val userService: UserService + private val userService: UserService, + private val followerQueryService: FollowerQueryService, + private val userRepository: UserRepository ) : AccountApiService { override suspend fun verifyCredentials(userid: Long): CredentialAccount = transaction.transaction { @@ -31,6 +34,42 @@ class AccountApiServiceImpl( 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) + } + 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 6f93fa36..03fa7a69 100644 --- a/src/main/resources/openapi/mastodon.yaml +++ b/src/main/resources/openapi/mastodon.yaml @@ -206,6 +206,55 @@ paths: 200: description: 成功 + /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/timelines/public: get: tags: @@ -1314,6 +1363,8 @@ components: type: string client_secret: type: string + redirect_uri: + type: string required: - name - vapid_key @@ -1333,6 +1384,65 @@ components: - client_name - 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: OAuth2: type: oauth2 From 85b899b088ffdfb7857102fb23db0b769d7a0fbd Mon Sep 17 00:00:00 2001 From: usbharu <64310155+usbharu@users.noreply.github.com> Date: Tue, 21 Nov 2023 12:38:41 +0900 Subject: [PATCH 02/19] =?UTF-8?q?fix:=20OAuth2=E8=AA=8D=E8=A8=BC=E3=81=AB?= =?UTF-8?q?=E5=A4=B1=E6=95=97=E3=81=99=E3=82=8B=E3=82=88=E3=81=86=E3=81=AB?= =?UTF-8?q?=E3=81=AA=E3=81=A3=E3=81=A6=E3=81=84=E3=82=8B=E3=81=AE=E3=82=92?= =?UTF-8?q?=E4=BF=AE=E6=AD=A3?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../application/config/SecurityConfig.kt | 43 ++++++++++++------- .../mastodon/service/app/AppApiService.kt | 3 +- src/main/resources/application.yml | 2 +- src/main/resources/logback.xml | 4 +- 4 files changed, 33 insertions(+), 19 deletions(-) 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 a5762bb1..13d73219 100644 --- a/src/main/kotlin/dev/usbharu/hideout/application/config/SecurityConfig.kt +++ b/src/main/kotlin/dev/usbharu/hideout/application/config/SecurityConfig.kt @@ -11,6 +11,7 @@ import dev.usbharu.hideout.core.infrastructure.springframework.httpsignature.Htt import dev.usbharu.hideout.core.infrastructure.springframework.httpsignature.HttpSignatureUserDetailsService import dev.usbharu.hideout.core.infrastructure.springframework.httpsignature.HttpSignatureVerifierComposite import dev.usbharu.hideout.core.infrastructure.springframework.oauth2.UserDetailsImpl +import dev.usbharu.hideout.core.infrastructure.springframework.oauth2.UserDetailsServiceImpl import dev.usbharu.hideout.core.query.UserQueryService import dev.usbharu.hideout.util.RsaUtil import dev.usbharu.httpsignature.sign.RsaSha256HttpSignatureSigner @@ -31,6 +32,7 @@ import org.springframework.http.converter.json.Jackson2ObjectMapperBuilder import org.springframework.http.converter.json.MappingJackson2HttpMessageConverter import org.springframework.security.authentication.AccountStatusUserDetailsChecker import org.springframework.security.authentication.AuthenticationManager +import org.springframework.security.authentication.dao.DaoAuthenticationProvider import org.springframework.security.config.Customizer import org.springframework.security.config.annotation.authentication.configuration.AuthenticationConfiguration import org.springframework.security.config.annotation.web.builders.HttpSecurity @@ -59,7 +61,7 @@ import java.security.interfaces.RSAPrivateKey import java.security.interfaces.RSAPublicKey import java.util.* -@EnableWebSecurity(debug = false) +@EnableWebSecurity(debug = true) @Configuration @Suppress("FunctionMaxLength", "TooManyFunctions") class SecurityConfig { @@ -75,13 +77,12 @@ class SecurityConfig { @Order(1) fun httpSignatureFilterChain( http: HttpSecurity, - httpSignatureFilter: HttpSignatureFilter, introspector: HandlerMappingIntrospector ): SecurityFilterChain { val builder = MvcRequestMatcher.Builder(introspector) http .securityMatcher("/inbox", "/outbox", "/users/*/inbox", "/users/*/outbox", "/users/*/posts/*") - .addFilter(httpSignatureFilter) + .addFilter(getHttpSignatureFilter(http.getSharedObject(AuthenticationManager::class.java))) .addFilterBefore( ExceptionTranslationFilter(HttpStatusEntryPoint(HttpStatus.UNAUTHORIZED)), HttpSignatureFilter::class.java @@ -108,12 +109,11 @@ class SecurityConfig { .sessionManagement { it.sessionCreationPolicy(SessionCreationPolicy.STATELESS) } - return http.build() } - @Bean - fun getHttpSignatureFilter(authenticationManager: AuthenticationManager): HttpSignatureFilter { + + fun getHttpSignatureFilter(authenticationManager: AuthenticationManager?): HttpSignatureFilter { val httpSignatureFilter = HttpSignatureFilter(DefaultSignatureHeaderParser()) httpSignatureFilter.setAuthenticationManager(authenticationManager) httpSignatureFilter.setContinueFilterChainOnUnsuccessfulAuthentication(false) @@ -124,6 +124,13 @@ class SecurityConfig { return httpSignatureFilter } + @Bean + fun daoAuthenticationProvider(userDetailsServiceImpl: UserDetailsServiceImpl): DaoAuthenticationProvider { + val daoAuthenticationProvider = DaoAuthenticationProvider() + daoAuthenticationProvider.setUserDetailsService(userDetailsServiceImpl) + return daoAuthenticationProvider + } + @Bean fun httpSignatureAuthenticationProvider(transaction: Transaction): PreAuthenticatedAuthenticationProvider { val provider = PreAuthenticatedAuthenticationProvider() @@ -187,16 +194,22 @@ class SecurityConfig { } http.oauth2ResourceServer { it.jwt(Customizer.withDefaults()) - }.passwordManagement { }.formLogin(Customizer.withDefaults()).csrf { - it.ignoringRequestMatchers(builder.pattern("/users/*/inbox")) - it.ignoringRequestMatchers(builder.pattern(HttpMethod.POST, "/api/v1/apps")) - it.ignoringRequestMatchers(builder.pattern("/inbox")) - it.ignoringRequestMatchers(PathRequest.toH2Console()) - }.headers { - it.frameOptions { - it.sameOrigin() - } } + .passwordManagement { } + .formLogin { + + } + .csrf { + it.ignoringRequestMatchers(builder.pattern("/users/*/inbox")) + it.ignoringRequestMatchers(builder.pattern(HttpMethod.POST, "/api/v1/apps")) + it.ignoringRequestMatchers(builder.pattern("/inbox")) + it.ignoringRequestMatchers(PathRequest.toH2Console()) + } + .headers { + it.frameOptions { + it.sameOrigin() + } + } return http.build() } diff --git a/src/main/kotlin/dev/usbharu/hideout/mastodon/service/app/AppApiService.kt b/src/main/kotlin/dev/usbharu/hideout/mastodon/service/app/AppApiService.kt index 6d7d463e..d2306123 100644 --- a/src/main/kotlin/dev/usbharu/hideout/mastodon/service/app/AppApiService.kt +++ b/src/main/kotlin/dev/usbharu/hideout/mastodon/service/app/AppApiService.kt @@ -60,7 +60,8 @@ class AppApiServiceImpl( "invalid-vapid-key", appsRequest.website, id, - clientSecret + clientSecret, + appsRequest.redirectUris ) } } diff --git a/src/main/resources/application.yml b/src/main/resources/application.yml index 2d0a6c82..32d326d7 100644 --- a/src/main/resources/application.yml +++ b/src/main/resources/application.yml @@ -19,7 +19,7 @@ spring: default-property-inclusion: always datasource: driver-class-name: org.h2.Driver - url: "jdbc:h2:./test-dev4;MODE=POSTGRESQL;TRACE_LEVEL_FILE=4" + url: "jdbc:h2:./test-dev4;MODE=POSTGRESQL" username: "" password: "" # data: diff --git a/src/main/resources/logback.xml b/src/main/resources/logback.xml index 5e4e2bc3..1f2e9e02 100644 --- a/src/main/resources/logback.xml +++ b/src/main/resources/logback.xml @@ -4,7 +4,7 @@ %d{YYYY-MM-dd HH:mm:ss.SSS} [%thread] %-5level [%X{x-request-id}] %logger{36} - %msg%n - + @@ -12,7 +12,7 @@ - + From 72afbeb065f7415f4af502a4be3cc078e408b0ba Mon Sep 17 00:00:00 2001 From: usbharu <64310155+usbharu@users.noreply.github.com> Date: Tue, 21 Nov 2023 15:38:56 +0900 Subject: [PATCH 03/19] wip --- .../application/config/SecurityConfig.kt | 41 ++- .../exposed/ExposedTransaction.kt | 11 +- .../httpsignature/HttpSignatureFilter.kt | 28 +- .../HttpSignatureUserDetailsService.kt | 1 + src/main/resources/application.yml | 22 +- .../resources/db/migration/V1__Init_DB.sql | 324 +++++++++--------- src/main/resources/logback.xml | 2 +- 7 files changed, 245 insertions(+), 184 deletions(-) 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 13d73219..c34eeac2 100644 --- a/src/main/kotlin/dev/usbharu/hideout/application/config/SecurityConfig.kt +++ b/src/main/kotlin/dev/usbharu/hideout/application/config/SecurityConfig.kt @@ -1,11 +1,13 @@ package dev.usbharu.hideout.application.config import com.fasterxml.jackson.annotation.JsonInclude +import com.fasterxml.jackson.databind.ObjectMapper import com.nimbusds.jose.jwk.JWKSet import com.nimbusds.jose.jwk.RSAKey import com.nimbusds.jose.jwk.source.ImmutableJWKSet import com.nimbusds.jose.jwk.source.JWKSource import com.nimbusds.jose.proc.SecurityContext +import dev.usbharu.hideout.activitypub.service.objects.user.APUserService import dev.usbharu.hideout.application.external.Transaction import dev.usbharu.hideout.core.infrastructure.springframework.httpsignature.HttpSignatureFilter import dev.usbharu.hideout.core.infrastructure.springframework.httpsignature.HttpSignatureUserDetailsService @@ -17,7 +19,9 @@ import dev.usbharu.hideout.util.RsaUtil import dev.usbharu.httpsignature.sign.RsaSha256HttpSignatureSigner import dev.usbharu.httpsignature.verify.DefaultSignatureHeaderParser import dev.usbharu.httpsignature.verify.RsaSha256HttpSignatureVerifier +import jakarta.annotation.PostConstruct import org.springframework.beans.factory.annotation.Autowired +import org.springframework.beans.factory.annotation.Qualifier import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty import org.springframework.boot.autoconfigure.jackson.Jackson2ObjectMapperBuilderCustomizer import org.springframework.boot.autoconfigure.security.servlet.PathRequest @@ -34,6 +38,7 @@ import org.springframework.security.authentication.AccountStatusUserDetailsCheck import org.springframework.security.authentication.AuthenticationManager import org.springframework.security.authentication.dao.DaoAuthenticationProvider import org.springframework.security.config.Customizer +import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder import org.springframework.security.config.annotation.authentication.configuration.AuthenticationConfiguration import org.springframework.security.config.annotation.web.builders.HttpSecurity import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity @@ -61,7 +66,8 @@ import java.security.interfaces.RSAPrivateKey import java.security.interfaces.RSAPublicKey import java.util.* -@EnableWebSecurity(debug = true) + +@EnableWebSecurity(debug = false) @Configuration @Suppress("FunctionMaxLength", "TooManyFunctions") class SecurityConfig { @@ -77,12 +83,13 @@ class SecurityConfig { @Order(1) fun httpSignatureFilterChain( http: HttpSecurity, + httpSignatureFilter: HttpSignatureFilter, introspector: HandlerMappingIntrospector ): SecurityFilterChain { val builder = MvcRequestMatcher.Builder(introspector) http .securityMatcher("/inbox", "/outbox", "/users/*/inbox", "/users/*/outbox", "/users/*/posts/*") - .addFilter(getHttpSignatureFilter(http.getSharedObject(AuthenticationManager::class.java))) + .addFilter(httpSignatureFilter) .addFilterBefore( ExceptionTranslationFilter(HttpStatusEntryPoint(HttpStatus.UNAUTHORIZED)), HttpSignatureFilter::class.java @@ -112,9 +119,15 @@ class SecurityConfig { return http.build() } - - fun getHttpSignatureFilter(authenticationManager: AuthenticationManager?): HttpSignatureFilter { - val httpSignatureFilter = HttpSignatureFilter(DefaultSignatureHeaderParser()) + @Bean + fun getHttpSignatureFilter( + authenticationManager: AuthenticationManager, + @Qualifier("activitypub") objectMapper: ObjectMapper, + apUserService: APUserService, + transaction: Transaction + ): HttpSignatureFilter { + val httpSignatureFilter = + HttpSignatureFilter(DefaultSignatureHeaderParser(), objectMapper, apUserService, transaction) httpSignatureFilter.setAuthenticationManager(authenticationManager) httpSignatureFilter.setContinueFilterChainOnUnsuccessfulAuthentication(false) val authenticationEntryPointFailureHandler = @@ -125,13 +138,16 @@ class SecurityConfig { } @Bean + @Order(2) fun daoAuthenticationProvider(userDetailsServiceImpl: UserDetailsServiceImpl): DaoAuthenticationProvider { val daoAuthenticationProvider = DaoAuthenticationProvider() daoAuthenticationProvider.setUserDetailsService(userDetailsServiceImpl) + return daoAuthenticationProvider } @Bean + @Order(1) fun httpSignatureAuthenticationProvider(transaction: Transaction): PreAuthenticatedAuthenticationProvider { val provider = PreAuthenticatedAuthenticationProvider() provider.setPreAuthenticatedUserDetailsService( @@ -280,3 +296,18 @@ data class JwkConfig( val publicKey: String, val privateKey: String ) + + +@Configuration +class PostSecurityConfig( + val auth: AuthenticationManagerBuilder, + val daoAuthenticationProvider: DaoAuthenticationProvider, + val httpSignatureAuthenticationProvider: PreAuthenticatedAuthenticationProvider +) { + + @PostConstruct + fun config() { + auth.authenticationProvider(daoAuthenticationProvider) + auth.authenticationProvider(httpSignatureAuthenticationProvider) + } +} diff --git a/src/main/kotlin/dev/usbharu/hideout/application/infrastructure/exposed/ExposedTransaction.kt b/src/main/kotlin/dev/usbharu/hideout/application/infrastructure/exposed/ExposedTransaction.kt index 4dc8316b..3b5d83b7 100644 --- a/src/main/kotlin/dev/usbharu/hideout/application/infrastructure/exposed/ExposedTransaction.kt +++ b/src/main/kotlin/dev/usbharu/hideout/application/infrastructure/exposed/ExposedTransaction.kt @@ -1,7 +1,10 @@ package dev.usbharu.hideout.application.infrastructure.exposed import dev.usbharu.hideout.application.external.Transaction +import kotlinx.coroutines.runBlocking 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.springframework.stereotype.Service import java.sql.Connection @@ -9,13 +12,17 @@ import java.sql.Connection @Service class ExposedTransaction : Transaction { override suspend fun transaction(block: suspend () -> T): T { - return newSuspendedTransaction(MDCContext(), transactionIsolation = Connection.TRANSACTION_SERIALIZABLE) { - block() + return org.jetbrains.exposed.sql.transactions.transaction(transactionIsolation = Connection.TRANSACTION_SERIALIZABLE) { + addLogger(StdOutSqlLogger) + runBlocking { + block() + } } } override suspend fun transaction(transactionLevel: Int, block: suspend () -> T): T { return newSuspendedTransaction(MDCContext(), transactionIsolation = transactionLevel) { + addLogger(StdOutSqlLogger) block() } } diff --git a/src/main/kotlin/dev/usbharu/hideout/core/infrastructure/springframework/httpsignature/HttpSignatureFilter.kt b/src/main/kotlin/dev/usbharu/hideout/core/infrastructure/springframework/httpsignature/HttpSignatureFilter.kt index 8b3c1b11..e3566886 100644 --- a/src/main/kotlin/dev/usbharu/hideout/core/infrastructure/springframework/httpsignature/HttpSignatureFilter.kt +++ b/src/main/kotlin/dev/usbharu/hideout/core/infrastructure/springframework/httpsignature/HttpSignatureFilter.kt @@ -1,20 +1,34 @@ package dev.usbharu.hideout.core.infrastructure.springframework.httpsignature +import com.fasterxml.jackson.databind.ObjectMapper +import dev.usbharu.hideout.activitypub.service.objects.user.APUserService +import dev.usbharu.hideout.application.external.Transaction import dev.usbharu.httpsignature.common.HttpHeaders import dev.usbharu.httpsignature.common.HttpMethod import dev.usbharu.httpsignature.common.HttpRequest import dev.usbharu.httpsignature.verify.SignatureHeaderParser import jakarta.servlet.http.HttpServletRequest +import kotlinx.coroutines.runBlocking +import org.springframework.beans.factory.annotation.Qualifier import org.springframework.security.web.authentication.preauth.AbstractPreAuthenticatedProcessingFilter import java.net.URL -class HttpSignatureFilter(private val httpSignatureHeaderParser: SignatureHeaderParser) : +class HttpSignatureFilter( + private val httpSignatureHeaderParser: SignatureHeaderParser, + @Qualifier("activitypub") private val objectMapper: ObjectMapper, + private val apUserService: APUserService, + private val transaction: Transaction, +) : AbstractPreAuthenticatedProcessingFilter() { - override fun getPreAuthenticatedPrincipal(request: HttpServletRequest?): Any? { - val headersList = request?.headerNames?.toList().orEmpty() + + + override fun getPreAuthenticatedPrincipal(request: HttpServletRequest): Any? { + + + val headersList = request.headerNames?.toList().orEmpty() val headers = - headersList.associateWith { header -> request?.getHeaders(header)?.toList().orEmpty() } + headersList.associateWith { header -> request.getHeaders(header)?.toList().orEmpty() } val signature = try { httpSignatureHeaderParser.parse(HttpHeaders(headers)) @@ -23,6 +37,12 @@ class HttpSignatureFilter(private val httpSignatureHeaderParser: SignatureHeader } catch (_: RuntimeException) { return "" } + runBlocking { + transaction.transaction { + + apUserService.fetchPerson(signature.keyId) + } + } return signature.keyId } diff --git a/src/main/kotlin/dev/usbharu/hideout/core/infrastructure/springframework/httpsignature/HttpSignatureUserDetailsService.kt b/src/main/kotlin/dev/usbharu/hideout/core/infrastructure/springframework/httpsignature/HttpSignatureUserDetailsService.kt index a2e2a258..83b0c326 100644 --- a/src/main/kotlin/dev/usbharu/hideout/core/infrastructure/springframework/httpsignature/HttpSignatureUserDetailsService.kt +++ b/src/main/kotlin/dev/usbharu/hideout/core/infrastructure/springframework/httpsignature/HttpSignatureUserDetailsService.kt @@ -36,6 +36,7 @@ class HttpSignatureUserDetailsService( try { userQueryService.findByKeyId(keyId) } catch (e: FailedToGetResourcesException) { + throw UsernameNotFoundException("User not found", e) } } diff --git a/src/main/resources/application.yml b/src/main/resources/application.yml index 32d326d7..3762c6c1 100644 --- a/src/main/resources/application.yml +++ b/src/main/resources/application.yml @@ -1,6 +1,6 @@ hideout: url: "https://test-hideout.usbharu.dev" - use-mongodb: false + use-mongodb: true security: jwt: generate: true @@ -18,16 +18,18 @@ spring: WRITE_DATES_AS_TIMESTAMPS: false default-property-inclusion: always datasource: - driver-class-name: org.h2.Driver - url: "jdbc:h2:./test-dev4;MODE=POSTGRESQL" - username: "" + hikari: + transaction-isolation: "TRANSACTION_SERIALIZABLE" + driver-class-name: org.postgresql.Driver + url: "jdbc:postgresql:hideout2" + username: "postgres" password: "" - # data: - # mongodb: - # auto-index-creation: true - # host: localhost - # port: 27017 - # database: hideout + data: + mongodb: + auto-index-creation: true + host: localhost + port: 27017 + database: hideout # username: hideoutuser # password: hideoutpass servlet: diff --git a/src/main/resources/db/migration/V1__Init_DB.sql b/src/main/resources/db/migration/V1__Init_DB.sql index 34da6594..15a61994 100644 --- a/src/main/resources/db/migration/V1__Init_DB.sql +++ b/src/main/resources/db/migration/V1__Init_DB.sql @@ -1,188 +1,188 @@ -CREATE TABLE IF NOT EXISTS "INSTANCE" +create table if not exists instance ( - ID BIGINT PRIMARY KEY, - "NAME" VARCHAR(1000) NOT NULL, - DESCRIPTION VARCHAR(5000) NOT NULL, - URL VARCHAR(255) NOT NULL, - ICON_URL VARCHAR(255) NOT NULL, - SHARED_INBOX VARCHAR(255) NULL, - SOFTWARE VARCHAR(255) NOT NULL, - VERSION VARCHAR(255) NOT NULL, - IS_BLOCKED BOOLEAN NOT NULL, - IS_MUTED BOOLEAN NOT NULL, - MODERATION_NOTE VARCHAR(10000) NOT NULL, - CREATED_AT TIMESTAMP NOT NULL + id bigint primary key, + "name" varchar(1000) not null, + description varchar(5000) not null, + url varchar(255) not null, + icon_url varchar(255) not null, + shared_inbox varchar(255) null, + software varchar(255) not null, + version varchar(255) not null, + is_blocked boolean not null, + is_muted boolean not null, + moderation_note varchar(10000) not null, + created_at timestamp not null ); -CREATE TABLE IF NOT EXISTS USERS +create table if not exists users ( - ID BIGINT PRIMARY KEY, - "NAME" VARCHAR(300) NOT NULL, - "DOMAIN" VARCHAR(1000) NOT NULL, - SCREEN_NAME VARCHAR(300) NOT NULL, - DESCRIPTION VARCHAR(10000) NOT NULL, - PASSWORD VARCHAR(255) NULL, - INBOX VARCHAR(1000) NOT NULL, - OUTBOX VARCHAR(1000) NOT NULL, - URL VARCHAR(1000) NOT NULL, - PUBLIC_KEY VARCHAR(10000) NOT NULL, - PRIVATE_KEY VARCHAR(10000) NULL, - CREATED_AT BIGINT NOT NULL, - KEY_ID VARCHAR(1000) NOT NULL, - "FOLLOWING" VARCHAR(1000) NULL, - FOLLOWERS VARCHAR(1000) NULL, - "INSTANCE" BIGINT NULL, - CONSTRAINT FK_USERS_INSTANCE__ID FOREIGN KEY ("INSTANCE") REFERENCES "INSTANCE" (ID) ON DELETE RESTRICT ON UPDATE RESTRICT + id bigint primary key, + "name" varchar(300) not null, + "domain" varchar(1000) not null, + screen_name varchar(300) not null, + description varchar(10000) not null, + password varchar(255) null, + inbox varchar(1000) not null, + outbox varchar(1000) not null, + url varchar(1000) not null, + public_key varchar(10000) not null, + private_key varchar(10000) null, + created_at bigint not null, + key_id varchar(1000) not null, + "following" varchar(1000) null, + followers varchar(1000) null, + "instance" bigint null, + 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 ( - ID BIGSERIAL PRIMARY KEY, - USER_ID BIGINT NOT NULL, - FOLLOWER_ID BIGINT NOT NULL, - CONSTRAINT FK_FOLLOW_REQUESTS_USER_ID__ID FOREIGN KEY (USER_ID) REFERENCES USERS (ID) ON DELETE RESTRICT ON UPDATE RESTRICT, - CONSTRAINT FK_FOLLOW_REQUESTS_FOLLOWER_ID__ID FOREIGN KEY (FOLLOWER_ID) REFERENCES USERS (ID) ON DELETE RESTRICT ON UPDATE RESTRICT + id bigserial primary key, + user_id bigint not null, + follower_id bigint not null, + constraint fk_follow_requests_user_id__id foreign key (user_id) references users (id) on delete restrict on update restrict, + constraint fk_follow_requests_follower_id__id foreign key (follower_id) references users (id) on delete restrict on update restrict ); -CREATE TABLE IF NOT EXISTS MEDIA +create table if not exists media ( - ID BIGINT PRIMARY KEY, - "NAME" VARCHAR(255) NOT NULL, - URL VARCHAR(255) NOT NULL, - REMOTE_URL VARCHAR(255) NULL, - THUMBNAIL_URL VARCHAR(255) NULL, - "TYPE" INT NOT NULL, - BLURHASH VARCHAR(255) NULL, - MIME_TYPE VARCHAR(255) NOT NULL, - DESCRIPTION VARCHAR(4000) NULL + id bigint primary key, + "name" varchar(255) not null, + url varchar(255) not null, + remote_url varchar(255) null, + thumbnail_url varchar(255) null, + "type" int not null, + blurhash varchar(255) null, + mime_type varchar(255) not null, + description varchar(4000) null ); -CREATE TABLE IF NOT EXISTS META_INFO +create table if not exists meta_info ( - ID BIGINT PRIMARY KEY, - VERSION VARCHAR(1000) NOT NULL, - KID VARCHAR(1000) NOT NULL, - JWT_PRIVATE_KEY VARCHAR(100000) NOT NULL, - JWT_PUBLIC_KEY VARCHAR(100000) NOT NULL + id bigint primary key, + version varchar(1000) not null, + kid varchar(1000) not null, + jwt_private_key varchar(100000) not null, + jwt_public_key varchar(100000) not null ); -CREATE TABLE IF NOT EXISTS POSTS +create table if not exists posts ( - ID BIGINT PRIMARY KEY, - USER_ID BIGINT NOT NULL, - OVERVIEW VARCHAR(100) NULL, - TEXT VARCHAR(3000) NOT NULL, - CREATED_AT BIGINT NOT NULL, - VISIBILITY INT DEFAULT 0 NOT NULL, - URL VARCHAR(500) NOT NULL, - REPOST_ID BIGINT NULL, - REPLY_ID BIGINT NULL, - "SENSITIVE" BOOLEAN DEFAULT FALSE NOT NULL, - AP_ID VARCHAR(100) NOT NULL + id bigint primary key, + user_id bigint not null, + overview varchar(100) null, + text varchar(3000) not null, + created_at bigint not null, + visibility int default 0 not null, + url varchar(500) not null, + repost_id bigint null, + reply_id bigint null, + "sensitive" boolean default false not null, + ap_id varchar(100) not null ); -ALTER TABLE POSTS - ADD CONSTRAINT FK_POSTS_USERID__ID FOREIGN KEY (USER_ID) REFERENCES USERS (ID) ON DELETE RESTRICT ON UPDATE RESTRICT; -ALTER TABLE POSTS - ADD CONSTRAINT FK_POSTS_REPOSTID__ID FOREIGN KEY (REPOST_ID) REFERENCES POSTS (ID) ON DELETE RESTRICT ON UPDATE RESTRICT; -ALTER TABLE POSTS - ADD CONSTRAINT FK_POSTS_REPLYID__ID FOREIGN KEY (REPLY_ID) REFERENCES POSTS (ID) ON DELETE RESTRICT ON UPDATE RESTRICT; -CREATE TABLE IF NOT EXISTS POSTS_MEDIA +alter table posts + add constraint fk_posts_userid__id foreign key (user_id) references users (id) on delete restrict on update restrict; +alter table posts + add constraint fk_posts_repostid__id foreign key (repost_id) references posts (id) on delete restrict on update restrict; +alter table posts + add constraint fk_posts_replyid__id foreign key (reply_id) references posts (id) on delete restrict on update restrict; +create table if not exists posts_media ( - POST_ID BIGINT, - MEDIA_ID BIGINT, - CONSTRAINT pk_PostsMedia PRIMARY KEY (POST_ID, MEDIA_ID) + post_id bigint, + media_id bigint, + constraint pk_postsmedia primary key (post_id, media_id) ); -ALTER TABLE POSTS_MEDIA - ADD CONSTRAINT FK_POSTS_MEDIA_POST_ID__ID FOREIGN KEY (POST_ID) REFERENCES POSTS (ID) ON DELETE CASCADE ON UPDATE CASCADE; -ALTER TABLE POSTS_MEDIA - ADD CONSTRAINT FK_POSTS_MEDIA_MEDIA_ID__ID FOREIGN KEY (MEDIA_ID) REFERENCES MEDIA (ID) ON DELETE CASCADE ON UPDATE CASCADE; -CREATE TABLE IF NOT EXISTS REACTIONS +alter table posts_media + add constraint fk_posts_media_post_id__id foreign key (post_id) references posts (id) on delete cascade on update cascade; +alter table posts_media + add constraint fk_posts_media_media_id__id foreign key (media_id) references media (id) on delete cascade on update cascade; +create table if not exists reactions ( - ID BIGSERIAL PRIMARY KEY, - EMOJI_ID BIGINT NOT NULL, - POST_ID BIGINT NOT NULL, - USER_ID BIGINT NOT NULL + id bigserial primary key, + emoji_id bigint not null, + post_id bigint not null, + user_id bigint not null ); -ALTER TABLE REACTIONS - ADD CONSTRAINT FK_REACTIONS_POST_ID__ID FOREIGN KEY (POST_ID) REFERENCES POSTS (ID) ON DELETE RESTRICT ON UPDATE RESTRICT; -ALTER TABLE REACTIONS - ADD CONSTRAINT FK_REACTIONS_USER_ID__ID FOREIGN KEY (USER_ID) REFERENCES USERS (ID) ON DELETE RESTRICT ON UPDATE RESTRICT; -CREATE TABLE IF NOT EXISTS TIMELINES +alter table reactions + add constraint fk_reactions_post_id__id foreign key (post_id) references posts (id) on delete restrict on update restrict; +alter table reactions + add constraint fk_reactions_user_id__id foreign key (user_id) references users (id) on delete restrict on update restrict; +create table if not exists timelines ( - ID BIGINT PRIMARY KEY, - USER_ID BIGINT NOT NULL, - TIMELINE_ID BIGINT NOT NULL, - POST_ID BIGINT NOT NULL, - POST_USER_ID BIGINT NOT NULL, - CREATED_AT BIGINT NOT NULL, - REPLY_ID BIGINT NULL, - REPOST_ID BIGINT NULL, - VISIBILITY INT NOT NULL, - "SENSITIVE" BOOLEAN NOT NULL, - IS_LOCAL BOOLEAN NOT NULL, - IS_PURE_REPOST BOOLEAN NOT NULL, - MEDIA_IDS VARCHAR(255) NOT NULL + id bigint primary key, + user_id bigint not null, + timeline_id bigint not null, + post_id bigint not null, + post_user_id bigint not null, + created_at bigint not null, + reply_id bigint null, + repost_id bigint null, + visibility int not null, + "sensitive" boolean not null, + is_local boolean not null, + is_pure_repost boolean not null, + media_ids varchar(255) not null ); -CREATE TABLE IF NOT EXISTS USERS_FOLLOWERS +create table if not exists users_followers ( - ID BIGSERIAL PRIMARY KEY, - USER_ID BIGINT NOT NULL, - FOLLOWER_ID BIGINT NOT NULL, - CONSTRAINT FK_USERS_FOLLOWERS_USER_ID__ID FOREIGN KEY (USER_ID) REFERENCES USERS (ID) ON DELETE RESTRICT ON UPDATE RESTRICT, - CONSTRAINT FK_USERS_FOLLOWERS_FOLLOWER_ID__ID FOREIGN KEY (FOLLOWER_ID) REFERENCES USERS (ID) ON DELETE RESTRICT ON UPDATE RESTRICT + id bigserial primary key, + user_id bigint not null, + follower_id bigint not null, + constraint fk_users_followers_user_id__id foreign key (user_id) references users (id) on delete restrict on update restrict, + constraint fk_users_followers_follower_id__id foreign key (follower_id) references users (id) on delete restrict on update restrict ); -CREATE TABLE IF NOT EXISTS APPLICATION_AUTHORIZATION +create table if not exists application_authorization ( - ID VARCHAR(255) PRIMARY KEY, - REGISTERED_CLIENT_ID VARCHAR(255) NOT NULL, - PRINCIPAL_NAME VARCHAR(255) NOT NULL, - AUTHORIZATION_GRANT_TYPE VARCHAR(255) NOT NULL, - AUTHORIZED_SCOPES VARCHAR(1000) DEFAULT NULL NULL, - "ATTRIBUTES" VARCHAR(4000) DEFAULT NULL NULL, - "STATE" VARCHAR(500) DEFAULT NULL NULL, - AUTHORIZATION_CODE_VALUE VARCHAR(4000) DEFAULT NULL NULL, - AUTHORIZATION_CODE_ISSUED_AT TIMESTAMP DEFAULT NULL NULL, - AUTHORIZATION_CODE_EXPIRES_AT TIMESTAMP DEFAULT NULL NULL, - AUTHORIZATION_CODE_METADATA VARCHAR(2000) DEFAULT NULL NULL, - ACCESS_TOKEN_VALUE VARCHAR(4000) DEFAULT NULL NULL, - ACCESS_TOKEN_ISSUED_AT TIMESTAMP DEFAULT NULL NULL, - ACCESS_TOKEN_EXPIRES_AT TIMESTAMP DEFAULT NULL NULL, - ACCESS_TOKEN_METADATA VARCHAR(2000) DEFAULT NULL NULL, - ACCESS_TOKEN_TYPE VARCHAR(255) DEFAULT NULL NULL, - ACCESS_TOKEN_SCOPES VARCHAR(1000) DEFAULT NULL NULL, - REFRESH_TOKEN_VALUE VARCHAR(4000) DEFAULT NULL NULL, - REFRESH_TOKEN_ISSUED_AT TIMESTAMP DEFAULT NULL NULL, - REFRESH_TOKEN_EXPIRES_AT TIMESTAMP DEFAULT NULL NULL, - REFRESH_TOKEN_METADATA VARCHAR(2000) DEFAULT NULL NULL, - OIDC_ID_TOKEN_VALUE VARCHAR(4000) DEFAULT NULL NULL, - OIDC_ID_TOKEN_ISSUED_AT TIMESTAMP DEFAULT NULL NULL, - OIDC_ID_TOKEN_EXPIRES_AT TIMESTAMP DEFAULT NULL NULL, - OIDC_ID_TOKEN_METADATA VARCHAR(2000) DEFAULT NULL NULL, - OIDC_ID_TOKEN_CLAIMS VARCHAR(2000) DEFAULT NULL NULL, - USER_CODE_VALUE VARCHAR(4000) DEFAULT NULL NULL, - USER_CODE_ISSUED_AT TIMESTAMP DEFAULT NULL NULL, - USER_CODE_EXPIRES_AT TIMESTAMP DEFAULT NULL NULL, - USER_CODE_METADATA VARCHAR(2000) DEFAULT NULL NULL, - DEVICE_CODE_VALUE VARCHAR(4000) DEFAULT NULL NULL, - DEVICE_CODE_ISSUED_AT TIMESTAMP DEFAULT NULL NULL, - DEVICE_CODE_EXPIRES_AT TIMESTAMP DEFAULT NULL NULL, - DEVICE_CODE_METADATA VARCHAR(2000) DEFAULT NULL NULL + id varchar(255) primary key, + registered_client_id varchar(255) not null, + principal_name varchar(255) not null, + authorization_grant_type varchar(255) not null, + authorized_scopes varchar(1000) default null null, + "attributes" varchar(4000) default null null, + "state" varchar(500) default null null, + authorization_code_value varchar(4000) default null null, + authorization_code_issued_at timestamp default null null, + authorization_code_expires_at timestamp default null null, + authorization_code_metadata varchar(2000) default null null, + access_token_value varchar(4000) default null null, + access_token_issued_at timestamp default null null, + access_token_expires_at timestamp default null null, + access_token_metadata varchar(2000) default null null, + access_token_type varchar(255) default null null, + access_token_scopes varchar(1000) default null null, + refresh_token_value varchar(4000) default null null, + refresh_token_issued_at timestamp default null null, + refresh_token_expires_at timestamp default null null, + refresh_token_metadata varchar(2000) default null null, + oidc_id_token_value varchar(4000) default null null, + oidc_id_token_issued_at timestamp default null null, + oidc_id_token_expires_at timestamp default null null, + oidc_id_token_metadata varchar(2000) default null null, + oidc_id_token_claims varchar(2000) default null null, + user_code_value varchar(4000) default null null, + user_code_issued_at timestamp default null null, + user_code_expires_at timestamp default null null, + user_code_metadata varchar(2000) default null null, + device_code_value varchar(4000) default null null, + device_code_issued_at timestamp default null null, + device_code_expires_at timestamp default null null, + device_code_metadata varchar(2000) default null null ); -CREATE TABLE IF NOT EXISTS OAUTH2_AUTHORIZATION_CONSENT +create table if not exists oauth2_authorization_consent ( - REGISTERED_CLIENT_ID VARCHAR(100), - PRINCIPAL_NAME VARCHAR(200), - AUTHORITIES VARCHAR(1000) NOT NULL, - CONSTRAINT pk_oauth2_authorization_consent PRIMARY KEY (REGISTERED_CLIENT_ID, PRINCIPAL_NAME) + registered_client_id varchar(100), + principal_name varchar(200), + authorities varchar(1000) not null, + constraint pk_oauth2_authorization_consent primary key (registered_client_id, principal_name) ); -CREATE TABLE IF NOT EXISTS REGISTERED_CLIENT +create table if not exists registered_client ( - ID VARCHAR(100) PRIMARY KEY, - CLIENT_ID VARCHAR(100) NOT NULL, - CLIENT_ID_ISSUED_AT TIMESTAMP DEFAULT CURRENT_TIMESTAMP NOT NULL, - CLIENT_SECRET VARCHAR(200) DEFAULT NULL NULL, - CLIENT_SECRET_EXPIRES_AT TIMESTAMP DEFAULT NULL NULL, - CLIENT_NAME VARCHAR(200) NOT NULL, - CLIENT_AUTHENTICATION_METHODS VARCHAR(1000) NOT NULL, - AUTHORIZATION_GRANT_TYPES VARCHAR(1000) NOT NULL, - REDIRECT_URIS VARCHAR(1000) DEFAULT NULL NULL, - POST_LOGOUT_REDIRECT_URIS VARCHAR(1000) DEFAULT NULL NULL, - SCOPES VARCHAR(1000) NOT NULL, - CLIENT_SETTINGS VARCHAR(2000) NOT NULL, - TOKEN_SETTINGS VARCHAR(2000) NOT NULL + id varchar(100) primary key, + client_id varchar(100) not null, + client_id_issued_at timestamp default current_timestamp not null, + client_secret varchar(200) default null null, + client_secret_expires_at timestamp default null null, + client_name varchar(200) not null, + client_authentication_methods varchar(1000) not null, + authorization_grant_types varchar(1000) not null, + redirect_uris varchar(1000) default null null, + post_logout_redirect_uris varchar(1000) default null null, + scopes varchar(1000) not null, + client_settings varchar(2000) not null, + token_settings varchar(2000) not null ) diff --git a/src/main/resources/logback.xml b/src/main/resources/logback.xml index 1f2e9e02..9ba872ba 100644 --- a/src/main/resources/logback.xml +++ b/src/main/resources/logback.xml @@ -4,7 +4,7 @@ %d{YYYY-MM-dd HH:mm:ss.SSS} [%thread] %-5level [%X{x-request-id}] %logger{36} - %msg%n - + From f50c85315c63b1041ebb9b9ebae8946b64fa377a Mon Sep 17 00:00:00 2001 From: usbharu <64310155+usbharu@users.noreply.github.com> Date: Wed, 6 Dec 2023 18:12:35 +0900 Subject: [PATCH 04/19] =?UTF-8?q?fix:=20#190=20PostgreSQL=E3=82=92?= =?UTF-8?q?=E4=BD=BF=E7=94=A8=E3=81=97=E3=81=A6=E3=81=84=E3=82=8B=E3=81=A8?= =?UTF-8?q?=E3=81=8D=E3=81=ABOAuth2=E3=82=AF=E3=83=A9=E3=82=A4=E3=82=A2?= =?UTF-8?q?=E3=83=B3=E3=83=88=E3=82=92=E4=BD=9C=E6=88=90=E3=81=A7=E3=81=8D?= =?UTF-8?q?=E3=81=AA=E3=81=84=E5=95=8F=E9=A1=8C=E3=82=92=E4=BF=AE=E6=AD=A3?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../dev/usbharu/hideout/mastodon/service/app/AppApiService.kt | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/main/kotlin/dev/usbharu/hideout/mastodon/service/app/AppApiService.kt b/src/main/kotlin/dev/usbharu/hideout/mastodon/service/app/AppApiService.kt index d2306123..7324dd33 100644 --- a/src/main/kotlin/dev/usbharu/hideout/mastodon/service/app/AppApiService.kt +++ b/src/main/kotlin/dev/usbharu/hideout/mastodon/service/app/AppApiService.kt @@ -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.stereotype.Service import java.time.Duration -import java.time.Instant import java.util.* @Service @@ -46,7 +45,7 @@ class AppApiServiceImpl( .tokenSettings( TokenSettings.builder() .accessTokenTimeToLive( - Duration.ofSeconds((Instant.MAX.epochSecond - Instant.now().epochSecond - 10000) / 1000) + Duration.ofSeconds(31536000000) ) .build() ) From 0b1d4d0666fef1f4ce83def2c634c63f10117b64 Mon Sep 17 00:00:00 2001 From: usbharu <64310155+usbharu@users.noreply.github.com> Date: Wed, 6 Dec 2023 18:19:33 +0900 Subject: [PATCH 05/19] =?UTF-8?q?fix:=20#191=20Image=E3=81=AEmediaType?= =?UTF-8?q?=E3=82=92nullable=E3=81=AB?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../hideout/activitypub/domain/model/Image.kt | 15 +++++++++++---- 1 file changed, 11 insertions(+), 4 deletions(-) diff --git a/src/main/kotlin/dev/usbharu/hideout/activitypub/domain/model/Image.kt b/src/main/kotlin/dev/usbharu/hideout/activitypub/domain/model/Image.kt index 5b63ef5e..c3e4649a 100644 --- a/src/main/kotlin/dev/usbharu/hideout/activitypub/domain/model/Image.kt +++ b/src/main/kotlin/dev/usbharu/hideout/activitypub/domain/model/Image.kt @@ -4,12 +4,11 @@ import dev.usbharu.hideout.activitypub.domain.model.objects.Object open class Image( type: List = emptyList(), - val mediaType: String, + val mediaType: String? = null, val url: String ) : Object( add(type, "Image") ) { - override fun equals(other: Any?): Boolean { if (this === other) return true if (javaClass != other?.javaClass) return false @@ -25,10 +24,18 @@ open class Image( override fun hashCode(): Int { var result = super.hashCode() - result = 31 * result + mediaType.hashCode() + result = 31 * result + (mediaType?.hashCode() ?: 0) result = 31 * result + url.hashCode() 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()}" + } + + } From c6311d232920e2821591c6c44cc17d557d1b4847 Mon Sep 17 00:00:00 2001 From: usbharu <64310155+usbharu@users.noreply.github.com> Date: Wed, 6 Dec 2023 18:23:56 +0900 Subject: [PATCH 06/19] =?UTF-8?q?fix:=20#191=20Document=E3=81=AEname?= =?UTF-8?q?=E3=81=8Cnull=E3=81=AE=E3=81=A8=E3=81=8D=E7=A9=BA=E6=96=87?= =?UTF-8?q?=E5=AD=97=E3=81=AB=E3=81=AA=E3=82=8B=E3=82=88=E3=81=86=E3=81=AB?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../dev/usbharu/hideout/activitypub/domain/model/Document.kt | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/main/kotlin/dev/usbharu/hideout/activitypub/domain/model/Document.kt b/src/main/kotlin/dev/usbharu/hideout/activitypub/domain/model/Document.kt index d8b7ff7e..c6c20250 100644 --- a/src/main/kotlin/dev/usbharu/hideout/activitypub/domain/model/Document.kt +++ b/src/main/kotlin/dev/usbharu/hideout/activitypub/domain/model/Document.kt @@ -1,9 +1,12 @@ 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 open class Document( type: List = emptyList(), + @JsonSetter(nulls = Nulls.AS_EMPTY) override val name: String = "", val mediaType: String, val url: String From 05f5e78d97d255992e42cd2b7cb3d7f8a9fe7dc6 Mon Sep 17 00:00:00 2001 From: usbharu <64310155+usbharu@users.noreply.github.com> Date: Wed, 6 Dec 2023 20:46:20 +0900 Subject: [PATCH 07/19] =?UTF-8?q?fix:=20=E4=B8=8D=E8=A6=81=E3=81=AA?= =?UTF-8?q?=E3=83=88=E3=83=A9=E3=83=B3=E3=82=B6=E3=82=AF=E3=82=B7=E3=83=A7?= =?UTF-8?q?=E3=83=B3=E5=AE=A3=E8=A8=80=E3=82=92=E5=89=8A=E9=99=A4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../service/inbox/InboxJobProcessor.kt | 24 +++++++++---------- 1 file changed, 12 insertions(+), 12 deletions(-) diff --git a/src/main/kotlin/dev/usbharu/hideout/activitypub/service/inbox/InboxJobProcessor.kt b/src/main/kotlin/dev/usbharu/hideout/activitypub/service/inbox/InboxJobProcessor.kt index 65220c41..9ca3e982 100644 --- a/src/main/kotlin/dev/usbharu/hideout/activitypub/service/inbox/InboxJobProcessor.kt +++ b/src/main/kotlin/dev/usbharu/hideout/activitypub/service/inbox/InboxJobProcessor.kt @@ -99,22 +99,22 @@ class InboxJobProcessor( val verify = signature?.let { verifyHttpSignature(httpRequest, it, transaction) } ?: false - transaction.transaction { - logger.debug("Is verifying success? {}", verify) - val activityPubProcessor = - activityPubProcessorList.firstOrNull { it.isSupported(param.type) } as ActivityPubProcessor? + logger.debug("Is verifying success? {}", verify) - if (activityPubProcessor == null) { - logger.warn("ActivityType {} is not support.", param.type) - throw IllegalStateException("ActivityPubProcessor not found.") - } + val activityPubProcessor = + activityPubProcessorList.firstOrNull { it.isSupported(param.type) } as ActivityPubProcessor? - val value = objectMapper.treeToValue(jsonNode, activityPubProcessor.type()) - activityPubProcessor.process(ActivityPubProcessContext(value, jsonNode, httpRequest, signature, verify)) - - logger.info("SUCCESS Process inbox. type: {}", param.type) + if (activityPubProcessor == null) { + logger.warn("ActivityType {} is not support.", param.type) + throw IllegalStateException("ActivityPubProcessor not found.") } + + val value = objectMapper.treeToValue(jsonNode, activityPubProcessor.type()) + activityPubProcessor.process(ActivityPubProcessContext(value, jsonNode, httpRequest, signature, verify)) + + logger.info("SUCCESS Process inbox. type: {}", param.type) + } override fun job(): InboxJob = InboxJob From cde817911e13b431fe6efa0f9e9c11850e8ab622 Mon Sep 17 00:00:00 2001 From: usbharu <64310155+usbharu@users.noreply.github.com> Date: Wed, 6 Dec 2023 20:46:46 +0900 Subject: [PATCH 08/19] =?UTF-8?q?feat:=20=E3=82=B8=E3=83=A7=E3=83=96?= =?UTF-8?q?=E3=82=AD=E3=83=A5=E3=83=BC=E3=81=AE=E3=83=AD=E3=82=B0=E3=81=AB?= =?UTF-8?q?jobId=E3=82=92=E8=BF=BD=E5=8A=A0=E3=81=99=E3=82=8B=E3=82=88?= =?UTF-8?q?=E3=81=86=E3=81=AB?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../kjobexposed/KJobJobQueueWorkerService.kt | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/src/main/kotlin/dev/usbharu/hideout/core/infrastructure/kjobexposed/KJobJobQueueWorkerService.kt b/src/main/kotlin/dev/usbharu/hideout/core/infrastructure/kjobexposed/KJobJobQueueWorkerService.kt index a03272c4..ff8e07e8 100644 --- a/src/main/kotlin/dev/usbharu/hideout/core/infrastructure/kjobexposed/KJobJobQueueWorkerService.kt +++ b/src/main/kotlin/dev/usbharu/hideout/core/infrastructure/kjobexposed/KJobJobQueueWorkerService.kt @@ -8,6 +8,7 @@ import kjob.core.dsl.JobRegisterContext import kjob.core.dsl.KJobFunctions import kjob.core.kjob import org.jetbrains.exposed.sql.transactions.TransactionManager +import org.slf4j.MDC import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty import org.springframework.stereotype.Service @@ -35,8 +36,13 @@ class KJobJobQueueWorkerService(private val jobQueueProcessorList: List Date: Wed, 6 Dec 2023 21:11:59 +0900 Subject: [PATCH 09/19] =?UTF-8?q?feat:=20#193=20#194=20=E3=83=A6=E3=83=BC?= =?UTF-8?q?=E3=82=B6=E3=83=BC=E8=A9=B3=E7=B4=B0=E7=94=BB=E9=9D=A2=E3=81=A7?= =?UTF-8?q?=E4=BD=BF=E3=82=8F=E3=82=8C=E3=82=8BAPI=E3=82=92=E5=AE=9F?= =?UTF-8?q?=E8=A3=85?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../exposedquery/StatusQueryServiceImpl.kt | 68 ++++++++++- .../account/MastodonAccountApiController.kt | 53 ++++++++- .../mastodon/query/StatusQueryService.kt | 30 +++++ .../service/account/AccountApiService.kt | 98 +++++++++++++++- src/main/resources/openapi/mastodon.yaml | 106 ++++++++++++++++++ 5 files changed, 345 insertions(+), 10 deletions(-) diff --git a/src/main/kotlin/dev/usbharu/hideout/mastodon/infrastructure/exposedquery/StatusQueryServiceImpl.kt b/src/main/kotlin/dev/usbharu/hideout/mastodon/infrastructure/exposedquery/StatusQueryServiceImpl.kt index 8010405d..9c5b46fe 100644 --- a/src/main/kotlin/dev/usbharu/hideout/mastodon/infrastructure/exposedquery/StatusQueryServiceImpl.kt +++ b/src/main/kotlin/dev/usbharu/hideout/mastodon/infrastructure/exposedquery/StatusQueryServiceImpl.kt @@ -4,9 +4,11 @@ import dev.usbharu.hideout.core.domain.model.media.toMediaAttachments import dev.usbharu.hideout.core.infrastructure.exposedrepository.* 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.Visibility.* import dev.usbharu.hideout.mastodon.interfaces.api.status.StatusQuery import dev.usbharu.hideout.mastodon.query.StatusQueryService import org.jetbrains.exposed.sql.ResultRow +import org.jetbrains.exposed.sql.andWhere import org.jetbrains.exposed.sql.select import org.springframework.stereotype.Repository 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 { + 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>): List { val statuses = pairs.map { it.first } return pairs @@ -111,11 +169,11 @@ private fun toStatus(it: ResultRow) = Status( ), content = it[Posts.text], visibility = when (it[Posts.visibility]) { - 0 -> Status.Visibility.public - 1 -> Status.Visibility.unlisted - 2 -> Status.Visibility.private - 3 -> Status.Visibility.direct - else -> Status.Visibility.public + 0 -> public + 1 -> unlisted + 2 -> private + 3 -> direct + else -> public }, sensitive = it[Posts.sensitive], spoilerText = it[Posts.overview].orEmpty(), 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 f3c05765..a7f3741c 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 @@ -3,11 +3,11 @@ package dev.usbharu.hideout.mastodon.interfaces.api.account import dev.usbharu.hideout.application.external.Transaction import dev.usbharu.hideout.controller.mastodon.generated.AccountApi import dev.usbharu.hideout.core.service.user.UserCreateDto -import dev.usbharu.hideout.domain.mastodon.model.generated.Account -import dev.usbharu.hideout.domain.mastodon.model.generated.CredentialAccount -import dev.usbharu.hideout.domain.mastodon.model.generated.FollowRequestBody -import dev.usbharu.hideout.domain.mastodon.model.generated.Relationship +import dev.usbharu.hideout.domain.mastodon.model.generated.* 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.HttpStatus import org.springframework.http.ResponseEntity @@ -58,4 +58,49 @@ class MastodonAccountApiController( httpHeaders.location = URI("/users/$username") 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> = runBlocking { + val principal = SecurityContextHolder.getContext().getAuthentication().principal as Jwt + + val userid = principal.getClaim("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?, + withSuspended: Boolean + ): ResponseEntity> = runBlocking { + val principal = SecurityContextHolder.getContext().getAuthentication().principal as Jwt + + val userid = principal.getClaim("uid").toLong() + + ResponseEntity.ok( + accountApiService.relationships(userid, id.orEmpty().mapNotNull { it.toLongOrNull() }, withSuspended) + .asFlow() + ) + } } diff --git a/src/main/kotlin/dev/usbharu/hideout/mastodon/query/StatusQueryService.kt b/src/main/kotlin/dev/usbharu/hideout/mastodon/query/StatusQueryService.kt index a5777360..2b4e2a31 100644 --- a/src/main/kotlin/dev/usbharu/hideout/mastodon/query/StatusQueryService.kt +++ b/src/main/kotlin/dev/usbharu/hideout/mastodon/query/StatusQueryService.kt @@ -6,4 +6,34 @@ import dev.usbharu.hideout.mastodon.interfaces.api.status.StatusQuery interface StatusQueryService { suspend fun findByPostIds(ids: List): List suspend fun findByPostIdsWithMediaIds(statusQueries: List): List + + /** + * アカウントの投稿一覧を取得します + * + * @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 } 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 b2e2792a..7a3a6820 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 @@ -6,14 +6,31 @@ import dev.usbharu.hideout.core.query.FollowerQueryService import dev.usbharu.hideout.core.service.user.UserCreateDto import dev.usbharu.hideout.core.service.user.UserService import dev.usbharu.hideout.domain.mastodon.model.generated.* +import dev.usbharu.hideout.mastodon.query.StatusQueryService +import org.slf4j.LoggerFactory import org.springframework.stereotype.Service @Service 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 + suspend fun verifyCredentials(userid: Long): CredentialAccount 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, withSuspended: Boolean): List } @Service @@ -22,9 +39,48 @@ class AccountApiServiceImpl( private val transaction: Transaction, private val userService: UserService, private val followerQueryService: FollowerQueryService, - private val userRepository: UserRepository + private val userRepository: UserRepository, + private val statusQueryService: StatusQueryService ) : 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 { + 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 { val account = accountService.findById(userid) from(account) @@ -70,6 +126,42 @@ class AccountApiServiceImpl( return@transaction accountService.findById(id) } + override suspend fun relationships(userid: Long, id: List, withSuspended: Boolean): List { + if (id.isEmpty()) { + return emptyList() + } + + + logger.warn("id is too long! ({}) truncate to 20", id.size) + + val subList = id.subList(0, 20) + + return 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 { return CredentialAccount( id = account.id, @@ -107,4 +199,8 @@ class AccountApiServiceImpl( role = Role(0, "Admin", "", 32) ) } + + companion object { + private val logger = LoggerFactory.getLogger(AccountApiServiceImpl::class.java) + } } diff --git a/src/main/resources/openapi/mastodon.yaml b/src/main/resources/openapi/mastodon.yaml index 03fa7a69..5d39ad40 100644 --- a/src/main/resources/openapi/mastodon.yaml +++ b/src/main/resources/openapi/mastodon.yaml @@ -206,6 +206,37 @@ paths: 200: 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: @@ -255,6 +286,81 @@ paths: 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: get: tags: From cb8b8c916b17955faa26d08692ec3286435dd850 Mon Sep 17 00:00:00 2001 From: usbharu <64310155+usbharu@users.noreply.github.com> Date: Thu, 7 Dec 2023 00:02:41 +0900 Subject: [PATCH 10/19] =?UTF-8?q?feat:=20Twidere=E3=81=A7=E3=83=95?= =?UTF-8?q?=E3=82=A9=E3=83=AD=E3=83=BC=E3=83=9C=E3=82=BF=E3=83=B3=E3=81=8C?= =?UTF-8?q?=E8=A1=A8=E7=A4=BA=E3=81=95=E3=82=8C=E3=82=8B=E3=82=88=E3=81=86?= =?UTF-8?q?=E3=81=AB?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../hideout/application/config/SecurityConfig.kt | 4 +++- .../service/account/AccountApiService.kt | 10 ++++++---- .../mastodon/service/account/AccountService.kt | 16 +++++++++++----- src/main/resources/openapi/mastodon.yaml | 2 +- 4 files changed, 21 insertions(+), 11 deletions(-) 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 59caf58d..fd129a61 100644 --- a/src/main/kotlin/dev/usbharu/hideout/application/config/SecurityConfig.kt +++ b/src/main/kotlin/dev/usbharu/hideout/application/config/SecurityConfig.kt @@ -186,11 +186,13 @@ class SecurityConfig { authorize(POST, "/api/v1/accounts", permitAll) authorize("/auth/sign_up", hasRole("ANONYMOUS")) - authorize(GET, "/files", permitAll) + authorize(GET, "/files/*", permitAll) authorize(GET, "/users/*/icon.jpg", permitAll) authorize(GET, "/users/*/header.jpg", permitAll) authorize(GET, "/api/v1/accounts/verify_credentials", hasAnyScope("read", "read:accounts")) + authorize(GET, "/api/v1/accounts/*", permitAll) + authorize(GET, "/api/v1/accounts/*/statuses", permitAll) authorize(POST, "/api/v1/media", hasAnyScope("write", "write:media")) authorize(POST, "/api/v1/statuses", hasAnyScope("write", "write:statuses")) 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 7a3a6820..946a6710 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 @@ -9,6 +9,7 @@ import dev.usbharu.hideout.domain.mastodon.model.generated.* import dev.usbharu.hideout.mastodon.query.StatusQueryService import org.slf4j.LoggerFactory import org.springframework.stereotype.Service +import kotlin.math.min @Service interface AccountApiService { @@ -126,17 +127,18 @@ class AccountApiServiceImpl( return@transaction accountService.findById(id) } - override suspend fun relationships(userid: Long, id: List, withSuspended: Boolean): List { + override suspend fun relationships(userid: Long, id: List, withSuspended: Boolean): List = + transaction.transaction { if (id.isEmpty()) { - return emptyList() + return@transaction emptyList() } logger.warn("id is too long! ({}) truncate to 20", id.size) - val subList = id.subList(0, 20) + val subList = id.subList(0, min(id.size, 20)) - return subList.map { + return@transaction subList.map { val alreadyFollow = followerQueryService.alreadyFollow(userid, it) diff --git a/src/main/kotlin/dev/usbharu/hideout/mastodon/service/account/AccountService.kt b/src/main/kotlin/dev/usbharu/hideout/mastodon/service/account/AccountService.kt index a7f5f766..72050167 100644 --- a/src/main/kotlin/dev/usbharu/hideout/mastodon/service/account/AccountService.kt +++ b/src/main/kotlin/dev/usbharu/hideout/mastodon/service/account/AccountService.kt @@ -1,5 +1,6 @@ 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.domain.mastodon.model.generated.Account import org.springframework.stereotype.Service @@ -10,9 +11,14 @@ interface AccountService { } @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 { val findById = userQueryService.findById(id) + val userUrl = applicationConfig.url.toString() + "/users/" + findById.id.toString() + return Account( id = findById.id.toString(), username = findById.name, @@ -20,10 +26,10 @@ class AccountServiceImpl(private val userQueryService: UserQueryService) : Accou url = findById.url, displayName = findById.screenName, note = findById.description, - avatar = findById.url + "/icon.jpg", - avatarStatic = findById.url + "/icon.jpg", - header = findById.url + "/header.jpg", - headerStatic = findById.url + "/header.jpg", + avatar = "$userUrl/icon.jpg", + avatarStatic = "$userUrl/icon.jpg", + header = "$userUrl/header.jpg", + headerStatic = "$userUrl/header.jpg", locked = false, fields = emptyList(), emojis = emptyList(), diff --git a/src/main/resources/openapi/mastodon.yaml b/src/main/resources/openapi/mastodon.yaml index 5d39ad40..3049c1d9 100644 --- a/src/main/resources/openapi/mastodon.yaml +++ b/src/main/resources/openapi/mastodon.yaml @@ -215,7 +215,7 @@ paths: - "read:follows" parameters: - in: query - name: id + name: id[] required: false schema: type: array From 08d72e4e0500e5be4a57839a411a48fda01a68dd Mon Sep 17 00:00:00 2001 From: usbharu <64310155+usbharu@users.noreply.github.com> Date: Thu, 7 Dec 2023 01:08:48 +0900 Subject: [PATCH 11/19] =?UTF-8?q?fix:=20AP=E3=81=AB=E9=85=8D=E4=BF=A1?= =?UTF-8?q?=E3=81=99=E3=82=8B=E3=82=A2=E3=82=A4=E3=82=B3=E3=83=B3=E3=81=AE?= =?UTF-8?q?URL=E3=82=92=E4=BF=AE=E6=AD=A3?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../activitypub/service/objects/user/APUserService.kt | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/main/kotlin/dev/usbharu/hideout/activitypub/service/objects/user/APUserService.kt b/src/main/kotlin/dev/usbharu/hideout/activitypub/service/objects/user/APUserService.kt index fff97e01..a7efb238 100644 --- a/src/main/kotlin/dev/usbharu/hideout/activitypub/service/objects/user/APUserService.kt +++ b/src/main/kotlin/dev/usbharu/hideout/activitypub/service/objects/user/APUserService.kt @@ -58,8 +58,8 @@ class APUserServiceImpl( url = userUrl, icon = Image( type = emptyList(), - mediaType = "image/png", - url = "$userUrl/icon.png" + mediaType = "image/jpeg", + url = "$userUrl/icon.jpg" ), publicKey = Key( id = userEntity.keyId, @@ -124,8 +124,8 @@ class APUserServiceImpl( url = id, icon = Image( type = emptyList(), - mediaType = "image/png", - url = "$id/icon.png" + mediaType = "image/jpeg", + url = "$id/icon.jpg" ), publicKey = Key( id = userEntity.keyId, From b8271a522a249b78ea816e719f1a628987b51998 Mon Sep 17 00:00:00 2001 From: usbharu <64310155+usbharu@users.noreply.github.com> Date: Thu, 7 Dec 2023 01:09:47 +0900 Subject: [PATCH 12/19] =?UTF-8?q?feat:=20ActivityPub=E3=83=AA=E3=82=BD?= =?UTF-8?q?=E3=83=BC=E3=82=B9=E3=81=B8=E3=82=A2=E3=82=AF=E3=82=BB=E3=82=B9?= =?UTF-8?q?=E3=81=A7=E3=81=8D=E3=81=AA=E3=81=8F=E3=81=AA=E3=81=A3=E3=81=A6?= =?UTF-8?q?=E3=81=84=E3=81=9F=E3=81=AE=E3=82=92=E4=BF=AE=E6=AD=A3?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../dev/usbharu/hideout/application/config/SecurityConfig.kt | 2 ++ 1 file changed, 2 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 fd129a61..ec87b8e8 100644 --- a/src/main/kotlin/dev/usbharu/hideout/application/config/SecurityConfig.kt +++ b/src/main/kotlin/dev/usbharu/hideout/application/config/SecurityConfig.kt @@ -180,6 +180,8 @@ class SecurityConfig { authorize(POST, "/inbox", permitAll) authorize(POST, "/users/*/inbox", permitAll) + authorize(GET, "/users/*", permitAll) + authorize(GET, "/users/*/posts/*", permitAll) authorize(POST, "/api/v1/apps", permitAll) authorize(GET, "/api/v1/instance/**", permitAll) From 63536aca2d4c09921d4d8dddb587412a7051787a Mon Sep 17 00:00:00 2001 From: usbharu <64310155+usbharu@users.noreply.github.com> Date: Thu, 7 Dec 2023 01:10:33 +0900 Subject: [PATCH 13/19] =?UTF-8?q?fix:=20Misskey=E3=81=AE=E8=84=86=E5=BC=B1?= =?UTF-8?q?=E6=80=A7=E4=BF=AE=E6=AD=A3=E3=81=A7HTTP=20Signature=E3=81=AE?= =?UTF-8?q?=E6=A4=9C=E8=A8=BC=E3=82=A8=E3=83=A9=E3=83=BC=E3=81=8C=E5=87=BA?= =?UTF-8?q?=E3=81=A6=E9=80=A3=E5=90=88=E3=81=A7=E3=81=8D=E3=81=AA=E3=81=8F?= =?UTF-8?q?=E3=81=AA=E3=81=A3=E3=81=A6=E3=81=84=E3=81=9F=E5=95=8F=E9=A1=8C?= =?UTF-8?q?=E3=82=92=E4=BF=AE=E6=AD=A3?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../activitypub/service/common/APRequestServiceImpl.kt | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/main/kotlin/dev/usbharu/hideout/activitypub/service/common/APRequestServiceImpl.kt b/src/main/kotlin/dev/usbharu/hideout/activitypub/service/common/APRequestServiceImpl.kt index 511d3e67..9721cf38 100644 --- a/src/main/kotlin/dev/usbharu/hideout/activitypub/service/common/APRequestServiceImpl.kt +++ b/src/main/kotlin/dev/usbharu/hideout/activitypub/service/common/APRequestServiceImpl.kt @@ -84,7 +84,7 @@ class APRequestServiceImpl( headers { appendAll(headers) append("Signature", sign.signatureHeader) - remove("Host") +// remove("Host") } } contentType(Activity) @@ -173,7 +173,7 @@ class APRequestServiceImpl( append("Accept", Activity) append("Date", date) append("Host", u.host) - append("Digest", "sha-256=$digest") + append("Digest", "SHA-256=$digest") } val sign = httpSignatureSigner.sign( @@ -193,7 +193,7 @@ class APRequestServiceImpl( headers { appendAll(headers) append("Signature", sign.signatureHeader) - remove("Host") +// remove("Host") } setBody(requestBody) contentType(Activity) From bec8af63886a43a13421d4b79293b62079f15867 Mon Sep 17 00:00:00 2001 From: usbharu <64310155+usbharu@users.noreply.github.com> Date: Thu, 7 Dec 2023 10:32:54 +0900 Subject: [PATCH 14/19] =?UTF-8?q?test:=20account=20api=20=E3=81=AEUnit?= =?UTF-8?q?=E3=83=86=E3=82=B9=E3=83=88=E3=82=92=E8=BF=BD=E5=8A=A0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../MastodonAccountApiControllerTest.kt | 16 + .../account/AccountApiServiceImplTest.kt | 330 ++++++++++++++++++ 2 files changed, 346 insertions(+) create mode 100644 src/test/kotlin/dev/usbharu/hideout/mastodon/service/account/AccountApiServiceImplTest.kt diff --git a/src/test/kotlin/dev/usbharu/hideout/mastodon/interfaces/api/account/MastodonAccountApiControllerTest.kt b/src/test/kotlin/dev/usbharu/hideout/mastodon/interfaces/api/account/MastodonAccountApiControllerTest.kt index ff114d93..895f33a4 100644 --- a/src/test/kotlin/dev/usbharu/hideout/mastodon/interfaces/api/account/MastodonAccountApiControllerTest.kt +++ b/src/test/kotlin/dev/usbharu/hideout/mastodon/interfaces/api/account/MastodonAccountApiControllerTest.kt @@ -125,4 +125,20 @@ class MastodonAccountApiControllerTest { .andExpect { header { string("location", "/users/hoge") } } .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() } } + + } } diff --git a/src/test/kotlin/dev/usbharu/hideout/mastodon/service/account/AccountApiServiceImplTest.kt b/src/test/kotlin/dev/usbharu/hideout/mastodon/service/account/AccountApiServiceImplTest.kt new file mode 100644 index 00000000..5cc6a833 --- /dev/null +++ b/src/test/kotlin/dev/usbharu/hideout/mastodon/service/account/AccountApiServiceImplTest.kt @@ -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) + } +} From a89a560742786421694b5a0afe98c5b6632f0a84 Mon Sep 17 00:00:00 2001 From: usbharu <64310155+usbharu@users.noreply.github.com> Date: Thu, 7 Dec 2023 11:14:32 +0900 Subject: [PATCH 15/19] =?UTF-8?q?test:=20accounts=20api=20=E3=81=AE?= =?UTF-8?q?=E7=B5=90=E5=90=88=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 | 100 ++++++++++++++++++ src/intTest/resources/sql/test-user2.sql | 10 ++ 2 files changed, 110 insertions(+) create mode 100644 src/intTest/resources/sql/test-user2.sql diff --git a/src/intTest/kotlin/mastodon/account/AccountApiTest.kt b/src/intTest/kotlin/mastodon/account/AccountApiTest.kt index fb8d66c6..fa419c24 100644 --- a/src/intTest/kotlin/mastodon/account/AccountApiTest.kt +++ b/src/intTest/kotlin/mastodon/account/AccountApiTest.kt @@ -14,6 +14,7 @@ import org.springframework.http.MediaType import org.springframework.security.core.authority.SimpleGrantedAuthority 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.csrf import org.springframework.security.test.web.servlet.request.SecurityMockMvcRequestPostProcessors.jwt import org.springframework.security.test.web.servlet.setup.SecurityMockMvcConfigurers.springSecurity import org.springframework.test.context.jdbc.Sql @@ -29,6 +30,7 @@ import org.springframework.web.context.WebApplicationContext @AutoConfigureMockMvc @Transactional @Sql("/sql/test-user.sql", executionPhase = Sql.ExecutionPhase.BEFORE_TEST_CLASS) +@Sql("/sql/test-user2.sql", executionPhase = Sql.ExecutionPhase.BEFORE_TEST_CLASS) class AccountApiTest { @Autowired @@ -159,6 +161,104 @@ class AccountApiTest { .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() } } + } + companion object { @JvmStatic @AfterAll diff --git a/src/intTest/resources/sql/test-user2.sql b/src/intTest/resources/sql/test-user2.sql new file mode 100644 index 00000000..3c305417 --- /dev/null +++ b/src/intTest/resources/sql/test-user2.sql @@ -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', 'localhost', '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); From 7edca4a21307c980db0259f95ec6e49fecc7a63a Mon Sep 17 00:00:00 2001 From: usbharu <64310155+usbharu@users.noreply.github.com> Date: Thu, 7 Dec 2023 11:14:51 +0900 Subject: [PATCH 16/19] =?UTF-8?q?fix:=20OAuth2=E3=81=AE=E3=82=B9=E3=82=B3?= =?UTF-8?q?=E3=83=BC=E3=83=97=E3=82=92=E4=BF=AE=E6=AD=A3?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../dev/usbharu/hideout/application/config/SecurityConfig.kt | 2 ++ 1 file changed, 2 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 ec87b8e8..eb19802f 100644 --- a/src/main/kotlin/dev/usbharu/hideout/application/config/SecurityConfig.kt +++ b/src/main/kotlin/dev/usbharu/hideout/application/config/SecurityConfig.kt @@ -193,8 +193,10 @@ class SecurityConfig { authorize(GET, "/users/*/header.jpg", permitAll) 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/statuses", hasAnyScope("write", "write:statuses")) From 7e6b3854f859d5763af6b6fac8c53182bf1d03fd Mon Sep 17 00:00:00 2001 From: usbharu <64310155+usbharu@users.noreply.github.com> Date: Thu, 7 Dec 2023 11:53:02 +0900 Subject: [PATCH 17/19] =?UTF-8?q?test:=20=E3=83=95=E3=82=A9=E3=83=AD?= =?UTF-8?q?=E3=83=BC=E3=81=AE=E7=B5=90=E5=90=88=E3=83=86=E3=82=B9=E3=83=88?= =?UTF-8?q?=E3=82=92=E8=BF=BD=E5=8A=A0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../kotlin/mastodon/account/AccountApiTest.kt | 22 +++++++++++++++++++ src/intTest/resources/application.yml | 2 +- ...iV1AccountsIdFollowPost フォローできる.sql | 18 +++++++++++++++ .../core/service/user/UserServiceImpl.kt | 3 +++ .../resources/db/migration/V1__Init_DB.sql | 2 +- 5 files changed, 45 insertions(+), 2 deletions(-) create mode 100644 src/intTest/resources/sql/accounts/apiV1AccountsIdFollowPost フォローできる.sql diff --git a/src/intTest/kotlin/mastodon/account/AccountApiTest.kt b/src/intTest/kotlin/mastodon/account/AccountApiTest.kt index fa419c24..4798f574 100644 --- a/src/intTest/kotlin/mastodon/account/AccountApiTest.kt +++ b/src/intTest/kotlin/mastodon/account/AccountApiTest.kt @@ -1,8 +1,10 @@ package mastodon.account import dev.usbharu.hideout.SpringApplication +import dev.usbharu.hideout.core.infrastructure.exposedquery.FollowerQueryServiceImpl import dev.usbharu.hideout.core.infrastructure.exposedquery.UserQueryServiceImpl import kotlinx.coroutines.test.runTest +import org.assertj.core.api.Assertions.assertThat import org.flywaydb.core.Flyway import org.junit.jupiter.api.AfterAll import org.junit.jupiter.api.BeforeEach @@ -33,9 +35,13 @@ import org.springframework.web.context.WebApplicationContext @Sql("/sql/test-user2.sql", executionPhase = Sql.ExecutionPhase.BEFORE_TEST_CLASS) class AccountApiTest { + @Autowired + private lateinit var followerQueryServiceImpl: FollowerQueryServiceImpl + @Autowired private lateinit var userQueryServiceImpl: UserQueryServiceImpl + @Autowired private lateinit var context: WebApplicationContext @@ -259,6 +265,22 @@ class AccountApiTest { .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 { @JvmStatic @AfterAll diff --git a/src/intTest/resources/application.yml b/src/intTest/resources/application.yml index c73fc1f3..51622edd 100644 --- a/src/intTest/resources/application.yml +++ b/src/intTest/resources/application.yml @@ -1,5 +1,5 @@ hideout: - url: "https://localhost:8080" + url: "https://example.com" use-mongodb: true security: jwt: diff --git a/src/intTest/resources/sql/accounts/apiV1AccountsIdFollowPost フォローできる.sql b/src/intTest/resources/sql/accounts/apiV1AccountsIdFollowPost フォローできる.sql new file mode 100644 index 00000000..53ea2830 --- /dev/null +++ b/src/intTest/resources/sql/accounts/apiV1AccountsIdFollowPost フォローできる.sql @@ -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); diff --git a/src/main/kotlin/dev/usbharu/hideout/core/service/user/UserServiceImpl.kt b/src/main/kotlin/dev/usbharu/hideout/core/service/user/UserServiceImpl.kt index d2a2e45e..e70ec782 100644 --- a/src/main/kotlin/dev/usbharu/hideout/core/service/user/UserServiceImpl.kt +++ b/src/main/kotlin/dev/usbharu/hideout/core/service/user/UserServiceImpl.kt @@ -114,10 +114,13 @@ class UserServiceImpl( } override suspend fun follow(id: Long, followerId: Long) { + logger.debug("START Follow id: {} → target: {}", followerId, id) followerQueryService.appendFollower(id, followerId) if (userRepository.findFollowRequestsById(id, followerId)) { + logger.debug("Follow request is accepted! ") userRepository.deleteFollowRequest(id, followerId) } + logger.debug("SUCCESS Follow id: {} → target: {}", followerId, id) } override suspend fun unfollow(id: Long, followerId: Long): Boolean { diff --git a/src/main/resources/db/migration/V1__Init_DB.sql b/src/main/resources/db/migration/V1__Init_DB.sql index e0188588..1440fb61 100644 --- a/src/main/resources/db/migration/V1__Init_DB.sql +++ b/src/main/resources/db/migration/V1__Init_DB.sql @@ -31,7 +31,7 @@ create table if not exists users "following" varchar(1000) null, followers varchar(1000) 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 ); create table if not exists follow_requests From 5f79f06831399f103b5727dfba590f63d9e9085a Mon Sep 17 00:00:00 2001 From: usbharu Date: Thu, 7 Dec 2023 14:30:35 +0900 Subject: [PATCH 18/19] Apply suggestions from code review Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com> --- .../hideout/activitypub/domain/model/Image.kt | 10 ++-- .../service/inbox/InboxJobProcessor.kt | 2 - .../HttpSignatureUserDetailsService.kt | 1 - .../service/account/AccountApiService.kt | 50 +++++++++---------- 4 files changed, 27 insertions(+), 36 deletions(-) diff --git a/src/main/kotlin/dev/usbharu/hideout/activitypub/domain/model/Image.kt b/src/main/kotlin/dev/usbharu/hideout/activitypub/domain/model/Image.kt index c3e4649a..8f77d4ae 100644 --- a/src/main/kotlin/dev/usbharu/hideout/activitypub/domain/model/Image.kt +++ b/src/main/kotlin/dev/usbharu/hideout/activitypub/domain/model/Image.kt @@ -31,11 +31,9 @@ open class Image( override fun toString(): String { return "Image(" + - "mediaType=$mediaType, " + - "url='$url'" + - ")" + - " ${super.toString()}" + "mediaType=$mediaType, " + + "url='$url'" + + ")" + + " ${super.toString()}" } - - } diff --git a/src/main/kotlin/dev/usbharu/hideout/activitypub/service/inbox/InboxJobProcessor.kt b/src/main/kotlin/dev/usbharu/hideout/activitypub/service/inbox/InboxJobProcessor.kt index 9ca3e982..301ac7ce 100644 --- a/src/main/kotlin/dev/usbharu/hideout/activitypub/service/inbox/InboxJobProcessor.kt +++ b/src/main/kotlin/dev/usbharu/hideout/activitypub/service/inbox/InboxJobProcessor.kt @@ -99,7 +99,6 @@ class InboxJobProcessor( val verify = signature?.let { verifyHttpSignature(httpRequest, it, transaction) } ?: false - logger.debug("Is verifying success? {}", verify) val activityPubProcessor = @@ -114,7 +113,6 @@ class InboxJobProcessor( activityPubProcessor.process(ActivityPubProcessContext(value, jsonNode, httpRequest, signature, verify)) logger.info("SUCCESS Process inbox. type: {}", param.type) - } override fun job(): InboxJob = InboxJob diff --git a/src/main/kotlin/dev/usbharu/hideout/core/infrastructure/springframework/httpsignature/HttpSignatureUserDetailsService.kt b/src/main/kotlin/dev/usbharu/hideout/core/infrastructure/springframework/httpsignature/HttpSignatureUserDetailsService.kt index 9ef79982..a75fe934 100644 --- a/src/main/kotlin/dev/usbharu/hideout/core/infrastructure/springframework/httpsignature/HttpSignatureUserDetailsService.kt +++ b/src/main/kotlin/dev/usbharu/hideout/core/infrastructure/springframework/httpsignature/HttpSignatureUserDetailsService.kt @@ -37,7 +37,6 @@ class HttpSignatureUserDetailsService( try { userQueryService.findByKeyId(keyId) } catch (e: FailedToGetResourcesException) { - throw UsernameNotFoundException("User not found", e) } } 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 946a6710..a3315107 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 @@ -92,10 +92,8 @@ class AccountApiServiceImpl( } override suspend fun follow(userid: Long, followeeId: Long): Relationship = transaction.transaction { - val alreadyFollow = followerQueryService.alreadyFollow(followeeId, userid) - val followRequest = if (alreadyFollow) { true } else { @@ -129,40 +127,38 @@ class AccountApiServiceImpl( override suspend fun relationships(userid: Long, id: List, withSuspended: Boolean): List = transaction.transaction { - if (id.isEmpty()) { - return@transaction emptyList() - } + if (id.isEmpty()) { + return@transaction emptyList() + } - - logger.warn("id is too long! ({}) truncate to 20", id.size) + 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 alreadyFollow = followerQueryService.alreadyFollow(userid, it) + val followed = followerQueryService.alreadyFollow(it, userid) - val followed = followerQueryService.alreadyFollow(it, userid) + val requested = userRepository.findFollowRequestsById(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 = "" - ) + 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 { return CredentialAccount( From e77c18b1b10fac608f65649f8c0c224be5096335 Mon Sep 17 00:00:00 2001 From: usbharu <64310155+usbharu@users.noreply.github.com> Date: Thu, 7 Dec 2023 15:01:17 +0900 Subject: [PATCH 19/19] =?UTF-8?q?test:=20=E3=83=86=E3=82=B9=E3=83=88?= =?UTF-8?q?=E3=82=92=E4=BF=AE=E6=AD=A3?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/intTest/kotlin/mastodon/account/AccountApiTest.kt | 4 ++-- src/intTest/resources/sql/test-user2.sql | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/intTest/kotlin/mastodon/account/AccountApiTest.kt b/src/intTest/kotlin/mastodon/account/AccountApiTest.kt index 4798f574..0c69e626 100644 --- a/src/intTest/kotlin/mastodon/account/AccountApiTest.kt +++ b/src/intTest/kotlin/mastodon/account/AccountApiTest.kt @@ -100,7 +100,7 @@ class AccountApiTest { .asyncDispatch() .andExpect { status { isFound() } } - userQueryServiceImpl.findByNameAndDomain("api-test-user-1", "localhost") + userQueryServiceImpl.findByNameAndDomain("api-test-user-1", "example.com") } @Test @@ -116,7 +116,7 @@ class AccountApiTest { .asyncDispatch() .andExpect { status { isFound() } } - userQueryServiceImpl.findByNameAndDomain("api-test-user-2", "localhost") + userQueryServiceImpl.findByNameAndDomain("api-test-user-2", "example.com") } @Test diff --git a/src/intTest/resources/sql/test-user2.sql b/src/intTest/resources/sql/test-user2.sql index 3c305417..7b123701 100644 --- a/src/intTest/resources/sql/test-user2.sql +++ b/src/intTest/resources/sql/test-user2.sql @@ -1,6 +1,6 @@ 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', 'localhost', 'Im test user.', 'THis account is test user.', +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',