Merge pull request #676 from usbharu/activitypub

Activitypub-actor-get
This commit is contained in:
usbharu 2025-02-16 12:12:26 +09:00 committed by GitHub
commit fdcf58fca9
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
25 changed files with 627 additions and 14 deletions

View File

@ -2,23 +2,114 @@ import kotlinx.kover.gradle.plugin.dsl.CoverageUnit
plugins {
alias(libs.plugins.kotlin.jvm)
alias(libs.plugins.detekt)
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 {
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 {
withType<Test> {
withType<io.gitlab.arturbosch.detekt.Detekt> {
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()
}
}
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 {
currentProject {
sources {
@ -57,3 +148,7 @@ kover {
}
}
springBoot{
buildInfo { }
}

View File

@ -1,5 +0,0 @@
package dev.usbharu
fun main() {
println("Hello World!")
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -83,7 +83,7 @@ class SecurityConfig {
}
@Bean
@Order(3)
@Order(6)
fun httpSecurityFilterChain(http: HttpSecurity): SecurityFilterChain {
http {
authorizeHttpRequests {

View File

@ -23,6 +23,7 @@ import dev.usbharu.hideout.generate.JsonOrFormModelMethodProcessor
import org.springframework.boot.web.servlet.FilterRegistrationBean
import org.springframework.context.annotation.Bean
import org.springframework.context.annotation.Configuration
import org.springframework.core.annotation.Order
import org.springframework.http.converter.HttpMessageConverter
import org.springframework.web.method.support.HandlerMethodArgumentResolver
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
@Configuration
@Order(2)
class MvcConfigurer(
private val jsonOrFormModelMethodProcessor: JsonOrFormModelMethodProcessor,
private val spaInterceptor: SPAInterceptor,
private val applicationRequestLogInterceptor: ApplicationRequestLogInterceptor
private val applicationRequestLogInterceptor: ApplicationRequestLogInterceptor,
) : WebMvcConfigurer {
override fun addArgumentResolvers(resolvers: MutableList<HandlerMethodArgumentResolver>) {
resolvers.add(jsonOrFormModelMethodProcessor)

View File

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

View File

@ -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.timelinerelationship.TimelineRelationship
@Suppress("TooManyFunctions")
interface TimelineStore {
suspend fun addPost(post: Post)
suspend fun updatePost(post: Post)

View File

@ -34,7 +34,7 @@ import dev.usbharu.hideout.mastodon.interfaces.api.generated.model.CustomEmoji a
@Suppress("IncompleteDestructuring")
@Repository
class StatusQueryServiceImpl : StatusQueryService {
class ExposedStatusQueryServiceImpl : StatusQueryService {
protected fun authorizedQuery(principal: Principal? = null): QueryAlias {
if (principal == null) {
@ -60,8 +60,16 @@ class StatusQueryServiceImpl : StatusQueryService {
.where {
Posts.visibility eq Visibility.PUBLIC.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)
}
.alias("authorized_table")

View File

@ -19,10 +19,10 @@ import org.springframework.transaction.annotation.Transactional
@Sql("/sql/relationships.sql", executionPhase = Sql.ExecutionPhase.BEFORE_TEST_METHOD)
@Transactional
@SpringBootTest(classes = [SpringApplication::class])
class StatusQueryServiceImplTest {
class ExposedStatusQueryServiceImplTest {
@Autowired
lateinit var statusQueryServiceImpl: StatusQueryServiceImpl
lateinit var statusQueryServiceImpl: ExposedStatusQueryServiceImpl
@Test
fun フォロワー限定をフォロワー以外は見れない() = runTest {

View File

@ -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" }
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" }
jsonld = { module = "com.github.jsonld-java:jsonld-java", version = "0.13.5" }
[bundles]
exposed = ["exposed-core", "exposed-java-time", "exposed-jdbc", "exposed-spring"]