diff --git a/src/main/kotlin/dev/usbharu/hideout/Application.kt b/src/main/kotlin/dev/usbharu/hideout/Application.kt index 0850d592..9321677c 100644 --- a/src/main/kotlin/dev/usbharu/hideout/Application.kt +++ b/src/main/kotlin/dev/usbharu/hideout/Application.kt @@ -19,6 +19,9 @@ import dev.usbharu.hideout.service.job.JobQueueParentService import dev.usbharu.hideout.service.job.KJobJobQueueParentService import dev.usbharu.hideout.service.signature.HttpSignatureVerifyService import dev.usbharu.hideout.service.signature.HttpSignatureVerifyServiceImpl +import io.ktor.client.* +import io.ktor.client.engine.cio.* +import io.ktor.client.plugins.logging.* import io.ktor.server.application.* import org.jetbrains.exposed.sql.Database import org.koin.ktor.ext.inject @@ -54,14 +57,23 @@ fun Application.module() { single { UserAuthRepository(get()) } single { UserAuthService(get(), get()) } single { HttpSignatureVerifyServiceImpl(get()) } - single { val kJobJobQueueService = KJobJobQueueParentService(get()) + single { + val kJobJobQueueService = KJobJobQueueParentService(get()) kJobJobQueueService.init(listOf()) kJobJobQueueService } - single{ ActivityPubFollowServiceImpl(get()) } + single { + HttpClient(CIO).config { + install(Logging) { + logger = Logger.DEFAULT + level = LogLevel.ALL + } + } + } + single { ActivityPubFollowServiceImpl(get(), get(), get()) } single { ActivityPubServiceImpl(get()) } single { UserService(get()) } - single { ActivityPubUserServiceImpl(get(), get()) } + single { ActivityPubUserServiceImpl(get(), get(),get()) } } diff --git a/src/main/kotlin/dev/usbharu/hideout/domain/model/User.kt b/src/main/kotlin/dev/usbharu/hideout/domain/model/User.kt index 19893891..28e57f99 100644 --- a/src/main/kotlin/dev/usbharu/hideout/domain/model/User.kt +++ b/src/main/kotlin/dev/usbharu/hideout/domain/model/User.kt @@ -2,16 +2,36 @@ package dev.usbharu.hideout.domain.model import org.jetbrains.exposed.dao.id.LongIdTable -data class User(val name: String,val domain: String, val screenName: String, val description: String) +data class User( + val name: String, + val domain: String, + val screenName: String, + val description: String, + val inbox: String, + val outbox: String, + val url: String +) data class UserEntity( val id: Long, val name: String, - val domain:String, + val domain: String, val screenName: String, - val description: String + val description: String, + val inbox: String, + val outbox: String, + val url: String ) { - constructor(id: Long, user: User) : this(id, user.name,user.domain, user.screenName, user.description) + constructor(id: Long, user: User) : this( + id, + user.name, + user.domain, + user.screenName, + user.description, + user.inbox, + user.outbox, + user.url + ) } object Users : LongIdTable("users") { @@ -19,6 +39,10 @@ object Users : LongIdTable("users") { val domain = varchar("domain", length = 255) val screenName = varchar("screen_name", length = 64) val description = varchar("description", length = 600) + val inbox = varchar("inbox", length = 255).uniqueIndex() + val outbox = varchar("outbox", length = 255).uniqueIndex() + val url = varchar("url", length = 255).uniqueIndex() + init { uniqueIndex(name, domain) } diff --git a/src/main/kotlin/dev/usbharu/hideout/domain/model/job/AcceptFollowJob.kt b/src/main/kotlin/dev/usbharu/hideout/domain/model/job/AcceptFollowJob.kt deleted file mode 100644 index 97748c4e..00000000 --- a/src/main/kotlin/dev/usbharu/hideout/domain/model/job/AcceptFollowJob.kt +++ /dev/null @@ -1,5 +0,0 @@ -package dev.usbharu.hideout.domain.model.job - -import kjob.core.Job - -object AcceptFollowJob : Job("AcceptFollowJob") diff --git a/src/main/kotlin/dev/usbharu/hideout/domain/model/job/ReceiveFollowJob.kt b/src/main/kotlin/dev/usbharu/hideout/domain/model/job/ReceiveFollowJob.kt new file mode 100644 index 00000000..6a200ec9 --- /dev/null +++ b/src/main/kotlin/dev/usbharu/hideout/domain/model/job/ReceiveFollowJob.kt @@ -0,0 +1,9 @@ +package dev.usbharu.hideout.domain.model.job + +import kjob.core.Job + +object ReceiveFollowJob : Job("ReceiveFollowJob"){ + val actor = string("actor") + val follow = string("follow") + val targetActor = string("targetActor") +} diff --git a/src/main/kotlin/dev/usbharu/hideout/repository/IUserRepository.kt b/src/main/kotlin/dev/usbharu/hideout/repository/IUserRepository.kt index 8da272fc..53001878 100644 --- a/src/main/kotlin/dev/usbharu/hideout/repository/IUserRepository.kt +++ b/src/main/kotlin/dev/usbharu/hideout/repository/IUserRepository.kt @@ -10,6 +10,8 @@ interface IUserRepository { suspend fun findByName(name: String): UserEntity? + suspend fun findByUrl(url:String):UserEntity? + suspend fun update(userEntity: UserEntity) suspend fun delete(id: Long) diff --git a/src/main/kotlin/dev/usbharu/hideout/repository/UserRepository.kt b/src/main/kotlin/dev/usbharu/hideout/repository/UserRepository.kt index c945aab8..2591e026 100644 --- a/src/main/kotlin/dev/usbharu/hideout/repository/UserRepository.kt +++ b/src/main/kotlin/dev/usbharu/hideout/repository/UserRepository.kt @@ -25,7 +25,10 @@ class UserRepository(private val database: Database) : IUserRepository { this[Users.name], this[Users.domain], this[Users.screenName], - this[Users.description] + this[Users.description], + this[Users.inbox], + this[Users.outbox], + this[Users.url] ) } @@ -35,7 +38,10 @@ class UserRepository(private val database: Database) : IUserRepository { this[Users.name], this[Users.domain], this[Users.screenName], - this[Users.description] + this[Users.description], + this[Users.inbox], + this[Users.outbox], + this[Users.url], ) } @@ -78,6 +84,12 @@ class UserRepository(private val database: Database) : IUserRepository { } } + override suspend fun findByUrl(url: String): UserEntity? { + return query { + Users.select { Users.url eq url }.singleOrNull()?.toUserEntity() + } + } + override suspend fun findFollowersById(id: Long): List { return query { val followers = Users.alias("FOLLOWERS") @@ -91,7 +103,13 @@ class UserRepository(private val database: Database) : IUserRepository { onColumn = { UsersFollowers.followerId }, otherColumn = { followers[Users.id] }) - .slice(followers.get(Users.id), followers.get(Users.name), followers.get(Users.domain), followers.get(Users.screenName), followers.get(Users.description)) + .slice( + followers.get(Users.id), + followers.get(Users.name), + followers.get(Users.domain), + followers.get(Users.screenName), + followers.get(Users.description) + ) .select { Users.id eq id } .map { UserEntity( @@ -100,6 +118,9 @@ class UserRepository(private val database: Database) : IUserRepository { domain = it[followers[Users.domain]], screenName = it[followers[Users.screenName]], description = it[followers[Users.description]], + inbox = it[followers[Users.inbox]], + outbox = it[followers[Users.outbox]], + url = it[followers[Users.url]], ) } } diff --git a/src/main/kotlin/dev/usbharu/hideout/service/activitypub/ActivityPubFollowServiceImpl.kt b/src/main/kotlin/dev/usbharu/hideout/service/activitypub/ActivityPubFollowServiceImpl.kt index 3e7a5dc3..3f311e5a 100644 --- a/src/main/kotlin/dev/usbharu/hideout/service/activitypub/ActivityPubFollowServiceImpl.kt +++ b/src/main/kotlin/dev/usbharu/hideout/service/activitypub/ActivityPubFollowServiceImpl.kt @@ -1,16 +1,40 @@ package dev.usbharu.hideout.service.activitypub +import com.fasterxml.jackson.module.kotlin.readValue import dev.usbharu.hideout.ap.Follow +import dev.usbharu.hideout.config.Config import dev.usbharu.hideout.domain.model.ActivityPubResponse import dev.usbharu.hideout.domain.model.ActivityPubStringResponse -import dev.usbharu.hideout.domain.model.job.AcceptFollowJob +import dev.usbharu.hideout.domain.model.job.ReceiveFollowJob +import dev.usbharu.hideout.plugins.postAp import dev.usbharu.hideout.service.job.JobQueueParentService +import io.ktor.client.* import io.ktor.http.* +import kjob.core.job.JobProps -class ActivityPubFollowServiceImpl(private val jobQueueParentService: JobQueueParentService) : ActivityPubFollowService { +class ActivityPubFollowServiceImpl( + private val jobQueueParentService: JobQueueParentService, + private val activityPubUserService: ActivityPubUserService, + private val httpClient: HttpClient +) : ActivityPubFollowService { override suspend fun receiveFollow(follow: Follow): ActivityPubResponse { // TODO: Verify HTTP Signature - jobQueueParentService.schedule(AcceptFollowJob) - return ActivityPubStringResponse(HttpStatusCode.OK,"{}",ContentType.Application.Json) + jobQueueParentService.schedule(ReceiveFollowJob) { + props[it.actor] = follow.actor + props[it.follow] = Config.configData.objectMapper.writeValueAsString(follow) + props[it.targetActor] = follow.`object` + } + return ActivityPubStringResponse(HttpStatusCode.OK, "{}", ContentType.Application.Json) + } + + suspend fun receiveFollowJob(props: JobProps) { + val actor = props[ReceiveFollowJob.actor] + val person = activityPubUserService.fetchPerson(actor) + val follow = Config.configData.objectMapper.readValue(props[ReceiveFollowJob.follow]) + httpClient.postAp( + urlString = person.inbox ?: throw IllegalArgumentException("inbox is not found"), + username = "${props[ReceiveFollowJob.targetActor]}#pubkey", + jsonLd = follow + ) } } diff --git a/src/main/kotlin/dev/usbharu/hideout/service/activitypub/ActivityPubUserService.kt b/src/main/kotlin/dev/usbharu/hideout/service/activitypub/ActivityPubUserService.kt index 55033e8c..0b67e383 100644 --- a/src/main/kotlin/dev/usbharu/hideout/service/activitypub/ActivityPubUserService.kt +++ b/src/main/kotlin/dev/usbharu/hideout/service/activitypub/ActivityPubUserService.kt @@ -4,4 +4,6 @@ import dev.usbharu.hideout.ap.Person interface ActivityPubUserService { suspend fun getPersonByName(name:String):Person + + suspend fun fetchPerson(url:String):Person } diff --git a/src/main/kotlin/dev/usbharu/hideout/service/activitypub/ActivityPubUserServiceImpl.kt b/src/main/kotlin/dev/usbharu/hideout/service/activitypub/ActivityPubUserServiceImpl.kt index 2b79799e..4af88bac 100644 --- a/src/main/kotlin/dev/usbharu/hideout/service/activitypub/ActivityPubUserServiceImpl.kt +++ b/src/main/kotlin/dev/usbharu/hideout/service/activitypub/ActivityPubUserServiceImpl.kt @@ -1,15 +1,23 @@ package dev.usbharu.hideout.service.activitypub +import com.fasterxml.jackson.module.kotlin.readValue import dev.usbharu.hideout.ap.Image import dev.usbharu.hideout.ap.Key import dev.usbharu.hideout.ap.Person import dev.usbharu.hideout.config.Config +import dev.usbharu.hideout.exception.UserNotFoundException import dev.usbharu.hideout.service.IUserAuthService import dev.usbharu.hideout.service.impl.UserService +import dev.usbharu.hideout.util.HttpUtil.Activity +import io.ktor.client.* +import io.ktor.client.request.* +import io.ktor.client.statement.* +import io.ktor.http.* -class ActivityPubUserServiceImpl(private val userService: UserService, private val userAuthService: IUserAuthService) : +class ActivityPubUserServiceImpl(private val userService: UserService, private val userAuthService: IUserAuthService,private val httpClient: HttpClient) : ActivityPubUserService { override suspend fun getPersonByName(name: String): Person { + // TODO: JOINで書き直し val userEntity = userService.findByName(name) val userAuthEntity = userAuthService.findByUserId(userEntity.id) val userUrl = "${Config.configData.url}/users/$name" @@ -37,4 +45,41 @@ class ActivityPubUserServiceImpl(private val userService: UserService, private v ) ) } + + override suspend fun fetchPerson(url: String): Person { + return try { + val userEntity = userService.findByUrl(url) + val userAuthEntity = userAuthService.findByUsername(userEntity.name) + return Person( + type = emptyList(), + name = userEntity.name, + id = url, + preferredUsername = userEntity.name, + summary = userEntity.description, + inbox = "$url/inbox", + outbox = "$url/outbox", + url = url, + icon = Image( + type = emptyList(), + name = "$url/icon.png", + mediaType = "image/png", + url = "$url/icon.png" + ), + publicKey = Key( + type = emptyList(), + name = "Public Key", + id = "$url#pubkey", + owner = url, + publicKeyPem = userAuthEntity.publicKey + ) + ) + + } catch (e:UserNotFoundException){ + val httpResponse = httpClient.get(url) { + accept(ContentType.Application.Activity) + } + Config.configData.objectMapper.readValue(httpResponse.bodyAsText()) + } + + } } diff --git a/src/main/kotlin/dev/usbharu/hideout/service/impl/UserAuthService.kt b/src/main/kotlin/dev/usbharu/hideout/service/impl/UserAuthService.kt index cf5b0a48..88247524 100644 --- a/src/main/kotlin/dev/usbharu/hideout/service/impl/UserAuthService.kt +++ b/src/main/kotlin/dev/usbharu/hideout/service/impl/UserAuthService.kt @@ -9,11 +9,7 @@ import dev.usbharu.hideout.repository.IUserAuthRepository import dev.usbharu.hideout.repository.IUserRepository import dev.usbharu.hideout.service.IUserAuthService import io.ktor.util.* -import java.security.KeyPair -import java.security.KeyPairGenerator -import java.security.MessageDigest -import java.security.PrivateKey -import java.security.PublicKey +import java.security.* import java.security.interfaces.RSAPrivateKey import java.security.interfaces.RSAPublicKey import java.util.* @@ -35,11 +31,15 @@ class UserAuthService( } override suspend fun registerAccount(username: String, hash: String) { + val url = "${Config.configData.url}/users/$username" val registerUser = User( name = username, domain = Config.configData.domain, screenName = username, - description = "" + description = "", + inbox = "$url/inbox", + outbox = "$url/outbox", + url = url ) val createdUser = userRepository.create(registerUser) @@ -83,8 +83,6 @@ class UserAuthService( } - - companion object { val sha256: MessageDigest = MessageDigest.getInstance("SHA-256") } diff --git a/src/main/kotlin/dev/usbharu/hideout/service/impl/UserService.kt b/src/main/kotlin/dev/usbharu/hideout/service/impl/UserService.kt index 4736338a..f090d38f 100644 --- a/src/main/kotlin/dev/usbharu/hideout/service/impl/UserService.kt +++ b/src/main/kotlin/dev/usbharu/hideout/service/impl/UserService.kt @@ -26,6 +26,10 @@ class UserService(private val userRepository: IUserRepository) { ?: throw UserNotFoundException("$name was not found.") } + suspend fun findByUrl(url: String): UserEntity { + return userRepository.findByUrl(url) ?: throw UserNotFoundException("$url was not found.") + } + suspend fun create(user: User): UserEntity { return userRepository.create(user) } diff --git a/src/main/kotlin/dev/usbharu/hideout/service/impl/WebFingerService.kt b/src/main/kotlin/dev/usbharu/hideout/service/impl/WebFingerService.kt index 8bf1c420..fe928bf6 100644 --- a/src/main/kotlin/dev/usbharu/hideout/service/impl/WebFingerService.kt +++ b/src/main/kotlin/dev/usbharu/hideout/service/impl/WebFingerService.kt @@ -66,8 +66,12 @@ class WebFingerService( userModel.preferredUsername ?: throw IllegalStateException(), domain, userName, - userModel.summary.orEmpty() + userModel.summary.orEmpty(), + "", + "", + "" ) + TODO() return userService.create(user) } }