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

This commit is contained in:
usbharu 2023-04-08 16:00:54 +09:00
parent 9644c284d7
commit fd2060a5d6
Signed by: usbharu
GPG Key ID: 6556747BF94EEBC8
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.IUserAuthService
import dev.usbharu.hideout.service.activitypub.ActivityPubService import dev.usbharu.hideout.service.activitypub.ActivityPubService
import dev.usbharu.hideout.service.activitypub.ActivityPubServiceImpl 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.UserAuthService
import dev.usbharu.hideout.service.impl.UserService import dev.usbharu.hideout.service.impl.UserService
import dev.usbharu.hideout.service.signature.HttpSignatureVerifyService import dev.usbharu.hideout.service.signature.HttpSignatureVerifyService
@ -42,6 +44,7 @@ fun Application.module() {
single<HttpSignatureVerifyService> { HttpSignatureVerifyServiceImpl(get()) } single<HttpSignatureVerifyService> { HttpSignatureVerifyServiceImpl(get()) }
single<ActivityPubService> { ActivityPubServiceImpl() } single<ActivityPubService> { ActivityPubServiceImpl() }
single<UserService> { UserService(get()) } single<UserService> { UserService(get()) }
single<ActivityPubUserService> { ActivityPubUserServiceImpl(get(),get()) }
} }
configureKoin(module) configureKoin(module)
@ -52,6 +55,7 @@ fun Application.module() {
configureRouting( configureRouting(
inject<HttpSignatureVerifyService>().value, inject<HttpSignatureVerifyService>().value,
inject<ActivityPubService>().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 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() 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>() { public class ContextDeserializer : JsonDeserializer<String>() {

View File

@ -17,5 +17,23 @@ open class Key : Object{
this.publicKeyPem = publicKeyPem 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() 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>>() { public class TypeSerializer : JsonSerializer<List<String>>() {

View File

@ -32,4 +32,31 @@ open class Person : Object {
this.publicKey = publicKey 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.activitypub.usersAP
import dev.usbharu.hideout.routing.wellknown.webfinger import dev.usbharu.hideout.routing.wellknown.webfinger
import dev.usbharu.hideout.service.activitypub.ActivityPubService 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.impl.UserService
import dev.usbharu.hideout.service.signature.HttpSignatureVerifyService import dev.usbharu.hideout.service.signature.HttpSignatureVerifyService
import io.ktor.server.application.* import io.ktor.server.application.*
@ -14,13 +15,14 @@ import io.ktor.server.routing.*
fun Application.configureRouting( fun Application.configureRouting(
httpSignatureVerifyService: HttpSignatureVerifyService, httpSignatureVerifyService: HttpSignatureVerifyService,
activityPubService: ActivityPubService, activityPubService: ActivityPubService,
userService:UserService userService:UserService,
activityPubUserService: ActivityPubUserService
) { ) {
install(AutoHeadResponse) install(AutoHeadResponse)
routing { routing {
inbox(httpSignatureVerifyService, activityPubService) inbox(httpSignatureVerifyService, activityPubService)
outbox() outbox()
usersAP(activityPubService) usersAP(activityPubUserService)
webfinger(userService) webfinger(userService)
} }
} }

View File

@ -1,5 +1,11 @@
package dev.usbharu.hideout.plugins 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.serialization.jackson.*
import io.ktor.server.application.* import io.ktor.server.application.*
import io.ktor.server.plugins.contentnegotiation.* import io.ktor.server.plugins.contentnegotiation.*
@ -8,6 +14,11 @@ import io.ktor.server.routing.*
fun Application.configureSerialization() { fun Application.configureSerialization() {
install(ContentNegotiation) { 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 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.ActivityPubService
import dev.usbharu.hideout.service.activitypub.ActivityPubUserService
import dev.usbharu.hideout.util.HttpUtil.Activity import dev.usbharu.hideout.util.HttpUtil.Activity
import io.ktor.http.* import io.ktor.http.*
import io.ktor.server.application.* import io.ktor.server.application.*
@ -8,11 +11,13 @@ 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 Routing.usersAP(activityPubService: ActivityPubService){ fun Routing.usersAP(activityPubUserService:ActivityPubUserService){
route("/users/{name}"){ route("/users/{name}"){
createChild(ContentTypeRouteSelector(ContentType.Application.Activity)).handle { createChild(ContentTypeRouteSelector(ContentType.Application.Activity)).handle {
val name = call.parameters["name"] ?: throw ParameterNotExistException("Parameter(name='name') does not exist.")
call.respond(HttpStatusCode.NotImplemented) 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 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.ActivityPubResponse
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.plugins.configureRouting import dev.usbharu.hideout.plugins.configureRouting
import dev.usbharu.hideout.plugins.configureSerialization
import dev.usbharu.hideout.repository.IUserRepository import dev.usbharu.hideout.repository.IUserRepository
import dev.usbharu.hideout.service.activitypub.ActivityPubService 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.activitypub.ActivityType
import dev.usbharu.hideout.service.impl.UserService import dev.usbharu.hideout.service.impl.UserService
import dev.usbharu.hideout.service.signature.HttpSignatureVerifyService import dev.usbharu.hideout.service.signature.HttpSignatureVerifyService
import dev.usbharu.hideout.util.HttpUtil.Activity import dev.usbharu.hideout.util.HttpUtil.Activity
import io.ktor.client.call.*
import io.ktor.client.request.* import io.ktor.client.request.*
import io.ktor.client.statement.*
import io.ktor.http.* import io.ktor.http.*
import io.ktor.server.config.* import io.ktor.server.config.*
import io.ktor.server.testing.* import io.ktor.server.testing.*
@ -25,7 +39,32 @@ class UsersAPTest {
environment { environment {
config = ApplicationConfig("empty.conf") 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 { application {
configureSerialization()
configureRouting(object : HttpSignatureVerifyService { configureRouting(object : HttpSignatureVerifyService {
override fun verify(headers: Headers): Boolean { override fun verify(headers: Headers): Boolean {
return true return true
@ -38,7 +77,7 @@ class UsersAPTest {
override fun processActivity(json: String, type: ActivityType): ActivityPubResponse? { override fun processActivity(json: String, type: ActivityType): ActivityPubResponse? {
TODO("Not yet implemented") TODO("Not yet implemented")
} }
},UserService(object : IUserRepository { }, UserService(object : IUserRepository {
override suspend fun create(user: User): UserEntity { override suspend fun create(user: User): UserEntity {
TODO("Not yet implemented") TODO("Not yet implemented")
} }
@ -78,12 +117,26 @@ class UsersAPTest {
override suspend fun findFollowersById(id: Long): List<UserEntity> { override suspend fun findFollowersById(id: Long): List<UserEntity> {
TODO("Not yet implemented") 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) accept(ContentType.Application.Activity)
}.let { }.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)
} }
} }
} }