Merge branch 'develop' into feature/mastodon-follow-api

# Conflicts:
#	src/main/kotlin/dev/usbharu/hideout/application/config/SecurityConfig.kt
#	src/main/kotlin/dev/usbharu/hideout/application/infrastructure/exposed/ExposedTransaction.kt
#	src/main/kotlin/dev/usbharu/hideout/core/infrastructure/springframework/httpsignature/HttpSignatureFilter.kt
#	src/main/resources/application.yml
#	src/main/resources/db/migration/V1__Init_DB.sql
This commit is contained in:
usbharu 2023-12-06 17:18:16 +09:00
commit b9ecd2feff
187 changed files with 4517 additions and 2536 deletions

View File

@ -1,34 +0,0 @@
name: Coverage
on:
pull_request:
permissions:
pull-requests: write
jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- name: Set up JDK 17
uses: actions/setup-java@v3
with:
java-version: '17'
distribution: 'temurin'
- name: Run JUnit
uses: gradle/gradle-build-action@v2.8.1
with:
arguments: koverXmlReport -x integrationTest
- name: Add coverage report to PR
if: always()
id: kover
uses: mi-kas/kover-report@v1
with:
path: |
${{ github.workspace }}/build/reports/kover/report.xml
token: ${{ secrets.GITHUB_TOKEN }}
title: Code Coverage
update-comment: true
min-coverage-overall: 80
min-coverage-changed-files: 80
coverage-counter-type: LINE

View File

@ -1,67 +0,0 @@
# This workflow uses actions that are not certified by GitHub.
# They are provided by a third-party and are governed by
# separate terms of service, privacy policy, and support
# documentation.
# This workflow will build a Java project with Gradle and cache/restore any dependencies to improve the workflow execution time
# For more information see: https://docs.github.com/en/actions/automating-builds-and-tests/building-and-testing-java-with-gradle
name: Integration Test
on:
pull_request:
branches: [ "develop" ]
permissions:
contents: read
checks: write
id-token: write
jobs:
integration-test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- name: Cache
uses: actions/cache@v3.3.2
with:
path: ~/.gradle/wrapper
key: gradle-wrapper-${{ hashFiles('gradle/wrapper/gradle-wrapper.properties') }}
- name: Cache
uses: actions/cache@v3.3.2
with:
path: |
~/.gradle/caches/jars-*
~/.gradle/caches/transforms-*
~/.gradle/caches/modules-*
key: gradle-dependencies-${{ steps.cache-key.outputs.week }}-${{ hashFiles('**/*.gradle.kts') }}
restore-keys: gradle-dependencies-${{ steps.cache-key.outputs.week }}-
- name: Cache
uses: actions/cache@v3.3.2
with:
path: |
~/.gradle/caches/build-cache-*
~/.gradle/caches/[0-9]*.*
.gradle
key: ${{ runner.os }}-gradle-build-${{ github.workflow }}-${{ steps.cache-key.outputs.week }}-${{ github.sha }}
restore-keys: ${{ runner.os }}-gradle-build-${{ github.workflow }}-${{ steps.cache-key.outputs.week }}-
- name: Set up JDK 17
uses: actions/setup-java@v3
with:
java-version: '17'
distribution: 'temurin'
- name: MongoDB in GitHub Actions
uses: supercharge/mongodb-github-action@1.10.0
with:
mongodb-version: latest
- name: Run Integration Test
uses: gradle/gradle-build-action@v2.8.1
with:
arguments: integrationTest
- name: Publish Test Report
uses: mikepenz/action-junit-report@v2
if: always()
with:
report_paths: '**/build/test-results/test/TEST-*.xml'

View File

@ -1,61 +0,0 @@
# This workflow uses actions that are not certified by GitHub.
# They are provided by a third-party and are governed by
# separate terms of service, privacy policy, and support
# documentation.
# This workflow will build a Java project with Gradle and cache/restore any dependencies to improve the workflow execution time
# For more information see: https://docs.github.com/en/actions/automating-builds-and-tests/building-and-testing-java-with-gradle
name: Lint
on:
pull_request:
branches: [ "develop" ]
permissions:
contents: read
pull-requests: write
jobs:
lint:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- name: Cache
uses: actions/cache@v3.3.2
with:
path: ~/.gradle/wrapper
key: gradle-wrapper-${{ hashFiles('gradle/wrapper/gradle-wrapper.properties') }}
- name: Cache
uses: actions/cache@v3.3.2
with:
path: |
~/.gradle/caches/jars-*
~/.gradle/caches/transforms-*
~/.gradle/caches/modules-*
key: gradle-dependencies-${{ steps.cache-key.outputs.week }}-${{ hashFiles('**/*.gradle.kts') }}
restore-keys: gradle-dependencies-${{ steps.cache-key.outputs.week }}-
- name: Cache
uses: actions/cache@v3.3.2
with:
path: |
~/.gradle/caches/build-cache-*
~/.gradle/caches/[0-9]*.*
.gradle
key: ${{ runner.os }}-gradle-build-${{ github.workflow }}-${{ steps.cache-key.outputs.week }}-${{ github.sha }}
restore-keys: ${{ runner.os }}-gradle-build-${{ github.workflow }}-${{ steps.cache-key.outputs.week }}-
- name: Set up JDK 17
uses: actions/setup-java@v3
with:
java-version: '17'
distribution: 'temurin'
- name: Build with Gradle
uses: gradle/gradle-build-action@67421db6bd0bf253fb4bd25b31ebb98943c375e1
with:
arguments: detektMain
- name: "reviewdog-suggester: Suggest any code changes based on diff with reviewdog"
if: ${{ always() }}
uses: reviewdog/action-suggester@v1
with:
github_token: ${{ secrets.GITHUB_TOKEN }}

View File

@ -0,0 +1,412 @@
name: PullRequest Merge Check
on:
pull_request:
branches:
- "develop"
permissions:
contents: read
checks: write
id-token: write
pull-requests: write
jobs:
setup:
name: Setup
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v3
- name: Gradle Wrapper Cache
uses: actions/cache@v3.3.2
with:
path: ~/.gradle/wrapper
key: gradle-wrapper-${{ hashFiles('gradle/wrapper/gradle-wrapper.properties') }}
- name: Dependencies Cache
uses: actions/cache@v3.3.2
with:
path: |
~/.gradle/cache/jars-*
~/.gradle/caches/transforms-*
~/.gradle/caches/modules-*
key: gradle-dependencies-${{ hashFiles('**/*.gradle.kts') }}
restore-keys: gradle-dependencies-
- name: Cache
uses: actions/cache@v3.3.2
with:
path: |
~/.gradle/caches/build-cache-*
~/.gradle/caches/[0-9]*.*
.gradle
key: ${{ runner.os }}-gradle-build-${{ github.workflow }}-${{ github.sha }}
restore-keys: ${{ runner.os }}-gradle-build-${{ github.workflow }}-
- name: Build Cache
uses: actions/cache@v3.3.2
with:
path: |
build
key: gradle-build-${{ hashFiles('**/*.gradle.kts') }}-${{ hashFiles('**/*.kt') }}-${{ github.sha }}
- name: Set up JDK 17
uses: actions/setup-java@v3
with:
java-version: '17'
distribution: 'temurin'
- name: Build
uses: gradle/gradle-build-action@v2.8.1
with:
arguments: testClasses
unit-test:
name: Unit Test
needs: [ setup ]
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v3
- name: Gradle Wrapper Cache
uses: actions/cache@v3.3.2
with:
path: ~/.gradle/wrapper
key: gradle-wrapper-${{ hashFiles('gradle/wrapper/gradle-wrapper.properties') }}
- name: Dependencies Cache
uses: actions/cache@v3.3.2
with:
path: |
~/.gradle/cache/jars-*
~/.gradle/caches/transforms-*
~/.gradle/caches/modules-*
key: gradle-dependencies-${{ hashFiles('**/*.gradle.kts') }}
restore-keys: gradle-dependencies-
- name: Cache
uses: actions/cache@v3.3.2
with:
path: |
~/.gradle/caches/build-cache-*
~/.gradle/caches/[0-9]*.*
.gradle
key: ${{ runner.os }}-gradle-build-${{ github.workflow }}-${{ github.sha }}
restore-keys: ${{ runner.os }}-gradle-build-${{ github.workflow }}-
- name: Build Cache
uses: actions/cache@v3.3.2
with:
path: |
build
key: gradle-build-${{ hashFiles('**/*.gradle.kts') }}-${{ hashFiles('src') }}-${{ github.sha }}
- name: Set up JDK 17
uses: actions/setup-java@v3
with:
java-version: '17'
distribution: 'temurin'
- name: Unit Test
uses: gradle/gradle-build-action@v2.8.1
with:
arguments: test
- name: Save Test Report
if: always()
uses: actions/cache/save@v3
with:
path: build/test-results
key: unit-test-report-${{ github.sha }}
integration-test:
name: Integration Test
needs: [ setup ]
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v3
- name: Gradle Wrapper Cache
uses: actions/cache@v3.3.2
with:
path: ~/.gradle/wrapper
key: gradle-wrapper-${{ hashFiles('gradle/wrapper/gradle-wrapper.properties') }}
- name: Dependencies Cache
uses: actions/cache@v3.3.2
with:
path: |
~/.gradle/cache/jars-*
~/.gradle/caches/transforms-*
~/.gradle/caches/modules-*
key: gradle-dependencies-${{ hashFiles('**/*.gradle.kts') }}
restore-keys: gradle-dependencies-
- name: Cache
uses: actions/cache@v3.3.2
with:
path: |
~/.gradle/caches/build-cache-*
~/.gradle/caches/[0-9]*.*
.gradle
key: ${{ runner.os }}-gradle-build-${{ github.workflow }}-${{ github.sha }}
restore-keys: ${{ runner.os }}-gradle-build-${{ github.workflow }}-
- name: Build Cache
uses: actions/cache@v3.3.2
with:
path: |
build
key: gradle-build-${{ hashFiles('**/*.gradle.kts') }}-${{ hashFiles('src') }}-${{ github.sha }}
- name: Set up JDK 17
uses: actions/setup-java@v3
with:
java-version: '17'
distribution: 'temurin'
- name: MongoDB in GitHub Actions
uses: supercharge/mongodb-github-action@1.10.0
with:
mongodb-version: latest
- name: Unit Test
uses: gradle/gradle-build-action@v2.8.1
with:
arguments: integrationTest
- name: Save Test Report
if: always()
uses: actions/cache/save@v3
with:
path: build/test-results
key: integration-test-report-${{ github.sha }}
coverage:
name: Coverage
needs: [ setup ]
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v3
- name: Gradle Wrapper Cache
uses: actions/cache@v3.3.2
with:
path: ~/.gradle/wrapper
key: gradle-wrapper-${{ hashFiles('gradle/wrapper/gradle-wrapper.properties') }}
- name: Dependencies Cache
uses: actions/cache@v3.3.2
with:
path: |
~/.gradle/cache/jars-*
~/.gradle/caches/transforms-*
~/.gradle/caches/modules-*
key: gradle-dependencies-${{ hashFiles('**/*.gradle.kts') }}
restore-keys: gradle-dependencies-
- name: Cache
uses: actions/cache@v3.3.2
with:
path: |
~/.gradle/caches/build-cache-*
~/.gradle/caches/[0-9]*.*
.gradle
key: ${{ runner.os }}-gradle-build-${{ github.workflow }}-${{ github.sha }}
restore-keys: ${{ runner.os }}-gradle-build-${{ github.workflow }}-
- name: Build Cache
uses: actions/cache@v3.3.2
with:
path: |
build
key: gradle-build-${{ hashFiles('**/*.gradle.kts') }}-${{ hashFiles('src') }}-${{ github.sha }}
- name: Set up JDK 17
uses: actions/setup-java@v3
with:
java-version: '17'
distribution: 'temurin'
- name: Run Kover
uses: gradle/gradle-build-action@v2.8.1
with:
arguments: koverXmlReport -x integrationTest -x e2eTest
- name: Add coverage report to PR
if: always()
id: kover
uses: mi-kas/kover-report@v1
with:
path: |
${{ github.workspace }}/build/reports/kover/report.xml
token: ${{ secrets.GITHUB_TOKEN }}
title: Code Coverage
update-comment: true
min-coverage-overall: 80
min-coverage-changed-files: 80
coverage-counter-type: LINE
report-tests:
name: Report Tests
if: success() || failure()
needs: [ unit-test,integration-test,e2e-test ]
runs-on: ubuntu-latest
steps:
- name: Restore Test Report
uses: actions/cache/restore@v3
with:
path: build/test-results
key: unit-test-report-${{ github.sha }}
- name: Restore Test Report
uses: actions/cache/restore@v3
with:
path: build/test-results
key: integration-test-report-${{ github.sha }}
- name: Restore Test Report
uses: actions/cache/restore@v3
with:
path: build/test-results
key: e2e-test-report-${{ github.sha }}
- name: JUnit Test Report
uses: mikepenz/action-junit-report@v2
with:
report_paths: '**/TEST-*.xml'
lint:
name: Lint
needs: [ setup ]
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v3
- name: Gradle Wrapper Cache
uses: actions/cache@v3.3.2
with:
path: ~/.gradle/wrapper
key: gradle-wrapper-${{ hashFiles('gradle/wrapper/gradle-wrapper.properties') }}
- name: Dependencies Cache
uses: actions/cache@v3.3.2
with:
path: |
~/.gradle/cache/jars-*
~/.gradle/caches/transforms-*
~/.gradle/caches/modules-*
key: gradle-dependencies-${{ hashFiles('**/*.gradle.kts') }}
restore-keys: gradle-dependencies-
- name: Cache
uses: actions/cache@v3.3.2
with:
path: |
~/.gradle/caches/build-cache-*
~/.gradle/caches/[0-9]*.*
.gradle
key: ${{ runner.os }}-gradle-build-${{ github.workflow }}-${{ github.sha }}
restore-keys: ${{ runner.os }}-gradle-build-${{ github.workflow }}-
- name: Build Cache
uses: actions/cache@v3.3.2
with:
path: |
build
key: gradle-build-${{ hashFiles('**/*.gradle.kts') }}-${{ hashFiles('src') }}-${{ github.sha }}
- name: Set up JDK 17
uses: actions/setup-java@v3
with:
java-version: '17'
distribution: 'temurin'
- name: Build with Gradle
uses: gradle/gradle-build-action@67421db6bd0bf253fb4bd25b31ebb98943c375e1
with:
arguments: detektMain
- name: "reviewdog-suggester: Suggest any code changes based on diff with reviewdog"
if: ${{ always() }}
uses: reviewdog/action-suggester@v1
with:
github_token: ${{ secrets.GITHUB_TOKEN }}
e2e-test:
name: E2E Test
needs: [ setup ]
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v3
- name: Gradle Wrapper Cache
uses: actions/cache@v3.3.2
with:
path: ~/.gradle/wrapper
key: gradle-wrapper-${{ hashFiles('gradle/wrapper/gradle-wrapper.properties') }}
- name: Dependencies Cache
uses: actions/cache@v3.3.2
with:
path: |
~/.gradle/cache/jars-*
~/.gradle/caches/transforms-*
~/.gradle/caches/modules-*
key: gradle-dependencies-${{ hashFiles('**/*.gradle.kts') }}
restore-keys: gradle-dependencies-
- name: Cache
uses: actions/cache@v3.3.2
with:
path: |
~/.gradle/caches/build-cache-*
~/.gradle/caches/[0-9]*.*
.gradle
key: ${{ runner.os }}-gradle-build-${{ github.workflow }}-${{ github.sha }}
restore-keys: ${{ runner.os }}-gradle-build-${{ github.workflow }}-
- name: Build Cache
uses: actions/cache@v3.3.2
with:
path: |
build
key: gradle-build-${{ hashFiles('**/*.gradle.kts') }}-${{ hashFiles('src') }}-${{ github.sha }}
- name: Set up JDK 17
uses: actions/setup-java@v3
with:
java-version: '17'
distribution: 'temurin'
- name: MongoDB in GitHub Actions
uses: supercharge/mongodb-github-action@1.10.0
with:
mongodb-version: latest
- name: setup-chrome
id: setup-chrome
uses: browser-actions/setup-chrome@v1.4.0
- name: Add Path
run: echo ${{ steps.setup-chrome.outputs.chrome-path }} >> $GITHUB_PATH
- name: E2E Test
uses: gradle/gradle-build-action@v2.8.1
with:
arguments: e2eTest
- name: Save Test Report
if: always()
uses: actions/cache/save@v3
with:
path: build/test-results
key: e2e-test-report-${{ github.sha }}

View File

@ -1,63 +0,0 @@
# This workflow uses actions that are not certified by GitHub.
# They are provided by a third-party and are governed by
# separate terms of service, privacy policy, and support
# documentation.
# This workflow will build a Java project with Gradle and cache/restore any dependencies to improve the workflow execution time
# For more information see: https://docs.github.com/en/actions/automating-builds-and-tests/building-and-testing-java-with-gradle
name: Test
on:
pull_request:
branches: [ "develop" ]
permissions:
contents: read
checks: write
id-token: write
jobs:
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- name: Cache
uses: actions/cache@v3.3.2
with:
path: ~/.gradle/wrapper
key: gradle-wrapper-${{ hashFiles('gradle/wrapper/gradle-wrapper.properties') }}
- name: Cache
uses: actions/cache@v3.3.2
with:
path: |
~/.gradle/caches/jars-*
~/.gradle/caches/transforms-*
~/.gradle/caches/modules-*
key: gradle-dependencies-${{ steps.cache-key.outputs.week }}-${{ hashFiles('**/*.gradle.kts') }}
restore-keys: gradle-dependencies-${{ steps.cache-key.outputs.week }}-
- name: Cache
uses: actions/cache@v3.3.2
with:
path: |
~/.gradle/caches/build-cache-*
~/.gradle/caches/[0-9]*.*
.gradle
key: ${{ runner.os }}-gradle-build-${{ github.workflow }}-${{ steps.cache-key.outputs.week }}-${{ github.sha }}
restore-keys: ${{ runner.os }}-gradle-build-${{ github.workflow }}-${{ steps.cache-key.outputs.week }}-
- name: Set up JDK 17
uses: actions/setup-java@v3
with:
java-version: '17'
distribution: 'temurin'
- name: Run JUnit
uses: gradle/gradle-build-action@v2.8.1
with:
arguments: test
- name: Publish Test Report
uses: mikepenz/action-junit-report@v2
if: always()
with:
report_paths: '**/build/test-results/test/TEST-*.xml'

3
.gitignore vendored
View File

@ -40,3 +40,6 @@ out/
/src/main/web/generated/ /src/main/web/generated/
/stats.html /stats.html
/tomcat/ /tomcat/
/tomcat-e2e/
/e2eTest.log
/files/

View File

