Merge pull request #155 from usbharu/feature/instance

Feature/instance
This commit is contained in:
usbharu 2023-11-19 12:18:29 +09:00 committed by GitHub
commit f2ab68adcb
37 changed files with 507 additions and 65 deletions

View File

@ -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);

View File

@ -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);

View File

@ -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);

View File

@ -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,

View File

@ -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,

View File

@ -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,

View File

@ -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,

View File

@ -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,

View File

@ -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);

View File

@ -1,5 +1,6 @@
package dev.usbharu.hideout.activitypub.domain.model.objects
@Suppress("VariableNaming")
open class ObjectValue : Object {
var `object`: String? = null

View File

@ -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<T>).objects
}
private suspend fun <T : Object> internalResolve(url: String, singer: User?, clazz: Class<T>): 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<T>).objects
}
private suspend fun <T : Object> runResolve(url: String, singer: User?, clazz: Class<T>): Object =
apRequestService.apGet(url, singer, clazz)
private suspend fun <T : Object> runResolve(url: String, singer: User?, clazz: Class<T>): 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<T : Object>(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<String, List<String>> {
TODO("Not yet implemented")
}
override suspend fun status(): Int {
TODO("Not yet implemented")
}
override suspend fun statusMessage(): String {
TODO("Not yet implemented")
}
}
}

View File

@ -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
}

View File

@ -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"]
)
)
}

View File

@ -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
)

View File

@ -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)
}

View File

@ -0,0 +1,15 @@
package dev.usbharu.hideout.core.domain.model.instance
class Nodeinfo {
var links: List<Links> = emptyList()
private constructor()
}
class Links {
var rel: String? = null
var href: String? = null
private constructor()
}

View File

@ -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()
}

View File

@ -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
)
}
}

View File

@ -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]
)
}
}

View File

@ -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]]
)
}
}

View File

@ -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()
}

View File

@ -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)
}

View File

@ -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)

View File

@ -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
}

View File

@ -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?,
)

View File

@ -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)
}
}

View File

@ -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}")

View File

@ -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
}

View File

@ -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<String, Long>(15)
private val valueStore = mutableMapOf<String, Object>()
private val valueStore = mutableMapOf<String, ResolveResponse>()
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.")

View File

@ -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<String, List<String>> = ktorHttpResponse.headers.toMap()
override suspend fun status(): Int = ktorHttpResponse.status.value
override suspend fun statusMessage(): String = ktorHttpResponse.status.description
}

View File

@ -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
}

View File

@ -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<String, List<String>>
suspend fun status(): Int
suspend fun statusMessage(): String
}

View File

@ -0,0 +1,5 @@
package dev.usbharu.hideout.core.service.resource
interface ResourceResolveService {
suspend fun resolve(url: String): ResolveResponse
}

View File

@ -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?
)

View File

@ -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)
}
}

View File

@ -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

View File

@ -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())