feat: Followを受け取ったらprintするように

This commit is contained in:
usbharu 2023-03-30 15:33:19 +09:00
parent 013bf02484
commit 08d36724f5
23 changed files with 261 additions and 40 deletions

View File

@ -49,6 +49,9 @@ dependencies {
testImplementation("io.ktor:ktor-server-tests-jvm:$ktor_version") testImplementation("io.ktor:ktor-server-tests-jvm:$ktor_version")
testImplementation("org.jetbrains.kotlin:kotlin-test-junit:$kotlin_version") testImplementation("org.jetbrains.kotlin:kotlin-test-junit:$kotlin_version")
implementation("io.ktor:ktor-client-core:$ktor_version")
implementation("io.ktor:ktor-client-cio:$ktor_version")
} }
jib { jib {

View File

@ -36,8 +36,9 @@ fun Application.module() {
} }
single<ConfigData> { single<ConfigData> {
ConfigData( ConfigData(
environment.config.property("hideout.hostname").getString(), url = environment.config.propertyOrNull("hideout.url")?.getString()
jacksonObjectMapper().enable(DeserializationFeature.ACCEPT_SINGLE_VALUE_AS_ARRAY) ?: environment.config.property("hideout.hostname").getString(),
objectMapper = jacksonObjectMapper().enable(DeserializationFeature.ACCEPT_SINGLE_VALUE_AS_ARRAY)
.setSerializationInclusion(JsonInclude.Include.NON_EMPTY) .setSerializationInclusion(JsonInclude.Include.NON_EMPTY)
) )
} }
@ -45,7 +46,7 @@ fun Application.module() {
single<IUserAuthRepository> { UserAuthRepository(get()) } single<IUserAuthRepository> { UserAuthRepository(get()) }
single<IUserAuthService> { UserAuthService(get(), get()) } single<IUserAuthService> { UserAuthService(get(), get()) }
single<UserService> { UserService(get()) } single<UserService> { UserService(get()) }
single<ActivityPubUserService> { ActivityPubUserService(get(),get()) } single<ActivityPubUserService> { ActivityPubUserService(get(), get(),get()) }
} }
configureKoin(module) configureKoin(module)
val configData by inject<ConfigData>() val configData by inject<ConfigData>()

View File