@ -1,6 +1,5 @@
import org.jetbrains.kotlin.gradle.tasks.KotlinCompile import org.jetbrains.kotlin.gradle.tasks.KotlinCompile
import org.openapitools.generator.gradle.plugin.tasks.GenerateTask import org.openapitools.generator.gradle.plugin.tasks.GenerateTask
import kotlin.math.max
val ktor_version: String by project val ktor_version: String by project
val kotlin_version: String by project val kotlin_version: String by project
@ -13,7 +12,7 @@ plugins {
kotlin("jvm") version "1.8.21" kotlin("jvm") version "1.8.21"
id("org.graalvm.buildtools.native") version "0.9.21" id("org.graalvm.buildtools.native") version "0.9.21"
id("io.gitlab.arturbosch.detekt") version "1.23.1" id("io.gitlab.arturbosch.detekt") version "1.23.1"
id("org.springframework.boot") version "3.1.3" id("org.springframework.boot") version "3.2.0"
kotlin("plugin.spring") version "1.8.21" kotlin("plugin.spring") version "1.8.21"
id("org.openapi.generator") version "7.0.1" id("org.openapi.generator") version "7.0.1"
id("org.jetbrains.kotlinx.kover") version "0.7.4" id("org.jetbrains.kotlinx.kover") version "0.7.4"
@ -32,6 +31,10 @@ sourceSets {
compileClasspath += sourceSets.main.get().output compileClasspath += sourceSets.main.get().output
runtimeClasspath += sourceSets.main.get().output runtimeClasspath += sourceSets.main.get().output
} }
create("e2eTest") {
compileClasspath += sourceSets.main.get().output
runtimeClasspath += sourceSets.main.get().output
}
} }
val intTestImplementation by configurations.getting { val intTestImplementation by configurations.getting {
@ -41,6 +44,14 @@ val intTestRuntimeOnly by configurations.getting {
extendsFrom(configurations.runtimeOnly.get()) extendsFrom(configurations.runtimeOnly.get())
} }
val e2eTestImplementation by configurations.getting {
extendsFrom(configurations.implementation.get())
}
val e2eTestRuntimeOnly by configurations.getting {
extendsFrom(configurations.runtimeOnly.get())
}
val integrationTest = task<Test>("integrationTest") { val integrationTest = task<Test>("integrationTest") {
description = "Runs integration tests." description = "Runs integration tests."
group = "verification" group = "verification"
@ -50,19 +61,26 @@ val integrationTest = task<Test>("integrationTest") {
shouldRunAfter("test") shouldRunAfter("test")
useJUnitPlatform() useJUnitPlatform()
testLogging {
events("passed")
}
} }
tasks.check { dependsOn(integrationTest) } val e2eTest = task<Test>("e2eTest") {
description = "Runs e2e tests."
group = "verification"
testClassesDirs = sourceSets["e2eTest"].output.classesDirs
classpath = sourceSets["e2eTest"].runtimeClasspath
shouldRunAfter("test")
useJUnitPlatform()
}
tasks.check {
dependsOn(integrationTest)
dependsOn(e2eTest)
}
tasks.withType<Test> { tasks.withType<Test> {
useJUnitPlatform() useJUnitPlatform()
val cpus = Runtime.getRuntime().availableProcessors()
maxParallelForks = max(1, cpus - 1)
setForkEvery(4)
doFirst { doFirst {
jvmArgs = arrayOf( jvmArgs = arrayOf(
"--add-opens", "java.base/java.lang=ALL-UNNAMED" "--add-opens", "java.base/java.lang=ALL-UNNAMED"
@ -180,6 +198,7 @@ dependencies {
implementation("org.postgresql:postgresql:42.6.0") implementation("org.postgresql:postgresql:42.6.0")
implementation("com.twelvemonkeys.imageio:imageio-webp:3.10.0") implementation("com.twelvemonkeys.imageio:imageio-webp:3.10.0")
implementation("org.apache.tika:tika-core:2.9.1") implementation("org.apache.tika:tika-core:2.9.1")
implementation("org.apache.tika:tika-parsers:2.9.1")
implementation("net.coobird:thumbnailator:0.4.20") implementation("net.coobird:thumbnailator:0.4.20")
implementation("org.bytedeco:javacv-platform:1.5.9") implementation("org.bytedeco:javacv-platform:1.5.9")
implementation("org.flywaydb:flyway-core") implementation("org.flywaydb:flyway-core")
@ -206,6 +225,18 @@ dependencies {
intTestImplementation("org.springframework.boot:spring-boot-starter-test") intTestImplementation("org.springframework.boot:spring-boot-starter-test")
intTestImplementation("org.springframework.security:spring-security-test") intTestImplementation("org.springframework.security:spring-security-test")
intTestImplementation("org.jetbrains.kotlin:kotlin-test-junit:$kotlin_version")
intTestImplementation("org.jetbrains.kotlinx:kotlinx-coroutines-test:1.6.4")
intTestImplementation("org.mockito.kotlin:mockito-kotlin:4.1.0")
e2eTestImplementation("org.springframework.boot:spring-boot-starter-test")
e2eTestImplementation("org.springframework.security:spring-security-test")
e2eTestImplementation("org.springframework.boot:spring-boot-starter-webflux")
e2eTestImplementation("org.jsoup:jsoup:1.17.1")
e2eTestImplementation("com.intuit.karate:karate-junit5:1.4.1")
} }

View File

@ -3,9 +3,7 @@ build:
weights: weights:
Indentation: 0 Indentation: 0
MagicNumber: 0 MagicNumber: 0
InjectDispatcher: 0
EnumEntryNameCase: 0 EnumEntryNameCase: 0
ReplaceSafeCallChainWithRun: 0
VariableNaming: 0 VariableNaming: 0
NoNameShadowing: 0 NoNameShadowing: 0
@ -78,7 +76,7 @@ complexity:
active: true active: true
ReplaceSafeCallChainWithRun: ReplaceSafeCallChainWithRun:
active: true active: false
StringLiteralDuplication: StringLiteralDuplication:
active: false active: false
@ -172,3 +170,5 @@ potential-bugs:
coroutines: coroutines:
RedundantSuspendModifier: RedundantSuspendModifier:
active: false active: false
InjectDispatcher:
active: false

View File

@ -1,5 +1,5 @@
ktor_version=2.3.0 ktor_version=2.3.0
kotlin_version=1.8.21 kotlin_version=1.9.21
logback_version=1.4.6 logback_version=1.4.6
kotlin.code.style=official kotlin.code.style=official
exposed_version=0.44.0 exposed_version=0.44.0

View File

@ -0,0 +1,28 @@
import dev.usbharu.hideout.core.infrastructure.exposedrepository.Users
import org.jetbrains.exposed.sql.and
import org.jetbrains.exposed.sql.select
import org.jetbrains.exposed.sql.selectAll
import java.net.MalformedURLException
import java.net.URL
object AssertionUtil {
@JvmStatic
fun assertUserExist(username: String, domain: String): Boolean {
val s = try {
val url = URL(domain)
url.host + ":" + url.port.toString().takeIf { it != "-1" }.orEmpty()
} catch (e: MalformedURLException) {
domain
}
val selectAll = Users.selectAll()
println(selectAll.fetchSize)
println(selectAll.toList().size)
selectAll.map { "@${it[Users.name]}@${it[Users.domain]}" }.forEach { println(it) }
return Users.select { Users.name eq username and (Users.domain eq s) }.empty().not()
}
}

View File

@ -0,0 +1,28 @@
import com.intuit.karate.junit5.Karate
object KarateUtil {
fun springBootKarateTest(path: String, scenario: String, clazz: Class<*>, port: String): Karate {
if (scenario.isEmpty()) {
return Karate.run(path).relativeTo(clazz).systemProperty("karate.port", port).karateEnv("dev")
} else {
return Karate.run(path).scenarioName(scenario).relativeTo(clazz).systemProperty("karate.port", port)
.karateEnv("dev")
}
}
fun e2eTest(path: String, scenario: String = "", properties: Map<String, String>, clazz: Class<*>): Karate {
val run = Karate.run(path)
val karate = if (scenario.isEmpty()) {
run
} else {
run.scenarioName(scenario)
}
var relativeTo = karate.relativeTo(clazz)
properties.map { relativeTo = relativeTo.systemProperty(it.key, it.value) }
return relativeTo.karateEnv("dev")
}
}

View File

@ -0,0 +1,85 @@
package federation
import AssertionUtil
import KarateUtil
import com.intuit.karate.core.MockServer
import com.intuit.karate.junit5.Karate
import dev.usbharu.hideout.SpringApplication
import kotlinx.coroutines.delay
import kotlinx.coroutines.runBlocking
import org.flywaydb.core.Flyway
import org.junit.jupiter.api.*
import org.springframework.beans.factory.annotation.Autowired
import org.springframework.boot.test.context.SpringBootTest
import org.springframework.boot.test.web.server.LocalServerPort
import org.springframework.transaction.annotation.Transactional
import java.net.MalformedURLException
import java.net.URL
@SpringBootTest(
classes = [SpringApplication::class],
webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT
)
@Transactional
class FollowAcceptTest {
@LocalServerPort
private var port = ""
@Karate.Test
@TestFactory
@Disabled
fun `FollowAcceptTest`(): Karate {
return KarateUtil.e2eTest(
"FollowAcceptTest", "Follow Accept Test",
mapOf("karate.port" to port),
javaClass
)
}
companion object {
lateinit var server: MockServer
lateinit var _remotePort: String
@JvmStatic
fun assertUserExist(username: String, domain: String) = runBlocking {
val s = try {
val url = URL(domain)
url.host + ":" + url.port.toString().takeIf { it != "-1" }.orEmpty()
} catch (e: MalformedURLException) {
domain
}
var check = false
repeat(10) {
delay(1000)
check = AssertionUtil.assertUserExist(username, s) or check
if (check) {
return@repeat
}
}
Assertions.assertTrue(check, "User @$username@$s not exist.")
}
@JvmStatic
fun getRemotePort(): String = _remotePort
@BeforeAll
@JvmStatic
fun beforeAll(@Autowired flyway: Flyway) {
server = MockServer.feature("classpath:federation/FollowAcceptMockServer.feature").http(0).build()
_remotePort = server.port.toString()
flyway.clean()
flyway.migrate()
}
@AfterAll
@JvmStatic
fun afterAll() {
server.stop()
}
}
}

View File

@ -0,0 +1,129 @@
package federation
import AssertionUtil
import KarateUtil
import com.intuit.karate.core.MockServer
import com.intuit.karate.junit5.Karate
import dev.usbharu.hideout.SpringApplication
import kotlinx.coroutines.delay
import kotlinx.coroutines.runBlocking
import org.flywaydb.core.Flyway
import org.junit.jupiter.api.AfterAll
import org.junit.jupiter.api.Assertions.assertTrue
import org.junit.jupiter.api.BeforeAll
import org.junit.jupiter.api.TestFactory
import org.springframework.beans.factory.annotation.Autowired
import org.springframework.boot.test.context.SpringBootTest
import org.springframework.boot.test.web.server.LocalServerPort
import org.springframework.transaction.annotation.Transactional
@SpringBootTest(
classes = [SpringApplication::class],
webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT
)
@Transactional
class InboxCommonTest {
@LocalServerPort
private var port = ""
@Karate.Test
@TestFactory
fun `inboxにHTTP Signature付きのリクエストがあったらリモートに取得しに行く`(): Karate {
return KarateUtil.e2eTest(
"InboxCommonTest",
"inboxにHTTP Signature付きのリクエストがあったらリモートに取得しに行く",
mapOf(
"karate.port" to port,
"karate.remotePort" to _remotePort
),
javaClass
)
}
@Karate.Test
@TestFactory
fun `user-inboxにHTTP Signature付きのリクエストがあったらリモートに取得しに行く`(): Karate {
return KarateUtil.e2eTest(
"InboxCommonTest",
"user-inboxにHTTP Signature付きのリクエストがあったらリモートに取得しに行く",
mapOf(
"karate.port" to port,
"karate.remotePort" to _remotePort
),
javaClass
)
}
@Karate.Test
@TestFactory
fun `inboxにHTTP Signatureがないリクエストがきたら401を返す`(): Karate {
return KarateUtil.e2eTest(
"InboxCommonTest",
"inboxにHTTP Signatureがないリクエストがきたら401を返す",
mapOf("karate.port" to port),
javaClass
)
}
@Karate.Test
@TestFactory
fun `user-inboxにHTTP Signatureがないリクエストがきたら401を返す`(): Karate {
return KarateUtil.e2eTest(
"InboxCommonTest",
"user-inboxにHTTP Signatureがないリクエストがきたら401を返す",
mapOf("karate.port" to port),
javaClass
)
}
@Karate.Test
@TestFactory
fun `inboxにConetnt-Type application *+json以外が来たら415を返す`(): Karate {
return KarateUtil.e2eTest(
"InboxCommonTest",
"inboxにContent-Type application/json以外が来たら415を返す",
mapOf("karate.port" to port),
javaClass
)
}
companion object {
lateinit var server: MockServer
lateinit var _remotePort: String
@JvmStatic
fun assertUserExist(username: String, domain: String) = runBlocking {
var check = false
repeat(10) {
delay(1000)
check = AssertionUtil.assertUserExist(username, domain) or check
if (check) {
return@repeat
}
}
assertTrue(check, "User @$username@$domain not exist.")
}
@JvmStatic
fun getRemotePort(): String = _remotePort
@BeforeAll
@JvmStatic
fun beforeAll(@Autowired flyway: Flyway) {
server = MockServer.feature("classpath:federation/InboxxCommonMockServerTest.feature").http(0).build()
_remotePort = server.port.toString()
flyway.clean()
flyway.migrate()
}
@AfterAll
@JvmStatic
fun afterAll() {
server.stop()
}
}
}

View File

@ -0,0 +1,54 @@
package oauth2
import KarateUtil
import com.intuit.karate.junit5.Karate
import dev.usbharu.hideout.SpringApplication
import org.flywaydb.core.Flyway
import org.junit.jupiter.api.AfterAll
import org.junit.jupiter.api.TestFactory
import org.springframework.beans.factory.annotation.Autowired
import org.springframework.boot.test.context.SpringBootTest
import org.springframework.boot.test.web.server.LocalServerPort
import org.springframework.test.context.jdbc.Sql
@SpringBootTest(
classes = [SpringApplication::class],
webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT,
)
@Sql("/oauth2/user.sql", executionPhase = Sql.ExecutionPhase.BEFORE_TEST_CLASS)
class OAuth2LoginTest {
@LocalServerPort
private var port = ""
@Karate.Test
@TestFactory
fun `スコープwrite readを持ったトークンの作成`(): Karate {
return KarateUtil.springBootKarateTest(
"Oauth2LoginTest",
"スコープwrite readを持ったトークンの作成",
javaClass,
port
)
}
@Karate.Test
@TestFactory
fun `スコープread_statuses write_statusesを持ったトークンの作成`(): Karate {
return KarateUtil.springBootKarateTest(
"Oauth2LoginTest",
"スコープread:statuses write:statusesを持ったトークンの作成",
javaClass,
port
)
}
companion object {
@JvmStatic
@AfterAll
fun dropDatabase(@Autowired flyway: Flyway) {
flyway.clean()
flyway.migrate()
}
}
}

View File

@ -0,0 +1,40 @@
hideout:
url: "https://localhost:8080"
use-mongodb: false
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"
storage:
type: local
spring:
flyway:
enabled: true
clean-disabled: false
datasource:
driver-class-name: org.h2.Driver
url: "jdbc:h2:./e2e-test;MODE=POSTGRESQL;DB_CLOSE_DELAY=-1;DB_CLOSE_ON_EXIT=FALSE;CASE_INSENSITIVE_IDENTIFIERS=true;TRACE_LEVEL_FILE=4"
username: ""
password:
data:
mongodb:
auto-index-creation: true
host: localhost
port: 27017
database: hideout
h2:
console:
enabled: true
# exposed:
# generate-ddl: true
# excluded-packages: dev.usbharu.hideout.core.infrastructure.kjobexposed
server:
port: 8080
tomcat:
basedir: tomcat-e2e
accesslog:
enabled: true

View File

@ -0,0 +1,140 @@
Feature: Follow Accept Mock Server
Background:
* def assertInbox = Java.type(`federation.FollowAcceptTest`)
* def req = {req: []}
Scenario: pathMatches('/users/test-follower') && methodIs('get')
* def remoteUrl = 'http://localhost:' + assertInbox.getRemotePort()
* def username = 'test-follower'
* def userUrl = remoteUrl + '/users/' + username
* def person =
"""
{
"@context": [
"https://www.w3.org/ns/activitystreams",
"https://w3id.org/security/v1",
{
"manuallyApprovesFollowers": "as:manuallyApprovesFollowers",
"toot": "http://joinmastodon.org/ns#",
"featured": {
"@id": "toot:featured",
"@type": "@id"
},
"featuredTags": {
"@id": "toot:featuredTags",
"@type": "@id"
},
"alsoKnownAs": {
"@id": "as:alsoKnownAs",
"@type": "@id"
},
"movedTo": {
"@id": "as:movedTo",
"@type": "@id"
},
"schema": "http://schema.org#",
"PropertyValue": "schema:PropertyValue",
"value": "schema:value",
"discoverable": "toot:discoverable",
"Device": "toot:Device",
"Ed25519Signature": "toot:Ed25519Signature",
"Ed25519Key": "toot:Ed25519Key",
"Curve25519Key": "toot:Curve25519Key",
"EncryptedMessage": "toot:EncryptedMessage",
"publicKeyBase64": "toot:publicKeyBase64",
"deviceId": "toot:deviceId",
"claim": {
"@type": "@id",
"@id": "toot:claim"
},
"fingerprintKey": {
"@type": "@id",
"@id": "toot:fingerprintKey"
},
"identityKey": {
"@type": "@id",
"@id": "toot:identityKey"
},
"devices": {
"@type": "@id",
"@id": "toot:devices"
},
"messageFranking": "toot:messageFranking",
"messageType": "toot:messageType",
"cipherText": "toot:cipherText",
"suspended": "toot:suspended",
"focalPoint": {
"@container": "@list",
"@id": "toot:focalPoint"
}
}
],
"id": #(userUrl),
"type": "Person",
"following": #(userUrl + '/following'),
"followers": #(userUrl + '/followers'),
"inbox": #(userUrl + '/inbox'),
"outbox": #(userUrl + '/outbox'),
"featured": #(userUrl + '/collections/featured'),
"featuredTags": #(userUrl + '/collections/tags'),
"preferredUsername": #(username),
"name": #(username),
"summary": "E2E Test User Jaga/Cotlin/Winter Boot/Ktol\nYonTude: https://example.com\nY(Tvvitter): https://example.com\n",
"url": #(userUrl + '/@' + username),
"manuallyApprovesFollowers": false,
"discoverable": true,
"published": "2016-03-16T00:00:00Z",
"devices": #(userUrl + '/collections/devices'),
"alsoKnownAs": [
#( 'https://example.com/users/' + username)
],
"publicKey": {
"id": #(userUrl + '#main-key'),
"owner": #(userUrl),
"publicKeyPem": "-----BEGIN PUBLIC KEY-----\nMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAvmtKo0xYGXR4M0LQQhK4\nBkKpRvvUxrqGV6Ew4CBHSyzdnbFsiBqHUWz4JvRiQvAPqQ4jQFpxVZCPr9xx6lJp\nx7EAAKIdVTnBBV4CYfu7QPsRqtjbB5408q+mo5oUXNs8xg2tcC42p2SJ5CRJX/fr\nOgCZwo3LC9pOBdCQZ+tiiPmWNBTNby99JZn4D/xNcwuhV04qcPoHYD9OPuxxGyzc\naVJ2mqJmvi/lewQuR8qnUIbz+Gik+xvyG6LmyuDoa1H2LDQfQXYb62G70HsYdu7a\ndObvZovytp+kkjP/cUaIYkhhOAYqAA4zCwVRY4NHK0MAMq9sMoUfNJa8U+zR9NvD\noQIDAQAB\n-----END PUBLIC KEY-----\n"
},
"tag": [],
"attachment": [
{
"type": "PropertyValue",
"name": "Pixib Fan-Bridge",
"value": "\u003ca href=\"https://example.com/hideout\" target=\"_blank\" rel=\"nofollow noopener noreferrer me\"\u003e\u003cspan class=\"invisible\"\u003ehttps://www.\u003c/span\u003e\u003cspan class=\"\"\u003eexample.com/hideout\u003c/span\u003e\u003cspan class=\"invisible\"\u003e\u003c/span\u003e\u003c/a\u003e"
},
{
"type": "PropertyValue",
"name": "GitHub",
"value": "\u003ca href=\"https://github.com/usbharu/hideout\" target=\"_blank\" rel=\"nofollow noopener noreferrer me\"\u003e\u003cspan class=\"invisible\"\u003ehttps://\u003c/span\u003e\u003cspan class=\"\"\u003egithub.com/usbharu/hideout\u003c/span\u003e\u003cspan class=\"invisible\"\u003e\u003c/span\u003e\u003c/a\u003e"
}
],
"endpoints": {
"sharedInbox": #(remoteUrl + '/inbox')
},
"icon": {
"type": "Image",
"mediaType": "image/jpeg",
"url": "https://upload.wikimedia.org/wikipedia/commons/thumb/3/30/Destroyer_Vozbuzhdenyy.jpg/320px-Destroyer_Vozbuzhdenyy.jpg"
},
"image": {
"type": "Image",
"mediaType": "image/jpeg",
"url": "https://upload.wikimedia.org/wikipedia/commons/thumb/6/63/Views_of_Mount_Fuji_from_%C5%8Cwakudani_20211202.jpg/320px-Views_of_Mount_Fuji_from_%C5%8Cwakudani_20211202.jpg"
}
}
"""
* set req.req[] = '/users/' + username
* def response = person
Scenario: pathMatches('/inbox') && methodIs('post')
* set req.req[] = '/inbox'
* def responseStatus = 202
Scenario: pathMatches('/internal-assertion-api/requests') && methodIs('get')
* def response = req
Scenario: pathMatches('/internal-assertion-api/requests/deleteAll') && methodIs('post')
* set req.req = []
* def responseStatus = 200

View File

@ -0,0 +1,29 @@
Feature: Follow Accept Test
Background:
* url baseUrl
* def assertionUtil = Java.type('AssertionUtil')
Scenario: Follow Accept Test
* def follow =
"""
{"type": "Follow","actor": #(remoteUrl + '/users/test-follower'),"object": #(baseUrl + '/users/test-user')}
"""
Given path '/inbox'
And header Signature = 'keyId="https://test-hideout.usbharu.dev/users/c#pubkey", algorithm="rsa-sha256", headers="x-request-id tpp-redirect-uri digest psu-id", signature="e/91pFiI5bRffP33EMrqoI5A0xjkg3Ar0kzRGHC/1RsLrDW0zV50dHly/qJJ5xrYHRlss3+vd0mznTLBs1X0hx0uXjpfvCvwclpSi8u+sqn+Y2bcQKzf7ah0vAONQd6zeTYW7e/1kDJreP43PsJyz29KZD16Yop8nM++YeEs6C5eWiyYXKoQozXnfmTOX3y6bhxfKKQWVcxA5aLOTmTZRYTsBsTy9zn8NjDQaRI/0gcyYPqpq+2g8j2DbyJu3Z6zP6VmwbGGlQU/s9Pa7G5LqUPH/sBMSlIeqh+Hvm2pL7B3/BMFvGtTD+e2mR60BFnLIxMYx+oX4o33J2XkFIODLQ=="'
And request follow
When method post
Then status 202
And retry until assertionUtil.assertUserExist('test-follower',remoteUrl)
* url remoteUrl
Given path '/internal-assertion-api/requests'
When method get
Then status 200
And match response.req contains ['/users/test-follower']
* url baseUrl

View File

@ -0,0 +1,158 @@
Feature: Inbox Common Test
Background:
* url baseUrl
Scenario: inboxにHTTP Signature付きのリクエストがあったらリモートに取得しに行く
* url remoteUrl
Given path '/internal-assertion-api/requests/deleteAll'
When method post
Then status 200
* url baseUrl
* def inbox =
"""
{ "type": "Follow" }
"""
Given path `/inbox`
And request inbox
# And header Signature = 'keyId="'+ remoteUrl +'/users/test-user#pubkey", algorithm="rsa-sha256", headers="(request-target)", signature="a"'
And header Signature = 'keyId="'+ remoteUrl +'/users/test-user#pubkey", algorithm="rsa-sha256", headers="(request-target) date host digest", signature="FfpkmBogW70FMo94yovGpl15L/m4bDjVIFb9mSZUstPE3H00nHiqNsjAq671qFMJsGOO1uWfLEExcdvzwTiC3wuHShzingvxQUbTgcgRTRZcHbtrOZxT8hYHGndpCXGv/NOLkfXDtZO9v5u0fnA2yJFokzyPHOPJ1cJliWlXP38Bl/pO4H5rBLQBZKpM2jYIjMyI78G2rDXNHEeGrGiyfB5SKb3H6zFQL+X9QpXUI4n0f07VsnwaDyp63oUopmzNUyBEuSqB+8va/lbfcWwrxpZnKGzQRZ+VBcV7jDoKGNOP9/O1xEI2CwB8sh+h6KVHdX3EQEvO1slaaLzcwRRqrQ=="'
When method post
Then status 202
* def assertInbox = Java.type(`federation.InboxCommonTest`)
And assertInbox.assertUserExist('test-user',remoteUrl)
* url remoteUrl
Given path '/internal-assertion-api/requests'
When method get
Then status 200
* url baseUrl
* print response
Then match response.req == ['/users/test-user']
Scenario: inboxにHTTP Signatureがないリクエストがきたら401を返す
* def inbox =
"""
{"type": "Follow"}
"""
Given path '/inbox'
And request inbox
When method post
Then status 401
Scenario: user-inboxにHTTP Signature付きのリクエストがあったらリモートに取得しに行く
* url remoteUrl
Given path '/internal-assertion-api/requests/deleteAll'
When method post
Then status 200
* url baseUrl
* def inbox =
"""
{ "type": "Follow" }
"""
Given path `/inbox`
And request inbox
# And header Signature = 'keyId="'+ remoteUrl +'/users/test-user#pubkey", algorithm="rsa-sha256", headers="(request-target)", signature="a"'
And header Signature = 'keyId="'+ remoteUrl +'/users/test-user2#pubkey", algorithm="rsa-sha256", headers="(request-target) date host digest", signature="FfpkmBogW70FMo94yovGpl15L/m4bDjVIFb9mSZUstPE3H00nHiqNsjAq671qFMJsGOO1uWfLEExcdvzwTiC3wuHShzingvxQUbTgcgRTRZcHbtrOZxT8hYHGndpCXGv/NOLkfXDtZO9v5u0fnA2yJFokzyPHOPJ1cJliWlXP38Bl/pO4H5rBLQBZKpM2jYIjMyI78G2rDXNHEeGrGiyfB5SKb3H6zFQL+X9QpXUI4n0f07VsnwaDyp63oUopmzNUyBEuSqB+8va/lbfcWwrxpZnKGzQRZ+VBcV7jDoKGNOP9/O1xEI2CwB8sh+h6KVHdX3EQEvO1slaaLzcwRRqrQ=="'
When method post
Then status 202
* def assertInbox = Java.type(`federation.InboxCommonTest`)
And assertInbox.assertUserExist('test-user2',remoteUrl)
* url remoteUrl
Given path '/internal-assertion-api/requests'
When method get
Then status 200
* url baseUrl
* print response
Then match response.req == ['/users/test-user2']
Scenario: user-inboxにHTTP Signatureがないリクエストがきたら401を返す
* def inbox =
"""
{"type": "Follow"}
"""
Given path '/inbox'
And request inbox
When method post
Then status 401
Scenario: inboxにContent-Type application/json以外が来たら415を返す
* def inbox =
"""
{"type": "Follow"}
"""
Given path '/inbox'
And request inbox
And header Signature = 'keyId="'+ remoteUrl +'/users/test-user#pubkey", algorithm="rsa-sha256", headers="(request-target) date host digest", signature="FfpkmBogW70FMo94yovGpl15L/m4bDjVIFb9mSZUstPE3H00nHiqNsjAq671qFMJsGOO1uWfLEExcdvzwTiC3wuHShzingvxQUbTgcgRTRZcHbtrOZxT8hYHGndpCXGv/NOLkfXDtZO9v5u0fnA2yJFokzyPHOPJ1cJliWlXP38Bl/pO4H5rBLQBZKpM2jYIjMyI78G2rDXNHEeGrGiyfB5SKb3H6zFQL+X9QpXUI4n0f07VsnwaDyp63oUopmzNUyBEuSqB+8va/lbfcWwrxpZnKGzQRZ+VBcV7jDoKGNOP9/O1xEI2CwB8sh+h6KVHdX3EQEvO1slaaLzcwRRqrQ=="'
And header Accept = 'application/activity+json'
And header Content-Type = 'application/json'
When method post
Then status 202
Given path '/inbox'
And request inbox
And header Signature = 'keyId="'+ remoteUrl +'/users/test-user#pubkey", algorithm="rsa-sha256", headers="(request-target) date host digest", signature="FfpkmBogW70FMo94yovGpl15L/m4bDjVIFb9mSZUstPE3H00nHiqNsjAq671qFMJsGOO1uWfLEExcdvzwTiC3wuHShzingvxQUbTgcgRTRZcHbtrOZxT8hYHGndpCXGv/NOLkfXDtZO9v5u0fnA2yJFokzyPHOPJ1cJliWlXP38Bl/pO4H5rBLQBZKpM2jYIjMyI78G2rDXNHEeGrGiyfB5SKb3H6zFQL+X9QpXUI4n0f07VsnwaDyp63oUopmzNUyBEuSqB+8va/lbfcWwrxpZnKGzQRZ+VBcV7jDoKGNOP9/O1xEI2CwB8sh+h6KVHdX3EQEvO1slaaLzcwRRqrQ=="'
And header Accept = 'application/activity+json'
And header Content-Type = 'application/activity+json'
When method post
Then status 202
Given path '/inbox'
And request inbox
And header Signature = 'keyId="'+ remoteUrl +'/users/test-user#pubkey", algorithm="rsa-sha256", headers="(request-target) date host digest", signature="FfpkmBogW70FMo94yovGpl15L/m4bDjVIFb9mSZUstPE3H00nHiqNsjAq671qFMJsGOO1uWfLEExcdvzwTiC3wuHShzingvxQUbTgcgRTRZcHbtrOZxT8hYHGndpCXGv/NOLkfXDtZO9v5u0fnA2yJFokzyPHOPJ1cJliWlXP38Bl/pO4H5rBLQBZKpM2jYIjMyI78G2rDXNHEeGrGiyfB5SKb3H6zFQL+X9QpXUI4n0f07VsnwaDyp63oUopmzNUyBEuSqB+8va/lbfcWwrxpZnKGzQRZ+VBcV7jDoKGNOP9/O1xEI2CwB8sh+h6KVHdX3EQEvO1slaaLzcwRRqrQ=="'
And header Accept = 'application/activity+json'
And header Content-Type = 'application/ld+json; profile="https://www.w3.org/ns/activitystreams"'
When method post
Then status 202
Given path '/inbox'
And header Signature = 'keyId="'+ remoteUrl +'/users/test-user#pubkey", algorithm="rsa-sha256", headers="(request-target) date host digest", signature="FfpkmBogW70FMo94yovGpl15L/m4bDjVIFb9mSZUstPE3H00nHiqNsjAq671qFMJsGOO1uWfLEExcdvzwTiC3wuHShzingvxQUbTgcgRTRZcHbtrOZxT8hYHGndpCXGv/NOLkfXDtZO9v5u0fnA2yJFokzyPHOPJ1cJliWlXP38Bl/pO4H5rBLQBZKpM2jYIjMyI78G2rDXNHEeGrGiyfB5SKb3H6zFQL+X9QpXUI4n0f07VsnwaDyp63oUopmzNUyBEuSqB+8va/lbfcWwrxpZnKGzQRZ+VBcV7jDoKGNOP9/O1xEI2CwB8sh+h6KVHdX3EQEvO1slaaLzcwRRqrQ=="'
And header Accept = 'application/activity+json'
When method post
Then status 415
* def html =
"""
<html>
</html>
"""
Given path '/inbox'
And header Signature = 'keyId="'+ remoteUrl +'/users/test-user#pubkey", algorithm="rsa-sha256", headers="(request-target) date host digest", signature="FfpkmBogW70FMo94yovGpl15L/m4bDjVIFb9mSZUstPE3H00nHiqNsjAq671qFMJsGOO1uWfLEExcdvzwTiC3wuHShzingvxQUbTgcgRTRZcHbtrOZxT8hYHGndpCXGv/NOLkfXDtZO9v5u0fnA2yJFokzyPHOPJ1cJliWlXP38Bl/pO4H5rBLQBZKpM2jYIjMyI78G2rDXNHEeGrGiyfB5SKb3H6zFQL+X9QpXUI4n0f07VsnwaDyp63oUopmzNUyBEuSqB+8va/lbfcWwrxpZnKGzQRZ+VBcV7jDoKGNOP9/O1xEI2CwB8sh+h6KVHdX3EQEvO1slaaLzcwRRqrQ=="'
And header Accept = 'application/activity+json'
And header Content-Type = 'text/html'
And request html
When method post
Then status 415

View File

@ -0,0 +1,136 @@
Feature: InboxCommonMockServer
Background:
* def assertInbox = Java.type(`federation.InboxCommonTest`)
* def req = {req: []}
Scenario: pathMatches('/users/{username}') && methodIs('get')
* def remoteUrl = 'http://localhost:' + assertInbox.getRemotePort()
* def username = pathParams.username
* def userUrl = remoteUrl + '/users/' + username
* def person =
"""
{
"@context": [
"https://www.w3.org/ns/activitystreams",
"https://w3id.org/security/v1",
{
"manuallyApprovesFollowers": "as:manuallyApprovesFollowers",
"toot": "http://joinmastodon.org/ns#",
"featured": {
"@id": "toot:featured",
"@type": "@id"
},
"featuredTags": {
"@id": "toot:featuredTags",
"@type": "@id"
},
"alsoKnownAs": {
"@id": "as:alsoKnownAs",
"@type": "@id"
},
"movedTo": {
"@id": "as:movedTo",
"@type": "@id"
},
"schema": "http://schema.org#",
"PropertyValue": "schema:PropertyValue",
"value": "schema:value",
"discoverable": "toot:discoverable",
"Device": "toot:Device",
"Ed25519Signature": "toot:Ed25519Signature",
"Ed25519Key": "toot:Ed25519Key",
"Curve25519Key": "toot:Curve25519Key",
"EncryptedMessage": "toot:EncryptedMessage",
"publicKeyBase64": "toot:publicKeyBase64",
"deviceId": "toot:deviceId",
"claim": {
"@type": "@id",
"@id": "toot:claim"
},
"fingerprintKey": {
"@type": "@id",
"@id": "toot:fingerprintKey"
},
"identityKey": {
"@type": "@id",
"@id": "toot:identityKey"
},
"devices": {
"@type": "@id",
"@id": "toot:devices"
},
"messageFranking": "toot:messageFranking",
"messageType": "toot:messageType",
"cipherText": "toot:cipherText",
"suspended": "toot:suspended",
"focalPoint": {
"@container": "@list",
"@id": "toot:focalPoint"
}
}
],
"id": #(userUrl),
"type": "Person",
"following": #(userUrl + '/following'),
"followers": #(userUrl + '/followers'),
"inbox": #(userUrl + '/inbox'),
"outbox": #(userUrl + '/outbox'),
"featured": #(userUrl + '/collections/featured'),
"featuredTags": #(userUrl + '/collections/tags'),
"preferredUsername": #(username),
"name": #(username),
"summary": "E2E Test User Jaga/Cotlin/Winter Boot/Ktol\nYonTude: https://example.com\nY(Tvvitter): https://example.com\n",
"url": #(userUrl + '/@' + username),
"manuallyApprovesFollowers": false,
"discoverable": true,
"published": "2016-03-16T00:00:00Z",
"devices": #(userUrl + '/collections/devices'),
"alsoKnownAs": [
#( 'https://example.com/users/' + username)
],
"publicKey": {
"id": #(userUrl + '#main-key'),
"owner": #(userUrl),
"publicKeyPem": "-----BEGIN PUBLIC KEY-----\nMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAvmtKo0xYGXR4M0LQQhK4\nBkKpRvvUxrqGV6Ew4CBHSyzdnbFsiBqHUWz4JvRiQvAPqQ4jQFpxVZCPr9xx6lJp\nx7EAAKIdVTnBBV4CYfu7QPsRqtjbB5408q+mo5oUXNs8xg2tcC42p2SJ5CRJX/fr\nOgCZwo3LC9pOBdCQZ+tiiPmWNBTNby99JZn4D/xNcwuhV04qcPoHYD9OPuxxGyzc\naVJ2mqJmvi/lewQuR8qnUIbz+Gik+xvyG6LmyuDoa1H2LDQfQXYb62G70HsYdu7a\ndObvZovytp+kkjP/cUaIYkhhOAYqAA4zCwVRY4NHK0MAMq9sMoUfNJa8U+zR9NvD\noQIDAQAB\n-----END PUBLIC KEY-----\n"
},
"tag": [],
"attachment": [
{
"type": "PropertyValue",
"name": "Pixib Fan-Bridge",
"value": "\u003ca href=\"https://example.com/hideout\" target=\"_blank\" rel=\"nofollow noopener noreferrer me\"\u003e\u003cspan class=\"invisible\"\u003ehttps://www.\u003c/span\u003e\u003cspan class=\"\"\u003eexample.com/hideout\u003c/span\u003e\u003cspan class=\"invisible\"\u003e\u003c/span\u003e\u003c/a\u003e"
},
{
"type": "PropertyValue",
"name": "GitHub",
"value": "\u003ca href=\"https://github.com/usbharu/hideout\" target=\"_blank\" rel=\"nofollow noopener noreferrer me\"\u003e\u003cspan class=\"invisible\"\u003ehttps://\u003c/span\u003e\u003cspan class=\"\"\u003egithub.com/usbharu/hideout\u003c/span\u003e\u003cspan class=\"invisible\"\u003e\u003c/span\u003e\u003c/a\u003e"
}
],
"endpoints": {
"sharedInbox": #(remoteUrl + '/inbox')
},
"icon": {
"type": "Image",
"mediaType": "image/jpeg",
"url": "https://upload.wikimedia.org/wikipedia/commons/thumb/3/30/Destroyer_Vozbuzhdenyy.jpg/320px-Destroyer_Vozbuzhdenyy.jpg"
},
"image": {
"type": "Image",
"mediaType": "image/jpeg",
"url": "https://upload.wikimedia.org/wikipedia/commons/thumb/6/63/Views_of_Mount_Fuji_from_%C5%8Cwakudani_20211202.jpg/320px-Views_of_Mount_Fuji_from_%C5%8Cwakudani_20211202.jpg"
}
}
"""
* set req.req[] = '/users/' + username
* def response = person
Scenario: pathMatches('/internal-assertion-api/requests') && methodIs('get')
* def response = req
Scenario: pathMatches('/internal-assertion-api/requests/deleteAll') && methodIs('post')
* set req.req = []
* def responseStatus = 200

View File

@ -0,0 +1,30 @@
function fn() {
var env = karate.env; // get java system property 'karate.env'
karate.log('karate.env system property was:', env);
if (!env) {
env = 'dev'; // a custom 'intelligent' default
karate.log('karate.env set to "dev" as default.');
}
let config;
if (env === 'test') {
let remotePort = karate.properties['karate.remotePort'] || '8081'
config = {
baseUrl: 'https://test-hideout.usbharu.dev',
remoteUrl: 'http://localhost:' + remotePort
}
} else if (env === 'dev') {
let port = karate.properties['karate.port'] || '8080'
let remotePort = karate.properties['karate.remotePort'] || '8081'
config = {
baseUrl: 'http://localhost:' + port,
remoteUrl: 'http://localhost:' + remotePort
}
} else {
throw 'Unknown environment [' + env + '].'
}
// don't waste time waiting for a connection or if servers don't respond within 0,3 seconds
karate.configure('connectTimeout', 1000);
karate.configure('readTimeout', 1000);
return config;
}

View File

@ -0,0 +1,21 @@
<configuration>
<appender name="FILE" class="ch.qos.logback.core.FileAppender">
<file>./e2eTest.log</file>
<encoder>
<charset>UTF-8</charset>
<pattern>%d{YYYY-MM-dd HH:mm:ss.SSS} [%thread] %-5level [%X{x-request-id}] %logger{36} - %msg%n</pattern>
</encoder>
</appender>
<appender name="STDOUT" class="ch.qos.logback.core.ConsoleAppender">
<encoder>
<pattern>%d{YYYY-MM-dd HH:mm:ss.SSS} [%thread] %-5level [%X{x-request-id}] %logger{36} - %msg%n</pattern>
</encoder>
</appender>
<root level="TRACE">
<appender-ref ref="STDOUT"/>
<appender-ref ref="FILE"/>
</root>
<logger name="org.springframework.security" level="TRACE"/>
<logger name="com.intuit.karate.driver" level="INFO"/>
<logger name="org.thymeleaf.TemplateEngine.CONFIG" level="INFO"/>
</configuration>

View File

@ -0,0 +1,95 @@
Feature: OAuth2 Login Test
Background:
* url baseUrl
* configure driver = { type: 'chrome',start: true, headless: true, showDriverLog: true, addOptions: [ '--headless=new' ] }
Scenario: スコープwrite readを持ったトークンの作成
* def apps =
"""
{
"client_name": "oauth2-test-client-1",
"redirect_uris": "https://usbharu.dev",
"scopes": "write read"
}
"""
Given path '/api/v1/apps'
And request apps
When method post
Then status 200
* def client_id = response.client_id
* def client_secret = response.client_secret
* def authorizeEndpoint = baseUrl + '/oauth/authorize?response_type=code&redirect_uri=https://usbharu.dev&client_id=' + client_id + '&scope=write%20read'
Given driver authorizeEndpoint
And driver.input('#username','test-user')
And driver.input('#password','password')
When driver.submit().click('body > div > form > button')
Then driver.waitForUrl(authorizeEndpoint + "&continue")
And driver.click('#read')
And driver.click('#write')
When driver.submit().click('#submit-consent')
Then driver.waitUntil("location.host == 'usbharu.dev'")
* def code = script("new URLSearchParams(document.location.search).get('code')")
Given path '/oauth/token'
And form field client_id = client_id
And form field client_secret = client_secret
And form field redirect_uri = 'https://usbharu.dev'
And form field grant_type = 'authorization_code'
And form field code = code
And form field scope = 'write read'
When method post
Then status 200
Scenario: スコープread:statuses write:statusesを持ったトークンの作成
* def apps =
"""
{
"client_name": "oauth2-test-client-2",
"redirect_uris": "https://usbharu.dev",
"scopes": "read:statuses write:statuses"
}
"""
Given path '/api/v1/apps'
And request apps
When method post
Then status 200
* def client_id = response.client_id
* def client_secret = response.client_secret
* def authorizeEndpoint = baseUrl + '/oauth/authorize?response_type=code&redirect_uri=https://usbharu.dev&client_id=' + client_id + '&scope=read:statuses+write:statuses'
Given driver authorizeEndpoint
And driver.input('#username','test-user')
And driver.input('#password','password')
When driver.submit().click('body > div > form > button')
Then driver.waitForUrl(authorizeEndpoint + "&continue")
And driver.click('/html/body/div/div[4]/div/form/div[1]/input')
And driver.click('/html/body/div/div[4]/div/form/div[2]/input')
When driver.submit().click('#submit-consent')
Then driver.waitUntil("location.host == 'usbharu.dev'")
* def code = script("new URLSearchParams(document.location.search).get('code')")
Given path '/oauth/token'
And form field client_id = client_id
And form field client_secret = client_secret
And form field redirect_uri = 'https://usbharu.dev'
And form field grant_type = 'authorization_code'
And form field code = code
And form field scope = 'write read'
When method post
Then status 200

View File

@ -0,0 +1,46 @@
insert into "USERS" (ID, NAME, DOMAIN, SCREEN_NAME, DESCRIPTION, PASSWORD, INBOX, OUTBOX, URL, PUBLIC_KEY, PRIVATE_KEY,
CREATED_AT, KEY_ID, FOLLOWING, FOLLOWERS, INSTANCE)
VALUES (1730415786666758144, 'test-user', 'localhost', 'Im test user.', 'THis account is test user.',
'$2a$10$/mWC/n7nC7X3l9qCEOKnredxne2zewoqEsJWTOdlKfg2zXKJ0F9Em', 'http://localhost/users/test-user/inbox',
'http://localhost/users/test-user/outbox', 'http://localhost/users/test-user',
'-----BEGIN PUBLIC KEY-----
MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAi4mifRg6huAIn6DXk3Vn
5tkRC0AO32ZJvczwXr9xDj4HJvrSUHBAxIwwIeuCceAYtiuZk4JmEKydeB6SRkoO
Nty93XZXS1SMmiHCvWOY5YlpnfFU1kLqW3fkXcLNls4XmzujLt1i2sT8mYkENAsP
h6K4SRtmktOVYZOWcVEcfLGKbJvaDD/+lKikNC1XCouylfGV/bA/FPY5vuI+7cdM
Mjana28JdiWlPWSdzcxtCSgN+nGWPjk2WWm8K+wK2zXqMxA0U0p4odyyILBGALxX
zMqObIQvpwPh/t+b6ohem4eq70/0/SwDhd+IzHkT3x4UzG1oxSQS/juPkO7uuS8p
uwIDAQAB
-----END PUBLIC KEY-----
',
'-----BEGIN PRIVATE KEY-----
MIIEvQIBADANBgkqhkiG9w0BAQEFAASCBKcwggSjAgEAAoIBAQCLiaJ9GDqG4Aif
oNeTdWfm2RELQA7fZkm9zPBev3EOPgcm+tJQcEDEjDAh64Jx4Bi2K5mTgmYQrJ14
HpJGSg423L3ddldLVIyaIcK9Y5jliWmd8VTWQupbd+Rdws2WzhebO6Mu3WLaxPyZ
iQQ0Cw+HorhJG2aS05Vhk5ZxURx8sYpsm9oMP/6UqKQ0LVcKi7KV8ZX9sD8U9jm+
4j7tx0wyNqdrbwl2JaU9ZJ3NzG0JKA36cZY+OTZZabwr7ArbNeozEDRTSnih3LIg
sEYAvFfMyo5shC+nA+H+35vqiF6bh6rvT/T9LAOF34jMeRPfHhTMbWjFJBL+O4+Q
7u65Lym7AgMBAAECggEADJLa7v3LbFLsxAGY22NFeRJPTF252VycwXshn9ANbnSd
bWBFqlTrKSrevXe82ekRIP09ygKCkvcS+3t5v9a1gDEU9MtQo2ubfdoT87/xS6G9
wCs6c1I1Twe3LtG6d9/bVbQiiLsPSNpeTrF/jPcAL780bvYGoK1rNQ85C7383Kl6
1nwZCD0itjkmzbO0nGMRCduW46OdQKiOMuEC7z0zwynH3cK3wGvdlKyLG4L3pPZm
1/Uz7AZTieqSCjSgcgmaut7dmS49e3j8ujfb3wcKscfHoofyqNWsW1xyU1WytO9a
QLh9wlqfvGlfwQWkY6z6uFmc4XfRVZSC8nic4cAW3QKBgQC4PYbR5AuylDcfc6Am
jpL5mcF6qEMnEPgnL9z5VvuLs1f/JEyx5VgzQreDOKc1KOxDX7Xhok4gpvIJv1fi
zimviszEmIpHdPvgS7mP2hu42bSIjwVaXpny5aEEZbB6HQ9pGDW/MSsgmb6x31Kx
o+sslpqf9cpalI35UPtkNaEJNwKBgQDB4tVUQ5gGPKllEfCN64B/B7wodWr5cUNU
UpUXdFPCu+HXnRen6GKLo+25wmCUGtcIuvCY1Xm+tL0Z7jrI+oOD4CL9ob7BJrPF
XCq0jUhaEzWFGp1SOa6n+35fWPkCfG4EwfsK8+PWoZsZc1eykMxIJmBln3vufuHz
qybfhy0VnQKBgD2tAxvyXmQar9VMjLk7k0IRUa6w80H5sUjVAgFKOA0NLZEQ4sfO
wdbvJ6W66mamW2k2ehmdjs/pcy8GKfKYF2ZXbbMGaYwAQm1UjDr2xb78yi3Iyv70
mk6wxlVFgW1vmwAQhbWKTSitryO2YeVrvUeA5yRTULk/78Mdc/qY5V7DAoGAAu3I
RzOWMlHsRSiWN66dDE4zm3DaotYBLF7q/aW2NjTcXoNy/ghWpMFfL/UtvE8DfJBG
XiirZCQazy94F90g63cRUD+HQCezg4G2629O7n1ny5DxW3Kfns3/xLT1XgI/Lzc2
8Z1pja53R1Ukt//T9isOPbrBBoNIKoQlXC8QkUkCgYEAsib3uOMAIOJab5jc8FSj
VG+Cg2H63J5DgUUwx2Y0DPENugdGyYzCDMVPBNaB0Ru1SpqbUjgqh+YHynunSVeu
hDXMOteeyeVHUGw8mvcCEt53uRYVNW/rzXTMqfLVxbsJZHCsJBtFpwcgD2w4NjS2
Ja15+ZWbOA4vJA9pOh3x4XM=
-----END PRIVATE KEY-----
', 1701398248417,
'http://localhost/users/test-user#pubkey', 'http://localhost/users/test-user/following',
'http://localhost/users/test-users/followers', null);

View File

@ -1,6 +1,8 @@
package activitypub.inbox package activitypub.inbox
import dev.usbharu.hideout.SpringApplication import dev.usbharu.hideout.SpringApplication
import org.flywaydb.core.Flyway
import org.junit.jupiter.api.AfterAll
import org.junit.jupiter.api.BeforeEach import org.junit.jupiter.api.BeforeEach
import org.junit.jupiter.api.Test import org.junit.jupiter.api.Test
import org.springframework.beans.factory.annotation.Autowired import org.springframework.beans.factory.annotation.Autowired
@ -45,6 +47,7 @@ class InboxTest {
content = "{}" content = "{}"
contentType = MediaType.APPLICATION_JSON contentType = MediaType.APPLICATION_JSON
} }
.asyncDispatch()
.andExpect { status { isUnauthorized() } } .andExpect { status { isUnauthorized() } }
} }
@ -55,6 +58,7 @@ class InboxTest {
.post("/inbox") { .post("/inbox") {
content = "{}" content = "{}"
contentType = MediaType.APPLICATION_JSON contentType = MediaType.APPLICATION_JSON
header("Signature", "")
} }
.asyncDispatch() .asyncDispatch()
.andExpect { status { isAccepted() } } .andExpect { status { isAccepted() } }
@ -68,6 +72,7 @@ class InboxTest {
content = "{}" content = "{}"
contentType = MediaType.APPLICATION_JSON contentType = MediaType.APPLICATION_JSON
} }
.asyncDispatch()
.andExpect { status { isUnauthorized() } } .andExpect { status { isUnauthorized() } }
} }
@ -78,6 +83,7 @@ class InboxTest {
.post("/users/hoge/inbox") { .post("/users/hoge/inbox") {
content = "{}" content = "{}"
contentType = MediaType.APPLICATION_JSON contentType = MediaType.APPLICATION_JSON
header("Signature", "")
} }
.asyncDispatch() .asyncDispatch()
.andExpect { status { isAccepted() } } .andExpect { status { isAccepted() } }
@ -88,4 +94,13 @@ class InboxTest {
@Bean @Bean
fun testTransaction() = TestTransaction fun testTransaction() = TestTransaction
} }
companion object {
@JvmStatic
@AfterAll
fun dropDatabase(@Autowired flyway: Flyway) {
flyway.clean()
flyway.migrate()
}
}
} }

View File

@ -1,6 +1,8 @@
package activitypub.note package activitypub.note
import dev.usbharu.hideout.SpringApplication import dev.usbharu.hideout.SpringApplication
import org.flywaydb.core.Flyway
import org.junit.jupiter.api.AfterAll
import org.junit.jupiter.api.BeforeEach import org.junit.jupiter.api.BeforeEach
import org.junit.jupiter.api.Test import org.junit.jupiter.api.Test
import org.springframework.beans.factory.annotation.Autowired import org.springframework.beans.factory.annotation.Autowired
@ -178,4 +180,13 @@ class NoteTest {
.andExpect { jsonPath("\$.attachment[1].type") { value("Document") } } .andExpect { jsonPath("\$.attachment[1].type") { value("Document") } }
.andExpect { jsonPath("\$.attachment[1].url") { value("https://example.com/media/test-media2.png") } } .andExpect { jsonPath("\$.attachment[1].url") { value("https://example.com/media/test-media2.png") } }
} }
companion object {
@JvmStatic
@AfterAll
fun dropDatabase(@Autowired flyway: Flyway) {
flyway.clean()
flyway.migrate()
}
}
} }

