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("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 {

View File

@ -36,8 +36,9 @@ fun Application.module() {
}
single<ConfigData> {
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<IUserAuthRepository> { UserAuthRepository(get()) }
single<IUserAuthService> { UserAuthService(get(), get()) }
single<UserService> { UserService(get()) }
single<ActivityPubUserService> { ActivityPubUserService(get(),get()) }
single<ActivityPubUserService> { ActivityPubUserService(get(), get(),get()) }
}
configureKoin(module)
val configData by inject<ConfigData>()

View File

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

View File

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

View File

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

View File

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

View File

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

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

@ -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<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 {
transaction(database) {
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.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<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) {
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<User> {
return query {
Users.selectAll().map { it.toUser() }

View File

@ -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<UserIdPrincipal>()
if (principal != null && name != null) {
// iUserService.findByName(name)

View File

@ -16,7 +16,7 @@ fun Application.wellKnown(userService: UserService) {
get("/host-meta") {
//language=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()
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}"
)
)
)

View File

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

View File

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

View File

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

View File

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

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