feat: フォローされたときにAcceptを返すように

This commit is contained in:
usbharu 2023-04-10 00:36:36 +09:00
parent 2faef66dc2
commit 839aa693ce
12 changed files with 169 additions and 29 deletions

View File

@ -19,6 +19,9 @@ import dev.usbharu.hideout.service.job.JobQueueParentService
import dev.usbharu.hideout.service.job.KJobJobQueueParentService import dev.usbharu.hideout.service.job.KJobJobQueueParentService
import dev.usbharu.hideout.service.signature.HttpSignatureVerifyService import dev.usbharu.hideout.service.signature.HttpSignatureVerifyService
import dev.usbharu.hideout.service.signature.HttpSignatureVerifyServiceImpl 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 io.ktor.server.application.*
import org.jetbrains.exposed.sql.Database import org.jetbrains.exposed.sql.Database
import org.koin.ktor.ext.inject import org.koin.ktor.ext.inject
@ -54,14 +57,23 @@ fun Application.module() {
single<IUserAuthRepository> { UserAuthRepository(get()) } single<IUserAuthRepository> { UserAuthRepository(get()) }
single<IUserAuthService> { UserAuthService(get(), get()) } single<IUserAuthService> { UserAuthService(get(), get()) }
single<HttpSignatureVerifyService> { HttpSignatureVerifyServiceImpl(get()) } single<HttpSignatureVerifyService> { HttpSignatureVerifyServiceImpl(get()) }
single<JobQueueParentService> { val kJobJobQueueService = KJobJobQueueParentService(get()) single<JobQueueParentService> {
val kJobJobQueueService = KJobJobQueueParentService(get())
kJobJobQueueService.init(listOf()) kJobJobQueueService.init(listOf())
kJobJobQueueService kJobJobQueueService
} }
single<ActivityPubFollowService>{ ActivityPubFollowServiceImpl(get()) } single<HttpClient> {
HttpClient(CIO).config {
install(Logging) {
logger = Logger.DEFAULT
level = LogLevel.ALL
}
}
}
single<ActivityPubFollowService> { ActivityPubFollowServiceImpl(get(), get(), get()) }
single<ActivityPubService> { ActivityPubServiceImpl(get()) } single<ActivityPubService> { ActivityPubServiceImpl(get()) }
single<UserService> { UserService(get()) } single<UserService> { UserService(get()) }
single<ActivityPubUserService> { ActivityPubUserServiceImpl(get(), get()) } single<ActivityPubUserService> { ActivityPubUserServiceImpl(get(), get(),get()) }
} }

View File