View File

@ -2,6 +2,8 @@ package activitypub.webfinger
import dev.usbharu.hideout.SpringApplication import dev.usbharu.hideout.SpringApplication
import dev.usbharu.hideout.application.external.Transaction import dev.usbharu.hideout.application.external.Transaction
import org.flywaydb.core.Flyway
import org.junit.jupiter.api.AfterAll
import org.junit.jupiter.api.Test import org.junit.jupiter.api.Test
import org.springframework.beans.factory.annotation.Autowired import org.springframework.beans.factory.annotation.Autowired
import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc
@ -24,6 +26,7 @@ class WebFingerTest {
@Test @Test
@Sql("/sql/test-user.sql") @Sql("/sql/test-user.sql")
fun `webfinger 存在するユーザーを取得`() { fun `webfinger 存在するユーザーを取得`() {
mockMvc mockMvc
.get("/.well-known/webfinger?resource=acct:test-user@example.com") .get("/.well-known/webfinger?resource=acct:test-user@example.com")
@ -52,7 +55,7 @@ class WebFingerTest {
@Test @Test
fun `webfinger 存在しないユーザーに404`() { fun `webfinger 存在しないユーザーに404`() {
mockMvc mockMvc
.get("/.well-known/webfinger?resource=acct:test-user@example.com") .get("/.well-known/webfinger?resource=acct:invalid-user-notfound-afdjashfal@example.com")
.andExpect { status { isNotFound() } } .andExpect { status { isNotFound() } }
} }
@ -80,4 +83,13 @@ class WebFingerTest {
@Bean @Bean
fun testTransaction(): Transaction = TestTransaction fun testTransaction(): Transaction = TestTransaction
} }
companion object {
@JvmStatic
@AfterAll
fun dropDatabase(@Autowired flyway: Flyway) {
flyway.clean()
flyway.migrate()
}
}
} }

View File

@ -0,0 +1,170 @@
package mastodon.account
import dev.usbharu.hideout.SpringApplication
import dev.usbharu.hideout.core.infrastructure.exposedquery.UserQueryServiceImpl
import kotlinx.coroutines.test.runTest
import org.flywaydb.core.Flyway
import org.junit.jupiter.api.AfterAll
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.core.authority.SimpleGrantedAuthority
import org.springframework.security.test.context.support.WithAnonymousUser
import org.springframework.security.test.web.servlet.request.SecurityMockMvcRequestPostProcessors
import org.springframework.security.test.web.servlet.request.SecurityMockMvcRequestPostProcessors.jwt
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.post
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])
@AutoConfigureMockMvc
@Transactional
@Sql("/sql/test-user.sql", executionPhase = Sql.ExecutionPhase.BEFORE_TEST_CLASS)
class AccountApiTest {
@Autowired
private lateinit var userQueryServiceImpl: UserQueryServiceImpl
@Autowired
private lateinit var context: WebApplicationContext
private lateinit var mockMvc: MockMvc
@BeforeEach
fun setUp() {
mockMvc = MockMvcBuilders.webAppContextSetup(context)
.apply<DefaultMockMvcBuilder>(springSecurity())
.build()
}
@Test
fun `apiV1AccountsVerifyCredentialsGetにreadでアクセスできる`() {
mockMvc
.get("/api/v1/accounts/verify_credentials") {
with(jwt().jwt { it.claim("uid", "1") }.authorities(SimpleGrantedAuthority("SCOPE_read")))
}
.asyncDispatch()
.andDo { print() }
.andExpect { status { isOk() } }
}
@Test
fun `apiV1AccountsVerifyCredentialsGetにread_accountsでアクセスできる`() {
mockMvc
.get("/api/v1/accounts/verify_credentials") {
with(jwt().jwt { it.claim("uid", "1") }.authorities(SimpleGrantedAuthority("SCOPE_read:accounts")))
}
.asyncDispatch()
.andDo { print() }
.andExpect { status { isOk() } }
}
@Test
@WithAnonymousUser
fun apiV1AccountsVerifyCredentialsGetに匿名でアクセスすると401() {
mockMvc
.get("/api/v1/accounts/verify_credentials")
.andExpect { status { isUnauthorized() } }
}
@Test
@WithAnonymousUser
fun apiV1AccountsPostに匿名でPOSTしたらアカウントを作成できる() = runTest {
mockMvc
.post("/api/v1/accounts") {
contentType = MediaType.APPLICATION_FORM_URLENCODED
param("username", "api-test-user-1")
param("password", "very-secure-password")
param("email", "test@example.com")
param("agreement", "true")
param("locale", "")
with(SecurityMockMvcRequestPostProcessors.csrf())
}
.asyncDispatch()
.andExpect { status { isFound() } }
userQueryServiceImpl.findByNameAndDomain("api-test-user-1", "localhost")
}
@Test
@WithAnonymousUser
fun apiV1AccountsPostで必須パラメーター以外を省略しても作成できる() = runTest {
mockMvc
.post("/api/v1/accounts") {
contentType = MediaType.APPLICATION_FORM_URLENCODED
param("username", "api-test-user-2")
param("password", "very-secure-password")
with(SecurityMockMvcRequestPostProcessors.csrf())
}
.asyncDispatch()
.andExpect { status { isFound() } }
userQueryServiceImpl.findByNameAndDomain("api-test-user-2", "localhost")
}
@Test
@WithAnonymousUser
fun apiV1AccountsPostでusernameパラメーターを省略したら400() = runTest {
mockMvc
.post("/api/v1/accounts") {
contentType = MediaType.APPLICATION_FORM_URLENCODED
param("username", "api-test-user-3")
with(SecurityMockMvcRequestPostProcessors.csrf())
}
.andExpect { status { isBadRequest() } }
}
@Test
@WithAnonymousUser
fun apiV1AccountsPostでpasswordパラメーターを省略したら400() = runTest {
mockMvc
.post("/api/v1/accounts") {
contentType = MediaType.APPLICATION_FORM_URLENCODED
param("username", "api-test-user-4")
with(SecurityMockMvcRequestPostProcessors.csrf())
}
.andExpect { status { isBadRequest() } }
}
@Test
@WithAnonymousUser
fun apiV1AccountsPostでJSONで作ろうとしても400() {
mockMvc
.post("/api/v1/accounts") {
contentType = MediaType.APPLICATION_JSON
content = """{"username":"api-test-user-5","password":"very-very-secure-password"}"""
with(SecurityMockMvcRequestPostProcessors.csrf())
}
.andExpect { status { isUnsupportedMediaType() } }
}
@Test
@WithAnonymousUser
fun apiV1AccountsPostにCSRFトークンは必要() {
mockMvc
.post("/api/v1/accounts") {
contentType = MediaType.APPLICATION_FORM_URLENCODED
param("username", "api-test-user-2")
param("password", "very-secure-password")
}
.andExpect { status { isForbidden() } }
}
companion object {
@JvmStatic
@AfterAll
fun dropDatabase(@Autowired flyway: Flyway) {
flyway.clean()
flyway.migrate()
}
}
}

