mirror of https://github.com/usbharu/Hideout.git
commit
fdcf58fca9
|
@ -2,23 +2,114 @@ import kotlinx.kover.gradle.plugin.dsl.CoverageUnit
|
||||||
|
|
||||||
plugins {
|
plugins {
|
||||||
alias(libs.plugins.kotlin.jvm)
|
alias(libs.plugins.kotlin.jvm)
|
||||||
|
alias(libs.plugins.detekt)
|
||||||
alias(libs.plugins.kover)
|
alias(libs.plugins.kover)
|
||||||
|
alias(libs.plugins.spring.boot)
|
||||||
|
alias(libs.plugins.kotlin.spring)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
apply {
|
||||||
|
plugin("io.spring.dependency-management")
|
||||||
|
}
|
||||||
|
|
||||||
|
group = "dev.usbharu"
|
||||||
|
version = "1.0-SNAPSHOT"
|
||||||
|
|
||||||
|
repositories {
|
||||||
|
mavenCentral()
|
||||||
|
maven {
|
||||||
|
url = uri("https://git.usbharu.dev/api/packages/usbharu/maven")
|
||||||
|
}
|
||||||
|
maven {
|
||||||
|
name = "GitHubPackages"
|
||||||
|
url = uri("https://maven.pkg.github.com/usbharu/activity-streams-serialization")
|
||||||
|
credentials {
|
||||||
|
|
||||||
|
username = project.findProperty("gpr.user") as String? ?: System.getenv("USERNAME")
|
||||||
|
password = project.findProperty("gpr.key") as String? ?: System.getenv("TOKEN")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
dependencies {
|
||||||
|
detektPlugins(libs.detekt.formatting)
|
||||||
|
implementation(project(":hideout-core"))
|
||||||
|
implementation("org.springframework.boot:spring-boot-starter-web")
|
||||||
|
implementation("org.springframework.boot:spring-boot-starter-security")
|
||||||
|
implementation(libs.bundles.jackson)
|
||||||
|
implementation(libs.owl.producer.api)
|
||||||
|
implementation(libs.owl.producer.embedded)
|
||||||
|
implementation(libs.owl.common.serialize.jackson)
|
||||||
|
implementation(libs.activity.streams.serialization)
|
||||||
|
implementation(libs.jsonld)
|
||||||
|
implementation(libs.coroutines.core)
|
||||||
|
implementation(libs.bundles.exposed)
|
||||||
|
testImplementation("org.springframework.boot:spring-boot-starter-test")
|
||||||
|
testImplementation("org.springframework.security:spring-security-test")
|
||||||
|
testImplementation(libs.bundles.spring.boot.oauth2)
|
||||||
|
testImplementation(libs.kotlin.junit)
|
||||||
|
testImplementation(libs.coroutines.test)
|
||||||
|
testImplementation(libs.h2db)
|
||||||
|
testImplementation(libs.flyway.core)
|
||||||
|
}
|
||||||
|
|
||||||
tasks.test {
|
tasks.test {
|
||||||
useJUnitPlatform()
|
useJUnitPlatform()
|
||||||
}
|
}
|
||||||
|
kotlin {
|
||||||
|
jvmToolchain(21)
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
configurations {
|
||||||
|
matching { it.name == "detekt" }.all {
|
||||||
|
resolutionStrategy.eachDependency {
|
||||||
|
if (requested.group == "org.jetbrains.kotlin") {
|
||||||
|
useVersion(io.gitlab.arturbosch.detekt.getSupportedKotlinVersion())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
all {
|
||||||
|
exclude("org.apache.logging.log4j", "log4j-slf4j2-impl")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
tasks {
|
tasks {
|
||||||
|
withType<io.gitlab.arturbosch.detekt.Detekt> {
|
||||||
withType<Test> {
|
exclude("**/generated/**")
|
||||||
|
setSource("src/main/kotlin")
|
||||||
|
exclude("build/")
|
||||||
|
configureEach {
|
||||||
|
exclude("**/org/koin/ksp/generated/**", "**/generated/**")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
withType<io.gitlab.arturbosch.detekt.DetektCreateBaselineTask> {
|
||||||
|
configureEach {
|
||||||
|
exclude("**/org/koin/ksp/generated/**", "**/generated/**")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
withType<Test> {
|
||||||
useJUnitPlatform()
|
useJUnitPlatform()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
project.gradle.taskGraph.whenReady {
|
||||||
|
if (this.hasTask(":koverGenerateArtifact")) {
|
||||||
|
val task = this.allTasks.find { it.name == "test" }
|
||||||
|
val verificationTask = task as VerificationTask
|
||||||
|
verificationTask.ignoreFailures = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
detekt {
|
||||||
|
parallel = true
|
||||||
|
config.setFrom(files("../detekt.yml"))
|
||||||
|
buildUponDefaultConfig = true
|
||||||
|
basePath = "${rootDir.absolutePath}/src/main/kotlin"
|
||||||
|
autoCorrect = true
|
||||||
|
}
|
||||||
|
|
||||||
kover {
|
kover {
|
||||||
currentProject {
|
currentProject {
|
||||||
sources {
|
sources {
|
||||||
|
@ -57,3 +148,7 @@ kover {
|
||||||
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
springBoot{
|
||||||
|
buildInfo { }
|
||||||
|
}
|
|
@ -1,5 +0,0 @@
|
||||||
package dev.usbharu
|
|
||||||
|
|
||||||
fun main() {
|
|
||||||
println("Hello World!")
|
|
||||||
}
|
|
|
@ -0,0 +1,33 @@
|
||||||
|
package dev.usbharu.hideout.activitypub.application.actor
|
||||||
|
|
||||||
|
import dev.usbharu.activitystreamsserialization.other.JsonLd
|
||||||
|
import dev.usbharu.hideout.activitypub.external.activitystreams.ActorTranslator
|
||||||
|
import dev.usbharu.hideout.core.application.shared.AbstractApplicationService
|
||||||
|
import dev.usbharu.hideout.core.application.shared.Transaction
|
||||||
|
import dev.usbharu.hideout.core.config.ApplicationConfig
|
||||||
|
import dev.usbharu.hideout.core.domain.model.actor.ActorRepository
|
||||||
|
import dev.usbharu.hideout.core.domain.model.support.domain.apHost
|
||||||
|
import dev.usbharu.hideout.core.domain.model.support.principal.Principal
|
||||||
|
import org.slf4j.LoggerFactory
|
||||||
|
import org.springframework.stereotype.Service
|
||||||
|
|
||||||
|
@Service
|
||||||
|
class GetActorApplicationService(
|
||||||
|
private val actorRepository: ActorRepository,
|
||||||
|
private val applicationConfig: ApplicationConfig,
|
||||||
|
private val actorTranslator: ActorTranslator,
|
||||||
|
transaction: Transaction,
|
||||||
|
) : AbstractApplicationService<String, JsonLd>(
|
||||||
|
transaction,
|
||||||
|
logger,
|
||||||
|
) {
|
||||||
|
override suspend fun internalExecute(command: String, principal: Principal): JsonLd {
|
||||||
|
val actor = actorRepository.findByNameAndDomain(command, applicationConfig.url.apHost)
|
||||||
|
?: throw IllegalArgumentException("Actor $command not found")
|
||||||
|
return actorTranslator.translate(actor, null, null)
|
||||||
|
}
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
private val logger = LoggerFactory.getLogger(GetActorApplicationService::class.java)
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,35 @@
|
||||||
|
package dev.usbharu.hideout.activitypub.config
|
||||||
|
|
||||||
|
import org.springframework.context.annotation.Bean
|
||||||
|
import org.springframework.context.annotation.Configuration
|
||||||
|
import org.springframework.core.annotation.Order
|
||||||
|
import org.springframework.http.HttpMethod.GET
|
||||||
|
import org.springframework.http.HttpMethod.POST
|
||||||
|
import org.springframework.security.config.annotation.web.builders.HttpSecurity
|
||||||
|
import org.springframework.security.config.annotation.web.invoke
|
||||||
|
import org.springframework.security.web.SecurityFilterChain
|
||||||
|
import org.springframework.security.web.util.matcher.RequestMatcher
|
||||||
|
|
||||||
|
@Configuration
|
||||||
|
class ActivityPubSecurityConfig {
|
||||||
|
@Bean
|
||||||
|
@Order(4)
|
||||||
|
fun activityPubSecurityFilterChain(http: HttpSecurity): SecurityFilterChain {
|
||||||
|
http {
|
||||||
|
securityMatcher(
|
||||||
|
RequestMatcher {
|
||||||
|
val accept = it.getHeader("Accept") ?: ""
|
||||||
|
return@RequestMatcher accept == "application/json" || accept == "application/activity+json"
|
||||||
|
}
|
||||||
|
)
|
||||||
|
authorizeHttpRequests {
|
||||||
|
authorize(POST, "/inbox", permitAll)
|
||||||
|
authorize(POST, "/users/{username}/inbox", permitAll)
|
||||||
|
authorize(GET, "/outbox", permitAll)
|
||||||
|
authorize(GET, "/users/{username}/outbox", permitAll)
|
||||||
|
authorize(GET, "/users/{username}", permitAll)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return http.build()
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,16 @@
|
||||||
|
package dev.usbharu.hideout.activitypub.config
|
||||||
|
|
||||||
|
import dev.usbharu.hideout.activitypub.external.activitystreams.ActivityStreamHttpMessageConverter
|
||||||
|
import org.springframework.context.annotation.Configuration
|
||||||
|
import org.springframework.core.annotation.Order
|
||||||
|
import org.springframework.http.converter.HttpMessageConverter
|
||||||
|
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer
|
||||||
|
|
||||||
|
@Configuration
|
||||||
|
@Order(2)
|
||||||
|
class ActivityPubWebMvcConfigurer(private val activityStreamsHttpMessageConverter: ActivityStreamHttpMessageConverter) :
|
||||||
|
WebMvcConfigurer {
|
||||||
|
override fun extendMessageConverters(converters: MutableList<HttpMessageConverter<*>>) {
|
||||||
|
converters.add(activityStreamsHttpMessageConverter)
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,29 @@
|
||||||
|
package dev.usbharu.hideout.activitypub.config
|
||||||
|
|
||||||
|
import com.fasterxml.jackson.databind.ObjectMapper
|
||||||
|
import dev.usbharu.owl.common.property.*
|
||||||
|
import dev.usbharu.owl.producer.api.OWL
|
||||||
|
import dev.usbharu.owl.producer.api.OwlProducer
|
||||||
|
import dev.usbharu.owl.producer.embedded.EMBEDDED
|
||||||
|
import org.springframework.context.annotation.Bean
|
||||||
|
import org.springframework.context.annotation.Configuration
|
||||||
|
|
||||||
|
@Configuration
|
||||||
|
class OwlConfig {
|
||||||
|
@Bean
|
||||||
|
fun owlProducer(objectMapper: ObjectMapper): OwlProducer {
|
||||||
|
return OWL(EMBEDDED) {
|
||||||
|
this.propertySerializerFactory = CustomPropertySerializerFactory(
|
||||||
|
setOf(
|
||||||
|
IntegerPropertySerializer(),
|
||||||
|
StringPropertyValueSerializer(),
|
||||||
|
DoublePropertySerializer(),
|
||||||
|
BooleanPropertySerializer(),
|
||||||
|
LongPropertySerializer(),
|
||||||
|
FloatPropertySerializer(),
|
||||||
|
ObjectPropertySerializer(objectMapper),
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,7 @@
|
||||||
|
package dev.usbharu.hideout.activitypub.domain.shared.jobqueue
|
||||||
|
|
||||||
|
import dev.usbharu.hideout.activitypub.domain.task.Task
|
||||||
|
|
||||||
|
interface TaskPublisher {
|
||||||
|
suspend fun publish(task: Task<*>)
|
||||||
|
}
|
|
@ -0,0 +1,13 @@
|
||||||
|
package dev.usbharu.hideout.activitypub.domain.task
|
||||||
|
|
||||||
|
import dev.usbharu.hideout.core.domain.model.support.domain.Domain
|
||||||
|
import dev.usbharu.owl.common.task.Task
|
||||||
|
import java.time.Instant
|
||||||
|
|
||||||
|
class Task<out T : TaskBody>(
|
||||||
|
val id: String,
|
||||||
|
val name: String,
|
||||||
|
val publishedOn: Instant,
|
||||||
|
val body: T,
|
||||||
|
val domain: Domain
|
||||||
|
) : Task()
|
|
@ -0,0 +1,7 @@
|
||||||
|
package dev.usbharu.hideout.activitypub.domain.task
|
||||||
|
|
||||||
|
import dev.usbharu.hideout.core.domain.model.support.principal.Principal
|
||||||
|
|
||||||
|
abstract class TaskBody(private val map: Map<String, Any?>, val principal: Principal) {
|
||||||
|
fun toMap(): Map<String, Any?> = map
|
||||||
|
}
|
|
@ -0,0 +1,41 @@
|
||||||
|
package dev.usbharu.hideout.activitypub.external.activitystreams
|
||||||
|
|
||||||
|
import com.github.jsonldjava.core.JsonLdOptions
|
||||||
|
import com.github.jsonldjava.core.JsonLdProcessor
|
||||||
|
import com.github.jsonldjava.utils.JsonUtils
|
||||||
|
import dev.usbharu.activitystreamsserialization.json.impl.JacksonSerializationConverter
|
||||||
|
import dev.usbharu.activitystreamsserialization.other.JsonLd
|
||||||
|
import org.springframework.http.HttpInputMessage
|
||||||
|
import org.springframework.http.HttpOutputMessage
|
||||||
|
import org.springframework.http.MediaType
|
||||||
|
import org.springframework.http.converter.HttpMessageConverter
|
||||||
|
import org.springframework.stereotype.Component
|
||||||
|
|
||||||
|
@Component
|
||||||
|
class ActivityStreamHttpMessageConverter : HttpMessageConverter<JsonLd> {
|
||||||
|
override fun canRead(clazz: Class<*>, mediaType: MediaType?): Boolean = false
|
||||||
|
|
||||||
|
override fun canWrite(clazz: Class<*>, mediaType: MediaType?): Boolean = JsonLd::class.java.isAssignableFrom(clazz)
|
||||||
|
|
||||||
|
override fun getSupportedMediaTypes(): MutableList<MediaType> = mutableListOf()
|
||||||
|
|
||||||
|
override fun write(t: JsonLd, contentType: MediaType?, outputMessage: HttpOutputMessage) {
|
||||||
|
outputMessage.headers.contentType = MediaType.APPLICATION_JSON
|
||||||
|
outputMessage.body.bufferedWriter()
|
||||||
|
.use {
|
||||||
|
it.write(
|
||||||
|
JsonUtils.toString(
|
||||||
|
JsonLdProcessor.compact(
|
||||||
|
JsonUtils.fromString(JacksonSerializationConverter.convert(t.json).toString()),
|
||||||
|
"https://www.w3.org/ns/activitystreams",
|
||||||
|
JsonLdOptions()
|
||||||
|
)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun read(clazz: Class<out JsonLd>, inputMessage: HttpInputMessage): JsonLd {
|
||||||
|
TODO("Not yet implemented")
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,51 @@
|
||||||
|
package dev.usbharu.hideout.activitypub.external.activitystreams
|
||||||
|
|
||||||
|
import dev.usbharu.activitystreamsserialization.dsl.ActivityBuilder
|
||||||
|
import dev.usbharu.activitystreamsserialization.other.JsonLd
|
||||||
|
import dev.usbharu.hideout.core.domain.model.actor.Actor
|
||||||
|
import dev.usbharu.hideout.core.domain.model.media.Media
|
||||||
|
import org.springframework.stereotype.Service
|
||||||
|
|
||||||
|
@Service
|
||||||
|
class ActorTranslator {
|
||||||
|
fun translate(actor: Actor, iconMedia: Media?, bannerMedia: Media?): JsonLd {
|
||||||
|
// todo actorにbot等の属性が生えてきたら対応する
|
||||||
|
val person = ActivityBuilder().Person {
|
||||||
|
name(actor.name.name)
|
||||||
|
id(actor.url)
|
||||||
|
preferredUsername(actor.name.name)
|
||||||
|
inbox(actor.inbox)
|
||||||
|
outbox(actor.outbox)
|
||||||
|
followers(actor.followersEndpoint)
|
||||||
|
following(actor.followingEndpoint)
|
||||||
|
publicKey {
|
||||||
|
listOf(
|
||||||
|
Key {
|
||||||
|
owner(actor.url)
|
||||||
|
publicKeyPem(actor.publicKey.publicKey)
|
||||||
|
id(actor.keyId.keyId)
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
iconMedia?.let {
|
||||||
|
icon {
|
||||||
|
listOf(
|
||||||
|
Image {
|
||||||
|
url(iconMedia.url)
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
bannerMedia?.let {
|
||||||
|
image {
|
||||||
|
listOf(
|
||||||
|
Image {
|
||||||
|
url(bannerMedia.url)
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return person
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,13 @@
|
||||||
|
package dev.usbharu.hideout.activitypub.infrastructure.owl
|
||||||
|
|
||||||
|
import dev.usbharu.hideout.activitypub.domain.shared.jobqueue.TaskPublisher
|
||||||
|
import dev.usbharu.hideout.activitypub.domain.task.Task
|
||||||
|
import dev.usbharu.owl.producer.api.OwlProducer
|
||||||
|
import org.springframework.stereotype.Service
|
||||||
|
|
||||||
|
@Service
|
||||||
|
class OwlTaskPublisher(private val owlProducer: OwlProducer) : TaskPublisher {
|
||||||
|
override suspend fun publish(task: Task<*>) {
|
||||||
|
owlProducer.publishTask(task)
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,20 @@
|
||||||
|
package dev.usbharu.hideout.activitypub.interfaces.api
|
||||||
|
|
||||||
|
import dev.usbharu.activitystreamsserialization.other.JsonLd
|
||||||
|
import dev.usbharu.hideout.activitypub.application.actor.GetActorApplicationService
|
||||||
|
import dev.usbharu.hideout.core.domain.model.support.principal.Anonymous
|
||||||
|
import org.springframework.http.ResponseEntity
|
||||||
|
import org.springframework.stereotype.Controller
|
||||||
|
import org.springframework.web.bind.annotation.GetMapping
|
||||||
|
import org.springframework.web.bind.annotation.PathVariable
|
||||||
|
|
||||||
|
@Controller
|
||||||
|
class APActorController(private val getActorApplicationService: GetActorApplicationService) {
|
||||||
|
@GetMapping(
|
||||||
|
"/users/{username}",
|
||||||
|
// consumes = ["application/activity+json"],
|
||||||
|
produces = ["application/activity+json"]
|
||||||
|
)
|
||||||
|
suspend fun user(@PathVariable username: String): ResponseEntity<JsonLd> =
|
||||||
|
ResponseEntity.ok(getActorApplicationService.execute(username, Anonymous))
|
||||||
|
}
|
|
@ -0,0 +1,49 @@
|
||||||
|
package activity
|
||||||
|
|
||||||
|
import dev.usbharu.hideout.SpringApplication
|
||||||
|
import dev.usbharu.hideout.activitypub.interfaces.api.APActorController
|
||||||
|
import org.junit.jupiter.api.BeforeEach
|
||||||
|
import org.junit.jupiter.api.Test
|
||||||
|
import org.springframework.beans.factory.annotation.Autowired
|
||||||
|
import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc
|
||||||
|
import org.springframework.boot.test.context.SpringBootTest
|
||||||
|
import org.springframework.http.MediaType
|
||||||
|
import org.springframework.security.test.web.servlet.setup.SecurityMockMvcConfigurers.springSecurity
|
||||||
|
import org.springframework.test.context.jdbc.Sql
|
||||||
|
import org.springframework.test.web.servlet.MockMvc
|
||||||
|
import org.springframework.test.web.servlet.get
|
||||||
|
import org.springframework.test.web.servlet.setup.DefaultMockMvcBuilder
|
||||||
|
import org.springframework.test.web.servlet.setup.MockMvcBuilders
|
||||||
|
import org.springframework.transaction.annotation.Transactional
|
||||||
|
import org.springframework.web.context.WebApplicationContext
|
||||||
|
|
||||||
|
@SpringBootTest(classes = [SpringApplication::class, APActorController::class])
|
||||||
|
@AutoConfigureMockMvc
|
||||||
|
@Transactional
|
||||||
|
@Sql("/sql/actors.sql", executionPhase = Sql.ExecutionPhase.BEFORE_TEST_CLASS)
|
||||||
|
class APActorControllerTest {
|
||||||
|
|
||||||
|
@Autowired
|
||||||
|
private lateinit var context: WebApplicationContext
|
||||||
|
|
||||||
|
private lateinit var mockMvc: MockMvc
|
||||||
|
|
||||||
|
@BeforeEach
|
||||||
|
fun setUp() {
|
||||||
|
mockMvc = MockMvcBuilders.webAppContextSetup(context)
|
||||||
|
.apply<DefaultMockMvcBuilder>(springSecurity())
|
||||||
|
.build()
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun user() {
|
||||||
|
mockMvc
|
||||||
|
.get("/users/test") {
|
||||||
|
accept(MediaType("application", "activity+json"))
|
||||||
|
}
|
||||||
|
.asyncDispatch()
|
||||||
|
.andDo { print() }
|
||||||
|
.andExpect { status { isOk() } }
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,55 @@
|
||||||
|
package dev.usbharu.hideout.activitypub.external.activitystreams
|
||||||
|
|
||||||
|
import com.fasterxml.jackson.module.kotlin.jacksonObjectMapper
|
||||||
|
import com.github.jsonldjava.core.JsonLdOptions
|
||||||
|
import com.github.jsonldjava.core.JsonLdProcessor
|
||||||
|
import com.github.jsonldjava.utils.JsonUtils
|
||||||
|
import dev.usbharu.activitystreamsserialization.json.impl.JacksonSerializationConverter
|
||||||
|
import org.junit.jupiter.api.Test
|
||||||
|
import kotlin.test.assertEquals
|
||||||
|
import kotlin.test.assertTrue
|
||||||
|
|
||||||
|
class ActorTranslatorTest {
|
||||||
|
@Test
|
||||||
|
fun translate() {
|
||||||
|
val actor = TestActorFactory.create()
|
||||||
|
val translate = ActorTranslator().translate(actor, null, null)
|
||||||
|
println(translate)
|
||||||
|
val compact = JsonLdProcessor.compact(
|
||||||
|
JsonUtils.fromString(JacksonSerializationConverter.convert(translate.json).toString()),
|
||||||
|
"https://www.w3.org/ns/activitystreams",
|
||||||
|
JsonLdOptions()
|
||||||
|
)
|
||||||
|
println(JsonUtils.toPrettyString(compact))
|
||||||
|
|
||||||
|
val readTree = jacksonObjectMapper().readTree(JsonUtils.toString(compact))
|
||||||
|
|
||||||
|
assertEquals(actor.url.toString(), readTree["id"].asText())
|
||||||
|
|
||||||
|
assertEquals("Person", readTree["type"].asText())
|
||||||
|
|
||||||
|
// inbox, outbox のテスト
|
||||||
|
assertEquals(actor.inbox.toString(), readTree["inbox"].asText())
|
||||||
|
assertEquals(actor.outbox.toString(), readTree["outbox"].asText())
|
||||||
|
|
||||||
|
// followers, following のテスト
|
||||||
|
assertEquals(actor.followersEndpoint.toString(), readTree["followers"].asText())
|
||||||
|
assertEquals(actor.followingEndpoint.toString(), readTree["following"].asText())
|
||||||
|
|
||||||
|
// preferredUsername のテスト
|
||||||
|
assertEquals(actor.name.name, readTree["preferredUsername"].asText())
|
||||||
|
|
||||||
|
// name のテスト
|
||||||
|
assertEquals(actor.screenName.screenName, readTree["name"].asText())
|
||||||
|
|
||||||
|
// publicKey のテスト
|
||||||
|
val publicKeyNode = readTree["https://w3id.org/security#publicKey"]
|
||||||
|
assertTrue(publicKeyNode.isObject) // publicKey がオブジェクトか確認
|
||||||
|
assertEquals(actor.keyId.keyId, publicKeyNode["id"].asText())
|
||||||
|
assertEquals(actor.url.toString(), publicKeyNode["https://w3id.org/security#owner"].asText())
|
||||||
|
assertEquals(actor.publicKey.publicKey, publicKeyNode["https://w3id.org/security#publicKeyPem"].asText())
|
||||||
|
|
||||||
|
// @context のテスト
|
||||||
|
assertEquals("https://www.w3.org/ns/activitystreams", readTree["@context"].asText())
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,85 @@
|
||||||
|
package dev.usbharu.hideout.activitypub.external.activitystreams
|
||||||
|
|
||||||
|
import dev.usbharu.hideout.core.domain.model.actor.*
|
||||||
|
import dev.usbharu.hideout.core.domain.model.emoji.CustomEmojiId
|
||||||
|
import dev.usbharu.hideout.core.domain.model.instance.InstanceId
|
||||||
|
import dev.usbharu.hideout.core.domain.model.media.MediaId
|
||||||
|
import dev.usbharu.hideout.core.domain.model.support.domain.Domain
|
||||||
|
import dev.usbharu.hideout.core.infrastructure.other.TwitterSnowflakeIdGenerateService
|
||||||
|
import kotlinx.coroutines.runBlocking
|
||||||
|
import java.net.URI
|
||||||
|
import java.time.Instant
|
||||||
|
|
||||||
|
object TestActorFactory {
|
||||||
|
private val idGenerateService = TwitterSnowflakeIdGenerateService
|
||||||
|
|
||||||
|
fun create(
|
||||||
|
id: Long = generateId(),
|
||||||
|
actorName: String = "test-$id",
|
||||||
|
domain: String = "example.com",
|
||||||
|
actorScreenName: String = actorName,
|
||||||
|
description: String = "test description",
|
||||||
|
inbox: URI = URI.create("https://example.com/$id/inbox"),
|
||||||
|
outbox: URI = URI.create("https://example.com/$id/outbox"),
|
||||||
|
uri: URI = URI.create("https://example.com/$id"),
|
||||||
|
publicKey: ActorPublicKey = ActorPublicKey(""),
|
||||||
|
privateKey: ActorPrivateKey? = null,
|
||||||
|
createdAt: Instant = Instant.now(),
|
||||||
|
keyId: String = "https://example.com/$id#key-id",
|
||||||
|
followersEndpoint: URI = URI.create("https://example.com/$id/followers"),
|
||||||
|
followingEndpoint: URI = URI.create("https://example.com/$id/following"),
|
||||||
|
instanceId: Long = 1L,
|
||||||
|
locked: Boolean = false,
|
||||||
|
followersCount: Int = 0,
|
||||||
|
followingCount: Int = 0,
|
||||||
|
postCount: Int = 0,
|
||||||
|
lastPostDate: Instant? = null,
|
||||||
|
lastUpdateAt: Instant = createdAt,
|
||||||
|
suspend: Boolean = false,
|
||||||
|
alsoKnownAs: Set<ActorId> = emptySet(),
|
||||||
|
moveTo: Long? = null,
|
||||||
|
emojiIds: Set<CustomEmojiId> = emptySet(),
|
||||||
|
deleted: Boolean = false,
|
||||||
|
icon: Long? = null,
|
||||||
|
banner: Long? = null,
|
||||||
|
): Actor {
|
||||||
|
return runBlocking {
|
||||||
|
Actor(
|
||||||
|
id = ActorId(id),
|
||||||
|
name = ActorName(actorName),
|
||||||
|
domain = Domain(domain),
|
||||||
|
screenName = ActorScreenName(actorScreenName),
|
||||||
|
description = ActorDescription(description),
|
||||||
|
inbox = inbox,
|
||||||
|
outbox = outbox,
|
||||||
|
url = uri,
|
||||||
|
publicKey = publicKey,
|
||||||
|
privateKey = privateKey,
|
||||||
|
createdAt = createdAt,
|
||||||
|
keyId = ActorKeyId(keyId),
|
||||||
|
followersEndpoint = followersEndpoint,
|
||||||
|
followingEndpoint = followingEndpoint,
|
||||||
|
instance = InstanceId(instanceId),
|
||||||
|
locked = locked,
|
||||||
|
followersCount = ActorRelationshipCount(followersCount),
|
||||||
|
followingCount = ActorRelationshipCount(followingCount),
|
||||||
|
postsCount = ActorPostsCount(postCount),
|
||||||
|
lastPostAt = lastPostDate,
|
||||||
|
lastUpdateAt = lastUpdateAt,
|
||||||
|
suspend = suspend,
|
||||||
|
alsoKnownAs = alsoKnownAs,
|
||||||
|
moveTo = moveTo?.let { ActorId(it) },
|
||||||
|
emojiIds = emojiIds,
|
||||||
|
deleted = deleted,
|
||||||
|
icon = icon?.let { MediaId(it) },
|
||||||
|
banner = banner?.let { MediaId(it) },
|
||||||
|
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun generateId(): Long = runBlocking {
|
||||||
|
idGenerateService.generateId()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -0,0 +1,14 @@
|
||||||
|
spring:
|
||||||
|
datasource:
|
||||||
|
url: "jdbc:h2:mem:test;MODE=POSTGRESQL;DB_CLOSE_DELAY=-1;CASE_INSENSITIVE_IDENTIFIERS=true;TRACE_LEVEL_FILE=4;"
|
||||||
|
driver-class-name: org.h2.Driver
|
||||||
|
flyway:
|
||||||
|
clean-disabled: false
|
||||||
|
hideout:
|
||||||
|
url: "https://example.com"
|
||||||
|
security:
|
||||||
|
jwt:
|
||||||
|
generate: true
|
||||||
|
key-id: a
|
||||||
|
private-key: "MIIEvwIBADANBgkqhkiG9w0BAQEFAASCBKkwggSlAgEAAoIBAQC7VJTUt9Us8cKjMzEfYyjiWA4R4/M2bS1GB4t7NXp98C3SC6dVMvDuictGeurT8jNbvJZHtCSuYEvuNMoSfm76oqFvAp8Gy0iz5sxjZmSnXyCdPEovGhLa0VzMaQ8s+CLOyS56YyCFGeJZqgtzJ6GR3eqoYSW9b9UMvkBpZODSctWSNGj3P7jRFDO5VoTwCQAWbFnOjDfH5Ulgp2PKSQnSJP3AJLQNFNe7br1XbrhV//eO+t51mIpGSDCUv3E0DDFcWDTH9cXDTTlRZVEiR2BwpZOOkE/Z0/BVnhZYL71oZV34bKfWjQIt6V/isSMahdsAASACp4ZTGtwiVuNd9tybAgMBAAECggEBAKTmjaS6tkK8BlPXClTQ2vpz/N6uxDeS35mXpqasqskVlaAidgg/sWqpjXDbXr93otIMLlWsM+X0CqMDgSXKejLS2jx4GDjI1ZTXg++0AMJ8sJ74pWzVDOfmCEQ/7wXs3+cbnXhKriO8Z036q92Qc1+N87SI38nkGa0ABH9CN83HmQqt4fB7UdHzuIRe/me2PGhIq5ZBzj6h3BpoPGzEP+x3l9YmK8t/1cN0pqI+dQwYdgfGjackLu/2qH80MCF7IyQaseZUOJyKrCLtSD/Iixv/hzDEUPfOCjFDgTpzf3cwta8+oE4wHCo1iI1/4TlPkwmXx4qSXtmw4aQPz7IDQvECgYEA8KNThCO2gsC2I9PQDM/8Cw0O983WCDY+oi+7JPiNAJwv5DYBqEZB1QYdj06YD16XlC/HAZMsMku1na2TN0driwenQQWzoev3g2S7gRDoS/FCJSI3jJ+kjgtaA7Qmzlgk1TxODN+G1H91HW7t0l7VnL27IWyYo2qRRK3jzxqUiPUCgYEAx0oQs2reBQGMVZnApD1jeq7n4MvNLcPvt8b/eU9iUv6Y4Mj0Suo/AU8lYZXm8ubbqAlwz2VSVunD2tOplHyMUrtCtObAfVDUAhCndKaA9gApgfb3xw1IKbuQ1u4IF1FJl3VtumfQn//LiH1B3rXhcdyo3/vIttEk48RakUKClU8CgYEAzV7W3COOlDDcQd935DdtKBFRAPRPAlspQUnzMi5eSHMD/ISLDY5IiQHbIH83D4bvXq0X7qQoSBSNP7Dvv3HYuqMhf0DaegrlBuJllFVVq9qPVRnKxt1Il2HgxOBvbhOT+9in1BzA+YJ99UzC85O0Qz06A+CmtHEy4aZ2kj5hHjECgYEAmNS4+A8Fkss8Js1RieK2LniBxMgmYml3pfVLKGnzmng7H2+cwPLhPIzIuwytXywh2bzbsYEfYx3EoEVgMEpPhoarQnYPukrJO4gwE2o5Te6T5mJSZGlQJQj9q4ZB2Dfzet6INsK0oG8XVGXSpQvQh3RUYekCZQkBBFcpqWpbIEsCgYAnM3DQf3FJoSnXaMhrVBIovic5l0xFkEHskAjFTevO86Fsz1C2aSeRKSqGFoOQ0tmJzBEs1R6KqnHInicDTQrKhArgLXX4v3CddjfTRJkFWDbE/CkvKZNOrcf1nhaGCPspRJj2KUkj1Fhl9Cncdn/RsYEONbwQSjIfMPkvxF+8HQ=="
|
||||||
|
public-key: "MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAu1SU1LfVLPHCozMxH2Mo4lgOEePzNm0tRgeLezV6ffAt0gunVTLw7onLRnrq0/IzW7yWR7QkrmBL7jTKEn5u+qKhbwKfBstIs+bMY2Zkp18gnTxKLxoS2tFczGkPLPgizskuemMghRniWaoLcyehkd3qqGElvW/VDL5AaWTg0nLVkjRo9z+40RQzuVaE8AkAFmxZzow3x+VJYKdjykkJ0iT9wCS0DRTXu269V264Vf/3jvredZiKRkgwlL9xNAwxXFg0x/XFw005UWVRIkdgcKWTjpBP2dPwVZ4WWC+9aGVd+Gyn1o0CLelf4rEjGoXbAAEgAqeGUxrcIlbjXfbcmwIDAQAB"
|
|
@ -0,0 +1,35 @@
|
||||||
|
insert into instance(id, name, description, url, icon_url, shared_inbox, software, version, is_blocked, is_muted,
|
||||||
|
moderation_note, created_at)
|
||||||
|
VALUES (1, 'instance', 'description', 'https://example.com', 'https://example.com', 'https://example.com', 'software',
|
||||||
|
'version', false, false, 'note', current_timestamp)
|
||||||
|
, (2, 'instance', 'description', 'https://remote.example.com', 'https://example.com', 'https://remote.example.com',
|
||||||
|
'software',
|
||||||
|
'version', false, false, 'note', current_timestamp)
|
||||||
|
, (3, 'instance', 'description', 'https://remote2.example.com', 'https://example.com',
|
||||||
|
'https://remote2.example.com', 'software',
|
||||||
|
'version', false, false, 'note', current_timestamp);
|
||||||
|
|
||||||
|
insert into actors(id, name, domain, screen_name, description, inbox, outbox, url, public_key, private_key, created_at,
|
||||||
|
key_id, following, followers, instance, locked, following_count, followers_count, posts_count,
|
||||||
|
last_post_at, last_update_at, suspend, move_to, icon, banner)
|
||||||
|
VALUES (1, 'test', 'example.com', 'test-actor', 'actor_description', 'https://example.com/test/inbox',
|
||||||
|
'https://example.com/outbox', 'https://example.com/test', '---BEGIN PUBLIC KEY---', '---BEGIN PRIVATE KEY---',
|
||||||
|
current_timestamp, 'https://example.com/test#main-key', 'https://example.com/test/following',
|
||||||
|
'https://example.com/test/followers', 1, false, 1, 0, 0, null, current_timestamp, false, null, null, null),
|
||||||
|
(2, 'test', 'remote.example.com', 'test-actor', 'actor_description', 'https://remote.example.com/test/inbox',
|
||||||
|
'https://remote.example.com/outbox', 'https://remote.example.com', '---BEGIN PUBLIC KEY---',
|
||||||
|
'---BEGIN PRIVATE KEY---',
|
||||||
|
current_timestamp, 'https://remote.example.com/test#main-key', 'https://remote.example.com/test/following',
|
||||||
|
'https://remote.example.com/test/followers', 2, false, 1, 0, 0, null, current_timestamp, false, null, null,
|
||||||
|
null),
|
||||||
|
(3, 'test', 'remote2.example.com', 'test-actor', 'actor_description', 'https://remote2.example.com/test/inbox',
|
||||||
|
'https://remote2.example.com/test/outbox', 'https://remote2.example.com/test', '---BEGIN PUBLIC KEY---',
|
||||||
|
'---BEGIN PRIVATE KEY---',
|
||||||
|
current_timestamp, 'https://remote2.example.com/test#main-key', 'https://remote2.example.com/test/following',
|
||||||
|
'https://example.com/followers', 3, false, 1, 0, 0, null, current_timestamp, false, null, null, null),
|
||||||
|
(4, 'test2', 'remote2.example.com', 'test-actor', 'actor_description', 'https://example.com/inbox',
|
||||||
|
'https://remote2.example.com/test2/outbox', 'https://remote2.example.com/test2', '---BEGIN PUBLIC KEY---',
|
||||||
|
'---BEGIN PRIVATE KEY---',
|
||||||
|
current_timestamp, 'https://remote2.example.com/test2#main-key', 'https://remote2.example.com/test2/following',
|
||||||
|
'https://remote2.example.com/test2/followers', 3, false, 1, 0, 0, null, current_timestamp, false, null, null,
|
||||||
|
null);
|
|
@ -83,7 +83,7 @@ class SecurityConfig {
|
||||||
}
|
}
|
||||||
|
|
||||||
@Bean
|
@Bean
|
||||||
@Order(3)
|
@Order(6)
|
||||||
fun httpSecurityFilterChain(http: HttpSecurity): SecurityFilterChain {
|
fun httpSecurityFilterChain(http: HttpSecurity): SecurityFilterChain {
|
||||||
http {
|
http {
|
||||||
authorizeHttpRequests {
|
authorizeHttpRequests {
|
||||||
|
|
|
@ -23,6 +23,7 @@ import dev.usbharu.hideout.generate.JsonOrFormModelMethodProcessor
|
||||||
import org.springframework.boot.web.servlet.FilterRegistrationBean
|
import org.springframework.boot.web.servlet.FilterRegistrationBean
|
||||||
import org.springframework.context.annotation.Bean
|
import org.springframework.context.annotation.Bean
|
||||||
import org.springframework.context.annotation.Configuration
|
import org.springframework.context.annotation.Configuration
|
||||||
|
import org.springframework.core.annotation.Order
|
||||||
import org.springframework.http.converter.HttpMessageConverter
|
import org.springframework.http.converter.HttpMessageConverter
|
||||||
import org.springframework.web.method.support.HandlerMethodArgumentResolver
|
import org.springframework.web.method.support.HandlerMethodArgumentResolver
|
||||||
import org.springframework.web.servlet.config.annotation.InterceptorRegistry
|
import org.springframework.web.servlet.config.annotation.InterceptorRegistry
|
||||||
|
@ -31,10 +32,11 @@ import org.springframework.web.servlet.mvc.method.annotation.RequestResponseBody
|
||||||
import org.springframework.web.servlet.mvc.method.annotation.ServletModelAttributeMethodProcessor
|
import org.springframework.web.servlet.mvc.method.annotation.ServletModelAttributeMethodProcessor
|
||||||
|
|
||||||
@Configuration
|
@Configuration
|
||||||
|
@Order(2)
|
||||||
class MvcConfigurer(
|
class MvcConfigurer(
|
||||||
private val jsonOrFormModelMethodProcessor: JsonOrFormModelMethodProcessor,
|
private val jsonOrFormModelMethodProcessor: JsonOrFormModelMethodProcessor,
|
||||||
private val spaInterceptor: SPAInterceptor,
|
private val spaInterceptor: SPAInterceptor,
|
||||||
private val applicationRequestLogInterceptor: ApplicationRequestLogInterceptor
|
private val applicationRequestLogInterceptor: ApplicationRequestLogInterceptor,
|
||||||
) : WebMvcConfigurer {
|
) : WebMvcConfigurer {
|
||||||
override fun addArgumentResolvers(resolvers: MutableList<HandlerMethodArgumentResolver>) {
|
override fun addArgumentResolvers(resolvers: MutableList<HandlerMethodArgumentResolver>) {
|
||||||
resolvers.add(jsonOrFormModelMethodProcessor)
|
resolvers.add(jsonOrFormModelMethodProcessor)
|
||||||
|
|
|
@ -0,0 +1,6 @@
|
||||||
|
package dev.usbharu.hideout.core.domain.model.support.principal
|
||||||
|
|
||||||
|
import dev.usbharu.hideout.core.domain.model.actor.ActorId
|
||||||
|
import dev.usbharu.hideout.core.domain.model.support.acct.Acct
|
||||||
|
|
||||||
|
class RemoteUser(actorId: ActorId, acct: Acct?) : Principal(actorId, null, acct)
|
|
@ -25,6 +25,7 @@ import dev.usbharu.hideout.core.domain.model.support.timelineobjectdetail.Timeli
|
||||||
import dev.usbharu.hideout.core.domain.model.timeline.Timeline
|
import dev.usbharu.hideout.core.domain.model.timeline.Timeline
|
||||||
import dev.usbharu.hideout.core.domain.model.timelinerelationship.TimelineRelationship
|
import dev.usbharu.hideout.core.domain.model.timelinerelationship.TimelineRelationship
|
||||||
|
|
||||||
|
@Suppress("TooManyFunctions")
|
||||||
interface TimelineStore {
|
interface TimelineStore {
|
||||||
suspend fun addPost(post: Post)
|
suspend fun addPost(post: Post)
|
||||||
suspend fun updatePost(post: Post)
|
suspend fun updatePost(post: Post)
|
||||||
|
|
|
@ -34,7 +34,7 @@ import dev.usbharu.hideout.mastodon.interfaces.api.generated.model.CustomEmoji a
|
||||||
|
|
||||||
@Suppress("IncompleteDestructuring")
|
@Suppress("IncompleteDestructuring")
|
||||||
@Repository
|
@Repository
|
||||||
class StatusQueryServiceImpl : StatusQueryService {
|
class ExposedStatusQueryServiceImpl : StatusQueryService {
|
||||||
|
|
||||||
protected fun authorizedQuery(principal: Principal? = null): QueryAlias {
|
protected fun authorizedQuery(principal: Principal? = null): QueryAlias {
|
||||||
if (principal == null) {
|
if (principal == null) {
|
||||||
|
@ -60,8 +60,16 @@ class StatusQueryServiceImpl : StatusQueryService {
|
||||||
.where {
|
.where {
|
||||||
Posts.visibility eq Visibility.PUBLIC.name or
|
Posts.visibility eq Visibility.PUBLIC.name or
|
||||||
(Posts.visibility eq Visibility.UNLISTED.name) or
|
(Posts.visibility eq Visibility.UNLISTED.name) or
|
||||||
(Posts.visibility eq Visibility.DIRECT.name and (PostsVisibleActors.actorId eq principal.actorId.id)) or
|
(
|
||||||
(Posts.visibility eq Visibility.FOLLOWERS.name and (Relationships.blocking eq false and (relationshipsAlias[Relationships.following] eq true))) or
|
Posts.visibility eq Visibility.DIRECT.name and
|
||||||
|
(PostsVisibleActors.actorId eq principal.actorId.id)
|
||||||
|
) or
|
||||||
|
(
|
||||||
|
Posts.visibility eq Visibility.FOLLOWERS.name and (
|
||||||
|
Relationships.blocking eq false and
|
||||||
|
(relationshipsAlias[Relationships.following] eq true)
|
||||||
|
)
|
||||||
|
) or
|
||||||
(Posts.actorId eq principal.actorId.id)
|
(Posts.actorId eq principal.actorId.id)
|
||||||
}
|
}
|
||||||
.alias("authorized_table")
|
.alias("authorized_table")
|
||||||
|
|
|
@ -19,10 +19,10 @@ import org.springframework.transaction.annotation.Transactional
|
||||||
@Sql("/sql/relationships.sql", executionPhase = Sql.ExecutionPhase.BEFORE_TEST_METHOD)
|
@Sql("/sql/relationships.sql", executionPhase = Sql.ExecutionPhase.BEFORE_TEST_METHOD)
|
||||||
@Transactional
|
@Transactional
|
||||||
@SpringBootTest(classes = [SpringApplication::class])
|
@SpringBootTest(classes = [SpringApplication::class])
|
||||||
class StatusQueryServiceImplTest {
|
class ExposedStatusQueryServiceImplTest {
|
||||||
|
|
||||||
@Autowired
|
@Autowired
|
||||||
lateinit var statusQueryServiceImpl: StatusQueryServiceImpl
|
lateinit var statusQueryServiceImpl: ExposedStatusQueryServiceImpl
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun フォロワー限定をフォロワー以外は見れない() = runTest {
|
fun フォロワー限定をフォロワー以外は見れない() = runTest {
|
|
@ -104,9 +104,12 @@ mockito-kotlin = { module = "org.mockito.kotlin:mockito-kotlin", version = "5.4.
|
||||||
|
|
||||||
http-signature = { module = "dev.usbharu:http-signature", version = "1.0.0" }
|
http-signature = { module = "dev.usbharu:http-signature", version = "1.0.0" }
|
||||||
emoji-kt = { module = "dev.usbharu:emoji-kt", version = "2.0.1" }
|
emoji-kt = { module = "dev.usbharu:emoji-kt", version = "2.0.1" }
|
||||||
|
activity-streams-serialization = { module = "dev.usbharu:activity-streams-serialization", version = "0.5.0" }
|
||||||
|
|
||||||
logback-ecs-encoder = { module = "co.elastic.logging:logback-ecs-encoder", version = "1.6.0" }
|
logback-ecs-encoder = { module = "co.elastic.logging:logback-ecs-encoder", version = "1.6.0" }
|
||||||
|
|
||||||
|
jsonld = { module = "com.github.jsonld-java:jsonld-java", version = "0.13.5" }
|
||||||
|
|
||||||
[bundles]
|
[bundles]
|
||||||
|
|
||||||
exposed = ["exposed-core", "exposed-java-time", "exposed-jdbc", "exposed-spring"]
|
exposed = ["exposed-core", "exposed-java-time", "exposed-jdbc", "exposed-spring"]
|
||||||
|
|
Loading…
Reference in New Issue