Merge pull request #10 from usbharu/feature/refactor-user

Feature/refactor user
This commit is contained in:
usbharu 2023-04-29 21:53:23 +09:00 committed by GitHub
commit 1a6edbdac6
55 changed files with 889 additions and 655 deletions

View File

@ -8,12 +8,17 @@ import dev.usbharu.hideout.config.ConfigData
import dev.usbharu.hideout.domain.model.job.DeliverPostJob import dev.usbharu.hideout.domain.model.job.DeliverPostJob
import dev.usbharu.hideout.domain.model.job.ReceiveFollowJob import dev.usbharu.hideout.domain.model.job.ReceiveFollowJob
import dev.usbharu.hideout.plugins.* import dev.usbharu.hideout.plugins.*
import dev.usbharu.hideout.repository.* import dev.usbharu.hideout.repository.IPostRepository
import dev.usbharu.hideout.repository.IUserRepository
import dev.usbharu.hideout.repository.PostRepositoryImpl
import dev.usbharu.hideout.repository.UserRepository
import dev.usbharu.hideout.routing.register import dev.usbharu.hideout.routing.register
import dev.usbharu.hideout.service.IPostService import dev.usbharu.hideout.service.IPostService
import dev.usbharu.hideout.service.IUserAuthService import dev.usbharu.hideout.service.IUserAuthService
import dev.usbharu.hideout.service.IdGenerateService
import dev.usbharu.hideout.service.TwitterSnowflakeIdGenerateService import dev.usbharu.hideout.service.TwitterSnowflakeIdGenerateService
import dev.usbharu.hideout.service.activitypub.* import dev.usbharu.hideout.service.activitypub.*
import dev.usbharu.hideout.service.impl.IUserService
import dev.usbharu.hideout.service.impl.PostService import dev.usbharu.hideout.service.impl.PostService
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
@ -57,9 +62,8 @@ fun Application.parent() {
) )
} }
single<IUserRepository> { UserRepository(get()) } single<IUserRepository> { UserRepository(get(),get()) }
single<IUserAuthRepository> { UserAuthRepository(get()) } single<IUserAuthService> { UserAuthService(get()) }
single<IUserAuthService> { UserAuthService(get(), get()) }
single<HttpSignatureVerifyService> { HttpSignatureVerifyServiceImpl(get()) } single<HttpSignatureVerifyService> { HttpSignatureVerifyServiceImpl(get()) }
single<JobQueueParentService> { single<JobQueueParentService> {
val kJobJobQueueService = KJobJobQueueParentService(get()) val kJobJobQueueService = KJobJobQueueParentService(get())
@ -79,11 +83,12 @@ fun Application.parent() {
} }
single<ActivityPubFollowService> { ActivityPubFollowServiceImpl(get(), get(), get(), get()) } single<ActivityPubFollowService> { ActivityPubFollowServiceImpl(get(), get(), get(), get()) }
single<ActivityPubService> { ActivityPubServiceImpl(get(), get()) } single<ActivityPubService> { ActivityPubServiceImpl(get(), get()) }
single<UserService> { UserService(get()) } single<IUserService> { UserService(get(),get()) }
single<ActivityPubUserService> { ActivityPubUserServiceImpl(get(), get(), get()) } single<ActivityPubUserService> { ActivityPubUserServiceImpl(get(), get()) }
single<ActivityPubNoteService> { ActivityPubNoteServiceImpl(get(), get(), get()) } single<ActivityPubNoteService> { ActivityPubNoteServiceImpl(get(), get(), get()) }
single<IPostService> { PostService(get(), get()) } single<IPostService> { PostService(get(), get()) }
single<IPostRepository> { PostRepositoryImpl(get(), TwitterSnowflakeIdGenerateService) } single<IPostRepository> { PostRepositoryImpl(get(), get()) }
single<IdGenerateService> {TwitterSnowflakeIdGenerateService}
} }
@ -92,11 +97,11 @@ fun Application.parent() {
configureStaticRouting() configureStaticRouting()
configureMonitoring() configureMonitoring()
configureSerialization() configureSerialization()
register(inject<IUserAuthService>().value) register(inject<IUserService>().value)
configureRouting( configureRouting(
inject<HttpSignatureVerifyService>().value, inject<HttpSignatureVerifyService>().value,
inject<ActivityPubService>().value, inject<ActivityPubService>().value,
inject<UserService>().value, inject<IUserService>().value,
inject<ActivityPubUserService>().value, inject<ActivityPubUserService>().value,
inject<IPostService>().value inject<IPostService>().value
) )

View File

@ -1,5 +1,6 @@
package dev.usbharu.hideout.domain.model package dev.usbharu.hideout.domain.model
import dev.usbharu.hideout.repository.Users
import org.jetbrains.exposed.sql.ResultRow import org.jetbrains.exposed.sql.ResultRow
import org.jetbrains.exposed.sql.Table import org.jetbrains.exposed.sql.Table
@ -28,7 +29,7 @@ data class Post(
data class PostEntity( data class PostEntity(
val id: Long, val id: Long,
val userId:Long, val userId: Long,
val overview: String? = null, val overview: String? = null,
val text: String, val text: String,
val createdAt: Long, val createdAt: Long,
@ -38,7 +39,7 @@ data class PostEntity(
val replyId: Long? = null val replyId: Long? = null
) )
fun ResultRow.toPost():PostEntity{ fun ResultRow.toPost(): PostEntity {
return PostEntity( return PostEntity(
id = this[Posts.id], id = this[Posts.id],
userId = this[Posts.userId], userId = this[Posts.userId],

View File

@ -1,63 +0,0 @@
package dev.usbharu.hideout.domain.model
import org.jetbrains.exposed.dao.id.LongIdTable
import org.jetbrains.exposed.sql.ResultRow
data class User(
val name: String,
val domain: String,
val screenName: String,
val description: String,
val inbox: String,
val outbox: String,
val url: String
)
data class UserEntity(
val id: Long,
val name: String,
val domain: String,
val screenName: String,
val description: String,
val inbox: String,
val outbox: String,
val url: String
) {
constructor(id: Long, user: User) : this(
id,
user.name,
user.domain,
user.screenName,
user.description,
user.inbox,
user.outbox,
user.url
)
}
object Users : LongIdTable("users") {
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)
val inbox = varchar("inbox", length = 255).uniqueIndex()
val outbox = varchar("outbox", length = 255).uniqueIndex()
val url = varchar("url", length = 255).uniqueIndex()
init {
uniqueIndex(name, domain)
}
}
fun ResultRow.toUser(): User {
return User(
this[Users.name],
this[Users.domain],
this[Users.screenName],
this[Users.description],
this[Users.inbox],
this[Users.outbox],
this[Users.url]
)
}

View File

@ -1,34 +0,0 @@
package dev.usbharu.hideout.domain.model
import org.jetbrains.exposed.dao.id.LongIdTable
import org.jetbrains.exposed.sql.ReferenceOption
data class UserAuthentication(
val userId: Long,
val hash: String?,
val publicKey: String,
val privateKey: String?
)
data class UserAuthenticationEntity(
val id: Long,
val userId: Long,
val hash: String?,
val publicKey: String,
val privateKey: String?
) {
constructor(id: Long, userAuthentication: UserAuthentication) : this(
id,
userAuthentication.userId,
userAuthentication.hash,
userAuthentication.publicKey,
userAuthentication.privateKey
)
}
object UsersAuthentication : LongIdTable("users_auth") {
val userId = long("user_id").references(Users.id, onUpdate = ReferenceOption.CASCADE)
val hash = varchar("hash", length = 64).nullable()
val publicKey = varchar("public_key", length = 1000_000)
val privateKey = varchar("private_key", length = 1000_000).nullable()
}

View File

@ -1,11 +0,0 @@
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

@ -1,17 +1,16 @@
package dev.usbharu.hideout.domain.model.ap package dev.usbharu.hideout.domain.model.ap
open class Accept : Object { open class Accept : Object {
public var `object`: Object? = null var `object`: Object? = null
public var actor:String? = null
protected constructor() : super() protected constructor() : super()
constructor( constructor(
type: List<String> = emptyList(), type: List<String> = emptyList(),
name: String, name: String,
`object`: Object?, `object`: Object?,
actor: String? actor: String?
) : super(add(type,"Accept"), name) { ) : super(add(type, "Accept"), name,actor) {
this.`object` = `object` this.`object` = `object`
this.actor = actor
} }
override fun equals(other: Any?): Boolean { override fun equals(other: Any?): Boolean {

View File

@ -1,10 +1,21 @@
package dev.usbharu.hideout.domain.model.ap package dev.usbharu.hideout.domain.model.ap
open class Create : Object { open class Create : Object {
var `object` : Object? = null var `object`: Object? = null
protected constructor() : super() protected constructor() : super()
constructor(type: List<String> = emptyList(), name: String, `object`: Object?) : super(add(type,"Create"), name) { constructor(
type: List<String> = emptyList(),
name: String? = null,
`object`: Object?,
actor: String? = null,
id: String? = null
) : super(
add(type, "Create"),
name,
actor,
id
) {
this.`object` = `object` this.`object` = `object`
} }

View File

@ -1,17 +1,16 @@
package dev.usbharu.hideout.domain.model.ap package dev.usbharu.hideout.domain.model.ap
open class Follow : Object { open class Follow : Object {
public var `object`:String? = null var `object`: String? = null
public var actor:String? = null
protected constructor() : super() protected constructor() : super()
constructor( constructor(
type: List<String> = emptyList(), type: List<String> = emptyList(),
name: String, name: String,
`object`: String?, `object`: String?,
actor: String? actor: String?
) : super(add(type,"Follow"), name) { ) : super(add(type, "Follow"), name,actor) {
this.`object` = `object` this.`object` = `object`
this.actor = actor
} }

View File

@ -6,7 +6,7 @@ open class Image : Object {
protected constructor() : super() protected constructor() : super()
constructor(type: List<String> = emptyList(), name: String, mediaType: String?, url: String?) : super( constructor(type: List<String> = emptyList(), name: String, mediaType: String?, url: String?) : super(
add(type,"Image"), add(type, "Image"),
name name
) { ) {
this.mediaType = mediaType this.mediaType = mediaType

View File

@ -2,6 +2,7 @@ package dev.usbharu.hideout.domain.model.ap
import com.fasterxml.jackson.annotation.JsonAutoDetect import com.fasterxml.jackson.annotation.JsonAutoDetect
import com.fasterxml.jackson.annotation.JsonCreator import com.fasterxml.jackson.annotation.JsonCreator
import com.fasterxml.jackson.annotation.JsonInclude
import com.fasterxml.jackson.annotation.JsonProperty import com.fasterxml.jackson.annotation.JsonProperty
import com.fasterxml.jackson.core.JsonGenerator import com.fasterxml.jackson.core.JsonGenerator
import com.fasterxml.jackson.databind.JsonDeserializer import com.fasterxml.jackson.databind.JsonDeserializer
@ -15,11 +16,12 @@ import com.fasterxml.jackson.databind.annotation.JsonSerialize
open class JsonLd { open class JsonLd {
@JsonProperty("@context") @JsonProperty("@context")
@JsonDeserialize(contentUsing = ContextDeserializer::class) @JsonDeserialize(contentUsing = ContextDeserializer::class)
@JsonSerialize(using = ContextSerializer::class) @JsonSerialize(include = JsonSerialize.Inclusion.NON_EMPTY, using = ContextSerializer::class)
var context:List<String> = emptyList() @JsonInclude(JsonInclude.Include.NON_EMPTY)
var context: List<String> = emptyList()
@JsonCreator @JsonCreator
constructor(context:List<String>){ constructor(context: List<String>) {
this.context = context this.context = context
} }
@ -43,9 +45,12 @@ open class JsonLd {
} }
public class ContextDeserializer : JsonDeserializer<String>() { class ContextDeserializer : JsonDeserializer<String>() {
override fun deserialize(p0: com.fasterxml.jackson.core.JsonParser?, p1: com.fasterxml.jackson.databind.DeserializationContext?): String { override fun deserialize(
val readTree : JsonNode = p0?.codec?.readTree(p0) ?: return "" p0: com.fasterxml.jackson.core.JsonParser?,
p1: com.fasterxml.jackson.databind.DeserializationContext?
): String {
val readTree: JsonNode = p0?.codec?.readTree(p0) ?: return ""
if (readTree.isObject) { if (readTree.isObject) {
return "" return ""
} }
@ -53,17 +58,23 @@ public class ContextDeserializer : JsonDeserializer<String>() {
} }
} }
public class ContextSerializer : JsonSerializer<List<String>>() { class ContextSerializer : JsonSerializer<List<String>>() {
override fun isEmpty(value: List<String>?): Boolean {
return value.isNullOrEmpty()
}
override fun serialize(value: List<String>?, gen: JsonGenerator?, serializers: SerializerProvider?) { override fun serialize(value: List<String>?, gen: JsonGenerator?, serializers: SerializerProvider?) {
if (value.isNullOrEmpty()) { if (value.isNullOrEmpty()) {
gen?.writeNull() gen?.writeNull()
return return
} }
if (value?.size == 1) { if (value.size == 1) {
gen?.writeString(value[0]) gen?.writeString(value[0])
} else { } else {
gen?.writeStartArray() gen?.writeStartArray()
value?.forEach { value.forEach {
gen?.writeString(it) gen?.writeString(it)
} }
gen?.writeEndArray() gen?.writeEndArray()

View File

@ -1,9 +1,9 @@
package dev.usbharu.hideout.domain.model.ap package dev.usbharu.hideout.domain.model.ap
open class Key : Object { open class Key : Object {
var id:String? = null var owner: String? = null
var owner:String? = null var publicKeyPem: String? = null
var publicKeyPem:String? = null
protected constructor() : super() protected constructor() : super()
constructor( constructor(
type: List<String>, type: List<String>,
@ -11,8 +11,7 @@ open class Key : Object {
id: String?, id: String?,
owner: String?, owner: String?,
publicKeyPem: String? publicKeyPem: String?
) : super(add(type,"Key"), name) { ) : super(add(type, "Key"), name,id) {
this.id = id
this.owner = owner this.owner = owner
this.publicKeyPem = publicKeyPem this.publicKeyPem = publicKeyPem
} }

View File

@ -1,11 +1,11 @@
package dev.usbharu.hideout.domain.model.ap package dev.usbharu.hideout.domain.model.ap
open class Note : Object { open class Note : Object {
var id:String? = null var attributedTo: String? = null
var attributedTo:String? = null var content: String? = null
var content:String? = null var published: String? = null
var published:String? = null var to: List<String> = emptyList()
var to:List<String> = emptyList()
protected constructor() : super() protected constructor() : super()
constructor( constructor(
type: List<String> = emptyList(), type: List<String> = emptyList(),
@ -15,8 +15,11 @@ open class Note : Object {
content: String?, content: String?,
published: String?, published: String?,
to: List<String> = emptyList() to: List<String> = emptyList()
) : super(add(type,"Note"), name) { ) : super(
this.id = id type = add(type, "Note"),
name = name,
id = id
) {
this.attributedTo = attributedTo this.attributedTo = attributedTo
this.content = content this.content = content
this.published = published this.published = published

View File

@ -9,44 +9,54 @@ open class Object : JsonLd {
@JsonSerialize(using = TypeSerializer::class) @JsonSerialize(using = TypeSerializer::class)
private var type: List<String> = emptyList() private var type: List<String> = emptyList()
var name: String? = null var name: String? = null
var actor: String? = null
var id:String? = null
protected constructor() protected constructor()
constructor(type: List<String>, name: String) : super() { constructor(type: List<String>, name: String? = null,actor:String? = null,id:String? = null) : super() {
this.type = type this.type = type
this.name = name this.name = name
this.actor = actor
this.id = id
} }
companion object { companion object {
@JvmStatic @JvmStatic
protected fun add(list:List<String>,type:String):List<String> { protected fun add(list: List<String>, type: String): List<String> {
val toMutableList = list.toMutableList() val toMutableList = list.toMutableList()
toMutableList.add(type) toMutableList.add(type)
return toMutableList.distinct() return toMutableList.distinct()
} }
} }
override fun equals(other: Any?): Boolean { override fun equals(other: Any?): Boolean {
if (this === other) return true if (this === other) return true
if (other !is Object) return false if (other !is Object) return false
if (!super.equals(other)) return false
if (type != other.type) return false if (type != other.type) return false
return name == other.name if (name != other.name) return false
return actor == other.actor
} }
override fun hashCode(): Int { override fun hashCode(): Int {
var result = type.hashCode() var result = super.hashCode()
result = 31 * result + type.hashCode()
result = 31 * result + (name?.hashCode() ?: 0) result = 31 * result + (name?.hashCode() ?: 0)
result = 31 * result + (actor?.hashCode() ?: 0)
return result return result
} }
override fun toString(): String { override fun toString(): String {
return "Object(type=$type, name=$name) ${super.toString()}" return "Object(type=$type, name=$name, actor=$actor) ${super.toString()}"
} }
} }
public class TypeSerializer : JsonSerializer<List<String>>() { class TypeSerializer : JsonSerializer<List<String>>() {
override fun serialize(value: List<String>?, gen: JsonGenerator?, serializers: SerializerProvider?) { override fun serialize(value: List<String>?, gen: JsonGenerator?, serializers: SerializerProvider?) {
println(value) println(value)
if (value?.size == 1) { if (value?.size == 1) {

View File

@ -1,14 +1,14 @@
package dev.usbharu.hideout.domain.model.ap package dev.usbharu.hideout.domain.model.ap
open class Person : Object { open class Person : Object {
private var id:String? = null var preferredUsername: String? = null
var preferredUsername:String? = null var summary: String? = null
var summary:String? = null var inbox: String? = null
var inbox:String? = null var outbox: String? = null
var outbox:String? = null private var url: String? = null
private var url:String? = null
private var icon: Image? = null private var icon: Image? = null
var publicKey: Key? = null var publicKey: Key? = null
protected constructor() : super() protected constructor() : super()
constructor( constructor(
type: List<String> = emptyList(), type: List<String> = emptyList(),
@ -21,15 +21,14 @@ open class Person : Object {
url: String?, url: String?,
icon: Image?, icon: Image?,
publicKey: Key? publicKey: Key?
) : super(add(type,"Person"), name) { ) : super(add(type, "Person"), name,id = id) {
this.id = id
this.preferredUsername = preferredUsername this.preferredUsername = preferredUsername
this.summary = summary this.summary = summary
this.inbox = inbox this.inbox = inbox
this.outbox = outbox this.outbox = outbox
this.url = url this.url = url
this.icon = icon this.icon = icon
this.publicKey = publicKey this.publicKey = publicKey
} }
override fun equals(other: Any?): Boolean { override fun equals(other: Any?): Boolean {

View File

@ -1,6 +1,6 @@
package dev.usbharu.hideout.domain.model.api package dev.usbharu.hideout.domain.model.api
data class StatusForPost( data class StatusForPost(
val status:String, val status: String,
val userId:Long val userId: Long
) )

View File

@ -0,0 +1,12 @@
package dev.usbharu.hideout.domain.model.hideout.dto
data class RemoteUserCreateDto(
val name:String,
val domain:String,
val screenName:String,
val description:String,
val inbox:String,
val outbox:String,
val url:String,
val publicKey:String,
)

View File

@ -0,0 +1,8 @@
package dev.usbharu.hideout.domain.model.hideout.dto
data class UserCreateDto(
val name:String,
val screenName:String,
val description:String,
val password:String
)

View File

@ -0,0 +1,18 @@
package dev.usbharu.hideout.domain.model.hideout.entity
import java.time.Instant
data class User(
val id: Long,
val name: String,
val domain: String,
val screenName: String,
val description: String,
val password: String? = null,
val inbox: String,
val outbox: String,
val url: String,
val publicKey: String,
val privateKey: String? = null,
val createdAt: Instant
)

View File

@ -4,13 +4,13 @@ import kjob.core.Job
sealed class HideoutJob(name: String = "") : Job(name) sealed class HideoutJob(name: String = "") : Job(name)
object ReceiveFollowJob : HideoutJob("ReceiveFollowJob"){ object ReceiveFollowJob : HideoutJob("ReceiveFollowJob") {
val actor = string("actor") val actor = string("actor")
val follow = string("follow") val follow = string("follow")
val targetActor = string("targetActor") val targetActor = string("targetActor")
} }
object DeliverPostJob : HideoutJob("DeliverPostJob"){ object DeliverPostJob : HideoutJob("DeliverPostJob") {
val post = string("post") val post = string("post")
val actor = string("actor") val actor = string("actor")
val inbox = string("inbox") val inbox = string("inbox")

View File

@ -1,5 +1,5 @@
package dev.usbharu.hideout.domain.model.wellknown package dev.usbharu.hideout.domain.model.wellknown
data class WebFinger(val subject:String,val links:List<Link>){ data class WebFinger(val subject: String, val links: List<Link>) {
data class Link(val rel:String,val type:String,val href:String) data class Link(val rel: String, val type: String, val href: String)
} }

View File

@ -1,8 +1,8 @@
package dev.usbharu.hideout.plugins package dev.usbharu.hideout.plugins
import dev.usbharu.hideout.domain.model.ap.JsonLd
import dev.usbharu.hideout.config.Config import dev.usbharu.hideout.config.Config
import dev.usbharu.hideout.service.IUserAuthService import dev.usbharu.hideout.domain.model.ap.JsonLd
import dev.usbharu.hideout.repository.IUserRepository
import dev.usbharu.hideout.service.impl.UserAuthService import dev.usbharu.hideout.service.impl.UserAuthService
import dev.usbharu.hideout.util.HttpUtil.Activity import dev.usbharu.hideout.util.HttpUtil.Activity
import io.ktor.client.* import io.ktor.client.*
@ -44,6 +44,13 @@ suspend fun HttpClient.postAp(urlString: String, username: String, jsonLd: JsonL
} }
} }
suspend fun HttpClient.getAp(urlString: String, username: String): HttpResponse {
return this.get(urlString) {
header("Accept", ContentType.Application.Activity)
header("Signature", "keyId=\"$username\",algorithm=\"rsa-sha256\",headers=\"(request-target) host date\"")
}
}
class HttpSignaturePluginConfig { class HttpSignaturePluginConfig {
lateinit var keyMap: KeyMap lateinit var keyMap: KeyMap
} }
@ -56,6 +63,7 @@ val httpSignaturePlugin = createClientPlugin("HttpSign", ::HttpSignaturePluginCo
request.header("Date", format.format(Date())) request.header("Date", format.format(Date()))
request.header("Host", "${request.url.host}")
println(request.bodyType) println(request.bodyType)
println(request.bodyType?.type) println(request.bodyType?.type)
if (request.bodyType?.type == String::class) { if (request.bodyType?.type == String::class) {
@ -63,8 +71,8 @@ val httpSignaturePlugin = createClientPlugin("HttpSign", ::HttpSignaturePluginCo
println("Digest !!") println("Digest !!")
// UserAuthService.sha256.reset() // UserAuthService.sha256.reset()
val digest = val digest =
Base64.getEncoder().encodeToString(UserAuthService.sha256.digest(body.toByteArray(Charsets.UTF_8))) Base64.getEncoder().encodeToString(UserAuthService.sha256.digest(body.toByteArray(Charsets.UTF_8)))
request.headers.append("Digest", "sha-256="+digest) request.headers.append("Digest", "sha-256=" + digest)
} }
if (request.headers.contains("Signature")) { if (request.headers.contains("Signature")) {
@ -104,6 +112,10 @@ val httpSignaturePlugin = createClientPlugin("HttpSign", ::HttpSignaturePluginCo
"Date" "Date"
} }
"host" -> {
"Host"
}
else -> { else -> {
it it
} }
@ -126,7 +138,7 @@ val httpSignaturePlugin = createClientPlugin("HttpSign", ::HttpSignaturePluginCo
override fun addHeader(name: String?, value: String?) { override fun addHeader(name: String?, value: String?) {
val split = value?.split("=").orEmpty() val split = value?.split("=").orEmpty()
name?.let { request.header(it, split.get(0)+"=\""+split.get(1).trim('"')+"\"") } name?.let { request.header(it, split.get(0) + "=\"" + split.get(1).trim('"') + "\"") }
} }
override fun method(): String { override fun method(): String {
@ -139,19 +151,24 @@ val httpSignaturePlugin = createClientPlugin("HttpSign", ::HttpSignaturePluginCo
}) })
val signatureHeader = request.headers.getAll("Signature").orEmpty()
request.headers.remove("Signature")
signatureHeader.map { it.replace("; ", ",").replace(";", ",") }.joinToString(",")
.let { request.header("Signature", it) }
} }
} }
} }
class KtorKeyMap(private val userAuthRepository: IUserAuthService) : KeyMap { class KtorKeyMap(private val userAuthRepository: IUserRepository) : KeyMap {
override fun getPublicKey(keyId: String?): PublicKey = runBlocking { override fun getPublicKey(keyId: String?): PublicKey = runBlocking {
val username = (keyId ?: throw IllegalArgumentException("keyId is null")).substringBeforeLast("#pubkey") val username = (keyId ?: throw IllegalArgumentException("keyId is null")).substringBeforeLast("#pubkey")
.substringAfterLast("/") .substringAfterLast("/")
val publicBytes = Base64.getDecoder().decode( val publicBytes = Base64.getDecoder().decode(
userAuthRepository.findByUsername( userAuthRepository.findByNameAndDomain(
username username, Config.configData.domain
).publicKey?.replace("-----BEGIN PUBLIC KEY-----", "-----END PUBLIC KEY-----")?.replace("", "") )?.publicKey?.replace("-----BEGIN PUBLIC KEY-----", "-----END PUBLIC KEY-----")?.replace("", "")
?.replace("\n", "") ?.replace("\n", "")
) )
val x509EncodedKeySpec = X509EncodedKeySpec(publicBytes) val x509EncodedKeySpec = X509EncodedKeySpec(publicBytes)
@ -162,9 +179,9 @@ class KtorKeyMap(private val userAuthRepository: IUserAuthService) : KeyMap {
val username = (keyId ?: throw IllegalArgumentException("keyId is null")).substringBeforeLast("#pubkey") val username = (keyId ?: throw IllegalArgumentException("keyId is null")).substringBeforeLast("#pubkey")
.substringAfterLast("/") .substringAfterLast("/")
val publicBytes = Base64.getDecoder().decode( val publicBytes = Base64.getDecoder().decode(
userAuthRepository.findByUsername( userAuthRepository.findByNameAndDomain(
username username, Config.configData.domain
).privateKey?.replace("-----BEGIN PRIVATE KEY-----", "")?.replace("-----END PRIVATE KEY-----", "") )?.privateKey?.replace("-----BEGIN PRIVATE KEY-----", "")?.replace("-----END PRIVATE KEY-----", "")
?.replace("\n", "") ?.replace("\n", "")
) )
val x509EncodedKeySpec = PKCS8EncodedKeySpec(publicBytes) val x509EncodedKeySpec = PKCS8EncodedKeySpec(publicBytes)

View File

@ -1,10 +1,10 @@
package dev.usbharu.hideout.plugins package dev.usbharu.hideout.plugins
import io.ktor.http.* import io.ktor.http.*
import io.ktor.server.application.*
import io.ktor.server.plugins.cors.routing.* import io.ktor.server.plugins.cors.routing.*
import io.ktor.server.plugins.defaultheaders.* import io.ktor.server.plugins.defaultheaders.*
import io.ktor.server.plugins.forwardedheaders.* import io.ktor.server.plugins.forwardedheaders.*
import io.ktor.server.application.*
fun Application.configureHTTP() { fun Application.configureHTTP() {
install(CORS) { install(CORS) {

View File

@ -1,9 +1,8 @@
package dev.usbharu.hideout.plugins package dev.usbharu.hideout.plugins
import io.ktor.server.plugins.callloging.*
import org.slf4j.event.*
import io.ktor.server.request.*
import io.ktor.server.application.* import io.ktor.server.application.*
import io.ktor.server.plugins.callloging.*
import org.slf4j.event.Level
fun Application.configureMonitoring() { fun Application.configureMonitoring() {
install(CallLogging) { install(CallLogging) {

View File

@ -8,7 +8,7 @@ import dev.usbharu.hideout.routing.wellknown.webfinger
import dev.usbharu.hideout.service.IPostService import dev.usbharu.hideout.service.IPostService
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.ActivityPubUserService
import dev.usbharu.hideout.service.impl.UserService import dev.usbharu.hideout.service.impl.IUserService
import dev.usbharu.hideout.service.signature.HttpSignatureVerifyService import dev.usbharu.hideout.service.signature.HttpSignatureVerifyService
import io.ktor.server.application.* import io.ktor.server.application.*
import io.ktor.server.plugins.autohead.* import io.ktor.server.plugins.autohead.*
@ -17,7 +17,7 @@ import io.ktor.server.routing.*
fun Application.configureRouting( fun Application.configureRouting(
httpSignatureVerifyService: HttpSignatureVerifyService, httpSignatureVerifyService: HttpSignatureVerifyService,
activityPubService: ActivityPubService, activityPubService: ActivityPubService,
userService: UserService, userService: IUserService,
activityPubUserService: ActivityPubUserService, activityPubUserService: ActivityPubUserService,
postService: IPostService postService: IPostService
) { ) {
@ -25,7 +25,7 @@ fun Application.configureRouting(
routing { routing {
inbox(httpSignatureVerifyService, activityPubService) inbox(httpSignatureVerifyService, activityPubService)
outbox() outbox()
usersAP(activityPubUserService,userService) usersAP(activityPubUserService, userService)
webfinger(userService) webfinger(userService)
route("/api/v1") { route("/api/v1") {

View File

@ -3,8 +3,6 @@ package dev.usbharu.hideout.plugins
import dev.usbharu.hideout.service.IUserAuthService import dev.usbharu.hideout.service.IUserAuthService
import io.ktor.server.application.* import io.ktor.server.application.*
import io.ktor.server.auth.* import io.ktor.server.auth.*
import io.ktor.server.sessions.*
import kotlin.collections.set
data class UserSession(val username: String) : Principal data class UserSession(val username: String) : Principal

View File

@ -4,13 +4,9 @@ import com.fasterxml.jackson.annotation.JsonInclude
import com.fasterxml.jackson.annotation.JsonSetter import com.fasterxml.jackson.annotation.JsonSetter
import com.fasterxml.jackson.annotation.Nulls import com.fasterxml.jackson.annotation.Nulls
import com.fasterxml.jackson.databind.DeserializationFeature 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.*
import io.ktor.server.response.*
import io.ktor.server.routing.*
fun Application.configureSerialization() { fun Application.configureSerialization() {
install(ContentNegotiation) { install(ContentNegotiation) {

View File

@ -1,15 +0,0 @@
package dev.usbharu.hideout.repository
import dev.usbharu.hideout.domain.model.UserAuthentication
import dev.usbharu.hideout.domain.model.UserAuthenticationEntity
interface IUserAuthRepository {
suspend fun create(userAuthentication: UserAuthentication):UserAuthenticationEntity
suspend fun findById(id:Long):UserAuthenticationEntity?
suspend fun update(userAuthenticationEntity: UserAuthenticationEntity)
suspend fun delete(id:Long)
suspend fun findByUserId(id: Long): UserAuthenticationEntity?
}

View File

@ -1,32 +1,38 @@
package dev.usbharu.hideout.repository package dev.usbharu.hideout.repository
import dev.usbharu.hideout.domain.model.User import dev.usbharu.hideout.domain.model.hideout.entity.User
import dev.usbharu.hideout.domain.model.UserEntity
interface IUserRepository { interface IUserRepository {
suspend fun create(user: User): UserEntity suspend fun save(user: User): User
suspend fun findById(id: Long): UserEntity? suspend fun findById(id: Long): User?
suspend fun findByIds(ids: List<Long>): List<UserEntity> suspend fun findByIds(ids: List<Long>): List<User>
suspend fun findByName(name: String): UserEntity? suspend fun findByName(name: String): List<User>
suspend fun findByNameAndDomains(names: List<Pair<String,String>>): List<UserEntity> suspend fun findByNameAndDomain(name: String, domain: String): User?
suspend fun findByUrl(url:String):UserEntity? suspend fun findByDomain(domain:String): List<User>
suspend fun findByUrls(urls: List<String>): List<UserEntity> suspend fun findByNameAndDomains(names: List<Pair<String,String>>): List<User>
suspend fun update(userEntity: UserEntity) suspend fun findByUrl(url:String): User?
suspend fun findByUrls(urls: List<String>): List<User>
@Deprecated("", ReplaceWith("save(userEntity)"))
suspend fun update(userEntity: User) = save(userEntity)
suspend fun delete(id: Long) 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<User>
suspend fun createFollower(id: Long, follower: Long) suspend fun createFollower(id: Long, follower: Long)
suspend fun deleteFollower(id: Long, follower: Long) suspend fun deleteFollower(id: Long, follower: Long)
suspend fun findFollowersById(id: Long): List<UserEntity> suspend fun findFollowersById(id: Long): List<User>
suspend fun nextId():Long
} }

View File

@ -1,63 +0,0 @@
package dev.usbharu.hideout.repository
import dev.usbharu.hideout.domain.model.UserAuthentication
import dev.usbharu.hideout.domain.model.UserAuthenticationEntity
import dev.usbharu.hideout.domain.model.UsersAuthentication
import kotlinx.coroutines.Dispatchers
import org.jetbrains.exposed.sql.*
import org.jetbrains.exposed.sql.transactions.experimental.newSuspendedTransaction
import org.jetbrains.exposed.sql.transactions.transaction
class UserAuthRepository(private val database: Database) : IUserAuthRepository {
init {
transaction(database) {
SchemaUtils.create(UsersAuthentication)
SchemaUtils.createMissingTablesAndColumns(UsersAuthentication)
}
}
private fun ResultRow.toUserAuth():UserAuthenticationEntity{
return UserAuthenticationEntity(
id = this[UsersAuthentication.id].value,
userId = this[UsersAuthentication.userId],
hash = this[UsersAuthentication.hash],
publicKey = this[UsersAuthentication.publicKey],
privateKey = this[UsersAuthentication.privateKey]
)
}
suspend fun <T> query(block: suspend () -> T): T =
newSuspendedTransaction(Dispatchers.IO) {block()}
override suspend fun create(userAuthentication: UserAuthentication): UserAuthenticationEntity {
return query {
UserAuthenticationEntity(
UsersAuthentication.insert {
it[userId] = userAuthentication.userId
it[hash] = userAuthentication.hash
it[publicKey] = userAuthentication.publicKey
it[privateKey] = userAuthentication.privateKey
}[UsersAuthentication.id].value,userAuthentication
)
}
}
override suspend fun findById(id: Long): UserAuthenticationEntity? {
TODO("Not yet implemented")
}
override suspend fun findByUserId(id:Long):UserAuthenticationEntity? {
return query {
UsersAuthentication.select { UsersAuthentication.userId eq id }.map { it.toUserAuth() }.singleOrNull()
}
}
override suspend fun update(userAuthenticationEntity: UserAuthenticationEntity) {
TODO("Not yet implemented")
}
override suspend fun delete(id: Long) {
TODO("Not yet implemented")
}
}

View File

@ -1,16 +1,17 @@
package dev.usbharu.hideout.repository package dev.usbharu.hideout.repository
import dev.usbharu.hideout.domain.model.User import dev.usbharu.hideout.domain.model.hideout.entity.User
import dev.usbharu.hideout.domain.model.UserEntity import dev.usbharu.hideout.service.IdGenerateService
import dev.usbharu.hideout.domain.model.Users
import dev.usbharu.hideout.domain.model.UsersFollowers
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import org.jetbrains.exposed.dao.id.LongIdTable
import org.jetbrains.exposed.sql.* import org.jetbrains.exposed.sql.*
import org.jetbrains.exposed.sql.SqlExpressionBuilder.eq import org.jetbrains.exposed.sql.SqlExpressionBuilder.eq
import org.jetbrains.exposed.sql.transactions.experimental.newSuspendedTransaction import org.jetbrains.exposed.sql.transactions.experimental.newSuspendedTransaction
import org.jetbrains.exposed.sql.transactions.transaction import org.jetbrains.exposed.sql.transactions.transaction
import java.time.Instant
class UserRepository(private val database: Database) : IUserRepository { class UserRepository(private val database: Database, private val idGenerateService: IdGenerateService) :
IUserRepository {
init { init {
transaction(database) { transaction(database) {
SchemaUtils.create(Users) SchemaUtils.create(Users)
@ -20,45 +21,43 @@ class UserRepository(private val database: Database) : IUserRepository {
} }
} }
private fun ResultRow.toUser(): User {
return User(
this[Users.name],
this[Users.domain],
this[Users.screenName],
this[Users.description],
this[Users.inbox],
this[Users.outbox],
this[Users.url]
)
}
private fun ResultRow.toUserEntity(): UserEntity {
return UserEntity(
this[Users.id].value,
this[Users.name],
this[Users.domain],
this[Users.screenName],
this[Users.description],
this[Users.inbox],
this[Users.outbox],
this[Users.url],
)
}
suspend fun <T> query(block: suspend () -> T): T = suspend fun <T> query(block: suspend () -> T): T =
newSuspendedTransaction(Dispatchers.IO) { block() } newSuspendedTransaction(Dispatchers.IO) { block() }
override suspend fun create(user: User): UserEntity { override suspend fun save(user: User): User {
return query { return query {
UserEntity(Users.insert { val singleOrNull = Users.select { Users.id eq user.id }.singleOrNull()
it[name] = user.name if (singleOrNull == null) {
it[domain] = user.domain Users.insert {
it[screenName] = user.screenName it[id] = user.id
it[description] = user.description it[name] = user.name
it[inbox] = user.inbox it[domain] = user.domain
it[outbox] = user.outbox it[screenName] = user.screenName
it[url] = user.url it[description] = user.description
}[Users.id].value, user) it[password] = user.password
it[inbox] = user.inbox
it[outbox] = user.outbox
it[url] = user.url
it[createdAt] = user.createdAt.toEpochMilli()
it[publicKey] = user.publicKey
it[privateKey] = user.privateKey
}
} else {
Users.update({ Users.id eq user.id }) {
it[name] = user.name
it[domain] = user.domain
it[screenName] = user.screenName
it[description] = user.description
it[password] = user.password
it[inbox] = user.inbox
it[outbox] = user.outbox
it[url] = user.url
it[createdAt] = user.createdAt.toEpochMilli()
it[publicKey] = user.publicKey
it[privateKey] = user.privateKey
}
}
return@query user
} }
} }
@ -71,59 +70,73 @@ class UserRepository(private val database: Database) : IUserRepository {
} }
} }
override suspend fun findById(id: Long): UserEntity? { override suspend fun findById(id: Long): User? {
return query { return query {
Users.select { Users.id eq id }.map { Users.select { Users.id eq id }.map {
it.toUserEntity() it.toUser()
}.singleOrNull() }.singleOrNull()
} }
} }
override suspend fun findByIds(ids: List<Long>): List<UserEntity> { override suspend fun findByIds(ids: List<Long>): List<User> {
return query { return query {
Users.select { Users.id inList ids }.map { Users.select { Users.id inList ids }.map {
it.toUserEntity() it.toUser()
} }
} }
} }
override suspend fun findByName(name: String): UserEntity? { override suspend fun findByName(name: String): List<User> {
return query { return query {
Users.select { Users.name eq name }.map { Users.select { Users.name eq name }.map {
it.toUserEntity() it.toUser()
}.singleOrNull() }
} }
} }
override suspend fun findByNameAndDomains(names: List<Pair<String, String>>): List<UserEntity> { override suspend fun findByNameAndDomain(name: String, domain: String): User? {
return query {
Users.select { Users.name eq name and (Users.domain eq domain) }.singleOrNull()?.toUser()
}
}
override suspend fun findByDomain(domain: String): List<User> {
return query {
Users.select { Users.domain eq domain }.map {
it.toUser()
}
}
}
override suspend fun findByNameAndDomains(names: List<Pair<String, String>>): List<User> {
return query { return query {
val selectAll = Users.selectAll() val selectAll = Users.selectAll()
names.forEach { (name, domain) -> names.forEach { (name, domain) ->
selectAll.orWhere { Users.name eq name and (Users.domain eq domain) } selectAll.orWhere { Users.name eq name and (Users.domain eq domain) }
} }
selectAll.map { it.toUserEntity() } selectAll.map { it.toUser() }
} }
} }
override suspend fun findByUrl(url: String): UserEntity? { override suspend fun findByUrl(url: String): User? {
return query { return query {
Users.select { Users.url eq url }.singleOrNull()?.toUserEntity() Users.select { Users.url eq url }.singleOrNull()?.toUser()
} }
} }
override suspend fun findByUrls(urls: List<String>): List<UserEntity> { override suspend fun findByUrls(urls: List<String>): List<User> {
return query { return query {
Users.select { Users.url inList urls }.map { it.toUserEntity() } Users.select { Users.url inList urls }.map { it.toUser() }
} }
} }
override suspend fun findFollowersById(id: Long): List<UserEntity> { override suspend fun findFollowersById(id: Long): List<User> {
return query { return query {
val followers = Users.alias("FOLLOWERS") val followers = Users.alias("FOLLOWERS")
Users.innerJoin( Users.innerJoin(
otherTable = UsersFollowers, otherTable = UsersFollowers,
onColumn = { Users.id }, onColumn = { Users.id },
otherColumn = { UsersFollowers.userId }) otherColumn = { userId })
.innerJoin( .innerJoin(
otherTable = followers, otherTable = followers,
@ -136,41 +149,35 @@ class UserRepository(private val database: Database) : IUserRepository {
followers.get(Users.domain), followers.get(Users.domain),
followers.get(Users.screenName), followers.get(Users.screenName),
followers.get(Users.description), followers.get(Users.description),
followers.get(Users.password),
followers.get(Users.inbox), followers.get(Users.inbox),
followers.get(Users.outbox), followers.get(Users.outbox),
followers.get(Users.url) followers.get(Users.url),
followers.get(Users.publicKey),
followers.get(Users.privateKey),
followers.get(Users.createdAt)
) )
.select { Users.id eq id } .select { Users.id eq id }
.map { .map {
UserEntity( User(
id = it[followers[Users.id]].value, id = it[followers[Users.id]],
name = it[followers[Users.name]], name = it[followers[Users.name]],
domain = it[followers[Users.domain]], domain = it[followers[Users.domain]],
screenName = it[followers[Users.screenName]], screenName = it[followers[Users.screenName]],
description = it[followers[Users.description]], description = it[followers[Users.description]],
password = it[followers[Users.password]],
inbox = it[followers[Users.inbox]], inbox = it[followers[Users.inbox]],
outbox = it[followers[Users.outbox]], outbox = it[followers[Users.outbox]],
url = it[followers[Users.url]], url = it[followers[Users.url]],
publicKey = it[followers[Users.publicKey]],
privateKey = it[followers[Users.privateKey]],
createdAt = Instant.ofEpochMilli(it[followers[Users.createdAt]])
) )
} }
} }
} }
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
it[inbox] = userEntity.inbox
it[outbox] = userEntity.outbox
it[url] = userEntity.url
}
}
}
override suspend fun delete(id: Long) { override suspend fun delete(id: Long) {
query { query {
Users.deleteWhere { Users.id.eq(id) } Users.deleteWhere { Users.id.eq(id) }
@ -189,9 +196,60 @@ class UserRepository(private val database: Database) : IUserRepository {
} }
} }
override suspend fun findAllByLimitAndByOffset(limit: Int, offset: Long): List<UserEntity> { override suspend fun findAllByLimitAndByOffset(limit: Int, offset: Long): List<User> {
return query { return query {
Users.selectAll().limit(limit, offset).map { it.toUserEntity() } Users.selectAll().limit(limit, offset).map { it.toUser() }
} }
} }
override suspend fun nextId(): Long {
return idGenerateService.generateId()
}
}
object Users : Table("users") {
val id = long("id")
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)
val password = varchar("password", length = 255).nullable()
val inbox = varchar("inbox", length = 255).uniqueIndex()
val outbox = varchar("outbox", length = 255).uniqueIndex()
val url = varchar("url", length = 255).uniqueIndex()
val publicKey = varchar("public_key", length = 10000)
val privateKey = varchar("private_key", length = 10000).nullable()
val createdAt = long("created_at")
override val primaryKey: PrimaryKey = PrimaryKey(id)
init {
uniqueIndex(name, domain)
}
}
fun ResultRow.toUser(): User {
return User(
this[Users.id],
this[Users.name],
this[Users.domain],
this[Users.screenName],
this[Users.description],
this[Users.password],
this[Users.inbox],
this[Users.outbox],
this[Users.url],
this[Users.publicKey],
this[Users.privateKey],
Instant.ofEpochMilli((this[Users.createdAt]))
)
}
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

@ -1,16 +1,15 @@
package dev.usbharu.hideout.routing package dev.usbharu.hideout.routing
import dev.usbharu.hideout.plugins.UserSession import dev.usbharu.hideout.domain.model.hideout.dto.UserCreateDto
import dev.usbharu.hideout.service.IUserAuthService import dev.usbharu.hideout.service.impl.IUserService
import io.ktor.http.* import io.ktor.http.*
import io.ktor.server.application.* import io.ktor.server.application.*
import io.ktor.server.auth.* import io.ktor.server.auth.*
import io.ktor.server.request.* 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.*
import io.ktor.server.sessions.*
fun Application.register(userAuthService: IUserAuthService) { fun Application.register(userService: IUserService) {
routing { routing {
get("/register") { get("/register") {
@ -39,13 +38,10 @@ fun Application.register(userAuthService: IUserAuthService) {
val parameters = call.receiveParameters() val parameters = call.receiveParameters()
val password = parameters["password"] ?: return@post call.respondRedirect("/register") val password = parameters["password"] ?: return@post call.respondRedirect("/register")
val username = parameters["username"] ?: return@post call.respondRedirect("/register") val username = parameters["username"] ?: return@post call.respondRedirect("/register")
if (userAuthService.usernameAlreadyUse(username)) { if (userService.usernameAlreadyUse(username)) {
return@post call.respondRedirect("/register") return@post call.respondRedirect("/register")
} }
userService.createLocalUser(UserCreateDto(username, username, "", password))
val hash = userAuthService.hash(password)
userAuthService.registerAccount(username,hash)
// call.respondRedirect("/login")
call.respondRedirect("/users/$username") call.respondRedirect("/users/$username")
} }
} }

View File

@ -3,7 +3,7 @@ package dev.usbharu.hideout.routing.activitypub
import dev.usbharu.hideout.exception.ParameterNotExistException import dev.usbharu.hideout.exception.ParameterNotExistException
import dev.usbharu.hideout.plugins.respondAp import dev.usbharu.hideout.plugins.respondAp
import dev.usbharu.hideout.service.activitypub.ActivityPubUserService import dev.usbharu.hideout.service.activitypub.ActivityPubUserService
import dev.usbharu.hideout.service.impl.UserService import dev.usbharu.hideout.service.impl.IUserService
import dev.usbharu.hideout.util.HttpUtil.Activity import dev.usbharu.hideout.util.HttpUtil.Activity
import dev.usbharu.hideout.util.HttpUtil.JsonLd import dev.usbharu.hideout.util.HttpUtil.JsonLd
import io.ktor.http.* import io.ktor.http.*
@ -12,9 +12,11 @@ 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(activityPubUserService: ActivityPubUserService, userService: UserService) { fun Routing.usersAP(activityPubUserService: ActivityPubUserService, userService: IUserService) {
route("/users/{name}") { route("/users/{name}") {
createChild(ContentTypeRouteSelector(ContentType.Application.Activity, ContentType.Application.JsonLd)).handle { createChild(ContentTypeRouteSelector(ContentType.Application.Activity, ContentType.Application.JsonLd)).handle {
call.application.log.debug("Signature: ${call.request.header("Signature")}")
call.application.log.debug("Authorization: ${call.request.header("Authorization")}")
val name = val name =
call.parameters["name"] ?: throw ParameterNotExistException("Parameter(name='name') does not exist.") call.parameters["name"] ?: throw ParameterNotExistException("Parameter(name='name') does not exist.")
val person = activityPubUserService.getPersonByName(name) val person = activityPubUserService.getPersonByName(name)
@ -24,7 +26,7 @@ fun Routing.usersAP(activityPubUserService: ActivityPubUserService, userService:
) )
} }
get { get {
val userEntity = userService.findByName(call.parameters["name"]!!) val userEntity = userService.findByNameLocalUser(call.parameters["name"]!!)
call.respondText(userEntity.toString() + "\n" + userService.findFollowersById(userEntity.id)) call.respondText(userEntity.toString() + "\n" + userService.findFollowersById(userEntity.id))
} }
} }
@ -33,9 +35,11 @@ fun Routing.usersAP(activityPubUserService: ActivityPubUserService, userService:
class ContentTypeRouteSelector(private vararg val contentType: ContentType) : RouteSelector() { class ContentTypeRouteSelector(private vararg val contentType: ContentType) : RouteSelector() {
override fun evaluate(context: RoutingResolveContext, segmentIndex: Int): RouteSelectorEvaluation { override fun evaluate(context: RoutingResolveContext, segmentIndex: Int): RouteSelectorEvaluation {
val requestContentType = context.call.application.log.debug("Accept: ${context.call.request.accept()}")
ContentType.parse(context.call.request.accept() ?: return RouteSelectorEvaluation.FailedParameter) val requestContentType = context.call.request.accept() ?: return RouteSelectorEvaluation.FailedParameter
return if (contentType.any { contentType -> contentType.match(requestContentType) }) { return if (requestContentType.split(",")
.find { contentType.find { contentType -> contentType.match(it) } != null } != null
) {
RouteSelectorEvaluation.Constant RouteSelectorEvaluation.Constant
} else { } else {
RouteSelectorEvaluation.FailedParameter RouteSelectorEvaluation.FailedParameter

View File

@ -4,14 +4,14 @@ import dev.usbharu.hideout.config.Config
import dev.usbharu.hideout.domain.model.wellknown.WebFinger import dev.usbharu.hideout.domain.model.wellknown.WebFinger
import dev.usbharu.hideout.exception.IllegalParameterException import dev.usbharu.hideout.exception.IllegalParameterException
import dev.usbharu.hideout.exception.ParameterNotExistException import dev.usbharu.hideout.exception.ParameterNotExistException
import dev.usbharu.hideout.service.impl.UserService import dev.usbharu.hideout.service.impl.IUserService
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.*
import io.ktor.server.response.* import io.ktor.server.response.*
import io.ktor.server.routing.* import io.ktor.server.routing.*
fun Routing.webfinger(userService:UserService){ fun Routing.webfinger(userService: IUserService){
route("/.well-known/webfinger"){ route("/.well-known/webfinger"){
get { get {
val acct = call.request.queryParameters["resource"]?.decodeURLPart() val acct = call.request.queryParameters["resource"]?.decodeURLPart()
@ -25,7 +25,7 @@ fun Routing.webfinger(userService:UserService){
.substringAfter("acct:") .substringAfter("acct:")
.trimStart('@') .trimStart('@')
val userEntity = userService.findByName(accountName) val userEntity = userService.findByNameLocalUser(accountName)
val webFinger = WebFinger( val webFinger = WebFinger(
subject = acct, subject = acct,

View File

@ -1,18 +1,14 @@
package dev.usbharu.hideout.service package dev.usbharu.hideout.service
import dev.usbharu.hideout.domain.model.UserAuthentication import java.security.KeyPair
import dev.usbharu.hideout.domain.model.UserAuthenticationEntity
interface IUserAuthService { interface IUserAuthService {
fun hash(password: String): String fun hash(password: String): String
suspend fun usernameAlreadyUse(username: String): Boolean suspend fun usernameAlreadyUse(username: String): Boolean
suspend fun registerAccount(username: String, hash: String)
suspend fun generateKeyPair():KeyPair
suspend fun verifyAccount(username: String, password: String): Boolean suspend fun verifyAccount(username: String, password: String): Boolean
suspend fun findByUserId(userId: Long): UserAuthenticationEntity
suspend fun findByUsername(username: String): UserAuthenticationEntity
suspend fun createAccount(userEntity: UserAuthentication): UserAuthenticationEntity
} }

View File

@ -1,14 +1,14 @@
package dev.usbharu.hideout.service.activitypub package dev.usbharu.hideout.service.activitypub
import com.fasterxml.jackson.module.kotlin.readValue import com.fasterxml.jackson.module.kotlin.readValue
import dev.usbharu.hideout.domain.model.ap.Accept
import dev.usbharu.hideout.domain.model.ap.Follow
import dev.usbharu.hideout.config.Config 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.ActivityPubStringResponse import dev.usbharu.hideout.domain.model.ActivityPubStringResponse
import dev.usbharu.hideout.domain.model.ap.Accept
import dev.usbharu.hideout.domain.model.ap.Follow
import dev.usbharu.hideout.domain.model.job.ReceiveFollowJob import dev.usbharu.hideout.domain.model.job.ReceiveFollowJob
import dev.usbharu.hideout.plugins.postAp import dev.usbharu.hideout.plugins.postAp
import dev.usbharu.hideout.service.impl.UserService import dev.usbharu.hideout.service.impl.IUserService
import dev.usbharu.hideout.service.job.JobQueueParentService import dev.usbharu.hideout.service.job.JobQueueParentService
import io.ktor.client.* import io.ktor.client.*
import io.ktor.http.* import io.ktor.http.*
@ -17,7 +17,7 @@ import kjob.core.job.JobProps
class ActivityPubFollowServiceImpl( class ActivityPubFollowServiceImpl(
private val jobQueueParentService: JobQueueParentService, private val jobQueueParentService: JobQueueParentService,
private val activityPubUserService: ActivityPubUserService, private val activityPubUserService: ActivityPubUserService,
private val userService: UserService, private val userService: IUserService,
private val httpClient: HttpClient private val httpClient: HttpClient
) : ActivityPubFollowService { ) : ActivityPubFollowService {
override suspend fun receiveFollow(follow: Follow): ActivityPubResponse { override suspend fun receiveFollow(follow: Follow): ActivityPubResponse {
@ -32,9 +32,9 @@ class ActivityPubFollowServiceImpl(
override suspend fun receiveFollowJob(props: JobProps<ReceiveFollowJob>) { override suspend fun receiveFollowJob(props: JobProps<ReceiveFollowJob>) {
val actor = props[ReceiveFollowJob.actor] val actor = props[ReceiveFollowJob.actor]
val person = activityPubUserService.fetchPerson(actor)
val follow = Config.configData.objectMapper.readValue<Follow>(props[ReceiveFollowJob.follow])
val targetActor = props[ReceiveFollowJob.targetActor] val targetActor = props[ReceiveFollowJob.targetActor]
val person = activityPubUserService.fetchPerson(actor,targetActor)
val follow = Config.configData.objectMapper.readValue<Follow>(props[ReceiveFollowJob.follow])
httpClient.postAp( httpClient.postAp(
urlString = person.inbox ?: throw IllegalArgumentException("inbox is not found"), urlString = person.inbox ?: throw IllegalArgumentException("inbox is not found"),
username = "$targetActor#pubkey", username = "$targetActor#pubkey",

View File

@ -7,7 +7,7 @@ import dev.usbharu.hideout.domain.model.ap.Create
import dev.usbharu.hideout.domain.model.ap.Note import dev.usbharu.hideout.domain.model.ap.Note
import dev.usbharu.hideout.domain.model.job.DeliverPostJob import dev.usbharu.hideout.domain.model.job.DeliverPostJob
import dev.usbharu.hideout.plugins.postAp import dev.usbharu.hideout.plugins.postAp
import dev.usbharu.hideout.service.impl.UserService import dev.usbharu.hideout.service.impl.IUserService
import dev.usbharu.hideout.service.job.JobQueueParentService import dev.usbharu.hideout.service.job.JobQueueParentService
import io.ktor.client.* import io.ktor.client.*
import kjob.core.job.JobProps import kjob.core.job.JobProps
@ -17,7 +17,7 @@ import java.time.Instant
class ActivityPubNoteServiceImpl( class ActivityPubNoteServiceImpl(
private val httpClient: HttpClient, private val httpClient: HttpClient,
private val jobQueueParentService: JobQueueParentService, private val jobQueueParentService: JobQueueParentService,
private val userService: UserService private val userService: IUserService
) : ActivityPubNoteService { ) : ActivityPubNoteService {
private val logger = LoggerFactory.getLogger(this::class.java) private val logger = LoggerFactory.getLogger(this::class.java)
@ -45,7 +45,7 @@ class ActivityPubNoteServiceImpl(
attributedTo = actor, attributedTo = actor,
content = postEntity.text, content = postEntity.text,
published = Instant.ofEpochMilli(postEntity.createdAt).toString(), published = Instant.ofEpochMilli(postEntity.createdAt).toString(),
to = listOf("https://www.w3.org/ns/activitystreams#Public", actor + "/followers") to = listOf("https://www.w3.org/ns/activitystreams#Public", actor + "/follower")
) )
val inbox = props[DeliverPostJob.inbox] val inbox = props[DeliverPostJob.inbox]
logger.debug("createNoteJob: actor={}, note={}, inbox={}", actor, postEntity, inbox) logger.debug("createNoteJob: actor={}, note={}, inbox={}", actor, postEntity, inbox)
@ -54,7 +54,9 @@ class ActivityPubNoteServiceImpl(
username = "$actor#pubkey", username = "$actor#pubkey",
jsonLd = Create( jsonLd = Create(
name = "Create Note", name = "Create Note",
`object` = note `object` = note,
actor = note.attributedTo,
id = "${Config.configData.url}/create/note/${postEntity.id}"
) )
) )
} }

View File

@ -33,7 +33,7 @@ class ActivityPubServiceImpl(
return ActivityType.values().first { it.name.equals(type.asText(), true) } return ActivityType.values().first { it.name.equals(type.asText(), true) }
} }
override suspend fun processActivity(json: String, type: ActivityType): ActivityPubResponse? { override suspend fun processActivity(json: String, type: ActivityType): ActivityPubResponse {
return when (type) { return when (type) {
ActivityType.Accept -> TODO() ActivityType.Accept -> TODO()
ActivityType.Add -> TODO() ActivityType.Add -> TODO()

View File

@ -5,5 +5,5 @@ import dev.usbharu.hideout.domain.model.ap.Person
interface ActivityPubUserService { interface ActivityPubUserService {
suspend fun getPersonByName(name:String): Person suspend fun getPersonByName(name:String): Person
suspend fun fetchPerson(url:String): Person suspend fun fetchPerson(url: String, targetActor: String? = null): Person
} }

View File

@ -1,16 +1,15 @@
package dev.usbharu.hideout.service.activitypub package dev.usbharu.hideout.service.activitypub
import com.fasterxml.jackson.module.kotlin.readValue import com.fasterxml.jackson.module.kotlin.readValue
import dev.usbharu.hideout.config.Config
import dev.usbharu.hideout.domain.model.ap.Image import dev.usbharu.hideout.domain.model.ap.Image
import dev.usbharu.hideout.domain.model.ap.Key import dev.usbharu.hideout.domain.model.ap.Key
import dev.usbharu.hideout.domain.model.ap.Person import dev.usbharu.hideout.domain.model.ap.Person
import dev.usbharu.hideout.config.Config import dev.usbharu.hideout.domain.model.hideout.dto.RemoteUserCreateDto
import dev.usbharu.hideout.domain.model.User
import dev.usbharu.hideout.domain.model.UserAuthentication
import dev.usbharu.hideout.exception.UserNotFoundException import dev.usbharu.hideout.exception.UserNotFoundException
import dev.usbharu.hideout.exception.ap.IllegalActivityPubObjectException import dev.usbharu.hideout.exception.ap.IllegalActivityPubObjectException
import dev.usbharu.hideout.service.IUserAuthService import dev.usbharu.hideout.plugins.getAp
import dev.usbharu.hideout.service.impl.UserService import dev.usbharu.hideout.service.impl.IUserService
import dev.usbharu.hideout.util.HttpUtil.Activity import dev.usbharu.hideout.util.HttpUtil.Activity
import io.ktor.client.* import io.ktor.client.*
import io.ktor.client.request.* import io.ktor.client.request.*
@ -19,8 +18,7 @@ import io.ktor.http.*
import org.slf4j.LoggerFactory import org.slf4j.LoggerFactory
class ActivityPubUserServiceImpl( class ActivityPubUserServiceImpl(
private val userService: UserService, private val userService: IUserService,
private val userAuthService: IUserAuthService,
private val httpClient: HttpClient private val httpClient: HttpClient
) : ) :
ActivityPubUserService { ActivityPubUserService {
@ -28,8 +26,7 @@ class ActivityPubUserServiceImpl(
private val logger = LoggerFactory.getLogger(this::class.java) private val logger = LoggerFactory.getLogger(this::class.java)
override suspend fun getPersonByName(name: String): Person { override suspend fun getPersonByName(name: String): Person {
// TODO: JOINで書き直し // TODO: JOINで書き直し
val userEntity = userService.findByName(name) val userEntity = userService.findByNameLocalUser(name)
val userAuthEntity = userAuthService.findByUserId(userEntity.id)
val userUrl = "${Config.configData.url}/users/$name" val userUrl = "${Config.configData.url}/users/$name"
return Person( return Person(
type = emptyList(), type = emptyList(),
@ -51,15 +48,14 @@ class ActivityPubUserServiceImpl(
name = "Public Key", name = "Public Key",
id = "$userUrl#pubkey", id = "$userUrl#pubkey",
owner = userUrl, owner = userUrl,
publicKeyPem = userAuthEntity.publicKey publicKeyPem = userEntity.publicKey
) )
) )
} }
override suspend fun fetchPerson(url: String): Person { override suspend fun fetchPerson(url: String, targetActor: String?): Person {
return try { return try {
val userEntity = userService.findByUrl(url) val userEntity = userService.findByUrl(url)
val userAuthEntity = userAuthService.findByUsername(userEntity.name)
return Person( return Person(
type = emptyList(), type = emptyList(),
name = userEntity.name, name = userEntity.name,
@ -80,33 +76,31 @@ class ActivityPubUserServiceImpl(
name = "Public Key", name = "Public Key",
id = "$url#pubkey", id = "$url#pubkey",
owner = url, owner = url,
publicKeyPem = userAuthEntity.publicKey publicKeyPem = userEntity.publicKey
) )
) )
} catch (e: UserNotFoundException) { } catch (e: UserNotFoundException) {
val httpResponse = httpClient.get(url) { val httpResponse = if (targetActor != null) {
accept(ContentType.Application.Activity) httpClient.getAp(url,"$targetActor#pubkey")
}else {
httpClient.get(url) {
accept(ContentType.Application.Activity)
}
} }
val person = Config.configData.objectMapper.readValue<Person>(httpResponse.bodyAsText()) val person = Config.configData.objectMapper.readValue<Person>(httpResponse.bodyAsText())
val userEntity = userService.create(
User( userService.createRemoteUser(
RemoteUserCreateDto(
name = person.preferredUsername name = person.preferredUsername
?: throw IllegalActivityPubObjectException("preferredUsername is null"), ?: throw IllegalActivityPubObjectException("preferredUsername is null"),
domain = url.substringAfter(":").substringBeforeLast("/"), domain = url.substringAfter("://").substringBeforeLast("/"),
screenName = (person.name ?: person.preferredUsername) ?: throw IllegalActivityPubObjectException("preferredUsername is null"), screenName = (person.name ?: person.preferredUsername) ?: throw IllegalActivityPubObjectException("preferredUsername is null"),
description = person.summary ?: "", description = person.summary ?: "",
inbox = person.inbox ?: throw IllegalActivityPubObjectException("inbox is null"), inbox = person.inbox ?: throw IllegalActivityPubObjectException("inbox is null"),
outbox = person.outbox ?: throw IllegalActivityPubObjectException("outbox is null"), outbox = person.outbox ?: throw IllegalActivityPubObjectException("outbox is null"),
url = url url = url,
) publicKey = person.publicKey?.publicKeyPem ?: throw IllegalActivityPubObjectException("publicKey is null"),
)
userAuthService.createAccount(
UserAuthentication(
userEntity.id,
null,
person.publicKey?.publicKeyPem ?: throw IllegalActivityPubObjectException("publicKey is null"),
null
) )
) )
person person

View File

@ -0,0 +1,33 @@
package dev.usbharu.hideout.service.impl
import dev.usbharu.hideout.domain.model.hideout.dto.RemoteUserCreateDto
import dev.usbharu.hideout.domain.model.hideout.dto.UserCreateDto
import dev.usbharu.hideout.domain.model.hideout.entity.User
interface IUserService {
suspend fun findAll(limit: Int? = 100, offset: Long? = 0): List<User>
suspend fun findById(id: Long): User
suspend fun findByIds(ids: List<Long>): List<User>
suspend fun findByName(name: String): List<User>
suspend fun findByNameLocalUser(name: String): User
suspend fun findByNameAndDomains(names: List<Pair<String, String>>): List<User>
suspend fun findByUrl(url: String): User
suspend fun findByUrls(urls: List<String>): List<User>
suspend fun usernameAlreadyUse(username: String): Boolean
suspend fun createLocalUser(user: UserCreateDto): User
suspend fun createRemoteUser(user: RemoteUserCreateDto): User
suspend fun findFollowersById(id: Long): List<User>
suspend fun addFollowers(id: Long, follower: Long)
}

View File

@ -1,23 +1,15 @@
package dev.usbharu.hideout.service.impl package dev.usbharu.hideout.service.impl
import dev.usbharu.hideout.config.Config 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.UserEntity
import dev.usbharu.hideout.exception.UserNotFoundException import dev.usbharu.hideout.exception.UserNotFoundException
import dev.usbharu.hideout.repository.IUserAuthRepository
import dev.usbharu.hideout.repository.IUserRepository import dev.usbharu.hideout.repository.IUserRepository
import dev.usbharu.hideout.service.IUserAuthService import dev.usbharu.hideout.service.IUserAuthService
import io.ktor.util.* import io.ktor.util.*
import java.security.* import java.security.*
import java.security.interfaces.RSAPrivateKey
import java.security.interfaces.RSAPublicKey
import java.util.* import java.util.*
class UserAuthService( class UserAuthService(
val userRepository: IUserRepository, val userRepository: IUserRepository
val userAuthRepository: IUserAuthRepository
) : IUserAuthService { ) : IUserAuthService {
@ -31,59 +23,15 @@ class UserAuthService(
return true return true
} }
override suspend fun registerAccount(username: String, hash: String) {
val url = "${Config.configData.url}/users/$username"
val registerUser = User(
name = username,
domain = Config.configData.domain,
screenName = username,
description = "",
inbox = "$url/inbox",
outbox = "$url/outbox",
url = url
)
val createdUser = userRepository.create(registerUser)
val keyPair = generateKeyPair()
val privateKey = keyPair.private as RSAPrivateKey
val publicKey = keyPair.public as RSAPublicKey
val userAuthentication = UserAuthentication(
createdUser.id,
hash,
publicKey.toPem(),
privateKey.toPem()
)
userAuthRepository.create(userAuthentication)
}
override suspend fun verifyAccount(username: String, password: String): Boolean { override suspend fun verifyAccount(username: String, password: String): Boolean {
val userEntity = userRepository.findByName(username) val userEntity = userRepository.findByNameAndDomain(username, Config.configData.domain)
?: throw UserNotFoundException("$username was not found") ?: throw UserNotFoundException("$username was not found")
val userAuthEntity = userAuthRepository.findByUserId(userEntity.id) return userEntity.password == hash(password)
?: throw UserNotFoundException("$username auth data was not found")
return userAuthEntity.hash == hash(password)
} }
override suspend fun findByUserId(userId: Long): UserAuthenticationEntity { override suspend fun generateKeyPair(): KeyPair {
return userAuthRepository.findByUserId(userId) ?: throw UserNotFoundException("$userId was not found")
}
override suspend fun findByUsername(username: String): UserAuthenticationEntity {
val userEntity = userRepository.findByName(username) ?: throw UserNotFoundException("$username was not found")
return userAuthRepository.findByUserId(userEntity.id)
?: throw UserNotFoundException("$username auth data was not found")
}
override suspend fun createAccount(userEntity: UserAuthentication): UserAuthenticationEntity {
return userAuthRepository.create(userEntity)
}
private fun generateKeyPair(): KeyPair {
val keyPairGenerator = KeyPairGenerator.getInstance("RSA") val keyPairGenerator = KeyPairGenerator.getInstance("RSA")
keyPairGenerator.initialize(1024) keyPairGenerator.initialize(2048)
return keyPairGenerator.generateKeyPair() return keyPairGenerator.generateKeyPair()
} }
@ -93,14 +41,14 @@ class UserAuthService(
} }
} }
public fun PublicKey.toPem(): String { fun PublicKey.toPem(): String {
return "-----BEGIN PUBLIC KEY-----\n" + return "-----BEGIN PUBLIC KEY-----\n" +
Base64.getEncoder().encodeToString(encoded).chunked(64).joinToString("\n") + Base64.getEncoder().encodeToString(encoded).chunked(64).joinToString("\n") +
"\n-----END PUBLIC KEY-----\n" "\n-----END PUBLIC KEY-----\n"
} }
public fun PrivateKey.toPem(): String { fun PrivateKey.toPem(): String {
return "-----BEGIN PRIVATE KEY-----" + return "-----BEGIN PRIVATE KEY-----\n" +
Base64.getEncoder().encodeToString(encoded).chunked(64).joinToString("\n") + Base64.getEncoder().encodeToString(encoded).chunked(64).joinToString("\n") +
"\n-----END PRIVATE KEY-----\n" "\n-----END PRIVATE KEY-----\n"
} }

View File

@ -1,15 +1,20 @@
package dev.usbharu.hideout.service.impl package dev.usbharu.hideout.service.impl
import dev.usbharu.hideout.domain.model.User import dev.usbharu.hideout.config.Config
import dev.usbharu.hideout.domain.model.UserEntity import dev.usbharu.hideout.domain.model.hideout.dto.RemoteUserCreateDto
import dev.usbharu.hideout.domain.model.hideout.dto.UserCreateDto
import dev.usbharu.hideout.domain.model.hideout.entity.User
import dev.usbharu.hideout.exception.UserNotFoundException import dev.usbharu.hideout.exception.UserNotFoundException
import dev.usbharu.hideout.repository.IUserRepository import dev.usbharu.hideout.repository.IUserRepository
import dev.usbharu.hideout.service.IUserAuthService
import java.lang.Integer.min import java.lang.Integer.min
import java.time.Instant
class UserService(private val userRepository: IUserRepository) { class UserService(private val userRepository: IUserRepository, private val userAuthService: IUserAuthService) :
IUserService {
private val maxLimit = 100 private val maxLimit = 100
suspend fun findAll(limit: Int? = maxLimit, offset: Long? = 0): List<UserEntity> { override suspend fun findAll(limit: Int?, offset: Long?): List<User> {
return userRepository.findAllByLimitAndByOffset( return userRepository.findAllByLimitAndByOffset(
min(limit ?: maxLimit, maxLimit), min(limit ?: maxLimit, maxLimit),
@ -17,40 +22,83 @@ class UserService(private val userRepository: IUserRepository) {
) )
} }
suspend fun findById(id: Long): UserEntity { override suspend fun findById(id: Long): User {
return userRepository.findById(id) ?: throw UserNotFoundException("$id was not found.") return userRepository.findById(id) ?: throw UserNotFoundException("$id was not found.")
} }
suspend fun findByIds(ids: List<Long>): List<UserEntity> { override suspend fun findByIds(ids: List<Long>): List<User> {
return userRepository.findByIds(ids) return userRepository.findByIds(ids)
} }
suspend fun findByName(name: String): UserEntity { override suspend fun findByName(name: String): List<User> {
return userRepository.findByName(name) return userRepository.findByName(name)
}
override suspend fun findByNameLocalUser(name: String): User {
return userRepository.findByNameAndDomain(name, Config.configData.domain)
?: throw UserNotFoundException("$name was not found.") ?: throw UserNotFoundException("$name was not found.")
} }
suspend fun findByNameAndDomains(names: List<Pair<String,String>>): List<UserEntity> { override suspend fun findByNameAndDomains(names: List<Pair<String, String>>): List<User> {
return userRepository.findByNameAndDomains(names) return userRepository.findByNameAndDomains(names)
} }
suspend fun findByUrl(url: String): UserEntity { override suspend fun findByUrl(url: String): User {
return userRepository.findByUrl(url) ?: throw UserNotFoundException("$url was not found.") return userRepository.findByUrl(url) ?: throw UserNotFoundException("$url was not found.")
} }
suspend fun findByUrls(urls: List<String>): List<UserEntity> { override suspend fun findByUrls(urls: List<String>): List<User> {
return userRepository.findByUrls(urls) return userRepository.findByUrls(urls)
} }
suspend fun create(user: User): UserEntity { override suspend fun usernameAlreadyUse(username: String): Boolean {
return userRepository.create(user) val findByNameAndDomain = userRepository.findByNameAndDomain(username, Config.configData.domain)
return findByNameAndDomain != null
} }
suspend fun findFollowersById(id: Long): List<UserEntity> { override suspend fun createLocalUser(user: UserCreateDto): User {
val nextId = userRepository.nextId()
val HashedPassword = userAuthService.hash(user.password)
val keyPair = userAuthService.generateKeyPair()
val userEntity = User(
id = nextId,
name = user.name,
domain = Config.configData.domain,
screenName = user.screenName,
description = user.description,
password = HashedPassword,
inbox = "${Config.configData.url}/users/${user.name}/inbox",
outbox = "${Config.configData.url}/users/${user.name}/outbox",
url = "${Config.configData.url}/users/${user.name}",
publicKey = keyPair.public.toPem(),
privateKey = keyPair.private.toPem(),
Instant.now()
)
return userRepository.save(userEntity)
}
override suspend fun createRemoteUser(user: RemoteUserCreateDto): User {
val nextId = userRepository.nextId()
val userEntity = User(
id = nextId,
name = user.name,
domain = user.domain,
screenName = user.screenName,
description = user.description,
inbox = user.inbox,
outbox = user.outbox,
url = user.url,
publicKey = user.publicKey,
createdAt = Instant.now()
)
return userRepository.save(userEntity)
}
override suspend fun findFollowersById(id: Long): List<User> {
return userRepository.findFollowersById(id) return userRepository.findFollowersById(id)
} }
suspend fun addFollowers(id: Long, follower: Long) { override suspend fun addFollowers(id: Long, follower: Long) {
return userRepository.createFollower(id, follower) return userRepository.createFollower(id, follower)
} }

View File

@ -1,15 +1,16 @@
package dev.usbharu.hideout.service.signature package dev.usbharu.hideout.service.signature
import dev.usbharu.hideout.plugins.KtorKeyMap import dev.usbharu.hideout.plugins.KtorKeyMap
import dev.usbharu.hideout.repository.IUserRepository
import dev.usbharu.hideout.service.IUserAuthService import dev.usbharu.hideout.service.IUserAuthService
import io.ktor.http.* import io.ktor.http.*
import tech.barbero.http.message.signing.HttpMessage import tech.barbero.http.message.signing.HttpMessage
import tech.barbero.http.message.signing.SignatureHeaderVerifier import tech.barbero.http.message.signing.SignatureHeaderVerifier
class HttpSignatureVerifyServiceImpl(private val userAuthService: IUserAuthService) : HttpSignatureVerifyService { class HttpSignatureVerifyServiceImpl(private val userAuthService: IUserRepository) : HttpSignatureVerifyService {
override fun verify(headers: Headers): Boolean { override fun verify(headers: Headers): Boolean {
val build = SignatureHeaderVerifier.builder().keyMap(KtorKeyMap(userAuthService)).build() val build = SignatureHeaderVerifier.builder().keyMap(KtorKeyMap(userAuthService)).build()
return true; return true
build.verify(object : HttpMessage { build.verify(object : HttpMessage {
override fun headerValues(name: String?): MutableList<String> { override fun headerValues(name: String?): MutableList<String> {
return name?.let { headers.getAll(it) }?.toMutableList() ?: mutableListOf() return name?.let { headers.getAll(it) }?.toMutableList() ?: mutableListOf()

View File

@ -30,6 +30,10 @@ object HttpUtil {
get() = ContentType("application", "activity+json") get() = ContentType("application", "activity+json")
val ContentType.Application.JsonLd: ContentType val ContentType.Application.JsonLd: ContentType
get() = ContentType("application", "ld+json", listOf(HeaderValueParam("profile", "https://www.w3.org/ns/activitystreams"))) get() = ContentType(
"application",
"ld+json",
listOf(HeaderValueParam("profile", "https://www.w3.org/ns/activitystreams"))
)
// fun // fun
} }

View File

@ -14,7 +14,6 @@ import org.jetbrains.exposed.dao.id.LongIdTable
import org.jetbrains.exposed.sql.* import org.jetbrains.exposed.sql.*
import org.jetbrains.exposed.sql.SqlExpressionBuilder.eq import org.jetbrains.exposed.sql.SqlExpressionBuilder.eq
import org.jetbrains.exposed.sql.SqlExpressionBuilder.inList import org.jetbrains.exposed.sql.SqlExpressionBuilder.inList
import org.jetbrains.exposed.sql.SqlExpressionBuilder.isNull
import org.jetbrains.exposed.sql.SqlExpressionBuilder.plus import org.jetbrains.exposed.sql.SqlExpressionBuilder.plus
import org.jetbrains.exposed.sql.transactions.experimental.newSuspendedTransaction import org.jetbrains.exposed.sql.transactions.experimental.newSuspendedTransaction
import org.jetbrains.exposed.sql.transactions.transaction import org.jetbrains.exposed.sql.transactions.transaction

View File

@ -1,5 +1,5 @@
import {Component} from "solid-js"; import {Component} from "solid-js";
export const App:Component = () => { export const App: Component = () => {
return (<p>aaa</p>) return (<p>aaa</p>)
} }

View File

@ -1,10 +1,10 @@
<!DOCTYPE html> <!DOCTYPE html>
<html lang="en"> <html lang="en">
<head> <head>
<meta charset="utf-8" /> <meta charset="utf-8"/>
<meta name="viewport" content="width=device-width, initial-scale=1" /> <meta content="width=device-width, initial-scale=1" name="viewport"/>
<meta name="theme-color" content="#000000" /> <meta content="#000000" name="theme-color"/>
<title>Solid App</title> <title>Solid App</title>
</head> </head>
<body> <body>
<noscript>You need to enable JavaScript to run this app.</noscript> <noscript>You need to enable JavaScript to run this app.</noscript>

View File

@ -1,13 +1,8 @@
package dev.usbharu.hideout.plugins package dev.usbharu.hideout.plugins
import dev.usbharu.hideout.domain.model.ap.JsonLd import dev.usbharu.hideout.domain.model.ap.JsonLd
import dev.usbharu.hideout.domain.model.User import dev.usbharu.hideout.domain.model.hideout.entity.User
import dev.usbharu.hideout.domain.model.UserAuthentication
import dev.usbharu.hideout.domain.model.UserAuthenticationEntity
import dev.usbharu.hideout.domain.model.UserEntity
import dev.usbharu.hideout.repository.IUserAuthRepository
import dev.usbharu.hideout.repository.IUserRepository import dev.usbharu.hideout.repository.IUserRepository
import dev.usbharu.hideout.service.impl.UserAuthService
import dev.usbharu.hideout.service.impl.toPem import dev.usbharu.hideout.service.impl.toPem
import io.ktor.client.* import io.ktor.client.*
import io.ktor.client.engine.mock.* import io.ktor.client.engine.mock.*
@ -15,43 +10,62 @@ import io.ktor.client.plugins.logging.*
import kotlinx.coroutines.runBlocking import kotlinx.coroutines.runBlocking
import org.junit.jupiter.api.Test import org.junit.jupiter.api.Test
import java.security.KeyPairGenerator import java.security.KeyPairGenerator
import java.security.interfaces.RSAPrivateKey import java.time.Instant
import java.security.interfaces.RSAPublicKey
class ActivityPubKtTest { class ActivityPubKtTest {
@Test @Test
fun HttpSignTest(): Unit = runBlocking { fun HttpSignTest(): Unit = runBlocking {
val ktorKeyMap = KtorKeyMap(UserAuthService(object : IUserRepository { val ktorKeyMap = KtorKeyMap(object : IUserRepository {
override suspend fun create(user: User): UserEntity { override suspend fun save(user: User): User {
TODO("Not yet implemented") TODO("Not yet implemented")
} }
override suspend fun findById(id: Long): UserEntity? { override suspend fun findById(id: Long): User? {
TODO("Not yet implemented") TODO("Not yet implemented")
} }
override suspend fun findByIds(ids: List<Long>): List<UserEntity> { override suspend fun findByIds(ids: List<Long>): List<User> {
TODO("Not yet implemented") TODO("Not yet implemented")
} }
override suspend fun findByName(name: String): UserEntity? { override suspend fun findByName(name: String): List<User> {
return UserEntity(1, "test", "localhost", "test", "","","","") TODO()
} }
override suspend fun findByNameAndDomains(names: List<Pair<String, String>>): List<UserEntity> { override suspend fun findByNameAndDomain(name: String, domain: String): User? {
val keyPairGenerator = KeyPairGenerator.getInstance("RSA")
keyPairGenerator.initialize(1024)
val generateKeyPair = keyPairGenerator.generateKeyPair()
return User(
1,
"test",
"localhost",
"test",
"",
"",
"",
"",
"",
"",
generateKeyPair.private.toPem(),
Instant.now()
)
}
override suspend fun findByDomain(domain: String): List<User> {
TODO("Not yet implemented") TODO("Not yet implemented")
} }
override suspend fun findByUrl(url: String): UserEntity? { override suspend fun findByNameAndDomains(names: List<Pair<String, String>>): List<User> {
TODO("Not yet implemented") TODO("Not yet implemented")
} }
override suspend fun findByUrls(urls: List<String>): List<UserEntity> { override suspend fun findByUrl(url: String): User? {
TODO("Not yet implemented") TODO("Not yet implemented")
} }
override suspend fun update(userEntity: UserEntity) { override suspend fun findByUrls(urls: List<String>): List<User> {
TODO("Not yet implemented") TODO("Not yet implemented")
} }
@ -63,7 +77,7 @@ class ActivityPubKtTest {
TODO("Not yet implemented") TODO("Not yet implemented")
} }
override suspend fun findAllByLimitAndByOffset(limit: Int, offset: Long): List<UserEntity> { override suspend fun findAllByLimitAndByOffset(limit: Int, offset: Long): List<User> {
TODO("Not yet implemented") TODO("Not yet implemented")
} }
@ -75,37 +89,15 @@ class ActivityPubKtTest {
TODO("Not yet implemented") TODO("Not yet implemented")
} }
override suspend fun findFollowersById(id: Long): List<UserEntity> { override suspend fun findFollowersById(id: Long): List<User> {
TODO("Not yet implemented") TODO("Not yet implemented")
} }
}, object : IUserAuthRepository { override suspend fun nextId(): Long {
override suspend fun create(userAuthentication: UserAuthentication): UserAuthenticationEntity {
TODO("Not yet implemented") TODO("Not yet implemented")
} }
override suspend fun findById(id: Long): UserAuthenticationEntity? { })
TODO("Not yet implemented")
}
override suspend fun update(userAuthenticationEntity: UserAuthenticationEntity) {
TODO("Not yet implemented")
}
override suspend fun delete(id: Long) {
TODO("Not yet implemented")
}
override suspend fun findByUserId(id: Long): UserAuthenticationEntity? {
val keyPairGenerator = KeyPairGenerator.getInstance("RSA")
keyPairGenerator.initialize(1024)
val generateKeyPair = keyPairGenerator.generateKeyPair()
return UserAuthenticationEntity(
1, 1, "test", (generateKeyPair.public as RSAPublicKey).toPem(),
(generateKeyPair.private as RSAPrivateKey).toPem()
)
}
}))
val httpClient = HttpClient(MockEngine { httpRequestData -> val httpClient = HttpClient(MockEngine { httpRequestData ->
respondOk() respondOk()

View File

@ -1,52 +1,67 @@
package dev.usbharu.hideout.plugins package dev.usbharu.hideout.plugins
import dev.usbharu.hideout.domain.model.User import dev.usbharu.hideout.domain.model.hideout.entity.User
import dev.usbharu.hideout.domain.model.UserAuthentication
import dev.usbharu.hideout.domain.model.UserAuthenticationEntity
import dev.usbharu.hideout.domain.model.UserEntity
import dev.usbharu.hideout.repository.IUserAuthRepository
import dev.usbharu.hideout.repository.IUserRepository import dev.usbharu.hideout.repository.IUserRepository
import dev.usbharu.hideout.service.impl.UserAuthService
import dev.usbharu.hideout.service.impl.toPem import dev.usbharu.hideout.service.impl.toPem
import org.junit.jupiter.api.Test import org.junit.jupiter.api.Test
import java.security.KeyPairGenerator import java.security.KeyPairGenerator
import java.security.interfaces.RSAPrivateKey import java.time.Instant
import java.security.interfaces.RSAPublicKey
class KtorKeyMapTest { class KtorKeyMapTest {
@Test @Test
fun getPrivateKey() { fun getPrivateKey() {
val ktorKeyMap = KtorKeyMap(UserAuthService(object : IUserRepository { val ktorKeyMap = KtorKeyMap(object : IUserRepository {
override suspend fun create(user: User): UserEntity { override suspend fun save(user: User): User {
TODO("Not yet implemented") TODO("Not yet implemented")
} }
override suspend fun findById(id: Long): UserEntity? { override suspend fun findById(id: Long): User? {
TODO("Not yet implemented") TODO("Not yet implemented")
} }
override suspend fun findByIds(ids: List<Long>): List<UserEntity> { override suspend fun findByIds(ids: List<Long>): List<User> {
TODO("Not yet implemented") TODO("Not yet implemented")
} }
override suspend fun findByName(name: String): UserEntity? { override suspend fun findByName(name: String): List<User> {
return UserEntity(1, "test", "localhost", "test", "","","","") TODO()
} }
override suspend fun findByNameAndDomains(names: List<Pair<String, String>>): List<UserEntity> { override suspend fun findByNameAndDomain(name: String, domain: String): User? {
val keyPairGenerator = KeyPairGenerator.getInstance("RSA")
keyPairGenerator.initialize(1024)
val generateKeyPair = keyPairGenerator.generateKeyPair()
return User(
1,
"test",
"localhost",
"test",
"",
"",
"",
"",
"",
"",
generateKeyPair.private.toPem(),
createdAt = Instant.now()
)
}
override suspend fun findByDomain(domain: String): List<User> {
TODO("Not yet implemented") TODO("Not yet implemented")
} }
override suspend fun findByUrl(url: String): UserEntity? { override suspend fun findByNameAndDomains(names: List<Pair<String, String>>): List<User> {
TODO("Not yet implemented") TODO("Not yet implemented")
} }
override suspend fun findByUrls(urls: List<String>): List<UserEntity> { override suspend fun findByUrl(url: String): User? {
TODO("Not yet implemented") TODO("Not yet implemented")
} }
override suspend fun update(userEntity: UserEntity) { override suspend fun findByUrls(urls: List<String>): List<User> {
TODO("Not yet implemented") TODO("Not yet implemented")
} }
@ -58,7 +73,7 @@ class KtorKeyMapTest {
TODO("Not yet implemented") TODO("Not yet implemented")
} }
override suspend fun findAllByLimitAndByOffset(limit: Int, offset: Long): List<UserEntity> { override suspend fun findAllByLimitAndByOffset(limit: Int, offset: Long): List<User> {
TODO("Not yet implemented") TODO("Not yet implemented")
} }
@ -70,37 +85,15 @@ class KtorKeyMapTest {
TODO("Not yet implemented") TODO("Not yet implemented")
} }
override suspend fun findFollowersById(id: Long): List<UserEntity> { override suspend fun findFollowersById(id: Long): List<User> {
TODO("Not yet implemented") TODO("Not yet implemented")
} }
}, object : IUserAuthRepository { override suspend fun nextId(): Long {
override suspend fun create(userAuthentication: UserAuthentication): UserAuthenticationEntity {
TODO("Not yet implemented") TODO("Not yet implemented")
} }
override suspend fun findById(id: Long): UserAuthenticationEntity? { })
TODO("Not yet implemented")
}
override suspend fun update(userAuthenticationEntity: UserAuthenticationEntity) {
TODO("Not yet implemented")
}
override suspend fun delete(id: Long) {
TODO("Not yet implemented")
}
override suspend fun findByUserId(id: Long): UserAuthenticationEntity? {
val keyPairGenerator = KeyPairGenerator.getInstance("RSA")
keyPairGenerator.initialize(1024)
val generateKeyPair = keyPairGenerator.generateKeyPair()
return UserAuthenticationEntity(
1, 1, "test", (generateKeyPair.public as RSAPublicKey).toPem(),
(generateKeyPair.private as RSAPrivateKey).toPem()
)
}
}))
ktorKeyMap.getPrivateKey("test") ktorKeyMap.getPrivateKey("test")
} }

View File

@ -2,9 +2,8 @@
package dev.usbharu.hideout.repository package dev.usbharu.hideout.repository
import dev.usbharu.hideout.domain.model.User import dev.usbharu.hideout.domain.model.hideout.entity.User
import dev.usbharu.hideout.domain.model.Users import dev.usbharu.hideout.service.IdGenerateService
import dev.usbharu.hideout.domain.model.UsersFollowers
import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.test.runTest import kotlinx.coroutines.test.runTest
import org.jetbrains.exposed.sql.Database import org.jetbrains.exposed.sql.Database
@ -15,6 +14,9 @@ import org.junit.jupiter.api.AfterEach
import org.junit.jupiter.api.Assertions.assertIterableEquals import org.junit.jupiter.api.Assertions.assertIterableEquals
import org.junit.jupiter.api.BeforeEach import org.junit.jupiter.api.BeforeEach
import org.junit.jupiter.api.Test import org.junit.jupiter.api.Test
import java.time.Clock
import java.time.Instant
import java.time.ZoneId
class UserRepositoryTest { class UserRepositoryTest {
@ -41,38 +43,54 @@ class UserRepositoryTest {
@Test @Test
fun `findFollowersById フォロワー一覧を取得`() = runTest { fun `findFollowersById フォロワー一覧を取得`() = runTest {
val userRepository = UserRepository(db) val userRepository = UserRepository(db, object : IdGenerateService {
val user = userRepository.create( override suspend fun generateId(): Long {
TODO("Not yet implemented")
}
})
val user = userRepository.save(
User( User(
"test", id = 0L,
"example.com", name = "test",
"testUser", domain = "example.com",
"This user is test user.", screenName = "testUser",
"https://example.com/inbox", description = "This user is test user.",
"https://example.com/outbox", password = "https://example.com/inbox",
"https://example.com" inbox = "",
outbox = "https://example.com/outbox",
url = "https://example.com",
publicKey = "",
createdAt = Instant.now(Clock.tickMillis(ZoneId.systemDefault()))
) )
) )
val follower = userRepository.create( val follower = userRepository.save(
User( User(
"follower", id = 1L,
"follower.example.com", name = "follower",
"followerUser", domain = "follower.example.com",
"This user is follower user.", screenName = "followerUser",
"https://follower.example.com/inbox", description = "This user is follower user.",
"https://follower.example.com/outbox", password = "",
"https://follower.example.com" inbox = "https://follower.example.com/inbox",
outbox = "https://follower.example.com/outbox",
url = "https://follower.example.com",
publicKey = "",
createdAt = Instant.now(Clock.tickMillis(ZoneId.systemDefault()))
) )
) )
val follower2 = userRepository.create( val follower2 = userRepository.save(
User( User(
"follower2", id = 3L,
"follower2.example.com", name = "follower2",
"followerUser2", domain = "follower2.example.com",
"This user is follower user 2.", screenName = "followerUser2",
"https://follower2.example.com/inbox", description = "This user is follower user 2.",
"https://follower2.example.com/outbox", password = "",
"https://follower2.example.com" inbox = "https://follower2.example.com/inbox",
outbox = "https://follower2.example.com/outbox",
url = "https://follower2.example.com",
publicKey = "",
createdAt = Instant.now(Clock.tickMillis(ZoneId.systemDefault()))
) )
) )
userRepository.createFollower(user.id, follower.id) userRepository.createFollower(user.id, follower.id)
@ -85,27 +103,37 @@ class UserRepositoryTest {
@Test @Test
fun `createFollower フォロワー追加`() = runTest { fun `createFollower フォロワー追加`() = runTest {
val userRepository = UserRepository(db) val userRepository = UserRepository(db, object : IdGenerateService {
val user = userRepository.create( override suspend fun generateId(): Long {
User( TODO("Not yet implemented")
}
})
val user = userRepository.save(
User(0L,
"test", "test",
"example.com", "example.com",
"testUser", "testUser",
"This user is test user.", "This user is test user.",
"https://example.com/inbox", "https://example.com/inbox",
"",
"https://example.com/outbox", "https://example.com/outbox",
"https://example.com" "https://example.com",
publicKey = "",
createdAt = Instant.now()
) )
) )
val follower = userRepository.create( val follower = userRepository.save(
User( User(1L,
"follower", "follower",
"follower.example.com", "follower.example.com",
"followerUser", "followerUser",
"This user is follower user.", "This user is follower user.",
"",
"https://follower.example.com/inbox", "https://follower.example.com/inbox",
"https://follower.example.com/outbox", "https://follower.example.com/outbox",
"https://follower.example.com" "https://follower.example.com",
publicKey = "",
createdAt = Instant.now()
) )
) )
userRepository.createFollower(user.id, follower.id) userRepository.createFollower(user.id, follower.id)

View File

@ -6,6 +6,7 @@ import dev.usbharu.hideout.plugins.configureSerialization
import dev.usbharu.hideout.plugins.configureStatusPages import dev.usbharu.hideout.plugins.configureStatusPages
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.ActivityPubUserService
import dev.usbharu.hideout.service.impl.IUserService
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.client.request.* import io.ktor.client.request.*
@ -45,7 +46,7 @@ class InboxRoutingKtTest {
val activityPubService = mock<ActivityPubService>{ val activityPubService = mock<ActivityPubService>{
on { parseActivity(any()) } doThrow JsonParseException() on { parseActivity(any()) } doThrow JsonParseException()
} }
val userService = mock<UserService>() val userService = mock<IUserService>()
val activityPubUserService = mock<ActivityPubUserService>() val activityPubUserService = mock<ActivityPubUserService>()
application { application {
configureStatusPages() configureStatusPages()
@ -82,7 +83,7 @@ class InboxRoutingKtTest {
val activityPubService = mock<ActivityPubService>{ val activityPubService = mock<ActivityPubService>{
on { parseActivity(any()) } doThrow JsonParseException() on { parseActivity(any()) } doThrow JsonParseException()
} }
val userService = mock<UserService>() val userService = mock<IUserService>()
val activityPubUserService = mock<ActivityPubUserService>() val activityPubUserService = mock<ActivityPubUserService>()
application { application {
configureStatusPages() configureStatusPages()

View File

@ -9,13 +9,15 @@ import com.fasterxml.jackson.module.kotlin.readValue
import dev.usbharu.hideout.domain.model.ap.Image import dev.usbharu.hideout.domain.model.ap.Image
import dev.usbharu.hideout.domain.model.ap.Key import dev.usbharu.hideout.domain.model.ap.Key
import dev.usbharu.hideout.domain.model.ap.Person import dev.usbharu.hideout.domain.model.ap.Person
import dev.usbharu.hideout.domain.model.hideout.entity.User
import dev.usbharu.hideout.plugins.configureRouting import dev.usbharu.hideout.plugins.configureRouting
import dev.usbharu.hideout.plugins.configureSerialization import dev.usbharu.hideout.plugins.configureSerialization
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.ActivityPubUserService
import dev.usbharu.hideout.service.impl.UserService import dev.usbharu.hideout.service.impl.IUserService
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 dev.usbharu.hideout.util.HttpUtil.JsonLd
import io.ktor.client.request.* import io.ktor.client.request.*
import io.ktor.client.statement.* import io.ktor.client.statement.*
import io.ktor.http.* import io.ktor.http.*
@ -24,8 +26,11 @@ import io.ktor.server.testing.*
import org.junit.jupiter.api.Test import org.junit.jupiter.api.Test
import org.mockito.ArgumentMatchers.anyString import org.mockito.ArgumentMatchers.anyString
import org.mockito.kotlin.doReturn import org.mockito.kotlin.doReturn
import org.mockito.kotlin.eq
import org.mockito.kotlin.mock import org.mockito.kotlin.mock
import java.time.Instant
import kotlin.test.assertEquals import kotlin.test.assertEquals
import kotlin.test.assertTrue
class UsersAPTest { class UsersAPTest {
@ -62,7 +67,7 @@ class UsersAPTest {
val httpSignatureVerifyService = mock<HttpSignatureVerifyService> {} val httpSignatureVerifyService = mock<HttpSignatureVerifyService> {}
val activityPubService = mock<ActivityPubService> {} val activityPubService = mock<ActivityPubService> {}
val userService = mock<UserService> {} val userService = mock<IUserService> {}
val activityPubUserService = mock<ActivityPubUserService> { val activityPubUserService = mock<ActivityPubUserService> {
onBlocking { getPersonByName(anyString()) } doReturn person onBlocking { getPersonByName(anyString()) } doReturn person
@ -70,7 +75,13 @@ class UsersAPTest {
application { application {
configureSerialization() configureSerialization()
configureRouting(httpSignatureVerifyService, activityPubService, userService, activityPubUserService,mock()) configureRouting(
httpSignatureVerifyService,
activityPubService,
userService,
activityPubUserService,
mock()
)
} }
client.get("/users/test") { client.get("/users/test") {
accept(ContentType.Application.Activity) accept(ContentType.Application.Activity)
@ -88,4 +99,121 @@ class UsersAPTest {
assertEquals(person, readValue) assertEquals(person, readValue)
} }
} }
@Test()
// @Disabled
fun `ユーザのURLにAcceptヘッダーをActivityとJson-LDにしてアクセスしたときPersonが返ってくる`() = testApplication {
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")
val httpSignatureVerifyService = mock<HttpSignatureVerifyService> {}
val activityPubService = mock<ActivityPubService> {}
val userService = mock<IUserService> {}
val activityPubUserService = mock<ActivityPubUserService> {
onBlocking { getPersonByName(anyString()) } doReturn person
}
application {
configureSerialization()
configureRouting(
httpSignatureVerifyService,
activityPubService,
userService,
activityPubUserService,
mock()
)
}
client.get("/users/test") {
accept(ContentType.Application.JsonLd)
accept(ContentType.Application.Activity)
}.let {
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)
assertEquals(person, readValue)
}
}
@Test
// @Disabled
fun contentType_Test() {
assertTrue(ContentType.Application.Activity.match("application/activity+json"))
val listOf = listOf(ContentType.Application.JsonLd, ContentType.Application.Activity)
assertTrue(listOf.find { contentType ->
contentType.match("application/ld+json; profile=\"https://www.w3.org/ns/activitystreams\"")
}.let { it != null })
assertTrue(ContentType.Application.JsonLd.match("application/ld+json; profile=\"https://www.w3.org/ns/activitystreams\""))
}
@Test
fun ユーザーのURLにAcceptヘッダーをhtmlにしてアクセスしたときはただの文字を返す() = testApplication {
environment {
config = ApplicationConfig("empty.conf")
}
val userService = mock<IUserService> {
onBlocking { findByNameLocalUser(eq("test")) } doReturn User(
1L,
"test",
"example.com",
"test",
"",
"hashedPassword",
"https://example.com/inbox",
"https://example.com/outbox",
"https://example.com",
"-----BEGIN PUBLIC KEY-----...-----END PUBLIC KEY-----",
"-----BEGIN PRIVATE KEY-----...-----END PRIVATE KEY-----",
Instant.now()
)
}
application {
configureRouting(
mock(),
mock(),
userService,
mock(),
mock()
)
}
client.get("/users/test") {
accept(ContentType.Text.Html)
}.let {
assertEquals(HttpStatusCode.OK, it.status)
assertTrue(it.contentType()?.match(ContentType.Text.Plain) == true)
}
}
} }

View File

@ -6,10 +6,10 @@ package dev.usbharu.hideout.service.activitypub
import com.fasterxml.jackson.module.kotlin.readValue import com.fasterxml.jackson.module.kotlin.readValue
import dev.usbharu.hideout.config.Config import dev.usbharu.hideout.config.Config
import dev.usbharu.hideout.config.ConfigData import dev.usbharu.hideout.config.ConfigData
import dev.usbharu.hideout.domain.model.UserEntity
import dev.usbharu.hideout.domain.model.ap.* import dev.usbharu.hideout.domain.model.ap.*
import dev.usbharu.hideout.domain.model.hideout.entity.User
import dev.usbharu.hideout.domain.model.job.ReceiveFollowJob import dev.usbharu.hideout.domain.model.job.ReceiveFollowJob
import dev.usbharu.hideout.service.impl.UserService import dev.usbharu.hideout.service.impl.IUserService
import dev.usbharu.hideout.service.job.JobQueueParentService import dev.usbharu.hideout.service.job.JobQueueParentService
import io.ktor.client.* import io.ktor.client.*
import io.ktor.client.engine.mock.* import io.ktor.client.engine.mock.*
@ -23,6 +23,7 @@ import org.junit.jupiter.api.Test
import org.mockito.ArgumentMatchers.anyString import org.mockito.ArgumentMatchers.anyString
import org.mockito.kotlin.* import org.mockito.kotlin.*
import utils.JsonObjectMapper import utils.JsonObjectMapper
import java.time.Instant
class ActivityPubFollowServiceImplTest { class ActivityPubFollowServiceImplTest {
@Test @Test
@ -49,8 +50,9 @@ class ActivityPubFollowServiceImplTest {
val follow = scheduleContext.props.props[ReceiveFollowJob.follow.name] val follow = scheduleContext.props.props[ReceiveFollowJob.follow.name]
assertEquals("https://follower.example.com", actor) assertEquals("https://follower.example.com", actor)
assertEquals("https://example.com", targetActor) assertEquals("https://example.com", targetActor)
//language=JSON
assertEquals( assertEquals(
"""{"type":"Follow","name":"Follow","object":"https://example.com","actor":"https://follower.example.com","@context":null}""", """{"type":"Follow","name":"Follow","actor":"https://follower.example.com","object":"https://example.com","@context":null}""",
follow follow
) )
} }
@ -84,11 +86,11 @@ class ActivityPubFollowServiceImplTest {
) )
val activityPubUserService = mock<ActivityPubUserService> { val activityPubUserService = mock<ActivityPubUserService> {
onBlocking { fetchPerson(anyString()) } doReturn person onBlocking { fetchPerson(anyString(), any()) } doReturn person
} }
val userService = mock<UserService> { val userService = mock<IUserService> {
onBlocking { findByUrls(any()) } doReturn listOf( onBlocking { findByUrls(any()) } doReturn listOf(
UserEntity( User(
id = 1L, id = 1L,
name = "test", name = "test",
domain = "example.com", domain = "example.com",
@ -96,9 +98,11 @@ class ActivityPubFollowServiceImplTest {
description = "This user is test user.", description = "This user is test user.",
inbox = "https://example.com/inbox", inbox = "https://example.com/inbox",
outbox = "https://example.com/outbox", outbox = "https://example.com/outbox",
url = "https://example.com" url = "https://example.com",
publicKey = "",
createdAt = Instant.now()
), ),
UserEntity( User(
id = 2L, id = 2L,
name = "follower", name = "follower",
domain = "follower.example.com", domain = "follower.example.com",
@ -106,7 +110,9 @@ class ActivityPubFollowServiceImplTest {
description = "This user is test follower user.", description = "This user is test follower user.",
inbox = "https://follower.example.com/inbox", inbox = "https://follower.example.com/inbox",
outbox = "https://follower.example.com/outbox", outbox = "https://follower.example.com/outbox",
url = "https://follower.example.com" url = "https://follower.example.com",
publicKey = "",
createdAt = Instant.now()
) )
) )
onBlocking { addFollowers(any(), any()) } doReturn Unit onBlocking { addFollowers(any(), any()) } doReturn Unit

View File

@ -6,9 +6,9 @@ package dev.usbharu.hideout.service.activitypub
import dev.usbharu.hideout.config.Config import dev.usbharu.hideout.config.Config
import dev.usbharu.hideout.config.ConfigData import dev.usbharu.hideout.config.ConfigData
import dev.usbharu.hideout.domain.model.PostEntity import dev.usbharu.hideout.domain.model.PostEntity
import dev.usbharu.hideout.domain.model.UserEntity import dev.usbharu.hideout.domain.model.hideout.entity.User
import dev.usbharu.hideout.domain.model.job.DeliverPostJob import dev.usbharu.hideout.domain.model.job.DeliverPostJob
import dev.usbharu.hideout.service.impl.UserService import dev.usbharu.hideout.service.impl.IUserService
import dev.usbharu.hideout.service.job.JobQueueParentService import dev.usbharu.hideout.service.job.JobQueueParentService
import io.ktor.client.* import io.ktor.client.*
import io.ktor.client.engine.mock.* import io.ktor.client.engine.mock.*
@ -20,13 +20,14 @@ import org.junit.jupiter.api.Test
import org.mockito.Mockito.eq import org.mockito.Mockito.eq
import org.mockito.kotlin.* import org.mockito.kotlin.*
import utils.JsonObjectMapper import utils.JsonObjectMapper
import java.time.Instant
import kotlin.test.assertEquals import kotlin.test.assertEquals
class ActivityPubNoteServiceImplTest { class ActivityPubNoteServiceImplTest {
@Test @Test
fun `createPost 新しい投稿`() = runTest { fun `createPost 新しい投稿`() = runTest {
val followers = listOf<UserEntity>( val followers = listOf<User>(
UserEntity( User(
2L, 2L,
"follower", "follower",
"follower.example.com", "follower.example.com",
@ -34,9 +35,12 @@ class ActivityPubNoteServiceImplTest {
"test follower user", "test follower user",
"https://follower.example.com/inbox", "https://follower.example.com/inbox",
"https://follower.example.com/outbox", "https://follower.example.com/outbox",
"https://follower.example.com" "https://follower.example.com",
"",
publicKey = "",
createdAt = Instant.now()
), ),
UserEntity( User(
3L, 3L,
"follower2", "follower2",
"follower2.example.com", "follower2.example.com",
@ -44,11 +48,14 @@ class ActivityPubNoteServiceImplTest {
"test follower2 user", "test follower2 user",
"https://follower2.example.com/inbox", "https://follower2.example.com/inbox",
"https://follower2.example.com/outbox", "https://follower2.example.com/outbox",
"https:.//follower2.example.com" "https://follower2.example.com",
"",
publicKey = "",
createdAt = Instant.now()
) )
) )
val userService = mock<UserService> { val userService = mock<IUserService> {
onBlocking { findById(eq(1L)) } doReturn UserEntity( onBlocking { findById(eq(1L)) } doReturn User(
1L, 1L,
"test", "test",
"example.com", "example.com",
@ -56,7 +63,10 @@ class ActivityPubNoteServiceImplTest {
"test user", "test user",
"https://example.com/inbox", "https://example.com/inbox",
"https://example.com/outbox", "https://example.com/outbox",
"https:.//example.com" "https:.//example.com",
"",
publicKey = "",
createdAt = Instant.now()
) )
onBlocking { findFollowersById(eq(1L)) } doReturn followers onBlocking { findFollowersById(eq(1L)) } doReturn followers
} }

View File

@ -0,0 +1,88 @@
@file:OptIn(ExperimentalCoroutinesApi::class)
package dev.usbharu.hideout.service.impl
import dev.usbharu.hideout.config.Config
import dev.usbharu.hideout.config.ConfigData
import dev.usbharu.hideout.domain.model.hideout.dto.RemoteUserCreateDto
import dev.usbharu.hideout.domain.model.hideout.dto.UserCreateDto
import dev.usbharu.hideout.repository.IUserRepository
import dev.usbharu.hideout.service.IUserAuthService
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.test.runTest
import org.junit.jupiter.api.Test
import org.mockito.ArgumentMatchers.anyString
import org.mockito.kotlin.*
import java.security.KeyPairGenerator
import kotlin.test.assertEquals
import kotlin.test.assertNull
class UserServiceTest{
@Test
fun `createLocalUser ローカルユーザーを作成できる`() = runTest {
Config.configData = ConfigData(domain = "example.com", url = "https://example.com")
val userRepository = mock<IUserRepository> {
onBlocking { nextId() } doReturn 110001L
}
val generateKeyPair = KeyPairGenerator.getInstance("RSA").generateKeyPair()
val userAuthService = mock<IUserAuthService> {
onBlocking { hash(anyString()) } doReturn "hashedPassword"
onBlocking { generateKeyPair() } doReturn generateKeyPair
}
val userService = UserService(userRepository, userAuthService)
userService.createLocalUser(UserCreateDto("test", "testUser", "XXXXXXXXXXXXX", "test"))
verify(userRepository, times(1)).save(any())
argumentCaptor<dev.usbharu.hideout.domain.model.hideout.entity.User> {
verify(userRepository, times(1)).save(capture())
assertEquals("test", firstValue.name)
assertEquals("testUser", firstValue.screenName)
assertEquals("XXXXXXXXXXXXX", firstValue.description)
assertEquals("hashedPassword", firstValue.password)
assertEquals(110001L, firstValue.id)
assertEquals("https://example.com/users/test", firstValue.url)
assertEquals("example.com", firstValue.domain)
assertEquals("https://example.com/users/test/inbox", firstValue.inbox)
assertEquals("https://example.com/users/test/outbox", firstValue.outbox)
assertEquals(generateKeyPair.public.toPem(),firstValue.publicKey)
assertEquals(generateKeyPair.private.toPem(),firstValue.privateKey)
}
}
@Test
fun `createRemoteUser リモートユーザーを作成できる`() = runTest {
Config.configData = ConfigData(domain = "example.com", url = "https://example.com")
val userRepository = mock<IUserRepository>{
onBlocking { nextId() } doReturn 113345L
}
val userService = UserService(userRepository,mock())
val user = RemoteUserCreateDto(
"test",
"example.com",
"testUser",
"test user",
"https://example.com/inbox",
"https://example.com/outbox",
"https://example.com",
"-----BEGIN PUBLIC KEY-----...-----END PUBLIC KEY-----"
)
userService.createRemoteUser(user)
verify(userRepository, times(1)).save(any())
argumentCaptor<dev.usbharu.hideout.domain.model.hideout.entity.User> {
verify(userRepository, times(1)).save(capture())
assertEquals("test", firstValue.name)
assertEquals("testUser", firstValue.screenName)
assertEquals("test user", firstValue.description)
assertNull(firstValue.password)
assertEquals(113345L, firstValue.id)
assertEquals("https://example.com", firstValue.url)
assertEquals("example.com", firstValue.domain)
assertEquals("https://example.com/inbox", firstValue.inbox)
assertEquals("https://example.com/outbox", firstValue.outbox)
assertEquals("-----BEGIN PUBLIC KEY-----...-----END PUBLIC KEY-----",firstValue.publicKey)
assertNull(firstValue.privateKey)
}
}
}