feat: ユーザーページをacceptヘッダーがactivityの状態でアクセスするとpersonを返すように

This commit is contained in:
usbharu 2023-04-08 16:00:54 +09:00
parent 2a1824edbf
commit e77c28c6c2
12 changed files with 224 additions and 11 deletions

View File

@ -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<HttpSignatureVerifyService> { HttpSignatureVerifyServiceImpl(get()) }
single<ActivityPubService> { ActivityPubServiceImpl() }
single<UserService> { UserService(get()) }
single<ActivityPubUserService> { ActivityPubUserServiceImpl(get(),get()) }
}
configureKoin(module)
@ -52,6 +55,7 @@ fun Application.module() {
configureRouting(
inject<HttpSignatureVerifyService>().value,
inject<ActivityPubService>().value,
inject<UserService>().value
inject<UserService>().value,
inject<ActivityPubUserService>().value
)
}

View File

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

View File

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

View File

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

View File

@ -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<List<String>>() {

View File

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

View File

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

View File

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

View File

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

View File

@ -0,0 +1,7 @@
package dev.usbharu.hideout.service.activitypub
import dev.usbharu.hideout.ap.Person
interface ActivityPubUserService {
suspend fun getPersonByName(name:String):Person
}

View File

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

View File

@ -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<UserEntity> {
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<Person>(actual)
println(objectMapper.writeValueAsString(person))
println(objectMapper.writeValueAsString(readValue))
assertEquals(person, readValue)
}
}
}