View File

@ -0,0 +1,99 @@
package mastodon.apps
import dev.usbharu.hideout.SpringApplication
import dev.usbharu.hideout.core.infrastructure.springframework.oauth2.RegisteredClient
import org.assertj.core.api.Assertions.assertThat
import org.flywaydb.core.Flyway
import org.jetbrains.exposed.sql.select
import org.junit.jupiter.api.AfterAll
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.context.support.WithAnonymousUser
import org.springframework.security.test.web.servlet.setup.SecurityMockMvcConfigurers
import org.springframework.test.web.servlet.MockMvc
import org.springframework.test.web.servlet.post
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])
@AutoConfigureMockMvc
@Transactional
class AppTest {
@Autowired
private lateinit var context: WebApplicationContext
private lateinit var mockMvc: MockMvc
@BeforeEach
fun setUp() {
mockMvc = MockMvcBuilders.webAppContextSetup(context)
.apply<DefaultMockMvcBuilder>(SecurityMockMvcConfigurers.springSecurity())
.build()
}
@Test
@WithAnonymousUser
fun apiV1AppsPostにformで匿名でappを作成できる() {
mockMvc
.post("/api/v1/apps") {
contentType = MediaType.APPLICATION_FORM_URLENCODED
param("client_name", "test-client")
param("redirect_uris", "https://example.com")
param("scopes", "write read")
param("website", "https://example.com")
}
.asyncDispatch()
.andExpect { status { isOk() } }
val app = RegisteredClient
.select { RegisteredClient.clientName eq "test-client" }
.single()
assertThat(app[RegisteredClient.clientName]).isEqualTo("test-client")
assertThat(app[RegisteredClient.redirectUris]).isEqualTo("https://example.com")
assertThat(app[RegisteredClient.scopes]).isEqualTo("read,write")
}
@Test
@WithAnonymousUser
fun apiV1AppsPostにjsonで匿名でappを作成できる() {
mockMvc
.post("/api/v1/apps") {
contentType = MediaType.APPLICATION_JSON
content = """{
"client_name": "test-client-2",
"redirect_uris": "https://example.com",
"scopes": "write read",
"website": "https;//example.com"
}"""
}
.asyncDispatch()
.andExpect { status { isOk() } }
val app = RegisteredClient
.select { RegisteredClient.clientName eq "test-client-2" }
.single()
assertThat(app[RegisteredClient.clientName]).isEqualTo("test-client-2")
assertThat(app[RegisteredClient.redirectUris]).isEqualTo("https://example.com")
assertThat(app[RegisteredClient.scopes]).isEqualTo("read,write")
}
companion object {
@JvmStatic
@AfterAll
fun dropDatabase(@Autowired flyway: Flyway) {
flyway.clean()
flyway.migrate()
}
}
}

View File

@ -0,0 +1,124 @@
package mastodon.media
import dev.usbharu.hideout.SpringApplication
import dev.usbharu.hideout.core.service.media.MediaDataStore
import dev.usbharu.hideout.core.service.media.MediaSaveRequest
import dev.usbharu.hideout.core.service.media.SuccessSavedMedia
import kotlinx.coroutines.test.runTest
import org.flywaydb.core.Flyway
import org.junit.jupiter.api.AfterAll
import org.junit.jupiter.api.BeforeEach
import org.junit.jupiter.api.Test
import org.mockito.kotlin.any
import org.mockito.kotlin.doReturn
import org.mockito.kotlin.whenever
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.boot.test.mock.mockito.MockBean
import org.springframework.mock.web.MockMultipartFile
import org.springframework.security.core.authority.SimpleGrantedAuthority
import org.springframework.security.test.web.servlet.request.SecurityMockMvcRequestPostProcessors.jwt
import org.springframework.security.test.web.servlet.setup.SecurityMockMvcConfigurers
import org.springframework.test.context.jdbc.Sql
import org.springframework.test.web.servlet.MockMvc
import org.springframework.test.web.servlet.multipart
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])
@AutoConfigureMockMvc
@Transactional
@Sql("/sql/test-user.sql", executionPhase = Sql.ExecutionPhase.BEFORE_TEST_CLASS)
class MediaTest {
@Autowired
private lateinit var context: WebApplicationContext
private lateinit var mockMvc: MockMvc
@MockBean
private lateinit var mediaDataStore: MediaDataStore
@BeforeEach
fun setUp() {
mockMvc = MockMvcBuilders.webAppContextSetup(context)
.apply<DefaultMockMvcBuilder>(SecurityMockMvcConfigurers.springSecurity())
.build()
}
@Test
fun メディアをアップロードできる() = runTest {
whenever(mediaDataStore.save(any<MediaSaveRequest>())).doReturn(SuccessSavedMedia("", "a", "a"))
mockMvc
.multipart("/api/v1/media") {
file(
MockMultipartFile(
"file",
"400x400.png",
"image/png",
String.javaClass.classLoader.getResourceAsStream("media/400x400.png")
)
)
with(jwt().jwt { it.claim("uid", "1") }.authorities(SimpleGrantedAuthority("SCOPE_write")))
}
.asyncDispatch()
.andExpect { status { isOk() } }
}
@Test
fun write_mediaスコープでメディアをアップロードできる() = runTest {
whenever(mediaDataStore.save(any<MediaSaveRequest>())).doReturn(SuccessSavedMedia("", "b", "b"))
mockMvc
.multipart("/api/v1/media") {
file(
MockMultipartFile(
"file",
"400x400.png",
"image/png",
String.javaClass.classLoader.getResourceAsStream("media/400x400.png")
)
)
with(jwt().jwt { it.claim("uid", "1") }.authorities(SimpleGrantedAuthority("SCOPE_write:media")))
}
.asyncDispatch()
.andExpect { status { isOk() } }
}
@Test
fun 権限がないと403() = runTest {
whenever(mediaDataStore.save(any<MediaSaveRequest>())).doReturn(SuccessSavedMedia("", "", ""))
mockMvc
.multipart("/api/v1/media") {
file(
MockMultipartFile(
"file",
"400x400.png",
"image/png",
String.javaClass.classLoader.getResourceAsStream("media/400x400.png")
)
)
with(jwt().jwt { it.claim("uid", "1") }.authorities(SimpleGrantedAuthority("SCOPE_read")))
}
.andExpect { status { isForbidden() } }
}
companion object {
@JvmStatic
@AfterAll
fun dropDatabase(@Autowired flyway: Flyway) {
flyway.clean()
flyway.migrate()
}
}
}

View File

@ -0,0 +1,156 @@
package mastodon.status
import dev.usbharu.hideout.SpringApplication
import org.flywaydb.core.Flyway
import org.junit.jupiter.api.AfterAll
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.core.authority.SimpleGrantedAuthority
import org.springframework.security.test.context.support.WithAnonymousUser
import org.springframework.security.test.web.servlet.request.SecurityMockMvcRequestPostProcessors
import org.springframework.security.test.web.servlet.request.SecurityMockMvcRequestPostProcessors.csrf
import org.springframework.security.test.web.servlet.setup.SecurityMockMvcConfigurers
import org.springframework.test.context.jdbc.Sql
import org.springframework.test.web.servlet.MockMvc
import org.springframework.test.web.servlet.post
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])
@AutoConfigureMockMvc
@Transactional
@Sql("/sql/test-user.sql", executionPhase = Sql.ExecutionPhase.BEFORE_TEST_CLASS)
class StatusTest {
@Autowired
private lateinit var context: WebApplicationContext
private lateinit var mockMvc: MockMvc
@BeforeEach
fun setUp() {
mockMvc = MockMvcBuilders.webAppContextSetup(context)
.apply<DefaultMockMvcBuilder>(SecurityMockMvcConfigurers.springSecurity())
.build()
}
@Test
fun 投稿できる() {
mockMvc
.post("/api/v1/statuses") {
contentType = MediaType.APPLICATION_JSON
content = """{"status":"hello"}"""
with(
SecurityMockMvcRequestPostProcessors.jwt()
.jwt { it.claim("uid", "1") }.authorities(SimpleGrantedAuthority("SCOPE_write"))
)
}
.asyncDispatch()
.andExpect { status { isOk() } }
}
@Test
fun write_statusesスコープで投稿できる() {
mockMvc
.post("/api/v1/statuses") {
contentType = MediaType.APPLICATION_JSON
content = """{"status":"hello"}"""
with(
SecurityMockMvcRequestPostProcessors.jwt()
.jwt { it.claim("uid", "1") }.authorities(SimpleGrantedAuthority("SCOPE_write:statuses"))
)
}
.asyncDispatch()
.andExpect { status { isOk() } }
}
@Test
fun 権限がないと403() {
mockMvc
.post("/api/v1/statuses") {
contentType = MediaType.APPLICATION_JSON
content = """{"status":"hello"}"""
with(
SecurityMockMvcRequestPostProcessors.jwt()
.jwt { it.claim("uid", "1") }.authorities(SimpleGrantedAuthority("SCOPE_read"))
)
}
.andExpect { status { isForbidden() } }
}
@Test
@WithAnonymousUser
fun 匿名だと401() {
mockMvc
.post("/api/v1/statuses") {
contentType = MediaType.APPLICATION_JSON
content = """{"status":"hello"}"""
with(csrf())
}
.andExpect { status { isUnauthorized() } }
}
@Test
@WithAnonymousUser
fun 匿名の場合通常はcsrfが無いので403() {
mockMvc
.post("/api/v1/statuses") {
contentType = MediaType.APPLICATION_JSON
content = """{"status":"hello"}"""
}
.andExpect { status { isForbidden() } }
}
@Test
fun formでも投稿できる() {
mockMvc
.post("/api/v1/statuses") {
contentType = MediaType.APPLICATION_FORM_URLENCODED
param("status", "hello")
with(
SecurityMockMvcRequestPostProcessors.jwt()
.jwt { it.claim("uid", "1") }.authorities(SimpleGrantedAuthority("SCOPE_write:statuses"))
)
}
.asyncDispatch()
.andExpect { status { isOk() } }
}
@Test
@Sql("/sql/test-post.sql")
fun in_reply_to_idを指定したら返信として処理される() {
mockMvc
.post("/api/v1/statuses") {
contentType = MediaType.APPLICATION_JSON
//language=JSON
content = """{
"status": "hello",
"in_reply_to_id": "1"
}"""
with(
SecurityMockMvcRequestPostProcessors.jwt()
.jwt { it.claim("uid", "1") }.authorities(SimpleGrantedAuthority("SCOPE_write"))
)
}
.asyncDispatch()
.andDo { print() }
.andExpect { status { isOk() } }
.andExpect { jsonPath("\$.in_reply_to_id") { value("1") } }
}
companion object {
@JvmStatic
@AfterAll
fun dropDatabase(@Autowired flyway: Flyway) {
flyway.clean()
flyway.migrate()
}
}
}

View File

@ -8,18 +8,15 @@ hideout:
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==" 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" public-key: "MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAu1SU1LfVLPHCozMxH2Mo4lgOEePzNm0tRgeLezV6ffAt0gunVTLw7onLRnrq0/IzW7yWR7QkrmBL7jTKEn5u+qKhbwKfBstIs+bMY2Zkp18gnTxKLxoS2tFczGkPLPgizskuemMghRniWaoLcyehkd3qqGElvW/VDL5AaWTg0nLVkjRo9z+40RQzuVaE8AkAFmxZzow3x+VJYKdjykkJ0iT9wCS0DRTXu269V264Vf/3jvredZiKRkgwlL9xNAwxXFg0x/XFw005UWVRIkdgcKWTjpBP2dPwVZ4WWC+9aGVd+Gyn1o0CLelf4rEjGoXbAAEgAqeGUxrcIlbjXfbcmwIDAQAB"
storage: storage:
use-s3: true type: local
endpoint: "http://localhost:8082/test-hideout"
public-url: "http://localhost:8082/test-hideout"
bucket: "test-hideout"
region: "auto"
access-key: ""
secret-key: ""
spring: spring:
flyway:
enabled: true
clean-disabled: false
datasource: datasource:
driver-class-name: org.h2.Driver driver-class-name: org.h2.Driver
url: "jdbc:h2:mem:test;MODE=POSTGRESQL;DB_CLOSE_DELAY=-1" url: "jdbc:h2:mem:test;MODE=POSTGRESQL;DB_CLOSE_DELAY=-1;CASE_INSENSITIVE_IDENTIFIERS=true;TRACE_LEVEL_FILE=4;"
username: "" username: ""
password: password:
data: data:
@ -32,8 +29,8 @@ spring:
console: console:
enabled: true enabled: true
exposed: # exposed:
generate-ddl: true # generate-ddl: true
excluded-packages: dev.usbharu.hideout.core.infrastructure.kjobexposed # excluded-packages: dev.usbharu.hideout.core.infrastructure.kjobexposed
server: server:
port: 8080 port: 8080

View File

@ -0,0 +1,2 @@
junit.jupiter.testclass.order.default=org.junit.jupiter.api.ClassOrderer$Random
junit.jupiter.testmethod.order.default=org.junit.jupiter.api.MethodOrderer$Random

View File

@ -7,4 +7,5 @@
<root level="TRACE"> <root level="TRACE">
<appender-ref ref="STDOUT"/> <appender-ref ref="STDOUT"/>
</root> </root>
<logger name="org.springframework.security" level="TRACE"/>
</configuration> </configuration>

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.1 KiB

View File

@ -0,0 +1,3 @@
insert into posts (id, user_id, overview, text, created_at, visibility, url, repost_id, reply_id, sensitive, ap_id)
VALUES (1, 1, null, 'hello', 1234455, 0, 'https://localhost/users/1/posts/1', null, null, false,
'https://users/1/posts/1');

View File

@ -0,0 +1,21 @@
package dev.usbharu.hideout.activitypub.domain.exception
import java.io.Serial
class ActivityPubProcessException : RuntimeException {
constructor() : super()
constructor(message: String?) : super(message)
constructor(message: String?, cause: Throwable?) : super(message, cause)
constructor(cause: Throwable?) : super(cause)
constructor(message: String?, cause: Throwable?, enableSuppression: Boolean, writableStackTrace: Boolean) : super(
message,
cause,
enableSuppression,
writableStackTrace
)
companion object {
@Serial
private const val serialVersionUID: Long = 5370068873167636639L
}
}

View File

@ -0,0 +1,21 @@
package dev.usbharu.hideout.activitypub.domain.exception
import java.io.Serial
class FailedProcessException : RuntimeException {
constructor() : super()
constructor(message: String?) : super(message)
constructor(message: String?, cause: Throwable?) : super(message, cause)
constructor(cause: Throwable?) : super(cause)
constructor(message: String?, cause: Throwable?, enableSuppression: Boolean, writableStackTrace: Boolean) : super(
message,
cause,
enableSuppression,
writableStackTrace
)
companion object {
@Serial
private const val serialVersionUID: Long = -1305337651143409144L
}
}

View File

@ -1,10 +1,16 @@
package dev.usbharu.hideout.activitypub.domain.exception package dev.usbharu.hideout.activitypub.domain.exception
import dev.usbharu.hideout.core.domain.exception.FailedToGetResourcesException import dev.usbharu.hideout.core.domain.exception.FailedToGetResourcesException
import java.io.Serial
class FailedToGetActivityPubResourceException : FailedToGetResourcesException { class FailedToGetActivityPubResourceException : FailedToGetResourcesException {
constructor() : super() constructor() : super()
constructor(s: String?) : super(s) constructor(s: String?) : super(s)
constructor(message: String?, cause: Throwable?) : super(message, cause) constructor(message: String?, cause: Throwable?) : super(message, cause)
constructor(cause: Throwable?) : super(cause) constructor(cause: Throwable?) : super(cause)
companion object {
@Serial
private const val serialVersionUID: Long = 6420233106776818052L
}
} }

View File

@ -0,0 +1,21 @@
package dev.usbharu.hideout.activitypub.domain.exception
import java.io.Serial
class HttpSignatureUnauthorizedException : RuntimeException {
constructor() : super()
constructor(message: String?) : super(message)
constructor(message: String?, cause: Throwable?) : super(message, cause)
constructor(cause: Throwable?) : super(cause)
constructor(message: String?, cause: Throwable?, enableSuppression: Boolean, writableStackTrace: Boolean) : super(
message,
cause,
enableSuppression,
writableStackTrace
)
companion object {
@Serial
private const val serialVersionUID: Long = -6449793151674654501L
}
}

View File

@ -1,40 +1,52 @@
package dev.usbharu.hideout.activitypub.domain.model package dev.usbharu.hideout.activitypub.domain.model
import com.fasterxml.jackson.annotation.JsonCreator
import com.fasterxml.jackson.annotation.JsonProperty
import com.fasterxml.jackson.databind.annotation.JsonDeserialize import com.fasterxml.jackson.databind.annotation.JsonDeserialize
import dev.usbharu.hideout.activitypub.domain.model.objects.Object import dev.usbharu.hideout.activitypub.domain.model.objects.Object
import dev.usbharu.hideout.activitypub.domain.model.objects.ObjectDeserializer import dev.usbharu.hideout.activitypub.domain.model.objects.ObjectDeserializer
open class Accept : Object { open class Accept @JsonCreator constructor(
@JsonDeserialize(using = ObjectDeserializer::class)
var `object`: Object? = null
protected constructor()
constructor(
type: List<String> = emptyList(), type: List<String> = emptyList(),
name: String, override val name: String,
`object`: Object?, @JsonDeserialize(using = ObjectDeserializer::class)
actor: String? @JsonProperty("object")
) : super( val apObject: Object,
type = add(type, "Accept"), override val actor: String
name = name, ) : Object(
actor = actor type = add(type, "Accept")
) { ),
this.`object` = `object` HasActor,
} HasName {
override fun toString(): String = "Accept(`object`=$`object`) ${super.toString()}"
override fun equals(other: Any?): Boolean { override fun equals(other: Any?): Boolean {
if (this === other) return true if (this === other) return true
if (other !is Accept) return false if (javaClass != other?.javaClass) return false
if (!super.equals(other)) return false if (!super.equals(other)) return false
return `object` == other.`object` other as Accept
if (name != other.name) return false
if (apObject != other.apObject) return false
if (actor != other.actor) return false
return true
} }
override fun hashCode(): Int { override fun hashCode(): Int {
var result = super.hashCode() var result = super.hashCode()
result = 31 * result + (`object`?.hashCode() ?: 0) result = 31 * result + name.hashCode()
result = 31 * result + apObject.hashCode()
result = 31 * result + actor.hashCode()
return result return result
} }
override fun toString(): String {
return "Accept(" +
"name='$name', " +
"apObject=$apObject, " +
"actor='$actor'" +
")" +
" ${super.toString()}"
}
} }

View File

@ -1,48 +1,64 @@
package dev.usbharu.hideout.activitypub.domain.model package dev.usbharu.hideout.activitypub.domain.model
import com.fasterxml.jackson.annotation.JsonProperty
import com.fasterxml.jackson.databind.annotation.JsonDeserialize import com.fasterxml.jackson.databind.annotation.JsonDeserialize
import dev.usbharu.hideout.activitypub.domain.model.objects.Object import dev.usbharu.hideout.activitypub.domain.model.objects.Object
import dev.usbharu.hideout.activitypub.domain.model.objects.ObjectDeserializer import dev.usbharu.hideout.activitypub.domain.model.objects.ObjectDeserializer
open class Create : Object { open class Create(
@JsonDeserialize(using = ObjectDeserializer::class)
var `object`: Object? = null
var to: List<String> = emptyList()
var cc: List<String> = emptyList()
protected constructor() : super()
constructor(
type: List<String> = emptyList(), type: List<String> = emptyList(),
name: String? = null, override val name: String,
`object`: Object?, @JsonDeserialize(using = ObjectDeserializer::class)
actor: String? = null, @JsonProperty("object")
id: String? = null, val apObject: Object,
to: List<String> = emptyList(), override val actor: String,
cc: List<String> = emptyList() override val id: String,
) : super( val to: List<String> = emptyList(),
type = add(type, "Create"), val cc: List<String> = emptyList()
name = name, ) : Object(
actor = actor, type = add(type, "Create")
id = id ),
) { HasId,
this.`object` = `object` HasName,
this.to = to HasActor {
this.cc = cc
}
override fun equals(other: Any?): Boolean { override fun equals(other: Any?): Boolean {
if (this === other) return true if (this === other) return true
if (other !is Create) return false if (javaClass != other?.javaClass) return false
if (!super.equals(other)) return false if (!super.equals(other)) return false
return `object` == other.`object` other as Create
if (name != other.name) return false
if (apObject != other.apObject) return false
if (actor != other.actor) return false
if (id != other.id) return false
if (to != other.to) return false
if (cc != other.cc) return false
return true
} }
override fun hashCode(): Int { override fun hashCode(): Int {
var result = super.hashCode() var result = super.hashCode()
result = 31 * result + (`object`?.hashCode() ?: 0) result = 31 * result + name.hashCode()
result = 31 * result + apObject.hashCode()
result = 31 * result + actor.hashCode()
result = 31 * result + id.hashCode()
result = 31 * result + to.hashCode()
result = 31 * result + cc.hashCode()
return result return result
} }
override fun toString(): String = "Create(`object`=$`object`) ${super.toString()}" override fun toString(): String {
return "Create(" +
"name='$name', " +
"apObject=$apObject, " +
"actor='$actor', " +
"id='$id', " +
"to=$to, " +
"cc=$cc" +
")" +
" ${super.toString()}"
}
} }

View File

@ -1,45 +1,55 @@
package dev.usbharu.hideout.activitypub.domain.model package dev.usbharu.hideout.activitypub.domain.model
import com.fasterxml.jackson.annotation.JsonProperty
import com.fasterxml.jackson.databind.annotation.JsonDeserialize import com.fasterxml.jackson.databind.annotation.JsonDeserialize
import dev.usbharu.hideout.activitypub.domain.model.objects.Object import dev.usbharu.hideout.activitypub.domain.model.objects.Object
import dev.usbharu.hideout.activitypub.domain.model.objects.ObjectDeserializer import dev.usbharu.hideout.activitypub.domain.model.objects.ObjectDeserializer
open class Delete : Object { open class Delete : Object, HasId, HasActor {
@JsonDeserialize(using = ObjectDeserializer::class) @JsonDeserialize(using = ObjectDeserializer::class)
var `object`: Object? = null @JsonProperty("object")
var published: String? = null val apObject: Object
val published: String
override val actor: String
override val id: String
constructor( constructor(
type: List<String> = emptyList(), type: List<String> = emptyList(),
name: String? = "Delete",
actor: String, actor: String,
id: String, id: String,
`object`: Object, `object`: Object,
published: String? published: String
) : super(add(type, "Delete"), name, actor, id) { ) : super(add(type, "Delete")) {
this.`object` = `object` this.apObject = `object`
this.published = published this.published = published
this.actor = actor
this.id = id
} }
protected constructor() : super()
override fun equals(other: Any?): Boolean { override fun equals(other: Any?): Boolean {
if (this === other) return true if (this === other) return true
if (other !is Delete) return false if (javaClass != other?.javaClass) return false
if (!super.equals(other)) return false if (!super.equals(other)) return false
if (`object` != other.`object`) return false other as Delete
if (apObject != other.apObject) return false
if (published != other.published) return false if (published != other.published) return false
if (actor != other.actor) return false
if (id != other.id) return false
return true return true
} }
override fun hashCode(): Int { override fun hashCode(): Int {
var result = super.hashCode() var result = super.hashCode()
result = 31 * result + (`object`?.hashCode() ?: 0) result = 31 * result + apObject.hashCode()
result = 31 * result + (published?.hashCode() ?: 0) result = 31 * result + published.hashCode()
result = 31 * result + actor.hashCode()
result = 31 * result + id.hashCode()
return result return result
} }
override fun toString(): String = "Delete(`object`=$`object`, published=$published) ${super.toString()}" override fun toString(): String =
"Delete(`object`=$apObject, published=$published, actor='$actor', id='$id') ${super.toString()}"
} }

View File

@ -2,44 +2,37 @@ package dev.usbharu.hideout.activitypub.domain.model
import dev.usbharu.hideout.activitypub.domain.model.objects.Object import dev.usbharu.hideout.activitypub.domain.model.objects.Object
open class Document : Object { open class Document(
var mediaType: String? = null
var url: String? = null
protected constructor() : super()
constructor(
type: List<String> = emptyList(), type: List<String> = emptyList(),
name: String? = null, override val name: String = "",
mediaType: String, val mediaType: String,
url: String val url: String
) : super( ) : Object(
type = add(type, "Document"), type = add(type, "Document")
name = name, ),
actor = null, HasName {
id = null
) {
this.mediaType = mediaType
this.url = url
}
override fun equals(other: Any?): Boolean { override fun equals(other: Any?): Boolean {
if (this === other) return true if (this === other) return true
if (other !is Document) return false if (javaClass != other?.javaClass) return false
if (!super.equals(other)) return false if (!super.equals(other)) return false
other as Document
if (mediaType != other.mediaType) return false if (mediaType != other.mediaType) return false
if (url != other.url) return false if (url != other.url) return false
if (name != other.name) return false
return true return true
} }
override fun hashCode(): Int { override fun hashCode(): Int {
var result = super.hashCode() var result = super.hashCode()
result = 31 * result + (mediaType?.hashCode() ?: 0) result = 31 * result + mediaType.hashCode()
result = 31 * result + (url?.hashCode() ?: 0) result = 31 * result + url.hashCode()
result = 31 * result + name.hashCode()
return result return result
} }
override fun toString(): String = "Document(mediaType=$mediaType, url=$url) ${super.toString()}" override fun toString(): String = "Document(mediaType=$mediaType, url=$url, name='$name') ${super.toString()}"
} }

