From fd2060a5d6e932d074ce178e291391e3fc3358bf Mon Sep 17 00:00:00 2001 From: usbharu <64310155+usbharu@users.noreply.github.com> Date: Sat, 8 Apr 2023 16:00:54 +0900 Subject: [PATCH] =?UTF-8?q?feat:=20=E3=83=A6=E3=83=BC=E3=82=B6=E3=83=BC?= =?UTF-8?q?=E3=83=9A=E3=83=BC=E3=82=B8=E3=82=92accept=E3=83=98=E3=83=83?= =?UTF-8?q?=E3=83=80=E3=83=BC=E3=81=8Cactivity=E3=81=AE=E7=8A=B6=E6=85=8B?= =?UTF-8?q?=E3=81=A7=E3=82=A2=E3=82=AF=E3=82=BB=E3=82=B9=E3=81=99=E3=82=8B?= =?UTF-8?q?=E3=81=A8person=E3=82=92=E8=BF=94=E3=81=99=E3=82=88=E3=81=86?= =?UTF-8?q?=E3=81=AB?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../kotlin/dev/usbharu/hideout/Application.kt | 6 +- .../kotlin/dev/usbharu/hideout/ap/Image.kt | 17 ++++++ .../kotlin/dev/usbharu/hideout/ap/JsonLd.kt | 13 ++++ src/main/kotlin/dev/usbharu/hideout/ap/Key.kt | 18 ++++++ .../kotlin/dev/usbharu/hideout/ap/Object.kt | 16 +++++ .../kotlin/dev/usbharu/hideout/ap/Person.kt | 27 ++++++++ .../dev/usbharu/hideout/plugins/Routing.kt | 6 +- .../usbharu/hideout/plugins/Serialization.kt | 13 +++- .../routing/activitypub/UserRouting.kt | 11 +++- .../activitypub/ActivityPubUserService.kt | 7 +++ .../activitypub/ActivityPubUserServiceImpl.kt | 40 ++++++++++++ .../routing/activitypub/UsersAPTest.kt | 61 +++++++++++++++++-- 12 files changed, 224 insertions(+), 11 deletions(-) create mode 100644 src/main/kotlin/dev/usbharu/hideout/service/activitypub/ActivityPubUserService.kt create mode 100644 src/main/kotlin/dev/usbharu/hideout/service/activitypub/ActivityPubUserServiceImpl.kt diff --git a/src/main/kotlin/dev/usbharu/hideout/Application.kt b/src/main/kotlin/dev/usbharu/hideout/Application.kt index 3770dd5d..02938ee8 100644 --- a/src/main/kotlin/dev/usbharu/hideout/Application.kt +++ b/src/main/kotlin/dev/usbharu/hideout/Application.kt @@ -8,6 +8,8 @@ import dev.usbharu.hideout.repository.UserRepository import dev.usbharu.hideout.service.IUserAuthService import dev.usbharu.hideout.service.activitypub.ActivityPubService import dev.usbharu.hideout.service.activitypub.ActivityPubServiceImpl +import dev.usbharu.hideout.service.activitypub.ActivityPubUserService +import dev.usbharu.hideout.service.activitypub.ActivityPubUserServiceImpl import dev.usbharu.hideout.service.impl.UserAuthService import dev.usbharu.hideout.service.impl.UserService import dev.usbharu.hideout.service.signature.HttpSignatureVerifyService @@ -42,6 +44,7 @@ fun Application.module() { single { HttpSignatureVerifyServiceImpl(get()) } single { ActivityPubServiceImpl() } single { UserService(get()) } + single { ActivityPubUserServiceImpl(get(),get()) } } configureKoin(module) @@ -52,6 +55,7 @@ fun Application.module() { configureRouting( inject().value, inject().value, - inject().value + inject().value, + inject().value ) } diff --git a/src/main/kotlin/dev/usbharu/hideout/ap/Image.kt b/src/main/kotlin/dev/usbharu/hideout/ap/Image.kt index 38d3cbbc..29639e20 100644 --- a/src/main/kotlin/dev/usbharu/hideout/ap/Image.kt +++ b/src/main/kotlin/dev/usbharu/hideout/ap/Image.kt @@ -13,4 +13,21 @@ open class Image : Object { this.url = url } + override fun equals(other: Any?): Boolean { + if (this === other) return true + if (other !is Image) return false + if (!super.equals(other)) return false + + if (mediaType != other.mediaType) return false + return url == other.url + } + + override fun hashCode(): Int { + var result = super.hashCode() + result = 31 * result + (mediaType?.hashCode() ?: 0) + result = 31 * result + (url?.hashCode() ?: 0) + return result + } + + } diff --git a/src/main/kotlin/dev/usbharu/hideout/ap/JsonLd.kt b/src/main/kotlin/dev/usbharu/hideout/ap/JsonLd.kt index 51985b34..bef45e70 100644 --- a/src/main/kotlin/dev/usbharu/hideout/ap/JsonLd.kt +++ b/src/main/kotlin/dev/usbharu/hideout/ap/JsonLd.kt @@ -25,6 +25,19 @@ open class JsonLd { } protected constructor() + + override fun equals(other: Any?): Boolean { + if (this === other) return true + if (other !is JsonLd) return false + + return context == other.context + } + + override fun hashCode(): Int { + return context.hashCode() + } + + } public class ContextDeserializer : JsonDeserializer() { diff --git a/src/main/kotlin/dev/usbharu/hideout/ap/Key.kt b/src/main/kotlin/dev/usbharu/hideout/ap/Key.kt index 0c7470da..ab3ae13b 100644 --- a/src/main/kotlin/dev/usbharu/hideout/ap/Key.kt +++ b/src/main/kotlin/dev/usbharu/hideout/ap/Key.kt @@ -17,5 +17,23 @@ open class Key : Object{ this.publicKeyPem = publicKeyPem } + override fun equals(other: Any?): Boolean { + if (this === other) return true + if (other !is Key) return false + if (!super.equals(other)) return false + + if (id != other.id) return false + if (owner != other.owner) return false + return publicKeyPem == other.publicKeyPem + } + + override fun hashCode(): Int { + var result = super.hashCode() + result = 31 * result + (id?.hashCode() ?: 0) + result = 31 * result + (owner?.hashCode() ?: 0) + result = 31 * result + (publicKeyPem?.hashCode() ?: 0) + return result + } + } diff --git a/src/main/kotlin/dev/usbharu/hideout/ap/Object.kt b/src/main/kotlin/dev/usbharu/hideout/ap/Object.kt index 76d609e5..1a05564d 100644 --- a/src/main/kotlin/dev/usbharu/hideout/ap/Object.kt +++ b/src/main/kotlin/dev/usbharu/hideout/ap/Object.kt @@ -24,6 +24,22 @@ open class Object : JsonLd { return toMutableList.distinct() } } + + override fun equals(other: Any?): Boolean { + if (this === other) return true + if (other !is Object) return false + + if (type != other.type) return false + return name == other.name + } + + override fun hashCode(): Int { + var result = type.hashCode() + result = 31 * result + (name?.hashCode() ?: 0) + return result + } + + } public class TypeSerializer : JsonSerializer>() { diff --git a/src/main/kotlin/dev/usbharu/hideout/ap/Person.kt b/src/main/kotlin/dev/usbharu/hideout/ap/Person.kt index 2a1f29f7..270eba23 100644 --- a/src/main/kotlin/dev/usbharu/hideout/ap/Person.kt +++ b/src/main/kotlin/dev/usbharu/hideout/ap/Person.kt @@ -32,4 +32,31 @@ open class Person : Object { this.publicKey = publicKey } + override fun equals(other: Any?): Boolean { + if (this === other) return true + if (other !is Person) return false + + if (id != other.id) return false + if (preferredUsername != other.preferredUsername) return false + if (summary != other.summary) return false + if (inbox != other.inbox) return false + if (outbox != other.outbox) return false + if (url != other.url) return false + if (icon != other.icon) return false + return publicKey == other.publicKey + } + + override fun hashCode(): Int { + var result = id?.hashCode() ?: 0 + result = 31 * result + (preferredUsername?.hashCode() ?: 0) + result = 31 * result + (summary?.hashCode() ?: 0) + result = 31 * result + (inbox?.hashCode() ?: 0) + result = 31 * result + (outbox?.hashCode() ?: 0) + result = 31 * result + (url?.hashCode() ?: 0) + result = 31 * result + (icon?.hashCode() ?: 0) + result = 31 * result + (publicKey?.hashCode() ?: 0) + return result + } + + } diff --git a/src/main/kotlin/dev/usbharu/hideout/plugins/Routing.kt b/src/main/kotlin/dev/usbharu/hideout/plugins/Routing.kt index bddeb896..6b44c0fd 100644 --- a/src/main/kotlin/dev/usbharu/hideout/plugins/Routing.kt +++ b/src/main/kotlin/dev/usbharu/hideout/plugins/Routing.kt @@ -5,6 +5,7 @@ import dev.usbharu.hideout.routing.activitypub.outbox import dev.usbharu.hideout.routing.activitypub.usersAP import dev.usbharu.hideout.routing.wellknown.webfinger import dev.usbharu.hideout.service.activitypub.ActivityPubService +import dev.usbharu.hideout.service.activitypub.ActivityPubUserService import dev.usbharu.hideout.service.impl.UserService import dev.usbharu.hideout.service.signature.HttpSignatureVerifyService import io.ktor.server.application.* @@ -14,13 +15,14 @@ import io.ktor.server.routing.* fun Application.configureRouting( httpSignatureVerifyService: HttpSignatureVerifyService, activityPubService: ActivityPubService, - userService:UserService + userService:UserService, + activityPubUserService: ActivityPubUserService ) { install(AutoHeadResponse) routing { inbox(httpSignatureVerifyService, activityPubService) outbox() - usersAP(activityPubService) + usersAP(activityPubUserService) webfinger(userService) } } diff --git a/src/main/kotlin/dev/usbharu/hideout/plugins/Serialization.kt b/src/main/kotlin/dev/usbharu/hideout/plugins/Serialization.kt index c07bc345..c8b71a7d 100644 --- a/src/main/kotlin/dev/usbharu/hideout/plugins/Serialization.kt +++ b/src/main/kotlin/dev/usbharu/hideout/plugins/Serialization.kt @@ -1,5 +1,11 @@ package dev.usbharu.hideout.plugins +import com.fasterxml.jackson.annotation.JsonInclude +import com.fasterxml.jackson.annotation.JsonSetter +import com.fasterxml.jackson.annotation.Nulls +import com.fasterxml.jackson.databind.DeserializationFeature +import dev.usbharu.hideout.util.HttpUtil.Activity +import io.ktor.http.* import io.ktor.serialization.jackson.* import io.ktor.server.application.* import io.ktor.server.plugins.contentnegotiation.* @@ -8,6 +14,11 @@ import io.ktor.server.routing.* fun Application.configureSerialization() { install(ContentNegotiation) { - jackson() + jackson { + enable(DeserializationFeature.ACCEPT_SINGLE_VALUE_AS_ARRAY) + setSerializationInclusion(JsonInclude.Include.NON_EMPTY) + configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false) + configOverride(List::class.java).setSetterInfo(JsonSetter.Value.forContentNulls(Nulls.AS_EMPTY)) + } } } diff --git a/src/main/kotlin/dev/usbharu/hideout/routing/activitypub/UserRouting.kt b/src/main/kotlin/dev/usbharu/hideout/routing/activitypub/UserRouting.kt index 8848840a..05ab3f2c 100644 --- a/src/main/kotlin/dev/usbharu/hideout/routing/activitypub/UserRouting.kt +++ b/src/main/kotlin/dev/usbharu/hideout/routing/activitypub/UserRouting.kt @@ -1,6 +1,9 @@ package dev.usbharu.hideout.routing.activitypub +import dev.usbharu.hideout.config.Config +import dev.usbharu.hideout.exception.ParameterNotExistException import dev.usbharu.hideout.service.activitypub.ActivityPubService +import dev.usbharu.hideout.service.activitypub.ActivityPubUserService import dev.usbharu.hideout.util.HttpUtil.Activity import io.ktor.http.* import io.ktor.server.application.* @@ -8,11 +11,13 @@ import io.ktor.server.request.* import io.ktor.server.response.* import io.ktor.server.routing.* -fun Routing.usersAP(activityPubService: ActivityPubService){ +fun Routing.usersAP(activityPubUserService:ActivityPubUserService){ route("/users/{name}"){ createChild(ContentTypeRouteSelector(ContentType.Application.Activity)).handle { - - call.respond(HttpStatusCode.NotImplemented) + val name = call.parameters["name"] ?: throw ParameterNotExistException("Parameter(name='name') does not exist.") + val person = activityPubUserService.getPersonByName(name) + call.response.header("Content-Type", ContentType.Application.Activity.toString()) + call.respond(HttpStatusCode.OK,Config.configData.objectMapper.writeValueAsString(person)) } } } diff --git a/src/main/kotlin/dev/usbharu/hideout/service/activitypub/ActivityPubUserService.kt b/src/main/kotlin/dev/usbharu/hideout/service/activitypub/ActivityPubUserService.kt new file mode 100644 index 00000000..55033e8c --- /dev/null +++ b/src/main/kotlin/dev/usbharu/hideout/service/activitypub/ActivityPubUserService.kt @@ -0,0 +1,7 @@ +package dev.usbharu.hideout.service.activitypub + +import dev.usbharu.hideout.ap.Person + +interface ActivityPubUserService { + suspend fun getPersonByName(name: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 new file mode 100644 index 00000000..830359d7 --- /dev/null +++ b/src/main/kotlin/dev/usbharu/hideout/service/activitypub/ActivityPubUserServiceImpl.kt @@ -0,0 +1,40 @@ +package dev.usbharu.hideout.service.activitypub + +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.service.IUserAuthService +import dev.usbharu.hideout.service.impl.UserService + +class ActivityPubUserServiceImpl(private val userService: UserService, private val userAuthService: IUserAuthService) : + ActivityPubUserService { + override suspend fun getPersonByName(name: String): Person { + val userEntity = userService.findByName(name) + val userAuthEntity = userAuthService.findByUserId(userEntity.id) + val userUrl = "${Config.configData.url}/users$name" + return Person( + type = emptyList(), + name = userEntity.name, + id = userUrl, + preferredUsername = name, + summary = userEntity.description, + inbox = "$userUrl/inbox", + outbox = "$userUrl/outbox", + url = userUrl, + icon = Image( + type = emptyList(), + name = "$userUrl/icon.png", + mediaType = "image/png", + url = "$userUrl/icon.png" + ), + publicKey = Key( + type = emptyList(), + name = "Public Key", + id = "$userUrl#pubkey", + owner = userUrl, + publicKeyPem = userAuthEntity.publicKey + ) + ) + } +} diff --git a/src/test/kotlin/dev/usbharu/hideout/routing/activitypub/UsersAPTest.kt b/src/test/kotlin/dev/usbharu/hideout/routing/activitypub/UsersAPTest.kt index a0d0723e..226a5e51 100644 --- a/src/test/kotlin/dev/usbharu/hideout/routing/activitypub/UsersAPTest.kt +++ b/src/test/kotlin/dev/usbharu/hideout/routing/activitypub/UsersAPTest.kt @@ -1,16 +1,30 @@ package dev.usbharu.hideout.routing.activitypub +import com.fasterxml.jackson.annotation.JsonInclude +import com.fasterxml.jackson.annotation.JsonSetter +import com.fasterxml.jackson.annotation.Nulls +import com.fasterxml.jackson.databind.DeserializationFeature +import com.fasterxml.jackson.module.kotlin.jacksonObjectMapper +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.domain.model.ActivityPubResponse import dev.usbharu.hideout.domain.model.User import dev.usbharu.hideout.domain.model.UserEntity import dev.usbharu.hideout.plugins.configureRouting +import dev.usbharu.hideout.plugins.configureSerialization import dev.usbharu.hideout.repository.IUserRepository import dev.usbharu.hideout.service.activitypub.ActivityPubService +import dev.usbharu.hideout.service.activitypub.ActivityPubUserService import dev.usbharu.hideout.service.activitypub.ActivityType import dev.usbharu.hideout.service.impl.UserService import dev.usbharu.hideout.service.signature.HttpSignatureVerifyService import dev.usbharu.hideout.util.HttpUtil.Activity +import io.ktor.client.call.* import io.ktor.client.request.* +import io.ktor.client.statement.* import io.ktor.http.* import io.ktor.server.config.* import io.ktor.server.testing.* @@ -25,7 +39,32 @@ class UsersAPTest { environment { config = ApplicationConfig("empty.conf") } + val person = Person( + type = emptyList(), + name = "test", + id = "http://example.com/users/test", + preferredUsername = "test", + summary = "test user", + inbox = "http://example.com/users/test/inbox", + outbox = "http://example.com/users/test/outbox", + url = "http://example.com/users/test", + icon = Image( + type = emptyList(), + name = "http://example.com/users/test/icon.png", + mediaType = "image/png", + url = "http://example.com/users/test/icon.png" + ), + publicKey = Key( + type = emptyList(), + name = "Public Key", + id = "http://example.com/users/test#pubkey", + owner = "https://example.com/users/test", + publicKeyPem = "-----BEGIN PUBLIC KEY-----\n\n-----END PUBLIC KEY-----" + ) + ) + person.context = listOf("https://www.w3.org/ns/activitystreams") application { + configureSerialization() configureRouting(object : HttpSignatureVerifyService { override fun verify(headers: Headers): Boolean { return true @@ -38,7 +77,7 @@ class UsersAPTest { override fun processActivity(json: String, type: ActivityType): ActivityPubResponse? { TODO("Not yet implemented") } - },UserService(object : IUserRepository { + }, UserService(object : IUserRepository { override suspend fun create(user: User): UserEntity { TODO("Not yet implemented") } @@ -78,12 +117,26 @@ class UsersAPTest { override suspend fun findFollowersById(id: Long): List { TODO("Not yet implemented") } - })) + }), object : ActivityPubUserService { + override suspend fun getPersonByName(name: String): Person { + return person + } + + }) } - client.get("/users/test"){ + client.get("/users/test") { accept(ContentType.Application.Activity) }.let { - assertEquals(HttpStatusCode.NotImplemented, it.status) + val objectMapper = jacksonObjectMapper().enable(DeserializationFeature.ACCEPT_SINGLE_VALUE_AS_ARRAY) + .setSerializationInclusion(JsonInclude.Include.NON_EMPTY) + .configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false) + objectMapper.configOverride(List::class.java).setSetterInfo(JsonSetter.Value.forValueNulls( + Nulls.AS_EMPTY)) + val actual = it.bodyAsText() + val readValue = objectMapper.readValue(actual) + println(objectMapper.writeValueAsString(person)) + println(objectMapper.writeValueAsString(readValue)) + assertEquals(person, readValue) } } }