mirror of https://github.com/usbharu/Hideout.git
Merge pull request 'feature/tukurinaosi' (#1) from feature/tukurinaosi into master
Reviewed-on: http://git.usbharu-server/usbharu/hideout/pulls/1
This commit is contained in:
commit
e39347b35b
|
@ -28,6 +28,14 @@ repositories {
|
||||||
mavenCentral()
|
mavenCentral()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
kotlin {
|
||||||
|
target {
|
||||||
|
compilations.all {
|
||||||
|
kotlinOptions.jvmTarget = JavaVersion.VERSION_11.toString()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
dependencies {
|
dependencies {
|
||||||
implementation("io.ktor:ktor-server-core-jvm:$ktor_version")
|
implementation("io.ktor:ktor-server-core-jvm:$ktor_version")
|
||||||
implementation("io.ktor:ktor-server-auth-jvm:$ktor_version")
|
implementation("io.ktor:ktor-server-auth-jvm:$ktor_version")
|
||||||
|
@ -51,18 +59,30 @@ dependencies {
|
||||||
implementation("io.insert-koin:koin-ktor:$koin_version")
|
implementation("io.insert-koin:koin-ktor:$koin_version")
|
||||||
implementation("io.insert-koin:koin-logger-slf4j:$koin_version")
|
implementation("io.insert-koin:koin-logger-slf4j:$koin_version")
|
||||||
implementation("io.ktor:ktor-client-logging-jvm:2.2.4")
|
implementation("io.ktor:ktor-client-logging-jvm:2.2.4")
|
||||||
|
implementation("io.ktor:ktor-server-host-common-jvm:2.2.4")
|
||||||
|
implementation("io.ktor:ktor-server-status-pages-jvm:2.2.4")
|
||||||
|
|
||||||
testImplementation("io.ktor:ktor-server-tests-jvm:$ktor_version")
|
testImplementation("io.ktor:ktor-server-tests-jvm:$ktor_version")
|
||||||
testImplementation("org.jetbrains.kotlin:kotlin-test-junit:$kotlin_version")
|
testImplementation("org.jetbrains.kotlin:kotlin-test-junit:$kotlin_version")
|
||||||
|
testImplementation ("org.jetbrains.kotlinx:kotlinx-coroutines-test:1.6.4")
|
||||||
|
testImplementation("io.ktor:ktor-client-mock:$ktor_version")
|
||||||
|
|
||||||
implementation("io.ktor:ktor-client-core:$ktor_version")
|
implementation("io.ktor:ktor-client-core:$ktor_version")
|
||||||
implementation("io.ktor:ktor-client-cio:$ktor_version")
|
implementation("io.ktor:ktor-client-cio:$ktor_version")
|
||||||
implementation("io.ktor:ktor-client-content-negotiation:$ktor_version")
|
implementation("io.ktor:ktor-client-content-negotiation:$ktor_version")
|
||||||
testImplementation("io.ktor:ktor-client-mock:$ktor_version")
|
testImplementation("io.ktor:ktor-client-mock:$ktor_version")
|
||||||
|
|
||||||
implementation("tech.barbero.http-messages-signing:http-messages-signing-core:1.0.0")
|
implementation("tech.barbero.http-messages-signing:http-messages-signing-core:1.0.0")
|
||||||
|
|
||||||
testImplementation("org.junit.jupiter:junit-jupiter:5.8.1")
|
testImplementation("org.junit.jupiter:junit-jupiter:5.8.1")
|
||||||
|
testImplementation("org.mockito.kotlin:mockito-kotlin:4.1.0")
|
||||||
|
testImplementation("org.mockito:mockito-inline:5.2.0")
|
||||||
|
|
||||||
|
|
||||||
|
implementation("org.drewcarlson:kjob-core:0.6.0")
|
||||||
|
testImplementation("io.ktor:ktor-server-test-host-jvm:2.2.4")
|
||||||
|
|
||||||
|
testImplementation("org.slf4j:slf4j-simple:2.0.7")
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
jib {
|
jib {
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
ktor_version=2.2.4
|
ktor_version=2.2.4
|
||||||
kotlin_version=1.8.10
|
kotlin_version=1.8.10
|
||||||
logback_version=1.2.11
|
logback_version=1.4.6
|
||||||
kotlin.code.style=official
|
kotlin.code.style=official
|
||||||
exposed_version=0.41.1
|
exposed_version=0.41.1
|
||||||
h2_version=2.1.214
|
h2_version=2.1.214
|
||||||
|
|
|
@ -5,54 +5,73 @@ import com.fasterxml.jackson.databind.DeserializationFeature
|
||||||
import com.fasterxml.jackson.module.kotlin.jacksonObjectMapper
|
import com.fasterxml.jackson.module.kotlin.jacksonObjectMapper
|
||||||
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.job.ReceiveFollowJob
|
||||||
import dev.usbharu.hideout.plugins.*
|
import dev.usbharu.hideout.plugins.*
|
||||||
import dev.usbharu.hideout.repository.IUserAuthRepository
|
import dev.usbharu.hideout.repository.IUserAuthRepository
|
||||||
import dev.usbharu.hideout.repository.IUserRepository
|
import dev.usbharu.hideout.repository.IUserRepository
|
||||||
import dev.usbharu.hideout.repository.UserAuthRepository
|
import dev.usbharu.hideout.repository.UserAuthRepository
|
||||||
import dev.usbharu.hideout.repository.UserRepository
|
import dev.usbharu.hideout.repository.UserRepository
|
||||||
import dev.usbharu.hideout.routing.*
|
import dev.usbharu.hideout.routing.register
|
||||||
import dev.usbharu.hideout.service.*
|
import dev.usbharu.hideout.service.IUserAuthService
|
||||||
|
import dev.usbharu.hideout.service.activitypub.*
|
||||||
|
import dev.usbharu.hideout.service.impl.UserAuthService
|
||||||
|
import dev.usbharu.hideout.service.impl.UserService
|
||||||
|
import dev.usbharu.hideout.service.job.JobQueueParentService
|
||||||
|
import dev.usbharu.hideout.service.job.KJobJobQueueParentService
|
||||||
|
import dev.usbharu.hideout.service.signature.HttpSignatureVerifyService
|
||||||
|
import dev.usbharu.hideout.service.signature.HttpSignatureVerifyServiceImpl
|
||||||
|
import dev.usbharu.kjob.exposed.ExposedKJob
|
||||||
import io.ktor.client.*
|
import io.ktor.client.*
|
||||||
import io.ktor.client.engine.cio.*
|
import io.ktor.client.engine.cio.*
|
||||||
import io.ktor.client.plugins.contentnegotiation.*
|
|
||||||
import io.ktor.client.plugins.logging.*
|
import io.ktor.client.plugins.logging.*
|
||||||
import io.ktor.serialization.jackson.*
|
|
||||||
import io.ktor.server.application.*
|
import io.ktor.server.application.*
|
||||||
|
import kjob.core.Job
|
||||||
|
import kjob.core.KJob
|
||||||
|
import kjob.core.dsl.JobContextWithProps
|
||||||
|
import kjob.core.dsl.JobRegisterContext
|
||||||
|
import kjob.core.dsl.KJobFunctions
|
||||||
|
import kjob.core.kjob
|
||||||
import org.jetbrains.exposed.sql.Database
|
import org.jetbrains.exposed.sql.Database
|
||||||
import org.koin.ktor.ext.inject
|
import org.koin.ktor.ext.inject
|
||||||
import java.util.*
|
|
||||||
|
|
||||||
fun main(args: Array<String>): Unit = io.ktor.server.netty.EngineMain.main(args)
|
fun main(args: Array<String>): Unit = io.ktor.server.netty.EngineMain.main(args)
|
||||||
|
|
||||||
@Suppress("unused") // application.conf references the main function. This annotation prevents the IDE from marking it as unused.
|
val Application.property: Application.(propertyName: String) -> String
|
||||||
fun Application.module() {
|
get() = {
|
||||||
val module = org.koin.dsl.module {
|
environment.config.property(it).getString()
|
||||||
|
|
||||||
single<Database> {
|
|
||||||
Database.connect(
|
|
||||||
url = environment.config.property("hideout.database.url").getString(),
|
|
||||||
driver = environment.config.property("hideout.database.driver").getString(),
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
single<ConfigData> {
|
|
||||||
ConfigData(
|
@Suppress("unused") // application.conf references the main function. This annotation prevents the IDE from marking it as unused.
|
||||||
url = environment.config.propertyOrNull("hideout.url")?.getString()
|
fun Application.parent() {
|
||||||
?: environment.config.property("hideout.hostname").getString(),
|
|
||||||
|
Config.configData = ConfigData(
|
||||||
|
url = property("hideout.url"),
|
||||||
objectMapper = jacksonObjectMapper().enable(DeserializationFeature.ACCEPT_SINGLE_VALUE_AS_ARRAY)
|
objectMapper = jacksonObjectMapper().enable(DeserializationFeature.ACCEPT_SINGLE_VALUE_AS_ARRAY)
|
||||||
.setSerializationInclusion(JsonInclude.Include.NON_EMPTY)
|
.setSerializationInclusion(JsonInclude.Include.NON_EMPTY)
|
||||||
.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false)
|
.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false)
|
||||||
)
|
)
|
||||||
|
|
||||||
|
val module = org.koin.dsl.module {
|
||||||
|
single<Database> {
|
||||||
|
Database.connect(
|
||||||
|
url = property("hideout.database.url"),
|
||||||
|
driver = property("hideout.database.driver"),
|
||||||
|
user = property("hideout.database.username"),
|
||||||
|
password = property("hideout.database.password")
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
single<IUserRepository> { UserRepository(get()) }
|
||||||
|
single<IUserAuthRepository> { UserAuthRepository(get()) }
|
||||||
|
single<IUserAuthService> { UserAuthService(get(), get()) }
|
||||||
|
single<HttpSignatureVerifyService> { HttpSignatureVerifyServiceImpl(get()) }
|
||||||
|
single<JobQueueParentService> {
|
||||||
|
val kJobJobQueueService = KJobJobQueueParentService(get())
|
||||||
|
kJobJobQueueService.init(listOf())
|
||||||
|
kJobJobQueueService
|
||||||
}
|
}
|
||||||
single<HttpClient> {
|
single<HttpClient> {
|
||||||
HttpClient(CIO) {
|
HttpClient(CIO).config {
|
||||||
install(ContentNegotiation) {
|
|
||||||
|
|
||||||
jackson {
|
|
||||||
enable(DeserializationFeature.ACCEPT_SINGLE_VALUE_AS_ARRAY)
|
|
||||||
setSerializationInclusion(JsonInclude.Include.NON_EMPTY)
|
|
||||||
configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
install(Logging) {
|
install(Logging) {
|
||||||
logger = Logger.DEFAULT
|
logger = Logger.DEFAULT
|
||||||
level = LogLevel.ALL
|
level = LogLevel.ALL
|
||||||
|
@ -62,33 +81,37 @@ fun Application.module() {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
single<IUserRepository> { UserRepository(get()) }
|
single<ActivityPubFollowService> { ActivityPubFollowServiceImpl(get(), get(), get(),get()) }
|
||||||
single<IUserAuthRepository> { UserAuthRepository(get()) }
|
single<ActivityPubService> { ActivityPubServiceImpl(get()) }
|
||||||
single<IUserAuthService> { UserAuthService(get(), get()) }
|
|
||||||
single<UserService> { UserService(get()) }
|
single<UserService> { UserService(get()) }
|
||||||
single<ActivityPubService> { ActivityPubService() }
|
single<ActivityPubUserService> { ActivityPubUserServiceImpl(get(), get(), get()) }
|
||||||
single<ActivityPubUserService> { ActivityPubUserService(get(), get(), get(), get()) }
|
|
||||||
single<IWebFingerService> { WebFingerService(get(), get()) }
|
|
||||||
}
|
}
|
||||||
configureKoin(module)
|
|
||||||
val configData by inject<ConfigData>()
|
|
||||||
Config.configData = configData
|
|
||||||
val decode = Base64.getDecoder().decode("76pc9N9hspQqapj30kCaLJA14O/50ptCg50zCA1oxjA=")
|
|
||||||
|
|
||||||
val pair = "admin" to decode
|
|
||||||
println(pair)
|
configureKoin(module)
|
||||||
val userAuthService by inject<IUserAuthService>()
|
|
||||||
val userService by inject<UserService>()
|
|
||||||
configureSecurity(userAuthService)
|
|
||||||
configureHTTP()
|
configureHTTP()
|
||||||
|
configureSockets()
|
||||||
configureMonitoring()
|
configureMonitoring()
|
||||||
configureSerialization()
|
configureSerialization()
|
||||||
configureSockets()
|
register(inject<IUserAuthService>().value)
|
||||||
val activityPubUserService by inject<ActivityPubUserService>()
|
configureRouting(
|
||||||
user(userService, activityPubUserService)
|
inject<HttpSignatureVerifyService>().value,
|
||||||
login()
|
inject<ActivityPubService>().value,
|
||||||
register(userAuthService)
|
inject<UserService>().value,
|
||||||
wellKnown(userService)
|
inject<ActivityPubUserService>().value
|
||||||
val activityPubService by inject<ActivityPubService>()
|
)
|
||||||
userActivityPubRouting(activityPubService, activityPubUserService)
|
}
|
||||||
|
@Suppress("unused")
|
||||||
|
fun Application.worker() {
|
||||||
|
val kJob = kjob(ExposedKJob) {
|
||||||
|
connectionDatabase = inject<Database>().value
|
||||||
|
}.start()
|
||||||
|
|
||||||
|
val activityPubService = inject<ActivityPubService>().value
|
||||||
|
|
||||||
|
kJob.register(ReceiveFollowJob){
|
||||||
|
execute {
|
||||||
|
activityPubService.processActivity(this,it)
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -13,4 +13,26 @@ open class Accept : Object {
|
||||||
this.`object` = `object`
|
this.`object` = `object`
|
||||||
this.actor = actor
|
this.actor = actor
|
||||||
}
|
}
|
||||||
|
|
||||||
|
override fun equals(other: Any?): Boolean {
|
||||||
|
if (this === other) return true
|
||||||
|
if (other !is Accept) return false
|
||||||
|
if (!super.equals(other)) return false
|
||||||
|
|
||||||
|
if (`object` != other.`object`) return false
|
||||||
|
return actor == other.actor
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun hashCode(): Int {
|
||||||
|
var result = super.hashCode()
|
||||||
|
result = 31 * result + (`object`?.hashCode() ?: 0)
|
||||||
|
result = 31 * result + (actor?.hashCode() ?: 0)
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun toString(): String {
|
||||||
|
return "Accept(`object`=$`object`, actor=$actor) ${super.toString()}"
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -13,4 +13,21 @@ open class Image : Object {
|
||||||
this.url = url
|
this.url = url
|
||||||
}
|
}
|
||||||
|
|
||||||
|
override fun equals(other: Any?): Boolean {
|
||||||
|
if (this === other) return true
|
||||||
|
if (other !is Image) return false
|
||||||
|
if (!super.equals(other)) return false
|
||||||
|
|
||||||
|
if (mediaType != other.mediaType) return false
|
||||||
|
return url == other.url
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun hashCode(): Int {
|
||||||
|
var result = super.hashCode()
|
||||||
|
result = 31 * result + (mediaType?.hashCode() ?: 0)
|
||||||
|
result = 31 * result + (url?.hashCode() ?: 0)
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -25,6 +25,23 @@ open class JsonLd {
|
||||||
}
|
}
|
||||||
|
|
||||||
protected constructor()
|
protected constructor()
|
||||||
|
|
||||||
|
override fun equals(other: Any?): Boolean {
|
||||||
|
if (this === other) return true
|
||||||
|
if (other !is JsonLd) return false
|
||||||
|
|
||||||
|
return context == other.context
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun hashCode(): Int {
|
||||||
|
return context.hashCode()
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun toString(): String {
|
||||||
|
return "JsonLd(context=$context)"
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public class ContextDeserializer : JsonDeserializer<String>() {
|
public class ContextDeserializer : JsonDeserializer<String>() {
|
||||||
|
|
|
@ -1,9 +1,9 @@
|
||||||
package dev.usbharu.hideout.ap
|
package dev.usbharu.hideout.ap
|
||||||
|
|
||||||
open class Key : Object{
|
open class Key : Object{
|
||||||
private var id:String? = null
|
var id:String? = null
|
||||||
private var owner:String? = null
|
var owner:String? = null
|
||||||
private var publicKeyPem:String? = null
|
var publicKeyPem:String? = null
|
||||||
protected constructor() : super()
|
protected constructor() : super()
|
||||||
constructor(
|
constructor(
|
||||||
type: List<String>,
|
type: List<String>,
|
||||||
|
@ -17,5 +17,23 @@ open class Key : Object{
|
||||||
this.publicKeyPem = publicKeyPem
|
this.publicKeyPem = publicKeyPem
|
||||||
}
|
}
|
||||||
|
|
||||||
|
override fun equals(other: Any?): Boolean {
|
||||||
|
if (this === other) return true
|
||||||
|
if (other !is Key) return false
|
||||||
|
if (!super.equals(other)) return false
|
||||||
|
|
||||||
|
if (id != other.id) return false
|
||||||
|
if (owner != other.owner) return false
|
||||||
|
return publicKeyPem == other.publicKeyPem
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun hashCode(): Int {
|
||||||
|
var result = super.hashCode()
|
||||||
|
result = 31 * result + (id?.hashCode() ?: 0)
|
||||||
|
result = 31 * result + (owner?.hashCode() ?: 0)
|
||||||
|
result = 31 * result + (publicKeyPem?.hashCode() ?: 0)
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -24,6 +24,26 @@ open class Object : JsonLd {
|
||||||
return toMutableList.distinct()
|
return toMutableList.distinct()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
override fun equals(other: Any?): Boolean {
|
||||||
|
if (this === other) return true
|
||||||
|
if (other !is Object) return false
|
||||||
|
|
||||||
|
if (type != other.type) return false
|
||||||
|
return name == other.name
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun hashCode(): Int {
|
||||||
|
var result = type.hashCode()
|
||||||
|
result = 31 * result + (name?.hashCode() ?: 0)
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun toString(): String {
|
||||||
|
return "Object(type=$type, name=$name) ${super.toString()}"
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public class TypeSerializer : JsonSerializer<List<String>>() {
|
public class TypeSerializer : JsonSerializer<List<String>>() {
|
||||||
|
|
|
@ -5,10 +5,10 @@ open class Person : Object {
|
||||||
var preferredUsername:String? = null
|
var preferredUsername:String? = null
|
||||||
var summary:String? = null
|
var summary:String? = null
|
||||||
var inbox:String? = null
|
var inbox:String? = null
|
||||||
private 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
|
||||||
private var publicKey:Key? = null
|
var publicKey:Key? = null
|
||||||
protected constructor() : super()
|
protected constructor() : super()
|
||||||
constructor(
|
constructor(
|
||||||
type: List<String> = emptyList(),
|
type: List<String> = emptyList(),
|
||||||
|
@ -32,4 +32,31 @@ open class Person : Object {
|
||||||
this.publicKey = publicKey
|
this.publicKey = publicKey
|
||||||
}
|
}
|
||||||
|
|
||||||
|
override fun equals(other: Any?): Boolean {
|
||||||
|
if (this === other) return true
|
||||||
|
if (other !is Person) return false
|
||||||
|
|
||||||
|
if (id != other.id) return false
|
||||||
|
if (preferredUsername != other.preferredUsername) return false
|
||||||
|
if (summary != other.summary) return false
|
||||||
|
if (inbox != other.inbox) return false
|
||||||
|
if (outbox != other.outbox) return false
|
||||||
|
if (url != other.url) return false
|
||||||
|
if (icon != other.icon) return false
|
||||||
|
return publicKey == other.publicKey
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun hashCode(): Int {
|
||||||
|
var result = id?.hashCode() ?: 0
|
||||||
|
result = 31 * result + (preferredUsername?.hashCode() ?: 0)
|
||||||
|
result = 31 * result + (summary?.hashCode() ?: 0)
|
||||||
|
result = 31 * result + (inbox?.hashCode() ?: 0)
|
||||||
|
result = 31 * result + (outbox?.hashCode() ?: 0)
|
||||||
|
result = 31 * result + (url?.hashCode() ?: 0)
|
||||||
|
result = 31 * result + (icon?.hashCode() ?: 0)
|
||||||
|
result = 31 * result + (publicKey?.hashCode() ?: 0)
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,24 @@
|
||||||
|
package dev.usbharu.hideout.domain.model
|
||||||
|
|
||||||
|
import dev.usbharu.hideout.ap.JsonLd
|
||||||
|
import dev.usbharu.hideout.util.HttpUtil.Activity
|
||||||
|
import io.ktor.http.*
|
||||||
|
|
||||||
|
sealed class ActivityPubResponse(
|
||||||
|
val httpStatusCode: HttpStatusCode,
|
||||||
|
val contentType: ContentType = ContentType.Application.Activity
|
||||||
|
)
|
||||||
|
|
||||||
|
class ActivityPubStringResponse(
|
||||||
|
httpStatusCode: HttpStatusCode = HttpStatusCode.OK,
|
||||||
|
val message: String,
|
||||||
|
contentType: ContentType = ContentType.Application.Activity
|
||||||
|
) :
|
||||||
|
ActivityPubResponse(httpStatusCode, contentType)
|
||||||
|
|
||||||
|
class ActivityPubObjectResponse(
|
||||||
|
httpStatusCode: HttpStatusCode = HttpStatusCode.OK,
|
||||||
|
val message: JsonLd,
|
||||||
|
contentType: ContentType = ContentType.Application.Activity
|
||||||
|
) :
|
||||||
|
ActivityPubResponse(httpStatusCode, contentType)
|
|
@ -2,16 +2,36 @@ package dev.usbharu.hideout.domain.model
|
||||||
|
|
||||||
import org.jetbrains.exposed.dao.id.LongIdTable
|
import org.jetbrains.exposed.dao.id.LongIdTable
|
||||||
|
|
||||||
data class User(val name: String,val domain: String, val screenName: String, val description: String)
|
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(
|
data class UserEntity(
|
||||||
val id: Long,
|
val id: Long,
|
||||||
val name: String,
|
val name: String,
|
||||||
val domain:String,
|
val domain: String,
|
||||||
val screenName: String,
|
val screenName: String,
|
||||||
val description: 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)
|
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") {
|
object Users : LongIdTable("users") {
|
||||||
|
@ -19,6 +39,10 @@ object Users : LongIdTable("users") {
|
||||||
val domain = varchar("domain", length = 255)
|
val domain = varchar("domain", length = 255)
|
||||||
val screenName = varchar("screen_name", length = 64)
|
val screenName = varchar("screen_name", length = 64)
|
||||||
val description = varchar("description", length = 600)
|
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 {
|
init {
|
||||||
uniqueIndex(name, domain)
|
uniqueIndex(name, domain)
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,11 @@
|
||||||
|
package dev.usbharu.hideout.domain.model.job
|
||||||
|
|
||||||
|
import kjob.core.Job
|
||||||
|
|
||||||
|
sealed class HideoutJob(name: String = "") : Job(name)
|
||||||
|
|
||||||
|
object ReceiveFollowJob : HideoutJob("ReceiveFollowJob"){
|
||||||
|
val actor = string("actor")
|
||||||
|
val follow = string("follow")
|
||||||
|
val targetActor = string("targetActor")
|
||||||
|
}
|
|
@ -0,0 +1,5 @@
|
||||||
|
package dev.usbharu.hideout.domain.model.wellknown
|
||||||
|
|
||||||
|
data class WebFinger(val subject:String,val links:List<Link>){
|
||||||
|
data class Link(val rel:String,val type:String,val href:String)
|
||||||
|
}
|
|
@ -0,0 +1,8 @@
|
||||||
|
package dev.usbharu.hideout.exception
|
||||||
|
|
||||||
|
class HttpSignatureVerifyException : IllegalArgumentException {
|
||||||
|
constructor() : super()
|
||||||
|
constructor(s: String?) : super(s)
|
||||||
|
constructor(message: String?, cause: Throwable?) : super(message, cause)
|
||||||
|
constructor(cause: Throwable?) : super(cause)
|
||||||
|
}
|
|
@ -0,0 +1,8 @@
|
||||||
|
package dev.usbharu.hideout.exception
|
||||||
|
|
||||||
|
class IllegalParameterException : IllegalArgumentException {
|
||||||
|
constructor() : super()
|
||||||
|
constructor(s: String?) : super(s)
|
||||||
|
constructor(message: String?, cause: Throwable?) : super(message, cause)
|
||||||
|
constructor(cause: Throwable?) : super(cause)
|
||||||
|
}
|
|
@ -0,0 +1,8 @@
|
||||||
|
package dev.usbharu.hideout.exception
|
||||||
|
|
||||||
|
class JsonParseException : IllegalArgumentException {
|
||||||
|
constructor() : super()
|
||||||
|
constructor(s: String?) : super(s)
|
||||||
|
constructor(message: String?, cause: Throwable?) : super(message, cause)
|
||||||
|
constructor(cause: Throwable?) : super(cause)
|
||||||
|
}
|
|
@ -0,0 +1,8 @@
|
||||||
|
package dev.usbharu.hideout.exception
|
||||||
|
|
||||||
|
class ParameterNotExistException : IllegalArgumentException {
|
||||||
|
constructor() : super()
|
||||||
|
constructor(s: String?) : super(s)
|
||||||
|
constructor(message: String?, cause: Throwable?) : super(message, cause)
|
||||||
|
constructor(cause: Throwable?) : super(cause)
|
||||||
|
}
|
|
@ -0,0 +1,8 @@
|
||||||
|
package dev.usbharu.hideout.exception.ap
|
||||||
|
|
||||||
|
class IllegalActivityPubObjectException : IllegalArgumentException {
|
||||||
|
constructor() : super()
|
||||||
|
constructor(s: String?) : super(s)
|
||||||
|
constructor(message: String?, cause: Throwable?) : super(message, cause)
|
||||||
|
constructor(cause: Throwable?) : super(cause)
|
||||||
|
}
|
|
@ -3,7 +3,7 @@ package dev.usbharu.hideout.plugins
|
||||||
import dev.usbharu.hideout.ap.JsonLd
|
import dev.usbharu.hideout.ap.JsonLd
|
||||||
import dev.usbharu.hideout.config.Config
|
import dev.usbharu.hideout.config.Config
|
||||||
import dev.usbharu.hideout.service.IUserAuthService
|
import dev.usbharu.hideout.service.IUserAuthService
|
||||||
import dev.usbharu.hideout.service.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.*
|
||||||
import io.ktor.client.plugins.api.*
|
import io.ktor.client.plugins.api.*
|
||||||
|
@ -12,7 +12,6 @@ import io.ktor.client.statement.*
|
||||||
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.util.*
|
|
||||||
import kotlinx.coroutines.runBlocking
|
import kotlinx.coroutines.runBlocking
|
||||||
import tech.barbero.http.message.signing.HttpMessage
|
import tech.barbero.http.message.signing.HttpMessage
|
||||||
import tech.barbero.http.message.signing.HttpMessageSigner
|
import tech.barbero.http.message.signing.HttpMessageSigner
|
||||||
|
|
|
@ -8,6 +8,5 @@ import io.ktor.server.application.*
|
||||||
fun Application.configureMonitoring() {
|
fun Application.configureMonitoring() {
|
||||||
install(CallLogging) {
|
install(CallLogging) {
|
||||||
level = Level.INFO
|
level = Level.INFO
|
||||||
filter { call -> call.request.path().startsWith("/") }
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,15 +1,28 @@
|
||||||
package dev.usbharu.hideout.plugins
|
package dev.usbharu.hideout.plugins
|
||||||
|
|
||||||
import io.ktor.server.routing.*
|
import dev.usbharu.hideout.routing.activitypub.inbox
|
||||||
import io.ktor.server.response.*
|
import dev.usbharu.hideout.routing.activitypub.outbox
|
||||||
import io.ktor.server.plugins.autohead.*
|
import dev.usbharu.hideout.routing.activitypub.usersAP
|
||||||
|
import dev.usbharu.hideout.routing.wellknown.webfinger
|
||||||
|
import dev.usbharu.hideout.service.activitypub.ActivityPubService
|
||||||
|
import dev.usbharu.hideout.service.activitypub.ActivityPubUserService
|
||||||
|
import dev.usbharu.hideout.service.impl.UserService
|
||||||
|
import dev.usbharu.hideout.service.signature.HttpSignatureVerifyService
|
||||||
import io.ktor.server.application.*
|
import io.ktor.server.application.*
|
||||||
|
import io.ktor.server.plugins.autohead.*
|
||||||
|
import io.ktor.server.routing.*
|
||||||
|
|
||||||
fun Application.configureRouting() {
|
fun Application.configureRouting(
|
||||||
|
httpSignatureVerifyService: HttpSignatureVerifyService,
|
||||||
|
activityPubService: ActivityPubService,
|
||||||
|
userService:UserService,
|
||||||
|
activityPubUserService: ActivityPubUserService
|
||||||
|
) {
|
||||||
install(AutoHeadResponse)
|
install(AutoHeadResponse)
|
||||||
routing {
|
routing {
|
||||||
get("/") {
|
inbox(httpSignatureVerifyService, activityPubService)
|
||||||
call.respondText("Hello World!")
|
outbox()
|
||||||
}
|
usersAP(activityPubUserService)
|
||||||
|
webfinger(userService)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,5 +1,11 @@
|
||||||
package dev.usbharu.hideout.plugins
|
package dev.usbharu.hideout.plugins
|
||||||
|
|
||||||
|
import com.fasterxml.jackson.annotation.JsonInclude
|
||||||
|
import com.fasterxml.jackson.annotation.JsonSetter
|
||||||
|
import com.fasterxml.jackson.annotation.Nulls
|
||||||
|
import com.fasterxml.jackson.databind.DeserializationFeature
|
||||||
|
import dev.usbharu.hideout.util.HttpUtil.Activity
|
||||||
|
import io.ktor.http.*
|
||||||
import io.ktor.serialization.jackson.*
|
import io.ktor.serialization.jackson.*
|
||||||
import io.ktor.server.application.*
|
import io.ktor.server.application.*
|
||||||
import io.ktor.server.plugins.contentnegotiation.*
|
import io.ktor.server.plugins.contentnegotiation.*
|
||||||
|
@ -8,11 +14,11 @@ import io.ktor.server.routing.*
|
||||||
|
|
||||||
fun Application.configureSerialization() {
|
fun Application.configureSerialization() {
|
||||||
install(ContentNegotiation) {
|
install(ContentNegotiation) {
|
||||||
jackson()
|
jackson {
|
||||||
}
|
enable(DeserializationFeature.ACCEPT_SINGLE_VALUE_AS_ARRAY)
|
||||||
routing {
|
setSerializationInclusion(JsonInclude.Include.NON_EMPTY)
|
||||||
get("/json/kotlinx-serialization") {
|
configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false)
|
||||||
call.respond(mapOf("hello" to "world"))
|
configOverride(List::class.java).setSetterInfo(JsonSetter.Value.forContentNulls(Nulls.AS_EMPTY))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,17 @@
|
||||||
|
package dev.usbharu.hideout.plugins
|
||||||
|
|
||||||
|
import io.ktor.http.*
|
||||||
|
import io.ktor.server.application.*
|
||||||
|
import io.ktor.server.plugins.statuspages.*
|
||||||
|
import io.ktor.server.response.*
|
||||||
|
|
||||||
|
fun Application.configureStatusPages() {
|
||||||
|
install(StatusPages) {
|
||||||
|
exception<Throwable> { call, cause ->
|
||||||
|
call.respondText(text = "500: $cause", status = HttpStatusCode.InternalServerError)
|
||||||
|
}
|
||||||
|
exception<IllegalArgumentException> { call, cause ->
|
||||||
|
call.respondText(text = "400: $cause", status = HttpStatusCode.BadRequest)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -8,8 +8,16 @@ interface IUserRepository {
|
||||||
|
|
||||||
suspend fun findById(id: Long): UserEntity?
|
suspend fun findById(id: Long): UserEntity?
|
||||||
|
|
||||||
|
suspend fun findByIds(ids: List<Long>): List<UserEntity>
|
||||||
|
|
||||||
suspend fun findByName(name: String): UserEntity?
|
suspend fun findByName(name: String): UserEntity?
|
||||||
|
|
||||||
|
suspend fun findByNameAndDomains(names: List<Pair<String,String>>): List<UserEntity>
|
||||||
|
|
||||||
|
suspend fun findByUrl(url:String):UserEntity?
|
||||||
|
|
||||||
|
suspend fun findByUrls(urls: List<String>): List<UserEntity>
|
||||||
|
|
||||||
suspend fun update(userEntity: UserEntity)
|
suspend fun update(userEntity: UserEntity)
|
||||||
|
|
||||||
suspend fun delete(id: Long)
|
suspend fun delete(id: Long)
|
||||||
|
|
|
@ -25,7 +25,10 @@ class UserRepository(private val database: Database) : IUserRepository {
|
||||||
this[Users.name],
|
this[Users.name],
|
||||||
this[Users.domain],
|
this[Users.domain],
|
||||||
this[Users.screenName],
|
this[Users.screenName],
|
||||||
this[Users.description]
|
this[Users.description],
|
||||||
|
this[Users.inbox],
|
||||||
|
this[Users.outbox],
|
||||||
|
this[Users.url]
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -35,7 +38,10 @@ class UserRepository(private val database: Database) : IUserRepository {
|
||||||
this[Users.name],
|
this[Users.name],
|
||||||
this[Users.domain],
|
this[Users.domain],
|
||||||
this[Users.screenName],
|
this[Users.screenName],
|
||||||
this[Users.description]
|
this[Users.description],
|
||||||
|
this[Users.inbox],
|
||||||
|
this[Users.outbox],
|
||||||
|
this[Users.url],
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -49,6 +55,9 @@ class UserRepository(private val database: Database) : IUserRepository {
|
||||||
it[domain] = user.domain
|
it[domain] = user.domain
|
||||||
it[screenName] = user.screenName
|
it[screenName] = user.screenName
|
||||||
it[description] = user.description
|
it[description] = user.description
|
||||||
|
it[inbox] = user.inbox
|
||||||
|
it[outbox] = user.outbox
|
||||||
|
it[url] = user.url
|
||||||
}[Users.id].value, user)
|
}[Users.id].value, user)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -70,6 +79,14 @@ class UserRepository(private val database: Database) : IUserRepository {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
override suspend fun findByIds(ids: List<Long>): List<UserEntity> {
|
||||||
|
return query {
|
||||||
|
Users.select { Users.id inList ids }.map {
|
||||||
|
it.toUserEntity()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
override suspend fun findByName(name: String): UserEntity? {
|
override suspend fun findByName(name: String): UserEntity? {
|
||||||
return query {
|
return query {
|
||||||
Users.select { Users.name eq name }.map {
|
Users.select { Users.name eq name }.map {
|
||||||
|
@ -78,6 +95,26 @@ class UserRepository(private val database: Database) : IUserRepository {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
override suspend fun findByNameAndDomains(names: List<Pair<String, String>>): List<UserEntity> {
|
||||||
|
return query {
|
||||||
|
val selectAll = Users.selectAll()
|
||||||
|
names.forEach { (name, domain) ->
|
||||||
|
selectAll.orWhere { Users.name eq name and (Users.domain eq domain) }
|
||||||
|
}
|
||||||
|
selectAll.map { it.toUserEntity() }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override suspend fun findByUrl(url: String): UserEntity? {
|
||||||
|
return query {
|
||||||
|
Users.select { Users.url eq url }.singleOrNull()?.toUserEntity()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override suspend fun findByUrls(urls: List<String>): List<UserEntity> {
|
||||||
|
TODO("Not yet implemented")
|
||||||
|
}
|
||||||
|
|
||||||
override suspend fun findFollowersById(id: Long): List<UserEntity> {
|
override suspend fun findFollowersById(id: Long): List<UserEntity> {
|
||||||
return query {
|
return query {
|
||||||
val followers = Users.alias("FOLLOWERS")
|
val followers = Users.alias("FOLLOWERS")
|
||||||
|
@ -91,7 +128,16 @@ class UserRepository(private val database: Database) : IUserRepository {
|
||||||
onColumn = { UsersFollowers.followerId },
|
onColumn = { UsersFollowers.followerId },
|
||||||
otherColumn = { followers[Users.id] })
|
otherColumn = { followers[Users.id] })
|
||||||
|
|
||||||
.slice(followers.get(Users.id), followers.get(Users.name), followers.get(Users.domain), followers.get(Users.screenName), followers.get(Users.description))
|
.slice(
|
||||||
|
followers.get(Users.id),
|
||||||
|
followers.get(Users.name),
|
||||||
|
followers.get(Users.domain),
|
||||||
|
followers.get(Users.screenName),
|
||||||
|
followers.get(Users.description),
|
||||||
|
followers.get(Users.inbox),
|
||||||
|
followers.get(Users.outbox),
|
||||||
|
followers.get(Users.url)
|
||||||
|
)
|
||||||
.select { Users.id eq id }
|
.select { Users.id eq id }
|
||||||
.map {
|
.map {
|
||||||
UserEntity(
|
UserEntity(
|
||||||
|
@ -100,6 +146,9 @@ class UserRepository(private val database: Database) : IUserRepository {
|
||||||
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]],
|
||||||
|
inbox = it[followers[Users.inbox]],
|
||||||
|
outbox = it[followers[Users.outbox]],
|
||||||
|
url = it[followers[Users.url]],
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -113,6 +162,9 @@ class UserRepository(private val database: Database) : IUserRepository {
|
||||||
it[domain] = userEntity.domain
|
it[domain] = userEntity.domain
|
||||||
it[screenName] = userEntity.screenName
|
it[screenName] = userEntity.screenName
|
||||||
it[description] = userEntity.description
|
it[description] = userEntity.description
|
||||||
|
it[inbox] = userEntity.inbox
|
||||||
|
it[outbox] = userEntity.outbox
|
||||||
|
it[url] = userEntity.url
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -4,8 +4,8 @@ import dev.usbharu.hideout.domain.model.User
|
||||||
import dev.usbharu.hideout.plugins.UserSession
|
import dev.usbharu.hideout.plugins.UserSession
|
||||||
import dev.usbharu.hideout.plugins.respondAp
|
import dev.usbharu.hideout.plugins.respondAp
|
||||||
import dev.usbharu.hideout.plugins.tokenAuth
|
import dev.usbharu.hideout.plugins.tokenAuth
|
||||||
import dev.usbharu.hideout.service.ActivityPubUserService
|
import dev.usbharu.hideout.service.impl.ActivityPubUserService
|
||||||
import dev.usbharu.hideout.service.UserService
|
import dev.usbharu.hideout.service.impl.UserService
|
||||||
import dev.usbharu.hideout.util.HttpUtil
|
import dev.usbharu.hideout.util.HttpUtil
|
||||||
import io.ktor.http.*
|
import io.ktor.http.*
|
||||||
import io.ktor.server.application.*
|
import io.ktor.server.application.*
|
||||||
|
@ -13,7 +13,6 @@ 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.*
|
|
||||||
|
|
||||||
@Suppress("unused")
|
@Suppress("unused")
|
||||||
fun Application.user(userService: UserService, activityPubUserService: ActivityPubUserService) {
|
fun Application.user(userService: UserService, activityPubUserService: ActivityPubUserService) {
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
package dev.usbharu.hideout.routing
|
package dev.usbharu.hideout.routing
|
||||||
|
|
||||||
import dev.usbharu.hideout.config.Config
|
import dev.usbharu.hideout.config.Config
|
||||||
import dev.usbharu.hideout.service.UserService
|
import dev.usbharu.hideout.service.impl.UserService
|
||||||
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.*
|
||||||
|
|
|
@ -0,0 +1,72 @@
|
||||||
|
package dev.usbharu.hideout.routing.activitypub
|
||||||
|
|
||||||
|
import dev.usbharu.hideout.config.Config
|
||||||
|
import dev.usbharu.hideout.domain.model.ActivityPubObjectResponse
|
||||||
|
import dev.usbharu.hideout.domain.model.ActivityPubStringResponse
|
||||||
|
import dev.usbharu.hideout.exception.HttpSignatureVerifyException
|
||||||
|
import dev.usbharu.hideout.service.signature.HttpSignatureVerifyService
|
||||||
|
import io.ktor.http.*
|
||||||
|
import io.ktor.server.application.*
|
||||||
|
import io.ktor.server.request.*
|
||||||
|
import io.ktor.server.response.*
|
||||||
|
import io.ktor.server.routing.*
|
||||||
|
|
||||||
|
fun Routing.inbox(
|
||||||
|
httpSignatureVerifyService: HttpSignatureVerifyService,
|
||||||
|
activityPubService: dev.usbharu.hideout.service.activitypub.ActivityPubService
|
||||||
|
){
|
||||||
|
|
||||||
|
route("/inbox") {
|
||||||
|
get {
|
||||||
|
call.respond(HttpStatusCode.MethodNotAllowed)
|
||||||
|
}
|
||||||
|
post {
|
||||||
|
if (httpSignatureVerifyService.verify(call.request.headers).not()) {
|
||||||
|
throw HttpSignatureVerifyException()
|
||||||
|
}
|
||||||
|
val json = call.receiveText()
|
||||||
|
call.application.log.trace("Received: $json")
|
||||||
|
val activityTypes = activityPubService.parseActivity(json)
|
||||||
|
call.application.log.debug("ActivityTypes: ${activityTypes.name}")
|
||||||
|
val response = activityPubService.processActivity(json, activityTypes)
|
||||||
|
when (response) {
|
||||||
|
is ActivityPubObjectResponse -> call.respond(
|
||||||
|
response.httpStatusCode,
|
||||||
|
Config.configData.objectMapper.writeValueAsString(response.message.apply {
|
||||||
|
context =
|
||||||
|
listOf("https://www.w3.org/ns/activitystreams")
|
||||||
|
})
|
||||||
|
)
|
||||||
|
is ActivityPubStringResponse -> call.respond(response.httpStatusCode, response.message)
|
||||||
|
null -> call.respond(HttpStatusCode.NotImplemented)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
route("/users/{name}/inbox"){
|
||||||
|
get {
|
||||||
|
call.respond(HttpStatusCode.MethodNotAllowed)
|
||||||
|
}
|
||||||
|
post {
|
||||||
|
if (httpSignatureVerifyService.verify(call.request.headers).not()) {
|
||||||
|
throw HttpSignatureVerifyException()
|
||||||
|
}
|
||||||
|
val json = call.receiveText()
|
||||||
|
call.application.log.trace("Received: $json")
|
||||||
|
val activityTypes = activityPubService.parseActivity(json)
|
||||||
|
call.application.log.debug("ActivityTypes: ${activityTypes.name}")
|
||||||
|
val response = activityPubService.processActivity(json, activityTypes)
|
||||||
|
when (response) {
|
||||||
|
is ActivityPubObjectResponse -> call.respond(
|
||||||
|
response.httpStatusCode,
|
||||||
|
Config.configData.objectMapper.writeValueAsString(response.message.apply {
|
||||||
|
context =
|
||||||
|
listOf("https://www.w3.org/ns/activitystreams")
|
||||||
|
})
|
||||||
|
)
|
||||||
|
is ActivityPubStringResponse -> call.respond(response.httpStatusCode, response.message)
|
||||||
|
null -> call.respond(HttpStatusCode.NotImplemented)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -0,0 +1,27 @@
|
||||||
|
package dev.usbharu.hideout.routing.activitypub
|
||||||
|
|
||||||
|
import io.ktor.http.*
|
||||||
|
import io.ktor.server.application.*
|
||||||
|
import io.ktor.server.response.*
|
||||||
|
import io.ktor.server.routing.*
|
||||||
|
|
||||||
|
fun Routing.outbox() {
|
||||||
|
|
||||||
|
route("/outbox") {
|
||||||
|
get {
|
||||||
|
call.respond(HttpStatusCode.NotImplemented)
|
||||||
|
}
|
||||||
|
post {
|
||||||
|
call.respond(HttpStatusCode.NotImplemented)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
route("/users/{name}/outbox"){
|
||||||
|
get {
|
||||||
|
call.respond(HttpStatusCode.NotImplemented)
|
||||||
|
}
|
||||||
|
post {
|
||||||
|
call.respond(HttpStatusCode.NotImplemented)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -0,0 +1,39 @@
|
||||||
|
package dev.usbharu.hideout.routing.activitypub
|
||||||
|
|
||||||
|
import dev.usbharu.hideout.exception.ParameterNotExistException
|
||||||
|
import dev.usbharu.hideout.plugins.respondAp
|
||||||
|
import dev.usbharu.hideout.service.activitypub.ActivityPubUserService
|
||||||
|
import dev.usbharu.hideout.util.HttpUtil.Activity
|
||||||
|
import dev.usbharu.hideout.util.HttpUtil.JsonLd
|
||||||
|
import io.ktor.http.*
|
||||||
|
import io.ktor.server.application.*
|
||||||
|
import io.ktor.server.request.*
|
||||||
|
import io.ktor.server.routing.*
|
||||||
|
|
||||||
|
fun Routing.usersAP(activityPubUserService: ActivityPubUserService) {
|
||||||
|
route("/users/{name}") {
|
||||||
|
createChild(ContentTypeRouteSelector(ContentType.Application.Activity, ContentType.Application.JsonLd)).handle {
|
||||||
|
val name =
|
||||||
|
call.parameters["name"] ?: throw ParameterNotExistException("Parameter(name='name') does not exist.")
|
||||||
|
val person = activityPubUserService.getPersonByName(name)
|
||||||
|
return@handle call.respondAp(
|
||||||
|
person,
|
||||||
|
HttpStatusCode.OK
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class ContentTypeRouteSelector(private vararg val contentType: ContentType) : RouteSelector() {
|
||||||
|
override fun evaluate(context: RoutingResolveContext, segmentIndex: Int): RouteSelectorEvaluation {
|
||||||
|
|
||||||
|
val requestContentType =
|
||||||
|
ContentType.parse(context.call.request.accept() ?: return RouteSelectorEvaluation.FailedParameter)
|
||||||
|
return if (contentType.any { contentType -> contentType.match(requestContentType) }) {
|
||||||
|
RouteSelectorEvaluation.Constant
|
||||||
|
} else {
|
||||||
|
RouteSelectorEvaluation.FailedParameter
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -1,54 +0,0 @@
|
||||||
package dev.usbharu.hideout.routing
|
|
||||||
|
|
||||||
import com.fasterxml.jackson.module.kotlin.readValue
|
|
||||||
import dev.usbharu.hideout.ap.Follow
|
|
||||||
import dev.usbharu.hideout.config.Config
|
|
||||||
import dev.usbharu.hideout.service.ActivityPubService
|
|
||||||
import dev.usbharu.hideout.service.ActivityPubUserService
|
|
||||||
import dev.usbharu.hideout.util.HttpUtil
|
|
||||||
import io.ktor.http.*
|
|
||||||
import io.ktor.server.application.*
|
|
||||||
import io.ktor.server.request.*
|
|
||||||
import io.ktor.server.response.*
|
|
||||||
import io.ktor.server.routing.*
|
|
||||||
|
|
||||||
fun Application.userActivityPubRouting(activityPubService: ActivityPubService, activityPubUserService: ActivityPubUserService) {
|
|
||||||
routing {
|
|
||||||
route("/users/{name}") {
|
|
||||||
route("/inbox") {
|
|
||||||
get {
|
|
||||||
call.respond(HttpStatusCode.MethodNotAllowed)
|
|
||||||
}
|
|
||||||
post {
|
|
||||||
if (!HttpUtil.isContentTypeOfActivityPub(call.request.contentType())) {
|
|
||||||
return@post call.respond(HttpStatusCode.BadRequest)
|
|
||||||
}
|
|
||||||
val bodyText = call.receiveText()
|
|
||||||
println(bodyText)
|
|
||||||
when (activityPubService.switchApType(bodyText)) {
|
|
||||||
ActivityPubService.ActivityType.Follow -> {
|
|
||||||
val readValue = Config.configData.objectMapper.readValue<Follow>(bodyText)
|
|
||||||
activityPubUserService.receiveFollow(readValue)
|
|
||||||
return@post call.respond(HttpStatusCode.Accepted)
|
|
||||||
}
|
|
||||||
|
|
||||||
ActivityPubService.ActivityType.Undo -> {
|
|
||||||
return@post call.respond(HttpStatusCode.Accepted)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
}
|
|
||||||
route("/outbox") {
|
|
||||||
get {
|
|
||||||
call.respond(HttpStatusCode.MethodNotAllowed)
|
|
||||||
|
|
||||||
}
|
|
||||||
post {
|
|
||||||
|
|
||||||
call.respond(HttpStatusCode.MethodNotAllowed)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -0,0 +1,44 @@
|
||||||
|
package dev.usbharu.hideout.routing.wellknown
|
||||||
|
|
||||||
|
import dev.usbharu.hideout.config.Config
|
||||||
|
import dev.usbharu.hideout.domain.model.wellknown.WebFinger
|
||||||
|
import dev.usbharu.hideout.exception.IllegalParameterException
|
||||||
|
import dev.usbharu.hideout.exception.ParameterNotExistException
|
||||||
|
import dev.usbharu.hideout.service.impl.UserService
|
||||||
|
import dev.usbharu.hideout.util.HttpUtil.Activity
|
||||||
|
import io.ktor.http.*
|
||||||
|
import io.ktor.server.application.*
|
||||||
|
import io.ktor.server.response.*
|
||||||
|
import io.ktor.server.routing.*
|
||||||
|
|
||||||
|
fun Routing.webfinger(userService:UserService){
|
||||||
|
route("/.well-known/webfinger"){
|
||||||
|
get {
|
||||||
|
val acct = call.request.queryParameters["resource"]?.decodeURLPart()
|
||||||
|
?: throw ParameterNotExistException("Parameter(name='resource') does not exist.")
|
||||||
|
|
||||||
|
if (acct.startsWith("acct:").not()) {
|
||||||
|
throw IllegalParameterException("Parameter(name='resource') is not start with 'acct:'.")
|
||||||
|
}
|
||||||
|
|
||||||
|
val accountName = acct.substringBeforeLast("@")
|
||||||
|
.substringAfter("acct:")
|
||||||
|
.trimStart('@')
|
||||||
|
|
||||||
|
val userEntity = userService.findByName(accountName)
|
||||||
|
|
||||||
|
val webFinger = WebFinger(
|
||||||
|
subject = acct,
|
||||||
|
links = listOf(
|
||||||
|
WebFinger.Link(
|
||||||
|
rel = "self",
|
||||||
|
type = ContentType.Application.Activity.toString(),
|
||||||
|
href = "${Config.configData.url}/users/${userEntity.name}"
|
||||||
|
)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
return@get call.respond(webFinger)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,16 +1,18 @@
|
||||||
package dev.usbharu.hideout.service
|
package dev.usbharu.hideout.service
|
||||||
|
|
||||||
|
import dev.usbharu.hideout.domain.model.UserAuthentication
|
||||||
import dev.usbharu.hideout.domain.model.UserAuthenticationEntity
|
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 registerAccount(username: String, hash: String)
|
||||||
|
|
||||||
suspend fun verifyAccount(username: String,password: String): Boolean
|
suspend fun verifyAccount(username: String, password: String): Boolean
|
||||||
|
|
||||||
suspend fun findByUserId(userId: Long):UserAuthenticationEntity
|
suspend fun findByUserId(userId: Long): UserAuthenticationEntity
|
||||||
|
|
||||||
suspend fun findByUsername(username: String):UserAuthenticationEntity
|
suspend fun findByUsername(username: String): UserAuthenticationEntity
|
||||||
|
suspend fun createAccount(userEntity: UserAuthentication): UserAuthenticationEntity
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,11 @@
|
||||||
|
package dev.usbharu.hideout.service.activitypub
|
||||||
|
|
||||||
|
import dev.usbharu.hideout.ap.Follow
|
||||||
|
import dev.usbharu.hideout.domain.model.ActivityPubResponse
|
||||||
|
import dev.usbharu.hideout.domain.model.job.ReceiveFollowJob
|
||||||
|
import kjob.core.job.JobProps
|
||||||
|
|
||||||
|
interface ActivityPubFollowService {
|
||||||
|
suspend fun receiveFollow(follow:Follow):ActivityPubResponse
|
||||||
|
suspend fun receiveFollowJob(props: JobProps<ReceiveFollowJob>)
|
||||||
|
}
|
|
@ -0,0 +1,52 @@
|
||||||
|
package dev.usbharu.hideout.service.activitypub
|
||||||
|
|
||||||
|
import com.fasterxml.jackson.module.kotlin.readValue
|
||||||
|
import dev.usbharu.hideout.ap.Accept
|
||||||
|
import dev.usbharu.hideout.ap.Follow
|
||||||
|
import dev.usbharu.hideout.config.Config
|
||||||
|
import dev.usbharu.hideout.domain.model.ActivityPubResponse
|
||||||
|
import dev.usbharu.hideout.domain.model.ActivityPubStringResponse
|
||||||
|
import dev.usbharu.hideout.domain.model.job.ReceiveFollowJob
|
||||||
|
import dev.usbharu.hideout.plugins.postAp
|
||||||
|
import dev.usbharu.hideout.service.impl.UserService
|
||||||
|
import dev.usbharu.hideout.service.job.JobQueueParentService
|
||||||
|
import io.ktor.client.*
|
||||||
|
import io.ktor.http.*
|
||||||
|
import kjob.core.job.JobProps
|
||||||
|
|
||||||
|
class ActivityPubFollowServiceImpl(
|
||||||
|
private val jobQueueParentService: JobQueueParentService,
|
||||||
|
private val activityPubUserService: ActivityPubUserService,
|
||||||
|
private val userService: UserService,
|
||||||
|
private val httpClient: HttpClient
|
||||||
|
) : ActivityPubFollowService {
|
||||||
|
override suspend fun receiveFollow(follow: Follow): ActivityPubResponse {
|
||||||
|
// TODO: Verify HTTP Signature
|
||||||
|
jobQueueParentService.schedule(ReceiveFollowJob) {
|
||||||
|
props[it.actor] = follow.actor
|
||||||
|
props[it.follow] = Config.configData.objectMapper.writeValueAsString(follow)
|
||||||
|
props[it.targetActor] = follow.`object`
|
||||||
|
}
|
||||||
|
return ActivityPubStringResponse(HttpStatusCode.OK, "{}", ContentType.Application.Json)
|
||||||
|
}
|
||||||
|
|
||||||
|
override suspend fun receiveFollowJob(props: JobProps<ReceiveFollowJob>) {
|
||||||
|
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]
|
||||||
|
httpClient.postAp(
|
||||||
|
urlString = person.inbox ?: throw IllegalArgumentException("inbox is not found"),
|
||||||
|
username = "$targetActor#pubkey",
|
||||||
|
jsonLd = Accept(
|
||||||
|
name = "Follow",
|
||||||
|
`object` = follow,
|
||||||
|
actor = targetActor
|
||||||
|
)
|
||||||
|
)
|
||||||
|
val users =
|
||||||
|
userService.findByUrls(listOf(targetActor, follow.actor ?: throw IllegalArgumentException("actor is null")))
|
||||||
|
|
||||||
|
userService.addFollowers(users.first { it.url == targetActor }.id, users.first { it.url == follow.actor }.id)
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,45 @@
|
||||||
|
package dev.usbharu.hideout.service.activitypub
|
||||||
|
|
||||||
|
import dev.usbharu.hideout.domain.model.ActivityPubResponse
|
||||||
|
import dev.usbharu.hideout.domain.model.job.HideoutJob
|
||||||
|
import kjob.core.dsl.JobContextWithProps
|
||||||
|
|
||||||
|
interface ActivityPubService {
|
||||||
|
fun parseActivity(json: String): ActivityType
|
||||||
|
|
||||||
|
suspend fun processActivity(json: String, type: ActivityType): ActivityPubResponse?
|
||||||
|
|
||||||
|
suspend fun <T : HideoutJob> processActivity(job: JobContextWithProps<T>,hideoutJob: HideoutJob)
|
||||||
|
}
|
||||||
|
|
||||||
|
enum class ActivityType {
|
||||||
|
Accept,
|
||||||
|
Add,
|
||||||
|
Announce,
|
||||||
|
Arrive,
|
||||||
|
Block,
|
||||||
|
Create,
|
||||||
|
Delete,
|
||||||
|
Dislike,
|
||||||
|
Flag,
|
||||||
|
Follow,
|
||||||
|
Ignore,
|
||||||
|
Invite,
|
||||||
|
Join,
|
||||||
|
Leave,
|
||||||
|
Like,
|
||||||
|
Listen,
|
||||||
|
Move,
|
||||||
|
Offer,
|
||||||
|
Question,
|
||||||
|
Reject,
|
||||||
|
Read,
|
||||||
|
Remove,
|
||||||
|
TentativeReject,
|
||||||
|
TentativeAccept,
|
||||||
|
Travel,
|
||||||
|
Undo,
|
||||||
|
Update,
|
||||||
|
View,
|
||||||
|
Other
|
||||||
|
}
|
|
@ -0,0 +1,82 @@
|
||||||
|
package dev.usbharu.hideout.service.activitypub
|
||||||
|
|
||||||
|
import com.fasterxml.jackson.databind.JsonNode
|
||||||
|
import dev.usbharu.hideout.ap.Follow
|
||||||
|
import dev.usbharu.hideout.config.Config
|
||||||
|
import dev.usbharu.hideout.domain.model.ActivityPubResponse
|
||||||
|
import dev.usbharu.hideout.domain.model.job.HideoutJob
|
||||||
|
import dev.usbharu.hideout.domain.model.job.ReceiveFollowJob
|
||||||
|
import dev.usbharu.hideout.exception.JsonParseException
|
||||||
|
import kjob.core.Job
|
||||||
|
import kjob.core.dsl.JobContextWithProps
|
||||||
|
import kjob.core.job.JobProps
|
||||||
|
import org.slf4j.LoggerFactory
|
||||||
|
import kotlin.reflect.full.createInstance
|
||||||
|
import kotlin.reflect.full.primaryConstructor
|
||||||
|
|
||||||
|
class ActivityPubServiceImpl(private val activityPubFollowService: ActivityPubFollowService) : ActivityPubService {
|
||||||
|
|
||||||
|
val logger = LoggerFactory.getLogger(this::class.java)
|
||||||
|
override fun parseActivity(json: String): ActivityType {
|
||||||
|
val readTree = Config.configData.objectMapper.readTree(json)
|
||||||
|
logger.debug("readTree: {}", readTree)
|
||||||
|
if (readTree.isObject.not()) {
|
||||||
|
throw JsonParseException("Json is not object.")
|
||||||
|
}
|
||||||
|
val type = readTree["type"]
|
||||||
|
if (type.isArray) {
|
||||||
|
return type.mapNotNull { jsonNode: JsonNode ->
|
||||||
|
ActivityType.values().firstOrNull { it.name.equals(jsonNode.asText(), true) }
|
||||||
|
}.first()
|
||||||
|
}
|
||||||
|
return ActivityType.values().first { it.name.equals(type.asText(), true) }
|
||||||
|
}
|
||||||
|
|
||||||
|
override suspend fun processActivity(json: String, type: ActivityType): ActivityPubResponse? {
|
||||||
|
return when (type) {
|
||||||
|
ActivityType.Accept -> TODO()
|
||||||
|
ActivityType.Add -> TODO()
|
||||||
|
ActivityType.Announce -> TODO()
|
||||||
|
ActivityType.Arrive -> TODO()
|
||||||
|
ActivityType.Block -> TODO()
|
||||||
|
ActivityType.Create -> TODO()
|
||||||
|
ActivityType.Delete -> TODO()
|
||||||
|
ActivityType.Dislike -> TODO()
|
||||||
|
ActivityType.Flag -> TODO()
|
||||||
|
ActivityType.Follow -> activityPubFollowService.receiveFollow(
|
||||||
|
Config.configData.objectMapper.readValue(
|
||||||
|
json,
|
||||||
|
Follow::class.java
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
ActivityType.Ignore -> TODO()
|
||||||
|
ActivityType.Invite -> TODO()
|
||||||
|
ActivityType.Join -> TODO()
|
||||||
|
ActivityType.Leave -> TODO()
|
||||||
|
ActivityType.Like -> TODO()
|
||||||
|
ActivityType.Listen -> TODO()
|
||||||
|
ActivityType.Move -> TODO()
|
||||||
|
ActivityType.Offer -> TODO()
|
||||||
|
ActivityType.Question -> TODO()
|
||||||
|
ActivityType.Reject -> TODO()
|
||||||
|
ActivityType.Read -> TODO()
|
||||||
|
ActivityType.Remove -> TODO()
|
||||||
|
ActivityType.TentativeReject -> TODO()
|
||||||
|
ActivityType.TentativeAccept -> TODO()
|
||||||
|
ActivityType.Travel -> TODO()
|
||||||
|
ActivityType.Undo -> TODO()
|
||||||
|
ActivityType.Update -> TODO()
|
||||||
|
ActivityType.View -> TODO()
|
||||||
|
ActivityType.Other -> TODO()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override suspend fun <T : HideoutJob> processActivity(job: JobContextWithProps<T>, hideoutJob: HideoutJob) {
|
||||||
|
when (hideoutJob) {
|
||||||
|
ReceiveFollowJob -> activityPubFollowService.receiveFollowJob(job.props as JobProps<ReceiveFollowJob>)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
}
|
|
@ -0,0 +1,9 @@
|
||||||
|
package dev.usbharu.hideout.service.activitypub
|
||||||
|
|
||||||
|
import dev.usbharu.hideout.ap.Person
|
||||||
|
|
||||||
|
interface ActivityPubUserService {
|
||||||
|
suspend fun getPersonByName(name:String):Person
|
||||||
|
|
||||||
|
suspend fun fetchPerson(url:String):Person
|
||||||
|
}
|
|
@ -0,0 +1,113 @@
|
||||||
|
package dev.usbharu.hideout.service.activitypub
|
||||||
|
|
||||||
|
import com.fasterxml.jackson.module.kotlin.readValue
|
||||||
|
import dev.usbharu.hideout.ap.Image
|
||||||
|
import dev.usbharu.hideout.ap.Key
|
||||||
|
import dev.usbharu.hideout.ap.Person
|
||||||
|
import dev.usbharu.hideout.config.Config
|
||||||
|
import dev.usbharu.hideout.domain.model.User
|
||||||
|
import dev.usbharu.hideout.domain.model.UserAuthentication
|
||||||
|
import dev.usbharu.hideout.exception.UserNotFoundException
|
||||||
|
import dev.usbharu.hideout.exception.ap.IllegalActivityPubObjectException
|
||||||
|
import dev.usbharu.hideout.service.IUserAuthService
|
||||||
|
import dev.usbharu.hideout.service.impl.UserService
|
||||||
|
import dev.usbharu.hideout.util.HttpUtil.Activity
|
||||||
|
import io.ktor.client.*
|
||||||
|
import io.ktor.client.request.*
|
||||||
|
import io.ktor.client.statement.*
|
||||||
|
import io.ktor.http.*
|
||||||
|
|
||||||
|
class ActivityPubUserServiceImpl(
|
||||||
|
private val userService: UserService,
|
||||||
|
private val userAuthService: IUserAuthService,
|
||||||
|
private val httpClient: HttpClient
|
||||||
|
) :
|
||||||
|
ActivityPubUserService {
|
||||||
|
override suspend fun getPersonByName(name: String): Person {
|
||||||
|
// TODO: JOINで書き直し
|
||||||
|
val userEntity = userService.findByName(name)
|
||||||
|
val userAuthEntity = userAuthService.findByUserId(userEntity.id)
|
||||||
|
val userUrl = "${Config.configData.url}/users/$name"
|
||||||
|
return Person(
|
||||||
|
type = emptyList(),
|
||||||
|
name = userEntity.name,
|
||||||
|
id = userUrl,
|
||||||
|
preferredUsername = name,
|
||||||
|
summary = userEntity.description,
|
||||||
|
inbox = "$userUrl/inbox",
|
||||||
|
outbox = "$userUrl/outbox",
|
||||||
|
url = userUrl,
|
||||||
|
icon = Image(
|
||||||
|
type = emptyList(),
|
||||||
|
name = "$userUrl/icon.png",
|
||||||
|
mediaType = "image/png",
|
||||||
|
url = "$userUrl/icon.png"
|
||||||
|
),
|
||||||
|
publicKey = Key(
|
||||||
|
type = emptyList(),
|
||||||
|
name = "Public Key",
|
||||||
|
id = "$userUrl#pubkey",
|
||||||
|
owner = userUrl,
|
||||||
|
publicKeyPem = userAuthEntity.publicKey
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
override suspend fun fetchPerson(url: String): Person {
|
||||||
|
return try {
|
||||||
|
val userEntity = userService.findByUrl(url)
|
||||||
|
val userAuthEntity = userAuthService.findByUsername(userEntity.name)
|
||||||
|
return Person(
|
||||||
|
type = emptyList(),
|
||||||
|
name = userEntity.name,
|
||||||
|
id = url,
|
||||||
|
preferredUsername = userEntity.name,
|
||||||
|
summary = userEntity.description,
|
||||||
|
inbox = "$url/inbox",
|
||||||
|
outbox = "$url/outbox",
|
||||||
|
url = url,
|
||||||
|
icon = Image(
|
||||||
|
type = emptyList(),
|
||||||
|
name = "$url/icon.png",
|
||||||
|
mediaType = "image/png",
|
||||||
|
url = "$url/icon.png"
|
||||||
|
),
|
||||||
|
publicKey = Key(
|
||||||
|
type = emptyList(),
|
||||||
|
name = "Public Key",
|
||||||
|
id = "$url#pubkey",
|
||||||
|
owner = url,
|
||||||
|
publicKeyPem = userAuthEntity.publicKey
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
} catch (e: UserNotFoundException) {
|
||||||
|
val httpResponse = httpClient.get(url) {
|
||||||
|
accept(ContentType.Application.Activity)
|
||||||
|
}
|
||||||
|
val person = Config.configData.objectMapper.readValue<Person>(httpResponse.bodyAsText())
|
||||||
|
val userEntity = userService.create(
|
||||||
|
User(
|
||||||
|
name = person.preferredUsername
|
||||||
|
?: throw IllegalActivityPubObjectException("preferredUsername is null"),
|
||||||
|
domain = url.substringAfter(":").substringBeforeLast("/"),
|
||||||
|
screenName = person.name ?: throw IllegalActivityPubObjectException("name is null"),
|
||||||
|
description = person.summary ?: throw IllegalActivityPubObjectException("summary is null"),
|
||||||
|
inbox = person.inbox ?: throw IllegalActivityPubObjectException("inbox is null"),
|
||||||
|
outbox = person.outbox ?: throw IllegalActivityPubObjectException("outbox is null"),
|
||||||
|
url = url
|
||||||
|
)
|
||||||
|
)
|
||||||
|
userAuthService.createAccount(
|
||||||
|
UserAuthentication(
|
||||||
|
userEntity.id,
|
||||||
|
null,
|
||||||
|
person.publicKey?.publicKeyPem ?: throw IllegalActivityPubObjectException("publicKey is null"),
|
||||||
|
null
|
||||||
|
)
|
||||||
|
)
|
||||||
|
person
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,6 +1,5 @@
|
||||||
package dev.usbharu.hideout.service
|
package dev.usbharu.hideout.service.impl
|
||||||
|
|
||||||
import com.fasterxml.jackson.databind.ObjectMapper
|
|
||||||
import dev.usbharu.hideout.config.Config
|
import dev.usbharu.hideout.config.Config
|
||||||
|
|
||||||
class ActivityPubService() {
|
class ActivityPubService() {
|
||||||
|
@ -10,7 +9,7 @@ class ActivityPubService() {
|
||||||
Undo
|
Undo
|
||||||
}
|
}
|
||||||
|
|
||||||
fun switchApType(json:String):ActivityType{
|
fun switchApType(json:String): ActivityType {
|
||||||
val typeAsText = Config.configData.objectMapper.readTree(json).get("type").asText()
|
val typeAsText = Config.configData.objectMapper.readTree(json).get("type").asText()
|
||||||
return when(typeAsText){
|
return when(typeAsText){
|
||||||
"Follow" -> ActivityType.Follow
|
"Follow" -> ActivityType.Follow
|
|
@ -1,8 +1,10 @@
|
||||||
package dev.usbharu.hideout.service
|
package dev.usbharu.hideout.service.impl
|
||||||
|
|
||||||
import dev.usbharu.hideout.ap.*
|
import dev.usbharu.hideout.ap.*
|
||||||
import dev.usbharu.hideout.config.Config
|
import dev.usbharu.hideout.config.Config
|
||||||
import dev.usbharu.hideout.plugins.postAp
|
import dev.usbharu.hideout.plugins.postAp
|
||||||
|
import dev.usbharu.hideout.service.IUserAuthService
|
||||||
|
import dev.usbharu.hideout.service.IWebFingerService
|
||||||
import io.ktor.client.*
|
import io.ktor.client.*
|
||||||
|
|
||||||
class ActivityPubUserService(
|
class ActivityPubUserService(
|
|
@ -1,4 +1,4 @@
|
||||||
package dev.usbharu.hideout.service
|
package dev.usbharu.hideout.service.impl
|
||||||
|
|
||||||
import java.security.PrivateKey
|
import java.security.PrivateKey
|
||||||
|
|
|
@ -1,19 +1,16 @@
|
||||||
package dev.usbharu.hideout.service
|
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.User
|
||||||
import dev.usbharu.hideout.domain.model.UserAuthentication
|
import dev.usbharu.hideout.domain.model.UserAuthentication
|
||||||
import dev.usbharu.hideout.domain.model.UserAuthenticationEntity
|
import dev.usbharu.hideout.domain.model.UserAuthenticationEntity
|
||||||
import dev.usbharu.hideout.domain.model.Users.screenName
|
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.IUserAuthRepository
|
||||||
import dev.usbharu.hideout.repository.IUserRepository
|
import dev.usbharu.hideout.repository.IUserRepository
|
||||||
|
import dev.usbharu.hideout.service.IUserAuthService
|
||||||
import io.ktor.util.*
|
import io.ktor.util.*
|
||||||
import java.security.KeyPair
|
import java.security.*
|
||||||
import java.security.KeyPairGenerator
|
|
||||||
import java.security.MessageDigest
|
|
||||||
import java.security.PrivateKey
|
|
||||||
import java.security.PublicKey
|
|
||||||
import java.security.interfaces.RSAPrivateKey
|
import java.security.interfaces.RSAPrivateKey
|
||||||
import java.security.interfaces.RSAPublicKey
|
import java.security.interfaces.RSAPublicKey
|
||||||
import java.util.*
|
import java.util.*
|
||||||
|
@ -35,11 +32,15 @@ class UserAuthService(
|
||||||
}
|
}
|
||||||
|
|
||||||
override suspend fun registerAccount(username: String, hash: String) {
|
override suspend fun registerAccount(username: String, hash: String) {
|
||||||
|
val url = "${Config.configData.url}/users/$username"
|
||||||
val registerUser = User(
|
val registerUser = User(
|
||||||
name = username,
|
name = username,
|
||||||
domain = Config.configData.domain,
|
domain = Config.configData.domain,
|
||||||
screenName = username,
|
screenName = username,
|
||||||
description = ""
|
description = "",
|
||||||
|
inbox = "$url/inbox",
|
||||||
|
outbox = "$url/outbox",
|
||||||
|
url = url
|
||||||
)
|
)
|
||||||
val createdUser = userRepository.create(registerUser)
|
val createdUser = userRepository.create(registerUser)
|
||||||
|
|
||||||
|
@ -76,6 +77,10 @@ class UserAuthService(
|
||||||
?: throw UserNotFoundException("$username auth data was not found")
|
?: throw UserNotFoundException("$username auth data was not found")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
override suspend fun createAccount(userEntity: UserAuthentication): UserAuthenticationEntity {
|
||||||
|
return userAuthRepository.create(userEntity)
|
||||||
|
}
|
||||||
|
|
||||||
private fun generateKeyPair(): KeyPair {
|
private fun generateKeyPair(): KeyPair {
|
||||||
val keyPairGenerator = KeyPairGenerator.getInstance("RSA")
|
val keyPairGenerator = KeyPairGenerator.getInstance("RSA")
|
||||||
keyPairGenerator.initialize(1024)
|
keyPairGenerator.initialize(1024)
|
||||||
|
@ -83,8 +88,6 @@ class UserAuthService(
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
val sha256: MessageDigest = MessageDigest.getInstance("SHA-256")
|
val sha256: MessageDigest = MessageDigest.getInstance("SHA-256")
|
||||||
}
|
}
|
|
@ -1,4 +1,4 @@
|
||||||
package dev.usbharu.hideout.service
|
package dev.usbharu.hideout.service.impl
|
||||||
|
|
||||||
import dev.usbharu.hideout.domain.model.User
|
import dev.usbharu.hideout.domain.model.User
|
||||||
import dev.usbharu.hideout.domain.model.UserEntity
|
import dev.usbharu.hideout.domain.model.UserEntity
|
||||||
|
@ -21,11 +21,27 @@ class UserService(private val userRepository: IUserRepository) {
|
||||||
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> {
|
||||||
|
return userRepository.findByIds(ids)
|
||||||
|
}
|
||||||
|
|
||||||
suspend fun findByName(name: String): UserEntity {
|
suspend fun findByName(name: String): UserEntity {
|
||||||
return userRepository.findByName(name)
|
return userRepository.findByName(name)
|
||||||
?: throw UserNotFoundException("$name was not found.")
|
?: throw UserNotFoundException("$name was not found.")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
suspend fun findByNameAndDomains(names: List<Pair<String,String>>): List<UserEntity> {
|
||||||
|
return userRepository.findByNameAndDomains(names)
|
||||||
|
}
|
||||||
|
|
||||||
|
suspend fun findByUrl(url: String): UserEntity {
|
||||||
|
return userRepository.findByUrl(url) ?: throw UserNotFoundException("$url was not found.")
|
||||||
|
}
|
||||||
|
|
||||||
|
suspend fun findByUrls(urls: List<String>): List<UserEntity> {
|
||||||
|
return userRepository.findByUrls(urls)
|
||||||
|
}
|
||||||
|
|
||||||
suspend fun create(user: User): UserEntity {
|
suspend fun create(user: User): UserEntity {
|
||||||
return userRepository.create(user)
|
return userRepository.create(user)
|
||||||
}
|
}
|
|
@ -1,8 +1,9 @@
|
||||||
package dev.usbharu.hideout.service
|
package dev.usbharu.hideout.service.impl
|
||||||
|
|
||||||
import dev.usbharu.hideout.ap.Person
|
import dev.usbharu.hideout.ap.Person
|
||||||
import dev.usbharu.hideout.domain.model.User
|
import dev.usbharu.hideout.domain.model.User
|
||||||
import dev.usbharu.hideout.domain.model.UserEntity
|
import dev.usbharu.hideout.domain.model.UserEntity
|
||||||
|
import dev.usbharu.hideout.service.IWebFingerService
|
||||||
import dev.usbharu.hideout.util.HttpUtil
|
import dev.usbharu.hideout.util.HttpUtil
|
||||||
import dev.usbharu.hideout.webfinger.WebFinger
|
import dev.usbharu.hideout.webfinger.WebFinger
|
||||||
import io.ktor.client.*
|
import io.ktor.client.*
|
||||||
|
@ -65,8 +66,12 @@ class WebFingerService(
|
||||||
userModel.preferredUsername ?: throw IllegalStateException(),
|
userModel.preferredUsername ?: throw IllegalStateException(),
|
||||||
domain,
|
domain,
|
||||||
userName,
|
userName,
|
||||||
userModel.summary.orEmpty()
|
userModel.summary.orEmpty(),
|
||||||
|
"",
|
||||||
|
"",
|
||||||
|
""
|
||||||
)
|
)
|
||||||
|
TODO()
|
||||||
return userService.create(user)
|
return userService.create(user)
|
||||||
}
|
}
|
||||||
}
|
}
|
|
@ -0,0 +1,10 @@
|
||||||
|
package dev.usbharu.hideout.service.job
|
||||||
|
|
||||||
|
import kjob.core.Job
|
||||||
|
import kjob.core.dsl.ScheduleContext
|
||||||
|
|
||||||
|
interface JobQueueParentService {
|
||||||
|
|
||||||
|
fun init(jobDefines:List<Job>)
|
||||||
|
suspend fun <J : Job> schedule(job: J, block: ScheduleContext<J>.(J) -> Unit = {})
|
||||||
|
}
|
|
@ -0,0 +1,10 @@
|
||||||
|
package dev.usbharu.hideout.service.job
|
||||||
|
|
||||||
|
import kjob.core.Job
|
||||||
|
import kjob.core.dsl.JobContextWithProps
|
||||||
|
import kjob.core.dsl.JobRegisterContext
|
||||||
|
import kjob.core.dsl.KJobFunctions
|
||||||
|
|
||||||
|
interface JobQueueWorkerService {
|
||||||
|
fun init(defines: List<Pair<Job, JobRegisterContext<Job, JobContextWithProps<Job>>.(Job) -> KJobFunctions<Job, JobContextWithProps<Job>>>>)
|
||||||
|
}
|
|
@ -0,0 +1,24 @@
|
||||||
|
package dev.usbharu.hideout.service.job
|
||||||
|
|
||||||
|
import dev.usbharu.kjob.exposed.ExposedKJob
|
||||||
|
import kjob.core.Job
|
||||||
|
import kjob.core.KJob
|
||||||
|
import kjob.core.dsl.ScheduleContext
|
||||||
|
import kjob.core.kjob
|
||||||
|
import org.jetbrains.exposed.sql.Database
|
||||||
|
|
||||||
|
class KJobJobQueueParentService(private val database: Database) : JobQueueParentService {
|
||||||
|
|
||||||
|
val kjob: KJob = kjob(ExposedKJob) {
|
||||||
|
connectionDatabase = database
|
||||||
|
isWorker = false
|
||||||
|
}.start()
|
||||||
|
|
||||||
|
override fun init(jobDefines: List<Job>) {
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
override suspend fun <J : Job> schedule(job: J,block:ScheduleContext<J>.(J)->Unit) {
|
||||||
|
kjob.schedule(job,block)
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,28 @@
|
||||||
|
package dev.usbharu.hideout.service.job
|
||||||
|
|
||||||
|
import dev.usbharu.kjob.exposed.ExposedKJob
|
||||||
|
import kjob.core.Job
|
||||||
|
import kjob.core.dsl.JobContextWithProps
|
||||||
|
import kjob.core.dsl.JobRegisterContext
|
||||||
|
import kjob.core.dsl.KJobFunctions
|
||||||
|
import kjob.core.kjob
|
||||||
|
import org.jetbrains.exposed.sql.Database
|
||||||
|
|
||||||
|
class KJobJobQueueWorkerService(private val database: Database) : JobQueueWorkerService {
|
||||||
|
|
||||||
|
val kjob by lazy {
|
||||||
|
kjob(ExposedKJob) {
|
||||||
|
connectionDatabase = database
|
||||||
|
nonBlockingMaxJobs = 10
|
||||||
|
blockingMaxJobs = 10
|
||||||
|
jobExecutionPeriodInSeconds = 10
|
||||||
|
}.start()
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun init(defines: List<Pair<Job,JobRegisterContext<Job, JobContextWithProps<Job>>.(Job) -> KJobFunctions<Job, JobContextWithProps<Job>>>>) {
|
||||||
|
defines.forEach { job ->
|
||||||
|
kjob.register(job.first, job.second)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -0,0 +1,7 @@
|
||||||
|
package dev.usbharu.hideout.service.signature
|
||||||
|
|
||||||
|
import io.ktor.http.*
|
||||||
|
|
||||||
|
interface HttpSignatureVerifyService {
|
||||||
|
fun verify(headers:Headers):Boolean
|
||||||
|
}
|
|
@ -0,0 +1,24 @@
|
||||||
|
package dev.usbharu.hideout.service.signature
|
||||||
|
|
||||||
|
import dev.usbharu.hideout.plugins.KtorKeyMap
|
||||||
|
import dev.usbharu.hideout.service.IUserAuthService
|
||||||
|
import io.ktor.http.*
|
||||||
|
import tech.barbero.http.message.signing.HttpMessage
|
||||||
|
import tech.barbero.http.message.signing.SignatureHeaderVerifier
|
||||||
|
|
||||||
|
class HttpSignatureVerifyServiceImpl(private val userAuthService: IUserAuthService) : HttpSignatureVerifyService {
|
||||||
|
override fun verify(headers: Headers): Boolean {
|
||||||
|
val build = SignatureHeaderVerifier.builder().keyMap(KtorKeyMap(userAuthService)).build()
|
||||||
|
return true;
|
||||||
|
build.verify(object : HttpMessage {
|
||||||
|
override fun headerValues(name: String?): MutableList<String> {
|
||||||
|
return name?.let { headers.getAll(it) }?.toMutableList() ?: mutableListOf()
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun addHeader(name: String?, value: String?) {
|
||||||
|
TODO()
|
||||||
|
}
|
||||||
|
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
|
@ -28,5 +28,8 @@ object HttpUtil {
|
||||||
|
|
||||||
val ContentType.Application.Activity: ContentType
|
val ContentType.Application.Activity: ContentType
|
||||||
get() = ContentType("application", "activity+json")
|
get() = ContentType("application", "activity+json")
|
||||||
|
|
||||||
|
val ContentType.Application.JsonLd: ContentType
|
||||||
|
get() = ContentType("application", "ld+json", listOf(HeaderValueParam("profile", "https://www.w3.org/ns/activitystreams")))
|
||||||
// fun
|
// fun
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,290 @@
|
||||||
|
package dev.usbharu.kjob.exposed
|
||||||
|
|
||||||
|
import kjob.core.job.JobProgress
|
||||||
|
import kjob.core.job.JobSettings
|
||||||
|
import kjob.core.job.JobStatus
|
||||||
|
import kjob.core.job.ScheduledJob
|
||||||
|
import kjob.core.repository.JobRepository
|
||||||
|
import kotlinx.coroutines.Dispatchers
|
||||||
|
import kotlinx.coroutines.flow.Flow
|
||||||
|
import kotlinx.coroutines.flow.asFlow
|
||||||
|
import kotlinx.serialization.encodeToString
|
||||||
|
import kotlinx.serialization.json.*
|
||||||
|
import org.jetbrains.exposed.dao.id.LongIdTable
|
||||||
|
import org.jetbrains.exposed.sql.*
|
||||||
|
import org.jetbrains.exposed.sql.SqlExpressionBuilder.eq
|
||||||
|
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.transactions.experimental.newSuspendedTransaction
|
||||||
|
import org.jetbrains.exposed.sql.transactions.transaction
|
||||||
|
import java.time.Clock
|
||||||
|
import java.time.Instant
|
||||||
|
import java.util.*
|
||||||
|
|
||||||
|
class ExposedJobRepository(
|
||||||
|
private val database: Database,
|
||||||
|
private val tableName: String,
|
||||||
|
private val clock: Clock,
|
||||||
|
private val json: Json
|
||||||
|
) :
|
||||||
|
JobRepository {
|
||||||
|
|
||||||
|
class Jobs(tableName: String) : LongIdTable(tableName) {
|
||||||
|
val status = text("status")
|
||||||
|
val runAt = long("runAt").nullable()
|
||||||
|
val statusMessage = text("statusMessage").nullable()
|
||||||
|
val retries = integer("retries")
|
||||||
|
val kjobId = char("kjobId", 36).nullable()
|
||||||
|
val createdAt = long("createdAt")
|
||||||
|
val updatedAt = long("updatedAt")
|
||||||
|
val jobId = text("jobId")
|
||||||
|
val name = text("name")
|
||||||
|
val properties = text("properties").nullable()
|
||||||
|
val step = integer("step")
|
||||||
|
val max = integer("max").nullable()
|
||||||
|
val startedAt = long("startedAt").nullable()
|
||||||
|
val completedAt = long("completedAt").nullable()
|
||||||
|
}
|
||||||
|
|
||||||
|
val jobs: Jobs = Jobs(tableName)
|
||||||
|
|
||||||
|
fun createTable() {
|
||||||
|
transaction(database) {
|
||||||
|
SchemaUtils.create(jobs)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
suspend fun <T> query(block: suspend () -> T): T = newSuspendedTransaction(Dispatchers.IO) { block() }
|
||||||
|
|
||||||
|
|
||||||
|
override suspend fun completeProgress(id: String): Boolean {
|
||||||
|
val now = Instant.now(clock).toEpochMilli()
|
||||||
|
return query {
|
||||||
|
jobs.update({ jobs.id eq id.toLong() }) {
|
||||||
|
it[jobs.completedAt] = now
|
||||||
|
it[jobs.updatedAt] = now
|
||||||
|
} == 1
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override suspend fun exist(jobId: String): Boolean {
|
||||||
|
return query {
|
||||||
|
jobs.select(jobs.jobId eq jobId).empty().not()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override suspend fun findNext(names: Set<String>, status: Set<JobStatus>, limit: Int): Flow<ScheduledJob> {
|
||||||
|
return query {
|
||||||
|
jobs.select(
|
||||||
|
jobs.status.inList(list = status.map { it.name })
|
||||||
|
.and(if (names.isEmpty()) Op.TRUE else jobs.name.inList(names))
|
||||||
|
).limit(limit)
|
||||||
|
.map { it.toScheduledJob() }.asFlow()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override suspend fun get(id: String): ScheduledJob? {
|
||||||
|
val single = query { jobs.select(jobs.id eq id.toLong()).singleOrNull() } ?: return null
|
||||||
|
return single.toScheduledJob()
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
override suspend fun reset(id: String, oldKjobId: UUID?): Boolean {
|
||||||
|
return query {
|
||||||
|
jobs.update({ jobs.id eq id.toLong() and if (oldKjobId == null) jobs.kjobId.isNull() else jobs.kjobId eq oldKjobId.toString() }) {
|
||||||
|
it[jobs.status] = JobStatus.CREATED.name
|
||||||
|
it[jobs.statusMessage] = null
|
||||||
|
it[jobs.kjobId] = null
|
||||||
|
it[jobs.step] = 0
|
||||||
|
it[jobs.max] = null
|
||||||
|
it[jobs.startedAt] = null
|
||||||
|
it[jobs.completedAt] = null
|
||||||
|
it[jobs.updatedAt] = Instant.now(clock).toEpochMilli()
|
||||||
|
} == 1
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override suspend fun save(jobSettings: JobSettings, runAt: Instant?): ScheduledJob {
|
||||||
|
val now = Instant.now(clock)
|
||||||
|
val scheduledJob =
|
||||||
|
ScheduledJob("", JobStatus.CREATED, runAt, null, 0, null, now, now, jobSettings, JobProgress(0))
|
||||||
|
val id = query {
|
||||||
|
jobs.insert {
|
||||||
|
it[jobs.status] = scheduledJob.status.name
|
||||||
|
it[jobs.createdAt] = scheduledJob.createdAt.toEpochMilli()
|
||||||
|
it[jobs.updatedAt] = scheduledJob.updatedAt.toEpochMilli()
|
||||||
|
it[jobs.jobId] = scheduledJob.settings.id
|
||||||
|
it[jobs.name] = scheduledJob.settings.name
|
||||||
|
it[jobs.properties] = scheduledJob.settings.properties.stringify()
|
||||||
|
it[jobs.runAt] = scheduledJob.runAt?.toEpochMilli()
|
||||||
|
it[jobs.statusMessage] = null
|
||||||
|
it[jobs.retries] = 0
|
||||||
|
it[jobs.kjobId] = null
|
||||||
|
it[jobs.step] = 0
|
||||||
|
it[jobs.max] = null
|
||||||
|
it[jobs.startedAt] = null
|
||||||
|
it[jobs.completedAt] = null
|
||||||
|
}[jobs.id].value
|
||||||
|
}
|
||||||
|
return scheduledJob.copy(id = id.toString())
|
||||||
|
}
|
||||||
|
|
||||||
|
override suspend fun setProgressMax(id: String, max: Long): Boolean {
|
||||||
|
val now = Instant.now(clock).toEpochMilli()
|
||||||
|
return query {
|
||||||
|
jobs.update({ jobs.id eq id.toLong() }) {
|
||||||
|
it[jobs.max] = max.toInt()
|
||||||
|
it[jobs.updatedAt] = now
|
||||||
|
} == 1
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override suspend fun startProgress(id: String): Boolean {
|
||||||
|
val now = Instant.now(clock).toEpochMilli()
|
||||||
|
return query {
|
||||||
|
jobs.update({ jobs.id eq id.toLong() }) {
|
||||||
|
it[jobs.startedAt] = now
|
||||||
|
it[jobs.updatedAt] = now
|
||||||
|
} == 1
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override suspend fun stepProgress(id: String, step: Long): Boolean {
|
||||||
|
val now = Instant.now(clock).toEpochMilli()
|
||||||
|
return query {
|
||||||
|
jobs.update({ jobs.id eq id.toLong() }) {
|
||||||
|
it[jobs.step] = jobs.step + step.toInt()
|
||||||
|
it[jobs.updatedAt] = now
|
||||||
|
} == 1
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override suspend fun update(
|
||||||
|
id: String,
|
||||||
|
oldKjobId: UUID?,
|
||||||
|
kjobId: UUID?,
|
||||||
|
status: JobStatus,
|
||||||
|
statusMessage: String?,
|
||||||
|
retries: Int
|
||||||
|
): Boolean {
|
||||||
|
return query {
|
||||||
|
jobs.update({ (jobs.id eq id.toLong()) and if (oldKjobId == null) jobs.kjobId.isNull() else jobs.kjobId eq oldKjobId.toString() }) {
|
||||||
|
it[jobs.status] = status.name
|
||||||
|
it[jobs.retries] = retries
|
||||||
|
it[jobs.updatedAt] = Instant.now(clock).toEpochMilli()
|
||||||
|
it[jobs.id] = id.toLong()
|
||||||
|
it[jobs.statusMessage] = statusMessage
|
||||||
|
it[jobs.kjobId] = kjobId.toString()
|
||||||
|
} == 1
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun String?.parseJsonMap(): Map<String, Any> {
|
||||||
|
this ?: return emptyMap()
|
||||||
|
return json.parseToJsonElement(this).jsonObject.mapValues { (_, el) ->
|
||||||
|
if (el is JsonObject) {
|
||||||
|
val t = el["t"]?.jsonPrimitive?.content ?: error("Cannot get jsonPrimitive")
|
||||||
|
val value = el["v"]?.jsonArray ?: error("Cannot get jsonArray")
|
||||||
|
when (t) {
|
||||||
|
"s" -> value.map { it.jsonPrimitive.content }
|
||||||
|
"d" -> value.map { it.jsonPrimitive.double }
|
||||||
|
"l" -> value.map { it.jsonPrimitive.long }
|
||||||
|
"i" -> value.map { it.jsonPrimitive.int }
|
||||||
|
"b" -> value.map { it.jsonPrimitive.boolean }
|
||||||
|
else -> error("Unknown type prefix '$t'")
|
||||||
|
}.toList()
|
||||||
|
} else {
|
||||||
|
val content = el.jsonPrimitive.content
|
||||||
|
val t = content.substringBefore(':')
|
||||||
|
val value = content.substringAfter(':')
|
||||||
|
when (t) {
|
||||||
|
"s" -> value
|
||||||
|
"d" -> value.toDouble()
|
||||||
|
"l" -> value.toLong()
|
||||||
|
"i" -> value.toInt()
|
||||||
|
"b" -> value.toBoolean()
|
||||||
|
else -> error("Unknown type prefix '$t'")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun Map<String, Any>.stringify(): String? {
|
||||||
|
if (isEmpty()) {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
fun listSerialize(value: List<*>): JsonElement {
|
||||||
|
return if (value.isEmpty()) {
|
||||||
|
buildJsonObject {
|
||||||
|
put("t", "s")
|
||||||
|
putJsonArray("v") {}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
val (t, values) = when (val item = value.first()) {
|
||||||
|
is Double -> "d" to (value as List<Double>).map(::JsonPrimitive)
|
||||||
|
is Long -> "l" to (value as List<Long>).map(::JsonPrimitive)
|
||||||
|
is Int -> "i" to (value as List<Int>).map(::JsonPrimitive)
|
||||||
|
is String -> "s" to (value as List<String>).map(::JsonPrimitive)
|
||||||
|
is Boolean -> "b" to (value as List<Boolean>).map(::JsonPrimitive)
|
||||||
|
else -> error("Cannot serialize unsupported list property value: $value")
|
||||||
|
}
|
||||||
|
buildJsonObject {
|
||||||
|
put("t", t)
|
||||||
|
put("v", JsonArray(values))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun createJsonPrimitive(string: String, value: Any) = JsonPrimitive("$string:$value")
|
||||||
|
|
||||||
|
val jsonObject = JsonObject(
|
||||||
|
mapValues { (_, value) ->
|
||||||
|
when (value) {
|
||||||
|
is List<*> -> listSerialize(value)
|
||||||
|
is Double -> createJsonPrimitive("d", value)
|
||||||
|
is Long -> createJsonPrimitive("l", value)
|
||||||
|
is Int -> createJsonPrimitive("i", value)
|
||||||
|
is String -> createJsonPrimitive("s", value)
|
||||||
|
is Boolean -> createJsonPrimitive("b", value)
|
||||||
|
else -> error("Cannot serialize unsupported property value: $value")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)
|
||||||
|
return json.encodeToString(jsonObject)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun ResultRow.toScheduledJob(): ScheduledJob {
|
||||||
|
val single = this
|
||||||
|
jobs.run {
|
||||||
|
return ScheduledJob(
|
||||||
|
id = single[this.id].value.toString(),
|
||||||
|
status = JobStatus.valueOf(single[status]),
|
||||||
|
runAt = single[runAt]?.let { Instant.ofEpochMilli(it) },
|
||||||
|
statusMessage = single[statusMessage],
|
||||||
|
retries = single[retries],
|
||||||
|
kjobId = single[kjobId]?.let {
|
||||||
|
try {
|
||||||
|
UUID.fromString(it)
|
||||||
|
} catch (e: IllegalArgumentException) {
|
||||||
|
null
|
||||||
|
}
|
||||||
|
},
|
||||||
|
createdAt = Instant.ofEpochMilli(single[createdAt]),
|
||||||
|
updatedAt = Instant.ofEpochMilli(single[updatedAt]),
|
||||||
|
settings = JobSettings(
|
||||||
|
id = single[jobId],
|
||||||
|
name = single[name],
|
||||||
|
properties = single[properties].parseJsonMap()
|
||||||
|
),
|
||||||
|
progress = JobProgress(
|
||||||
|
step = single[step].toLong(),
|
||||||
|
max = single[max]?.toLong(),
|
||||||
|
startedAt = single[startedAt]?.let { Instant.ofEpochMilli(it) },
|
||||||
|
completedAt = single[completedAt]?.let { Instant.ofEpochMilli(it) }
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,50 @@
|
||||||
|
package dev.usbharu.kjob.exposed
|
||||||
|
|
||||||
|
import kjob.core.BaseKJob
|
||||||
|
import kjob.core.KJob
|
||||||
|
import kjob.core.KJobFactory
|
||||||
|
import kotlinx.coroutines.runBlocking
|
||||||
|
import org.jetbrains.exposed.sql.Database
|
||||||
|
import java.time.Clock
|
||||||
|
|
||||||
|
class ExposedKJob(config: Configuration) : BaseKJob<ExposedKJob.Configuration>(config) {
|
||||||
|
|
||||||
|
companion object : KJobFactory<ExposedKJob, Configuration> {
|
||||||
|
override fun create(configure: Configuration.() -> Unit): KJob {
|
||||||
|
return ExposedKJob(Configuration().apply(configure))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class Configuration : BaseKJob.Configuration() {
|
||||||
|
var connectionString: String? = null
|
||||||
|
var driverClassName: String? = null
|
||||||
|
var connectionDatabase: Database? = null
|
||||||
|
|
||||||
|
var jobTableName = "kjobJobs"
|
||||||
|
|
||||||
|
var lockTableName = "kjobLocks"
|
||||||
|
|
||||||
|
var expireLockInMinutes = 5L
|
||||||
|
}
|
||||||
|
|
||||||
|
private val database: Database = config.connectionDatabase ?: Database.connect(
|
||||||
|
requireNotNull(config.connectionString),
|
||||||
|
requireNotNull(config.driverClassName)
|
||||||
|
)
|
||||||
|
|
||||||
|
override val jobRepository: ExposedJobRepository
|
||||||
|
get() = ExposedJobRepository(database, config.jobTableName, Clock.systemUTC(), config.json)
|
||||||
|
override val lockRepository: ExposedLockRepository
|
||||||
|
get() = ExposedLockRepository(database, config, clock)
|
||||||
|
|
||||||
|
override fun start(): KJob {
|
||||||
|
jobRepository.createTable()
|
||||||
|
lockRepository.createTable()
|
||||||
|
return super.start()
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun shutdown() = runBlocking {
|
||||||
|
super.shutdown()
|
||||||
|
lockRepository.clearExpired()
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,74 @@
|
||||||
|
package dev.usbharu.kjob.exposed
|
||||||
|
|
||||||
|
import kjob.core.job.Lock
|
||||||
|
import kjob.core.repository.LockRepository
|
||||||
|
import kotlinx.coroutines.Dispatchers
|
||||||
|
import org.jetbrains.exposed.dao.id.UUIDTable
|
||||||
|
import org.jetbrains.exposed.sql.*
|
||||||
|
import org.jetbrains.exposed.sql.SqlExpressionBuilder.eq
|
||||||
|
import org.jetbrains.exposed.sql.SqlExpressionBuilder.greater
|
||||||
|
import org.jetbrains.exposed.sql.transactions.experimental.newSuspendedTransaction
|
||||||
|
import org.jetbrains.exposed.sql.transactions.transaction
|
||||||
|
import java.time.Clock
|
||||||
|
import java.time.Instant
|
||||||
|
import java.util.*
|
||||||
|
import kotlin.time.Duration.Companion.minutes
|
||||||
|
|
||||||
|
class ExposedLockRepository(
|
||||||
|
private val database: Database,
|
||||||
|
private val config: ExposedKJob.Configuration,
|
||||||
|
private val clock: Clock
|
||||||
|
) : LockRepository {
|
||||||
|
|
||||||
|
class Locks(tableName: String) : UUIDTable(tableName) {
|
||||||
|
val updatedAt = long("updatedAt")
|
||||||
|
val expiresAt = long("expiresAt")
|
||||||
|
}
|
||||||
|
|
||||||
|
val locks: Locks = Locks(config.lockTableName)
|
||||||
|
|
||||||
|
fun createTable() {
|
||||||
|
transaction(database) {
|
||||||
|
SchemaUtils.create(locks)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
suspend fun <T> query(block: suspend () -> T): T = newSuspendedTransaction(Dispatchers.IO) { block() }
|
||||||
|
|
||||||
|
override suspend fun exists(id: UUID): Boolean {
|
||||||
|
val now = Instant.now(clock)
|
||||||
|
return query {
|
||||||
|
locks.select(locks.id eq id and locks.expiresAt.greater(now.toEpochMilli())).empty().not()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override suspend fun ping(id: UUID): Lock {
|
||||||
|
val now = Instant.now(clock)
|
||||||
|
val expiresAt = now.plusSeconds(config.expireLockInMinutes.minutes.inWholeSeconds)
|
||||||
|
val lock = Lock(id, now)
|
||||||
|
query {
|
||||||
|
if (locks.select(locks.id eq id).limit(1)
|
||||||
|
.map { Lock(it[locks.id].value, Instant.ofEpochMilli(it[locks.expiresAt])) }.isEmpty()
|
||||||
|
) {
|
||||||
|
locks.insert {
|
||||||
|
it[locks.id] = id
|
||||||
|
it[locks.updatedAt] = now.toEpochMilli()
|
||||||
|
it[locks.expiresAt] = expiresAt.toEpochMilli()
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
locks.update({ locks.id eq id }) {
|
||||||
|
it[locks.updatedAt] = now.toEpochMilli()
|
||||||
|
it[locks.expiresAt] = expiresAt.toEpochMilli()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return lock
|
||||||
|
}
|
||||||
|
|
||||||
|
suspend fun clearExpired() {
|
||||||
|
val now = Instant.now(clock).toEpochMilli()
|
||||||
|
query {
|
||||||
|
locks.deleteWhere { locks.expiresAt greater now }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -6,13 +6,13 @@ ktor {
|
||||||
watch = [classes, resources]
|
watch = [classes, resources]
|
||||||
}
|
}
|
||||||
application {
|
application {
|
||||||
modules = [dev.usbharu.hideout.ApplicationKt.module]
|
modules = [dev.usbharu.hideout.ApplicationKt.parent,dev.usbharu.hideout.ApplicationKt.worker]
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
hideout {
|
hideout {
|
||||||
hostname = "https://localhost:8080"
|
url = "http://localhost:8080"
|
||||||
hostname = ${?HOSTNAME}
|
|
||||||
database {
|
database {
|
||||||
url = "jdbc:h2:./test;MODE=POSTGRESQL"
|
url = "jdbc:h2:./test;MODE=POSTGRESQL"
|
||||||
driver = "org.h2.Driver"
|
driver = "org.h2.Driver"
|
||||||
|
|
|
@ -0,0 +1,7 @@
|
||||||
|
package dev.usbharu.hideout
|
||||||
|
|
||||||
|
import io.ktor.server.application.*
|
||||||
|
|
||||||
|
fun Application.empty(){
|
||||||
|
|
||||||
|
}
|
|
@ -7,8 +7,8 @@ import dev.usbharu.hideout.domain.model.UserAuthenticationEntity
|
||||||
import dev.usbharu.hideout.domain.model.UserEntity
|
import dev.usbharu.hideout.domain.model.UserEntity
|
||||||
import dev.usbharu.hideout.repository.IUserAuthRepository
|
import dev.usbharu.hideout.repository.IUserAuthRepository
|
||||||
import dev.usbharu.hideout.repository.IUserRepository
|
import dev.usbharu.hideout.repository.IUserRepository
|
||||||
import dev.usbharu.hideout.service.UserAuthService
|
import dev.usbharu.hideout.service.impl.UserAuthService
|
||||||
import dev.usbharu.hideout.service.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.*
|
||||||
import io.ktor.client.plugins.logging.*
|
import io.ktor.client.plugins.logging.*
|
||||||
|
@ -17,7 +17,6 @@ import org.junit.jupiter.api.Test
|
||||||
import java.security.KeyPairGenerator
|
import java.security.KeyPairGenerator
|
||||||
import java.security.interfaces.RSAPrivateKey
|
import java.security.interfaces.RSAPrivateKey
|
||||||
import java.security.interfaces.RSAPublicKey
|
import java.security.interfaces.RSAPublicKey
|
||||||
import java.util.*
|
|
||||||
|
|
||||||
class ActivityPubKtTest {
|
class ActivityPubKtTest {
|
||||||
@Test
|
@Test
|
||||||
|
@ -32,8 +31,24 @@ class ActivityPubKtTest {
|
||||||
TODO("Not yet implemented")
|
TODO("Not yet implemented")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
override suspend fun findByIds(ids: List<Long>): List<UserEntity> {
|
||||||
|
TODO("Not yet implemented")
|
||||||
|
}
|
||||||
|
|
||||||
override suspend fun findByName(name: String): UserEntity? {
|
override suspend fun findByName(name: String): UserEntity? {
|
||||||
return UserEntity(1, "test", "localhost", "test", "")
|
return UserEntity(1, "test", "localhost", "test", "","","","")
|
||||||
|
}
|
||||||
|
|
||||||
|
override suspend fun findByNameAndDomains(names: List<Pair<String, String>>): List<UserEntity> {
|
||||||
|
TODO("Not yet implemented")
|
||||||
|
}
|
||||||
|
|
||||||
|
override suspend fun findByUrl(url: String): UserEntity? {
|
||||||
|
TODO("Not yet implemented")
|
||||||
|
}
|
||||||
|
|
||||||
|
override suspend fun findByUrls(urls: List<String>): List<UserEntity> {
|
||||||
|
TODO("Not yet implemented")
|
||||||
}
|
}
|
||||||
|
|
||||||
override suspend fun update(userEntity: UserEntity) {
|
override suspend fun update(userEntity: UserEntity) {
|
||||||
|
|
|
@ -6,13 +6,12 @@ import dev.usbharu.hideout.domain.model.UserAuthenticationEntity
|
||||||
import dev.usbharu.hideout.domain.model.UserEntity
|
import dev.usbharu.hideout.domain.model.UserEntity
|
||||||
import dev.usbharu.hideout.repository.IUserAuthRepository
|
import dev.usbharu.hideout.repository.IUserAuthRepository
|
||||||
import dev.usbharu.hideout.repository.IUserRepository
|
import dev.usbharu.hideout.repository.IUserRepository
|
||||||
import dev.usbharu.hideout.service.UserAuthService
|
import dev.usbharu.hideout.service.impl.UserAuthService
|
||||||
import dev.usbharu.hideout.service.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.security.interfaces.RSAPrivateKey
|
||||||
import java.security.interfaces.RSAPublicKey
|
import java.security.interfaces.RSAPublicKey
|
||||||
import java.util.*
|
|
||||||
|
|
||||||
class KtorKeyMapTest {
|
class KtorKeyMapTest {
|
||||||
|
|
||||||
|
@ -27,8 +26,24 @@ class KtorKeyMapTest {
|
||||||
TODO("Not yet implemented")
|
TODO("Not yet implemented")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
override suspend fun findByIds(ids: List<Long>): List<UserEntity> {
|
||||||
|
TODO("Not yet implemented")
|
||||||
|
}
|
||||||
|
|
||||||
override suspend fun findByName(name: String): UserEntity? {
|
override suspend fun findByName(name: String): UserEntity? {
|
||||||
return UserEntity(1, "test", "localhost", "test", "")
|
return UserEntity(1, "test", "localhost", "test", "","","","")
|
||||||
|
}
|
||||||
|
|
||||||
|
override suspend fun findByNameAndDomains(names: List<Pair<String, String>>): List<UserEntity> {
|
||||||
|
TODO("Not yet implemented")
|
||||||
|
}
|
||||||
|
|
||||||
|
override suspend fun findByUrl(url: String): UserEntity? {
|
||||||
|
TODO("Not yet implemented")
|
||||||
|
}
|
||||||
|
|
||||||
|
override suspend fun findByUrls(urls: List<String>): List<UserEntity> {
|
||||||
|
TODO("Not yet implemented")
|
||||||
}
|
}
|
||||||
|
|
||||||
override suspend fun update(userEntity: UserEntity) {
|
override suspend fun update(userEntity: UserEntity) {
|
||||||
|
|
|
@ -0,0 +1,120 @@
|
||||||
|
@file:OptIn(ExperimentalCoroutinesApi::class)
|
||||||
|
|
||||||
|
package dev.usbharu.hideout.repository
|
||||||
|
|
||||||
|
import dev.usbharu.hideout.domain.model.User
|
||||||
|
import dev.usbharu.hideout.domain.model.Users
|
||||||
|
import dev.usbharu.hideout.domain.model.UsersFollowers
|
||||||
|
import kotlinx.coroutines.ExperimentalCoroutinesApi
|
||||||
|
import kotlinx.coroutines.test.runTest
|
||||||
|
import org.jetbrains.exposed.sql.Database
|
||||||
|
import org.jetbrains.exposed.sql.SchemaUtils
|
||||||
|
import org.jetbrains.exposed.sql.select
|
||||||
|
import org.jetbrains.exposed.sql.transactions.transaction
|
||||||
|
import org.junit.jupiter.api.AfterEach
|
||||||
|
import org.junit.jupiter.api.Assertions.assertIterableEquals
|
||||||
|
import org.junit.jupiter.api.BeforeEach
|
||||||
|
import org.junit.jupiter.api.Test
|
||||||
|
|
||||||
|
|
||||||
|
class UserRepositoryTest {
|
||||||
|
|
||||||
|
lateinit var db: Database
|
||||||
|
|
||||||
|
@BeforeEach
|
||||||
|
fun beforeEach() {
|
||||||
|
db = Database.connect("jdbc:h2:mem:test;DB_CLOSE_DELAY=-1", driver = "org.h2.Driver")
|
||||||
|
transaction(db) {
|
||||||
|
SchemaUtils.create(Users)
|
||||||
|
SchemaUtils.create(UsersFollowers)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@AfterEach
|
||||||
|
fun tearDown() {
|
||||||
|
transaction(db) {
|
||||||
|
|
||||||
|
SchemaUtils.drop(UsersFollowers)
|
||||||
|
SchemaUtils.drop(Users)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `findFollowersById フォロワー一覧を取得`() = runTest {
|
||||||
|
val userRepository = UserRepository(db)
|
||||||
|
val user = userRepository.create(
|
||||||
|
User(
|
||||||
|
"test",
|
||||||
|
"example.com",
|
||||||
|
"testUser",
|
||||||
|
"This user is test user.",
|
||||||
|
"https://example.com/inbox",
|
||||||
|
"https://example.com/outbox",
|
||||||
|
"https://example.com"
|
||||||
|
)
|
||||||
|
)
|
||||||
|
val follower = userRepository.create(
|
||||||
|
User(
|
||||||
|
"follower",
|
||||||
|
"follower.example.com",
|
||||||
|
"followerUser",
|
||||||
|
"This user is follower user.",
|
||||||
|
"https://follower.example.com/inbox",
|
||||||
|
"https://follower.example.com/outbox",
|
||||||
|
"https://follower.example.com"
|
||||||
|
)
|
||||||
|
)
|
||||||
|
val follower2 = userRepository.create(
|
||||||
|
User(
|
||||||
|
"follower2",
|
||||||
|
"follower2.example.com",
|
||||||
|
"followerUser2",
|
||||||
|
"This user is follower user 2.",
|
||||||
|
"https://follower2.example.com/inbox",
|
||||||
|
"https://follower2.example.com/outbox",
|
||||||
|
"https://follower2.example.com"
|
||||||
|
)
|
||||||
|
)
|
||||||
|
userRepository.createFollower(user.id, follower.id)
|
||||||
|
userRepository.createFollower(user.id, follower2.id)
|
||||||
|
userRepository.findFollowersById(user.id).let {
|
||||||
|
assertIterableEquals(listOf(follower, follower2), it)
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `createFollower フォロワー追加`() = runTest {
|
||||||
|
val userRepository = UserRepository(db)
|
||||||
|
val user = userRepository.create(
|
||||||
|
User(
|
||||||
|
"test",
|
||||||
|
"example.com",
|
||||||
|
"testUser",
|
||||||
|
"This user is test user.",
|
||||||
|
"https://example.com/inbox",
|
||||||
|
"https://example.com/outbox",
|
||||||
|
"https://example.com"
|
||||||
|
)
|
||||||
|
)
|
||||||
|
val follower = userRepository.create(
|
||||||
|
User(
|
||||||
|
"follower",
|
||||||
|
"follower.example.com",
|
||||||
|
"followerUser",
|
||||||
|
"This user is follower user.",
|
||||||
|
"https://follower.example.com/inbox",
|
||||||
|
"https://follower.example.com/outbox",
|
||||||
|
"https://follower.example.com"
|
||||||
|
)
|
||||||
|
)
|
||||||
|
userRepository.createFollower(user.id, follower.id)
|
||||||
|
transaction {
|
||||||
|
|
||||||
|
val followerIds =
|
||||||
|
UsersFollowers.select { UsersFollowers.userId eq user.id }.map { it[UsersFollowers.followerId] }
|
||||||
|
assertIterableEquals(listOf(follower.id), followerIds)
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,12 +0,0 @@
|
||||||
package dev.usbharu.hideout.routing
|
|
||||||
|
|
||||||
import org.junit.jupiter.api.Test
|
|
||||||
|
|
||||||
class UserRoutingKtTest {
|
|
||||||
@Test
|
|
||||||
fun userIconTest() {
|
|
||||||
println(String.Companion::class.java.classLoader)
|
|
||||||
println(String::class.java.classLoader)
|
|
||||||
println(String.javaClass.classLoader.getResourceAsStream("icon.png")?.readAllBytes())
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -0,0 +1,96 @@
|
||||||
|
package dev.usbharu.hideout.routing.activitypub
|
||||||
|
|
||||||
|
import dev.usbharu.hideout.exception.JsonParseException
|
||||||
|
import dev.usbharu.hideout.plugins.configureRouting
|
||||||
|
import dev.usbharu.hideout.plugins.configureSerialization
|
||||||
|
import dev.usbharu.hideout.plugins.configureStatusPages
|
||||||
|
import dev.usbharu.hideout.service.activitypub.ActivityPubService
|
||||||
|
import dev.usbharu.hideout.service.activitypub.ActivityPubUserService
|
||||||
|
import dev.usbharu.hideout.service.impl.UserService
|
||||||
|
import dev.usbharu.hideout.service.signature.HttpSignatureVerifyService
|
||||||
|
import io.ktor.client.request.*
|
||||||
|
import io.ktor.http.*
|
||||||
|
import io.ktor.server.config.*
|
||||||
|
import io.ktor.server.testing.*
|
||||||
|
import org.junit.jupiter.api.Assertions
|
||||||
|
import org.junit.jupiter.api.Test
|
||||||
|
import org.mockito.kotlin.any
|
||||||
|
import org.mockito.kotlin.doReturn
|
||||||
|
import org.mockito.kotlin.doThrow
|
||||||
|
import org.mockito.kotlin.mock
|
||||||
|
|
||||||
|
class InboxRoutingKtTest {
|
||||||
|
@Test
|
||||||
|
fun `sharedInboxにGETしたら405が帰ってくる`() = testApplication {
|
||||||
|
environment {
|
||||||
|
config = ApplicationConfig("empty.conf")
|
||||||
|
}
|
||||||
|
application {
|
||||||
|
configureSerialization()
|
||||||
|
configureRouting(mock(), mock(), mock(), mock())
|
||||||
|
}
|
||||||
|
client.get("/inbox").let {
|
||||||
|
Assertions.assertEquals(HttpStatusCode.MethodNotAllowed, it.status)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `sharedInboxに空のリクエストボディでPOSTしたら400が帰ってくる`() = testApplication {
|
||||||
|
environment {
|
||||||
|
config = ApplicationConfig("empty.conf")
|
||||||
|
}
|
||||||
|
val httpSignatureVerifyService = mock<HttpSignatureVerifyService>{
|
||||||
|
on { verify(any()) } doReturn true
|
||||||
|
}
|
||||||
|
val activityPubService = mock<ActivityPubService>{
|
||||||
|
on { parseActivity(any()) } doThrow JsonParseException()
|
||||||
|
}
|
||||||
|
val userService = mock<UserService>()
|
||||||
|
val activityPubUserService = mock<ActivityPubUserService>()
|
||||||
|
application {
|
||||||
|
configureStatusPages()
|
||||||
|
configureSerialization()
|
||||||
|
configureRouting(httpSignatureVerifyService, activityPubService, userService, activityPubUserService)
|
||||||
|
}
|
||||||
|
client.post("/inbox").let {
|
||||||
|
Assertions.assertEquals(HttpStatusCode.BadRequest, it.status)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `ユーザのinboxにGETしたら405が帰ってくる`() = testApplication {
|
||||||
|
environment {
|
||||||
|
config = ApplicationConfig("empty.conf")
|
||||||
|
}
|
||||||
|
application {
|
||||||
|
configureSerialization()
|
||||||
|
configureRouting(mock(), mock(), mock(), mock())
|
||||||
|
}
|
||||||
|
client.get("/users/test/inbox").let {
|
||||||
|
Assertions.assertEquals(HttpStatusCode.MethodNotAllowed, it.status)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `ユーザーのinboxに空のリクエストボディでPOSTしたら400が帰ってくる`() = testApplication {
|
||||||
|
environment {
|
||||||
|
config = ApplicationConfig("empty.conf")
|
||||||
|
}
|
||||||
|
val httpSignatureVerifyService = mock<HttpSignatureVerifyService>{
|
||||||
|
on { verify(any()) } doReturn true
|
||||||
|
}
|
||||||
|
val activityPubService = mock<ActivityPubService>{
|
||||||
|
on { parseActivity(any()) } doThrow JsonParseException()
|
||||||
|
}
|
||||||
|
val userService = mock<UserService>()
|
||||||
|
val activityPubUserService = mock<ActivityPubUserService>()
|
||||||
|
application {
|
||||||
|
configureStatusPages()
|
||||||
|
configureSerialization()
|
||||||
|
configureRouting(httpSignatureVerifyService, activityPubService, userService, activityPubUserService)
|
||||||
|
}
|
||||||
|
client.post("/users/test/inbox").let {
|
||||||
|
Assertions.assertEquals(HttpStatusCode.BadRequest, it.status)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,91 @@
|
||||||
|
package dev.usbharu.hideout.routing.activitypub
|
||||||
|
|
||||||
|
import com.fasterxml.jackson.annotation.JsonInclude
|
||||||
|
import com.fasterxml.jackson.annotation.JsonSetter
|
||||||
|
import com.fasterxml.jackson.annotation.Nulls
|
||||||
|
import com.fasterxml.jackson.databind.DeserializationFeature
|
||||||
|
import com.fasterxml.jackson.module.kotlin.jacksonObjectMapper
|
||||||
|
import com.fasterxml.jackson.module.kotlin.readValue
|
||||||
|
import dev.usbharu.hideout.ap.Image
|
||||||
|
import dev.usbharu.hideout.ap.Key
|
||||||
|
import dev.usbharu.hideout.ap.Person
|
||||||
|
import dev.usbharu.hideout.plugins.configureRouting
|
||||||
|
import dev.usbharu.hideout.plugins.configureSerialization
|
||||||
|
import dev.usbharu.hideout.service.activitypub.ActivityPubService
|
||||||
|
import dev.usbharu.hideout.service.activitypub.ActivityPubUserService
|
||||||
|
import dev.usbharu.hideout.service.impl.UserService
|
||||||
|
import dev.usbharu.hideout.service.signature.HttpSignatureVerifyService
|
||||||
|
import dev.usbharu.hideout.util.HttpUtil.Activity
|
||||||
|
import io.ktor.client.request.*
|
||||||
|
import io.ktor.client.statement.*
|
||||||
|
import io.ktor.http.*
|
||||||
|
import io.ktor.server.config.*
|
||||||
|
import io.ktor.server.testing.*
|
||||||
|
import org.junit.jupiter.api.Test
|
||||||
|
import org.mockito.ArgumentMatchers.anyString
|
||||||
|
import org.mockito.kotlin.doReturn
|
||||||
|
import org.mockito.kotlin.mock
|
||||||
|
import kotlin.test.assertEquals
|
||||||
|
|
||||||
|
|
||||||
|
class UsersAPTest {
|
||||||
|
|
||||||
|
@Test()
|
||||||
|
fun `ユーザのURLにAcceptヘッダーをActivityにしてアクセスしたとき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<UserService> {}
|
||||||
|
|
||||||
|
val activityPubUserService = mock<ActivityPubUserService> {
|
||||||
|
onBlocking { getPersonByName(anyString()) } doReturn person
|
||||||
|
}
|
||||||
|
|
||||||
|
application {
|
||||||
|
configureSerialization()
|
||||||
|
configureRouting(httpSignatureVerifyService, activityPubService, userService, activityPubUserService)
|
||||||
|
}
|
||||||
|
client.get("/users/test") {
|
||||||
|
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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,153 @@
|
||||||
|
@file:OptIn(ExperimentalCoroutinesApi::class)
|
||||||
|
@file:Suppress("INVISIBLE_MEMBER", "INVISIBLE_REFERENCE")
|
||||||
|
|
||||||
|
package dev.usbharu.hideout.service.activitypub
|
||||||
|
|
||||||
|
import com.fasterxml.jackson.module.kotlin.readValue
|
||||||
|
import dev.usbharu.hideout.ap.*
|
||||||
|
import dev.usbharu.hideout.config.Config
|
||||||
|
import dev.usbharu.hideout.config.ConfigData
|
||||||
|
import dev.usbharu.hideout.domain.model.UserEntity
|
||||||
|
import dev.usbharu.hideout.domain.model.job.ReceiveFollowJob
|
||||||
|
import dev.usbharu.hideout.service.impl.UserService
|
||||||
|
import dev.usbharu.hideout.service.job.JobQueueParentService
|
||||||
|
import io.ktor.client.*
|
||||||
|
import io.ktor.client.engine.mock.*
|
||||||
|
import kjob.core.dsl.ScheduleContext
|
||||||
|
import kjob.core.job.JobProps
|
||||||
|
import kotlinx.coroutines.ExperimentalCoroutinesApi
|
||||||
|
import kotlinx.coroutines.test.runTest
|
||||||
|
import kotlinx.serialization.json.Json
|
||||||
|
import org.junit.jupiter.api.Assertions.assertEquals
|
||||||
|
import org.junit.jupiter.api.Test
|
||||||
|
import org.mockito.ArgumentMatchers.anyString
|
||||||
|
import org.mockito.kotlin.*
|
||||||
|
import utils.JsonObjectMapper
|
||||||
|
|
||||||
|
class ActivityPubFollowServiceImplTest {
|
||||||
|
@Test
|
||||||
|
fun `receiveFollow フォロー受付処理`() = runTest {
|
||||||
|
val jobQueueParentService = mock<JobQueueParentService> {
|
||||||
|
onBlocking { schedule(eq(ReceiveFollowJob), any()) } doReturn Unit
|
||||||
|
}
|
||||||
|
val activityPubFollowService = ActivityPubFollowServiceImpl(jobQueueParentService, mock(), mock(), mock())
|
||||||
|
activityPubFollowService.receiveFollow(
|
||||||
|
Follow(
|
||||||
|
emptyList(),
|
||||||
|
"Follow",
|
||||||
|
"https://example.com",
|
||||||
|
"https://follower.example.com"
|
||||||
|
)
|
||||||
|
)
|
||||||
|
verify(jobQueueParentService, times(1)).schedule(eq(ReceiveFollowJob), any())
|
||||||
|
argumentCaptor<ScheduleContext<ReceiveFollowJob>.(ReceiveFollowJob) -> Unit> {
|
||||||
|
verify(jobQueueParentService, times(1)).schedule(eq(ReceiveFollowJob), capture())
|
||||||
|
val scheduleContext = ScheduleContext<ReceiveFollowJob>(Json)
|
||||||
|
firstValue.invoke(scheduleContext, ReceiveFollowJob)
|
||||||
|
val actor = scheduleContext.props.props[ReceiveFollowJob.actor.name]
|
||||||
|
val targetActor = scheduleContext.props.props[ReceiveFollowJob.targetActor.name]
|
||||||
|
val follow = scheduleContext.props.props[ReceiveFollowJob.follow.name]
|
||||||
|
assertEquals("https://follower.example.com", actor)
|
||||||
|
assertEquals("https://example.com", targetActor)
|
||||||
|
assertEquals(
|
||||||
|
"""{"type":"Follow","name":"Follow","object":"https://example.com","actor":"https://follower.example.com","@context":null}""",
|
||||||
|
follow
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `receiveFollowJob フォロー受付処理のJob`() = runTest {
|
||||||
|
Config.configData = ConfigData(objectMapper = JsonObjectMapper.objectMapper)
|
||||||
|
val person = Person(
|
||||||
|
type = emptyList(),
|
||||||
|
name = "follower",
|
||||||
|
id = "https://follower.example.com",
|
||||||
|
preferredUsername = "followerUser",
|
||||||
|
summary = "This user is follower user.",
|
||||||
|
inbox = "https://follower.example.com/inbox",
|
||||||
|
outbox = "https://follower.example.com/outbox",
|
||||||
|
url = "https://follower.example.com",
|
||||||
|
icon = Image(
|
||||||
|
type = emptyList(),
|
||||||
|
name = "https://follower.example.com/image",
|
||||||
|
mediaType = "image/png",
|
||||||
|
url = "https://follower.example.com/image"
|
||||||
|
),
|
||||||
|
publicKey = Key(
|
||||||
|
type = emptyList(),
|
||||||
|
name = "Public Key",
|
||||||
|
id = "https://follower.example.com#main-key",
|
||||||
|
owner = "https://follower.example.com",
|
||||||
|
publicKeyPem = "BEGIN PUBLIC KEY...END PUBLIC KEY",
|
||||||
|
)
|
||||||
|
|
||||||
|
)
|
||||||
|
val activityPubUserService = mock<ActivityPubUserService> {
|
||||||
|
onBlocking { fetchPerson(anyString()) } doReturn person
|
||||||
|
}
|
||||||
|
val userService = mock<UserService> {
|
||||||
|
onBlocking { findByUrls(any()) } doReturn listOf(
|
||||||
|
UserEntity(
|
||||||
|
id = 1L,
|
||||||
|
name = "test",
|
||||||
|
domain = "example.com",
|
||||||
|
screenName = "testUser",
|
||||||
|
description = "This user is test user.",
|
||||||
|
inbox = "https://example.com/inbox",
|
||||||
|
outbox = "https://example.com/outbox",
|
||||||
|
url = "https://example.com"
|
||||||
|
),
|
||||||
|
UserEntity(
|
||||||
|
id = 2L,
|
||||||
|
name = "follower",
|
||||||
|
domain = "follower.example.com",
|
||||||
|
screenName = "followerUser",
|
||||||
|
description = "This user is test follower user.",
|
||||||
|
inbox = "https://follower.example.com/inbox",
|
||||||
|
outbox = "https://follower.example.com/outbox",
|
||||||
|
url = "https://follower.example.com"
|
||||||
|
)
|
||||||
|
)
|
||||||
|
onBlocking { addFollowers(any(), any()) } doReturn Unit
|
||||||
|
}
|
||||||
|
val activityPubFollowService =
|
||||||
|
ActivityPubFollowServiceImpl(
|
||||||
|
mock(),
|
||||||
|
activityPubUserService,
|
||||||
|
userService,
|
||||||
|
HttpClient(MockEngine { httpRequestData ->
|
||||||
|
assertEquals(person.inbox, httpRequestData.url.toString())
|
||||||
|
val accept = Accept(
|
||||||
|
type = emptyList(),
|
||||||
|
name = "Follow",
|
||||||
|
`object` = Follow(
|
||||||
|
type = emptyList(),
|
||||||
|
name = "Follow",
|
||||||
|
`object` = "https://example.com",
|
||||||
|
actor = "https://follower.example.com"
|
||||||
|
),
|
||||||
|
actor = "https://example.com"
|
||||||
|
)
|
||||||
|
accept.context += "https://www.w3.org/ns/activitystreams"
|
||||||
|
assertEquals(
|
||||||
|
accept,
|
||||||
|
Config.configData.objectMapper.readValue<Accept>(
|
||||||
|
httpRequestData.body.toByteArray().decodeToString()
|
||||||
|
)
|
||||||
|
)
|
||||||
|
respondOk()
|
||||||
|
})
|
||||||
|
)
|
||||||
|
activityPubFollowService.receiveFollowJob(
|
||||||
|
JobProps(
|
||||||
|
data = mapOf<String, Any>(
|
||||||
|
ReceiveFollowJob.actor.name to "https://follower.example.com",
|
||||||
|
ReceiveFollowJob.targetActor.name to "https://example.com",
|
||||||
|
ReceiveFollowJob.follow.name to """{"type":"Follow","name":"Follow","object":"https://example.com","actor":"https://follower.example.com","@context":null}"""
|
||||||
|
),
|
||||||
|
json = Json
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,16 @@
|
||||||
|
package dev.usbharu.hideout.service.job
|
||||||
|
|
||||||
|
import kjob.core.Job
|
||||||
|
import org.jetbrains.exposed.sql.Database
|
||||||
|
import org.junit.jupiter.api.Test
|
||||||
|
|
||||||
|
class KJobJobQueueWorkerServiceTest {
|
||||||
|
|
||||||
|
object TestJob : Job("test-job")
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun init() {
|
||||||
|
val kJobJobWorkerService = KJobJobQueueWorkerService(Database.connect("jdbc:h2:mem:"))
|
||||||
|
kJobJobWorkerService.init(listOf(TestJob to { it -> execute { it as TestJob;println(it.propNames) } }))
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,28 @@
|
||||||
|
package utils
|
||||||
|
|
||||||
|
import org.jetbrains.exposed.sql.Transaction
|
||||||
|
import org.jetbrains.exposed.sql.transactions.TransactionManager
|
||||||
|
import org.junit.jupiter.api.extension.*
|
||||||
|
|
||||||
|
class DBResetInterceptor : BeforeAllCallback,AfterAllCallback,BeforeEachCallback,AfterEachCallback {
|
||||||
|
private lateinit var transactionAll: Transaction
|
||||||
|
private lateinit var transactionEach: Transaction
|
||||||
|
|
||||||
|
override fun beforeAll(context: ExtensionContext?) {
|
||||||
|
transactionAll = TransactionManager.manager.newTransaction()
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun afterAll(context: ExtensionContext?) {
|
||||||
|
transactionAll.rollback()
|
||||||
|
transactionAll.close()
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun beforeEach(context: ExtensionContext?) {
|
||||||
|
transactionEach = TransactionManager.manager.newTransaction(outerTransaction = transactionAll)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun afterEach(context: ExtensionContext?) {
|
||||||
|
transactionEach.rollback()
|
||||||
|
transactionEach.close()
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,18 @@
|
||||||
|
package utils
|
||||||
|
|
||||||
|
import org.jetbrains.exposed.sql.Database
|
||||||
|
import org.jetbrains.exposed.sql.DatabaseConfig
|
||||||
|
import org.junit.jupiter.api.extension.ExtendWith
|
||||||
|
|
||||||
|
|
||||||
|
@ExtendWith(DBResetInterceptor::class)
|
||||||
|
abstract class DatabaseTestBase {
|
||||||
|
companion object {
|
||||||
|
init {
|
||||||
|
Database.connect(
|
||||||
|
"jdbc:h2:mem:test;DB_CLOSE_DELAY=-1",
|
||||||
|
driver = "org.h2.Driver",
|
||||||
|
databaseConfig = DatabaseConfig { useNestedTransactions = true })
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,22 @@
|
||||||
|
package utils
|
||||||
|
|
||||||
|
import com.fasterxml.jackson.annotation.JsonInclude
|
||||||
|
import com.fasterxml.jackson.annotation.JsonSetter
|
||||||
|
import com.fasterxml.jackson.annotation.Nulls
|
||||||
|
import com.fasterxml.jackson.databind.DeserializationFeature
|
||||||
|
import com.fasterxml.jackson.module.kotlin.jacksonObjectMapper
|
||||||
|
|
||||||
|
object JsonObjectMapper {
|
||||||
|
val objectMapper: com.fasterxml.jackson.databind.ObjectMapper =
|
||||||
|
jacksonObjectMapper().enable(DeserializationFeature.ACCEPT_SINGLE_VALUE_AS_ARRAY)
|
||||||
|
.setSerializationInclusion(JsonInclude.Include.NON_EMPTY)
|
||||||
|
.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false)
|
||||||
|
|
||||||
|
init {
|
||||||
|
objectMapper.configOverride(List::class.java).setSetterInfo(
|
||||||
|
JsonSetter.Value.forValueNulls(
|
||||||
|
Nulls.AS_EMPTY
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,21 @@
|
||||||
|
ktor {
|
||||||
|
development = true
|
||||||
|
deployment {
|
||||||
|
port = 8080
|
||||||
|
port = ${?PORT}
|
||||||
|
}
|
||||||
|
application {
|
||||||
|
modules = [dev.usbharu.hideout.EmptyKt.empty]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
hideout {
|
||||||
|
hostname = "https://localhost:8080"
|
||||||
|
hostname = ${?HOSTNAME}
|
||||||
|
database {
|
||||||
|
url = "jdbc:h2:./test;MODE=POSTGRESQL"
|
||||||
|
driver = "org.h2.Driver"
|
||||||
|
username = ""
|
||||||
|
password = ""
|
||||||
|
}
|
||||||
|
}
|
Loading…
Reference in New Issue