View File

@ -2,27 +2,17 @@ package dev.usbharu.hideout.activitypub.domain.model
import dev.usbharu.hideout.activitypub.domain.model.objects.Object import dev.usbharu.hideout.activitypub.domain.model.objects.Object
open class Emoji : Object { open class Emoji(
var updated: String? = null
var icon: Image? = null
protected constructor() : super()
constructor(
type: List<String>, type: List<String>,
name: String?, override val name: String,
actor: String?, override val id: String,
id: String?, val updated: String,
updated: String?, val icon: Image
icon: Image? ) : Object(
) : super( type = add(type, "Emoji")
type = add(type, "Emoji"), ),
name = name, HasName,
actor = actor, HasId {
id = id
) {
this.updated = updated
this.icon = icon
}
override fun equals(other: Any?): Boolean { override fun equals(other: Any?): Boolean {
if (this === other) return true if (this === other) return true
@ -35,8 +25,8 @@ open class Emoji : Object {
override fun hashCode(): Int { override fun hashCode(): Int {
var result = super.hashCode() var result = super.hashCode()
result = 31 * result + (updated?.hashCode() ?: 0) result = 31 * result + updated.hashCode()
result = 31 * result + (icon?.hashCode() ?: 0) result = 31 * result + icon.hashCode()
return result return result
} }

View File

@ -1,37 +1,36 @@
package dev.usbharu.hideout.activitypub.domain.model package dev.usbharu.hideout.activitypub.domain.model
import com.fasterxml.jackson.annotation.JsonProperty
import dev.usbharu.hideout.activitypub.domain.model.objects.Object import dev.usbharu.hideout.activitypub.domain.model.objects.Object
open class Follow : Object { open class Follow(
var `object`: String? = null
protected constructor() : super()
constructor(
type: List<String> = emptyList(), type: List<String> = emptyList(),
name: String?, @JsonProperty("object") val apObject: String,
`object`: String?, override val actor: String
actor: String? ) : Object(
) : super( type = add(type, "Follow")
type = add(type, "Follow"), ),
name = name, HasActor {
actor = actor
) {
this.`object` = `object`
}
override fun equals(other: Any?): Boolean { override fun equals(other: Any?): Boolean {
if (this === other) return true if (this === other) return true
if (other !is Follow) return false if (javaClass != other?.javaClass) return false
if (!super.equals(other)) return false if (!super.equals(other)) return false
return `object` == other.`object` other as Follow
if (apObject != other.apObject) return false
if (actor != other.actor) return false
return true
} }
override fun hashCode(): Int { override fun hashCode(): Int {
var result = super.hashCode() var result = super.hashCode()
result = 31 * result + (`object`?.hashCode() ?: 0) result = 31 * result + apObject.hashCode()
result = 31 * result + actor.hashCode()
return result return result
} }
override fun toString(): String = "Follow(`object`=$`object`) ${super.toString()}" override fun toString(): String = "Follow(`object`=$apObject, actor='$actor') ${super.toString()}"
} }

View File

@ -0,0 +1,5 @@
package dev.usbharu.hideout.activitypub.domain.model
interface HasActor {
val actor: String
}

View File

@ -0,0 +1,5 @@
package dev.usbharu.hideout.activitypub.domain.model
interface HasId {
val id: String
}

View File

@ -0,0 +1,5 @@
package dev.usbharu.hideout.activitypub.domain.model
interface HasName {
val name: String
}

View File

@ -2,32 +2,33 @@ package dev.usbharu.hideout.activitypub.domain.model
import dev.usbharu.hideout.activitypub.domain.model.objects.Object import dev.usbharu.hideout.activitypub.domain.model.objects.Object
open class Image : Object { open class Image(
private var mediaType: String? = null type: List<String> = emptyList(),
private var url: String? = null val mediaType: String,
val url: String
protected constructor() : super() ) : Object(
constructor(type: List<String> = emptyList(), name: String, mediaType: String?, url: String?) : super( add(type, "Image")
add(type, "Image"), ) {
name
) {
this.mediaType = mediaType
this.url = url
}
override fun equals(other: Any?): Boolean { override fun equals(other: Any?): Boolean {
if (this === other) return true if (this === other) return true
if (other !is Image) return false if (javaClass != other?.javaClass) return false
if (!super.equals(other)) return false if (!super.equals(other)) return false
other as Image
if (mediaType != other.mediaType) return false if (mediaType != other.mediaType) return false
return url == other.url if (url != other.url) return false
return true
} }
override fun hashCode(): Int { override fun hashCode(): Int {
var result = super.hashCode() var result = super.hashCode()
result = 31 * result + (mediaType?.hashCode() ?: 0) result = 31 * result + mediaType.hashCode()
result = 31 * result + (url?.hashCode() ?: 0) result = 31 * result + url.hashCode()
return result return result
} }
override fun toString(): String = "Image(mediaType=$mediaType, url=$url) ${super.toString()}"
} }

View File

@ -2,41 +2,36 @@ package dev.usbharu.hideout.activitypub.domain.model
import dev.usbharu.hideout.activitypub.domain.model.objects.Object import dev.usbharu.hideout.activitypub.domain.model.objects.Object
open class Key : Object { open class Key(
var owner: String? = null override val id: String,
var publicKeyPem: String? = null val owner: String,
val publicKeyPem: String
protected constructor() : super() ) : Object(
constructor( type = add(list = emptyList(), type = "Key")
type: List<String>, ),
name: String, HasId {
id: String,
owner: String?,
publicKeyPem: String?
) : super(
type = add(list = type, type = "Key"),
name = name,
id = id
) {
this.owner = owner
this.publicKeyPem = publicKeyPem
}
override fun equals(other: Any?): Boolean { override fun equals(other: Any?): Boolean {
if (this === other) return true if (this === other) return true
if (other !is Key) return false if (javaClass != other?.javaClass) return false
if (!super.equals(other)) return false if (!super.equals(other)) return false
other as Key
if (owner != other.owner) return false if (owner != other.owner) return false
return publicKeyPem == other.publicKeyPem if (publicKeyPem != other.publicKeyPem) return false
if (id != other.id) return false
return true
} }
override fun hashCode(): Int { override fun hashCode(): Int {
var result = super.hashCode() var result = super.hashCode()
result = 31 * result + (owner?.hashCode() ?: 0) result = 31 * result + owner.hashCode()
result = 31 * result + (publicKeyPem?.hashCode() ?: 0) result = 31 * result + publicKeyPem.hashCode()
result = 31 * result + id.hashCode()
return result return result
} }
override fun toString(): String = "Key(owner=$owner, publicKeyPem=$publicKeyPem) ${super.toString()}" override fun toString(): String = "Key(owner=$owner, publicKeyPem=$publicKeyPem, id='$id') ${super.toString()}"
} }

View File

@ -1,53 +1,57 @@
package dev.usbharu.hideout.activitypub.domain.model package dev.usbharu.hideout.activitypub.domain.model
import com.fasterxml.jackson.annotation.JsonProperty
import com.fasterxml.jackson.databind.annotation.JsonDeserialize import com.fasterxml.jackson.databind.annotation.JsonDeserialize
import dev.usbharu.hideout.activitypub.domain.model.objects.Object import dev.usbharu.hideout.activitypub.domain.model.objects.Object
import dev.usbharu.hideout.activitypub.domain.model.objects.ObjectDeserializer import dev.usbharu.hideout.activitypub.domain.model.objects.ObjectDeserializer
open class Like : Object { open class Like(
var `object`: String? = null
var content: String? = null
@JsonDeserialize(contentUsing = ObjectDeserializer::class)
var tag: List<Object> = emptyList()
protected constructor() : super()
constructor(
type: List<String> = emptyList(), type: List<String> = emptyList(),
name: String?, override val actor: String,
actor: String?, override val id: String,
id: String?, @JsonProperty("object") val apObject: String,
`object`: String?, val content: String,
content: String?, @JsonDeserialize(contentUsing = ObjectDeserializer::class) val tag: List<Object> = emptyList()
tag: List<Object> = emptyList() ) : Object(
) : super( type = add(type, "Like")
type = add(type, "Like"), ),
name = name, HasId,
actor = actor, HasActor {
id = id
) {
this.`object` = `object`
this.content = content
this.tag = tag
}
override fun equals(other: Any?): Boolean { override fun equals(other: Any?): Boolean {
if (this === other) return true if (this === other) return true
if (other !is Like) return false if (javaClass != other?.javaClass) return false
if (!super.equals(other)) return false if (!super.equals(other)) return false
if (`object` != other.`object`) return false other as Like
if (actor != other.actor) return false
if (id != other.id) return false
if (apObject != other.apObject) return false
if (content != other.content) return false if (content != other.content) return false
return tag == other.tag if (tag != other.tag) return false
return true
} }
override fun hashCode(): Int { override fun hashCode(): Int {
var result = super.hashCode() var result = super.hashCode()
result = 31 * result + (`object`?.hashCode() ?: 0) result = 31 * result + actor.hashCode()
result = 31 * result + (content?.hashCode() ?: 0) result = 31 * result + id.hashCode()
result = 31 * result + apObject.hashCode()
result = 31 * result + content.hashCode()
result = 31 * result + tag.hashCode() result = 31 * result + tag.hashCode()
return result return result
} }
override fun toString(): String = "Like(`object`=$`object`, content=$content, tag=$tag) ${super.toString()}" override fun toString(): String {
return "Like(" +
"actor='$actor', " +
"id='$id', " +
"apObject='$apObject', " +
"content='$content', " +
"tag=$tag" +
")" +
" ${super.toString()}"
}
} }

View File

@ -2,79 +2,70 @@ package dev.usbharu.hideout.activitypub.domain.model
import dev.usbharu.hideout.activitypub.domain.model.objects.Object import dev.usbharu.hideout.activitypub.domain.model.objects.Object
open class Note : Object { open class Note
var attributedTo: String? = null @Suppress("LongParameterList")
var attachment: List<Document> = emptyList() constructor(
var content: String? = null
var published: String? = null
var to: List<String> = emptyList()
var cc: List<String> = emptyList()
var sensitive: Boolean = false
var inReplyTo: String? = null
protected constructor() : super()
@Suppress("LongParameterList")
constructor(
type: List<String> = emptyList(), type: List<String> = emptyList(),
name: String, override val id: String,
id: String?, val attributedTo: String,
attributedTo: String?, val content: String,
content: String?, val published: String,
published: String?, val to: List<String> = emptyList(),
to: List<String> = emptyList(), val cc: List<String> = emptyList(),
cc: List<String> = emptyList(), val sensitive: Boolean = false,
sensitive: Boolean = false, val inReplyTo: String? = null,
inReplyTo: String? = null, val attachment: List<Document> = emptyList()
attachment: List<Document> = emptyList() ) : Object(
) : super( type = add(type, "Note")
type = add(type, "Note"), ),
name = name, HasId {
id = id
) {
this.attributedTo = attributedTo
this.content = content
this.published = published
this.to = to
this.cc = cc
this.sensitive = sensitive
this.inReplyTo = inReplyTo
this.attachment = attachment
}
override fun equals(other: Any?): Boolean { override fun equals(other: Any?): Boolean {
if (this === other) return true if (this === other) return true
if (other !is Note) return false if (javaClass != other?.javaClass) return false
if (!super.equals(other)) return false if (!super.equals(other)) return false
other as Note
if (id != other.id) return false
if (attributedTo != other.attributedTo) return false if (attributedTo != other.attributedTo) return false
if (attachment != other.attachment) return false
if (content != other.content) return false if (content != other.content) return false
if (published != other.published) return false if (published != other.published) return false
if (to != other.to) return false if (to != other.to) return false
if (cc != other.cc) return false if (cc != other.cc) return false
if (sensitive != other.sensitive) return false if (sensitive != other.sensitive) return false
if (inReplyTo != other.inReplyTo) return false if (inReplyTo != other.inReplyTo) return false
if (attachment != other.attachment) return false
return true return true
} }
override fun hashCode(): Int { override fun hashCode(): Int {
var result = super.hashCode() var result = super.hashCode()
result = 31 * result + (attributedTo?.hashCode() ?: 0) result = 31 * result + id.hashCode()
result = 31 * result + attachment.hashCode() result = 31 * result + attributedTo.hashCode()
result = 31 * result + (content?.hashCode() ?: 0) result = 31 * result + content.hashCode()
result = 31 * result + (published?.hashCode() ?: 0) result = 31 * result + published.hashCode()
result = 31 * result + to.hashCode() result = 31 * result + to.hashCode()
result = 31 * result + cc.hashCode() result = 31 * result + cc.hashCode()
result = 31 * result + sensitive.hashCode() result = 31 * result + sensitive.hashCode()
result = 31 * result + (inReplyTo?.hashCode() ?: 0) result = 31 * result + (inReplyTo?.hashCode() ?: 0)
result = 31 * result + attachment.hashCode()
return result return result
} }
override fun toString(): String { override fun toString(): String {
return "Note(attributedTo=$attributedTo, attachment=$attachment, " + return "Note(" +
"content=$content, published=$published, to=$to, cc=$cc, sensitive=$sensitive," + "id='$id', " +
" inReplyTo=$inReplyTo) ${super.toString()}" "attributedTo='$attributedTo', " +
"content='$content', " +
"published='$published', " +
"to=$to, " +
"cc=$cc, " +
"sensitive=$sensitive, " +
"inReplyTo=$inReplyTo, " +
"attachment=$attachment" +
")" +
" ${super.toString()}"
} }
} }

View File

@ -2,53 +2,33 @@ package dev.usbharu.hideout.activitypub.domain.model
import dev.usbharu.hideout.activitypub.domain.model.objects.Object import dev.usbharu.hideout.activitypub.domain.model.objects.Object
open class Person : Object { open class Person
var preferredUsername: String? = null @Suppress("LongParameterList")
var summary: String? = null constructor(
var inbox: String? = null
var outbox: String? = null
var url: String? = null
private var icon: Image? = null
var publicKey: Key? = null
var endpoints: Map<String, String> = emptyMap()
var following: String? = null
var followers: String? = null
protected constructor() : super()
@Suppress("LongParameterList")
constructor(
type: List<String> = emptyList(), type: List<String> = emptyList(),
name: String, override val name: String,
id: String?, override val id: String,
preferredUsername: String?, var preferredUsername: String?,
summary: String?, var summary: String?,
inbox: String?, var inbox: String,
outbox: String?, var outbox: String,
url: String?, var url: String,
icon: Image?, private var icon: Image?,
publicKey: Key?, var publicKey: Key,
endpoints: Map<String, String> = emptyMap(), var endpoints: Map<String, String> = emptyMap(),
followers: String?, var followers: String?,
following: String? var following: String?
) : super(add(type, "Person"), name, id = id) { ) : Object(add(type, "Person")), HasId, HasName {
this.preferredUsername = preferredUsername
this.summary = summary
this.inbox = inbox
this.outbox = outbox
this.url = url
this.icon = icon
this.publicKey = publicKey
this.endpoints = endpoints
this.followers = followers
this.following = following
}
override fun equals(other: Any?): Boolean { override fun equals(other: Any?): Boolean {
if (this === other) return true if (this === other) return true
if (other !is Person) return false if (javaClass != other?.javaClass) return false
if (!super.equals(other)) return false if (!super.equals(other)) return false
other as Person
if (name != other.name) return false
if (id != other.id) return false
if (preferredUsername != other.preferredUsername) return false if (preferredUsername != other.preferredUsername) return false
if (summary != other.summary) return false if (summary != other.summary) return false
if (inbox != other.inbox) return false if (inbox != other.inbox) return false
@ -57,20 +37,26 @@ open class Person : Object {
if (icon != other.icon) return false if (icon != other.icon) return false
if (publicKey != other.publicKey) return false if (publicKey != other.publicKey) return false
if (endpoints != other.endpoints) return false if (endpoints != other.endpoints) return false
if (followers != other.followers) return false
if (following != other.following) return false
return true return true
} }
override fun hashCode(): Int { override fun hashCode(): Int {
var result = super.hashCode() var result = super.hashCode()
result = 31 * result + name.hashCode()
result = 31 * result + id.hashCode()
result = 31 * result + (preferredUsername?.hashCode() ?: 0) result = 31 * result + (preferredUsername?.hashCode() ?: 0)
result = 31 * result + (summary?.hashCode() ?: 0) result = 31 * result + (summary?.hashCode() ?: 0)
result = 31 * result + (inbox?.hashCode() ?: 0) result = 31 * result + inbox.hashCode()
result = 31 * result + (outbox?.hashCode() ?: 0) result = 31 * result + outbox.hashCode()
result = 31 * result + (url?.hashCode() ?: 0) result = 31 * result + url.hashCode()
result = 31 * result + (icon?.hashCode() ?: 0) result = 31 * result + (icon?.hashCode() ?: 0)
result = 31 * result + (publicKey?.hashCode() ?: 0) result = 31 * result + publicKey.hashCode()
result = 31 * result + endpoints.hashCode() result = 31 * result + endpoints.hashCode()
result = 31 * result + (followers?.hashCode() ?: 0)
result = 31 * result + (following?.hashCode() ?: 0)
return result return result
} }
} }

View File

@ -2,11 +2,24 @@ package dev.usbharu.hideout.activitypub.domain.model
import dev.usbharu.hideout.activitypub.domain.model.objects.Object import dev.usbharu.hideout.activitypub.domain.model.objects.Object
open class Tombstone : Object { open class Tombstone(type: List<String> = emptyList(), override val id: String) :
constructor( Object(add(type, "Tombstone")),
type: List<String> = emptyList(), HasId {
name: String = "Tombstone", override fun equals(other: Any?): Boolean {
actor: String? = null, if (this === other) return true
id: String if (javaClass != other?.javaClass) return false
) : super(add(type, "Tombstone"), name, actor, id) if (!super.equals(other)) return false
other as Tombstone
return id == other.id
}
override fun hashCode(): Int {
var result = super.hashCode()
result = 31 * result + id.hashCode()
return result
}
override fun toString(): String = "Tombstone(id='$id') ${super.toString()}"
} }

View File

@ -3,42 +3,40 @@ package dev.usbharu.hideout.activitypub.domain.model
import com.fasterxml.jackson.databind.annotation.JsonDeserialize import com.fasterxml.jackson.databind.annotation.JsonDeserialize
import dev.usbharu.hideout.activitypub.domain.model.objects.Object import dev.usbharu.hideout.activitypub.domain.model.objects.Object
import dev.usbharu.hideout.activitypub.domain.model.objects.ObjectDeserializer import dev.usbharu.hideout.activitypub.domain.model.objects.ObjectDeserializer
import java.time.Instant
open class Undo : Object { open class Undo(
@JsonDeserialize(using = ObjectDeserializer::class)
var `object`: Object? = null
var published: String? = null
protected constructor() : super()
constructor(
type: List<String> = emptyList(), type: List<String> = emptyList(),
name: String, override val actor: String,
actor: String, override val id: String,
id: String?, @JsonDeserialize(using = ObjectDeserializer::class)
`object`: Object, @Suppress("VariableNaming") val `object`: Object,
published: Instant val published: String
) : super(add(type, "Undo"), name, actor, id) { ) : Object(add(type, "Undo")), HasId, HasActor {
this.`object` = `object`
this.published = published.toString()
}
override fun equals(other: Any?): Boolean { override fun equals(other: Any?): Boolean {
if (this === other) return true if (this === other) return true
if (other !is Undo) return false if (javaClass != other?.javaClass) return false
if (!super.equals(other)) return false if (!super.equals(other)) return false
other as Undo
if (`object` != other.`object`) return false if (`object` != other.`object`) return false
return published == other.published if (published != other.published) return false
if (actor != other.actor) return false
if (id != other.id) return false
return true
} }
override fun hashCode(): Int { override fun hashCode(): Int {
var result = super.hashCode() var result = super.hashCode()
result = 31 * result + (`object`?.hashCode() ?: 0) result = 31 * result + `object`.hashCode()
result = 31 * result + (published?.hashCode() ?: 0) result = 31 * result + published.hashCode()
result = 31 * result + actor.hashCode()
result = 31 * result + id.hashCode()
return result return result
} }
override fun toString(): String = "Undo(`object`=$`object`, published=$published) ${super.toString()}" override fun toString(): String =
"Undo(`object`=$`object`, published=$published, actor='$actor', id='$id') ${super.toString()}"
} }

View File

@ -12,41 +12,29 @@ open class Object : JsonLd {
set(value) { set(value) {
field = value.filter { it.isNotBlank() } field = value.filter { it.isNotBlank() }
} }
var name: String? = null
var actor: String? = null
var id: String? = null
protected constructor() protected constructor()
constructor(type: List<String>, name: String? = null, actor: String? = null, id: String? = null) : super() { constructor(type: List<String>) : super() {
this.type = type.filter { it.isNotBlank() } this.type = type.filter { it.isNotBlank() }
this.name = name
this.actor = actor
this.id = id
} }
override fun equals(other: Any?): Boolean { override fun equals(other: Any?): Boolean {
if (this === other) return true if (this === other) return true
if (other !is Object) return false if (javaClass != other?.javaClass) return false
if (!super.equals(other)) return false if (!super.equals(other)) return false
if (type != other.type) return false other as Object
if (name != other.name) return false
if (actor != other.actor) return false
if (id != other.id) return false
return true return type == other.type
} }
override fun hashCode(): Int { override fun hashCode(): Int {
var result = super.hashCode() var result = super.hashCode()
result = 31 * result + type.hashCode() result = 31 * result + type.hashCode()
result = 31 * result + (name?.hashCode() ?: 0)
result = 31 * result + (actor?.hashCode() ?: 0)
result = 31 * result + (id?.hashCode() ?: 0)
return result return result
} }
override fun toString(): String = "Object(type=$type, name=$name, actor=$actor, id=$id) ${super.toString()}" override fun toString(): String = "Object(type=$type) ${super.toString()}"
companion object { companion object {
@JvmStatic @JvmStatic

View File

@ -15,9 +15,6 @@ class ObjectDeserializer : JsonDeserializer<Object>() {
if (treeNode.isValueNode) { if (treeNode.isValueNode) {
return ObjectValue( return ObjectValue(
emptyList(), emptyList(),
null,
null,
null,
treeNode.asText() treeNode.asText()
) )
} else if (treeNode.isObject) { } else if (treeNode.isObject) {
@ -33,15 +30,8 @@ class ObjectDeserializer : JsonDeserializer<Object>() {
} }
return when (activityType) { return when (activityType) {
ExtendedActivityVocabulary.Follow -> { ExtendedActivityVocabulary.Follow -> p.codec.treeToValue(treeNode, Follow::class.java)
val readValue = p.codec.treeToValue(treeNode, Follow::class.java) ExtendedActivityVocabulary.Note -> p.codec.treeToValue(treeNode, Note::class.java)
readValue
}
ExtendedActivityVocabulary.Note -> {
p.codec.treeToValue(treeNode, Note::class.java)
}
ExtendedActivityVocabulary.Object -> p.codec.treeToValue(treeNode, Object::class.java) ExtendedActivityVocabulary.Object -> p.codec.treeToValue(treeNode, Object::class.java)
ExtendedActivityVocabulary.Link -> TODO() ExtendedActivityVocabulary.Link -> TODO()
ExtendedActivityVocabulary.Activity -> TODO() ExtendedActivityVocabulary.Activity -> TODO()

View File

@ -1,33 +1,27 @@
package dev.usbharu.hideout.activitypub.domain.model.objects package dev.usbharu.hideout.activitypub.domain.model.objects
import com.fasterxml.jackson.annotation.JsonCreator
@Suppress("VariableNaming") @Suppress("VariableNaming")
open class ObjectValue : Object { open class ObjectValue @JsonCreator constructor(type: List<String>, var `object`: String) : Object(
type
var `object`: String? = null ) {
protected constructor() : super()
constructor(type: List<String>, name: String?, actor: String?, id: String?, `object`: String?) : super(
type,
name,
actor,
id
) {
this.`object` = `object`
}
override fun equals(other: Any?): Boolean { override fun equals(other: Any?): Boolean {
if (this === other) return true if (this === other) return true
if (other !is ObjectValue) return false if (javaClass != other?.javaClass) return false
if (!super.equals(other)) return false if (!super.equals(other)) return false
other as ObjectValue
return `object` == other.`object` return `object` == other.`object`
} }
override fun hashCode(): Int { override fun hashCode(): Int {
var result = super.hashCode() var result = super.hashCode()
result = 31 * result + (`object`?.hashCode() ?: 0) result = 31 * result + `object`.hashCode()
return result return result
} }
override fun toString(): String = "ObjectValue(`object`=$`object`) ${super.toString()}" override fun toString(): String = "ObjectValue(`object`='$`object`') ${super.toString()}"
} }

View File

@ -64,7 +64,6 @@ class NoteQueryServiceImpl(private val postRepository: PostRepository, private v
this[Users.followers] this[Users.followers]
) )
return Note( return Note(
name = "Post",
id = this[Posts.apId], id = this[Posts.apId],
attributedTo = this[Users.url], attributedTo = this[Users.url],
content = this[Posts.text], content = this[Posts.text],
@ -80,7 +79,7 @@ class NoteQueryServiceImpl(private val postRepository: PostRepository, private v
private suspend fun Query.toNote(): Note { private suspend fun Query.toNote(): Note {
return this.groupBy { it[Posts.id] } return this.groupBy { it[Posts.id] }
.map { it.value } .map { it.value }
.map { it.first().toNote(it.mapNotNull { it.toMediaOrNull() }) } .map { it.first().toNote(it.mapNotNull { resultRow -> resultRow.toMediaOrNull() }) }
.singleOr { FailedToGetResourcesException("resource does not exist.") } .singleOr { FailedToGetResourcesException("resource does not exist.") }
} }

View File

@ -1,68 +0,0 @@
package dev.usbharu.hideout.activitypub.interfaces.api.common
import dev.usbharu.hideout.activitypub.domain.model.JsonLd
import dev.usbharu.hideout.util.HttpUtil.Activity
import io.ktor.http.*
sealed class ActivityPubResponse(
val httpStatusCode: HttpStatusCode,
val contentType: ContentType = ContentType.Application.Activity
) {
override fun equals(other: Any?): Boolean {
if (this === other) return true
if (other !is ActivityPubResponse) return false
if (httpStatusCode != other.httpStatusCode) return false
if (contentType != other.contentType) return false
return true
}
override fun hashCode(): Int {
var result = httpStatusCode.hashCode()
result = 31 * result + contentType.hashCode()
return result
}
override fun toString(): String = "ActivityPubResponse(httpStatusCode=$httpStatusCode, contentType=$contentType)"
}
class ActivityPubStringResponse(
httpStatusCode: HttpStatusCode = HttpStatusCode.OK,
val message: String,
contentType: ContentType = ContentType.Application.Activity
) : ActivityPubResponse(httpStatusCode, contentType) {
override fun equals(other: Any?): Boolean {
if (this === other) return true
if (other !is ActivityPubStringResponse) return false
if (message != other.message) return false
return true
}
override fun hashCode(): Int = message.hashCode()
override fun toString(): String = "ActivityPubStringResponse(message='$message') ${super.toString()}"
}
class ActivityPubObjectResponse(
httpStatusCode: HttpStatusCode = HttpStatusCode.OK,
val message: JsonLd,
contentType: ContentType = ContentType.Application.Activity
) : ActivityPubResponse(httpStatusCode, contentType) {
override fun equals(other: Any?): Boolean {
if (this === other) return true
if (other !is ActivityPubObjectResponse) return false
if (message != other.message) return false
return true
}
override fun hashCode(): Int = message.hashCode()
override fun toString(): String = "ActivityPubObjectResponse(message=$message) ${super.toString()}"
}

View File

@ -15,6 +15,7 @@ interface InboxController {
"application/activity+json", "application/activity+json",
"application/ld+json; profile=\"https://www.w3.org/ns/activitystreams\"" "application/ld+json; profile=\"https://www.w3.org/ns/activitystreams\""
], ],
consumes = ["application/json", "application/*+json"],
method = [RequestMethod.POST] method = [RequestMethod.POST]
) )
suspend fun inbox(@RequestBody string: String): ResponseEntity<Unit> suspend fun inbox(@RequestBody string: String): ResponseEntity<Unit>

View File

@ -1,16 +1,39 @@
package dev.usbharu.hideout.activitypub.interfaces.api.inbox package dev.usbharu.hideout.activitypub.interfaces.api.inbox
import dev.usbharu.hideout.activitypub.service.common.APService import dev.usbharu.hideout.activitypub.service.common.APService
import dev.usbharu.httpsignature.common.HttpHeaders
import dev.usbharu.httpsignature.common.HttpMethod
import dev.usbharu.httpsignature.common.HttpRequest
import org.slf4j.LoggerFactory import org.slf4j.LoggerFactory
import org.springframework.http.HttpHeaders.WWW_AUTHENTICATE
import org.springframework.http.HttpStatus import org.springframework.http.HttpStatus
import org.springframework.http.ResponseEntity import org.springframework.http.ResponseEntity
import org.springframework.web.bind.annotation.RequestBody import org.springframework.web.bind.annotation.RequestBody
import org.springframework.web.bind.annotation.RestController import org.springframework.web.bind.annotation.RestController
import org.springframework.web.context.request.RequestContextHolder
import org.springframework.web.context.request.ServletRequestAttributes
import java.net.URL
@RestController @RestController
class InboxControllerImpl(private val apService: APService) : InboxController { class InboxControllerImpl(private val apService: APService) : InboxController {
@Suppress("TooGenericExceptionCaught") @Suppress("TooGenericExceptionCaught")
override suspend fun inbox(@RequestBody string: String): ResponseEntity<Unit> { override suspend fun inbox(
@RequestBody string: String
): ResponseEntity<Unit> {
val request = (requireNotNull(RequestContextHolder.getRequestAttributes()) as ServletRequestAttributes).request
val headersList = request.headerNames?.toList().orEmpty()
LOGGER.trace("Inbox Headers {}", headersList)
if (headersList.map { it.lowercase() }.contains("signature").not()) {
return ResponseEntity.status(HttpStatus.UNAUTHORIZED)
.header(
WWW_AUTHENTICATE,
"Signature realm=\"Example\",headers=\"(request-target) date host digest\""
)
.build()
}
val parseActivity = try { val parseActivity = try {
apService.parseActivity(string) apService.parseActivity(string)
} catch (e: Exception) { } catch (e: Exception) {
@ -19,7 +42,29 @@ class InboxControllerImpl(private val apService: APService) : InboxController {
} }
LOGGER.info("INBOX Processing Activity Type: {}", parseActivity) LOGGER.info("INBOX Processing Activity Type: {}", parseActivity)
try { try {
apService.processActivity(string, parseActivity) val url = request.requestURL.toString()
val headers =
headersList.associateWith { header -> request.getHeaders(header)?.toList().orEmpty() }
val method = when (val method = request.method.lowercase()) {
"get" -> HttpMethod.GET
"post" -> HttpMethod.POST
else -> {
throw IllegalArgumentException("Unsupported method: $method")
}
}
apService.processActivity(
string,
parseActivity,
HttpRequest(
URL(url + request.queryString.orEmpty()),
HttpHeaders(headers),
method
),
headers
)
} catch (e: Exception) { } catch (e: Exception) {
LOGGER.warn("FAILED Process Activity $parseActivity", e) LOGGER.warn("FAILED Process Activity $parseActivity", e)
return ResponseEntity(HttpStatus.ACCEPTED) return ResponseEntity(HttpStatus.ACCEPTED)

View File

@ -1,57 +0,0 @@
package dev.usbharu.hideout.activitypub.service.activity.accept
import dev.usbharu.hideout.activitypub.domain.exception.IllegalActivityPubObjectException
import dev.usbharu.hideout.activitypub.domain.model.Accept
import dev.usbharu.hideout.activitypub.domain.model.Follow
import dev.usbharu.hideout.activitypub.interfaces.api.common.ActivityPubResponse
import dev.usbharu.hideout.activitypub.interfaces.api.common.ActivityPubStringResponse
import dev.usbharu.hideout.application.external.Transaction
import dev.usbharu.hideout.core.query.FollowerQueryService
import dev.usbharu.hideout.core.query.UserQueryService
import dev.usbharu.hideout.core.service.user.UserService
import io.ktor.http.*
import org.slf4j.LoggerFactory
import org.springframework.stereotype.Service
interface APAcceptService {
suspend fun receiveAccept(accept: Accept): ActivityPubResponse
}
@Service
class APAcceptServiceImpl(
private val userService: UserService,
private val userQueryService: UserQueryService,
private val followerQueryService: FollowerQueryService,
private val transaction: Transaction
) : APAcceptService {
override suspend fun receiveAccept(accept: Accept): ActivityPubResponse {
return transaction.transaction {
LOGGER.debug("START Follow")
LOGGER.trace("{}", accept)
val value = accept.`object` ?: throw IllegalActivityPubObjectException("object is null")
if (value.type.contains("Follow").not()) {
LOGGER.warn("FAILED Activity type is not 'Follow'")
throw IllegalActivityPubObjectException("Invalid type ${value.type}")
}
val follow = value as Follow
val userUrl = follow.`object` ?: throw IllegalActivityPubObjectException("object is null")
val followerUrl = follow.actor ?: throw IllegalActivityPubObjectException("actor is null")
val user = userQueryService.findByUrl(userUrl)
val follower = userQueryService.findByUrl(followerUrl)
if (followerQueryService.alreadyFollow(user.id, follower.id)) {
LOGGER.debug("END User already follow from ${follower.url} to ${user.url}")
return@transaction ActivityPubStringResponse(HttpStatusCode.OK, "accepted")
}
userService.follow(user.id, follower.id)
LOGGER.debug("SUCCESS Follow from ${follower.url} to ${user.url}.")
ActivityPubStringResponse(HttpStatusCode.OK, "accepted")
}
}
companion object {
private val LOGGER = LoggerFactory.getLogger(APAcceptServiceImpl::class.java)
}
}

View File

@ -0,0 +1,52 @@
package dev.usbharu.hideout.activitypub.service.activity.accept
import dev.usbharu.hideout.activitypub.domain.exception.IllegalActivityPubObjectException
import dev.usbharu.hideout.activitypub.domain.model.Accept
import dev.usbharu.hideout.activitypub.domain.model.Follow
import dev.usbharu.hideout.activitypub.service.common.AbstractActivityPubProcessor
import dev.usbharu.hideout.activitypub.service.common.ActivityPubProcessContext
import dev.usbharu.hideout.activitypub.service.common.ActivityType
import dev.usbharu.hideout.application.external.Transaction
import dev.usbharu.hideout.core.query.FollowerQueryService
import dev.usbharu.hideout.core.query.UserQueryService
import dev.usbharu.hideout.core.service.user.UserService
import org.springframework.stereotype.Service
@Service
class ApAcceptProcessor(
transaction: Transaction,
private val userQueryService: UserQueryService,
private val followerQueryService: FollowerQueryService,
private val userService: UserService
) :
AbstractActivityPubProcessor<Accept>(transaction) {
override suspend fun internalProcess(activity: ActivityPubProcessContext<Accept>) {
val value = activity.activity.apObject ?: throw IllegalActivityPubObjectException("object is null")
if (value.type.contains("Follow").not()) {
logger.warn("FAILED Activity type isn't Follow.")
throw IllegalActivityPubObjectException("Invalid type ${value.type}")
}
val follow = value as Follow
val userUrl = follow.apObject
val followerUrl = follow.actor
val user = userQueryService.findByUrl(userUrl)
val follower = userQueryService.findByUrl(followerUrl)
if (followerQueryService.alreadyFollow(user.id, follower.id)) {
logger.debug("END User already follow from ${follower.url} to ${user.url}.")
return
}
userService.follow(user.id, follower.id)
logger.debug("SUCCESS Follow from ${follower.url} to ${user.url}.")
}
override fun isSupported(activityType: ActivityType): Boolean = activityType == ActivityType.Accept
override fun type(): Class<Accept> = Accept::class.java
}

View File

@ -1,44 +0,0 @@
package dev.usbharu.hideout.activitypub.service.activity.create
import dev.usbharu.hideout.activitypub.domain.exception.IllegalActivityPubObjectException
import dev.usbharu.hideout.activitypub.domain.model.Create
import dev.usbharu.hideout.activitypub.domain.model.Note
import dev.usbharu.hideout.activitypub.interfaces.api.common.ActivityPubResponse
import dev.usbharu.hideout.activitypub.interfaces.api.common.ActivityPubStringResponse
import dev.usbharu.hideout.activitypub.service.objects.note.APNoteService
import dev.usbharu.hideout.application.external.Transaction
import io.ktor.http.*
import org.slf4j.LoggerFactory
import org.springframework.stereotype.Service
interface APCreateService {
suspend fun receiveCreate(create: Create): ActivityPubResponse
}
@Service
class APCreateServiceImpl(
private val apNoteService: APNoteService,
private val transaction: Transaction
) : APCreateService {
override suspend fun receiveCreate(create: Create): ActivityPubResponse {
LOGGER.debug("START Create new remote note.")
LOGGER.trace("{}", create)
val value = create.`object` ?: throw IllegalActivityPubObjectException("object is null")
if (value.type.contains("Note").not()) {
LOGGER.warn("FAILED Object type is not 'Note'")
throw IllegalActivityPubObjectException("object is not Note")
}
return transaction.transaction {
val note = value as Note
apNoteService.fetchNote(note)
LOGGER.debug("SUCCESS Create new remote note. ${note.id} by ${note.attributedTo}")
ActivityPubStringResponse(HttpStatusCode.OK, "Created")
}
}
companion object {
private val LOGGER = LoggerFactory.getLogger(APCreateServiceImpl::class.java)
}
}

View File

@ -33,7 +33,7 @@ class ApSendCreateServiceImpl(
val note = noteQueryService.findById(post.id).first val note = noteQueryService.findById(post.id).first
val create = Create( val create = Create(
name = "Create Note", name = "Create Note",
`object` = note, apObject = note,
actor = note.attributedTo, actor = note.attributedTo,
id = "${applicationConfig.url}/create/note/${post.id}" id = "${applicationConfig.url}/create/note/${post.id}"
) )

View File

@ -0,0 +1,22 @@
package dev.usbharu.hideout.activitypub.service.activity.create
import dev.usbharu.hideout.activitypub.domain.model.Create
import dev.usbharu.hideout.activitypub.domain.model.Note
import dev.usbharu.hideout.activitypub.service.common.AbstractActivityPubProcessor
import dev.usbharu.hideout.activitypub.service.common.ActivityPubProcessContext
import dev.usbharu.hideout.activitypub.service.common.ActivityType
import dev.usbharu.hideout.activitypub.service.objects.note.APNoteService
import dev.usbharu.hideout.application.external.Transaction
import org.springframework.stereotype.Service
@Service
class CreateActivityProcessor(transaction: Transaction, private val apNoteService: APNoteService) :
AbstractActivityPubProcessor<Create>(transaction) {
override suspend fun internalProcess(activity: ActivityPubProcessContext<Create>) {
apNoteService.fetchNote(activity.activity.apObject as Note)
}
override fun isSupported(activityType: ActivityType): Boolean = activityType == ActivityType.Create
override fun type(): Class<Create> = Create::class.java
}

View File

@ -0,0 +1,42 @@
package dev.usbharu.hideout.activitypub.service.activity.delete
import dev.usbharu.hideout.activitypub.domain.exception.IllegalActivityPubObjectException
import dev.usbharu.hideout.activitypub.domain.model.Delete
import dev.usbharu.hideout.activitypub.domain.model.HasId
import dev.usbharu.hideout.activitypub.service.common.AbstractActivityPubProcessor
import dev.usbharu.hideout.activitypub.service.common.ActivityPubProcessContext
import dev.usbharu.hideout.activitypub.service.common.ActivityType
import dev.usbharu.hideout.application.external.Transaction
import dev.usbharu.hideout.core.domain.exception.FailedToGetResourcesException
import dev.usbharu.hideout.core.domain.model.post.PostRepository
import dev.usbharu.hideout.core.query.PostQueryService
import org.springframework.stereotype.Service
@Service
class APDeleteProcessor(
transaction: Transaction,
private val postQueryService: PostQueryService,
private val postRepository: PostRepository
) :
AbstractActivityPubProcessor<Delete>(transaction) {
override suspend fun internalProcess(activity: ActivityPubProcessContext<Delete>) {
val value = activity.activity.apObject
if (value !is HasId) {
throw IllegalActivityPubObjectException("object hasn't id")
}
val deleteId = value.id
val post = try {
postQueryService.findByApId(deleteId)
} catch (e: FailedToGetResourcesException) {
logger.warn("FAILED delete id: {} is not found.", deleteId, e)
return
}
postRepository.delete(post.id)
}
override fun isSupported(activityType: ActivityType): Boolean = activityType == ActivityType.Delete
override fun type(): Class<Delete> = Delete::class.java
}

View File

@ -1,8 +0,0 @@
package dev.usbharu.hideout.activitypub.service.activity.delete
import dev.usbharu.hideout.activitypub.domain.model.Delete
import dev.usbharu.hideout.activitypub.interfaces.api.common.ActivityPubResponse
interface APReceiveDeleteService {
suspend fun receiveDelete(delete: Delete): ActivityPubResponse
}

View File

@ -1,31 +0,0 @@
package dev.usbharu.hideout.activitypub.service.activity.delete
import dev.usbharu.hideout.activitypub.domain.exception.IllegalActivityPubObjectException
import dev.usbharu.hideout.activitypub.domain.model.Delete
import dev.usbharu.hideout.activitypub.interfaces.api.common.ActivityPubResponse
import dev.usbharu.hideout.activitypub.interfaces.api.common.ActivityPubStringResponse
import dev.usbharu.hideout.application.external.Transaction
import dev.usbharu.hideout.core.domain.exception.FailedToGetResourcesException
import dev.usbharu.hideout.core.domain.model.post.PostRepository
import dev.usbharu.hideout.core.query.PostQueryService
import io.ktor.http.*
import org.springframework.stereotype.Service
@Service
class APReceiveDeleteServiceImpl(
private val postQueryService: PostQueryService,
private val postRepository: PostRepository,
private val transaction: Transaction
) : APReceiveDeleteService {
override suspend fun receiveDelete(delete: Delete): ActivityPubResponse = transaction.transaction {
val deleteId = delete.`object`?.id ?: throw IllegalActivityPubObjectException("object.id is null")
val post = try {
postQueryService.findByApId(deleteId)
} catch (_: FailedToGetResourcesException) {
return@transaction ActivityPubStringResponse(HttpStatusCode.OK, "Resource not found or already deleted")
}
postRepository.delete(post.id)
return@transaction ActivityPubStringResponse(HttpStatusCode.OK, "Resource was deleted.")
}
}

View File

@ -0,0 +1,36 @@
package dev.usbharu.hideout.activitypub.service.activity.follow
import com.fasterxml.jackson.databind.ObjectMapper
import dev.usbharu.hideout.activitypub.domain.model.Follow
import dev.usbharu.hideout.activitypub.service.common.AbstractActivityPubProcessor
import dev.usbharu.hideout.activitypub.service.common.ActivityPubProcessContext
import dev.usbharu.hideout.activitypub.service.common.ActivityType
import dev.usbharu.hideout.application.external.Transaction
import dev.usbharu.hideout.core.external.job.ReceiveFollowJob
import dev.usbharu.hideout.core.external.job.ReceiveFollowJobParam
import dev.usbharu.hideout.core.service.job.JobQueueParentService
import org.springframework.stereotype.Service
@Service
class APFollowProcessor(
transaction: Transaction,
private val jobQueueParentService: JobQueueParentService,
private val objectMapper: ObjectMapper
) :
AbstractActivityPubProcessor<Follow>(transaction) {
override suspend fun internalProcess(activity: ActivityPubProcessContext<Follow>) {
logger.info("FOLLOW from: {} to {}", activity.activity.actor, activity.activity.apObject)
// inboxをジョブキューに乗せているので既に不要だが、フォロー承認制アカウントを実装する際に必要なので残す
val jobProps = ReceiveFollowJobParam(
activity.activity.actor,
objectMapper.writeValueAsString(activity.activity),
activity.activity.apObject
)
jobQueueParentService.scheduleTypeSafe(ReceiveFollowJob, jobProps)
}
override fun isSupported(activityType: ActivityType): Boolean = activityType == ActivityType.Follow
override fun type(): Class<Follow> = Follow::class.java
}

View File

@ -0,0 +1,61 @@
package dev.usbharu.hideout.activitypub.service.activity.follow
import com.fasterxml.jackson.databind.ObjectMapper
import com.fasterxml.jackson.module.kotlin.readValue
import dev.usbharu.hideout.activitypub.domain.model.Accept
import dev.usbharu.hideout.activitypub.domain.model.Follow
import dev.usbharu.hideout.activitypub.service.common.APRequestService
import dev.usbharu.hideout.activitypub.service.objects.user.APUserService
import dev.usbharu.hideout.application.external.Transaction
import dev.usbharu.hideout.core.external.job.ReceiveFollowJob
import dev.usbharu.hideout.core.external.job.ReceiveFollowJobParam
import dev.usbharu.hideout.core.query.UserQueryService
import dev.usbharu.hideout.core.service.job.JobProcessor
import dev.usbharu.hideout.core.service.user.UserService
import org.slf4j.LoggerFactory
import org.springframework.stereotype.Service
@Service
class APReceiveFollowJobProcessor(
private val transaction: Transaction,
private val userQueryService: UserQueryService,
private val apUserService: APUserService,
private val objectMapper: ObjectMapper,
private val apRequestService: APRequestService,
private val userService: UserService
) :
JobProcessor<ReceiveFollowJobParam, ReceiveFollowJob> {
override suspend fun process(param: ReceiveFollowJobParam) = transaction.transaction {
val person = apUserService.fetchPerson(param.actor, param.targetActor)
val follow = objectMapper.readValue<Follow>(param.follow)
logger.info("START Follow from: {} to {}", param.targetActor, param.actor)
val signer = userQueryService.findByUrl(param.targetActor)
val urlString = person.inbox
apRequestService.apPost(
url = urlString,
body = Accept(
name = "Follow",
apObject = follow,
actor = param.targetActor
),
signer = signer
)
val targetEntity = userQueryService.findByUrl(param.targetActor)
val followActorEntity =
userQueryService.findByUrl(follow.actor)
userService.followRequest(targetEntity.id, followActorEntity.id)
logger.info("SUCCESS Follow from: {} to: {}", param.targetActor, param.actor)
}
override fun job(): ReceiveFollowJob = ReceiveFollowJob
companion object {
private val logger = LoggerFactory.getLogger(APReceiveFollowJobProcessor::class.java)
}
}

View File

@ -1,8 +0,0 @@
package dev.usbharu.hideout.activitypub.service.activity.follow
import dev.usbharu.hideout.core.external.job.ReceiveFollowJob
import kjob.core.job.JobProps
interface APReceiveFollowJobService {
suspend fun receiveFollowJob(props: JobProps<ReceiveFollowJob>)
}

View File

@ -1,61 +0,0 @@
package dev.usbharu.hideout.activitypub.service.activity.follow
import com.fasterxml.jackson.databind.ObjectMapper
import com.fasterxml.jackson.module.kotlin.readValue
import dev.usbharu.hideout.activitypub.domain.model.Accept
import dev.usbharu.hideout.activitypub.domain.model.Follow
import dev.usbharu.hideout.activitypub.service.common.APRequestService
import dev.usbharu.hideout.activitypub.service.objects.user.APUserService
import dev.usbharu.hideout.application.external.Transaction
import dev.usbharu.hideout.core.external.job.ReceiveFollowJob
import dev.usbharu.hideout.core.query.UserQueryService
import dev.usbharu.hideout.core.service.user.UserService
import kjob.core.job.JobProps
import org.slf4j.LoggerFactory
import org.springframework.beans.factory.annotation.Qualifier
import org.springframework.stereotype.Component
@Component
class APReceiveFollowJobServiceImpl(
private val apUserService: APUserService,
private val userQueryService: UserQueryService,
private val apRequestService: APRequestService,
private val userService: UserService,
@Qualifier("activitypub") private val objectMapper: ObjectMapper,
private val transaction: Transaction
) : APReceiveFollowJobService {
override suspend fun receiveFollowJob(props: JobProps<ReceiveFollowJob>) {
transaction.transaction {
val actor = props[ReceiveFollowJob.actor]
val targetActor = props[ReceiveFollowJob.targetActor]
val person = apUserService.fetchPerson(actor, targetActor)
val follow = objectMapper.readValue<Follow>(props[ReceiveFollowJob.follow])
logger.info("START Follow from: {} to: {}", targetActor, actor)
val signer = userQueryService.findByUrl(targetActor)
val urlString = person.inbox ?: throw IllegalArgumentException("inbox is not found")
apRequestService.apPost(
url = urlString,
body = Accept(
name = "Follow",
`object` = follow,
actor = targetActor
),
signer = signer
)
val targetEntity = userQueryService.findByUrl(targetActor)
val followActorEntity =
userQueryService.findByUrl(follow.actor ?: throw java.lang.IllegalArgumentException("Actor is null"))
userService.followRequest(targetEntity.id, followActorEntity.id)
logger.info("SUCCESS Follow from: {} to: {}", targetActor, actor)
}
}
companion object {
private val logger = LoggerFactory.getLogger(APReceiveFollowJobServiceImpl::class.java)
}
}

View File

@ -2,17 +2,14 @@ package dev.usbharu.hideout.activitypub.service.activity.follow
import com.fasterxml.jackson.databind.ObjectMapper import com.fasterxml.jackson.databind.ObjectMapper
import dev.usbharu.hideout.activitypub.domain.model.Follow import dev.usbharu.hideout.activitypub.domain.model.Follow
import dev.usbharu.hideout.activitypub.interfaces.api.common.ActivityPubResponse
import dev.usbharu.hideout.activitypub.interfaces.api.common.ActivityPubStringResponse
import dev.usbharu.hideout.core.external.job.ReceiveFollowJob import dev.usbharu.hideout.core.external.job.ReceiveFollowJob
import dev.usbharu.hideout.core.service.job.JobQueueParentService import dev.usbharu.hideout.core.service.job.JobQueueParentService
import io.ktor.http.*
import org.slf4j.LoggerFactory import org.slf4j.LoggerFactory
import org.springframework.beans.factory.annotation.Qualifier import org.springframework.beans.factory.annotation.Qualifier
import org.springframework.stereotype.Service import org.springframework.stereotype.Service
interface APReceiveFollowService { interface APReceiveFollowService {
suspend fun receiveFollow(follow: Follow): ActivityPubResponse suspend fun receiveFollow(follow: Follow)
} }
@Service @Service
@ -20,14 +17,14 @@ class APReceiveFollowServiceImpl(
private val jobQueueParentService: JobQueueParentService, private val jobQueueParentService: JobQueueParentService,
@Qualifier("activitypub") private val objectMapper: ObjectMapper @Qualifier("activitypub") private val objectMapper: ObjectMapper
) : APReceiveFollowService { ) : APReceiveFollowService {
override suspend fun receiveFollow(follow: Follow): ActivityPubResponse { override suspend fun receiveFollow(follow: Follow) {
logger.info("FOLLOW from: {} to: {}", follow.actor, follow.`object`) logger.info("FOLLOW from: {} to: {}", follow.actor, follow.apObject)
jobQueueParentService.schedule(ReceiveFollowJob) { jobQueueParentService.schedule(ReceiveFollowJob) {
props[ReceiveFollowJob.actor] = follow.actor props[ReceiveFollowJob.actor] = follow.actor
props[ReceiveFollowJob.follow] = objectMapper.writeValueAsString(follow) props[ReceiveFollowJob.follow] = objectMapper.writeValueAsString(follow)
props[ReceiveFollowJob.targetActor] = follow.`object` props[ReceiveFollowJob.targetActor] = follow.apObject
} }
return ActivityPubStringResponse(HttpStatusCode.OK, "{}", ContentType.Application.Json) return
} }
companion object { companion object {

View File

@ -15,8 +15,7 @@ class APSendFollowServiceImpl(
) : APSendFollowService { ) : APSendFollowService {
override suspend fun sendFollow(sendFollowDto: SendFollowDto) { override suspend fun sendFollow(sendFollowDto: SendFollowDto) {
val follow = Follow( val follow = Follow(
name = "Follow", apObject = sendFollowDto.followTargetUserId.url,
`object` = sendFollowDto.followTargetUserId.url,
actor = sendFollowDto.userId.url actor = sendFollowDto.userId.url
) )

View File

@ -0,0 +1,55 @@
package dev.usbharu.hideout.activitypub.service.activity.like
import dev.usbharu.hideout.activitypub.domain.exception.FailedToGetActivityPubResourceException
import dev.usbharu.hideout.activitypub.domain.model.Like
import dev.usbharu.hideout.activitypub.service.common.AbstractActivityPubProcessor
import dev.usbharu.hideout.activitypub.service.common.ActivityPubProcessContext
import dev.usbharu.hideout.activitypub.service.common.ActivityType
import dev.usbharu.hideout.activitypub.service.objects.note.APNoteService
import dev.usbharu.hideout.activitypub.service.objects.user.APUserService
import dev.usbharu.hideout.application.external.Transaction
import dev.usbharu.hideout.core.query.PostQueryService
import dev.usbharu.hideout.core.service.reaction.ReactionService
import org.springframework.stereotype.Service
@Service
class APLikeProcessor(
transaction: Transaction,
private val apUserService: APUserService,
private val apNoteService: APNoteService,
private val postQueryService: PostQueryService,
private val reactionService: ReactionService
) :
AbstractActivityPubProcessor<Like>(transaction) {
override suspend fun internalProcess(activity: ActivityPubProcessContext<Like>) {
val actor = activity.activity.actor
val content = activity.activity.content
val target = activity.activity.apObject
val personWithEntity = apUserService.fetchPersonWithEntity(actor)
try {
apNoteService.fetchNoteAsync(target).await()
} catch (e: FailedToGetActivityPubResourceException) {
logger.debug("FAILED failed to get {}", target)
logger.trace("", e)
return
}
val post = postQueryService.findByUrl(target)
reactionService.receiveReaction(
content,
actor.substringAfter("://").substringBefore("/"),
personWithEntity.second.id,
post.id
)
logger.debug("SUCCESS Add Like($content) from ${personWithEntity.second.url} to ${post.url}")
}
override fun isSupported(activityType: ActivityType): Boolean = activityType == ActivityType.Like
override fun type(): Class<Like> = Like::class.java
}

View File

@ -1,66 +0,0 @@
package dev.usbharu.hideout.activitypub.service.activity.like
import dev.usbharu.hideout.activitypub.domain.exception.FailedToGetActivityPubResourceException
import dev.usbharu.hideout.activitypub.domain.exception.IllegalActivityPubObjectException
import dev.usbharu.hideout.activitypub.domain.model.Like
import dev.usbharu.hideout.activitypub.interfaces.api.common.ActivityPubResponse
import dev.usbharu.hideout.activitypub.interfaces.api.common.ActivityPubStringResponse
import dev.usbharu.hideout.activitypub.service.objects.note.APNoteService
import dev.usbharu.hideout.activitypub.service.objects.user.APUserService
import dev.usbharu.hideout.application.external.Transaction
import dev.usbharu.hideout.core.query.PostQueryService
import dev.usbharu.hideout.core.service.reaction.ReactionService
import io.ktor.http.*
import org.slf4j.LoggerFactory
import org.springframework.stereotype.Service
interface APLikeService {
suspend fun receiveLike(like: Like): ActivityPubResponse
}
@Service
class APLikeServiceImpl(
private val reactionService: ReactionService,
private val apUserService: APUserService,
private val apNoteService: APNoteService,
private val postQueryService: PostQueryService,
private val transaction: Transaction
) : APLikeService {
override suspend fun receiveLike(like: Like): ActivityPubResponse {
LOGGER.debug("START Add Like")
LOGGER.trace("{}", like)
val actor = like.actor ?: throw IllegalActivityPubObjectException("actor is null")
val content = like.content ?: throw IllegalActivityPubObjectException("content is null")
like.`object` ?: throw IllegalActivityPubObjectException("object is null")
transaction.transaction {
LOGGER.trace("FETCH Liked Person $actor")
val person = apUserService.fetchPersonWithEntity(actor)
LOGGER.trace("{}", person.second)
LOGGER.trace("FETCH Liked Note ${like.`object`}")
try {
apNoteService.fetchNoteAsync(like.`object` ?: return@transaction).await()
} catch (e: FailedToGetActivityPubResourceException) {
LOGGER.debug("FAILED Failed to Get ${like.`object`}")
LOGGER.trace("", e)
return@transaction
}
val post = postQueryService.findByUrl(like.`object` ?: return@transaction)
LOGGER.trace("{}", post)
reactionService.receiveReaction(
content,
actor.substringAfter("://").substringBefore("/"),
person.second.id,
post.id
)
LOGGER.debug("SUCCESS Add Like($content) from ${person.second.url} to ${post.url}")
}
return ActivityPubStringResponse(HttpStatusCode.OK, "")
}
companion object {
private val LOGGER = LoggerFactory.getLogger(APLikeServiceImpl::class.java)
}
}

View File

@ -0,0 +1,36 @@
package dev.usbharu.hideout.activitypub.service.activity.like
import dev.usbharu.hideout.activitypub.domain.model.Like
import dev.usbharu.hideout.activitypub.service.common.APRequestService
import dev.usbharu.hideout.application.config.ApplicationConfig
import dev.usbharu.hideout.application.external.Transaction
import dev.usbharu.hideout.core.external.job.DeliverReactionJob
import dev.usbharu.hideout.core.external.job.DeliverReactionJobParam
import dev.usbharu.hideout.core.query.UserQueryService
import dev.usbharu.hideout.core.service.job.JobProcessor
import org.springframework.stereotype.Service
@Service
class ApReactionJobProcessor(
private val userQueryService: UserQueryService,
private val apRequestService: APRequestService,
private val applicationConfig: ApplicationConfig,
private val transaction: Transaction
) : JobProcessor<DeliverReactionJobParam, DeliverReactionJob> {
override suspend fun process(param: DeliverReactionJobParam): Unit = transaction.transaction {
val signer = userQueryService.findByUrl(param.actor)
apRequestService.apPost(
param.inbox,
Like(
actor = param.actor,
apObject = param.postUrl,
id = "${applicationConfig.url}/liek/note/${param.id}",
content = param.reaction
),
signer
)
}
override fun job(): DeliverReactionJob = DeliverReactionJob
}

View File

@ -1,10 +0,0 @@
package dev.usbharu.hideout.activitypub.service.activity.like
import dev.usbharu.hideout.core.external.job.DeliverReactionJob
import dev.usbharu.hideout.core.external.job.DeliverRemoveReactionJob
import kjob.core.job.JobProps
interface ApReactionJobService {
suspend fun reactionJob(props: JobProps<DeliverReactionJob>)
suspend fun removeReactionJob(props: JobProps<DeliverRemoveReactionJob>)
}

View File

@ -1,66 +0,0 @@
package dev.usbharu.hideout.activitypub.service.activity.like
import com.fasterxml.jackson.databind.ObjectMapper
import com.fasterxml.jackson.module.kotlin.readValue
import dev.usbharu.hideout.activitypub.domain.model.Like
import dev.usbharu.hideout.activitypub.domain.model.Undo
import dev.usbharu.hideout.activitypub.service.common.APRequestService
import dev.usbharu.hideout.application.config.ApplicationConfig
import dev.usbharu.hideout.core.external.job.DeliverReactionJob
import dev.usbharu.hideout.core.external.job.DeliverRemoveReactionJob
import dev.usbharu.hideout.core.query.UserQueryService
import kjob.core.job.JobProps
import org.springframework.beans.factory.annotation.Qualifier
import org.springframework.stereotype.Service
import java.time.Instant
@Service
class ApReactionJobServiceImpl(
private val userQueryService: UserQueryService,
private val apRequestService: APRequestService,
private val applicationConfig: ApplicationConfig,
@Qualifier("activitypub") private val objectMapper: ObjectMapper
) : ApReactionJobService {
override suspend fun reactionJob(props: JobProps<DeliverReactionJob>) {
val inbox = props[DeliverReactionJob.inbox]
val actor = props[DeliverReactionJob.actor]
val postUrl = props[DeliverReactionJob.postUrl]
val id = props[DeliverReactionJob.id]
val content = props[DeliverReactionJob.reaction]
val signer = userQueryService.findByUrl(actor)
apRequestService.apPost(
inbox,
Like(
name = "Like",
actor = actor,
`object` = postUrl,
id = "${applicationConfig.url}/like/note/$id",
content = content
),
signer
)
}
override suspend fun removeReactionJob(props: JobProps<DeliverRemoveReactionJob>) {
val inbox = props[DeliverRemoveReactionJob.inbox]
val actor = props[DeliverRemoveReactionJob.actor]
val like = objectMapper.readValue<Like>(props[DeliverRemoveReactionJob.like])
val id = props[DeliverRemoveReactionJob.id]
val signer = userQueryService.findByUrl(actor)
apRequestService.apPost(
inbox,
Undo(
name = "Undo Reaction",
actor = actor,
`object` = like,
id = "${applicationConfig.url}/undo/note/$id",
published = Instant.now()
),
signer
)
}
}

View File

@ -0,0 +1,43 @@
package dev.usbharu.hideout.activitypub.service.activity.like
import com.fasterxml.jackson.databind.ObjectMapper
import com.fasterxml.jackson.module.kotlin.readValue
import dev.usbharu.hideout.activitypub.domain.model.Like
import dev.usbharu.hideout.activitypub.domain.model.Undo
import dev.usbharu.hideout.activitypub.service.common.APRequestService
import dev.usbharu.hideout.application.config.ApplicationConfig
import dev.usbharu.hideout.application.external.Transaction
import dev.usbharu.hideout.core.external.job.DeliverRemoveReactionJob
import dev.usbharu.hideout.core.external.job.DeliverRemoveReactionJobParam
import dev.usbharu.hideout.core.query.UserQueryService
import dev.usbharu.hideout.core.service.job.JobProcessor
import org.springframework.stereotype.Service
import java.time.Instant
@Service
class ApRemoveReactionJobProcessor(
private val userQueryService: UserQueryService,
private val transaction: Transaction,
private val objectMapper: ObjectMapper,
private val apRequestService: APRequestService,
private val applicationConfig: ApplicationConfig
) : JobProcessor<DeliverRemoveReactionJobParam, DeliverRemoveReactionJob> {
override suspend fun process(param: DeliverRemoveReactionJobParam): Unit = transaction.transaction {
val like = objectMapper.readValue<Like>(param.like)
val signer = userQueryService.findByUrl(param.actor)
apRequestService.apPost(
param.inbox,
Undo(
actor = param.actor,
`object` = like,
id = "${applicationConfig.url}/undo/like/${param.id}",
published = Instant.now().toString()
),
signer
)
}
override fun job(): DeliverRemoveReactionJob = DeliverRemoveReactionJob
}

View File

@ -0,0 +1,55 @@
package dev.usbharu.hideout.activitypub.service.activity.undo
import dev.usbharu.hideout.activitypub.domain.model.Follow
import dev.usbharu.hideout.activitypub.domain.model.Undo
import dev.usbharu.hideout.activitypub.service.common.AbstractActivityPubProcessor
import dev.usbharu.hideout.activitypub.service.common.ActivityPubProcessContext
import dev.usbharu.hideout.activitypub.service.common.ActivityType
import dev.usbharu.hideout.activitypub.service.objects.user.APUserService
import dev.usbharu.hideout.application.external.Transaction
import dev.usbharu.hideout.core.query.UserQueryService
import dev.usbharu.hideout.core.service.user.UserService
import org.springframework.stereotype.Service
@Service
class APUndoProcessor(
transaction: Transaction,
private val apUserService: APUserService,
private val userQueryService: UserQueryService,
private val userService: UserService
) :
AbstractActivityPubProcessor<Undo>(transaction) {
override suspend fun internalProcess(activity: ActivityPubProcessContext<Undo>) {
val undo = activity.activity
if (undo.actor == null) {
return
}
val type =
undo.`object`.type.orEmpty()
.firstOrNull { it == "Block" || it == "Follow" || it == "Like" || it == "Announce" || it == "Accept" }
?: return
when (type) {
"Follow" -> {
val follow = undo.`object` as Follow
if (follow.apObject == null) {
return
}
apUserService.fetchPerson(undo.actor, follow.apObject)
val follower = userQueryService.findByUrl(undo.actor)
val target = userQueryService.findByUrl(follow.apObject)
userService.unfollow(target.id, follower.id)
return
}
else -> {}
}
TODO()
}
override fun isSupported(activityType: ActivityType): Boolean = activityType == ActivityType.Undo
override fun type(): Class<Undo> = Undo::class.java
}

View File

@ -1,56 +0,0 @@
package dev.usbharu.hideout.activitypub.service.activity.undo
import dev.usbharu.hideout.activitypub.domain.model.Follow
import dev.usbharu.hideout.activitypub.domain.model.Undo
import dev.usbharu.hideout.activitypub.interfaces.api.common.ActivityPubResponse
import dev.usbharu.hideout.activitypub.interfaces.api.common.ActivityPubStringResponse
import dev.usbharu.hideout.activitypub.service.objects.user.APUserService
import dev.usbharu.hideout.application.external.Transaction
import dev.usbharu.hideout.core.query.UserQueryService
import dev.usbharu.hideout.core.service.user.UserService
import io.ktor.http.*
import org.springframework.stereotype.Service
interface APUndoService {
suspend fun receiveUndo(undo: Undo): ActivityPubResponse
}
@Service
@Suppress("UnsafeCallOnNullableType")
class APUndoServiceImpl(
private val userService: UserService,
private val apUserService: APUserService,
private val userQueryService: UserQueryService,
private val transaction: Transaction
) : APUndoService {
override suspend fun receiveUndo(undo: Undo): ActivityPubResponse {
if (undo.actor == null) {
return ActivityPubStringResponse(HttpStatusCode.BadRequest, "actor is null")
}
val type =
undo.`object`?.type.orEmpty()
.firstOrNull { it == "Block" || it == "Follow" || it == "Like" || it == "Announce" || it == "Accept" }
?: return ActivityPubStringResponse(HttpStatusCode.BadRequest, "unknown type ${undo.`object`?.type}")
when (type) {
"Follow" -> {
val follow = undo.`object` as Follow
if (follow.`object` == null) {
return ActivityPubStringResponse(HttpStatusCode.BadRequest, "object.object is null")
}
transaction.transaction {
apUserService.fetchPerson(undo.actor!!, follow.`object`)
val follower = userQueryService.findByUrl(undo.actor!!)
val target = userQueryService.findByUrl(follow.`object`!!)
userService.unfollow(target.id, follower.id)
}
return ActivityPubStringResponse(HttpStatusCode.OK, "Accept")
}
else -> {}
}
TODO()
}
}

View File

@ -37,17 +37,31 @@ class APRequestServiceImpl(
logger.debug("START ActivityPub Request GET url: {}, signer: {}", url, signer?.url) logger.debug("START ActivityPub Request GET url: {}, signer: {}", url, signer?.url)
val date = dateTimeFormatter.format(ZonedDateTime.now(ZoneId.of("GMT"))) val date = dateTimeFormatter.format(ZonedDateTime.now(ZoneId.of("GMT")))
val u = URL(url) val u = URL(url)
if (signer?.privateKey == null) { val httpResponse = if (signer?.privateKey == null) {
val bodyAsText = httpClient.get(url) { apGetNotSign(url, date)
header("Accept", ContentType.Application.Activity) } else {
header("Date", date) apGetSign(date, u, signer, url)
}.bodyAsText()
logBody(bodyAsText, url)
return objectMapper.readValue(bodyAsText, responseClass)
} }
val bodyAsText = httpResponse.bodyAsText()
val readValue = objectMapper.readValue(bodyAsText, responseClass)
logger.debug(
"SUCCESS ActivityPub Request GET status: {} url: {}",
httpResponse.status,
httpResponse.request.url
)
logBody(bodyAsText, url)
return readValue
}
private suspend fun apGetSign(
date: String,
u: URL,
signer: User,
url: String
): HttpResponse {
val headers = headers { val headers = headers {
append("Accept", ContentType.Application.Activity) append("Accept", Activity)
append("Date", date) append("Date", date)
append("Host", u.host) append("Host", u.host)
} }
@ -60,7 +74,7 @@ class APRequestServiceImpl(
), ),
privateKey = PrivateKey( privateKey = PrivateKey(
keyId = "${signer.url}#pubkey", keyId = "${signer.url}#pubkey",
privateKey = RsaUtil.decodeRsaPrivateKeyPem(signer.privateKey), privateKey = RsaUtil.decodeRsaPrivateKeyPem(signer.privateKey!!),
), ),
signHeaders = listOf("(request-target)", "date", "host", "accept") signHeaders = listOf("(request-target)", "date", "host", "accept")
) )
@ -73,17 +87,14 @@ class APRequestServiceImpl(
remove("Host") remove("Host")
} }
} }
contentType(ContentType.Application.Activity) contentType(Activity)
} }
val bodyAsText = httpResponse.bodyAsText() return httpResponse
val readValue = objectMapper.readValue(bodyAsText, responseClass) }
logger.debug(
"SUCCESS ActivityPub Request GET status: {} url: {}", private suspend fun apGetNotSign(url: String, date: String?) = httpClient.get(url) {
httpResponse.status, header("Accept", Activity)
httpResponse.request.url header("Date", date)
)
logBody(bodyAsText, url)
return readValue
} }
override suspend fun <T : Object, R : Object> apPost( override suspend fun <T : Object, R : Object> apPost(
@ -96,18 +107,9 @@ class APRequestServiceImpl(
return objectMapper.readValue(bodyAsText, responseClass) return objectMapper.readValue(bodyAsText, responseClass)
} }
@Suppress("LongMethod")
override suspend fun <T : Object> apPost(url: String, body: T?, signer: User?): String { override suspend fun <T : Object> apPost(url: String, body: T?, signer: User?): String {
logger.debug("START ActivityPub Request POST url: {}, signer: {}", url, signer?.url) logger.debug("START ActivityPub Request POST url: {}, signer: {}", url, signer?.url)
val requestBody = if (body != null) { val requestBody = addContextIfNotNull(body)
val mutableListOf = mutableListOf<String>()
mutableListOf.add("https://www.w3.org/ns/activitystreams")
mutableListOf.addAll(body.context)
body.context = mutableListOf
objectMapper.writeValueAsString(body)
} else {
null
}
logger.trace( logger.trace(
""" """
@ -129,22 +131,46 @@ class APRequestServiceImpl(
val date = dateTimeFormatter.format(ZonedDateTime.now(ZoneId.of("GMT"))) val date = dateTimeFormatter.format(ZonedDateTime.now(ZoneId.of("GMT")))
val u = URL(url) val u = URL(url)
if (signer?.privateKey == null) { val httpResponse = if (signer?.privateKey == null) {
val bodyAsText = httpClient.post(url) { apPostNotSign(url, date, digest, requestBody)
accept(ContentType.Application.Activity) } else {
header("Date", date) apPostSign(date, u, digest, signer, requestBody)
header("Digest", "sha-256=$digest")
if (requestBody != null) {
setBody(requestBody)
contentType(ContentType.Application.Activity)
} }
}.bodyAsText()
val bodyAsText = httpResponse.bodyAsText()
logger.debug(
"SUCCESS ActivityPub Request POST status: {} url: {}",
httpResponse.status,
httpResponse.request.url
)
logBody(bodyAsText, url) logBody(bodyAsText, url)
return bodyAsText return bodyAsText
} }
private suspend fun apPostNotSign(
url: String,
date: String?,
digest: String,
requestBody: String?
) = httpClient.post(url) {
accept(Activity)
header("Date", date)
header("Digest", "sha-256=$digest")
if (requestBody != null) {
setBody(requestBody)
contentType(Activity)
}
}
private suspend fun apPostSign(
date: String,
u: URL,
digest: String,
signer: User,
requestBody: String?
): HttpResponse {
val headers = headers { val headers = headers {
append("Accept", ContentType.Application.Activity) append("Accept", Activity)
append("Date", date) append("Date", date)
append("Host", u.host) append("Host", u.host)
append("Digest", "sha-256=$digest") append("Digest", "sha-256=$digest")
@ -158,30 +184,31 @@ class APRequestServiceImpl(
), ),
privateKey = PrivateKey( privateKey = PrivateKey(
keyId = signer.keyId, keyId = signer.keyId,
privateKey = RsaUtil.decodeRsaPrivateKeyPem(signer.privateKey) privateKey = RsaUtil.decodeRsaPrivateKeyPem(signer.privateKey!!)
), ),
signHeaders = listOf("(request-target)", "date", "host", "digest") signHeaders = listOf("(request-target)", "date", "host", "digest")
) )
val httpResponse = httpClient.post(url) { val httpResponse = httpClient.post(u) {
headers {
headers { headers {
appendAll(headers) appendAll(headers)
append("Signature", sign.signatureHeader) append("Signature", sign.signatureHeader)
remove("Host") remove("Host")
} }
}
setBody(requestBody) setBody(requestBody)
contentType(ContentType.Application.Activity) contentType(Activity)
} }
val bodyAsText = httpResponse.bodyAsText() return httpResponse
logger.debug( }
"SUCCESS ActivityPub Request POST status: {} url: {}",
httpResponse.status, private fun <T : Object> addContextIfNotNull(body: T?) = if (body != null) {
httpResponse.request.url val mutableListOf = mutableListOf<String>()
) mutableListOf.add("https://www.w3.org/ns/activitystreams")
logBody(bodyAsText, url) mutableListOf.addAll(body.context)
return bodyAsText body.context = mutableListOf
objectMapper.writeValueAsString(body)
} else {
null
} }
private fun logBody(bodyAsText: String, url: String) { private fun logBody(bodyAsText: String, url: String) {

View File

@ -39,9 +39,8 @@ class APResourceResolveServiceImpl(
return (cacheManager.getOrWait(key) as APResolveResponse<T>).objects return (cacheManager.getOrWait(key) as APResolveResponse<T>).objects
} }
private suspend fun <T : Object> runResolve(url: String, singer: User?, clazz: Class<T>): ResolveResponse { private suspend fun <T : Object> runResolve(url: String, singer: User?, clazz: Class<T>): ResolveResponse =
return APResolveResponse(apRequestService.apGet(url, singer, clazz)) APResolveResponse(apRequestService.apGet(url, singer, clazz))
}
private fun genCacheKey(url: String, singerId: Long?): String { private fun genCacheKey(url: String, singerId: Long?): String {
if (singerId != null) { if (singerId != null) {

View File

@ -2,16 +2,10 @@ package dev.usbharu.hideout.activitypub.service.common
import com.fasterxml.jackson.databind.JsonNode import com.fasterxml.jackson.databind.JsonNode
import com.fasterxml.jackson.databind.ObjectMapper import com.fasterxml.jackson.databind.ObjectMapper
import com.fasterxml.jackson.module.kotlin.readValue
import dev.usbharu.hideout.activitypub.domain.exception.JsonParseException import dev.usbharu.hideout.activitypub.domain.exception.JsonParseException
import dev.usbharu.hideout.activitypub.domain.model.Follow import dev.usbharu.hideout.core.external.job.InboxJob
import dev.usbharu.hideout.activitypub.interfaces.api.common.ActivityPubResponse import dev.usbharu.hideout.core.service.job.JobQueueParentService
import dev.usbharu.hideout.activitypub.service.activity.accept.APAcceptService import dev.usbharu.httpsignature.common.HttpRequest
import dev.usbharu.hideout.activitypub.service.activity.create.APCreateService
import dev.usbharu.hideout.activitypub.service.activity.delete.APReceiveDeleteService
import dev.usbharu.hideout.activitypub.service.activity.follow.APReceiveFollowService
import dev.usbharu.hideout.activitypub.service.activity.like.APLikeService
import dev.usbharu.hideout.activitypub.service.activity.undo.APUndoService
import org.slf4j.Logger import org.slf4j.Logger
import org.slf4j.LoggerFactory import org.slf4j.LoggerFactory
import org.springframework.beans.factory.annotation.Qualifier import org.springframework.beans.factory.annotation.Qualifier
@ -20,7 +14,12 @@ import org.springframework.stereotype.Service
interface APService { interface APService {
fun parseActivity(json: String): ActivityType fun parseActivity(json: String): ActivityType
suspend fun processActivity(json: String, type: ActivityType): ActivityPubResponse? suspend fun processActivity(
json: String,
type: ActivityType,
httpRequest: HttpRequest,
map: Map<String, List<String>>
)
} }
enum class ActivityType { enum class ActivityType {
@ -176,13 +175,8 @@ enum class ExtendedVocabulary {
@Service @Service
class APServiceImpl( class APServiceImpl(
private val apReceiveFollowService: APReceiveFollowService, @Qualifier("activitypub") private val objectMapper: ObjectMapper,
private val apUndoService: APUndoService, private val jobQueueParentService: JobQueueParentService
private val apAcceptService: APAcceptService,
private val apCreateService: APCreateService,
private val apLikeService: APLikeService,
private val apReceiveDeleteService: APReceiveDeleteService,
@Qualifier("activitypub") private val objectMapper: ObjectMapper
) : APService { ) : APService {
val logger: Logger = LoggerFactory.getLogger(APServiceImpl::class.java) val logger: Logger = LoggerFactory.getLogger(APServiceImpl::class.java)
@ -224,23 +218,21 @@ class APServiceImpl(
} }
} }
@Suppress("CyclomaticComplexMethod", "NotImplementedDeclaration") override suspend fun processActivity(
override suspend fun processActivity(json: String, type: ActivityType): ActivityPubResponse { json: String,
type: ActivityType,
httpRequest: HttpRequest,
map: Map<String, List<String>>
) {
logger.debug("process activity: {}", type) logger.debug("process activity: {}", type)
return when (type) { jobQueueParentService.schedule(InboxJob) {
ActivityType.Accept -> apAcceptService.receiveAccept(objectMapper.readValue(json)) props[it.json] = json
ActivityType.Follow -> props[it.type] = type.name
apReceiveFollowService val writeValueAsString = objectMapper.writeValueAsString(httpRequest)
.receiveFollow(objectMapper.readValue(json, Follow::class.java)) println(writeValueAsString)
props[it.httpRequest] = writeValueAsString
ActivityType.Create -> apCreateService.receiveCreate(objectMapper.readValue(json)) props[it.headers] = objectMapper.writeValueAsString(map)
ActivityType.Like -> apLikeService.receiveLike(objectMapper.readValue(json))
ActivityType.Undo -> apUndoService.receiveUndo(objectMapper.readValue(json))
ActivityType.Delete -> apReceiveDeleteService.receiveDelete(objectMapper.readValue(json))
else -> {
throw IllegalArgumentException("$type is not supported.")
}
} }
return
} }
} }

View File

@ -0,0 +1,36 @@
package dev.usbharu.hideout.activitypub.service.common
import dev.usbharu.hideout.activitypub.domain.exception.ActivityPubProcessException
import dev.usbharu.hideout.activitypub.domain.exception.FailedProcessException
import dev.usbharu.hideout.activitypub.domain.exception.HttpSignatureUnauthorizedException
import dev.usbharu.hideout.activitypub.domain.model.objects.Object
import dev.usbharu.hideout.application.external.Transaction
import org.slf4j.Logger
import org.slf4j.LoggerFactory
import org.springframework.stereotype.Service
@Service
abstract class AbstractActivityPubProcessor<T : Object>(
private val transaction: Transaction,
private val allowUnauthorized: Boolean = false
) : ActivityPubProcessor<T> {
protected val logger: Logger = LoggerFactory.getLogger(this::class.java)
override suspend fun process(activity: ActivityPubProcessContext<T>) {
if (activity.isAuthorized.not() && allowUnauthorized.not()) {
throw HttpSignatureUnauthorizedException()
}
logger.info("START ActivityPub process")
try {
transaction.transaction {
internalProcess(activity)
}
} catch (e: ActivityPubProcessException) {
logger.warn("FAILED ActivityPub process", e)
throw FailedProcessException("Failed process", e)
}
logger.info("SUCCESS ActivityPub process")
}
abstract suspend fun internalProcess(activity: ActivityPubProcessContext<T>)
}

View File

@ -0,0 +1,14 @@
package dev.usbharu.hideout.activitypub.service.common
import com.fasterxml.jackson.databind.JsonNode
import dev.usbharu.hideout.activitypub.domain.model.objects.Object
import dev.usbharu.httpsignature.common.HttpRequest
import dev.usbharu.httpsignature.verify.Signature
data class ActivityPubProcessContext<T : Object>(
val activity: T,
val jsonNode: JsonNode,
val httpRequest: HttpRequest,
val signature: Signature?,
val isAuthorized: Boolean
)

View File

@ -0,0 +1,11 @@
package dev.usbharu.hideout.activitypub.service.common
import dev.usbharu.hideout.activitypub.domain.model.objects.Object
interface ActivityPubProcessor<T : Object> {
suspend fun process(activity: ActivityPubProcessContext<T>)
fun isSupported(activityType: ActivityType): Boolean
fun type(): Class<T>
}

View File

@ -1,8 +0,0 @@
package dev.usbharu.hideout.activitypub.service.common
import dev.usbharu.hideout.core.external.job.HideoutJob
import kjob.core.dsl.JobContextWithProps
interface ApJobService {
suspend fun <T : HideoutJob> processActivity(job: JobContextWithProps<T>, hideoutJob: HideoutJob)
}

View File

@ -1,46 +0,0 @@
package dev.usbharu.hideout.activitypub.service.common
import dev.usbharu.hideout.activitypub.service.activity.follow.APReceiveFollowJobService
import dev.usbharu.hideout.activitypub.service.activity.like.ApReactionJobService
import dev.usbharu.hideout.activitypub.service.objects.note.ApNoteJobService
import dev.usbharu.hideout.core.external.job.*
import kjob.core.dsl.JobContextWithProps
import kjob.core.job.JobProps
import org.slf4j.LoggerFactory
import org.springframework.stereotype.Service
@Service
class ApJobServiceImpl(
private val apReceiveFollowJobService: APReceiveFollowJobService,
private val apNoteJobService: ApNoteJobService,
private val apReactionJobService: ApReactionJobService
) : ApJobService {
@Suppress("REDUNDANT_ELSE_IN_WHEN")
override suspend fun <T : HideoutJob> processActivity(job: JobContextWithProps<T>, hideoutJob: HideoutJob) {
logger.debug("processActivity: ${hideoutJob.name}")
@Suppress("ElseCaseInsteadOfExhaustiveWhen")
// Springで作成されるプロキシの都合上パターンマッチングが壊れるので必須
when (hideoutJob) {
is ReceiveFollowJob -> {
apReceiveFollowJobService.receiveFollowJob(
job.props as JobProps<ReceiveFollowJob>
)
}
is DeliverPostJob -> apNoteJobService.createNoteJob(job.props as JobProps<DeliverPostJob>)
is DeliverReactionJob -> apReactionJobService.reactionJob(job.props as JobProps<DeliverReactionJob>)
is DeliverRemoveReactionJob -> apReactionJobService.removeReactionJob(
job.props as JobProps<DeliverRemoveReactionJob>
)
else -> {
throw IllegalStateException("WTF")
}
}
}
companion object {
private val logger = LoggerFactory.getLogger(ApJobServiceImpl::class.java)
}
}

View File

@ -0,0 +1,127 @@
package dev.usbharu.hideout.activitypub.service.inbox
import com.fasterxml.jackson.databind.ObjectMapper
import com.fasterxml.jackson.module.kotlin.readValue
import dev.usbharu.hideout.activitypub.domain.model.objects.Object
import dev.usbharu.hideout.activitypub.service.common.ActivityPubProcessContext
import dev.usbharu.hideout.activitypub.service.common.ActivityPubProcessor
import dev.usbharu.hideout.activitypub.service.objects.user.APUserService
import dev.usbharu.hideout.application.external.Transaction
import dev.usbharu.hideout.core.domain.exception.FailedToGetResourcesException
import dev.usbharu.hideout.core.external.job.InboxJob
import dev.usbharu.hideout.core.external.job.InboxJobParam
import dev.usbharu.hideout.core.query.UserQueryService
import dev.usbharu.hideout.core.service.job.JobProcessor
import dev.usbharu.hideout.util.RsaUtil
import dev.usbharu.httpsignature.common.HttpHeaders
import dev.usbharu.httpsignature.common.HttpMethod
import dev.usbharu.httpsignature.common.HttpRequest
import dev.usbharu.httpsignature.common.PublicKey
import dev.usbharu.httpsignature.verify.HttpSignatureVerifier
import dev.usbharu.httpsignature.verify.Signature
import dev.usbharu.httpsignature.verify.SignatureHeaderParser
import org.slf4j.LoggerFactory
import org.springframework.stereotype.Service
@Service
class InboxJobProcessor(
private val activityPubProcessorList: List<ActivityPubProcessor<*>>,
private val objectMapper: ObjectMapper,
private val signatureHeaderParser: SignatureHeaderParser,
private val signatureVerifier: HttpSignatureVerifier,
private val userQueryService: UserQueryService,
private val apUserService: APUserService,
private val transaction: Transaction
) : JobProcessor<InboxJobParam, InboxJob> {
private suspend fun verifyHttpSignature(
httpRequest: HttpRequest,
signature: Signature,
transaction: Transaction
): Boolean {
val requiredHeaders = when (httpRequest.method) {
HttpMethod.GET -> getRequiredHeaders
HttpMethod.POST -> postRequiredHeaders
}
if (signature.headers.containsAll(requiredHeaders).not()) {
logger.warn("FAILED Invalid signature. require: {}", requiredHeaders)
return false
}
val user = transaction.transaction {
try {
userQueryService.findByKeyId(signature.keyId)
} catch (_: FailedToGetResourcesException) {
apUserService.fetchPersonWithEntity(signature.keyId).second
}
}
@Suppress("TooGenericExceptionCaught")
val verify = try {
signatureVerifier.verify(
httpRequest,
PublicKey(RsaUtil.decodeRsaPublicKeyPem(user.publicKey), signature.keyId)
)
} catch (e: Exception) {
logger.warn("FAILED Verify Http Signature", e)
return false
}
return verify.success
}
@Suppress("TooGenericExceptionCaught")
private fun parseSignatureHeader(httpHeaders: HttpHeaders): Signature? {
return try {
println("Signature Header =" + httpHeaders.get("Signature").single())
signatureHeaderParser.parse(httpHeaders)
} catch (e: RuntimeException) {
logger.trace("FAILED parse signature header", e)
null
}
}
override suspend fun process(param: InboxJobParam) {
val jsonNode = objectMapper.readTree(param.json)
logger.info("START Process inbox. type: {}", param.type)
logger.trace("type: {}\njson: \n{}", param.type, jsonNode.toPrettyString())
val map = objectMapper.readValue<Map<String, List<String>>>(param.headers)
val httpRequest = objectMapper.readValue<HttpRequest>(param.httpRequest).copy(headers = HttpHeaders(map))
logger.trace("Request: {}\nheaders: {}", httpRequest, map)
val signature = parseSignatureHeader(httpRequest.headers)
logger.debug("Has signature? {}", signature != null)
val verify = signature?.let { verifyHttpSignature(httpRequest, it, transaction) } ?: false
transaction.transaction {
logger.debug("Is verifying success? {}", verify)
val activityPubProcessor =
activityPubProcessorList.firstOrNull { it.isSupported(param.type) } as ActivityPubProcessor<Object>?
if (activityPubProcessor == null) {
logger.warn("ActivityType {} is not support.", param.type)
throw IllegalStateException("ActivityPubProcessor not found.")
}
val value = objectMapper.treeToValue(jsonNode, activityPubProcessor.type())
activityPubProcessor.process(ActivityPubProcessContext(value, jsonNode, httpRequest, signature, verify))
logger.info("SUCCESS Process inbox. type: {}", param.type)
}
}
override fun job(): InboxJob = InboxJob
companion object {
private val logger = LoggerFactory.getLogger(InboxJobProcessor::class.java)
private val postRequiredHeaders = listOf("(request-target)", "date", "host", "digest")
private val getRequiredHeaders = listOf("(request-target)", "date", "host")
}
}

View File

@ -1,7 +1,6 @@
package dev.usbharu.hideout.activitypub.service.objects.note package dev.usbharu.hideout.activitypub.service.objects.note
import dev.usbharu.hideout.activitypub.domain.exception.FailedToGetActivityPubResourceException import dev.usbharu.hideout.activitypub.domain.exception.FailedToGetActivityPubResourceException
import dev.usbharu.hideout.activitypub.domain.exception.IllegalActivityPubObjectException
import dev.usbharu.hideout.activitypub.domain.model.Note import dev.usbharu.hideout.activitypub.domain.model.Note
import dev.usbharu.hideout.activitypub.query.NoteQueryService import dev.usbharu.hideout.activitypub.query.NoteQueryService
import dev.usbharu.hideout.activitypub.service.common.APResourceResolveService import dev.usbharu.hideout.activitypub.service.common.APResourceResolveService
@ -91,7 +90,7 @@ class APNoteServiceImpl(
requireNotNull(note.id) { "id is null" } requireNotNull(note.id) { "id is null" }
return try { return try {
noteQueryService.findByApid(note.id!!).first noteQueryService.findByApid(note.id).first
} catch (_: FailedToGetResourcesException) { } catch (_: FailedToGetResourcesException) {
saveNote(note, targetActor, url) saveNote(note, targetActor, url)
} }
@ -99,7 +98,7 @@ class APNoteServiceImpl(
private suspend fun saveNote(note: Note, targetActor: String?, url: String): Note { private suspend fun saveNote(note: Note, targetActor: String?, url: String): Note {
val person = apUserService.fetchPersonWithEntity( val person = apUserService.fetchPersonWithEntity(
note.attributedTo ?: throw IllegalActivityPubObjectException("note.attributedTo is null"), note.attributedTo,
targetActor targetActor
) )
@ -128,9 +127,9 @@ class APNoteServiceImpl(
.map { .map {
mediaService.uploadRemoteMedia( mediaService.uploadRemoteMedia(
RemoteMedia( RemoteMedia(
(it.name ?: it.url)!!, it.name,
it.url!!, it.url,
it.mediaType ?: "application/octet-stream", it.mediaType,
description = it.name description = it.name
) )
) )
@ -142,13 +141,13 @@ class APNoteServiceImpl(
postBuilder.of( postBuilder.of(
id = postRepository.generateId(), id = postRepository.generateId(),
userId = person.second.id, userId = person.second.id,
text = note.content.orEmpty(), text = note.content,
createdAt = Instant.parse(note.published).toEpochMilli(), createdAt = Instant.parse(note.published).toEpochMilli(),
visibility = visibility, visibility = visibility,
url = note.id ?: url, url = note.id,
replyId = reply?.id, replyId = reply?.id,
sensitive = note.sensitive, sensitive = note.sensitive,
apId = note.id ?: url, apId = note.id,
mediaIds = mediaList mediaIds = mediaList
) )
) )
@ -156,7 +155,7 @@ class APNoteServiceImpl(
} }
override suspend fun fetchNote(note: Note, targetActor: String?): Note = override suspend fun fetchNote(note: Note, targetActor: String?): Note =
saveIfMissing(note, targetActor, note.id ?: throw IllegalArgumentException("note.id is null")) saveIfMissing(note, targetActor, note.id)
companion object { companion object {
const val public: String = "https://www.w3.org/ns/activitystreams#Public" const val public: String = "https://www.w3.org/ns/activitystreams#Public"

View File

@ -0,0 +1,42 @@
package dev.usbharu.hideout.activitypub.service.objects.note
import com.fasterxml.jackson.databind.ObjectMapper
import com.fasterxml.jackson.module.kotlin.readValue
import dev.usbharu.hideout.activitypub.domain.model.Create
import dev.usbharu.hideout.activitypub.service.common.APRequestService
import dev.usbharu.hideout.application.external.Transaction
import dev.usbharu.hideout.core.external.job.DeliverPostJob
import dev.usbharu.hideout.core.external.job.DeliverPostJobParam
import dev.usbharu.hideout.core.query.UserQueryService
import dev.usbharu.hideout.core.service.job.JobProcessor
import org.slf4j.LoggerFactory
import org.springframework.stereotype.Service
@Service
class ApNoteJobProcessor(
private val transaction: Transaction,
private val objectMapper: ObjectMapper,
private val userQueryService: UserQueryService,
private val apRequestService: APRequestService
) : JobProcessor<DeliverPostJobParam, DeliverPostJob> {
override suspend fun process(param: DeliverPostJobParam) {
val create = objectMapper.readValue<Create>(param.create)
transaction.transaction {
val signer = userQueryService.findByUrl(param.actor)
logger.debug("CreateNoteJob: actor: {} create: {} inbox: {}", param.actor, create, param.inbox)
apRequestService.apPost(
param.inbox,
create,
signer
)
}
}
override fun job(): DeliverPostJob = DeliverPostJob
companion object {
private val logger = LoggerFactory.getLogger(ApNoteJobProcessor::class.java)
}
}

View File

@ -1,8 +0,0 @@
package dev.usbharu.hideout.activitypub.service.objects.note
import dev.usbharu.hideout.core.external.job.DeliverPostJob
import kjob.core.job.JobProps
interface ApNoteJobService {
suspend fun createNoteJob(props: JobProps<DeliverPostJob>)
}

View File

@ -1,41 +0,0 @@
package dev.usbharu.hideout.activitypub.service.objects.note
import com.fasterxml.jackson.databind.ObjectMapper
import com.fasterxml.jackson.module.kotlin.readValue
import dev.usbharu.hideout.activitypub.domain.model.Create
import dev.usbharu.hideout.activitypub.service.common.APRequestService
import dev.usbharu.hideout.application.external.Transaction
import dev.usbharu.hideout.core.external.job.DeliverPostJob
import dev.usbharu.hideout.core.query.UserQueryService
import kjob.core.job.JobProps
import org.slf4j.LoggerFactory
import org.springframework.beans.factory.annotation.Qualifier
import org.springframework.stereotype.Component
@Component
class ApNoteJobServiceImpl(
private val userQueryService: UserQueryService,
private val apRequestService: APRequestService,
@Qualifier("activitypub") private val objectMapper: ObjectMapper,
private val transaction: Transaction
) : ApNoteJobService {
override suspend fun createNoteJob(props: JobProps<DeliverPostJob>) {
val actor = props[DeliverPostJob.actor]
val create = objectMapper.readValue<Create>(props[DeliverPostJob.create])
transaction.transaction {
val signer = userQueryService.findByUrl(actor)
val inbox = props[DeliverPostJob.inbox]
logger.debug("createNoteJob: actor={}, create={}, inbox={}", actor, create, inbox)
apRequestService.apPost(
inbox,
create,
signer
)
}
}
companion object {
private val logger = LoggerFactory.getLogger(ApNoteJobServiceImpl::class.java)
}
}

View File

@ -4,6 +4,7 @@ import dev.usbharu.hideout.activitypub.domain.model.Note
import dev.usbharu.hideout.activitypub.query.NoteQueryService import dev.usbharu.hideout.activitypub.query.NoteQueryService
import dev.usbharu.hideout.application.external.Transaction import dev.usbharu.hideout.application.external.Transaction
import dev.usbharu.hideout.core.domain.exception.FailedToGetResourcesException import dev.usbharu.hideout.core.domain.exception.FailedToGetResourcesException
import dev.usbharu.hideout.core.domain.model.post.Post
import dev.usbharu.hideout.core.domain.model.post.Visibility import dev.usbharu.hideout.core.domain.model.post.Visibility
import dev.usbharu.hideout.core.query.FollowerQueryService import dev.usbharu.hideout.core.query.FollowerQueryService
import org.slf4j.LoggerFactory import org.slf4j.LoggerFactory
@ -28,20 +29,27 @@ class NoteApApiServiceImpl(
} }
Visibility.FOLLOWERS -> { Visibility.FOLLOWERS -> {
if (userId == null) { return@transaction getFollowersNote(userId, findById)
return@transaction null
}
if (followerQueryService.alreadyFollow(findById.second.userId, userId).not()) {
return@transaction null
}
return@transaction findById.first
} }
Visibility.DIRECT -> return@transaction null Visibility.DIRECT -> return@transaction null
} }
} }
private suspend fun getFollowersNote(
userId: Long?,
findById: Pair<Note, Post>
): Note? {
if (userId == null) {
return null
}
if (followerQueryService.alreadyFollow(findById.second.userId, userId)) {
return findById.first
}
return null
}
companion object { companion object {
private val logger = LoggerFactory.getLogger(NoteApApiServiceImpl::class.java) private val logger = LoggerFactory.getLogger(NoteApApiServiceImpl::class.java)
} }

View File

@ -14,6 +14,7 @@ import dev.usbharu.hideout.core.query.UserQueryService
import dev.usbharu.hideout.core.service.user.RemoteUserCreateDto import dev.usbharu.hideout.core.service.user.RemoteUserCreateDto
import dev.usbharu.hideout.core.service.user.UserService import dev.usbharu.hideout.core.service.user.UserService
import org.springframework.stereotype.Service import org.springframework.stereotype.Service
import org.springframework.transaction.annotation.Transactional
interface APUserService { interface APUserService {
suspend fun getPersonByName(name: String): Person suspend fun getPersonByName(name: String): Person
@ -57,13 +58,10 @@ class APUserServiceImpl(
url = userUrl, url = userUrl,
icon = Image( icon = Image(
type = emptyList(), type = emptyList(),
name = "$userUrl/icon.png",
mediaType = "image/png", mediaType = "image/png",
url = "$userUrl/icon.png" url = "$userUrl/icon.png"
), ),
publicKey = Key( publicKey = Key(
type = emptyList(),
name = "Public Key",
id = userEntity.keyId, id = userEntity.keyId,
owner = userUrl, owner = userUrl,
publicKeyPem = userEntity.publicKey publicKeyPem = userEntity.publicKey
@ -77,52 +75,33 @@ class APUserServiceImpl(
override suspend fun fetchPerson(url: String, targetActor: String?): Person = override suspend fun fetchPerson(url: String, targetActor: String?): Person =
fetchPersonWithEntity(url, targetActor).first fetchPersonWithEntity(url, targetActor).first
@Transactional
override suspend fun fetchPersonWithEntity(url: String, targetActor: String?): Pair<Person, User> { override suspend fun fetchPersonWithEntity(url: String, targetActor: String?): Pair<Person, User> {
return try { return try {
val userEntity = userQueryService.findByUrl(url) val userEntity = userQueryService.findByUrl(url)
return Person( val id = userEntity.url
type = emptyList(), return entityToPerson(userEntity, id) to userEntity
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 = userEntity.keyId,
owner = url,
publicKeyPem = userEntity.publicKey
),
endpoints = mapOf("sharedInbox" to "${applicationConfig.url}/inbox"),
followers = userEntity.followers,
following = userEntity.following
) to userEntity
} catch (ignore: FailedToGetResourcesException) { } catch (ignore: FailedToGetResourcesException) {
val person = apResourceResolveService.resolve<Person>(url, null as Long?) val person = apResourceResolveService.resolve<Person>(url, null as Long?)
val id = person.id
try {
val userEntity = userQueryService.findByUrl(id)
return entityToPerson(userEntity, id) to userEntity
} catch (_: FailedToGetResourcesException) {
}
person to userService.createRemoteUser( person to userService.createRemoteUser(
RemoteUserCreateDto( RemoteUserCreateDto(
name = person.preferredUsername name = person.preferredUsername
?: throw IllegalActivityPubObjectException("preferredUsername is null"), ?: throw IllegalActivityPubObjectException("preferredUsername is null"),
domain = url.substringAfter("://").substringBefore("/"), domain = id.substringAfter("://").substringBefore("/"),
screenName = (person.name ?: person.preferredUsername) screenName = person.name,
?: throw IllegalActivityPubObjectException("preferredUsername is null"),
description = person.summary.orEmpty(), description = person.summary.orEmpty(),
inbox = person.inbox ?: throw IllegalActivityPubObjectException("inbox is null"), inbox = person.inbox,
outbox = person.outbox ?: throw IllegalActivityPubObjectException("outbox is null"), outbox = person.outbox,
url = url, url = id,
publicKey = person.publicKey?.publicKeyPem publicKey = person.publicKey.publicKeyPem,
?: throw IllegalActivityPubObjectException("publicKey is null"), keyId = person.publicKey.id,
keyId = person.publicKey?.id ?: throw IllegalActivityPubObjectException("publicKey keyId is null"),
following = person.following, following = person.following,
followers = person.followers, followers = person.followers,
sharedInbox = person.endpoints["sharedInbox"] sharedInbox = person.endpoints["sharedInbox"]
@ -130,4 +109,31 @@ class APUserServiceImpl(
) )
} }
} }
private fun entityToPerson(
userEntity: User,
id: String
) = Person(
type = emptyList(),
name = userEntity.name,
id = id,
preferredUsername = userEntity.name,
summary = userEntity.description,
inbox = "$id/inbox",
outbox = "$id/outbox",
url = id,
icon = Image(
type = emptyList(),
mediaType = "image/png",
url = "$id/icon.png"
),
publicKey = Key(
id = userEntity.keyId,
owner = id,
publicKeyPem = userEntity.publicKey
),
endpoints = mapOf("sharedInbox" to "${applicationConfig.url}/inbox"),
followers = userEntity.followers,
following = userEntity.following
)
} }

View File

@ -7,6 +7,8 @@ import com.fasterxml.jackson.core.JsonParser
import com.fasterxml.jackson.databind.DeserializationFeature import com.fasterxml.jackson.databind.DeserializationFeature
import com.fasterxml.jackson.databind.ObjectMapper import com.fasterxml.jackson.databind.ObjectMapper
import com.fasterxml.jackson.module.kotlin.jacksonObjectMapper import com.fasterxml.jackson.module.kotlin.jacksonObjectMapper
import dev.usbharu.hideout.core.infrastructure.httpsignature.HttpRequestMixIn
import dev.usbharu.httpsignature.common.HttpRequest
import dev.usbharu.httpsignature.sign.HttpSignatureSigner import dev.usbharu.httpsignature.sign.HttpSignatureSigner
import dev.usbharu.httpsignature.sign.RsaSha256HttpSignatureSigner import dev.usbharu.httpsignature.sign.RsaSha256HttpSignatureSigner
import org.springframework.beans.factory.annotation.Qualifier import org.springframework.beans.factory.annotation.Qualifier
@ -24,11 +26,13 @@ class ActivityPubConfig {
val objectMapper = jacksonObjectMapper() val objectMapper = jacksonObjectMapper()
.enable(DeserializationFeature.ACCEPT_SINGLE_VALUE_AS_ARRAY) .enable(DeserializationFeature.ACCEPT_SINGLE_VALUE_AS_ARRAY)
.setSerializationInclusion(JsonInclude.Include.NON_EMPTY) .setSerializationInclusion(JsonInclude.Include.NON_EMPTY)
.setDefaultSetterInfo(JsonSetter.Value.forContentNulls(Nulls.AS_EMPTY)) .setDefaultSetterInfo(JsonSetter.Value.forContentNulls(Nulls.SKIP))
.setDefaultSetterInfo(JsonSetter.Value.forValueNulls(Nulls.SKIP))
.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false) .configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false)
.configure(JsonParser.Feature.ALLOW_COMMENTS, true) .configure(JsonParser.Feature.ALLOW_COMMENTS, true)
.configure(JsonParser.Feature.ALLOW_SINGLE_QUOTES, true) .configure(JsonParser.Feature.ALLOW_SINGLE_QUOTES, true)
.configure(JsonParser.Feature.ALLOW_TRAILING_COMMA, true) .configure(JsonParser.Feature.ALLOW_TRAILING_COMMA, true)
.addMixIn(HttpRequest::class.java, HttpRequestMixIn::class.java)
return objectMapper return objectMapper
} }

Some files were not shown because too many files have changed in this diff Show More