diff --git a/src/intTest/resources/sql/note/httpSignature認証でフォロワーがfollowers投稿を取得できる.sql b/src/intTest/resources/sql/note/httpSignature認証でフォロワーがfollowers投稿を取得できる.sql index f1a91192..53d28e59 100644 --- a/src/intTest/resources/sql/note/httpSignature認証でフォロワーがfollowers投稿を取得できる.sql +++ b/src/intTest/resources/sql/note/httpSignature認証でフォロワーがfollowers投稿を取得できる.sql @@ -1,5 +1,5 @@ insert into "USERS" (ID, NAME, DOMAIN, SCREEN_NAME, DESCRIPTION, PASSWORD, INBOX, OUTBOX, URL, PUBLIC_KEY, PRIVATE_KEY, - CREATED_AT, KEY_ID, FOLLOWING, FOLLOWERS) + CREATED_AT, KEY_ID, FOLLOWING, FOLLOWERS, INSTANCE) VALUES (8, 'test-user8', 'example.com', 'Im test-user8.', 'THis account is test-user8.', '5e884898da28047151d0e56f8dc6292773603d0d6aabbdd62a11ef721d1542d8', 'https://example.com/users/test-user8/inbox', @@ -7,10 +7,10 @@ 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'); + 'https://example.com/users/test-user8/followers', null); insert into "USERS" (ID, NAME, DOMAIN, SCREEN_NAME, DESCRIPTION, PASSWORD, INBOX, OUTBOX, URL, PUBLIC_KEY, PRIVATE_KEY, - CREATED_AT, KEY_ID, FOLLOWING, FOLLOWERS) + CREATED_AT, KEY_ID, FOLLOWING, FOLLOWERS, INSTANCE) VALUES (9, 'test-user9', 'follower.example.com', 'Im test-user9.', 'THis account is test-user9.', null, 'https://follower.example.com/users/test-user9/inbox', @@ -19,7 +19,7 @@ VALUES (9, 'test-user9', 'follower.example.com', 'Im test-user9.', 'THis account 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'); + 'https://follower.example.com/users/test-user9/followers', null); insert into USERS_FOLLOWERS (USER_ID, FOLLOWER_ID) VALUES (8, 9); diff --git a/src/intTest/resources/sql/note/httpSignature認証でフォロワーがpublic投稿を取得できる.sql b/src/intTest/resources/sql/note/httpSignature認証でフォロワーがpublic投稿を取得できる.sql index 20a7e5ba..e0fe1f83 100644 --- a/src/intTest/resources/sql/note/httpSignature認証でフォロワーがpublic投稿を取得できる.sql +++ b/src/intTest/resources/sql/note/httpSignature認証でフォロワーがpublic投稿を取得できる.sql @@ -1,5 +1,5 @@ insert into "USERS" (ID, NAME, DOMAIN, SCREEN_NAME, DESCRIPTION, PASSWORD, INBOX, OUTBOX, URL, PUBLIC_KEY, PRIVATE_KEY, - CREATED_AT, KEY_ID, FOLLOWING, FOLLOWERS) + CREATED_AT, KEY_ID, FOLLOWING, FOLLOWERS, INSTANCE) VALUES (4, 'test-user4', 'example.com', 'Im test user4.', 'THis account is test user4.', '5e884898da28047151d0e56f8dc6292773603d0d6aabbdd62a11ef721d1542d8', 'https://example.com/users/test-user4/inbox', @@ -7,10 +7,10 @@ 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'); + 'https://example.com/users/test-user4/followers', null); insert into "USERS" (ID, NAME, DOMAIN, SCREEN_NAME, DESCRIPTION, PASSWORD, INBOX, OUTBOX, URL, PUBLIC_KEY, PRIVATE_KEY, - CREATED_AT, KEY_ID, FOLLOWING, FOLLOWERS) + CREATED_AT, KEY_ID, FOLLOWING, FOLLOWERS, INSTANCE) VALUES (5, 'test-user5', 'follower.example.com', 'Im test user5.', 'THis account is test user5.', null, 'https://follower.example.com/users/test-user5/inbox', @@ -19,7 +19,7 @@ VALUES (5, 'test-user5', 'follower.example.com', 'Im test user5.', 'THis account 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'); + 'https://follower.example.com/users/test-user5/followers', null); insert into USERS_FOLLOWERS (USER_ID, FOLLOWER_ID) VALUES (4, 5); diff --git a/src/intTest/resources/sql/note/httpSignature認証でフォロワーがunlisted投稿を取得できる.sql b/src/intTest/resources/sql/note/httpSignature認証でフォロワーがunlisted投稿を取得できる.sql index cb7707a9..82c7cff9 100644 --- a/src/intTest/resources/sql/note/httpSignature認証でフォロワーがunlisted投稿を取得できる.sql +++ b/src/intTest/resources/sql/note/httpSignature認証でフォロワーがunlisted投稿を取得できる.sql @@ -1,5 +1,5 @@ insert into "USERS" (ID, NAME, DOMAIN, SCREEN_NAME, DESCRIPTION, PASSWORD, INBOX, OUTBOX, URL, PUBLIC_KEY, PRIVATE_KEY, - CREATED_AT, KEY_ID, FOLLOWING, FOLLOWERS) + CREATED_AT, KEY_ID, FOLLOWING, FOLLOWERS, INSTANCE) VALUES (6, 'test-user6', 'example.com', 'Im test-user6.', 'THis account is test-user6.', '5e884898da28047151d0e56f8dc6292773603d0d6aabbdd62a11ef721d1542d8', 'https://example.com/users/test-user6/inbox', @@ -7,10 +7,10 @@ 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'); + 'https://example.com/users/test-user6/followers', null); insert into "USERS" (ID, NAME, DOMAIN, SCREEN_NAME, DESCRIPTION, PASSWORD, INBOX, OUTBOX, URL, PUBLIC_KEY, PRIVATE_KEY, - CREATED_AT, KEY_ID, FOLLOWING, FOLLOWERS) + CREATED_AT, KEY_ID, FOLLOWING, FOLLOWERS, INSTANCE) VALUES (7, 'test-user7', 'follower.example.com', 'Im test-user7.', 'THis account is test-user7.', null, 'https://follower.example.com/users/test-user7/inbox', @@ -19,7 +19,7 @@ VALUES (7, 'test-user7', 'follower.example.com', 'Im test-user7.', 'THis account 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'); + 'https://follower.example.com/users/test-user7/followers', null); insert into USERS_FOLLOWERS (USER_ID, FOLLOWER_ID) VALUES (6, 7); diff --git a/src/intTest/resources/sql/note/メディア付き投稿はattachmentにDocumentとして画像が存在する.sql b/src/intTest/resources/sql/note/メディア付き投稿はattachmentにDocumentとして画像が存在する.sql index 810c33d4..2f0b65a7 100644 --- a/src/intTest/resources/sql/note/メディア付き投稿はattachmentにDocumentとして画像が存在する.sql +++ b/src/intTest/resources/sql/note/メディア付き投稿はattachmentにDocumentとして画像が存在する.sql @@ -1,5 +1,5 @@ insert into "USERS" (ID, NAME, DOMAIN, SCREEN_NAME, DESCRIPTION, PASSWORD, INBOX, OUTBOX, URL, PUBLIC_KEY, PRIVATE_KEY, - CREATED_AT, KEY_ID, FOLLOWING, FOLLOWERS) + CREATED_AT, KEY_ID, FOLLOWING, FOLLOWERS, INSTANCE) VALUES (11, 'test-user11', 'example.com', 'Im test-user11.', 'THis account is test-user11.', '5e884898da28047151d0e56f8dc6292773603d0d6aabbdd62a11ef721d1542d8', 'https://example.com/users/test-user11/inbox', @@ -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'); + 'https://example.com/users/test-user11/followers', null); insert into POSTS (ID, "userId", OVERVIEW, TEXT, "createdAt", VISIBILITY, URL, "repostId", "replyId", SENSITIVE, AP_ID) VALUES (1242, 11, null, 'test post', 12345680, 0, 'https://example.com/users/test-user11/posts/1242', null, null, false, diff --git a/src/intTest/resources/sql/note/リプライになっている投稿はinReplyToが存在する.sql b/src/intTest/resources/sql/note/リプライになっている投稿はinReplyToが存在する.sql index cf6f842f..d32ffce0 100644 --- a/src/intTest/resources/sql/note/リプライになっている投稿はinReplyToが存在する.sql +++ b/src/intTest/resources/sql/note/リプライになっている投稿はinReplyToが存在する.sql @@ -1,5 +1,5 @@ insert into "USERS" (ID, NAME, DOMAIN, SCREEN_NAME, DESCRIPTION, PASSWORD, INBOX, OUTBOX, URL, PUBLIC_KEY, PRIVATE_KEY, - CREATED_AT, KEY_ID, FOLLOWING, FOLLOWERS) + CREATED_AT, KEY_ID, FOLLOWING, FOLLOWERS, INSTANCE) VALUES (10, 'test-user10', 'example.com', 'Im test-user10.', 'THis account is test-user10.', '5e884898da28047151d0e56f8dc6292773603d0d6aabbdd62a11ef721d1542d8', 'https://example.com/users/test-user10/inbox', @@ -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'); + 'https://example.com/users/test-user10/followers', null); insert into POSTS (ID, "userId", OVERVIEW, TEXT, "createdAt", VISIBILITY, URL, "repostId", "replyId", SENSITIVE, AP_ID) VALUES (1240, 10, null, 'test post', 12345680, 0, 'https://example.com/users/test-user10/posts/1240', null, null, false, diff --git a/src/intTest/resources/sql/note/匿名でfollowers投稿を取得しようとすると404.sql b/src/intTest/resources/sql/note/匿名でfollowers投稿を取得しようとすると404.sql index 71ee8f8d..1da76bf1 100644 --- a/src/intTest/resources/sql/note/匿名でfollowers投稿を取得しようとすると404.sql +++ b/src/intTest/resources/sql/note/匿名でfollowers投稿を取得しようとすると404.sql @@ -1,5 +1,5 @@ insert into "USERS" (ID, NAME, DOMAIN, SCREEN_NAME, DESCRIPTION, PASSWORD, INBOX, OUTBOX, URL, PUBLIC_KEY, PRIVATE_KEY, - CREATED_AT, KEY_ID, FOLLOWING, FOLLOWERS) + CREATED_AT, KEY_ID, FOLLOWING, FOLLOWERS, INSTANCE) VALUES (3, 'test-user3', 'example.com', 'Im test user3.', 'THis account is test user3.', '5e884898da28047151d0e56f8dc6292773603d0d6aabbdd62a11ef721d1542d8', 'https://example.com/users/test-user3/inbox', @@ -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'); + 'https://example.com/users/test-user3/followers', null); insert into POSTS (ID, "userId", OVERVIEW, TEXT, "createdAt", VISIBILITY, URL, "repostId", "replyId", SENSITIVE, AP_ID) VALUES (1236, 3, null, 'test post', 12345680, 2, 'https://example.com/users/test-user3/posts/1236', null, null, false, diff --git a/src/intTest/resources/sql/note/匿名でpublic投稿を取得できる.sql b/src/intTest/resources/sql/note/匿名でpublic投稿を取得できる.sql index 23f38afc..e8362d0f 100644 --- a/src/intTest/resources/sql/note/匿名でpublic投稿を取得できる.sql +++ b/src/intTest/resources/sql/note/匿名でpublic投稿を取得できる.sql @@ -1,12 +1,12 @@ insert into "USERS" (ID, NAME, DOMAIN, SCREEN_NAME, DESCRIPTION, PASSWORD, INBOX, OUTBOX, URL, PUBLIC_KEY, PRIVATE_KEY, - CREATED_AT, KEY_ID, FOLLOWING, FOLLOWERS) + CREATED_AT, KEY_ID, FOLLOWING, FOLLOWERS, INSTANCE) VALUES (1, 'test-user', 'example.com', 'Im test user.', 'THis account is test user.', '5e884898da28047151d0e56f8dc6292773603d0d6aabbdd62a11ef721d1542d8', 'https://example.com/users/test-user/inbox', 'https://example.com/users/test-user/outbox', 'https://example.com/users/test-user', '-----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'); + 'https://example.com/users/test-users/followers', null); insert into POSTS (ID, "userId", OVERVIEW, TEXT, "createdAt", VISIBILITY, URL, "repostId", "replyId", SENSITIVE, AP_ID) VALUES (1234, 1, null, 'test post', 12345680, 0, 'https://example.com/users/test-user/posts/1234', null, null, false, diff --git a/src/intTest/resources/sql/note/匿名でunlisted投稿を取得できる.sql b/src/intTest/resources/sql/note/匿名でunlisted投稿を取得できる.sql index 88c8bf9a..fa04a3ce 100644 --- a/src/intTest/resources/sql/note/匿名でunlisted投稿を取得できる.sql +++ b/src/intTest/resources/sql/note/匿名でunlisted投稿を取得できる.sql @@ -1,5 +1,5 @@ insert into "USERS" (ID, NAME, DOMAIN, SCREEN_NAME, DESCRIPTION, PASSWORD, INBOX, OUTBOX, URL, PUBLIC_KEY, PRIVATE_KEY, - CREATED_AT, KEY_ID, FOLLOWING, FOLLOWERS) + CREATED_AT, KEY_ID, FOLLOWING, FOLLOWERS, INSTANCE) VALUES (2, 'test-user2', 'example.com', 'Im test user2.', 'THis account is test user2.', '5e884898da28047151d0e56f8dc6292773603d0d6aabbdd62a11ef721d1542d8', 'https://example.com/users/test-user2/inbox', @@ -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'); + 'https://example.com/users/test-user2/followers', null); insert into POSTS (ID, "userId", OVERVIEW, TEXT, "createdAt", VISIBILITY, URL, "repostId", "replyId", SENSITIVE, AP_ID) VALUES (1235, 2, null, 'test post', 12345680, 1, 'https://example.com/users/test-user2/posts/1235', null, null, false, diff --git a/src/intTest/resources/sql/test-user.sql b/src/intTest/resources/sql/test-user.sql index 8b6df0d4..a21d1795 100644 --- a/src/intTest/resources/sql/test-user.sql +++ b/src/intTest/resources/sql/test-user.sql @@ -1,9 +1,9 @@ insert into "USERS" (ID, NAME, DOMAIN, SCREEN_NAME, DESCRIPTION, PASSWORD, INBOX, OUTBOX, URL, PUBLIC_KEY, PRIVATE_KEY, - CREATED_AT, KEY_ID, FOLLOWING, FOLLOWERS) + CREATED_AT, KEY_ID, FOLLOWING, FOLLOWERS, INSTANCE) VALUES (1, 'test-user', 'example.com', 'Im test user.', 'THis account is test user.', '5e884898da28047151d0e56f8dc6292773603d0d6aabbdd62a11ef721d1542d8', 'https://example.com/users/test-user/inbox', 'https://example.com/users/test-user/outbox', 'https://example.com/users/test-user', '-----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'); + 'https://example.com/users/test-users/followers', null); diff --git a/src/main/kotlin/dev/usbharu/hideout/activitypub/domain/model/objects/ObjectValue.kt b/src/main/kotlin/dev/usbharu/hideout/activitypub/domain/model/objects/ObjectValue.kt index 1cd01d81..b97b2541 100644 --- a/src/main/kotlin/dev/usbharu/hideout/activitypub/domain/model/objects/ObjectValue.kt +++ b/src/main/kotlin/dev/usbharu/hideout/activitypub/domain/model/objects/ObjectValue.kt @@ -1,5 +1,6 @@ package dev.usbharu.hideout.activitypub.domain.model.objects +@Suppress("VariableNaming") open class ObjectValue : Object { var `object`: String? = null diff --git a/src/main/kotlin/dev/usbharu/hideout/activitypub/service/common/APResourceResolveServiceImpl.kt b/src/main/kotlin/dev/usbharu/hideout/activitypub/service/common/APResourceResolveServiceImpl.kt index 49906265..81b7aec3 100644 --- a/src/main/kotlin/dev/usbharu/hideout/activitypub/service/common/APResourceResolveServiceImpl.kt +++ b/src/main/kotlin/dev/usbharu/hideout/activitypub/service/common/APResourceResolveServiceImpl.kt @@ -3,7 +3,10 @@ package dev.usbharu.hideout.activitypub.service.common import dev.usbharu.hideout.activitypub.domain.model.objects.Object import dev.usbharu.hideout.core.domain.model.user.User import dev.usbharu.hideout.core.domain.model.user.UserRepository +import dev.usbharu.hideout.core.service.resource.CacheManager +import dev.usbharu.hideout.core.service.resource.ResolveResponse import org.springframework.stereotype.Service +import java.io.InputStream @Service class APResourceResolveServiceImpl( @@ -25,7 +28,7 @@ class APResourceResolveServiceImpl( cacheManager.putCache(key) { runResolve(url, singerId?.let { userRepository.findById(it) }, clazz) } - return cacheManager.getOrWait(key) as T + return (cacheManager.getOrWait(key) as APResolveResponse).objects } private suspend fun internalResolve(url: String, singer: User?, clazz: Class): T { @@ -33,11 +36,12 @@ class APResourceResolveServiceImpl( cacheManager.putCache(key) { runResolve(url, singer, clazz) } - return cacheManager.getOrWait(key) as T + return (cacheManager.getOrWait(key) as APResolveResponse).objects } - private suspend fun runResolve(url: String, singer: User?, clazz: Class): Object = - apRequestService.apGet(url, singer, clazz) + private suspend fun runResolve(url: String, singer: User?, clazz: Class): ResolveResponse { + return APResolveResponse(apRequestService.apGet(url, singer, clazz)) + } private fun genCacheKey(url: String, singerId: Long?): String { if (singerId != null) { @@ -45,4 +49,30 @@ class APResourceResolveServiceImpl( } return url } + + private class APResolveResponse(val objects: T) : ResolveResponse { + override suspend fun body(): InputStream { + TODO("Not yet implemented") + } + + override suspend fun bodyAsText(): String { + TODO("Not yet implemented") + } + + override suspend fun bodyAsBytes(): ByteArray { + TODO("Not yet implemented") + } + + override suspend fun header(): Map> { + TODO("Not yet implemented") + } + + override suspend fun status(): Int { + TODO("Not yet implemented") + } + + override suspend fun statusMessage(): String { + TODO("Not yet implemented") + } + } } diff --git a/src/main/kotlin/dev/usbharu/hideout/activitypub/service/common/CacheManager.kt b/src/main/kotlin/dev/usbharu/hideout/activitypub/service/common/CacheManager.kt deleted file mode 100644 index 83ae4f9d..00000000 --- a/src/main/kotlin/dev/usbharu/hideout/activitypub/service/common/CacheManager.kt +++ /dev/null @@ -1,9 +0,0 @@ -package dev.usbharu.hideout.activitypub.service.common - -import dev.usbharu.hideout.activitypub.domain.model.objects.Object - -interface CacheManager { - - suspend fun putCache(key: String, block: suspend () -> Object) - suspend fun getOrWait(key: String): Object -} 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 8aee16a4..9c0777e9 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 @@ -124,7 +124,8 @@ class APUserServiceImpl( ?: throw IllegalActivityPubObjectException("publicKey is null"), keyId = person.publicKey?.id ?: throw IllegalActivityPubObjectException("publicKey keyId is null"), following = person.following, - followers = person.followers + followers = person.followers, + sharedInbox = person.endpoints["sharedInbox"] ) ) } diff --git a/src/main/kotlin/dev/usbharu/hideout/core/domain/model/instance/Instance.kt b/src/main/kotlin/dev/usbharu/hideout/core/domain/model/instance/Instance.kt new file mode 100644 index 00000000..c777dcdc --- /dev/null +++ b/src/main/kotlin/dev/usbharu/hideout/core/domain/model/instance/Instance.kt @@ -0,0 +1,18 @@ +package dev.usbharu.hideout.core.domain.model.instance + +import java.time.Instant + +data class Instance( + val id: Long, + val name: String, + val description: String, + val url: String, + val iconUrl: String, + val sharedInbox: String?, + val software: String, + val version: String, + val isBlocked: Boolean, + val isMuted: Boolean, + val moderationNote: String, + val createdAt: Instant +) diff --git a/src/main/kotlin/dev/usbharu/hideout/core/domain/model/instance/InstanceRepository.kt b/src/main/kotlin/dev/usbharu/hideout/core/domain/model/instance/InstanceRepository.kt new file mode 100644 index 00000000..35b6026e --- /dev/null +++ b/src/main/kotlin/dev/usbharu/hideout/core/domain/model/instance/InstanceRepository.kt @@ -0,0 +1,8 @@ +package dev.usbharu.hideout.core.domain.model.instance + +interface InstanceRepository { + suspend fun generateId(): Long + suspend fun save(instance: Instance): Instance + suspend fun findById(id: Long): Instance + suspend fun delete(instance: Instance) +} diff --git a/src/main/kotlin/dev/usbharu/hideout/core/domain/model/instance/Nodeinfo.kt b/src/main/kotlin/dev/usbharu/hideout/core/domain/model/instance/Nodeinfo.kt new file mode 100644 index 00000000..427c76a3 --- /dev/null +++ b/src/main/kotlin/dev/usbharu/hideout/core/domain/model/instance/Nodeinfo.kt @@ -0,0 +1,15 @@ +package dev.usbharu.hideout.core.domain.model.instance + +class Nodeinfo { + + var links: List = emptyList() + + private constructor() +} + +class Links { + var rel: String? = null + var href: String? = null + + private constructor() +} diff --git a/src/main/kotlin/dev/usbharu/hideout/core/domain/model/instance/Nodeinfo2_0.kt b/src/main/kotlin/dev/usbharu/hideout/core/domain/model/instance/Nodeinfo2_0.kt new file mode 100644 index 00000000..fcd99c73 --- /dev/null +++ b/src/main/kotlin/dev/usbharu/hideout/core/domain/model/instance/Nodeinfo2_0.kt @@ -0,0 +1,23 @@ +package dev.usbharu.hideout.core.domain.model.instance + +@Suppress("ClassNaming") +class Nodeinfo2_0 { + var metadata: Metadata? = null + var software: Software? = null + + constructor() +} + +class Metadata { + var nodeName: String? = null + var nodeDescription: String? = null + + constructor() +} + +class Software { + var name: String? = null + var version: String? = null + + constructor() +} diff --git a/src/main/kotlin/dev/usbharu/hideout/core/domain/model/user/User.kt b/src/main/kotlin/dev/usbharu/hideout/core/domain/model/user/User.kt index bf021757..4c2ded1c 100644 --- a/src/main/kotlin/dev/usbharu/hideout/core/domain/model/user/User.kt +++ b/src/main/kotlin/dev/usbharu/hideout/core/domain/model/user/User.kt @@ -21,16 +21,18 @@ data class User private constructor( val createdAt: Instant, val keyId: String, val followers: String? = null, - val following: String? = null + val following: String? = null, + val instance: Long? = null ) { override fun toString(): String = "User(id=$id, name='$name', domain='$domain', screenName='$screenName', description='$description'," + - " password=$password, inbox='$inbox', outbox='$outbox', url='$url', publicKey='$publicKey'," + - " privateKey=$privateKey, createdAt=$createdAt, keyId='$keyId', followers=$followers," + - " following=$following)" + " password=$password, inbox='$inbox', outbox='$outbox', url='$url', publicKey='$publicKey', " + + "privateKey=$privateKey, createdAt=$createdAt, keyId='$keyId', followers=$followers," + + " following=$following, instance=$instance)" @Component class UserBuilder(private val characterLimit: CharacterLimit, private val applicationConfig: ApplicationConfig) { + private val logger = LoggerFactory.getLogger(UserBuilder::class.java) @Suppress("LongParameterList", "FunctionMinLength", "LongMethod") @@ -49,7 +51,8 @@ data class User private constructor( createdAt: Instant, keyId: String, following: String? = null, - followers: String? = null + followers: String? = null, + instance: Long? = null ): User { // idは0未満ではいけない require(id >= 0) { "id must be greater than or equal to 0." } @@ -141,7 +144,8 @@ data class User private constructor( createdAt = createdAt, keyId = keyId, followers = followers, - following = following + following = following, + instance = instance ) } } diff --git a/src/main/kotlin/dev/usbharu/hideout/core/infrastructure/exposed/UserResultRowMapper.kt b/src/main/kotlin/dev/usbharu/hideout/core/infrastructure/exposed/UserResultRowMapper.kt index c55a352a..bad247f3 100644 --- a/src/main/kotlin/dev/usbharu/hideout/core/infrastructure/exposed/UserResultRowMapper.kt +++ b/src/main/kotlin/dev/usbharu/hideout/core/infrastructure/exposed/UserResultRowMapper.kt @@ -25,7 +25,8 @@ class UserResultRowMapper(private val userBuilder: User.UserBuilder) : ResultRow createdAt = Instant.ofEpochMilli((resultRow[Users.createdAt])), keyId = resultRow[Users.keyId], followers = resultRow[Users.followers], - following = resultRow[Users.following] + following = resultRow[Users.following], + instance = resultRow[Users.instance] ) } } diff --git a/src/main/kotlin/dev/usbharu/hideout/core/infrastructure/exposedquery/FollowerQueryServiceImpl.kt b/src/main/kotlin/dev/usbharu/hideout/core/infrastructure/exposedquery/FollowerQueryServiceImpl.kt index 3034c0cb..4c62003e 100644 --- a/src/main/kotlin/dev/usbharu/hideout/core/infrastructure/exposedquery/FollowerQueryServiceImpl.kt +++ b/src/main/kotlin/dev/usbharu/hideout/core/infrastructure/exposedquery/FollowerQueryServiceImpl.kt @@ -38,7 +38,8 @@ class FollowerQueryServiceImpl(private val userBuilder: User.UserBuilder) : Foll followers[Users.createdAt], followers[Users.keyId], followers[Users.following], - followers[Users.followers] + followers[Users.followers], + followers[Users.instance] ) .select { Users.id eq id } .map { @@ -57,7 +58,8 @@ class FollowerQueryServiceImpl(private val userBuilder: User.UserBuilder) : Foll createdAt = Instant.ofEpochMilli(it[followers[Users.createdAt]]), keyId = it[followers[Users.keyId]], followers = it[followers[Users.followers]], - following = it[followers[Users.following]] + following = it[followers[Users.following]], + instance = it[followers[Users.instance]] ) } } @@ -89,7 +91,8 @@ class FollowerQueryServiceImpl(private val userBuilder: User.UserBuilder) : Foll followers[Users.createdAt], followers[Users.keyId], followers[Users.following], - followers[Users.followers] + followers[Users.followers], + followers[Users.instance] ) .select { Users.name eq name and (Users.domain eq domain) } .map { @@ -108,7 +111,8 @@ class FollowerQueryServiceImpl(private val userBuilder: User.UserBuilder) : Foll createdAt = Instant.ofEpochMilli(it[followers[Users.createdAt]]), keyId = it[followers[Users.keyId]], followers = it[followers[Users.followers]], - following = it[followers[Users.following]] + following = it[followers[Users.following]], + instance = it[followers[Users.instance]] ) } } @@ -140,7 +144,8 @@ class FollowerQueryServiceImpl(private val userBuilder: User.UserBuilder) : Foll followers[Users.createdAt], followers[Users.keyId], followers[Users.following], - followers[Users.followers] + followers[Users.followers], + followers[Users.instance] ) .select { followers[Users.id] eq id } .map { @@ -159,7 +164,8 @@ class FollowerQueryServiceImpl(private val userBuilder: User.UserBuilder) : Foll createdAt = Instant.ofEpochMilli(it[followers[Users.createdAt]]), keyId = it[followers[Users.keyId]], followers = it[followers[Users.followers]], - following = it[followers[Users.following]] + following = it[followers[Users.following]], + instance = it[followers[Users.instance]] ) } } @@ -191,7 +197,8 @@ class FollowerQueryServiceImpl(private val userBuilder: User.UserBuilder) : Foll followers[Users.createdAt], followers[Users.keyId], followers[Users.following], - followers[Users.followers] + followers[Users.followers], + followers[Users.instance] ) .select { followers[Users.name] eq name and (followers[Users.domain] eq domain) } .map { @@ -210,7 +217,8 @@ class FollowerQueryServiceImpl(private val userBuilder: User.UserBuilder) : Foll createdAt = Instant.ofEpochMilli(it[followers[Users.createdAt]]), keyId = it[followers[Users.keyId]], followers = it[followers[Users.followers]], - following = it[followers[Users.following]] + following = it[followers[Users.following]], + instance = it[followers[Users.instance]] ) } } diff --git a/src/main/kotlin/dev/usbharu/hideout/core/infrastructure/exposedquery/InstanceQueryServiceImpl.kt b/src/main/kotlin/dev/usbharu/hideout/core/infrastructure/exposedquery/InstanceQueryServiceImpl.kt new file mode 100644 index 00000000..587f57a1 --- /dev/null +++ b/src/main/kotlin/dev/usbharu/hideout/core/infrastructure/exposedquery/InstanceQueryServiceImpl.kt @@ -0,0 +1,16 @@ +package dev.usbharu.hideout.core.infrastructure.exposedquery + +import dev.usbharu.hideout.core.domain.exception.FailedToGetResourcesException +import dev.usbharu.hideout.core.infrastructure.exposedrepository.Instance +import dev.usbharu.hideout.core.infrastructure.exposedrepository.toInstance +import dev.usbharu.hideout.core.query.InstanceQueryService +import dev.usbharu.hideout.util.singleOr +import org.jetbrains.exposed.sql.select +import org.springframework.stereotype.Repository +import dev.usbharu.hideout.core.domain.model.instance.Instance as InstanceEntity + +@Repository +class InstanceQueryServiceImpl : InstanceQueryService { + override suspend fun findByUrl(url: String): InstanceEntity = Instance.select { Instance.url eq url } + .singleOr { FailedToGetResourcesException("url is doesn't exist") }.toInstance() +} diff --git a/src/main/kotlin/dev/usbharu/hideout/core/infrastructure/exposedrepository/InstanceRepositoryImpl.kt b/src/main/kotlin/dev/usbharu/hideout/core/infrastructure/exposedrepository/InstanceRepositoryImpl.kt new file mode 100644 index 00000000..edd79195 --- /dev/null +++ b/src/main/kotlin/dev/usbharu/hideout/core/infrastructure/exposedrepository/InstanceRepositoryImpl.kt @@ -0,0 +1,93 @@ +package dev.usbharu.hideout.core.infrastructure.exposedrepository + +import dev.usbharu.hideout.application.service.id.IdGenerateService +import dev.usbharu.hideout.core.domain.exception.FailedToGetResourcesException +import dev.usbharu.hideout.core.domain.model.instance.InstanceRepository +import dev.usbharu.hideout.util.singleOr +import org.jetbrains.exposed.sql.* +import org.jetbrains.exposed.sql.SqlExpressionBuilder.eq +import org.jetbrains.exposed.sql.javatime.timestamp +import org.springframework.stereotype.Repository +import dev.usbharu.hideout.core.domain.model.instance.Instance as InstanceEntity + +@Repository +class InstanceRepositoryImpl(private val idGenerateService: IdGenerateService) : InstanceRepository { + override suspend fun generateId(): Long = idGenerateService.generateId() + + override suspend fun save(instance: InstanceEntity): InstanceEntity { + if (Instance.select { Instance.id.eq(instance.id) }.empty()) { + Instance.insert { + it[id] = instance.id + it[name] = instance.name + it[description] = instance.description + it[url] = instance.url + it[iconUrl] = instance.iconUrl + it[sharedInbox] = instance.sharedInbox + it[software] = instance.software + it[version] = instance.version + it[isBlocked] = instance.isBlocked + it[isMuted] = instance.isMuted + it[moderationNote] = instance.moderationNote + it[createdAt] = instance.createdAt + } + } else { + Instance.update({ Instance.id eq instance.id }) { + it[name] = instance.name + it[description] = instance.description + it[url] = instance.url + it[iconUrl] = instance.iconUrl + it[sharedInbox] = instance.sharedInbox + it[software] = instance.software + it[version] = instance.version + it[isBlocked] = instance.isBlocked + it[isMuted] = instance.isMuted + it[moderationNote] = instance.moderationNote + it[createdAt] = instance.createdAt + } + } + return instance + } + + override suspend fun findById(id: Long): InstanceEntity { + return Instance.select { Instance.id eq id } + .singleOr { FailedToGetResourcesException("id: $id doesn't exist.") }.toInstance() + } + + override suspend fun delete(instance: InstanceEntity) { + Instance.deleteWhere { Instance.id eq instance.id } + } +} + +fun ResultRow.toInstance(): InstanceEntity { + return InstanceEntity( + id = this[Instance.id], + name = this[Instance.name], + description = this[Instance.description], + url = this[Instance.url], + iconUrl = this[Instance.iconUrl], + sharedInbox = this[Instance.sharedInbox], + software = this[Instance.software], + version = this[Instance.version], + isBlocked = this[Instance.isBlocked], + isMuted = this[Instance.isMuted], + moderationNote = this[Instance.moderationNote], + createdAt = this[Instance.createdAt] + ) +} + +object Instance : Table("instance") { + val id = long("id") + val name = varchar("name", 1000) + val description = varchar("description", 5000) + val url = varchar("url", 255) + val iconUrl = varchar("icon_url", 255) + val sharedInbox = varchar("shared_inbox", 255).nullable() + val software = varchar("software", 255) + val version = varchar("version", 255) + val isBlocked = bool("is_blocked") + val isMuted = bool("is_muted") + val moderationNote = varchar("moderation_note", 10000) + val createdAt = timestamp("created_at") + + override val primaryKey: PrimaryKey = PrimaryKey(id) +} diff --git a/src/main/kotlin/dev/usbharu/hideout/core/infrastructure/exposedrepository/UserRepositoryImpl.kt b/src/main/kotlin/dev/usbharu/hideout/core/infrastructure/exposedrepository/UserRepositoryImpl.kt index 6d45a5e7..5cd94ccf 100644 --- a/src/main/kotlin/dev/usbharu/hideout/core/infrastructure/exposedrepository/UserRepositoryImpl.kt +++ b/src/main/kotlin/dev/usbharu/hideout/core/infrastructure/exposedrepository/UserRepositoryImpl.kt @@ -35,6 +35,7 @@ class UserRepositoryImpl( it[keyId] = user.keyId it[following] = user.following it[followers] = user.followers + it[instance] = user.instance } } else { Users.update({ Users.id eq user.id }) { @@ -52,6 +53,7 @@ class UserRepositoryImpl( it[keyId] = user.keyId it[following] = user.following it[followers] = user.followers + it[instance] = user.instance } } return user @@ -98,6 +100,7 @@ object Users : Table("users") { 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() override val primaryKey: PrimaryKey = PrimaryKey(id) diff --git a/src/main/kotlin/dev/usbharu/hideout/core/query/InstanceQueryService.kt b/src/main/kotlin/dev/usbharu/hideout/core/query/InstanceQueryService.kt new file mode 100644 index 00000000..79e6b213 --- /dev/null +++ b/src/main/kotlin/dev/usbharu/hideout/core/query/InstanceQueryService.kt @@ -0,0 +1,7 @@ +package dev.usbharu.hideout.core.query + +import dev.usbharu.hideout.core.domain.model.instance.Instance + +interface InstanceQueryService { + suspend fun findByUrl(url: String): Instance +} diff --git a/src/main/kotlin/dev/usbharu/hideout/core/service/instance/InstanceCreateDto.kt b/src/main/kotlin/dev/usbharu/hideout/core/service/instance/InstanceCreateDto.kt new file mode 100644 index 00000000..d5345cf0 --- /dev/null +++ b/src/main/kotlin/dev/usbharu/hideout/core/service/instance/InstanceCreateDto.kt @@ -0,0 +1,11 @@ +package dev.usbharu.hideout.core.service.instance + +data class InstanceCreateDto( + val name: String?, + val description: String?, + val url: String, + val iconUrl: String, + val sharedInbox: String?, + val software: String?, + val version: String?, +) 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 new file mode 100644 index 00000000..4b0e2640 --- /dev/null +++ b/src/main/kotlin/dev/usbharu/hideout/core/service/instance/InstanceService.kt @@ -0,0 +1,114 @@ +package dev.usbharu.hideout.core.service.instance + +import com.fasterxml.jackson.databind.ObjectMapper +import dev.usbharu.hideout.core.domain.exception.FailedToGetResourcesException +import dev.usbharu.hideout.core.domain.model.instance.Instance +import dev.usbharu.hideout.core.domain.model.instance.InstanceRepository +import dev.usbharu.hideout.core.domain.model.instance.Nodeinfo +import dev.usbharu.hideout.core.domain.model.instance.Nodeinfo2_0 +import dev.usbharu.hideout.core.query.InstanceQueryService +import dev.usbharu.hideout.core.service.resource.ResourceResolveService +import org.slf4j.LoggerFactory +import org.springframework.beans.factory.annotation.Qualifier +import org.springframework.stereotype.Service +import java.net.URL +import java.time.Instant + +interface InstanceService { + suspend fun fetchInstance(url: String, sharedInbox: String? = null): Instance + suspend fun createNewInstance(instanceCreateDto: InstanceCreateDto): Instance +} + +@Service +class InstanceServiceImpl( + private val instanceRepository: InstanceRepository, + private val resourceResolveService: ResourceResolveService, + @Qualifier("activitypub") private val objectMapper: ObjectMapper, + private val instanceQueryService: InstanceQueryService +) : InstanceService { + override suspend fun fetchInstance(url: String, sharedInbox: String?): Instance { + val u = URL(url) + val resolveInstanceUrl = u.protocol + "://" + u.host + + try { + return instanceQueryService.findByUrl(url) + } catch (e: FailedToGetResourcesException) { + logger.info("Instance not found. try fetch instance info. url: {}", resolveInstanceUrl) + logger.debug("Failed to get resources. url: {}", resolveInstanceUrl, e) + } + + 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" -> { + 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) + } + + // 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 -> { + TODO() + } + } + } + + throw IllegalStateException("Nodeinfo aren't found.") + } + + override suspend fun createNewInstance(instanceCreateDto: InstanceCreateDto): Instance { + val instance = Instance( + id = instanceRepository.generateId(), + name = instanceCreateDto.name ?: instanceCreateDto.url, + description = instanceCreateDto.description.orEmpty(), + url = instanceCreateDto.url, + iconUrl = instanceCreateDto.iconUrl, + sharedInbox = instanceCreateDto.sharedInbox, + software = instanceCreateDto.software ?: "unknown", + version = instanceCreateDto.version ?: "unknown", + isBlocked = false, + isMuted = false, + moderationNote = "", + createdAt = Instant.now() + ) + instanceRepository.save(instance) + return instance + } + + companion object { + private val logger = LoggerFactory.getLogger(InstanceServiceImpl::class.java) + } +} diff --git a/src/main/kotlin/dev/usbharu/hideout/core/service/media/MediaServiceImpl.kt b/src/main/kotlin/dev/usbharu/hideout/core/service/media/MediaServiceImpl.kt index fbf99674..ff8f1d93 100644 --- a/src/main/kotlin/dev/usbharu/hideout/core/service/media/MediaServiceImpl.kt +++ b/src/main/kotlin/dev/usbharu/hideout/core/service/media/MediaServiceImpl.kt @@ -24,7 +24,7 @@ open class MediaServiceImpl( private val remoteMediaDownloadService: RemoteMediaDownloadService, private val renameService: MediaFileRenameService ) : MediaService { - @Suppress("LongMethod") + @Suppress("LongMethod", "NestedBlockDepth") override suspend fun uploadLocalMedia(mediaRequest: MediaRequest): EntityMedia { val fileName = mediaRequest.file.name logger.info( @@ -95,6 +95,7 @@ open class MediaServiceImpl( } // TODO: 仮の処理として保存したように動かす + @Suppress("LongMethod", "NestedBlockDepth") override suspend fun uploadRemoteMedia(remoteMedia: RemoteMedia): Media { logger.info("MEDIA Remote media. filename:${remoteMedia.name} url:${remoteMedia.url}") diff --git a/src/main/kotlin/dev/usbharu/hideout/core/service/resource/CacheManager.kt b/src/main/kotlin/dev/usbharu/hideout/core/service/resource/CacheManager.kt new file mode 100644 index 00000000..44dfd2a6 --- /dev/null +++ b/src/main/kotlin/dev/usbharu/hideout/core/service/resource/CacheManager.kt @@ -0,0 +1,6 @@ +package dev.usbharu.hideout.core.service.resource + +interface CacheManager { + suspend fun putCache(key: String, block: suspend () -> ResolveResponse) + suspend fun getOrWait(key: String): ResolveResponse +} diff --git a/src/main/kotlin/dev/usbharu/hideout/activitypub/service/common/InMemoryCacheManager.kt b/src/main/kotlin/dev/usbharu/hideout/core/service/resource/InMemoryCacheManager.kt similarity index 83% rename from src/main/kotlin/dev/usbharu/hideout/activitypub/service/common/InMemoryCacheManager.kt rename to src/main/kotlin/dev/usbharu/hideout/core/service/resource/InMemoryCacheManager.kt index 3c8320db..b48fadd7 100644 --- a/src/main/kotlin/dev/usbharu/hideout/activitypub/service/common/InMemoryCacheManager.kt +++ b/src/main/kotlin/dev/usbharu/hideout/core/service/resource/InMemoryCacheManager.kt @@ -1,6 +1,5 @@ -package dev.usbharu.hideout.activitypub.service.common +package dev.usbharu.hideout.core.service.resource -import dev.usbharu.hideout.activitypub.domain.model.objects.Object import dev.usbharu.hideout.util.LruCache import kotlinx.coroutines.delay import kotlinx.coroutines.sync.Mutex @@ -11,10 +10,10 @@ import java.time.Instant @Service class InMemoryCacheManager : CacheManager { private val cacheKey = LruCache(15) - private val valueStore = mutableMapOf() + private val valueStore = mutableMapOf() private val keyMutex = Mutex() - override suspend fun putCache(key: String, block: suspend () -> Object) { + override suspend fun putCache(key: String, block: suspend () -> ResolveResponse) { val needRunBlock: Boolean keyMutex.withLock { cacheKey.filter { Instant.ofEpochMilli(it.value).plusSeconds(300) <= Instant.now() } @@ -38,7 +37,7 @@ class InMemoryCacheManager : CacheManager { } } - override suspend fun getOrWait(key: String): Object { + override suspend fun getOrWait(key: String): ResolveResponse { while (valueStore.contains(key).not()) { if (cacheKey.containsKey(key).not()) { throw IllegalStateException("Invalid cache key.") diff --git a/src/main/kotlin/dev/usbharu/hideout/core/service/resource/KtorResolveResponse.kt b/src/main/kotlin/dev/usbharu/hideout/core/service/resource/KtorResolveResponse.kt new file mode 100644 index 00000000..3a5e2ad1 --- /dev/null +++ b/src/main/kotlin/dev/usbharu/hideout/core/service/resource/KtorResolveResponse.kt @@ -0,0 +1,31 @@ +package dev.usbharu.hideout.core.service.resource + +import io.ktor.client.statement.* +import io.ktor.util.* +import io.ktor.utils.io.jvm.javaio.* +import java.io.InputStream + +class KtorResolveResponse(val ktorHttpResponse: HttpResponse) : ResolveResponse { + + private lateinit var _bodyAsText: String + private lateinit var _bodyAsBytes: ByteArray + + override suspend fun body(): InputStream = ktorHttpResponse.bodyAsChannel().toInputStream() + override suspend fun bodyAsText(): String { + if (!this::_bodyAsText.isInitialized) { + _bodyAsText = ktorHttpResponse.bodyAsText() + } + return _bodyAsText + } + + override suspend fun bodyAsBytes(): ByteArray { + if (!this::_bodyAsBytes.isInitialized) { + _bodyAsBytes = ktorHttpResponse.readBytes() + } + return _bodyAsBytes + } + + override suspend fun header(): Map> = ktorHttpResponse.headers.toMap() + override suspend fun status(): Int = ktorHttpResponse.status.value + override suspend fun statusMessage(): String = ktorHttpResponse.status.description +} diff --git a/src/main/kotlin/dev/usbharu/hideout/core/service/resource/KtorResourceResolveService.kt b/src/main/kotlin/dev/usbharu/hideout/core/service/resource/KtorResourceResolveService.kt new file mode 100644 index 00000000..4cd99bc8 --- /dev/null +++ b/src/main/kotlin/dev/usbharu/hideout/core/service/resource/KtorResourceResolveService.kt @@ -0,0 +1,24 @@ +package dev.usbharu.hideout.core.service.resource + +import io.ktor.client.* +import io.ktor.client.request.* +import org.springframework.stereotype.Service + +@Service +open class KtorResourceResolveService(private val httpClient: HttpClient, private val cacheManager: CacheManager) : + ResourceResolveService { + override suspend fun resolve(url: String): ResolveResponse { + cacheManager.putCache(getCacheKey(url)) { + runResolve(url) + } + return cacheManager.getOrWait(getCacheKey(url)) + } + + protected suspend fun runResolve(url: String): ResolveResponse { + val httpResponse = httpClient.get(url) + + return KtorResolveResponse(httpResponse) + } + + protected suspend fun getCacheKey(url: String) = url +} diff --git a/src/main/kotlin/dev/usbharu/hideout/core/service/resource/ResolveResponse.kt b/src/main/kotlin/dev/usbharu/hideout/core/service/resource/ResolveResponse.kt new file mode 100644 index 00000000..6b0cc202 --- /dev/null +++ b/src/main/kotlin/dev/usbharu/hideout/core/service/resource/ResolveResponse.kt @@ -0,0 +1,12 @@ +package dev.usbharu.hideout.core.service.resource + +import java.io.InputStream + +interface ResolveResponse { + suspend fun body(): InputStream + suspend fun bodyAsText(): String + suspend fun bodyAsBytes(): ByteArray + suspend fun header(): Map> + suspend fun status(): Int + suspend fun statusMessage(): String +} diff --git a/src/main/kotlin/dev/usbharu/hideout/core/service/resource/ResourceResolveService.kt b/src/main/kotlin/dev/usbharu/hideout/core/service/resource/ResourceResolveService.kt new file mode 100644 index 00000000..b2229b30 --- /dev/null +++ b/src/main/kotlin/dev/usbharu/hideout/core/service/resource/ResourceResolveService.kt @@ -0,0 +1,5 @@ +package dev.usbharu.hideout.core.service.resource + +interface ResourceResolveService { + suspend fun resolve(url: String): ResolveResponse +} diff --git a/src/main/kotlin/dev/usbharu/hideout/core/service/user/RemoteUserCreateDto.kt b/src/main/kotlin/dev/usbharu/hideout/core/service/user/RemoteUserCreateDto.kt index 992956e3..de85e74d 100644 --- a/src/main/kotlin/dev/usbharu/hideout/core/service/user/RemoteUserCreateDto.kt +++ b/src/main/kotlin/dev/usbharu/hideout/core/service/user/RemoteUserCreateDto.kt @@ -11,5 +11,6 @@ data class RemoteUserCreateDto( val publicKey: String, val keyId: String, val followers: String?, - val following: String? + val following: String?, + val sharedInbox: String? ) 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 40df3646..6ee4a53c 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 @@ -8,7 +8,9 @@ import dev.usbharu.hideout.core.domain.model.user.UserRepository import dev.usbharu.hideout.core.query.FollowerQueryService import dev.usbharu.hideout.core.query.UserQueryService import dev.usbharu.hideout.core.service.follow.SendFollowDto +import dev.usbharu.hideout.core.service.instance.InstanceService import org.jetbrains.exposed.exceptions.ExposedSQLException +import org.slf4j.LoggerFactory import org.springframework.stereotype.Service import java.time.Instant @@ -20,7 +22,8 @@ class UserServiceImpl( private val userQueryService: UserQueryService, private val followerQueryService: FollowerQueryService, private val userBuilder: User.UserBuilder, - private val applicationConfig: ApplicationConfig + private val applicationConfig: ApplicationConfig, + private val instanceService: InstanceService ) : UserService { @@ -49,12 +52,20 @@ class UserServiceImpl( createdAt = Instant.now(), following = "$userUrl/following", followers = "$userUrl/followers", - keyId = "$userUrl#pubkey" + keyId = "$userUrl#pubkey", + instance = null ) return userRepository.save(userEntity) } override suspend fun createRemoteUser(user: RemoteUserCreateDto): User { + val instance = try { + instanceService.fetchInstance(user.url, user.sharedInbox) + } catch (e: Exception) { + logger.warn("FAILED to fetch instance. url: {}", user.url, e) + null + } + val nextId = userRepository.nextId() val userEntity = userBuilder.of( id = nextId, @@ -69,7 +80,8 @@ class UserServiceImpl( createdAt = Instant.now(), followers = user.followers, following = user.following, - keyId = user.keyId + keyId = user.keyId, + instance = instance?.id ) return try { userRepository.save(userEntity) @@ -106,4 +118,8 @@ class UserServiceImpl( followerQueryService.removeFollower(id, followerId) return false } + + companion object { + private val logger = LoggerFactory.getLogger(UserServiceImpl::class.java) + } } diff --git a/src/test/kotlin/dev/usbharu/hideout/activitypub/service/common/APResourceResolveServiceImplTest.kt b/src/test/kotlin/dev/usbharu/hideout/activitypub/service/common/APResourceResolveServiceImplTest.kt index 46b50898..5f167d6b 100644 --- a/src/test/kotlin/dev/usbharu/hideout/activitypub/service/common/APResourceResolveServiceImplTest.kt +++ b/src/test/kotlin/dev/usbharu/hideout/activitypub/service/common/APResourceResolveServiceImplTest.kt @@ -1,6 +1,7 @@ package dev.usbharu.hideout.activitypub.service.common import dev.usbharu.hideout.core.domain.model.user.UserRepository +import dev.usbharu.hideout.core.service.resource.InMemoryCacheManager import kotlinx.coroutines.async import kotlinx.coroutines.awaitAll import kotlinx.coroutines.test.runTest diff --git a/src/test/kotlin/dev/usbharu/hideout/core/service/user/UserServiceTest.kt b/src/test/kotlin/dev/usbharu/hideout/core/service/user/UserServiceTest.kt index a39dbd70..65a88e8b 100644 --- a/src/test/kotlin/dev/usbharu/hideout/core/service/user/UserServiceTest.kt +++ b/src/test/kotlin/dev/usbharu/hideout/core/service/user/UserServiceTest.kt @@ -41,6 +41,7 @@ class UserServiceTest { mock(), userBuilder, testApplicationConfig, + mock() ) userService.createLocalUser(UserCreateDto("test", "testUser", "XXXXXXXXXXXXX", "test")) verify(userRepository, times(1)).save(any()) @@ -67,7 +68,7 @@ class UserServiceTest { onBlocking { nextId() } doReturn 113345L } val userService = - UserServiceImpl(userRepository, mock(), mock(), mock(), mock(), userBuilder, testApplicationConfig) + UserServiceImpl(userRepository, mock(), mock(), mock(), mock(), userBuilder, testApplicationConfig, mock()) val user = RemoteUserCreateDto( name = "test", domain = "remote.example.com", @@ -79,7 +80,8 @@ class UserServiceTest { publicKey = "-----BEGIN PUBLIC KEY-----...-----END PUBLIC KEY-----", keyId = "a", following = "", - followers = "" + followers = "", + sharedInbox = null ) userService.createRemoteUser(user) verify(userRepository, times(1)).save(any())