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/
/stats.html
/tomcat/
/tomcat-e2e/
/e2eTest.log
/files/

View File

@ -1,6 +1,5 @@
import org.jetbrains.kotlin.gradle.tasks.KotlinCompile
import org.openapitools.generator.gradle.plugin.tasks.GenerateTask
import kotlin.math.max
val ktor_version: String by project
val kotlin_version: String by project
@ -13,7 +12,7 @@ plugins {
kotlin("jvm") version "1.8.21"
id("org.graalvm.buildtools.native") version "0.9.21"
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"
id("org.openapi.generator") version "7.0.1"
id("org.jetbrains.kotlinx.kover") version "0.7.4"
@ -32,6 +31,10 @@ sourceSets {
compileClasspath += 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 {
@ -41,6 +44,14 @@ val intTestRuntimeOnly by configurations.getting {
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") {
description = "Runs integration tests."
group = "verification"
@ -50,19 +61,26 @@ val integrationTest = task<Test>("integrationTest") {
shouldRunAfter("test")
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> {
useJUnitPlatform()
val cpus = Runtime.getRuntime().availableProcessors()
maxParallelForks = max(1, cpus - 1)
setForkEvery(4)
doFirst {
jvmArgs = arrayOf(
"--add-opens", "java.base/java.lang=ALL-UNNAMED"
@ -180,6 +198,7 @@ dependencies {
implementation("org.postgresql:postgresql:42.6.0")
implementation("com.twelvemonkeys.imageio:imageio-webp:3.10.0")
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("org.bytedeco:javacv-platform:1.5.9")
implementation("org.flywaydb:flyway-core")
@ -206,6 +225,18 @@ dependencies {
intTestImplementation("org.springframework.boot:spring-boot-starter-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:
Indentation: 0
MagicNumber: 0
InjectDispatcher: 0
EnumEntryNameCase: 0
ReplaceSafeCallChainWithRun: 0
VariableNaming: 0
NoNameShadowing: 0
@ -78,7 +76,7 @@ complexity:
active: true
ReplaceSafeCallChainWithRun:
active: true
active: false
StringLiteralDuplication:
active: false
@ -172,3 +170,5 @@ potential-bugs:
coroutines:
RedundantSuspendModifier:
active: false
InjectDispatcher:
active: false

View File

@ -1,5 +1,5 @@
ktor_version=2.3.0
kotlin_version=1.8.21
kotlin_version=1.9.21
logback_version=1.4.6
kotlin.code.style=official
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
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
@ -45,6 +47,7 @@ class InboxTest {
content = "{}"
contentType = MediaType.APPLICATION_JSON
}
.asyncDispatch()
.andExpect { status { isUnauthorized() } }
}
@ -55,6 +58,7 @@ class InboxTest {
.post("/inbox") {
content = "{}"
contentType = MediaType.APPLICATION_JSON
header("Signature", "")
}
.asyncDispatch()
.andExpect { status { isAccepted() } }
@ -68,6 +72,7 @@ class InboxTest {
content = "{}"
contentType = MediaType.APPLICATION_JSON
}
.asyncDispatch()
.andExpect { status { isUnauthorized() } }
}
@ -78,6 +83,7 @@ class InboxTest {
.post("/users/hoge/inbox") {
content = "{}"
contentType = MediaType.APPLICATION_JSON
header("Signature", "")
}
.asyncDispatch()
.andExpect { status { isAccepted() } }
@ -88,4 +94,13 @@ class InboxTest {
@Bean
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
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
@ -178,4 +180,13 @@ class NoteTest {
.andExpect { jsonPath("\$.attachment[1].type") { value("Document") } }
.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.application.external.Transaction
import org.flywaydb.core.Flyway
import org.junit.jupiter.api.AfterAll
import org.junit.jupiter.api.Test
import org.springframework.beans.factory.annotation.Autowired
import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc
@ -24,6 +26,7 @@ class WebFingerTest {
@Test
@Sql("/sql/test-user.sql")
fun `webfinger 存在するユーザーを取得`() {
mockMvc
.get("/.well-known/webfinger?resource=acct:test-user@example.com")
@ -52,7 +55,7 @@ class WebFingerTest {
@Test
fun `webfinger 存在しないユーザーに404`() {
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() } }
}
@ -80,4 +83,13 @@ class WebFingerTest {
@Bean
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=="
public-key: "MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAu1SU1LfVLPHCozMxH2Mo4lgOEePzNm0tRgeLezV6ffAt0gunVTLw7onLRnrq0/IzW7yWR7QkrmBL7jTKEn5u+qKhbwKfBstIs+bMY2Zkp18gnTxKLxoS2tFczGkPLPgizskuemMghRniWaoLcyehkd3qqGElvW/VDL5AaWTg0nLVkjRo9z+40RQzuVaE8AkAFmxZzow3x+VJYKdjykkJ0iT9wCS0DRTXu269V264Vf/3jvredZiKRkgwlL9xNAwxXFg0x/XFw005UWVRIkdgcKWTjpBP2dPwVZ4WWC+9aGVd+Gyn1o0CLelf4rEjGoXbAAEgAqeGUxrcIlbjXfbcmwIDAQAB"
storage:
use-s3: true
endpoint: "http://localhost:8082/test-hideout"
public-url: "http://localhost:8082/test-hideout"
bucket: "test-hideout"
region: "auto"
access-key: ""
secret-key: ""
type: local
spring:
flyway:
enabled: true
clean-disabled: false
datasource:
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: ""
password:
data:
@ -32,8 +29,8 @@ spring:
console:
enabled: true
exposed:
generate-ddl: true
excluded-packages: dev.usbharu.hideout.core.infrastructure.kjobexposed
# exposed:
# generate-ddl: true
# excluded-packages: dev.usbharu.hideout.core.infrastructure.kjobexposed
server:
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">
<appender-ref ref="STDOUT"/>
</root>
<logger name="org.springframework.security" level="TRACE"/>
</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
import dev.usbharu.hideout.core.domain.exception.FailedToGetResourcesException
import java.io.Serial
class FailedToGetActivityPubResourceException : FailedToGetResourcesException {
constructor() : super()
constructor(s: String?) : super(s)
constructor(message: String?, cause: Throwable?) : super(message, 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
import com.fasterxml.jackson.annotation.JsonCreator
import com.fasterxml.jackson.annotation.JsonProperty
import com.fasterxml.jackson.databind.annotation.JsonDeserialize
import dev.usbharu.hideout.activitypub.domain.model.objects.Object
import dev.usbharu.hideout.activitypub.domain.model.objects.ObjectDeserializer
open class Accept : Object {
@JsonDeserialize(using = ObjectDeserializer::class)
var `object`: Object? = null
protected constructor()
constructor(
open class Accept @JsonCreator constructor(
type: List<String> = emptyList(),
name: String,
`object`: Object?,
actor: String?
) : super(
type = add(type, "Accept"),
name = name,
actor = actor
) {
this.`object` = `object`
}
override fun toString(): String = "Accept(`object`=$`object`) ${super.toString()}"
override val name: String,
@JsonDeserialize(using = ObjectDeserializer::class)
@JsonProperty("object")
val apObject: Object,
override val actor: String
) : Object(
type = add(type, "Accept")
),
HasActor,
HasName {
override fun equals(other: Any?): Boolean {
if (this === other) return true
if (other !is Accept) return false
if (javaClass != other?.javaClass) 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 {
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
}
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
import com.fasterxml.jackson.annotation.JsonProperty
import com.fasterxml.jackson.databind.annotation.JsonDeserialize
import dev.usbharu.hideout.activitypub.domain.model.objects.Object
import dev.usbharu.hideout.activitypub.domain.model.objects.ObjectDeserializer
open class Create : Object {
@JsonDeserialize(using = ObjectDeserializer::class)
var `object`: Object? = null
var to: List<String> = emptyList()
var cc: List<String> = emptyList()
protected constructor() : super()
constructor(
open class Create(
type: List<String> = emptyList(),
name: String? = null,
`object`: Object?,
actor: String? = null,
id: String? = null,
to: List<String> = emptyList(),
cc: List<String> = emptyList()
) : super(
type = add(type, "Create"),
name = name,
actor = actor,
id = id
) {
this.`object` = `object`
this.to = to
this.cc = cc
}
override val name: String,
@JsonDeserialize(using = ObjectDeserializer::class)
@JsonProperty("object")
val apObject: Object,
override val actor: String,
override val id: String,
val to: List<String> = emptyList(),
val cc: List<String> = emptyList()
) : Object(
type = add(type, "Create")
),
HasId,
HasName,
HasActor {
override fun equals(other: Any?): Boolean {
if (this === other) return true
if (other !is Create) return false
if (javaClass != other?.javaClass) 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 {
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
}
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
import com.fasterxml.jackson.annotation.JsonProperty
import com.fasterxml.jackson.databind.annotation.JsonDeserialize
import dev.usbharu.hideout.activitypub.domain.model.objects.Object
import dev.usbharu.hideout.activitypub.domain.model.objects.ObjectDeserializer
open class Delete : Object {
open class Delete : Object, HasId, HasActor {
@JsonDeserialize(using = ObjectDeserializer::class)
var `object`: Object? = null
var published: String? = null
@JsonProperty("object")
val apObject: Object
val published: String
override val actor: String
override val id: String
constructor(
type: List<String> = emptyList(),
name: String? = "Delete",
actor: String,
id: String,
`object`: Object,
published: String?
) : super(add(type, "Delete"), name, actor, id) {
this.`object` = `object`
published: String
) : super(add(type, "Delete")) {
this.apObject = `object`
this.published = published
this.actor = actor
this.id = id
}
protected constructor() : super()
override fun equals(other: Any?): Boolean {
if (this === other) return true
if (other !is Delete) return false
if (javaClass != other?.javaClass) 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 (actor != other.actor) return false
if (id != other.id) return false
return true
}
override fun hashCode(): Int {
var result = super.hashCode()
result = 31 * result + (`object`?.hashCode() ?: 0)
result = 31 * result + (published?.hashCode() ?: 0)
result = 31 * result + apObject.hashCode()
result = 31 * result + published.hashCode()
result = 31 * result + actor.hashCode()
result = 31 * result + id.hashCode()
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
open class Document : Object {
var mediaType: String? = null
var url: String? = null
protected constructor() : super()
constructor(
open class Document(
type: List<String> = emptyList(),
name: String? = null,
mediaType: String,
url: String
) : super(
type = add(type, "Document"),
name = name,
actor = null,
id = null
) {
this.mediaType = mediaType
this.url = url
}
override val name: String = "",
val mediaType: String,
val url: String
) : Object(
type = add(type, "Document")
),
HasName {
override fun equals(other: Any?): Boolean {
if (this === other) return true
if (other !is Document) return false
if (javaClass != other?.javaClass) return false
if (!super.equals(other)) return false
other as Document
if (mediaType != other.mediaType) return false
if (url != other.url) return false
if (name != other.name) return false
return true
}
override fun hashCode(): Int {
var result = super.hashCode()
result = 31 * result + (mediaType?.hashCode() ?: 0)
result = 31 * result + (url?.hashCode() ?: 0)
result = 31 * result + mediaType.hashCode()
result = 31 * result + url.hashCode()
result = 31 * result + name.hashCode()
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
open class Emoji : Object {
var updated: String? = null
var icon: Image? = null
protected constructor() : super()
constructor(
open class Emoji(
type: List<String>,
name: String?,
actor: String?,
id: String?,
updated: String?,
icon: Image?
) : super(
type = add(type, "Emoji"),
name = name,
actor = actor,
id = id
) {
this.updated = updated
this.icon = icon
}
override val name: String,
override val id: String,
val updated: String,
val icon: Image
) : Object(
type = add(type, "Emoji")
),
HasName,
HasId {
override fun equals(other: Any?): Boolean {
if (this === other) return true
@ -35,8 +25,8 @@ open class Emoji : Object {
override fun hashCode(): Int {
var result = super.hashCode()
result = 31 * result + (updated?.hashCode() ?: 0)
result = 31 * result + (icon?.hashCode() ?: 0)
result = 31 * result + updated.hashCode()
result = 31 * result + icon.hashCode()
return result
}

View File

@ -1,37 +1,36 @@
package dev.usbharu.hideout.activitypub.domain.model
import com.fasterxml.jackson.annotation.JsonProperty
import dev.usbharu.hideout.activitypub.domain.model.objects.Object
open class Follow : Object {
var `object`: String? = null
protected constructor() : super()
constructor(
open class Follow(
type: List<String> = emptyList(),
name: String?,
`object`: String?,
actor: String?
) : super(
type = add(type, "Follow"),
name = name,
actor = actor
) {
this.`object` = `object`
}
@JsonProperty("object") val apObject: String,
override val actor: String
) : Object(
type = add(type, "Follow")
),
HasActor {
override fun equals(other: Any?): Boolean {
if (this === other) return true
if (other !is Follow) return false
if (javaClass != other?.javaClass) 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 {
var result = super.hashCode()
result = 31 * result + (`object`?.hashCode() ?: 0)
result = 31 * result + apObject.hashCode()
result = 31 * result + actor.hashCode()
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
open class Image : Object {
private var mediaType: String? = null
private var url: String? = null
protected constructor() : super()
constructor(type: List<String> = emptyList(), name: String, mediaType: String?, url: String?) : super(
add(type, "Image"),
name
open class Image(
type: List<String> = emptyList(),
val mediaType: String,
val url: String
) : Object(
add(type, "Image")
) {
this.mediaType = mediaType
this.url = url
}
override fun equals(other: Any?): Boolean {
if (this === other) return true
if (other !is Image) return false
if (javaClass != other?.javaClass) return false
if (!super.equals(other)) return false
other as Image
if (mediaType != other.mediaType) return false
return url == other.url
if (url != other.url) return false
return true
}
override fun hashCode(): Int {
var result = super.hashCode()
result = 31 * result + (mediaType?.hashCode() ?: 0)
result = 31 * result + (url?.hashCode() ?: 0)
result = 31 * result + mediaType.hashCode()
result = 31 * result + url.hashCode()
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
open class Key : Object {
var owner: String? = null
var publicKeyPem: String? = null
protected constructor() : super()
constructor(
type: List<String>,
name: String,
id: String,
owner: String?,
publicKeyPem: String?
) : super(
type = add(list = type, type = "Key"),
name = name,
id = id
) {
this.owner = owner
this.publicKeyPem = publicKeyPem
}
open class Key(
override val id: String,
val owner: String,
val publicKeyPem: String
) : Object(
type = add(list = emptyList(), type = "Key")
),
HasId {
override fun equals(other: Any?): Boolean {
if (this === other) return true
if (other !is Key) return false
if (javaClass != other?.javaClass) return false
if (!super.equals(other)) return false
other as Key
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 {
var result = super.hashCode()
result = 31 * result + (owner?.hashCode() ?: 0)
result = 31 * result + (publicKeyPem?.hashCode() ?: 0)
result = 31 * result + owner.hashCode()
result = 31 * result + publicKeyPem.hashCode()
result = 31 * result + id.hashCode()
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
import com.fasterxml.jackson.annotation.JsonProperty
import com.fasterxml.jackson.databind.annotation.JsonDeserialize
import dev.usbharu.hideout.activitypub.domain.model.objects.Object
import dev.usbharu.hideout.activitypub.domain.model.objects.ObjectDeserializer
open class Like : Object {
var `object`: String? = null
var content: String? = null
@JsonDeserialize(contentUsing = ObjectDeserializer::class)
var tag: List<Object> = emptyList()
protected constructor() : super()
constructor(
open class Like(
type: List<String> = emptyList(),
name: String?,
actor: String?,
id: String?,
`object`: String?,
content: String?,
tag: List<Object> = emptyList()
) : super(
type = add(type, "Like"),
name = name,
actor = actor,
id = id
) {
this.`object` = `object`
this.content = content
this.tag = tag
}
override val actor: String,
override val id: String,
@JsonProperty("object") val apObject: String,
val content: String,
@JsonDeserialize(contentUsing = ObjectDeserializer::class) val tag: List<Object> = emptyList()
) : Object(
type = add(type, "Like")
),
HasId,
HasActor {
override fun equals(other: Any?): Boolean {
if (this === other) return true
if (other !is Like) return false
if (javaClass != other?.javaClass) 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
return tag == other.tag
if (tag != other.tag) return false
return true
}
override fun hashCode(): Int {
var result = super.hashCode()
result = 31 * result + (`object`?.hashCode() ?: 0)
result = 31 * result + (content?.hashCode() ?: 0)
result = 31 * result + actor.hashCode()
result = 31 * result + id.hashCode()
result = 31 * result + apObject.hashCode()
result = 31 * result + content.hashCode()
result = 31 * result + tag.hashCode()
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
open class Note : Object {
var attributedTo: String? = null
var attachment: List<Document> = emptyList()
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()
open class Note
@Suppress("LongParameterList")
constructor(
type: List<String> = emptyList(),
name: String,
id: String?,
attributedTo: String?,
content: String?,
published: String?,
to: List<String> = emptyList(),
cc: List<String> = emptyList(),
sensitive: Boolean = false,
inReplyTo: String? = null,
attachment: List<Document> = emptyList()
) : super(
type = add(type, "Note"),
name = name,
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 val id: String,
val attributedTo: String,
val content: String,
val published: String,
val to: List<String> = emptyList(),
val cc: List<String> = emptyList(),
val sensitive: Boolean = false,
val inReplyTo: String? = null,
val attachment: List<Document> = emptyList()
) : Object(
type = add(type, "Note")
),
HasId {
override fun equals(other: Any?): Boolean {
if (this === other) return true
if (other !is Note) return false
if (javaClass != other?.javaClass) return false
if (!super.equals(other)) return false
other as Note
if (id != other.id) return false
if (attributedTo != other.attributedTo) return false
if (attachment != other.attachment) return false
if (content != other.content) return false
if (published != other.published) return false
if (to != other.to) return false
if (cc != other.cc) return false
if (sensitive != other.sensitive) return false
if (inReplyTo != other.inReplyTo) return false
if (attachment != other.attachment) return false
return true
}
override fun hashCode(): Int {
var result = super.hashCode()
result = 31 * result + (attributedTo?.hashCode() ?: 0)
result = 31 * result + attachment.hashCode()
result = 31 * result + (content?.hashCode() ?: 0)
result = 31 * result + (published?.hashCode() ?: 0)
result = 31 * result + id.hashCode()
result = 31 * result + attributedTo.hashCode()
result = 31 * result + content.hashCode()
result = 31 * result + published.hashCode()
result = 31 * result + to.hashCode()
result = 31 * result + cc.hashCode()
result = 31 * result + sensitive.hashCode()
result = 31 * result + (inReplyTo?.hashCode() ?: 0)
result = 31 * result + attachment.hashCode()
return result
}
override fun toString(): String {
return "Note(attributedTo=$attributedTo, attachment=$attachment, " +
"content=$content, published=$published, to=$to, cc=$cc, sensitive=$sensitive," +
" inReplyTo=$inReplyTo) ${super.toString()}"
return "Note(" +
"id='$id', " +
"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
open class Person : Object {
var preferredUsername: String? = null
var summary: String? = null
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()
open class Person
@Suppress("LongParameterList")
constructor(
type: List<String> = emptyList(),
name: String,
id: String?,
preferredUsername: String?,
summary: String?,
inbox: String?,
outbox: String?,
url: String?,
icon: Image?,
publicKey: Key?,
endpoints: Map<String, String> = emptyMap(),
followers: String?,
following: String?
) : super(add(type, "Person"), name, id = id) {
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 val name: String,
override val id: String,
var preferredUsername: String?,
var summary: String?,
var inbox: String,
var outbox: String,
var url: String,
private var icon: Image?,
var publicKey: Key,
var endpoints: Map<String, String> = emptyMap(),
var followers: String?,
var following: String?
) : Object(add(type, "Person")), HasId, HasName {
override fun equals(other: Any?): Boolean {
if (this === other) return true
if (other !is Person) return false
if (javaClass != other?.javaClass) 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 (summary != other.summary) return false
if (inbox != other.inbox) return false
@ -57,20 +37,26 @@ open class Person : Object {
if (icon != other.icon) return false
if (publicKey != other.publicKey) return false
if (endpoints != other.endpoints) return false
if (followers != other.followers) return false
if (following != other.following) return false
return true
}
override fun hashCode(): Int {
var result = super.hashCode()
result = 31 * result + name.hashCode()
result = 31 * result + id.hashCode()
result = 31 * result + (preferredUsername?.hashCode() ?: 0)
result = 31 * result + (summary?.hashCode() ?: 0)
result = 31 * result + (inbox?.hashCode() ?: 0)
result = 31 * result + (outbox?.hashCode() ?: 0)
result = 31 * result + (url?.hashCode() ?: 0)
result = 31 * result + inbox.hashCode()
result = 31 * result + outbox.hashCode()
result = 31 * result + url.hashCode()
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 + (followers?.hashCode() ?: 0)
result = 31 * result + (following?.hashCode() ?: 0)
return result
}
}

View File

@ -2,11 +2,24 @@ package dev.usbharu.hideout.activitypub.domain.model
import dev.usbharu.hideout.activitypub.domain.model.objects.Object
open class Tombstone : Object {
constructor(
type: List<String> = emptyList(),
name: String = "Tombstone",
actor: String? = null,
id: String
) : super(add(type, "Tombstone"), name, actor, id)
open class Tombstone(type: List<String> = emptyList(), override val id: String) :
Object(add(type, "Tombstone")),
HasId {
override fun equals(other: Any?): Boolean {
if (this === other) return true
if (javaClass != other?.javaClass) return false
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 dev.usbharu.hideout.activitypub.domain.model.objects.Object
import dev.usbharu.hideout.activitypub.domain.model.objects.ObjectDeserializer
import java.time.Instant
open class Undo : Object {
@JsonDeserialize(using = ObjectDeserializer::class)
var `object`: Object? = null
var published: String? = null
protected constructor() : super()
constructor(
open class Undo(
type: List<String> = emptyList(),
name: String,
actor: String,
id: String?,
`object`: Object,
published: Instant
) : super(add(type, "Undo"), name, actor, id) {
this.`object` = `object`
this.published = published.toString()
}
override val actor: String,
override val id: String,
@JsonDeserialize(using = ObjectDeserializer::class)
@Suppress("VariableNaming") val `object`: Object,
val published: String
) : Object(add(type, "Undo")), HasId, HasActor {
override fun equals(other: Any?): Boolean {
if (this === other) return true
if (other !is Undo) return false
if (javaClass != other?.javaClass) return false
if (!super.equals(other)) return false
other as Undo
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 {
var result = super.hashCode()
result = 31 * result + (`object`?.hashCode() ?: 0)
result = 31 * result + (published?.hashCode() ?: 0)
result = 31 * result + `object`.hashCode()
result = 31 * result + published.hashCode()
result = 31 * result + actor.hashCode()
result = 31 * result + id.hashCode()
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) {
field = value.filter { it.isNotBlank() }
}
var name: String? = null
var actor: String? = null
var id: String? = null
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.name = name
this.actor = actor
this.id = id
}
override fun equals(other: Any?): Boolean {
if (this === other) return true
if (other !is Object) return false
if (javaClass != other?.javaClass) return false
if (!super.equals(other)) return false
if (type != other.type) return false
if (name != other.name) return false
if (actor != other.actor) return false
if (id != other.id) return false
other as Object
return true
return type == other.type
}
override fun hashCode(): Int {
var result = super.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
}
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 {
@JvmStatic

View File

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

View File

@ -1,33 +1,27 @@
package dev.usbharu.hideout.activitypub.domain.model.objects
import com.fasterxml.jackson.annotation.JsonCreator
@Suppress("VariableNaming")
open class ObjectValue : Object {
var `object`: String? = null
protected constructor() : super()
constructor(type: List<String>, name: String?, actor: String?, id: String?, `object`: String?) : super(
type,
name,
actor,
id
open class ObjectValue @JsonCreator constructor(type: List<String>, var `object`: String) : Object(
type
) {
this.`object` = `object`
}
override fun equals(other: Any?): Boolean {
if (this === other) return true
if (other !is ObjectValue) return false
if (javaClass != other?.javaClass) return false
if (!super.equals(other)) return false
other as ObjectValue
return `object` == other.`object`
}
override fun hashCode(): Int {
var result = super.hashCode()
result = 31 * result + (`object`?.hashCode() ?: 0)
result = 31 * result + `object`.hashCode()
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]
)
return Note(
name = "Post",
id = this[Posts.apId],
attributedTo = this[Users.url],
content = this[Posts.text],
@ -80,7 +79,7 @@ class NoteQueryServiceImpl(private val postRepository: PostRepository, private v
private suspend fun Query.toNote(): Note {
return this.groupBy { it[Posts.id] }
.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.") }
}

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

View File

@ -1,16 +1,39 @@
package dev.usbharu.hideout.activitypub.interfaces.api.inbox
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.springframework.http.HttpHeaders.WWW_AUTHENTICATE
import org.springframework.http.HttpStatus
import org.springframework.http.ResponseEntity
import org.springframework.web.bind.annotation.RequestBody
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
class InboxControllerImpl(private val apService: APService) : InboxController {
@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 {
apService.parseActivity(string)
} catch (e: Exception) {
@ -19,7 +42,29 @@ class InboxControllerImpl(private val apService: APService) : InboxController {
}
LOGGER.info("INBOX Processing Activity Type: {}", parseActivity)
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) {
LOGGER.warn("FAILED Process Activity $parseActivity", e)
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 create = Create(
name = "Create Note",
`object` = note,
apObject = note,
actor = note.attributedTo,
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 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.service.job.JobQueueParentService
import io.ktor.http.*
import org.slf4j.LoggerFactory
import org.springframework.beans.factory.annotation.Qualifier
import org.springframework.stereotype.Service
interface APReceiveFollowService {
suspend fun receiveFollow(follow: Follow): ActivityPubResponse
suspend fun receiveFollow(follow: Follow)
}
@Service
@ -20,14 +17,14 @@ class APReceiveFollowServiceImpl(
private val jobQueueParentService: JobQueueParentService,
@Qualifier("activitypub") private val objectMapper: ObjectMapper
) : APReceiveFollowService {
override suspend fun receiveFollow(follow: Follow): ActivityPubResponse {
logger.info("FOLLOW from: {} to: {}", follow.actor, follow.`object`)
override suspend fun receiveFollow(follow: Follow) {
logger.info("FOLLOW from: {} to: {}", follow.actor, follow.apObject)
jobQueueParentService.schedule(ReceiveFollowJob) {
props[ReceiveFollowJob.actor] = follow.actor
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 {

View File

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

View File

@ -39,9 +39,8 @@ class APResourceResolveServiceImpl(
return (cacheManager.getOrWait(key) as APResolveResponse<T>).objects
}
private suspend fun <T : Object> runResolve(url: String, singer: User?, clazz: Class<T>): ResolveResponse {
return APResolveResponse(apRequestService.apGet(url, singer, clazz))
}
private suspend fun <T : Object> runResolve(url: String, singer: User?, clazz: Class<T>): ResolveResponse =
APResolveResponse(apRequestService.apGet(url, singer, clazz))
private fun genCacheKey(url: String, singerId: Long?): String {
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.ObjectMapper
import com.fasterxml.jackson.module.kotlin.readValue
import dev.usbharu.hideout.activitypub.domain.exception.JsonParseException
import dev.usbharu.hideout.activitypub.domain.model.Follow
import dev.usbharu.hideout.activitypub.interfaces.api.common.ActivityPubResponse
import dev.usbharu.hideout.activitypub.service.activity.accept.APAcceptService
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 dev.usbharu.hideout.core.external.job.InboxJob
import dev.usbharu.hideout.core.service.job.JobQueueParentService
import dev.usbharu.httpsignature.common.HttpRequest
import org.slf4j.Logger
import org.slf4j.LoggerFactory
import org.springframework.beans.factory.annotation.Qualifier
@ -20,7 +14,12 @@ import org.springframework.stereotype.Service
interface APService {
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 {
@ -176,13 +175,8 @@ enum class ExtendedVocabulary {
@Service
class APServiceImpl(
private val apReceiveFollowService: APReceiveFollowService,
private val apUndoService: APUndoService,
private val apAcceptService: APAcceptService,
private val apCreateService: APCreateService,
private val apLikeService: APLikeService,
private val apReceiveDeleteService: APReceiveDeleteService,
@Qualifier("activitypub") private val objectMapper: ObjectMapper
@Qualifier("activitypub") private val objectMapper: ObjectMapper,
private val jobQueueParentService: JobQueueParentService
) : APService {
val logger: Logger = LoggerFactory.getLogger(APServiceImpl::class.java)
@ -224,23 +218,21 @@ class APServiceImpl(
}
}
@Suppress("CyclomaticComplexMethod", "NotImplementedDeclaration")
override suspend fun processActivity(json: String, type: ActivityType): ActivityPubResponse {
override suspend fun processActivity(
json: String,
type: ActivityType,
httpRequest: HttpRequest,
map: Map<String, List<String>>
) {
logger.debug("process activity: {}", type)
return when (type) {
ActivityType.Accept -> apAcceptService.receiveAccept(objectMapper.readValue(json))
ActivityType.Follow ->
apReceiveFollowService
.receiveFollow(objectMapper.readValue(json, Follow::class.java))
ActivityType.Create -> apCreateService.receiveCreate(objectMapper.readValue(json))
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.")
}
}
jobQueueParentService.schedule(InboxJob) {
props[it.json] = json
props[it.type] = type.name
val writeValueAsString = objectMapper.writeValueAsString(httpRequest)
println(writeValueAsString)
props[it.httpRequest] = writeValueAsString
props[it.headers] = objectMapper.writeValueAsString(map)
}
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
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.query.NoteQueryService
import dev.usbharu.hideout.activitypub.service.common.APResourceResolveService
@ -91,7 +90,7 @@ class APNoteServiceImpl(
requireNotNull(note.id) { "id is null" }
return try {
noteQueryService.findByApid(note.id!!).first
noteQueryService.findByApid(note.id).first
} catch (_: FailedToGetResourcesException) {
saveNote(note, targetActor, url)
}
@ -99,7 +98,7 @@ class APNoteServiceImpl(
private suspend fun saveNote(note: Note, targetActor: String?, url: String): Note {
val person = apUserService.fetchPersonWithEntity(
note.attributedTo ?: throw IllegalActivityPubObjectException("note.attributedTo is null"),
note.attributedTo,
targetActor
)
@ -128,9 +127,9 @@ class APNoteServiceImpl(
.map {
mediaService.uploadRemoteMedia(
RemoteMedia(
(it.name ?: it.url)!!,
it.url!!,
it.mediaType ?: "application/octet-stream",
it.name,
it.url,
it.mediaType,
description = it.name
)
)
@ -142,13 +141,13 @@ class APNoteServiceImpl(
postBuilder.of(
id = postRepository.generateId(),
userId = person.second.id,
text = note.content.orEmpty(),
text = note.content,
createdAt = Instant.parse(note.published).toEpochMilli(),
visibility = visibility,
url = note.id ?: url,
url = note.id,
replyId = reply?.id,
sensitive = note.sensitive,
apId = note.id ?: url,
apId = note.id,
mediaIds = mediaList
)
)
@ -156,7 +155,7 @@ class APNoteServiceImpl(
}
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 {
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.application.external.Transaction
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.query.FollowerQueryService
import org.slf4j.LoggerFactory
@ -28,20 +29,27 @@ class NoteApApiServiceImpl(
}
Visibility.FOLLOWERS -> {
if (userId == null) {
return@transaction null
}
if (followerQueryService.alreadyFollow(findById.second.userId, userId).not()) {
return@transaction null
}
return@transaction findById.first
return@transaction getFollowersNote(userId, findById)
}
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 {
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.UserService
import org.springframework.stereotype.Service
import org.springframework.transaction.annotation.Transactional
interface APUserService {
suspend fun getPersonByName(name: String): Person
@ -57,13 +58,10 @@ class APUserServiceImpl(
url = userUrl,
icon = Image(
type = emptyList(),
name = "$userUrl/icon.png",
mediaType = "image/png",
url = "$userUrl/icon.png"
),
publicKey = Key(
type = emptyList(),
name = "Public Key",
id = userEntity.keyId,
owner = userUrl,
publicKeyPem = userEntity.publicKey
@ -77,52 +75,33 @@ class APUserServiceImpl(
override suspend fun fetchPerson(url: String, targetActor: String?): Person =
fetchPersonWithEntity(url, targetActor).first
@Transactional
override suspend fun fetchPersonWithEntity(url: String, targetActor: String?): Pair<Person, User> {
return try {
val userEntity = userQueryService.findByUrl(url)
return Person(
type = emptyList(),
name = userEntity.name,
id = url,
preferredUsername = userEntity.name,
summary = userEntity.description,
inbox = "$url/inbox",
outbox = "$url/outbox",
url = url,
icon = Image(
type = emptyList(),
name = "$url/icon.png",
mediaType = "image/png",
url = "$url/icon.png"
),
publicKey = Key(
type = emptyList(),
name = "Public Key",
id = userEntity.keyId,
owner = url,
publicKeyPem = userEntity.publicKey
),
endpoints = mapOf("sharedInbox" to "${applicationConfig.url}/inbox"),
followers = userEntity.followers,
following = userEntity.following
) to userEntity
val id = userEntity.url
return entityToPerson(userEntity, id) to userEntity
} catch (ignore: FailedToGetResourcesException) {
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(
RemoteUserCreateDto(
name = person.preferredUsername
?: throw IllegalActivityPubObjectException("preferredUsername is null"),
domain = url.substringAfter("://").substringBefore("/"),
screenName = (person.name ?: person.preferredUsername)
?: throw IllegalActivityPubObjectException("preferredUsername is null"),
domain = id.substringAfter("://").substringBefore("/"),
screenName = person.name,
description = person.summary.orEmpty(),
inbox = person.inbox ?: throw IllegalActivityPubObjectException("inbox is null"),
outbox = person.outbox ?: throw IllegalActivityPubObjectException("outbox is null"),
url = url,
publicKey = person.publicKey?.publicKeyPem
?: throw IllegalActivityPubObjectException("publicKey is null"),
keyId = person.publicKey?.id ?: throw IllegalActivityPubObjectException("publicKey keyId is null"),
inbox = person.inbox,
outbox = person.outbox,
url = id,
publicKey = person.publicKey.publicKeyPem,
keyId = person.publicKey.id,
following = person.following,
followers = person.followers,
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.ObjectMapper
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.RsaSha256HttpSignatureSigner
import org.springframework.beans.factory.annotation.Qualifier
@ -24,11 +26,13 @@ class ActivityPubConfig {
val objectMapper = jacksonObjectMapper()
.enable(DeserializationFeature.ACCEPT_SINGLE_VALUE_AS_ARRAY)
.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(JsonParser.Feature.ALLOW_COMMENTS, true)
.configure(JsonParser.Feature.ALLOW_SINGLE_QUOTES, true)
.configure(JsonParser.Feature.ALLOW_TRAILING_COMMA, true)
.addMixIn(HttpRequest::class.java, HttpRequestMixIn::class.java)
return objectMapper
}

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