@ -2,7 +2,7 @@ package dev.usbharu.hideout.ap
open class Object : JsonLd { open class Object : JsonLd {
private var type: List<String> = emptyList() private var type: List<String> = emptyList()
private var name: String? = null var name: String? = null
protected constructor() protected constructor()
constructor(type: List<String>, name: String) : super() { constructor(type: List<String>, name: String) : super() {

View File

@ -2,8 +2,8 @@ package dev.usbharu.hideout.ap
open class Person : Object { open class Person : Object {
private var id:String? = null private var id:String? = null
private var preferredUsername:String? = null var preferredUsername:String? = null
private var summary:String? = null var summary:String? = null
private var inbox:String? = null private var inbox:String? = null
private var outbox:String? = null private var outbox:String? = null
private var url:String? = null private var url:String? = null

View File

@ -7,4 +7,8 @@ object Config {
var configData: ConfigData = ConfigData() var configData: ConfigData = ConfigData()
} }
data class ConfigData(val hostname: String = "", val objectMapper: ObjectMapper = jacksonObjectMapper()) data class ConfigData(
val url: String = "",
val domain: String = url.substringAfter("://").substringBeforeLast(":"),
val objectMapper: ObjectMapper = jacksonObjectMapper()
)

View File

@ -2,19 +2,24 @@ 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 screenName: String, val description: String) data class User(val name: String,val domain: String, val screenName: String, val description: String)
data class UserEntity( data class UserEntity(
val id: Long, val id: Long,
val name: String, val name: String,
val domain:String,
val screenName: String, val screenName: String,
val description: String val description: String
) { ) {
constructor(id: Long, user: User) : this(id, user.name, user.screenName, user.description) constructor(id: Long, user: User) : this(id, user.name,user.domain, user.screenName, user.description)
} }
object Users : LongIdTable("users") { object Users : LongIdTable("users") {
val name = varchar("name", length = 64).uniqueIndex() val name = varchar("name", length = 64)
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)
init {
uniqueIndex(name, domain)
}
} }

View File

@ -5,17 +5,17 @@ import org.jetbrains.exposed.sql.ReferenceOption
data class UserAuthentication( data class UserAuthentication(
val userId: Long, val userId: Long,
val hash: String, val hash: String?,
val publicKey: String, val publicKey: String,
val privateKey: String val privateKey: String?
) )
data class UserAuthenticationEntity( data class UserAuthenticationEntity(
val id: Long, val id: Long,
val userId: Long, val userId: Long,
val hash: String, val hash: String?,
val publicKey: String, val publicKey: String,
val privateKey: String val privateKey: String?
) { ) {
constructor(id: Long, userAuthentication: UserAuthentication) : this( constructor(id: Long, userAuthentication: UserAuthentication) : this(
id, id,
@ -28,7 +28,7 @@ data class UserAuthenticationEntity(
object UsersAuthentication : LongIdTable("users_auth") { object UsersAuthentication : LongIdTable("users_auth") {
val userId = long("user_id").references(Users.id, onUpdate = ReferenceOption.CASCADE) val userId = long("user_id").references(Users.id, onUpdate = ReferenceOption.CASCADE)
val hash = varchar("hash", length = 64) val hash = varchar("hash", length = 64).nullable()
val publicKey = varchar("public_key", length = 1000_000) val publicKey = varchar("public_key", length = 1000_000)
val privateKey = varchar("private_key", length = 1000_000) val privateKey = varchar("private_key", length = 1000_000).nullable()
} }

View File

@ -0,0 +1,11 @@
package dev.usbharu.hideout.domain.model
import org.jetbrains.exposed.dao.id.LongIdTable
object UsersFollowers : LongIdTable("users_followers") {
val userId = long("user_id").references(Users.id).index()
val followerId = long("follower_id").references(Users.id)
init {
uniqueIndex(userId, followerId)
}
}

View File

@ -17,4 +17,8 @@ interface IUserRepository {
suspend fun findAll(): List<User> suspend fun findAll(): List<User>
suspend fun findAllByLimitAndByOffset(limit: Int, offset: Long = 0): List<UserEntity> suspend fun findAllByLimitAndByOffset(limit: Int, offset: Long = 0): List<UserEntity>
suspend fun createFollower(id: Long, follower: Long)
suspend fun deleteFollower(id: Long, follower: Long)
suspend fun findFollowersById(id: Long): List<UserEntity>
} }

View File

@ -13,6 +13,7 @@ class UserAuthRepository(private val database: Database) : IUserAuthRepository {
init { init {
transaction(database) { transaction(database) {
SchemaUtils.create(UsersAuthentication) SchemaUtils.create(UsersAuthentication)
SchemaUtils.createMissingTablesAndColumns(UsersAuthentication)
} }
} }

View File

@ -3,6 +3,7 @@ package dev.usbharu.hideout.repository
import dev.usbharu.hideout.domain.model.User import dev.usbharu.hideout.domain.model.User
import dev.usbharu.hideout.domain.model.UserEntity import dev.usbharu.hideout.domain.model.UserEntity
import dev.usbharu.hideout.domain.model.Users import dev.usbharu.hideout.domain.model.Users
import dev.usbharu.hideout.domain.model.UsersFollowers
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import org.jetbrains.exposed.sql.* import org.jetbrains.exposed.sql.*
import org.jetbrains.exposed.sql.SqlExpressionBuilder.eq import org.jetbrains.exposed.sql.SqlExpressionBuilder.eq
@ -13,12 +14,16 @@ class UserRepository(private val database: Database) : IUserRepository {
init { init {
transaction(database) { transaction(database) {
SchemaUtils.create(Users) SchemaUtils.create(Users)
SchemaUtils.create(UsersFollowers)
SchemaUtils.createMissingTablesAndColumns(Users)
SchemaUtils.createMissingTablesAndColumns(UsersFollowers)
} }
} }
private fun ResultRow.toUser(): User { private fun ResultRow.toUser(): User {
return User( return User(
this[Users.name], this[Users.name],
this[Users.domain],
this[Users.screenName], this[Users.screenName],
this[Users.description] this[Users.description]
) )
@ -28,6 +33,7 @@ class UserRepository(private val database: Database) : IUserRepository {
return UserEntity( return UserEntity(
this[Users.id].value, this[Users.id].value,
this[Users.name], this[Users.name],
this[Users.domain],
this[Users.screenName], this[Users.screenName],
this[Users.description] this[Users.description]
) )
@ -40,12 +46,22 @@ class UserRepository(private val database: Database) : IUserRepository {
return query { return query {
UserEntity(Users.insert { UserEntity(Users.insert {
it[name] = user.name it[name] = user.name
it[domain] = user.domain
it[screenName] = user.screenName it[screenName] = user.screenName
it[description] = user.description it[description] = user.description
}[Users.id].value, user) }[Users.id].value, user)
} }
} }
override suspend fun createFollower(id: Long, follower: Long) {
return query {
UsersFollowers.insert {
it[userId] = id
it[followerId] = follower
}
}
}
override suspend fun findById(id: Long): UserEntity? { override suspend fun findById(id: Long): UserEntity? {
return query { return query {
Users.select { Users.id eq id }.map { Users.select { Users.id eq id }.map {
@ -62,11 +78,36 @@ class UserRepository(private val database: Database) : IUserRepository {
} }
} }
override suspend fun findFollowersById(id: Long): List<UserEntity> {
return query {
val followers = Users.alias("followers")
Users.leftJoin(
otherTable = UsersFollowers,
onColumn = { Users.id },
otherColumn = { UsersFollowers.userId })
.leftJoin(
otherTable = followers,
onColumn = { UsersFollowers.followerId },
otherColumn = { followers[Users.id] })
.select { Users.id eq id }
.map {
UserEntity(
id = it[followers[Users.id]].value,
name = it[followers[Users.name]],
domain = it[followers[Users.domain]],
screenName = it[followers[Users.screenName]],
description = it[followers[Users.description]],
)
}
}
}
override suspend fun update(userEntity: UserEntity) { override suspend fun update(userEntity: UserEntity) {
return query { return query {
Users.update({ Users.id eq userEntity.id }) { Users.update({ Users.id eq userEntity.id }) {
it[name] = userEntity.name it[name] = userEntity.name
it[domain] = userEntity.domain
it[screenName] = userEntity.screenName it[screenName] = userEntity.screenName
it[description] = userEntity.description it[description] = userEntity.description
} }
@ -79,6 +120,12 @@ class UserRepository(private val database: Database) : IUserRepository {
} }
} }
override suspend fun deleteFollower(id: Long, follower: Long) {
query {
UsersFollowers.deleteWhere { (userId eq id).and(followerId eq follower) }
}
}
override suspend fun findAll(): List<User> { override suspend fun findAll(): List<User> {
return query { return query {
Users.selectAll().map { it.toUser() } Users.selectAll().map { it.toUser() }

View File

@ -50,7 +50,8 @@ fun Application.user(userService: UserService, activityPubUserService: ActivityP
val userModel = activityPubUserService.generateUserModel(name!!) val userModel = activityPubUserService.generateUserModel(name!!)
return@get call.respondAp(userModel) return@get call.respondAp(userModel)
} }
name?.let { it1 -> userService.findByName(it1).id }
?.let { it2 -> println(userService.findFollowersById(it2)) }
val principal = call.principal<UserIdPrincipal>() val principal = call.principal<UserIdPrincipal>()
if (principal != null && name != null) { if (principal != null && name != null) {
// iUserService.findByName(name) // iUserService.findByName(name)

View File

@ -16,7 +16,7 @@ fun Application.wellKnown(userService: UserService) {
get("/host-meta") { get("/host-meta") {
//language=XML //language=XML
val xml = """ val xml = """
<?xml version="1.0" encoding="UTF-8"?><XRD xmlns="http://docs.oasis-open.org/ns/xri/xrd-1.0"><Link rel="lrdd" type="application/xrd+xml" template="${Config.configData.hostname}/.well-known/webfinger?resource={uri}"/></XRD> <?xml version="1.0" encoding="UTF-8"?><XRD xmlns="http://docs.oasis-open.org/ns/xri/xrd-1.0"><Link rel="lrdd" type="application/xrd+xml" template="${Config.configData.url}/.well-known/webfinger?resource={uri}"/></XRD>
""".trimIndent() """.trimIndent()
return@get call.respondText( return@get call.respondText(
contentType = ContentType("application", "xrd+xml"), contentType = ContentType("application", "xrd+xml"),
@ -32,7 +32,7 @@ fun Application.wellKnown(userService: UserService) {
{ {
"rel": "lrdd", "rel": "lrdd",
"type": "application/jrd+json", "type": "application/jrd+json",
"template": "${Config.configData.hostname}/.well-known/webfinger?resource={uri}" "template": "${Config.configData.url}/.well-known/webfinger?resource={uri}"
} }
] ]
} }
@ -67,7 +67,7 @@ fun Application.wellKnown(userService: UserService) {
WebFingerResource.Link( WebFingerResource.Link(
rel = "self", rel = "self",
type = ContentType.Application.Activity.toString(), type = ContentType.Application.Activity.toString(),
href = "${Config.configData.hostname}/users/${userEntity.name}" href = "${Config.configData.url}/users/${userEntity.name}"
) )
) )
) )

View File

@ -1,30 +1,38 @@
package dev.usbharu.hideout.routing package dev.usbharu.hideout.routing
import dev.usbharu.hideout.service.ActivityPubService
import dev.usbharu.hideout.util.HttpUtil
import io.ktor.http.* import io.ktor.http.*
import io.ktor.server.application.* import io.ktor.server.application.*
import io.ktor.server.request.*
import io.ktor.server.response.* import io.ktor.server.response.*
import io.ktor.server.routing.* import io.ktor.server.routing.*
fun Application.userActivityPubRouting() { fun Application.userActivityPubRouting(activityPubService: ActivityPubService) {
routing { routing {
route("/users/{name}") { route("/users/{name}") {
route("/inbox") { route("/inbox") {
get { get {
call.respond(HttpStatusCode.OK) call.respond(HttpStatusCode.MethodNotAllowed)
} }
post { post {
call.respond(HttpStatusCode.OK) if (!HttpUtil.isContentTypeOfActivityPub(call.request.contentType())) {
return@post call.respond(HttpStatusCode.BadRequest)
}
val bodyText = call.receiveText()
println(bodyText)
activityPubService.switchApType(bodyText)
} }
} }
route("/outbox") { route("/outbox") {
get { get {
call.respond(HttpStatusCode.OK) call.respond(HttpStatusCode.MethodNotAllowed)
} }
post { post {
call.respond(HttpStatusCode.OK) call.respond(HttpStatusCode.MethodNotAllowed)
} }
} }
} }

View File

@ -1,5 +1,20 @@
package dev.usbharu.hideout.service package dev.usbharu.hideout.service
class ActivityPubService { import com.fasterxml.jackson.databind.ObjectMapper
class ActivityPubService(private val objectMapper: ObjectMapper) {
enum class ActivityType{
Follow,
Undo
}
fun switchApType(json:String):ActivityType{
val typeAsText = objectMapper.readTree(json).get("type").asText()
return when(typeAsText){
"Follow" -> ActivityType.Follow
"Undo" -> ActivityType.Undo
else -> throw IllegalArgumentException()
}
}
} }

View File

@ -4,15 +4,21 @@ 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 io.ktor.client.*
import io.ktor.client.call.*
import io.ktor.client.plugins.*
import io.ktor.client.request.*
import io.ktor.http.*
class ActivityPubUserService( class ActivityPubUserService(
private val httpClient:HttpClient,
private val userService: UserService, private val userService: UserService,
private val userAuthService: IUserAuthService private val userAuthService: IUserAuthService
) { ) {
suspend fun generateUserModel(name: String): Person { suspend fun generateUserModel(name: String): Person {
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.hostname}/users/$name" val userUrl = "${Config.configData.url}/users/$name"
return Person( return Person(
type = emptyList(), type = emptyList(),
name = userEntity.name, name = userEntity.name,
@ -37,4 +43,19 @@ class ActivityPubUserService(
) )
) )
} }
suspend fun fetchUserModel(url:String):Person? {
return try {
httpClient.get(url).body<Person>()
} catch (e: ResponseException) {
if (e.response.status == HttpStatusCode.NotFound) {
return null
}
throw e
}
}
suspend fun receiveFollow(){
}
} }

View File

@ -0,0 +1,7 @@
package dev.usbharu.hideout.service
class HttpSignService {
suspend fun sign(){
}
}

View File

@ -0,0 +1,15 @@
package dev.usbharu.hideout.service
import dev.usbharu.hideout.domain.model.UserEntity
import dev.usbharu.hideout.webfinger.WebFinger
interface IWebFingerService {
suspend fun fetch(acct:String): WebFinger?
suspend fun sync(webFinger: WebFinger):UserEntity
suspend fun fetchAndSync(acct: String):UserEntity{
val webFinger = fetch(acct)?: throw IllegalArgumentException()
return sync(webFinger)
}
}

View File

@ -1,8 +1,10 @@
package dev.usbharu.hideout.service package dev.usbharu.hideout.service
import dev.usbharu.hideout.config.Config
import dev.usbharu.hideout.domain.model.User import dev.usbharu.hideout.domain.model.User
import dev.usbharu.hideout.domain.model.UserAuthentication import dev.usbharu.hideout.domain.model.UserAuthentication
import dev.usbharu.hideout.domain.model.UserAuthenticationEntity import dev.usbharu.hideout.domain.model.UserAuthenticationEntity
import dev.usbharu.hideout.domain.model.Users.screenName
import dev.usbharu.hideout.exception.UserNotFoundException import dev.usbharu.hideout.exception.UserNotFoundException
import dev.usbharu.hideout.repository.IUserAuthRepository import dev.usbharu.hideout.repository.IUserAuthRepository
import dev.usbharu.hideout.repository.IUserRepository import dev.usbharu.hideout.repository.IUserRepository
@ -10,7 +12,6 @@ import io.ktor.util.*
import java.security.KeyPair import java.security.KeyPair
import java.security.KeyPairGenerator import java.security.KeyPairGenerator
import java.security.MessageDigest import java.security.MessageDigest
import java.security.interfaces.RSAPrivateCrtKey
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.*
@ -34,6 +35,7 @@ class UserAuthService(
override suspend fun registerAccount(username: String, hash: String) { override suspend fun registerAccount(username: String, hash: String) {
val registerUser = User( val registerUser = User(
name = username, name = username,
domain = Config.configData.domain,
screenName = username, screenName = username,
description = "" description = ""
) )

View File

@ -4,8 +4,6 @@ import dev.usbharu.hideout.domain.model.User
import dev.usbharu.hideout.domain.model.UserEntity import dev.usbharu.hideout.domain.model.UserEntity
import dev.usbharu.hideout.exception.UserNotFoundException import dev.usbharu.hideout.exception.UserNotFoundException
import dev.usbharu.hideout.repository.IUserRepository import dev.usbharu.hideout.repository.IUserRepository
import dev.usbharu.hideout.repository.UserRepository
import org.jetbrains.exposed.sql.Database
import java.lang.Integer.min import java.lang.Integer.min
class UserService(private val userRepository: IUserRepository) { class UserService(private val userRepository: IUserRepository) {
@ -24,10 +22,20 @@ class UserService(private val userRepository: IUserRepository) {
} }
suspend fun findByName(name: String): UserEntity { suspend fun findByName(name: String): UserEntity {
return userRepository.findByName(name) ?: throw UserNotFoundException("$name was not found.") return userRepository.findByName(name)
?: throw UserNotFoundException("$name was not found.")
} }
suspend fun create(user: User): UserEntity { suspend fun create(user: User): UserEntity {
return userRepository.create(user) return userRepository.create(user)
} }
suspend fun findFollowersById(id: Long): List<UserEntity> {
return userRepository.findFollowersById(id)
}
suspend fun addFollowers(id: Long, follower: Long) {
return userRepository.createFollower(id, follower)
}
} }

View File

@ -0,0 +1,59 @@
package dev.usbharu.hideout.service
import dev.usbharu.hideout.domain.model.User
import dev.usbharu.hideout.domain.model.UserEntity
import dev.usbharu.hideout.util.HttpUtil
import dev.usbharu.hideout.webfinger.WebFinger
import io.ktor.client.*
import io.ktor.client.call.*
import io.ktor.client.plugins.*
import io.ktor.client.request.*
import io.ktor.http.*
class WebFingerService(
private val httpClient: HttpClient,
private val userService: UserService,
private val userAuthService: IUserAuthService,
private val activityPubUserService: ActivityPubUserService
) : IWebFingerService {
override suspend fun fetch(acct: String): WebFinger? {
val fullName = acct.substringAfter("acct:")
val domain = fullName.substringAfterLast("@")
return try {
httpClient.get("https://$domain/.well-known/webfinger?resource=acct:$fullName")
.body<WebFinger>()
} catch (e: ResponseException) {
if (e.response.status == HttpStatusCode.NotFound) {
return null
}
throw e
}
}
override suspend fun sync(webFinger: WebFinger): UserEntity {
val link = webFinger.links.find {
it.rel == "self" && HttpUtil.isContentTypeOfActivityPub(
ContentType.parse(
it.type.orEmpty()
)
)
}?.href ?: throw Exception()
val fullName = webFinger.subject.substringAfter("acct:")
val domain = fullName.substringAfterLast("@")
val userName = fullName.substringBeforeLast("@")
val userModel = activityPubUserService.fetchUserModel(link) ?: throw Exception()
val user = User(
userModel.preferredUsername ?: throw IllegalStateException(),
domain,
userName,
userModel.summary.orEmpty()
)
return userService.create(user)
}
}

View File

@ -15,11 +15,15 @@ object HttpUtil {
if (subType == "activity+json") { if (subType == "activity+json") {
return true return true
} }
if (subType == "ld+json") { return subType == "ld+json"
return true
} }
return false
fun isContentTypeOfActivityPub(contentType: ContentType): Boolean {
return isContentTypeOfActivityPub(
contentType.contentType,
contentType.contentSubtype,
contentType.parameter("profile").orEmpty()
)
} }
val ContentType.Application.Activity: ContentType val ContentType.Application.Activity: ContentType

View File

@ -0,0 +1,5 @@
package dev.usbharu.hideout.webfinger
class WebFinger(val subject: String, val aliases: List<String>, val links: List<Link>) {
class Link(val rel: String, val type: String?, val href: String?, val template: String)
}