From 08d36724f52df26a768bb203591d84b3d2fdd68f Mon Sep 17 00:00:00 2001 From: usbharu <64310155+usbharu@users.noreply.github.com> Date: Thu, 30 Mar 2023 15:33:19 +0900 Subject: [PATCH] =?UTF-8?q?feat:=20Follow=E3=82=92=E5=8F=97=E3=81=91?= =?UTF-8?q?=E5=8F=96=E3=81=A3=E3=81=9F=E3=82=89print=E3=81=99=E3=82=8B?= =?UTF-8?q?=E3=82=88=E3=81=86=E3=81=AB?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- build.gradle.kts | 3 + .../kotlin/dev/usbharu/hideout/Application.kt | 7 ++- .../kotlin/dev/usbharu/hideout/ap/Object.kt | 2 +- .../kotlin/dev/usbharu/hideout/ap/Person.kt | 4 +- .../dev/usbharu/hideout/config/Config.kt | 6 +- .../dev/usbharu/hideout/domain/model/User.kt | 11 +++- .../domain/model/UserAuthentication.kt | 12 ++-- .../hideout/domain/model/UsersFollowers.kt | 11 ++++ .../hideout/repository/IUserRepository.kt | 10 +++- .../hideout/repository/UserAuthRepository.kt | 1 + .../hideout/repository/UserRepository.kt | 47 +++++++++++++++ .../usbharu/hideout/routing/UserRouting.kt | 3 +- .../hideout/routing/WellKnownRouting.kt | 6 +- .../hideout/routing/userActivityPubRouting.kt | 18 ++++-- .../hideout/service/ActivityPubService.kt | 17 +++++- .../hideout/service/ActivityPubUserService.kt | 23 +++++++- .../hideout/service/HttpSignService.kt | 7 +++ .../hideout/service/IWebFingerService.kt | 15 +++++ .../hideout/service/UserAuthService.kt | 4 +- .../usbharu/hideout/service/UserService.kt | 16 +++-- .../hideout/service/WebFingerService.kt | 59 +++++++++++++++++++ .../dev/usbharu/hideout/util/HttpUtil.kt | 14 +++-- .../usbharu/hideout/webfinger/WebFinger.kt | 5 ++ 23 files changed, 261 insertions(+), 40 deletions(-) create mode 100644 src/main/kotlin/dev/usbharu/hideout/domain/model/UsersFollowers.kt create mode 100644 src/main/kotlin/dev/usbharu/hideout/service/HttpSignService.kt create mode 100644 src/main/kotlin/dev/usbharu/hideout/service/IWebFingerService.kt create mode 100644 src/main/kotlin/dev/usbharu/hideout/service/WebFingerService.kt create mode 100644 src/main/kotlin/dev/usbharu/hideout/webfinger/WebFinger.kt diff --git a/build.gradle.kts b/build.gradle.kts index f8d21559..c6ffbfb9 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -49,6 +49,9 @@ dependencies { testImplementation("io.ktor:ktor-server-tests-jvm:$ktor_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 { diff --git a/src/main/kotlin/dev/usbharu/hideout/Application.kt b/src/main/kotlin/dev/usbharu/hideout/Application.kt index 1668430e..b0c127a0 100644 --- a/src/main/kotlin/dev/usbharu/hideout/Application.kt +++ b/src/main/kotlin/dev/usbharu/hideout/Application.kt @@ -36,8 +36,9 @@ fun Application.module() { } single { ConfigData( - environment.config.property("hideout.hostname").getString(), - jacksonObjectMapper().enable(DeserializationFeature.ACCEPT_SINGLE_VALUE_AS_ARRAY) + url = environment.config.propertyOrNull("hideout.url")?.getString() + ?: environment.config.property("hideout.hostname").getString(), + objectMapper = jacksonObjectMapper().enable(DeserializationFeature.ACCEPT_SINGLE_VALUE_AS_ARRAY) .setSerializationInclusion(JsonInclude.Include.NON_EMPTY) ) } @@ -45,7 +46,7 @@ fun Application.module() { single { UserAuthRepository(get()) } single { UserAuthService(get(), get()) } single { UserService(get()) } - single { ActivityPubUserService(get(),get()) } + single { ActivityPubUserService(get(), get(),get()) } } configureKoin(module) val configData by inject() diff --git a/src/main/kotlin/dev/usbharu/hideout/ap/Object.kt b/src/main/kotlin/dev/usbharu/hideout/ap/Object.kt index 72bec7f7..aa3aa920 100644 --- a/src/main/kotlin/dev/usbharu/hideout/ap/Object.kt +++ b/src/main/kotlin/dev/usbharu/hideout/ap/Object.kt @@ -2,7 +2,7 @@ package dev.usbharu.hideout.ap open class Object : JsonLd { private var type: List = emptyList() - private var name: String? = null + var name: String? = null protected constructor() constructor(type: List, name: String) : super() { diff --git a/src/main/kotlin/dev/usbharu/hideout/ap/Person.kt b/src/main/kotlin/dev/usbharu/hideout/ap/Person.kt index 80d7b601..22651e5e 100644 --- a/src/main/kotlin/dev/usbharu/hideout/ap/Person.kt +++ b/src/main/kotlin/dev/usbharu/hideout/ap/Person.kt @@ -2,8 +2,8 @@ package dev.usbharu.hideout.ap open class Person : Object { private var id:String? = null - private var preferredUsername:String? = null - private var summary:String? = null + var preferredUsername:String? = null + var summary:String? = null private var inbox:String? = null private var outbox:String? = null private var url:String? = null diff --git a/src/main/kotlin/dev/usbharu/hideout/config/Config.kt b/src/main/kotlin/dev/usbharu/hideout/config/Config.kt index fc4fe21b..02358c41 100644 --- a/src/main/kotlin/dev/usbharu/hideout/config/Config.kt +++ b/src/main/kotlin/dev/usbharu/hideout/config/Config.kt @@ -7,4 +7,8 @@ object Config { 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() +) 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 a0ce15ce..19893891 100644 --- a/src/main/kotlin/dev/usbharu/hideout/domain/model/User.kt +++ b/src/main/kotlin/dev/usbharu/hideout/domain/model/User.kt @@ -2,19 +2,24 @@ package dev.usbharu.hideout.domain.model 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( val id: Long, val name: String, + val domain:String, val screenName: 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") { - 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 description = varchar("description", length = 600) + init { + uniqueIndex(name, domain) + } } diff --git a/src/main/kotlin/dev/usbharu/hideout/domain/model/UserAuthentication.kt b/src/main/kotlin/dev/usbharu/hideout/domain/model/UserAuthentication.kt index edbab5bd..d1cd3d82 100644 --- a/src/main/kotlin/dev/usbharu/hideout/domain/model/UserAuthentication.kt +++ b/src/main/kotlin/dev/usbharu/hideout/domain/model/UserAuthentication.kt @@ -5,17 +5,17 @@ import org.jetbrains.exposed.sql.ReferenceOption data class UserAuthentication( val userId: Long, - val hash: String, + val hash: String?, val publicKey: String, - val privateKey: String + val privateKey: String? ) data class UserAuthenticationEntity( val id: Long, val userId: Long, - val hash: String, + val hash: String?, val publicKey: String, - val privateKey: String + val privateKey: String? ) { constructor(id: Long, userAuthentication: UserAuthentication) : this( id, @@ -28,7 +28,7 @@ data class UserAuthenticationEntity( object UsersAuthentication : LongIdTable("users_auth") { 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 privateKey = varchar("private_key", length = 1000_000) + val privateKey = varchar("private_key", length = 1000_000).nullable() } diff --git a/src/main/kotlin/dev/usbharu/hideout/domain/model/UsersFollowers.kt b/src/main/kotlin/dev/usbharu/hideout/domain/model/UsersFollowers.kt new file mode 100644 index 00000000..1f5de8ce --- /dev/null +++ b/src/main/kotlin/dev/usbharu/hideout/domain/model/UsersFollowers.kt @@ -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) + } +} diff --git a/src/main/kotlin/dev/usbharu/hideout/repository/IUserRepository.kt b/src/main/kotlin/dev/usbharu/hideout/repository/IUserRepository.kt index 7fa43bb8..8da272fc 100644 --- a/src/main/kotlin/dev/usbharu/hideout/repository/IUserRepository.kt +++ b/src/main/kotlin/dev/usbharu/hideout/repository/IUserRepository.kt @@ -8,13 +8,17 @@ interface IUserRepository { suspend fun findById(id: Long): UserEntity? - suspend fun findByName(name:String): UserEntity? + suspend fun findByName(name: String): UserEntity? suspend fun update(userEntity: UserEntity) suspend fun delete(id: Long) - suspend fun findAll():List + suspend fun findAll(): List - suspend fun findAllByLimitAndByOffset(limit:Int,offset:Long = 0):List + suspend fun findAllByLimitAndByOffset(limit: Int, offset: Long = 0): List + + suspend fun createFollower(id: Long, follower: Long) + suspend fun deleteFollower(id: Long, follower: Long) + suspend fun findFollowersById(id: Long): List } diff --git a/src/main/kotlin/dev/usbharu/hideout/repository/UserAuthRepository.kt b/src/main/kotlin/dev/usbharu/hideout/repository/UserAuthRepository.kt index 5cc91db4..0bb5543c 100644 --- a/src/main/kotlin/dev/usbharu/hideout/repository/UserAuthRepository.kt +++ b/src/main/kotlin/dev/usbharu/hideout/repository/UserAuthRepository.kt @@ -13,6 +13,7 @@ class UserAuthRepository(private val database: Database) : IUserAuthRepository { init { transaction(database) { SchemaUtils.create(UsersAuthentication) + SchemaUtils.createMissingTablesAndColumns(UsersAuthentication) } } diff --git a/src/main/kotlin/dev/usbharu/hideout/repository/UserRepository.kt b/src/main/kotlin/dev/usbharu/hideout/repository/UserRepository.kt index 5da1280f..030fd2c8 100644 --- a/src/main/kotlin/dev/usbharu/hideout/repository/UserRepository.kt +++ b/src/main/kotlin/dev/usbharu/hideout/repository/UserRepository.kt @@ -3,6 +3,7 @@ package dev.usbharu.hideout.repository import dev.usbharu.hideout.domain.model.User import dev.usbharu.hideout.domain.model.UserEntity import dev.usbharu.hideout.domain.model.Users +import dev.usbharu.hideout.domain.model.UsersFollowers import kotlinx.coroutines.Dispatchers import org.jetbrains.exposed.sql.* import org.jetbrains.exposed.sql.SqlExpressionBuilder.eq @@ -13,12 +14,16 @@ class UserRepository(private val database: Database) : IUserRepository { init { transaction(database) { SchemaUtils.create(Users) + SchemaUtils.create(UsersFollowers) + SchemaUtils.createMissingTablesAndColumns(Users) + SchemaUtils.createMissingTablesAndColumns(UsersFollowers) } } private fun ResultRow.toUser(): User { return User( this[Users.name], + this[Users.domain], this[Users.screenName], this[Users.description] ) @@ -28,6 +33,7 @@ class UserRepository(private val database: Database) : IUserRepository { return UserEntity( this[Users.id].value, this[Users.name], + this[Users.domain], this[Users.screenName], this[Users.description] ) @@ -40,12 +46,22 @@ class UserRepository(private val database: Database) : IUserRepository { return query { UserEntity(Users.insert { it[name] = user.name + it[domain] = user.domain it[screenName] = user.screenName it[description] = user.description }[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? { return query { 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 { + 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) { return query { Users.update({ Users.id eq userEntity.id }) { it[name] = userEntity.name + it[domain] = userEntity.domain it[screenName] = userEntity.screenName 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 { return query { Users.selectAll().map { it.toUser() } diff --git a/src/main/kotlin/dev/usbharu/hideout/routing/UserRouting.kt b/src/main/kotlin/dev/usbharu/hideout/routing/UserRouting.kt index 28bc70bd..32752d44 100644 --- a/src/main/kotlin/dev/usbharu/hideout/routing/UserRouting.kt +++ b/src/main/kotlin/dev/usbharu/hideout/routing/UserRouting.kt @@ -50,7 +50,8 @@ fun Application.user(userService: UserService, activityPubUserService: ActivityP val userModel = activityPubUserService.generateUserModel(name!!) return@get call.respondAp(userModel) } - + name?.let { it1 -> userService.findByName(it1).id } + ?.let { it2 -> println(userService.findFollowersById(it2)) } val principal = call.principal() if (principal != null && name != null) { // iUserService.findByName(name) diff --git a/src/main/kotlin/dev/usbharu/hideout/routing/WellKnownRouting.kt b/src/main/kotlin/dev/usbharu/hideout/routing/WellKnownRouting.kt index aa8a10eb..41fc6fd1 100644 --- a/src/main/kotlin/dev/usbharu/hideout/routing/WellKnownRouting.kt +++ b/src/main/kotlin/dev/usbharu/hideout/routing/WellKnownRouting.kt @@ -16,7 +16,7 @@ fun Application.wellKnown(userService: UserService) { get("/host-meta") { //language=XML val xml = """ - + """.trimIndent() return@get call.respondText( contentType = ContentType("application", "xrd+xml"), @@ -32,7 +32,7 @@ fun Application.wellKnown(userService: UserService) { { "rel": "lrdd", "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( rel = "self", type = ContentType.Application.Activity.toString(), - href = "${Config.configData.hostname}/users/${userEntity.name}" + href = "${Config.configData.url}/users/${userEntity.name}" ) ) ) diff --git a/src/main/kotlin/dev/usbharu/hideout/routing/userActivityPubRouting.kt b/src/main/kotlin/dev/usbharu/hideout/routing/userActivityPubRouting.kt index 41f85d63..73188727 100644 --- a/src/main/kotlin/dev/usbharu/hideout/routing/userActivityPubRouting.kt +++ b/src/main/kotlin/dev/usbharu/hideout/routing/userActivityPubRouting.kt @@ -1,30 +1,38 @@ package dev.usbharu.hideout.routing +import dev.usbharu.hideout.service.ActivityPubService +import dev.usbharu.hideout.util.HttpUtil import io.ktor.http.* import io.ktor.server.application.* +import io.ktor.server.request.* import io.ktor.server.response.* import io.ktor.server.routing.* -fun Application.userActivityPubRouting() { +fun Application.userActivityPubRouting(activityPubService: ActivityPubService) { routing { route("/users/{name}") { route("/inbox") { get { - call.respond(HttpStatusCode.OK) + call.respond(HttpStatusCode.MethodNotAllowed) } 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") { get { - call.respond(HttpStatusCode.OK) + call.respond(HttpStatusCode.MethodNotAllowed) } post { - call.respond(HttpStatusCode.OK) + call.respond(HttpStatusCode.MethodNotAllowed) } } } diff --git a/src/main/kotlin/dev/usbharu/hideout/service/ActivityPubService.kt b/src/main/kotlin/dev/usbharu/hideout/service/ActivityPubService.kt index d43a3742..565051b6 100644 --- a/src/main/kotlin/dev/usbharu/hideout/service/ActivityPubService.kt +++ b/src/main/kotlin/dev/usbharu/hideout/service/ActivityPubService.kt @@ -1,5 +1,20 @@ 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() + } + } } diff --git a/src/main/kotlin/dev/usbharu/hideout/service/ActivityPubUserService.kt b/src/main/kotlin/dev/usbharu/hideout/service/ActivityPubUserService.kt index 4189d2c9..3389514f 100644 --- a/src/main/kotlin/dev/usbharu/hideout/service/ActivityPubUserService.kt +++ b/src/main/kotlin/dev/usbharu/hideout/service/ActivityPubUserService.kt @@ -4,15 +4,21 @@ 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 io.ktor.client.* +import io.ktor.client.call.* +import io.ktor.client.plugins.* +import io.ktor.client.request.* +import io.ktor.http.* class ActivityPubUserService( + private val httpClient:HttpClient, private val userService: UserService, private val userAuthService: IUserAuthService ) { suspend fun generateUserModel(name: String): Person { val userEntity = userService.findByName(name) val userAuthEntity = userAuthService.findByUserId(userEntity.id) - val userUrl = "${Config.configData.hostname}/users/$name" + val userUrl = "${Config.configData.url}/users/$name" return Person( type = emptyList(), name = userEntity.name, @@ -37,4 +43,19 @@ class ActivityPubUserService( ) ) } + + suspend fun fetchUserModel(url:String):Person? { + return try { + httpClient.get(url).body() + } catch (e: ResponseException) { + if (e.response.status == HttpStatusCode.NotFound) { + return null + } + throw e + } + } + + suspend fun receiveFollow(){ + + } } diff --git a/src/main/kotlin/dev/usbharu/hideout/service/HttpSignService.kt b/src/main/kotlin/dev/usbharu/hideout/service/HttpSignService.kt new file mode 100644 index 00000000..aacd9b6a --- /dev/null +++ b/src/main/kotlin/dev/usbharu/hideout/service/HttpSignService.kt @@ -0,0 +1,7 @@ +package dev.usbharu.hideout.service + +class HttpSignService { + suspend fun sign(){ + + } +} diff --git a/src/main/kotlin/dev/usbharu/hideout/service/IWebFingerService.kt b/src/main/kotlin/dev/usbharu/hideout/service/IWebFingerService.kt new file mode 100644 index 00000000..d13f33c1 --- /dev/null +++ b/src/main/kotlin/dev/usbharu/hideout/service/IWebFingerService.kt @@ -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) + } +} diff --git a/src/main/kotlin/dev/usbharu/hideout/service/UserAuthService.kt b/src/main/kotlin/dev/usbharu/hideout/service/UserAuthService.kt index 2b65c70b..830e6112 100644 --- a/src/main/kotlin/dev/usbharu/hideout/service/UserAuthService.kt +++ b/src/main/kotlin/dev/usbharu/hideout/service/UserAuthService.kt @@ -1,8 +1,10 @@ package dev.usbharu.hideout.service +import dev.usbharu.hideout.config.Config import dev.usbharu.hideout.domain.model.User import dev.usbharu.hideout.domain.model.UserAuthentication 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.repository.IUserAuthRepository import dev.usbharu.hideout.repository.IUserRepository @@ -10,7 +12,6 @@ import io.ktor.util.* import java.security.KeyPair import java.security.KeyPairGenerator import java.security.MessageDigest -import java.security.interfaces.RSAPrivateCrtKey import java.security.interfaces.RSAPrivateKey import java.security.interfaces.RSAPublicKey import java.util.* @@ -34,6 +35,7 @@ class UserAuthService( override suspend fun registerAccount(username: String, hash: String) { val registerUser = User( name = username, + domain = Config.configData.domain, screenName = username, description = "" ) diff --git a/src/main/kotlin/dev/usbharu/hideout/service/UserService.kt b/src/main/kotlin/dev/usbharu/hideout/service/UserService.kt index a74f102a..3183b212 100644 --- a/src/main/kotlin/dev/usbharu/hideout/service/UserService.kt +++ b/src/main/kotlin/dev/usbharu/hideout/service/UserService.kt @@ -4,8 +4,6 @@ import dev.usbharu.hideout.domain.model.User import dev.usbharu.hideout.domain.model.UserEntity import dev.usbharu.hideout.exception.UserNotFoundException import dev.usbharu.hideout.repository.IUserRepository -import dev.usbharu.hideout.repository.UserRepository -import org.jetbrains.exposed.sql.Database import java.lang.Integer.min class UserService(private val userRepository: IUserRepository) { @@ -23,11 +21,21 @@ class UserService(private val userRepository: IUserRepository) { return userRepository.findById(id) ?: throw UserNotFoundException("$id was not found.") } - suspend fun findByName(name:String): UserEntity { - return userRepository.findByName(name) ?: throw UserNotFoundException("$name was not found.") + suspend fun findByName(name: String): UserEntity { + return userRepository.findByName(name) + ?: throw UserNotFoundException("$name was not found.") } suspend fun create(user: User): UserEntity { return userRepository.create(user) } + + suspend fun findFollowersById(id: Long): List { + return userRepository.findFollowersById(id) + } + + suspend fun addFollowers(id: Long, follower: Long) { + return userRepository.createFollower(id, follower) + } + } diff --git a/src/main/kotlin/dev/usbharu/hideout/service/WebFingerService.kt b/src/main/kotlin/dev/usbharu/hideout/service/WebFingerService.kt new file mode 100644 index 00000000..7e3fe711 --- /dev/null +++ b/src/main/kotlin/dev/usbharu/hideout/service/WebFingerService.kt @@ -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() + } 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) + } +} diff --git a/src/main/kotlin/dev/usbharu/hideout/util/HttpUtil.kt b/src/main/kotlin/dev/usbharu/hideout/util/HttpUtil.kt index a014b824..05c827d3 100644 --- a/src/main/kotlin/dev/usbharu/hideout/util/HttpUtil.kt +++ b/src/main/kotlin/dev/usbharu/hideout/util/HttpUtil.kt @@ -15,14 +15,18 @@ object HttpUtil { if (subType == "activity+json") { return true } - if (subType == "ld+json") { - return true + return subType == "ld+json" + } - } - return false + fun isContentTypeOfActivityPub(contentType: ContentType): Boolean { + return isContentTypeOfActivityPub( + contentType.contentType, + contentType.contentSubtype, + contentType.parameter("profile").orEmpty() + ) } val ContentType.Application.Activity: ContentType - get() = ContentType("application","activity+json") + get() = ContentType("application", "activity+json") // fun } diff --git a/src/main/kotlin/dev/usbharu/hideout/webfinger/WebFinger.kt b/src/main/kotlin/dev/usbharu/hideout/webfinger/WebFinger.kt new file mode 100644 index 00000000..22fa25e5 --- /dev/null +++ b/src/main/kotlin/dev/usbharu/hideout/webfinger/WebFinger.kt @@ -0,0 +1,5 @@ +package dev.usbharu.hideout.webfinger + +class WebFinger(val subject: String, val aliases: List, val links: List) { + class Link(val rel: String, val type: String?, val href: String?, val template: String) +}