diff --git a/src/e2eTest/resources/oauth2/user.sql b/src/e2eTest/resources/oauth2/user.sql index 351d988d..4184f284 100644 --- a/src/e2eTest/resources/oauth2/user.sql +++ b/src/e2eTest/resources/oauth2/user.sql @@ -44,7 +44,7 @@ Ja15+ZWbOA4vJA9pOh3x4XM= -----END PRIVATE KEY----- ', 1701398248417, 'http://localhost/users/test-user#pubkey', 'http://localhost/users/test-user/following', - 'http://localhost/users/test-users/followers', null, false, 0, 0, 0, null); + 'http://localhost/users/test-users/followers', 0, false, 0, 0, 0, null); insert into user_details (actor_id, password, auto_accept_followee_follow_request) values ( 1730415786666758144 diff --git a/src/intTest/resources/sql/accounts/apiV1AccountsIdFollowPost フォローできる.sql b/src/intTest/resources/sql/accounts/apiV1AccountsIdFollowPost フォローできる.sql index 2221dfc7..a2b01c22 100644 --- a/src/intTest/resources/sql/accounts/apiV1AccountsIdFollowPost フォローできる.sql +++ b/src/intTest/resources/sql/accounts/apiV1AccountsIdFollowPost フォローできる.sql @@ -7,11 +7,11 @@ VALUES (3733363, 'follow-test-user-1', 'example.com', 'follow-test-user-1-name', '-----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, false, 0, 0, 0, null), + 'https://example.com/users/follow-test-user-1/followers', 0, false, 0, 0, 0, null), (37335363, 'follow-test-user-2', 'example.com', 'follow-test-user-2-name', '', '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, false, 0, 0, 0, null); + 'https://example.com/users/follow-test-user-2/followers', 0, false, 0, 0, 0, null); diff --git a/src/intTest/resources/sql/note/httpSignature認証でフォロワーがfollowers投稿を取得できる.sql b/src/intTest/resources/sql/note/httpSignature認証でフォロワーがfollowers投稿を取得できる.sql index f522d4bc..dc2ab9f1 100644 --- a/src/intTest/resources/sql/note/httpSignature認証でフォロワーがfollowers投稿を取得できる.sql +++ b/src/intTest/resources/sql/note/httpSignature認証でフォロワーがfollowers投稿を取得できる.sql @@ -7,7 +7,7 @@ VALUES (8, 'test-user8', 'example.com', 'Im test-user8.', 'THis account is test- '-----BEGIN PUBLIC KEY-----...-----END PUBLIC KEY-----', '-----BEGIN PRIVATE KEY-----...-----END PRIVATE KEY-----', 12345678, 'https://example.com/users/test-user8#pubkey', 'https://example.com/users/test-user8/following', - 'https://example.com/users/test-user8/followers', null, false, 0, 0, 0, null), + 'https://example.com/users/test-user8/followers', 0, false, 0, 0, 0, null), (9, 'test-user9', 'follower.example.com', 'Im test-user9.', 'THis account is test-user9.', 'https://follower.example.com/users/test-user9/inbox', 'https://follower.example.com/users/test-user9/outbox', 'https://follower.example.com/users/test-user9', @@ -15,7 +15,7 @@ VALUES (8, 'test-user8', 'example.com', 'Im test-user8.', 'THis account is test- null, 12345678, 'https://follower.example.com/users/test-user9#pubkey', 'https://follower.example.com/users/test-user9/following', - 'https://follower.example.com/users/test-user9/followers', null, false, 0, 0, 0, null); + 'https://follower.example.com/users/test-user9/followers', 0, false, 0, 0, 0, null); insert into relationships (actor_id, target_actor_id, following, blocking, muting, follow_request, ignore_follow_request) diff --git a/src/intTest/resources/sql/note/httpSignature認証でフォロワーがpublic投稿を取得できる.sql b/src/intTest/resources/sql/note/httpSignature認証でフォロワーがpublic投稿を取得できる.sql index b0f688f2..a44861f4 100644 --- a/src/intTest/resources/sql/note/httpSignature認証でフォロワーがpublic投稿を取得できる.sql +++ b/src/intTest/resources/sql/note/httpSignature認証でフォロワーがpublic投稿を取得できる.sql @@ -7,7 +7,7 @@ VALUES (4, 'test-user4', 'example.com', 'Im test user4.', 'THis account is test '-----BEGIN PUBLIC KEY-----...-----END PUBLIC KEY-----', '-----BEGIN PRIVATE KEY-----...-----END PRIVATE KEY-----', 12345678, 'https://example.com/users/test-user4#pubkey', 'https://example.com/users/test-user4/following', - 'https://example.com/users/test-user4/followers', null, false, 0, 0, 0, null), + 'https://example.com/users/test-user4/followers', 0, false, 0, 0, 0, null), (5, 'test-user5', 'follower.example.com', 'Im test user5.', 'THis account is test user5.', 'https://follower.example.com/users/test-user5/inbox', 'https://follower.example.com/users/test-user5/outbox', 'https://follower.example.com/users/test-user5', @@ -15,7 +15,7 @@ VALUES (4, 'test-user4', 'example.com', 'Im test user4.', 'THis account is test null, 12345678, 'https://follower.example.com/users/test-user5#pubkey', 'https://follower.example.com/users/test-user5/following', - 'https://follower.example.com/users/test-user5/followers', null, false, 0, 0, 0, null); + 'https://follower.example.com/users/test-user5/followers', 0, false, 0, 0, 0, null); insert into relationships (actor_id, target_actor_id, following, blocking, muting, follow_request, ignore_follow_request) diff --git a/src/intTest/resources/sql/note/httpSignature認証でフォロワーがunlisted投稿を取得できる.sql b/src/intTest/resources/sql/note/httpSignature認証でフォロワーがunlisted投稿を取得できる.sql index 4a271f1f..77cb3395 100644 --- a/src/intTest/resources/sql/note/httpSignature認証でフォロワーがunlisted投稿を取得できる.sql +++ b/src/intTest/resources/sql/note/httpSignature認証でフォロワーがunlisted投稿を取得できる.sql @@ -7,7 +7,7 @@ VALUES (6, 'test-user6', 'example.com', 'Im test-user6.', 'THis account is test- '-----BEGIN PUBLIC KEY-----...-----END PUBLIC KEY-----', '-----BEGIN PRIVATE KEY-----...-----END PRIVATE KEY-----', 12345678, 'https://example.com/users/test-user6#pubkey', 'https://example.com/users/test-user6/following', - 'https://example.com/users/test-user6/followers', null, false, 0, 0, 0, null), + 'https://example.com/users/test-user6/followers', 0, false, 0, 0, 0, null), (7, 'test-user7', 'follower.example.com', 'Im test-user7.', 'THis account is test-user7.', 'https://follower.example.com/users/test-user7/inbox', 'https://follower.example.com/users/test-user7/outbox', 'https://follower.example.com/users/test-user7', @@ -15,7 +15,7 @@ VALUES (6, 'test-user6', 'example.com', 'Im test-user6.', 'THis account is test- null, 12345678, 'https://follower.example.com/users/test-user7#pubkey', 'https://follower.example.com/users/test-user7/following', - 'https://follower.example.com/users/test-user7/followers', null, false, 0, 0, 0, null); + 'https://follower.example.com/users/test-user7/followers', 0, false, 0, 0, 0, null); insert into relationships (actor_id, target_actor_id, following, blocking, muting, follow_request, ignore_follow_request) diff --git a/src/intTest/resources/sql/note/メディア付き投稿はattachmentにDocumentとして画像が存在する.sql b/src/intTest/resources/sql/note/メディア付き投稿はattachmentにDocumentとして画像が存在する.sql index b99b289f..6cc9db8c 100644 --- a/src/intTest/resources/sql/note/メディア付き投稿はattachmentにDocumentとして画像が存在する.sql +++ b/src/intTest/resources/sql/note/メディア付き投稿はattachmentにDocumentとして画像が存在する.sql @@ -7,7 +7,7 @@ VALUES (11, 'test-user11', 'example.com', 'Im test-user11.', 'THis account is te '-----BEGIN PUBLIC KEY-----...-----END PUBLIC KEY-----', '-----BEGIN PRIVATE KEY-----...-----END PRIVATE KEY-----', 12345678, 'https://example.com/users/test-user11#pubkey', 'https://example.com/users/test-user11/following', - 'https://example.com/users/test-user11/followers', null, false, 0, 0, 0, null); + 'https://example.com/users/test-user11/followers', 0, false, 0, 0, 0, null); insert into POSTS (id, actor_id, overview, content, text, created_at, visibility, url, repost_id, reply_id, sensitive, ap_id, diff --git a/src/intTest/resources/sql/note/リプライになっている投稿はinReplyToが存在する.sql b/src/intTest/resources/sql/note/リプライになっている投稿はinReplyToが存在する.sql index 67178345..41ac73a4 100644 --- a/src/intTest/resources/sql/note/リプライになっている投稿はinReplyToが存在する.sql +++ b/src/intTest/resources/sql/note/リプライになっている投稿はinReplyToが存在する.sql @@ -7,7 +7,7 @@ VALUES (10, 'test-user10', 'example.com', 'Im test-user10.', 'THis account is te '-----BEGIN PUBLIC KEY-----...-----END PUBLIC KEY-----', '-----BEGIN PRIVATE KEY-----...-----END PRIVATE KEY-----', 12345678, 'https://example.com/users/test-user10#pubkey', 'https://example.com/users/test-user10/following', - 'https://example.com/users/test-user10/followers', null, false, 0, 0, 0, null); + 'https://example.com/users/test-user10/followers', 0, false, 0, 0, 0, null); insert into POSTS (id, actor_id, overview, content, text, created_at, visibility, url, repost_id, reply_id, sensitive, ap_id, diff --git a/src/intTest/resources/sql/note/匿名でfollowers投稿を取得しようとすると404.sql b/src/intTest/resources/sql/note/匿名でfollowers投稿を取得しようとすると404.sql index b848530c..250cfb5a 100644 --- a/src/intTest/resources/sql/note/匿名でfollowers投稿を取得しようとすると404.sql +++ b/src/intTest/resources/sql/note/匿名でfollowers投稿を取得しようとすると404.sql @@ -7,7 +7,7 @@ VALUES (3, 'test-user3', 'example.com', 'Im test user3.', 'THis account is test '-----BEGIN PUBLIC KEY-----...-----END PUBLIC KEY-----', '-----BEGIN PRIVATE KEY-----...-----END PRIVATE KEY-----', 12345678, 'https://example.com/users/test-user3#pubkey', 'https://example.com/users/test-user3/following', - 'https://example.com/users/test-user3/followers', null, false, 0, 0, 0, null); + 'https://example.com/users/test-user3/followers', 0, false, 0, 0, 0, null); insert into POSTS (id, actor_id, overview, content, text, created_at, visibility, url, repost_id, reply_id, sensitive, ap_id, diff --git a/src/intTest/resources/sql/note/匿名でpublic投稿を取得できる.sql b/src/intTest/resources/sql/note/匿名でpublic投稿を取得できる.sql index 0a3272b8..777f9244 100644 --- a/src/intTest/resources/sql/note/匿名でpublic投稿を取得できる.sql +++ b/src/intTest/resources/sql/note/匿名でpublic投稿を取得できる.sql @@ -7,7 +7,7 @@ VALUES (1, 'test-user', 'example.com', 'Im test user.', 'THis account is test us '-----BEGIN PUBLIC KEY-----...-----END PUBLIC KEY-----', '-----BEGIN PRIVATE KEY-----...-----END PRIVATE KEY-----', 12345678, 'https://example.com/users/test-user#pubkey', 'https://example.com/users/test-user/following', - 'https://example.com/users/test-users/followers', null, false, 0, 0, 0, null); + 'https://example.com/users/test-users/followers', 0, false, 0, 0, 0, null); insert into POSTS (id, actor_id, overview, content, text, created_at, visibility, url, repost_id, reply_id, sensitive, ap_id, diff --git a/src/intTest/resources/sql/note/匿名でunlisted投稿を取得できる.sql b/src/intTest/resources/sql/note/匿名でunlisted投稿を取得できる.sql index abeb3150..b132734d 100644 --- a/src/intTest/resources/sql/note/匿名でunlisted投稿を取得できる.sql +++ b/src/intTest/resources/sql/note/匿名でunlisted投稿を取得できる.sql @@ -7,7 +7,7 @@ VALUES (2, 'test-user2', 'example.com', 'Im test user2.', 'THis account is test '-----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-user2/followers', null, false, 0, 0, 0, null); + 'https://example.com/users/test-user2/followers', 0, false, 0, 0, 0, null); insert into POSTS (id, actor_id, overview, content, text, created_at, visibility, url, repost_id, reply_id, sensitive, ap_id, diff --git a/src/intTest/resources/sql/test-user.sql b/src/intTest/resources/sql/test-user.sql index cdcc686d..d46e5280 100644 --- a/src/intTest/resources/sql/test-user.sql +++ b/src/intTest/resources/sql/test-user.sql @@ -7,4 +7,4 @@ VALUES (1, 'test-user', 'example.com', 'Im test user.', 'THis account is test us '-----BEGIN PUBLIC KEY-----...-----END PUBLIC KEY-----', '-----BEGIN PRIVATE KEY-----...-----END PRIVATE KEY-----', 12345678, 'https://example.com/users/test-user#pubkey', 'https://example.com/users/test-user/following', - 'https://example.com/users/test-users/followers', null, false, 0, 0, 0, null); + 'https://example.com/users/test-users/followers', 0, false, 0, 0, 0, null); diff --git a/src/intTest/resources/sql/test-user2.sql b/src/intTest/resources/sql/test-user2.sql index ef455c75..0f736704 100644 --- a/src/intTest/resources/sql/test-user2.sql +++ b/src/intTest/resources/sql/test-user2.sql @@ -7,4 +7,4 @@ VALUES (2, 'test-user2', 'example.com', 'Im test user.', 'THis account is test u '-----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, false, 0, 0, 0, null); + 'https://example.com/users/test-user2s/followers', 0, false, 0, 0, 0, null); diff --git a/src/main/kotlin/dev/usbharu/hideout/core/domain/model/actor/Actor.kt b/src/main/kotlin/dev/usbharu/hideout/core/domain/model/actor/Actor.kt index b45bcd66..eee1e824 100644 --- a/src/main/kotlin/dev/usbharu/hideout/core/domain/model/actor/Actor.kt +++ b/src/main/kotlin/dev/usbharu/hideout/core/domain/model/actor/Actor.kt @@ -18,37 +18,55 @@ package dev.usbharu.hideout.core.domain.model.actor import dev.usbharu.hideout.application.config.ApplicationConfig import dev.usbharu.hideout.application.config.CharacterLimit +import jakarta.validation.Validator +import jakarta.validation.constraints.* +import org.hibernate.validator.constraints.URL import org.slf4j.LoggerFactory import org.springframework.stereotype.Component import java.time.Instant import kotlin.math.max data class Actor private constructor( + @get:NotNull + @get:Positive val id: Long, + @get:Pattern(regexp = "^[a-zA-Z0-9_-]{1,300}\$") + @get:Size(min = 1) val name: String, val domain: String, val screenName: String, val description: String, + @get:URL val inbox: String, + @get:URL val outbox: String, + @get:URL val url: String, + @get:NotBlank val publicKey: String, val privateKey: String? = null, + @get:PastOrPresent val createdAt: Instant, + @get:NotBlank val keyId: String, val followers: String? = null, val following: String? = null, - val instance: Long? = null, + @get:PositiveOrZero + val instance: Long, val locked: Boolean, val followersCount: Int = 0, val followingCount: Int = 0, val postsCount: Int = 0, val lastPostDate: Instant? = null, - val emojis: List = emptyList() + val emojis: List = emptyList(), ) { @Component - class UserBuilder(private val characterLimit: CharacterLimit, private val applicationConfig: ApplicationConfig) { + class UserBuilder( + private val characterLimit: CharacterLimit, + private val applicationConfig: ApplicationConfig, + private val validator: Validator, + ) { private val logger = LoggerFactory.getLogger(UserBuilder::class.java) @@ -68,13 +86,13 @@ data class Actor private constructor( keyId: String, following: String? = null, followers: String? = null, - instance: Long? = null, + instance: Long, locked: Boolean, followersCount: Int = 0, followingCount: Int = 0, postsCount: Int = 0, lastPostDate: Instant? = null, - emojis: List = emptyList() + emojis: List = emptyList(), ): Actor { if (id == 0L) { return Actor( @@ -176,7 +194,7 @@ data class Actor private constructor( "keyId must contain non-blank characters." } - return Actor( + val actor = Actor( id = id, name = limitedName, domain = domain, @@ -199,6 +217,13 @@ data class Actor private constructor( lastPostDate = lastPostDate, emojis = emojis ) + + val validate = validator.validate(actor) + + for (constraintViolation in validate) { + throw IllegalArgumentException("${constraintViolation.propertyPath} : ${constraintViolation.message}") + } + return actor } } @@ -217,27 +242,27 @@ data class Actor private constructor( fun withLastPostAt(lastPostDate: Instant): Actor = this.copy(lastPostDate = lastPostDate) override fun toString(): String { return "Actor(" + - "id=$id, " + - "name='$name', " + - "domain='$domain', " + - "screenName='$screenName', " + - "description='$description', " + - "inbox='$inbox', " + - "outbox='$outbox', " + - "url='$url', " + - "publicKey='$publicKey', " + - "privateKey=$privateKey, " + - "createdAt=$createdAt, " + - "keyId='$keyId', " + - "followers=$followers, " + - "following=$following, " + - "instance=$instance, " + - "locked=$locked, " + - "followersCount=$followersCount, " + - "followingCount=$followingCount, " + - "postsCount=$postsCount, " + - "lastPostDate=$lastPostDate, " + - "emojis=$emojis" + - ")" + "id=$id, " + + "name='$name', " + + "domain='$domain', " + + "screenName='$screenName', " + + "description='$description', " + + "inbox='$inbox', " + + "outbox='$outbox', " + + "url='$url', " + + "publicKey='$publicKey', " + + "privateKey=$privateKey, " + + "createdAt=$createdAt, " + + "keyId='$keyId', " + + "followers=$followers, " + + "following=$following, " + + "instance=$instance, " + + "locked=$locked, " + + "followersCount=$followersCount, " + + "followingCount=$followingCount, " + + "postsCount=$postsCount, " + + "lastPostDate=$lastPostDate, " + + "emojis=$emojis" + + ")" } } diff --git a/src/main/kotlin/dev/usbharu/hideout/core/domain/model/post/Post.kt b/src/main/kotlin/dev/usbharu/hideout/core/domain/model/post/Post.kt index b4ceac6a..881f6ca3 100644 --- a/src/main/kotlin/dev/usbharu/hideout/core/domain/model/post/Post.kt +++ b/src/main/kotlin/dev/usbharu/hideout/core/domain/model/post/Post.kt @@ -18,31 +18,40 @@ package dev.usbharu.hideout.core.domain.model.post import dev.usbharu.hideout.application.config.CharacterLimit import dev.usbharu.hideout.core.service.post.PostContentFormatter +import jakarta.validation.Validator +import jakarta.validation.constraints.Positive +import org.hibernate.validator.constraints.URL import org.springframework.stereotype.Component import java.time.Instant data class Post private constructor( + @get:Positive val id: Long, + @get:Positive val actorId: Long, val overview: String? = null, val content: String, val text: String, + @get:Positive val createdAt: Long, val visibility: Visibility, + @get:URL val url: String, val repostId: Long? = null, val replyId: Long? = null, val sensitive: Boolean = false, + @get:URL val apId: String = url, val mediaIds: List = emptyList(), val delted: Boolean = false, - val emojiIds: List = emptyList() + val emojiIds: List = emptyList(), ) { @Component class PostBuilder( private val characterLimit: CharacterLimit, - private val postContentFormatter: PostContentFormatter + private val postContentFormatter: PostContentFormatter, + private val validator: Validator, ) { @Suppress("FunctionMinLength", "LongParameterList") fun of( @@ -86,7 +95,7 @@ data class Post private constructor( require((repostId ?: 0) >= 0) { "repostId must be greater then or equal to 0." } require((replyId ?: 0) >= 0) { "replyId must be greater then or equal to 0." } - return Post( + val post = Post( id = id, actorId = actorId, overview = limitedOverview, @@ -103,6 +112,14 @@ data class Post private constructor( delted = false, emojiIds = emojiIds ) + + val validate = validator.validate(post) + + for (constraintViolation in validate) { + throw IllegalArgumentException("${constraintViolation.propertyPath} : ${constraintViolation.message}") + } + + return post } @Suppress("LongParameterList") @@ -126,7 +143,7 @@ data class Post private constructor( require(actorId >= 0) { "actorId must be greater than or equal to 0." } - return Post( + val post = Post( id = id, actorId = actorId, overview = null, @@ -143,6 +160,14 @@ data class Post private constructor( delted = false, emojiIds = emptyList() ) + + val validate = validator.validate(post) + + for (constraintViolation in validate) { + throw IllegalArgumentException("${constraintViolation.propertyPath} : ${constraintViolation.message}") + } + + return post } @Suppress("LongParameterList") @@ -193,7 +218,7 @@ data class Post private constructor( require((replyId ?: 0) >= 0) { "replyId must be greater then or equal to 0." } - return Post( + val post = Post( id = id, actorId = actorId, overview = limitedOverview, @@ -210,6 +235,14 @@ data class Post private constructor( delted = false, emojiIds = emojiIds ) + + val validate = validator.validate(post) + + for (constraintViolation in validate) { + throw IllegalArgumentException("${constraintViolation.propertyPath} : ${constraintViolation.message}") + } + + return post } @Suppress("LongParameterList") diff --git a/src/main/kotlin/dev/usbharu/hideout/core/infrastructure/exposedrepository/ActorRepositoryImpl.kt b/src/main/kotlin/dev/usbharu/hideout/core/infrastructure/exposedrepository/ActorRepositoryImpl.kt index 67c33051..08bc5de4 100644 --- a/src/main/kotlin/dev/usbharu/hideout/core/infrastructure/exposedrepository/ActorRepositoryImpl.kt +++ b/src/main/kotlin/dev/usbharu/hideout/core/infrastructure/exposedrepository/ActorRepositoryImpl.kt @@ -167,7 +167,7 @@ object Actors : Table("actors") { val keyId = varchar("key_id", length = 1000) val following = varchar("following", length = 1000).nullable() val followers = varchar("followers", length = 1000).nullable() - val instance = long("instance").references(Instance.id).nullable() + val instance = long("instance").references(Instance.id) val locked = bool("locked") val followingCount = integer("following_count") val followersCount = integer("followers_count") diff --git a/src/main/kotlin/dev/usbharu/hideout/core/service/instance/InstanceService.kt b/src/main/kotlin/dev/usbharu/hideout/core/service/instance/InstanceService.kt index d377e2f4..a9a84f3d 100644 --- a/src/main/kotlin/dev/usbharu/hideout/core/service/instance/InstanceService.kt +++ b/src/main/kotlin/dev/usbharu/hideout/core/service/instance/InstanceService.kt @@ -37,7 +37,7 @@ interface InstanceService { class InstanceServiceImpl( private val instanceRepository: InstanceRepository, private val resourceResolveService: ResourceResolveService, - @Qualifier("activitypub") private val objectMapper: ObjectMapper + @Qualifier("activitypub") private val objectMapper: ObjectMapper, ) : InstanceService { override suspend fun fetchInstance(url: String, sharedInbox: String?): Instance { val u = URL(url) @@ -50,57 +50,53 @@ class InstanceServiceImpl( } logger.info("Instance not found. try fetch instance info. url: {}", resolveInstanceUrl) + @Suppress("TooGenericExceptionCaught") + try { + val nodeinfoJson = resourceResolveService.resolve("$resolveInstanceUrl/.well-known/nodeinfo").bodyAsText() + val nodeinfo = objectMapper.readValue(nodeinfoJson, Nodeinfo::class.java) + val nodeinfoPathMap = nodeinfo.links.associate { it.rel to it.href } - val nodeinfoJson = resourceResolveService.resolve("$resolveInstanceUrl/.well-known/nodeinfo").bodyAsText() - val nodeinfo = objectMapper.readValue(nodeinfoJson, Nodeinfo::class.java) - val nodeinfoPathMap = nodeinfo.links.associate { it.rel to it.href } + for ((key, value) in nodeinfoPathMap) { + when (key) { + "http://nodeinfo.diaspora.software/ns/schema/2.0", + "http://nodeinfo.diaspora.software/ns/schema/2.1", + -> { + val nodeinfo20 = objectMapper.readValue( + resourceResolveService.resolve(value!!).bodyAsText(), + Nodeinfo2_0::class.java + ) - for ((key, value) in nodeinfoPathMap) { - when (key) { - "http://nodeinfo.diaspora.software/ns/schema/2.0" -> { - val nodeinfo20 = objectMapper.readValue( - resourceResolveService.resolve(value!!).bodyAsText(), - Nodeinfo2_0::class.java - ) + val instanceCreateDto = InstanceCreateDto( + name = nodeinfo20.metadata?.nodeName, + description = nodeinfo20.metadata?.nodeDescription, + url = resolveInstanceUrl, + iconUrl = "$resolveInstanceUrl/favicon.ico", + sharedInbox = sharedInbox, + software = nodeinfo20.software?.name, + version = nodeinfo20.software?.version + ) + return createNewInstance(instanceCreateDto) + } - val instanceCreateDto = InstanceCreateDto( - name = nodeinfo20.metadata?.nodeName, - description = nodeinfo20.metadata?.nodeDescription, - url = resolveInstanceUrl, - iconUrl = "$resolveInstanceUrl/favicon.ico", - sharedInbox = sharedInbox, - software = nodeinfo20.software?.name, - version = nodeinfo20.software?.version - ) - return createNewInstance(instanceCreateDto) - } - - // TODO: 多分2.0と2.1で互換性有るのでそのまま使うけどなおす - "http://nodeinfo.diaspora.software/ns/schema/2.1" -> { - val nodeinfo20 = objectMapper.readValue( - resourceResolveService.resolve(value!!).bodyAsText(), - Nodeinfo2_0::class.java - ) - - val instanceCreateDto = InstanceCreateDto( - name = nodeinfo20.metadata?.nodeName, - description = nodeinfo20.metadata?.nodeDescription, - url = resolveInstanceUrl, - iconUrl = "$resolveInstanceUrl/favicon.ico", - sharedInbox = sharedInbox, - software = nodeinfo20.software?.name, - version = nodeinfo20.software?.version - ) - return createNewInstance(instanceCreateDto) - } - - else -> { - throw IllegalStateException("Unknown nodeinfo versions: $key url: $value") + else -> { + throw IllegalStateException("Unknown nodeinfo versions: $key url: $value") + } } } + } catch (e: Exception) { + logger.warn("FAILED Fetch Instance", e) } - - throw IllegalStateException("Nodeinfo aren't found.") + return createNewInstance( + InstanceCreateDto( + name = null, + description = null, + url = resolveInstanceUrl, + iconUrl = "$resolveInstanceUrl/favicon.ico", + sharedInbox = null, + software = null, + version = null + ) + ) } override suspend fun createNewInstance(instanceCreateDto: InstanceCreateDto): Instance { 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 5a459405..294656ca 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 @@ -78,7 +78,8 @@ class UserServiceImpl( following = "$userUrl/following", followers = "$userUrl/followers", keyId = "$userUrl#pubkey", - locked = false + locked = false, + instance = 0 ) val save = actorRepository.save(userEntity) userDetailRepository.save(UserDetail(nextId, hashedPassword, true)) @@ -95,13 +96,7 @@ class UserServiceImpl( throw IllegalStateException("Cannot create Deleted actor.") } - @Suppress("TooGenericExceptionCaught") - val instance = try { - instanceService.fetchInstance(user.url, user.sharedInbox) - } catch (e: Exception) { - logger.warn("FAILED to fetch instance. url: {}", user.url, e) - null - } + val instance = instanceService.fetchInstance(user.url, user.sharedInbox) val nextId = actorRepository.nextId() val userEntity = actorBuilder.of( @@ -118,7 +113,7 @@ class UserServiceImpl( followers = user.followers, following = user.following, keyId = user.keyId, - instance = instance?.id, + instance = instance.id, locked = user.locked ?: false ) return try { diff --git a/src/main/kotlin/dev/usbharu/hideout/mastodon/config/MastodonApiSecurityConfig.kt b/src/main/kotlin/dev/usbharu/hideout/mastodon/config/MastodonApiSecurityConfig.kt index ffb8d734..39ec70fb 100644 --- a/src/main/kotlin/dev/usbharu/hideout/mastodon/config/MastodonApiSecurityConfig.kt +++ b/src/main/kotlin/dev/usbharu/hideout/mastodon/config/MastodonApiSecurityConfig.kt @@ -31,6 +31,7 @@ import org.springframework.security.web.SecurityFilterChain class MastodonApiSecurityConfig { @Bean @Order(4) + @Suppress("LongMethod") fun mastodonApiSecurityFilterChain( http: HttpSecurity, rf: RoleHierarchyAuthorizationManagerFactory, diff --git a/src/main/resources/db/migration/V1__Init_DB.sql b/src/main/resources/db/migration/V1__Init_DB.sql index 4b76304f..09f4f0ca 100644 --- a/src/main/resources/db/migration/V1__Init_DB.sql +++ b/src/main/resources/db/migration/V1__Init_DB.sql @@ -45,7 +45,7 @@ create table if not exists actors key_id varchar(1000) not null, "following" varchar(1000) null, followers varchar(1000) null, - "instance" bigint null, + "instance" bigint not null, locked boolean not null, following_count int not null, followers_count int not null, @@ -240,10 +240,14 @@ create table if not exists relationships unique (actor_id, target_actor_id) ); +insert into instance (id, name, description, url, icon_url, shared_inbox, software, version, is_blocked, is_muted, + moderation_note, created_at) +values (0, 'system', '', '', '', null, '', '', false, false, '', current_timestamp); + insert into actors (id, name, domain, screen_name, description, inbox, outbox, url, public_key, private_key, created_at, key_id, following, followers, instance, locked, following_count, followers_count, posts_count, last_post_at) -values (0, 'ghost', '', '', '', '', '', '', '', null, 0, '', '', '', null, true, 0, 0, 0, null); +values (0, 'ghost', '', '', '', '', '', '', '', null, 0, '', '', '', 0, true, 0, 0, 0, null); create table if not exists deleted_actors ( diff --git a/src/test/kotlin/dev/usbharu/hideout/activitypub/service/objects/note/APNoteServiceImplTest.kt b/src/test/kotlin/dev/usbharu/hideout/activitypub/service/objects/note/APNoteServiceImplTest.kt index cda0d284..b5301461 100644 --- a/src/test/kotlin/dev/usbharu/hideout/activitypub/service/objects/note/APNoteServiceImplTest.kt +++ b/src/test/kotlin/dev/usbharu/hideout/activitypub/service/objects/note/APNoteServiceImplTest.kt @@ -45,6 +45,7 @@ import io.ktor.http.* import io.ktor.http.content.* import io.ktor.util.* import io.ktor.util.date.* +import jakarta.validation.Validation import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.Job @@ -60,7 +61,10 @@ import java.time.Instant class APNoteServiceImplTest { - val postBuilder = Post.PostBuilder(CharacterLimit(), DefaultPostContentFormatter(HtmlSanitizeConfig().policy())) + val postBuilder = Post.PostBuilder( + CharacterLimit(), DefaultPostContentFormatter(HtmlSanitizeConfig().policy()), + Validation.buildDefaultValidatorFactory().validator + ) @Test fun `fetchNote(String,String) ノートが既に存在する場合はDBから取得したものを返す`() = runTest { @@ -89,10 +93,7 @@ class APNoteServiceImplTest { apUserService = mock(), postService = mock(), apResourceResolveService = mock(), - postBuilder = Post.PostBuilder( - CharacterLimit(), - DefaultPostContentFormatter(HtmlSanitizeConfig().policy()) - ), + postBuilder = postBuilder, noteQueryService = noteQueryService, mock(), mock(), @@ -163,10 +164,7 @@ class APNoteServiceImplTest { apUserService = apUserService, postService = mock(), apResourceResolveService = apResourceResolveService, - postBuilder = Post.PostBuilder( - CharacterLimit(), - DefaultPostContentFormatter(HtmlSanitizeConfig().policy()) - ), + postBuilder = postBuilder, noteQueryService = noteQueryService, mock(), mock { }, @@ -216,14 +214,11 @@ class APNoteServiceImplTest { apUserService = mock(), postService = mock(), apResourceResolveService = apResourceResolveService, - postBuilder = Post.PostBuilder( - CharacterLimit(), - DefaultPostContentFormatter(HtmlSanitizeConfig().policy()) - ), + postBuilder = postBuilder, noteQueryService = noteQueryService, mock(), mock(), - mock { } + mock { } ) assertThrows { apNoteServiceImpl.fetchNote(url) } diff --git a/src/test/kotlin/dev/usbharu/hideout/core/domain/model/actor/ActorTest.kt b/src/test/kotlin/dev/usbharu/hideout/core/domain/model/actor/ActorTest.kt new file mode 100644 index 00000000..573dd862 --- /dev/null +++ b/src/test/kotlin/dev/usbharu/hideout/core/domain/model/actor/ActorTest.kt @@ -0,0 +1,13 @@ +package dev.usbharu.hideout.core.domain.model.actor + +import org.junit.jupiter.api.Test +import utils.UserBuilder + +class ActorTest { + @Test + fun validator() { + org.junit.jupiter.api.assertThrows { + UserBuilder.localUserOf(name = "うんこ") + } + } +} \ No newline at end of file diff --git a/src/test/kotlin/dev/usbharu/hideout/core/service/post/PostServiceImplTest.kt b/src/test/kotlin/dev/usbharu/hideout/core/service/post/PostServiceImplTest.kt index 0c5a43a4..80cbb571 100644 --- a/src/test/kotlin/dev/usbharu/hideout/core/service/post/PostServiceImplTest.kt +++ b/src/test/kotlin/dev/usbharu/hideout/core/service/post/PostServiceImplTest.kt @@ -26,6 +26,7 @@ import dev.usbharu.hideout.core.domain.model.post.Post import dev.usbharu.hideout.core.domain.model.post.PostRepository import dev.usbharu.hideout.core.domain.model.reaction.ReactionRepository import dev.usbharu.hideout.core.service.timeline.TimelineService +import jakarta.validation.Validation import kotlinx.coroutines.test.runTest import org.assertj.core.api.Assertions.assertThat import org.junit.jupiter.api.Test @@ -56,7 +57,7 @@ class PostServiceImplTest { private var postBuilder: Post.PostBuilder = Post.PostBuilder( CharacterLimit(), DefaultPostContentFormatter( HtmlSanitizeConfig().policy() - ) + ), Validation.buildDefaultValidatorFactory().validator ) @Mock diff --git a/src/test/kotlin/dev/usbharu/hideout/core/service/user/ActorServiceTest.kt b/src/test/kotlin/dev/usbharu/hideout/core/service/user/ActorServiceTest.kt index e0eac4e4..846c0d9e 100644 --- a/src/test/kotlin/dev/usbharu/hideout/core/service/user/ActorServiceTest.kt +++ b/src/test/kotlin/dev/usbharu/hideout/core/service/user/ActorServiceTest.kt @@ -20,11 +20,11 @@ package dev.usbharu.hideout.core.service.user import dev.usbharu.hideout.application.config.ApplicationConfig import dev.usbharu.hideout.application.config.CharacterLimit -import dev.usbharu.hideout.application.config.HtmlSanitizeConfig import dev.usbharu.hideout.core.domain.model.actor.Actor import dev.usbharu.hideout.core.domain.model.actor.ActorRepository -import dev.usbharu.hideout.core.domain.model.post.Post -import dev.usbharu.hideout.core.service.post.DefaultPostContentFormatter +import dev.usbharu.hideout.core.domain.model.instance.Instance +import dev.usbharu.hideout.core.service.instance.InstanceService +import jakarta.validation.Validation import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.test.runTest import org.junit.jupiter.api.Test @@ -33,12 +33,17 @@ import org.mockito.kotlin.* import utils.TestApplicationConfig.testApplicationConfig import java.net.URL import java.security.KeyPairGenerator +import java.time.Instant import kotlin.test.assertEquals import kotlin.test.assertNull class ActorServiceTest { - val actorBuilder = Actor.UserBuilder(CharacterLimit(), ApplicationConfig(URL("https://example.com"))) - val postBuilder = Post.PostBuilder(CharacterLimit(), DefaultPostContentFormatter(HtmlSanitizeConfig().policy())) + val actorBuilder = Actor.UserBuilder( + CharacterLimit(), + ApplicationConfig(URL("https://example.com")), + Validation.buildDefaultValidatorFactory().validator + ) + @Test fun `createLocalUser ローカルユーザーを作成できる`() = runTest { @@ -89,13 +94,34 @@ class ActorServiceTest { onBlocking { nextId() } doReturn 113345L } + val instanceService = mock { + onBlocking { + fetchInstance( + eq("https://remote.example.com"), + isNull() + ) + } doReturn Instance( + 12345L, + "", + "", + "https://remote.example.com", + "https://remote.example.com/favicon.ico", + null, + "unknown", + "", + false, + false, + "", + Instant.now() + ) + } val userService = UserServiceImpl( actorRepository = actorRepository, userAuthService = mock(), actorBuilder = actorBuilder, applicationConfig = testApplicationConfig, - instanceService = mock(), + instanceService = instanceService, userDetailRepository = mock(), deletedActorRepository = mock(), reactionRepository = mock(), diff --git a/src/test/kotlin/utils/PostBuilder.kt b/src/test/kotlin/utils/PostBuilder.kt index ef3cd173..4ddd2e89 100644 --- a/src/test/kotlin/utils/PostBuilder.kt +++ b/src/test/kotlin/utils/PostBuilder.kt @@ -22,13 +22,18 @@ import dev.usbharu.hideout.application.service.id.TwitterSnowflakeIdGenerateServ import dev.usbharu.hideout.core.domain.model.post.Post import dev.usbharu.hideout.core.domain.model.post.Visibility import dev.usbharu.hideout.core.service.post.DefaultPostContentFormatter +import jakarta.validation.Validation import kotlinx.coroutines.runBlocking import java.time.Instant object PostBuilder { private val postBuilder = - Post.PostBuilder(CharacterLimit(), DefaultPostContentFormatter(HtmlSanitizeConfig().policy())) + Post.PostBuilder( + CharacterLimit(), + DefaultPostContentFormatter(HtmlSanitizeConfig().policy()), + Validation.buildDefaultValidatorFactory().validator + ) private val idGenerator = TwitterSnowflakeIdGenerateService @@ -39,7 +44,7 @@ object PostBuilder { text: String = "Hello World", createdAt: Long = Instant.now().toEpochMilli(), visibility: Visibility = Visibility.PUBLIC, - url: String = "https://example.com/users/$userId/posts/$id" + url: String = "https://example.com/users/$userId/posts/$id", ): Post { return postBuilder.of( id = id, diff --git a/src/test/kotlin/utils/UserBuilder.kt b/src/test/kotlin/utils/UserBuilder.kt index 304403e9..0aff8e44 100644 --- a/src/test/kotlin/utils/UserBuilder.kt +++ b/src/test/kotlin/utils/UserBuilder.kt @@ -20,12 +20,16 @@ import dev.usbharu.hideout.application.config.ApplicationConfig import dev.usbharu.hideout.application.config.CharacterLimit import dev.usbharu.hideout.application.service.id.TwitterSnowflakeIdGenerateService import dev.usbharu.hideout.core.domain.model.actor.Actor +import jakarta.validation.Validation import kotlinx.coroutines.runBlocking import java.net.URL import java.time.Instant object UserBuilder { - private val actorBuilder = Actor.UserBuilder(CharacterLimit(), ApplicationConfig(URL("https://example.com"))) + private val actorBuilder = Actor.UserBuilder( + CharacterLimit(), ApplicationConfig(URL("https://example.com")), + Validation.buildDefaultValidatorFactory().validator + ) private val idGenerator = TwitterSnowflakeIdGenerateService @@ -43,7 +47,7 @@ object UserBuilder { createdAt: Instant = Instant.now(), keyId: String = "https://$domain/users/$id#pubkey", followers: String = "https://$domain/users/$id/followers", - following: String = "https://$domain/users/$id/following" + following: String = "https://$domain/users/$id/following", ): Actor { return actorBuilder.of( id = id, @@ -60,7 +64,8 @@ object UserBuilder { keyId = keyId, followers = followers, following = following, - locked = false + locked = false, + instance = 0 ) } @@ -77,7 +82,8 @@ object UserBuilder { createdAt: Instant = Instant.now(), keyId: String = "https://$domain/$id#pubkey", followers: String = "https://$domain/$id/followers", - following: String = "https://$domain/$id/following" + following: String = "https://$domain/$id/following", + instanceId: Long = generateId(), ): Actor { return actorBuilder.of( id = id, @@ -94,7 +100,8 @@ object UserBuilder { keyId = keyId, followers = followers, following = following, - locked = false + locked = false, + instance = instanceId ) }