@ -2,16 +2,36 @@ package dev.usbharu.hideout.domain.model
import org.jetbrains.exposed.dao.id.LongIdTable 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( data class UserEntity(
val id: Long, val id: Long,
val name: String, val name: String,
val domain:String, val domain: String,
val screenName: 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") { object Users : LongIdTable("users") {
@ -19,6 +39,10 @@ object Users : LongIdTable("users") {
val domain = varchar("domain", length = 255) val domain = varchar("domain", length = 255)
val screenName = varchar("screen_name", length = 64) val screenName = varchar("screen_name", length = 64)
val description = varchar("description", length = 600) 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 { init {
uniqueIndex(name, domain) uniqueIndex(name, domain)
} }

View File

@ -1,5 +0,0 @@
package dev.usbharu.hideout.domain.model.job
import kjob.core.Job
object AcceptFollowJob : Job("AcceptFollowJob")

View File

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

View File

@ -10,6 +10,8 @@ interface IUserRepository {
suspend fun findByName(name: String): UserEntity? suspend fun findByName(name: String): UserEntity?
suspend fun findByUrl(url:String):UserEntity?
suspend fun update(userEntity: UserEntity) suspend fun update(userEntity: UserEntity)
suspend fun delete(id: Long) suspend fun delete(id: Long)

View File

@ -25,7 +25,10 @@ class UserRepository(private val database: Database) : IUserRepository {
this[Users.name], this[Users.name],
this[Users.domain], this[Users.domain],
this[Users.screenName], 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.name],
this[Users.domain], this[Users.domain],
this[Users.screenName], 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<UserEntity> { override suspend fun findFollowersById(id: Long): List<UserEntity> {
return query { return query {
val followers = Users.alias("FOLLOWERS") val followers = Users.alias("FOLLOWERS")
@ -91,7 +103,13 @@ class UserRepository(private val database: Database) : IUserRepository {
onColumn = { UsersFollowers.followerId }, onColumn = { UsersFollowers.followerId },
otherColumn = { followers[Users.id] }) 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 } .select { Users.id eq id }
.map { .map {
UserEntity( UserEntity(
@ -100,6 +118,9 @@ class UserRepository(private val database: Database) : IUserRepository {
domain = it[followers[Users.domain]], domain = it[followers[Users.domain]],
screenName = it[followers[Users.screenName]], screenName = it[followers[Users.screenName]],
description = it[followers[Users.description]], description = it[followers[Users.description]],
inbox = it[followers[Users.inbox]],
outbox = it[followers[Users.outbox]],
url = it[followers[Users.url]],
) )
} }
} }

View File

@ -1,16 +1,40 @@
package dev.usbharu.hideout.service.activitypub package dev.usbharu.hideout.service.activitypub
import com.fasterxml.jackson.module.kotlin.readValue
import dev.usbharu.hideout.ap.Follow 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.ActivityPubResponse
import dev.usbharu.hideout.domain.model.ActivityPubStringResponse 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 dev.usbharu.hideout.service.job.JobQueueParentService
import io.ktor.client.*
import io.ktor.http.* 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 { override suspend fun receiveFollow(follow: Follow): ActivityPubResponse {
// TODO: Verify HTTP Signature // TODO: Verify HTTP Signature
jobQueueParentService.schedule(AcceptFollowJob) jobQueueParentService.schedule(ReceiveFollowJob) {
return ActivityPubStringResponse(HttpStatusCode.OK,"{}",ContentType.Application.Json) 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<ReceiveFollowJob>) {
val actor = props[ReceiveFollowJob.actor]
val person = activityPubUserService.fetchPerson(actor)
val follow = Config.configData.objectMapper.readValue<Follow>(props[ReceiveFollowJob.follow])
httpClient.postAp(
urlString = person.inbox ?: throw IllegalArgumentException("inbox is not found"),
username = "${props[ReceiveFollowJob.targetActor]}#pubkey",
jsonLd = follow
)
} }
} }

View File

@ -4,4 +4,6 @@ import dev.usbharu.hideout.ap.Person
interface ActivityPubUserService { interface ActivityPubUserService {
suspend fun getPersonByName(name:String):Person suspend fun getPersonByName(name:String):Person
suspend fun fetchPerson(url:String):Person
} }

View File

@ -1,15 +1,23 @@
package dev.usbharu.hideout.service.activitypub package dev.usbharu.hideout.service.activitypub
import com.fasterxml.jackson.module.kotlin.readValue
import dev.usbharu.hideout.ap.Image import dev.usbharu.hideout.ap.Image
import dev.usbharu.hideout.ap.Key import dev.usbharu.hideout.ap.Key
import dev.usbharu.hideout.ap.Person import dev.usbharu.hideout.ap.Person
import dev.usbharu.hideout.config.Config import dev.usbharu.hideout.config.Config
import dev.usbharu.hideout.exception.UserNotFoundException
import dev.usbharu.hideout.service.IUserAuthService import dev.usbharu.hideout.service.IUserAuthService
import dev.usbharu.hideout.service.impl.UserService 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 { ActivityPubUserService {
override suspend fun getPersonByName(name: String): Person { override suspend fun getPersonByName(name: String): Person {
// TODO: JOINで書き直し
val userEntity = userService.findByName(name) val userEntity = userService.findByName(name)
val userAuthEntity = userAuthService.findByUserId(userEntity.id) val userAuthEntity = userAuthService.findByUserId(userEntity.id)
val userUrl = "${Config.configData.url}/users/$name" 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<Person>(httpResponse.bodyAsText())
}
}
} }

View File

@ -9,11 +9,7 @@ import dev.usbharu.hideout.repository.IUserAuthRepository
import dev.usbharu.hideout.repository.IUserRepository import dev.usbharu.hideout.repository.IUserRepository
import dev.usbharu.hideout.service.IUserAuthService import dev.usbharu.hideout.service.IUserAuthService
import io.ktor.util.* import io.ktor.util.*
import java.security.KeyPair import java.security.*
import java.security.KeyPairGenerator
import java.security.MessageDigest
import java.security.PrivateKey
import java.security.PublicKey
import java.security.interfaces.RSAPrivateKey import java.security.interfaces.RSAPrivateKey
import java.security.interfaces.RSAPublicKey import java.security.interfaces.RSAPublicKey
import java.util.* import java.util.*
@ -35,11 +31,15 @@ class UserAuthService(
} }
override suspend fun registerAccount(username: String, hash: String) { override suspend fun registerAccount(username: String, hash: String) {
val url = "${Config.configData.url}/users/$username"
val registerUser = User( val registerUser = User(
name = username, name = username,
domain = Config.configData.domain, domain = Config.configData.domain,
screenName = username, screenName = username,
description = "" description = "",
inbox = "$url/inbox",
outbox = "$url/outbox",
url = url
) )
val createdUser = userRepository.create(registerUser) val createdUser = userRepository.create(registerUser)
@ -83,8 +83,6 @@ class UserAuthService(
} }
companion object { companion object {
val sha256: MessageDigest = MessageDigest.getInstance("SHA-256") val sha256: MessageDigest = MessageDigest.getInstance("SHA-256")
} }

View File

@ -26,6 +26,10 @@ class UserService(private val userRepository: IUserRepository) {
?: throw UserNotFoundException("$name was not found.") ?: 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 { suspend fun create(user: User): UserEntity {
return userRepository.create(user) return userRepository.create(user)
} }

View File

@ -66,8 +66,12 @@ class WebFingerService(
userModel.preferredUsername ?: throw IllegalStateException(), userModel.preferredUsername ?: throw IllegalStateException(),
domain, domain,
userName, userName,
userModel.summary.orEmpty() userModel.summary.orEmpty(),
"",
"",
""
) )
TODO()
return userService.create(user) return userService.create(user)
} }
} }