mirror of https://github.com/usbharu/Hideout.git
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:
commit
b9ecd2feff
|
@ -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
|
|
|
@ -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'
|
|
|
@ -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 }}
|
|
|
@ -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 }}
|
|
@ -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'
|
|
|
@ -40,3 +40,6 @@ out/
|
||||||
/src/main/web/generated/
|
/src/main/web/generated/
|
||||||
/stats.html
|
/stats.html
|
||||||
/tomcat/
|
/tomcat/
|
||||||
|
/tomcat-e2e/
|
||||||
|
/e2eTest.log
|
||||||
|
/files/
|
||||||
|
|
|
@ -1,6 +1,5 @@
|
||||||
import org.jetbrains.kotlin.gradle.tasks.KotlinCompile
|
import org.jetbrains.kotlin.gradle.tasks.KotlinCompile
|
||||||
import org.openapitools.generator.gradle.plugin.tasks.GenerateTask
|
import org.openapitools.generator.gradle.plugin.tasks.GenerateTask
|
||||||
import kotlin.math.max
|
|
||||||
|
|
||||||
val ktor_version: String by project
|
val ktor_version: String by project
|
||||||
val kotlin_version: String by project
|
val kotlin_version: String by project
|
||||||
|
@ -13,7 +12,7 @@ plugins {
|
||||||
kotlin("jvm") version "1.8.21"
|
kotlin("jvm") version "1.8.21"
|
||||||
id("org.graalvm.buildtools.native") version "0.9.21"
|
id("org.graalvm.buildtools.native") version "0.9.21"
|
||||||
id("io.gitlab.arturbosch.detekt") version "1.23.1"
|
id("io.gitlab.arturbosch.detekt") version "1.23.1"
|
||||||
id("org.springframework.boot") version "3.1.3"
|
id("org.springframework.boot") version "3.2.0"
|
||||||
kotlin("plugin.spring") version "1.8.21"
|
kotlin("plugin.spring") version "1.8.21"
|
||||||
id("org.openapi.generator") version "7.0.1"
|
id("org.openapi.generator") version "7.0.1"
|
||||||
id("org.jetbrains.kotlinx.kover") version "0.7.4"
|
id("org.jetbrains.kotlinx.kover") version "0.7.4"
|
||||||
|
@ -32,6 +31,10 @@ sourceSets {
|
||||||
compileClasspath += sourceSets.main.get().output
|
compileClasspath += sourceSets.main.get().output
|
||||||
runtimeClasspath += sourceSets.main.get().output
|
runtimeClasspath += sourceSets.main.get().output
|
||||||
}
|
}
|
||||||
|
create("e2eTest") {
|
||||||
|
compileClasspath += sourceSets.main.get().output
|
||||||
|
runtimeClasspath += sourceSets.main.get().output
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
val intTestImplementation by configurations.getting {
|
val intTestImplementation by configurations.getting {
|
||||||
|
@ -41,6 +44,14 @@ val intTestRuntimeOnly by configurations.getting {
|
||||||
extendsFrom(configurations.runtimeOnly.get())
|
extendsFrom(configurations.runtimeOnly.get())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
val e2eTestImplementation by configurations.getting {
|
||||||
|
extendsFrom(configurations.implementation.get())
|
||||||
|
}
|
||||||
|
|
||||||
|
val e2eTestRuntimeOnly by configurations.getting {
|
||||||
|
extendsFrom(configurations.runtimeOnly.get())
|
||||||
|
}
|
||||||
|
|
||||||
val integrationTest = task<Test>("integrationTest") {
|
val integrationTest = task<Test>("integrationTest") {
|
||||||
description = "Runs integration tests."
|
description = "Runs integration tests."
|
||||||
group = "verification"
|
group = "verification"
|
||||||
|
@ -50,19 +61,26 @@ val integrationTest = task<Test>("integrationTest") {
|
||||||
shouldRunAfter("test")
|
shouldRunAfter("test")
|
||||||
|
|
||||||
useJUnitPlatform()
|
useJUnitPlatform()
|
||||||
|
|
||||||
testLogging {
|
|
||||||
events("passed")
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
tasks.check { dependsOn(integrationTest) }
|
val e2eTest = task<Test>("e2eTest") {
|
||||||
|
description = "Runs e2e tests."
|
||||||
|
group = "verification"
|
||||||
|
|
||||||
|
testClassesDirs = sourceSets["e2eTest"].output.classesDirs
|
||||||
|
classpath = sourceSets["e2eTest"].runtimeClasspath
|
||||||
|
shouldRunAfter("test")
|
||||||
|
|
||||||
|
useJUnitPlatform()
|
||||||
|
}
|
||||||
|
|
||||||
|
tasks.check {
|
||||||
|
dependsOn(integrationTest)
|
||||||
|
dependsOn(e2eTest)
|
||||||
|
}
|
||||||
|
|
||||||
tasks.withType<Test> {
|
tasks.withType<Test> {
|
||||||
useJUnitPlatform()
|
useJUnitPlatform()
|
||||||
val cpus = Runtime.getRuntime().availableProcessors()
|
|
||||||
maxParallelForks = max(1, cpus - 1)
|
|
||||||
setForkEvery(4)
|
|
||||||
doFirst {
|
doFirst {
|
||||||
jvmArgs = arrayOf(
|
jvmArgs = arrayOf(
|
||||||
"--add-opens", "java.base/java.lang=ALL-UNNAMED"
|
"--add-opens", "java.base/java.lang=ALL-UNNAMED"
|
||||||
|
@ -180,6 +198,7 @@ dependencies {
|
||||||
implementation("org.postgresql:postgresql:42.6.0")
|
implementation("org.postgresql:postgresql:42.6.0")
|
||||||
implementation("com.twelvemonkeys.imageio:imageio-webp:3.10.0")
|
implementation("com.twelvemonkeys.imageio:imageio-webp:3.10.0")
|
||||||
implementation("org.apache.tika:tika-core:2.9.1")
|
implementation("org.apache.tika:tika-core:2.9.1")
|
||||||
|
implementation("org.apache.tika:tika-parsers:2.9.1")
|
||||||
implementation("net.coobird:thumbnailator:0.4.20")
|
implementation("net.coobird:thumbnailator:0.4.20")
|
||||||
implementation("org.bytedeco:javacv-platform:1.5.9")
|
implementation("org.bytedeco:javacv-platform:1.5.9")
|
||||||
implementation("org.flywaydb:flyway-core")
|
implementation("org.flywaydb:flyway-core")
|
||||||
|
@ -206,6 +225,18 @@ dependencies {
|
||||||
|
|
||||||
intTestImplementation("org.springframework.boot:spring-boot-starter-test")
|
intTestImplementation("org.springframework.boot:spring-boot-starter-test")
|
||||||
intTestImplementation("org.springframework.security:spring-security-test")
|
intTestImplementation("org.springframework.security:spring-security-test")
|
||||||
|
intTestImplementation("org.jetbrains.kotlin:kotlin-test-junit:$kotlin_version")
|
||||||
|
intTestImplementation("org.jetbrains.kotlinx:kotlinx-coroutines-test:1.6.4")
|
||||||
|
intTestImplementation("org.mockito.kotlin:mockito-kotlin:4.1.0")
|
||||||
|
|
||||||
|
e2eTestImplementation("org.springframework.boot:spring-boot-starter-test")
|
||||||
|
e2eTestImplementation("org.springframework.security:spring-security-test")
|
||||||
|
e2eTestImplementation("org.springframework.boot:spring-boot-starter-webflux")
|
||||||
|
e2eTestImplementation("org.jsoup:jsoup:1.17.1")
|
||||||
|
e2eTestImplementation("com.intuit.karate:karate-junit5:1.4.1")
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -3,9 +3,7 @@ build:
|
||||||
weights:
|
weights:
|
||||||
Indentation: 0
|
Indentation: 0
|
||||||
MagicNumber: 0
|
MagicNumber: 0
|
||||||
InjectDispatcher: 0
|
|
||||||
EnumEntryNameCase: 0
|
EnumEntryNameCase: 0
|
||||||
ReplaceSafeCallChainWithRun: 0
|
|
||||||
VariableNaming: 0
|
VariableNaming: 0
|
||||||
NoNameShadowing: 0
|
NoNameShadowing: 0
|
||||||
|
|
||||||
|
@ -78,7 +76,7 @@ complexity:
|
||||||
active: true
|
active: true
|
||||||
|
|
||||||
ReplaceSafeCallChainWithRun:
|
ReplaceSafeCallChainWithRun:
|
||||||
active: true
|
active: false
|
||||||
|
|
||||||
StringLiteralDuplication:
|
StringLiteralDuplication:
|
||||||
active: false
|
active: false
|
||||||
|
@ -172,3 +170,5 @@ potential-bugs:
|
||||||
coroutines:
|
coroutines:
|
||||||
RedundantSuspendModifier:
|
RedundantSuspendModifier:
|
||||||
active: false
|
active: false
|
||||||
|
InjectDispatcher:
|
||||||
|
active: false
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
ktor_version=2.3.0
|
ktor_version=2.3.0
|
||||||
kotlin_version=1.8.21
|
kotlin_version=1.9.21
|
||||||
logback_version=1.4.6
|
logback_version=1.4.6
|
||||||
kotlin.code.style=official
|
kotlin.code.style=official
|
||||||
exposed_version=0.44.0
|
exposed_version=0.44.0
|
||||||
|
|
|
@ -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()
|
||||||
|
}
|
||||||
|
}
|
|
@ -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")
|
||||||
|
}
|
||||||
|
}
|
|
@ -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()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -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()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -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()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -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
|
|
@ -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
|
|
@ -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
|
|
@ -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
|
|
@ -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
|
|
@ -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;
|
||||||
|
}
|
|
@ -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>
|
|
@ -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
|
|
@ -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);
|
|
@ -1,6 +1,8 @@
|
||||||
package activitypub.inbox
|
package activitypub.inbox
|
||||||
|
|
||||||
import dev.usbharu.hideout.SpringApplication
|
import dev.usbharu.hideout.SpringApplication
|
||||||
|
import org.flywaydb.core.Flyway
|
||||||
|
import org.junit.jupiter.api.AfterAll
|
||||||
import org.junit.jupiter.api.BeforeEach
|
import org.junit.jupiter.api.BeforeEach
|
||||||
import org.junit.jupiter.api.Test
|
import org.junit.jupiter.api.Test
|
||||||
import org.springframework.beans.factory.annotation.Autowired
|
import org.springframework.beans.factory.annotation.Autowired
|
||||||
|
@ -45,6 +47,7 @@ class InboxTest {
|
||||||
content = "{}"
|
content = "{}"
|
||||||
contentType = MediaType.APPLICATION_JSON
|
contentType = MediaType.APPLICATION_JSON
|
||||||
}
|
}
|
||||||
|
.asyncDispatch()
|
||||||
.andExpect { status { isUnauthorized() } }
|
.andExpect { status { isUnauthorized() } }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -55,6 +58,7 @@ class InboxTest {
|
||||||
.post("/inbox") {
|
.post("/inbox") {
|
||||||
content = "{}"
|
content = "{}"
|
||||||
contentType = MediaType.APPLICATION_JSON
|
contentType = MediaType.APPLICATION_JSON
|
||||||
|
header("Signature", "")
|
||||||
}
|
}
|
||||||
.asyncDispatch()
|
.asyncDispatch()
|
||||||
.andExpect { status { isAccepted() } }
|
.andExpect { status { isAccepted() } }
|
||||||
|
@ -68,6 +72,7 @@ class InboxTest {
|
||||||
content = "{}"
|
content = "{}"
|
||||||
contentType = MediaType.APPLICATION_JSON
|
contentType = MediaType.APPLICATION_JSON
|
||||||
}
|
}
|
||||||
|
.asyncDispatch()
|
||||||
.andExpect { status { isUnauthorized() } }
|
.andExpect { status { isUnauthorized() } }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -78,6 +83,7 @@ class InboxTest {
|
||||||
.post("/users/hoge/inbox") {
|
.post("/users/hoge/inbox") {
|
||||||
content = "{}"
|
content = "{}"
|
||||||
contentType = MediaType.APPLICATION_JSON
|
contentType = MediaType.APPLICATION_JSON
|
||||||
|
header("Signature", "")
|
||||||
}
|
}
|
||||||
.asyncDispatch()
|
.asyncDispatch()
|
||||||
.andExpect { status { isAccepted() } }
|
.andExpect { status { isAccepted() } }
|
||||||
|
@ -88,4 +94,13 @@ class InboxTest {
|
||||||
@Bean
|
@Bean
|
||||||
fun testTransaction() = TestTransaction
|
fun testTransaction() = TestTransaction
|
||||||
}
|
}
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
@JvmStatic
|
||||||
|
@AfterAll
|
||||||
|
fun dropDatabase(@Autowired flyway: Flyway) {
|
||||||
|
flyway.clean()
|
||||||
|
flyway.migrate()
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,6 +1,8 @@
|
||||||
package activitypub.note
|
package activitypub.note
|
||||||
|
|
||||||
import dev.usbharu.hideout.SpringApplication
|
import dev.usbharu.hideout.SpringApplication
|
||||||
|
import org.flywaydb.core.Flyway
|
||||||
|
import org.junit.jupiter.api.AfterAll
|
||||||
import org.junit.jupiter.api.BeforeEach
|
import org.junit.jupiter.api.BeforeEach
|
||||||
import org.junit.jupiter.api.Test
|
import org.junit.jupiter.api.Test
|
||||||
import org.springframework.beans.factory.annotation.Autowired
|
import org.springframework.beans.factory.annotation.Autowired
|
||||||
|
@ -178,4 +180,13 @@ class NoteTest {
|
||||||
.andExpect { jsonPath("\$.attachment[1].type") { value("Document") } }
|
.andExpect { jsonPath("\$.attachment[1].type") { value("Document") } }
|
||||||
.andExpect { jsonPath("\$.attachment[1].url") { value("https://example.com/media/test-media2.png") } }
|
.andExpect { jsonPath("\$.attachment[1].url") { value("https://example.com/media/test-media2.png") } }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
@JvmStatic
|
||||||
|
@AfterAll
|
||||||
|
fun dropDatabase(@Autowired flyway: Flyway) {
|
||||||
|
flyway.clean()
|
||||||
|
flyway.migrate()
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -2,6 +2,8 @@ package activitypub.webfinger
|
||||||
|
|
||||||
import dev.usbharu.hideout.SpringApplication
|
import dev.usbharu.hideout.SpringApplication
|
||||||
import dev.usbharu.hideout.application.external.Transaction
|
import dev.usbharu.hideout.application.external.Transaction
|
||||||
|
import org.flywaydb.core.Flyway
|
||||||
|
import org.junit.jupiter.api.AfterAll
|
||||||
import org.junit.jupiter.api.Test
|
import org.junit.jupiter.api.Test
|
||||||
import org.springframework.beans.factory.annotation.Autowired
|
import org.springframework.beans.factory.annotation.Autowired
|
||||||
import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc
|
import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc
|
||||||
|
@ -24,6 +26,7 @@ class WebFingerTest {
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
@Sql("/sql/test-user.sql")
|
@Sql("/sql/test-user.sql")
|
||||||
|
|
||||||
fun `webfinger 存在するユーザーを取得`() {
|
fun `webfinger 存在するユーザーを取得`() {
|
||||||
mockMvc
|
mockMvc
|
||||||
.get("/.well-known/webfinger?resource=acct:test-user@example.com")
|
.get("/.well-known/webfinger?resource=acct:test-user@example.com")
|
||||||
|
@ -52,7 +55,7 @@ class WebFingerTest {
|
||||||
@Test
|
@Test
|
||||||
fun `webfinger 存在しないユーザーに404`() {
|
fun `webfinger 存在しないユーザーに404`() {
|
||||||
mockMvc
|
mockMvc
|
||||||
.get("/.well-known/webfinger?resource=acct:test-user@example.com")
|
.get("/.well-known/webfinger?resource=acct:invalid-user-notfound-afdjashfal@example.com")
|
||||||
.andExpect { status { isNotFound() } }
|
.andExpect { status { isNotFound() } }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -80,4 +83,13 @@ class WebFingerTest {
|
||||||
@Bean
|
@Bean
|
||||||
fun testTransaction(): Transaction = TestTransaction
|
fun testTransaction(): Transaction = TestTransaction
|
||||||
}
|
}
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
@JvmStatic
|
||||||
|
@AfterAll
|
||||||
|
fun dropDatabase(@Autowired flyway: Flyway) {
|
||||||
|
flyway.clean()
|
||||||
|
flyway.migrate()
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -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()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -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()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -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()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -8,18 +8,15 @@ hideout:
|
||||||
private-key: "MIIEvwIBADANBgkqhkiG9w0BAQEFAASCBKkwggSlAgEAAoIBAQC7VJTUt9Us8cKjMzEfYyjiWA4R4/M2bS1GB4t7NXp98C3SC6dVMvDuictGeurT8jNbvJZHtCSuYEvuNMoSfm76oqFvAp8Gy0iz5sxjZmSnXyCdPEovGhLa0VzMaQ8s+CLOyS56YyCFGeJZqgtzJ6GR3eqoYSW9b9UMvkBpZODSctWSNGj3P7jRFDO5VoTwCQAWbFnOjDfH5Ulgp2PKSQnSJP3AJLQNFNe7br1XbrhV//eO+t51mIpGSDCUv3E0DDFcWDTH9cXDTTlRZVEiR2BwpZOOkE/Z0/BVnhZYL71oZV34bKfWjQIt6V/isSMahdsAASACp4ZTGtwiVuNd9tybAgMBAAECggEBAKTmjaS6tkK8BlPXClTQ2vpz/N6uxDeS35mXpqasqskVlaAidgg/sWqpjXDbXr93otIMLlWsM+X0CqMDgSXKejLS2jx4GDjI1ZTXg++0AMJ8sJ74pWzVDOfmCEQ/7wXs3+cbnXhKriO8Z036q92Qc1+N87SI38nkGa0ABH9CN83HmQqt4fB7UdHzuIRe/me2PGhIq5ZBzj6h3BpoPGzEP+x3l9YmK8t/1cN0pqI+dQwYdgfGjackLu/2qH80MCF7IyQaseZUOJyKrCLtSD/Iixv/hzDEUPfOCjFDgTpzf3cwta8+oE4wHCo1iI1/4TlPkwmXx4qSXtmw4aQPz7IDQvECgYEA8KNThCO2gsC2I9PQDM/8Cw0O983WCDY+oi+7JPiNAJwv5DYBqEZB1QYdj06YD16XlC/HAZMsMku1na2TN0driwenQQWzoev3g2S7gRDoS/FCJSI3jJ+kjgtaA7Qmzlgk1TxODN+G1H91HW7t0l7VnL27IWyYo2qRRK3jzxqUiPUCgYEAx0oQs2reBQGMVZnApD1jeq7n4MvNLcPvt8b/eU9iUv6Y4Mj0Suo/AU8lYZXm8ubbqAlwz2VSVunD2tOplHyMUrtCtObAfVDUAhCndKaA9gApgfb3xw1IKbuQ1u4IF1FJl3VtumfQn//LiH1B3rXhcdyo3/vIttEk48RakUKClU8CgYEAzV7W3COOlDDcQd935DdtKBFRAPRPAlspQUnzMi5eSHMD/ISLDY5IiQHbIH83D4bvXq0X7qQoSBSNP7Dvv3HYuqMhf0DaegrlBuJllFVVq9qPVRnKxt1Il2HgxOBvbhOT+9in1BzA+YJ99UzC85O0Qz06A+CmtHEy4aZ2kj5hHjECgYEAmNS4+A8Fkss8Js1RieK2LniBxMgmYml3pfVLKGnzmng7H2+cwPLhPIzIuwytXywh2bzbsYEfYx3EoEVgMEpPhoarQnYPukrJO4gwE2o5Te6T5mJSZGlQJQj9q4ZB2Dfzet6INsK0oG8XVGXSpQvQh3RUYekCZQkBBFcpqWpbIEsCgYAnM3DQf3FJoSnXaMhrVBIovic5l0xFkEHskAjFTevO86Fsz1C2aSeRKSqGFoOQ0tmJzBEs1R6KqnHInicDTQrKhArgLXX4v3CddjfTRJkFWDbE/CkvKZNOrcf1nhaGCPspRJj2KUkj1Fhl9Cncdn/RsYEONbwQSjIfMPkvxF+8HQ=="
|
private-key: "MIIEvwIBADANBgkqhkiG9w0BAQEFAASCBKkwggSlAgEAAoIBAQC7VJTUt9Us8cKjMzEfYyjiWA4R4/M2bS1GB4t7NXp98C3SC6dVMvDuictGeurT8jNbvJZHtCSuYEvuNMoSfm76oqFvAp8Gy0iz5sxjZmSnXyCdPEovGhLa0VzMaQ8s+CLOyS56YyCFGeJZqgtzJ6GR3eqoYSW9b9UMvkBpZODSctWSNGj3P7jRFDO5VoTwCQAWbFnOjDfH5Ulgp2PKSQnSJP3AJLQNFNe7br1XbrhV//eO+t51mIpGSDCUv3E0DDFcWDTH9cXDTTlRZVEiR2BwpZOOkE/Z0/BVnhZYL71oZV34bKfWjQIt6V/isSMahdsAASACp4ZTGtwiVuNd9tybAgMBAAECggEBAKTmjaS6tkK8BlPXClTQ2vpz/N6uxDeS35mXpqasqskVlaAidgg/sWqpjXDbXr93otIMLlWsM+X0CqMDgSXKejLS2jx4GDjI1ZTXg++0AMJ8sJ74pWzVDOfmCEQ/7wXs3+cbnXhKriO8Z036q92Qc1+N87SI38nkGa0ABH9CN83HmQqt4fB7UdHzuIRe/me2PGhIq5ZBzj6h3BpoPGzEP+x3l9YmK8t/1cN0pqI+dQwYdgfGjackLu/2qH80MCF7IyQaseZUOJyKrCLtSD/Iixv/hzDEUPfOCjFDgTpzf3cwta8+oE4wHCo1iI1/4TlPkwmXx4qSXtmw4aQPz7IDQvECgYEA8KNThCO2gsC2I9PQDM/8Cw0O983WCDY+oi+7JPiNAJwv5DYBqEZB1QYdj06YD16XlC/HAZMsMku1na2TN0driwenQQWzoev3g2S7gRDoS/FCJSI3jJ+kjgtaA7Qmzlgk1TxODN+G1H91HW7t0l7VnL27IWyYo2qRRK3jzxqUiPUCgYEAx0oQs2reBQGMVZnApD1jeq7n4MvNLcPvt8b/eU9iUv6Y4Mj0Suo/AU8lYZXm8ubbqAlwz2VSVunD2tOplHyMUrtCtObAfVDUAhCndKaA9gApgfb3xw1IKbuQ1u4IF1FJl3VtumfQn//LiH1B3rXhcdyo3/vIttEk48RakUKClU8CgYEAzV7W3COOlDDcQd935DdtKBFRAPRPAlspQUnzMi5eSHMD/ISLDY5IiQHbIH83D4bvXq0X7qQoSBSNP7Dvv3HYuqMhf0DaegrlBuJllFVVq9qPVRnKxt1Il2HgxOBvbhOT+9in1BzA+YJ99UzC85O0Qz06A+CmtHEy4aZ2kj5hHjECgYEAmNS4+A8Fkss8Js1RieK2LniBxMgmYml3pfVLKGnzmng7H2+cwPLhPIzIuwytXywh2bzbsYEfYx3EoEVgMEpPhoarQnYPukrJO4gwE2o5Te6T5mJSZGlQJQj9q4ZB2Dfzet6INsK0oG8XVGXSpQvQh3RUYekCZQkBBFcpqWpbIEsCgYAnM3DQf3FJoSnXaMhrVBIovic5l0xFkEHskAjFTevO86Fsz1C2aSeRKSqGFoOQ0tmJzBEs1R6KqnHInicDTQrKhArgLXX4v3CddjfTRJkFWDbE/CkvKZNOrcf1nhaGCPspRJj2KUkj1Fhl9Cncdn/RsYEONbwQSjIfMPkvxF+8HQ=="
|
||||||
public-key: "MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAu1SU1LfVLPHCozMxH2Mo4lgOEePzNm0tRgeLezV6ffAt0gunVTLw7onLRnrq0/IzW7yWR7QkrmBL7jTKEn5u+qKhbwKfBstIs+bMY2Zkp18gnTxKLxoS2tFczGkPLPgizskuemMghRniWaoLcyehkd3qqGElvW/VDL5AaWTg0nLVkjRo9z+40RQzuVaE8AkAFmxZzow3x+VJYKdjykkJ0iT9wCS0DRTXu269V264Vf/3jvredZiKRkgwlL9xNAwxXFg0x/XFw005UWVRIkdgcKWTjpBP2dPwVZ4WWC+9aGVd+Gyn1o0CLelf4rEjGoXbAAEgAqeGUxrcIlbjXfbcmwIDAQAB"
|
public-key: "MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAu1SU1LfVLPHCozMxH2Mo4lgOEePzNm0tRgeLezV6ffAt0gunVTLw7onLRnrq0/IzW7yWR7QkrmBL7jTKEn5u+qKhbwKfBstIs+bMY2Zkp18gnTxKLxoS2tFczGkPLPgizskuemMghRniWaoLcyehkd3qqGElvW/VDL5AaWTg0nLVkjRo9z+40RQzuVaE8AkAFmxZzow3x+VJYKdjykkJ0iT9wCS0DRTXu269V264Vf/3jvredZiKRkgwlL9xNAwxXFg0x/XFw005UWVRIkdgcKWTjpBP2dPwVZ4WWC+9aGVd+Gyn1o0CLelf4rEjGoXbAAEgAqeGUxrcIlbjXfbcmwIDAQAB"
|
||||||
storage:
|
storage:
|
||||||
use-s3: true
|
type: local
|
||||||
endpoint: "http://localhost:8082/test-hideout"
|
|
||||||
public-url: "http://localhost:8082/test-hideout"
|
|
||||||
bucket: "test-hideout"
|
|
||||||
region: "auto"
|
|
||||||
access-key: ""
|
|
||||||
secret-key: ""
|
|
||||||
|
|
||||||
spring:
|
spring:
|
||||||
|
flyway:
|
||||||
|
enabled: true
|
||||||
|
clean-disabled: false
|
||||||
datasource:
|
datasource:
|
||||||
driver-class-name: org.h2.Driver
|
driver-class-name: org.h2.Driver
|
||||||
url: "jdbc:h2:mem:test;MODE=POSTGRESQL;DB_CLOSE_DELAY=-1"
|
url: "jdbc:h2:mem:test;MODE=POSTGRESQL;DB_CLOSE_DELAY=-1;CASE_INSENSITIVE_IDENTIFIERS=true;TRACE_LEVEL_FILE=4;"
|
||||||
username: ""
|
username: ""
|
||||||
password:
|
password:
|
||||||
data:
|
data:
|
||||||
|
@ -32,8 +29,8 @@ spring:
|
||||||
console:
|
console:
|
||||||
enabled: true
|
enabled: true
|
||||||
|
|
||||||
exposed:
|
# exposed:
|
||||||
generate-ddl: true
|
# generate-ddl: true
|
||||||
excluded-packages: dev.usbharu.hideout.core.infrastructure.kjobexposed
|
# excluded-packages: dev.usbharu.hideout.core.infrastructure.kjobexposed
|
||||||
server:
|
server:
|
||||||
port: 8080
|
port: 8080
|
||||||
|
|
|
@ -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
|
|
@ -7,4 +7,5 @@
|
||||||
<root level="TRACE">
|
<root level="TRACE">
|
||||||
<appender-ref ref="STDOUT"/>
|
<appender-ref ref="STDOUT"/>
|
||||||
</root>
|
</root>
|
||||||
|
<logger name="org.springframework.security" level="TRACE"/>
|
||||||
</configuration>
|
</configuration>
|
||||||
|
|
Binary file not shown.
After Width: | Height: | Size: 7.1 KiB |
|
@ -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');
|
|
@ -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
|
||||||
|
}
|
||||||
|
}
|
|
@ -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
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,10 +1,16 @@
|
||||||
package dev.usbharu.hideout.activitypub.domain.exception
|
package dev.usbharu.hideout.activitypub.domain.exception
|
||||||
|
|
||||||
import dev.usbharu.hideout.core.domain.exception.FailedToGetResourcesException
|
import dev.usbharu.hideout.core.domain.exception.FailedToGetResourcesException
|
||||||
|
import java.io.Serial
|
||||||
|
|
||||||
class FailedToGetActivityPubResourceException : FailedToGetResourcesException {
|
class FailedToGetActivityPubResourceException : FailedToGetResourcesException {
|
||||||
constructor() : super()
|
constructor() : super()
|
||||||
constructor(s: String?) : super(s)
|
constructor(s: String?) : super(s)
|
||||||
constructor(message: String?, cause: Throwable?) : super(message, cause)
|
constructor(message: String?, cause: Throwable?) : super(message, cause)
|
||||||
constructor(cause: Throwable?) : super(cause)
|
constructor(cause: Throwable?) : super(cause)
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
@Serial
|
||||||
|
private const val serialVersionUID: Long = 6420233106776818052L
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,40 +1,52 @@
|
||||||
package dev.usbharu.hideout.activitypub.domain.model
|
package dev.usbharu.hideout.activitypub.domain.model
|
||||||
|
|
||||||
|
import com.fasterxml.jackson.annotation.JsonCreator
|
||||||
|
import com.fasterxml.jackson.annotation.JsonProperty
|
||||||
import com.fasterxml.jackson.databind.annotation.JsonDeserialize
|
import com.fasterxml.jackson.databind.annotation.JsonDeserialize
|
||||||
import dev.usbharu.hideout.activitypub.domain.model.objects.Object
|
import dev.usbharu.hideout.activitypub.domain.model.objects.Object
|
||||||
import dev.usbharu.hideout.activitypub.domain.model.objects.ObjectDeserializer
|
import dev.usbharu.hideout.activitypub.domain.model.objects.ObjectDeserializer
|
||||||
|
|
||||||
open class Accept : Object {
|
open class Accept @JsonCreator constructor(
|
||||||
|
type: List<String> = emptyList(),
|
||||||
|
override val name: String,
|
||||||
@JsonDeserialize(using = ObjectDeserializer::class)
|
@JsonDeserialize(using = ObjectDeserializer::class)
|
||||||
var `object`: Object? = null
|
@JsonProperty("object")
|
||||||
|
val apObject: Object,
|
||||||
protected constructor()
|
override val actor: String
|
||||||
constructor(
|
) : Object(
|
||||||
type: List<String> = emptyList(),
|
type = add(type, "Accept")
|
||||||
name: String,
|
),
|
||||||
`object`: Object?,
|
HasActor,
|
||||||
actor: String?
|
HasName {
|
||||||
) : super(
|
|
||||||
type = add(type, "Accept"),
|
|
||||||
name = name,
|
|
||||||
actor = actor
|
|
||||||
) {
|
|
||||||
this.`object` = `object`
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun toString(): String = "Accept(`object`=$`object`) ${super.toString()}"
|
|
||||||
|
|
||||||
override fun equals(other: Any?): Boolean {
|
override fun equals(other: Any?): Boolean {
|
||||||
if (this === other) return true
|
if (this === other) return true
|
||||||
if (other !is Accept) return false
|
if (javaClass != other?.javaClass) return false
|
||||||
if (!super.equals(other)) return false
|
if (!super.equals(other)) return false
|
||||||
|
|
||||||
return `object` == other.`object`
|
other as Accept
|
||||||
|
|
||||||
|
if (name != other.name) return false
|
||||||
|
if (apObject != other.apObject) return false
|
||||||
|
if (actor != other.actor) return false
|
||||||
|
|
||||||
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun hashCode(): Int {
|
override fun hashCode(): Int {
|
||||||
var result = super.hashCode()
|
var result = super.hashCode()
|
||||||
result = 31 * result + (`object`?.hashCode() ?: 0)
|
result = 31 * result + name.hashCode()
|
||||||
|
result = 31 * result + apObject.hashCode()
|
||||||
|
result = 31 * result + actor.hashCode()
|
||||||
return result
|
return result
|
||||||
}
|
}
|
||||||
|
|
||||||
|
override fun toString(): String {
|
||||||
|
return "Accept(" +
|
||||||
|
"name='$name', " +
|
||||||
|
"apObject=$apObject, " +
|
||||||
|
"actor='$actor'" +
|
||||||
|
")" +
|
||||||
|
" ${super.toString()}"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,48 +1,64 @@
|
||||||
package dev.usbharu.hideout.activitypub.domain.model
|
package dev.usbharu.hideout.activitypub.domain.model
|
||||||
|
|
||||||
|
import com.fasterxml.jackson.annotation.JsonProperty
|
||||||
import com.fasterxml.jackson.databind.annotation.JsonDeserialize
|
import com.fasterxml.jackson.databind.annotation.JsonDeserialize
|
||||||
import dev.usbharu.hideout.activitypub.domain.model.objects.Object
|
import dev.usbharu.hideout.activitypub.domain.model.objects.Object
|
||||||
import dev.usbharu.hideout.activitypub.domain.model.objects.ObjectDeserializer
|
import dev.usbharu.hideout.activitypub.domain.model.objects.ObjectDeserializer
|
||||||
|
|
||||||
open class Create : Object {
|
open class Create(
|
||||||
|
type: List<String> = emptyList(),
|
||||||
|
override val name: String,
|
||||||
@JsonDeserialize(using = ObjectDeserializer::class)
|
@JsonDeserialize(using = ObjectDeserializer::class)
|
||||||
var `object`: Object? = null
|
@JsonProperty("object")
|
||||||
var to: List<String> = emptyList()
|
val apObject: Object,
|
||||||
var cc: List<String> = emptyList()
|
override val actor: String,
|
||||||
|
override val id: String,
|
||||||
protected constructor() : super()
|
val to: List<String> = emptyList(),
|
||||||
constructor(
|
val cc: List<String> = emptyList()
|
||||||
type: List<String> = emptyList(),
|
) : Object(
|
||||||
name: String? = null,
|
type = add(type, "Create")
|
||||||
`object`: Object?,
|
),
|
||||||
actor: String? = null,
|
HasId,
|
||||||
id: String? = null,
|
HasName,
|
||||||
to: List<String> = emptyList(),
|
HasActor {
|
||||||
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 fun equals(other: Any?): Boolean {
|
override fun equals(other: Any?): Boolean {
|
||||||
if (this === other) return true
|
if (this === other) return true
|
||||||
if (other !is Create) return false
|
if (javaClass != other?.javaClass) return false
|
||||||
if (!super.equals(other)) return false
|
if (!super.equals(other)) return false
|
||||||
|
|
||||||
return `object` == other.`object`
|
other as Create
|
||||||
|
|
||||||
|
if (name != other.name) return false
|
||||||
|
if (apObject != other.apObject) return false
|
||||||
|
if (actor != other.actor) return false
|
||||||
|
if (id != other.id) return false
|
||||||
|
if (to != other.to) return false
|
||||||
|
if (cc != other.cc) return false
|
||||||
|
|
||||||
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun hashCode(): Int {
|
override fun hashCode(): Int {
|
||||||
var result = super.hashCode()
|
var result = super.hashCode()
|
||||||
result = 31 * result + (`object`?.hashCode() ?: 0)
|
result = 31 * result + name.hashCode()
|
||||||
|
result = 31 * result + apObject.hashCode()
|
||||||
|
result = 31 * result + actor.hashCode()
|
||||||
|
result = 31 * result + id.hashCode()
|
||||||
|
result = 31 * result + to.hashCode()
|
||||||
|
result = 31 * result + cc.hashCode()
|
||||||
return result
|
return result
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun toString(): String = "Create(`object`=$`object`) ${super.toString()}"
|
override fun toString(): String {
|
||||||
|
return "Create(" +
|
||||||
|
"name='$name', " +
|
||||||
|
"apObject=$apObject, " +
|
||||||
|
"actor='$actor', " +
|
||||||
|
"id='$id', " +
|
||||||
|
"to=$to, " +
|
||||||
|
"cc=$cc" +
|
||||||
|
")" +
|
||||||
|
" ${super.toString()}"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,45 +1,55 @@
|
||||||
package dev.usbharu.hideout.activitypub.domain.model
|
package dev.usbharu.hideout.activitypub.domain.model
|
||||||
|
|
||||||
|
import com.fasterxml.jackson.annotation.JsonProperty
|
||||||
import com.fasterxml.jackson.databind.annotation.JsonDeserialize
|
import com.fasterxml.jackson.databind.annotation.JsonDeserialize
|
||||||
import dev.usbharu.hideout.activitypub.domain.model.objects.Object
|
import dev.usbharu.hideout.activitypub.domain.model.objects.Object
|
||||||
import dev.usbharu.hideout.activitypub.domain.model.objects.ObjectDeserializer
|
import dev.usbharu.hideout.activitypub.domain.model.objects.ObjectDeserializer
|
||||||
|
|
||||||
open class Delete : Object {
|
open class Delete : Object, HasId, HasActor {
|
||||||
@JsonDeserialize(using = ObjectDeserializer::class)
|
@JsonDeserialize(using = ObjectDeserializer::class)
|
||||||
var `object`: Object? = null
|
@JsonProperty("object")
|
||||||
var published: String? = null
|
val apObject: Object
|
||||||
|
val published: String
|
||||||
|
override val actor: String
|
||||||
|
override val id: String
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
type: List<String> = emptyList(),
|
type: List<String> = emptyList(),
|
||||||
name: String? = "Delete",
|
|
||||||
actor: String,
|
actor: String,
|
||||||
id: String,
|
id: String,
|
||||||
`object`: Object,
|
`object`: Object,
|
||||||
published: String?
|
published: String
|
||||||
) : super(add(type, "Delete"), name, actor, id) {
|
) : super(add(type, "Delete")) {
|
||||||
this.`object` = `object`
|
this.apObject = `object`
|
||||||
this.published = published
|
this.published = published
|
||||||
|
this.actor = actor
|
||||||
|
this.id = id
|
||||||
}
|
}
|
||||||
|
|
||||||
protected constructor() : super()
|
|
||||||
|
|
||||||
override fun equals(other: Any?): Boolean {
|
override fun equals(other: Any?): Boolean {
|
||||||
if (this === other) return true
|
if (this === other) return true
|
||||||
if (other !is Delete) return false
|
if (javaClass != other?.javaClass) return false
|
||||||
if (!super.equals(other)) return false
|
if (!super.equals(other)) return false
|
||||||
|
|
||||||
if (`object` != other.`object`) return false
|
other as Delete
|
||||||
|
|
||||||
|
if (apObject != other.apObject) return false
|
||||||
if (published != other.published) return false
|
if (published != other.published) return false
|
||||||
|
if (actor != other.actor) return false
|
||||||
|
if (id != other.id) return false
|
||||||
|
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun hashCode(): Int {
|
override fun hashCode(): Int {
|
||||||
var result = super.hashCode()
|
var result = super.hashCode()
|
||||||
result = 31 * result + (`object`?.hashCode() ?: 0)
|
result = 31 * result + apObject.hashCode()
|
||||||
result = 31 * result + (published?.hashCode() ?: 0)
|
result = 31 * result + published.hashCode()
|
||||||
|
result = 31 * result + actor.hashCode()
|
||||||
|
result = 31 * result + id.hashCode()
|
||||||
return result
|
return result
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun toString(): String = "Delete(`object`=$`object`, published=$published) ${super.toString()}"
|
override fun toString(): String =
|
||||||
|
"Delete(`object`=$apObject, published=$published, actor='$actor', id='$id') ${super.toString()}"
|
||||||
}
|
}
|
||||||
|
|
|
@ -2,44 +2,37 @@ package dev.usbharu.hideout.activitypub.domain.model
|
||||||
|
|
||||||
import dev.usbharu.hideout.activitypub.domain.model.objects.Object
|
import dev.usbharu.hideout.activitypub.domain.model.objects.Object
|
||||||
|
|
||||||
open class Document : Object {
|
open class Document(
|
||||||
|
type: List<String> = emptyList(),
|
||||||
var mediaType: String? = null
|
override val name: String = "",
|
||||||
var url: String? = null
|
val mediaType: String,
|
||||||
|
val url: String
|
||||||
protected constructor() : super()
|
) : Object(
|
||||||
constructor(
|
type = add(type, "Document")
|
||||||
type: List<String> = emptyList(),
|
),
|
||||||
name: String? = null,
|
HasName {
|
||||||
mediaType: String,
|
|
||||||
url: String
|
|
||||||
) : super(
|
|
||||||
type = add(type, "Document"),
|
|
||||||
name = name,
|
|
||||||
actor = null,
|
|
||||||
id = null
|
|
||||||
) {
|
|
||||||
this.mediaType = mediaType
|
|
||||||
this.url = url
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun equals(other: Any?): Boolean {
|
override fun equals(other: Any?): Boolean {
|
||||||
if (this === other) return true
|
if (this === other) return true
|
||||||
if (other !is Document) return false
|
if (javaClass != other?.javaClass) return false
|
||||||
if (!super.equals(other)) return false
|
if (!super.equals(other)) return false
|
||||||
|
|
||||||
|
other as Document
|
||||||
|
|
||||||
if (mediaType != other.mediaType) return false
|
if (mediaType != other.mediaType) return false
|
||||||
if (url != other.url) return false
|
if (url != other.url) return false
|
||||||
|
if (name != other.name) return false
|
||||||
|
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun hashCode(): Int {
|
override fun hashCode(): Int {
|
||||||
var result = super.hashCode()
|
var result = super.hashCode()
|
||||||
result = 31 * result + (mediaType?.hashCode() ?: 0)
|
result = 31 * result + mediaType.hashCode()
|
||||||
result = 31 * result + (url?.hashCode() ?: 0)
|
result = 31 * result + url.hashCode()
|
||||||
|
result = 31 * result + name.hashCode()
|
||||||
return result
|
return result
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun toString(): String = "Document(mediaType=$mediaType, url=$url) ${super.toString()}"
|
override fun toString(): String = "Document(mediaType=$mediaType, url=$url, name='$name') ${super.toString()}"
|
||||||
}
|
}
|
||||||
|
|
|
@ -2,27 +2,17 @@ package dev.usbharu.hideout.activitypub.domain.model
|
||||||
|
|
||||||
import dev.usbharu.hideout.activitypub.domain.model.objects.Object
|
import dev.usbharu.hideout.activitypub.domain.model.objects.Object
|
||||||
|
|
||||||
open class Emoji : Object {
|
open class Emoji(
|
||||||
var updated: String? = null
|
type: List<String>,
|
||||||
var icon: Image? = null
|
override val name: String,
|
||||||
|
override val id: String,
|
||||||
protected constructor() : super()
|
val updated: String,
|
||||||
constructor(
|
val icon: Image
|
||||||
type: List<String>,
|
) : Object(
|
||||||
name: String?,
|
type = add(type, "Emoji")
|
||||||
actor: String?,
|
),
|
||||||
id: String?,
|
HasName,
|
||||||
updated: String?,
|
HasId {
|
||||||
icon: Image?
|
|
||||||
) : super(
|
|
||||||
type = add(type, "Emoji"),
|
|
||||||
name = name,
|
|
||||||
actor = actor,
|
|
||||||
id = id
|
|
||||||
) {
|
|
||||||
this.updated = updated
|
|
||||||
this.icon = icon
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun equals(other: Any?): Boolean {
|
override fun equals(other: Any?): Boolean {
|
||||||
if (this === other) return true
|
if (this === other) return true
|
||||||
|
@ -35,8 +25,8 @@ open class Emoji : Object {
|
||||||
|
|
||||||
override fun hashCode(): Int {
|
override fun hashCode(): Int {
|
||||||
var result = super.hashCode()
|
var result = super.hashCode()
|
||||||
result = 31 * result + (updated?.hashCode() ?: 0)
|
result = 31 * result + updated.hashCode()
|
||||||
result = 31 * result + (icon?.hashCode() ?: 0)
|
result = 31 * result + icon.hashCode()
|
||||||
return result
|
return result
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -1,37 +1,36 @@
|
||||||
package dev.usbharu.hideout.activitypub.domain.model
|
package dev.usbharu.hideout.activitypub.domain.model
|
||||||
|
|
||||||
|
import com.fasterxml.jackson.annotation.JsonProperty
|
||||||
import dev.usbharu.hideout.activitypub.domain.model.objects.Object
|
import dev.usbharu.hideout.activitypub.domain.model.objects.Object
|
||||||
|
|
||||||
open class Follow : Object {
|
open class Follow(
|
||||||
var `object`: String? = null
|
type: List<String> = emptyList(),
|
||||||
|
@JsonProperty("object") val apObject: String,
|
||||||
protected constructor() : super()
|
override val actor: String
|
||||||
constructor(
|
) : Object(
|
||||||
type: List<String> = emptyList(),
|
type = add(type, "Follow")
|
||||||
name: String?,
|
),
|
||||||
`object`: String?,
|
HasActor {
|
||||||
actor: String?
|
|
||||||
) : super(
|
|
||||||
type = add(type, "Follow"),
|
|
||||||
name = name,
|
|
||||||
actor = actor
|
|
||||||
) {
|
|
||||||
this.`object` = `object`
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun equals(other: Any?): Boolean {
|
override fun equals(other: Any?): Boolean {
|
||||||
if (this === other) return true
|
if (this === other) return true
|
||||||
if (other !is Follow) return false
|
if (javaClass != other?.javaClass) return false
|
||||||
if (!super.equals(other)) return false
|
if (!super.equals(other)) return false
|
||||||
|
|
||||||
return `object` == other.`object`
|
other as Follow
|
||||||
|
|
||||||
|
if (apObject != other.apObject) return false
|
||||||
|
if (actor != other.actor) return false
|
||||||
|
|
||||||
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun hashCode(): Int {
|
override fun hashCode(): Int {
|
||||||
var result = super.hashCode()
|
var result = super.hashCode()
|
||||||
result = 31 * result + (`object`?.hashCode() ?: 0)
|
result = 31 * result + apObject.hashCode()
|
||||||
|
result = 31 * result + actor.hashCode()
|
||||||
return result
|
return result
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun toString(): String = "Follow(`object`=$`object`) ${super.toString()}"
|
override fun toString(): String = "Follow(`object`=$apObject, actor='$actor') ${super.toString()}"
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,5 @@
|
||||||
|
package dev.usbharu.hideout.activitypub.domain.model
|
||||||
|
|
||||||
|
interface HasActor {
|
||||||
|
val actor: String
|
||||||
|
}
|
|
@ -0,0 +1,5 @@
|
||||||
|
package dev.usbharu.hideout.activitypub.domain.model
|
||||||
|
|
||||||
|
interface HasId {
|
||||||
|
val id: String
|
||||||
|
}
|
|
@ -0,0 +1,5 @@
|
||||||
|
package dev.usbharu.hideout.activitypub.domain.model
|
||||||
|
|
||||||
|
interface HasName {
|
||||||
|
val name: String
|
||||||
|
}
|
|
@ -2,32 +2,33 @@ package dev.usbharu.hideout.activitypub.domain.model
|
||||||
|
|
||||||
import dev.usbharu.hideout.activitypub.domain.model.objects.Object
|
import dev.usbharu.hideout.activitypub.domain.model.objects.Object
|
||||||
|
|
||||||
open class Image : Object {
|
open class Image(
|
||||||
private var mediaType: String? = null
|
type: List<String> = emptyList(),
|
||||||
private var url: String? = null
|
val mediaType: String,
|
||||||
|
val url: String
|
||||||
protected constructor() : super()
|
) : Object(
|
||||||
constructor(type: List<String> = emptyList(), name: String, mediaType: String?, url: String?) : super(
|
add(type, "Image")
|
||||||
add(type, "Image"),
|
) {
|
||||||
name
|
|
||||||
) {
|
|
||||||
this.mediaType = mediaType
|
|
||||||
this.url = url
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun equals(other: Any?): Boolean {
|
override fun equals(other: Any?): Boolean {
|
||||||
if (this === other) return true
|
if (this === other) return true
|
||||||
if (other !is Image) return false
|
if (javaClass != other?.javaClass) return false
|
||||||
if (!super.equals(other)) return false
|
if (!super.equals(other)) return false
|
||||||
|
|
||||||
|
other as Image
|
||||||
|
|
||||||
if (mediaType != other.mediaType) return false
|
if (mediaType != other.mediaType) return false
|
||||||
return url == other.url
|
if (url != other.url) return false
|
||||||
|
|
||||||
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun hashCode(): Int {
|
override fun hashCode(): Int {
|
||||||
var result = super.hashCode()
|
var result = super.hashCode()
|
||||||
result = 31 * result + (mediaType?.hashCode() ?: 0)
|
result = 31 * result + mediaType.hashCode()
|
||||||
result = 31 * result + (url?.hashCode() ?: 0)
|
result = 31 * result + url.hashCode()
|
||||||
return result
|
return result
|
||||||
}
|
}
|
||||||
|
|
||||||
|
override fun toString(): String = "Image(mediaType=$mediaType, url=$url) ${super.toString()}"
|
||||||
}
|
}
|
||||||
|
|
|
@ -2,41 +2,36 @@ package dev.usbharu.hideout.activitypub.domain.model
|
||||||
|
|
||||||
import dev.usbharu.hideout.activitypub.domain.model.objects.Object
|
import dev.usbharu.hideout.activitypub.domain.model.objects.Object
|
||||||
|
|
||||||
open class Key : Object {
|
open class Key(
|
||||||
var owner: String? = null
|
override val id: String,
|
||||||
var publicKeyPem: String? = null
|
val owner: String,
|
||||||
|
val publicKeyPem: String
|
||||||
protected constructor() : super()
|
) : Object(
|
||||||
constructor(
|
type = add(list = emptyList(), type = "Key")
|
||||||
type: List<String>,
|
),
|
||||||
name: String,
|
HasId {
|
||||||
id: String,
|
|
||||||
owner: String?,
|
|
||||||
publicKeyPem: String?
|
|
||||||
) : super(
|
|
||||||
type = add(list = type, type = "Key"),
|
|
||||||
name = name,
|
|
||||||
id = id
|
|
||||||
) {
|
|
||||||
this.owner = owner
|
|
||||||
this.publicKeyPem = publicKeyPem
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun equals(other: Any?): Boolean {
|
override fun equals(other: Any?): Boolean {
|
||||||
if (this === other) return true
|
if (this === other) return true
|
||||||
if (other !is Key) return false
|
if (javaClass != other?.javaClass) return false
|
||||||
if (!super.equals(other)) return false
|
if (!super.equals(other)) return false
|
||||||
|
|
||||||
|
other as Key
|
||||||
|
|
||||||
if (owner != other.owner) return false
|
if (owner != other.owner) return false
|
||||||
return publicKeyPem == other.publicKeyPem
|
if (publicKeyPem != other.publicKeyPem) return false
|
||||||
|
if (id != other.id) return false
|
||||||
|
|
||||||
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun hashCode(): Int {
|
override fun hashCode(): Int {
|
||||||
var result = super.hashCode()
|
var result = super.hashCode()
|
||||||
result = 31 * result + (owner?.hashCode() ?: 0)
|
result = 31 * result + owner.hashCode()
|
||||||
result = 31 * result + (publicKeyPem?.hashCode() ?: 0)
|
result = 31 * result + publicKeyPem.hashCode()
|
||||||
|
result = 31 * result + id.hashCode()
|
||||||
return result
|
return result
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun toString(): String = "Key(owner=$owner, publicKeyPem=$publicKeyPem) ${super.toString()}"
|
override fun toString(): String = "Key(owner=$owner, publicKeyPem=$publicKeyPem, id='$id') ${super.toString()}"
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,53 +1,57 @@
|
||||||
package dev.usbharu.hideout.activitypub.domain.model
|
package dev.usbharu.hideout.activitypub.domain.model
|
||||||
|
|
||||||
|
import com.fasterxml.jackson.annotation.JsonProperty
|
||||||
import com.fasterxml.jackson.databind.annotation.JsonDeserialize
|
import com.fasterxml.jackson.databind.annotation.JsonDeserialize
|
||||||
import dev.usbharu.hideout.activitypub.domain.model.objects.Object
|
import dev.usbharu.hideout.activitypub.domain.model.objects.Object
|
||||||
import dev.usbharu.hideout.activitypub.domain.model.objects.ObjectDeserializer
|
import dev.usbharu.hideout.activitypub.domain.model.objects.ObjectDeserializer
|
||||||
|
|
||||||
open class Like : Object {
|
open class Like(
|
||||||
var `object`: String? = null
|
type: List<String> = emptyList(),
|
||||||
var content: String? = null
|
override val actor: String,
|
||||||
|
override val id: String,
|
||||||
@JsonDeserialize(contentUsing = ObjectDeserializer::class)
|
@JsonProperty("object") val apObject: String,
|
||||||
var tag: List<Object> = emptyList()
|
val content: String,
|
||||||
|
@JsonDeserialize(contentUsing = ObjectDeserializer::class) val tag: List<Object> = emptyList()
|
||||||
protected constructor() : super()
|
) : Object(
|
||||||
constructor(
|
type = add(type, "Like")
|
||||||
type: List<String> = emptyList(),
|
),
|
||||||
name: String?,
|
HasId,
|
||||||
actor: String?,
|
HasActor {
|
||||||
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 fun equals(other: Any?): Boolean {
|
override fun equals(other: Any?): Boolean {
|
||||||
if (this === other) return true
|
if (this === other) return true
|
||||||
if (other !is Like) return false
|
if (javaClass != other?.javaClass) return false
|
||||||
if (!super.equals(other)) return false
|
if (!super.equals(other)) return false
|
||||||
|
|
||||||
if (`object` != other.`object`) return false
|
other as Like
|
||||||
|
|
||||||
|
if (actor != other.actor) return false
|
||||||
|
if (id != other.id) return false
|
||||||
|
if (apObject != other.apObject) return false
|
||||||
if (content != other.content) return false
|
if (content != other.content) return false
|
||||||
return tag == other.tag
|
if (tag != other.tag) return false
|
||||||
|
|
||||||
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun hashCode(): Int {
|
override fun hashCode(): Int {
|
||||||
var result = super.hashCode()
|
var result = super.hashCode()
|
||||||
result = 31 * result + (`object`?.hashCode() ?: 0)
|
result = 31 * result + actor.hashCode()
|
||||||
result = 31 * result + (content?.hashCode() ?: 0)
|
result = 31 * result + id.hashCode()
|
||||||
|
result = 31 * result + apObject.hashCode()
|
||||||
|
result = 31 * result + content.hashCode()
|
||||||
result = 31 * result + tag.hashCode()
|
result = 31 * result + tag.hashCode()
|
||||||
return result
|
return result
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun toString(): String = "Like(`object`=$`object`, content=$content, tag=$tag) ${super.toString()}"
|
override fun toString(): String {
|
||||||
|
return "Like(" +
|
||||||
|
"actor='$actor', " +
|
||||||
|
"id='$id', " +
|
||||||
|
"apObject='$apObject', " +
|
||||||
|
"content='$content', " +
|
||||||
|
"tag=$tag" +
|
||||||
|
")" +
|
||||||
|
" ${super.toString()}"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -2,79 +2,70 @@ package dev.usbharu.hideout.activitypub.domain.model
|
||||||
|
|
||||||
import dev.usbharu.hideout.activitypub.domain.model.objects.Object
|
import dev.usbharu.hideout.activitypub.domain.model.objects.Object
|
||||||
|
|
||||||
open class Note : Object {
|
open class Note
|
||||||
var attributedTo: String? = null
|
@Suppress("LongParameterList")
|
||||||
var attachment: List<Document> = emptyList()
|
constructor(
|
||||||
var content: String? = null
|
type: List<String> = emptyList(),
|
||||||
var published: String? = null
|
override val id: String,
|
||||||
var to: List<String> = emptyList()
|
val attributedTo: String,
|
||||||
var cc: List<String> = emptyList()
|
val content: String,
|
||||||
var sensitive: Boolean = false
|
val published: String,
|
||||||
var inReplyTo: String? = null
|
val to: List<String> = emptyList(),
|
||||||
|
val cc: List<String> = emptyList(),
|
||||||
protected constructor() : super()
|
val sensitive: Boolean = false,
|
||||||
|
val inReplyTo: String? = null,
|
||||||
@Suppress("LongParameterList")
|
val attachment: List<Document> = emptyList()
|
||||||
constructor(
|
) : Object(
|
||||||
type: List<String> = emptyList(),
|
type = add(type, "Note")
|
||||||
name: String,
|
),
|
||||||
id: String?,
|
HasId {
|
||||||
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 fun equals(other: Any?): Boolean {
|
override fun equals(other: Any?): Boolean {
|
||||||
if (this === other) return true
|
if (this === other) return true
|
||||||
if (other !is Note) return false
|
if (javaClass != other?.javaClass) return false
|
||||||
if (!super.equals(other)) return false
|
if (!super.equals(other)) return false
|
||||||
|
|
||||||
|
other as Note
|
||||||
|
|
||||||
|
if (id != other.id) return false
|
||||||
if (attributedTo != other.attributedTo) return false
|
if (attributedTo != other.attributedTo) return false
|
||||||
if (attachment != other.attachment) return false
|
|
||||||
if (content != other.content) return false
|
if (content != other.content) return false
|
||||||
if (published != other.published) return false
|
if (published != other.published) return false
|
||||||
if (to != other.to) return false
|
if (to != other.to) return false
|
||||||
if (cc != other.cc) return false
|
if (cc != other.cc) return false
|
||||||
if (sensitive != other.sensitive) return false
|
if (sensitive != other.sensitive) return false
|
||||||
if (inReplyTo != other.inReplyTo) return false
|
if (inReplyTo != other.inReplyTo) return false
|
||||||
|
if (attachment != other.attachment) return false
|
||||||
|
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun hashCode(): Int {
|
override fun hashCode(): Int {
|
||||||
var result = super.hashCode()
|
var result = super.hashCode()
|
||||||
result = 31 * result + (attributedTo?.hashCode() ?: 0)
|
result = 31 * result + id.hashCode()
|
||||||
result = 31 * result + attachment.hashCode()
|
result = 31 * result + attributedTo.hashCode()
|
||||||
result = 31 * result + (content?.hashCode() ?: 0)
|
result = 31 * result + content.hashCode()
|
||||||
result = 31 * result + (published?.hashCode() ?: 0)
|
result = 31 * result + published.hashCode()
|
||||||
result = 31 * result + to.hashCode()
|
result = 31 * result + to.hashCode()
|
||||||
result = 31 * result + cc.hashCode()
|
result = 31 * result + cc.hashCode()
|
||||||
result = 31 * result + sensitive.hashCode()
|
result = 31 * result + sensitive.hashCode()
|
||||||
result = 31 * result + (inReplyTo?.hashCode() ?: 0)
|
result = 31 * result + (inReplyTo?.hashCode() ?: 0)
|
||||||
|
result = 31 * result + attachment.hashCode()
|
||||||
return result
|
return result
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun toString(): String {
|
override fun toString(): String {
|
||||||
return "Note(attributedTo=$attributedTo, attachment=$attachment, " +
|
return "Note(" +
|
||||||
"content=$content, published=$published, to=$to, cc=$cc, sensitive=$sensitive," +
|
"id='$id', " +
|
||||||
" inReplyTo=$inReplyTo) ${super.toString()}"
|
"attributedTo='$attributedTo', " +
|
||||||
|
"content='$content', " +
|
||||||
|
"published='$published', " +
|
||||||
|
"to=$to, " +
|
||||||
|
"cc=$cc, " +
|
||||||
|
"sensitive=$sensitive, " +
|
||||||
|
"inReplyTo=$inReplyTo, " +
|
||||||
|
"attachment=$attachment" +
|
||||||
|
")" +
|
||||||
|
" ${super.toString()}"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -2,53 +2,33 @@ package dev.usbharu.hideout.activitypub.domain.model
|
||||||
|
|
||||||
import dev.usbharu.hideout.activitypub.domain.model.objects.Object
|
import dev.usbharu.hideout.activitypub.domain.model.objects.Object
|
||||||
|
|
||||||
open class Person : Object {
|
open class Person
|
||||||
var preferredUsername: String? = null
|
@Suppress("LongParameterList")
|
||||||
var summary: String? = null
|
constructor(
|
||||||
var inbox: String? = null
|
type: List<String> = emptyList(),
|
||||||
var outbox: String? = null
|
override val name: String,
|
||||||
var url: String? = null
|
override val id: String,
|
||||||
private var icon: Image? = null
|
var preferredUsername: String?,
|
||||||
var publicKey: Key? = null
|
var summary: String?,
|
||||||
var endpoints: Map<String, String> = emptyMap()
|
var inbox: String,
|
||||||
var following: String? = null
|
var outbox: String,
|
||||||
var followers: String? = null
|
var url: String,
|
||||||
|
private var icon: Image?,
|
||||||
protected constructor() : super()
|
var publicKey: Key,
|
||||||
|
var endpoints: Map<String, String> = emptyMap(),
|
||||||
@Suppress("LongParameterList")
|
var followers: String?,
|
||||||
constructor(
|
var following: String?
|
||||||
type: List<String> = emptyList(),
|
) : Object(add(type, "Person")), HasId, HasName {
|
||||||
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 fun equals(other: Any?): Boolean {
|
override fun equals(other: Any?): Boolean {
|
||||||
if (this === other) return true
|
if (this === other) return true
|
||||||
if (other !is Person) return false
|
if (javaClass != other?.javaClass) return false
|
||||||
if (!super.equals(other)) return false
|
if (!super.equals(other)) return false
|
||||||
|
|
||||||
|
other as Person
|
||||||
|
|
||||||
|
if (name != other.name) return false
|
||||||
|
if (id != other.id) return false
|
||||||
if (preferredUsername != other.preferredUsername) return false
|
if (preferredUsername != other.preferredUsername) return false
|
||||||
if (summary != other.summary) return false
|
if (summary != other.summary) return false
|
||||||
if (inbox != other.inbox) return false
|
if (inbox != other.inbox) return false
|
||||||
|
@ -57,20 +37,26 @@ open class Person : Object {
|
||||||
if (icon != other.icon) return false
|
if (icon != other.icon) return false
|
||||||
if (publicKey != other.publicKey) return false
|
if (publicKey != other.publicKey) return false
|
||||||
if (endpoints != other.endpoints) return false
|
if (endpoints != other.endpoints) return false
|
||||||
|
if (followers != other.followers) return false
|
||||||
|
if (following != other.following) return false
|
||||||
|
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun hashCode(): Int {
|
override fun hashCode(): Int {
|
||||||
var result = super.hashCode()
|
var result = super.hashCode()
|
||||||
|
result = 31 * result + name.hashCode()
|
||||||
|
result = 31 * result + id.hashCode()
|
||||||
result = 31 * result + (preferredUsername?.hashCode() ?: 0)
|
result = 31 * result + (preferredUsername?.hashCode() ?: 0)
|
||||||
result = 31 * result + (summary?.hashCode() ?: 0)
|
result = 31 * result + (summary?.hashCode() ?: 0)
|
||||||
result = 31 * result + (inbox?.hashCode() ?: 0)
|
result = 31 * result + inbox.hashCode()
|
||||||
result = 31 * result + (outbox?.hashCode() ?: 0)
|
result = 31 * result + outbox.hashCode()
|
||||||
result = 31 * result + (url?.hashCode() ?: 0)
|
result = 31 * result + url.hashCode()
|
||||||
result = 31 * result + (icon?.hashCode() ?: 0)
|
result = 31 * result + (icon?.hashCode() ?: 0)
|
||||||
result = 31 * result + (publicKey?.hashCode() ?: 0)
|
result = 31 * result + publicKey.hashCode()
|
||||||
result = 31 * result + endpoints.hashCode()
|
result = 31 * result + endpoints.hashCode()
|
||||||
|
result = 31 * result + (followers?.hashCode() ?: 0)
|
||||||
|
result = 31 * result + (following?.hashCode() ?: 0)
|
||||||
return result
|
return result
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -2,11 +2,24 @@ package dev.usbharu.hideout.activitypub.domain.model
|
||||||
|
|
||||||
import dev.usbharu.hideout.activitypub.domain.model.objects.Object
|
import dev.usbharu.hideout.activitypub.domain.model.objects.Object
|
||||||
|
|
||||||
open class Tombstone : Object {
|
open class Tombstone(type: List<String> = emptyList(), override val id: String) :
|
||||||
constructor(
|
Object(add(type, "Tombstone")),
|
||||||
type: List<String> = emptyList(),
|
HasId {
|
||||||
name: String = "Tombstone",
|
override fun equals(other: Any?): Boolean {
|
||||||
actor: String? = null,
|
if (this === other) return true
|
||||||
id: String
|
if (javaClass != other?.javaClass) return false
|
||||||
) : super(add(type, "Tombstone"), name, actor, id)
|
if (!super.equals(other)) return false
|
||||||
|
|
||||||
|
other as Tombstone
|
||||||
|
|
||||||
|
return id == other.id
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun hashCode(): Int {
|
||||||
|
var result = super.hashCode()
|
||||||
|
result = 31 * result + id.hashCode()
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun toString(): String = "Tombstone(id='$id') ${super.toString()}"
|
||||||
}
|
}
|
||||||
|
|
|
@ -3,42 +3,40 @@ package dev.usbharu.hideout.activitypub.domain.model
|
||||||
import com.fasterxml.jackson.databind.annotation.JsonDeserialize
|
import com.fasterxml.jackson.databind.annotation.JsonDeserialize
|
||||||
import dev.usbharu.hideout.activitypub.domain.model.objects.Object
|
import dev.usbharu.hideout.activitypub.domain.model.objects.Object
|
||||||
import dev.usbharu.hideout.activitypub.domain.model.objects.ObjectDeserializer
|
import dev.usbharu.hideout.activitypub.domain.model.objects.ObjectDeserializer
|
||||||
import java.time.Instant
|
|
||||||
|
|
||||||
open class Undo : Object {
|
|
||||||
|
|
||||||
|
open class Undo(
|
||||||
|
type: List<String> = emptyList(),
|
||||||
|
override val actor: String,
|
||||||
|
override val id: String,
|
||||||
@JsonDeserialize(using = ObjectDeserializer::class)
|
@JsonDeserialize(using = ObjectDeserializer::class)
|
||||||
var `object`: Object? = null
|
@Suppress("VariableNaming") val `object`: Object,
|
||||||
var published: String? = null
|
val published: String
|
||||||
|
) : Object(add(type, "Undo")), HasId, HasActor {
|
||||||
protected constructor() : super()
|
|
||||||
constructor(
|
|
||||||
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 fun equals(other: Any?): Boolean {
|
override fun equals(other: Any?): Boolean {
|
||||||
if (this === other) return true
|
if (this === other) return true
|
||||||
if (other !is Undo) return false
|
if (javaClass != other?.javaClass) return false
|
||||||
if (!super.equals(other)) return false
|
if (!super.equals(other)) return false
|
||||||
|
|
||||||
|
other as Undo
|
||||||
|
|
||||||
if (`object` != other.`object`) return false
|
if (`object` != other.`object`) return false
|
||||||
return published == other.published
|
if (published != other.published) return false
|
||||||
|
if (actor != other.actor) return false
|
||||||
|
if (id != other.id) return false
|
||||||
|
|
||||||
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun hashCode(): Int {
|
override fun hashCode(): Int {
|
||||||
var result = super.hashCode()
|
var result = super.hashCode()
|
||||||
result = 31 * result + (`object`?.hashCode() ?: 0)
|
result = 31 * result + `object`.hashCode()
|
||||||
result = 31 * result + (published?.hashCode() ?: 0)
|
result = 31 * result + published.hashCode()
|
||||||
|
result = 31 * result + actor.hashCode()
|
||||||
|
result = 31 * result + id.hashCode()
|
||||||
return result
|
return result
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun toString(): String = "Undo(`object`=$`object`, published=$published) ${super.toString()}"
|
override fun toString(): String =
|
||||||
|
"Undo(`object`=$`object`, published=$published, actor='$actor', id='$id') ${super.toString()}"
|
||||||
}
|
}
|
||||||
|
|
|
@ -12,41 +12,29 @@ open class Object : JsonLd {
|
||||||
set(value) {
|
set(value) {
|
||||||
field = value.filter { it.isNotBlank() }
|
field = value.filter { it.isNotBlank() }
|
||||||
}
|
}
|
||||||
var name: String? = null
|
|
||||||
var actor: String? = null
|
|
||||||
var id: String? = null
|
|
||||||
|
|
||||||
protected constructor()
|
protected constructor()
|
||||||
constructor(type: List<String>, name: String? = null, actor: String? = null, id: String? = null) : super() {
|
constructor(type: List<String>) : super() {
|
||||||
this.type = type.filter { it.isNotBlank() }
|
this.type = type.filter { it.isNotBlank() }
|
||||||
this.name = name
|
|
||||||
this.actor = actor
|
|
||||||
this.id = id
|
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun equals(other: Any?): Boolean {
|
override fun equals(other: Any?): Boolean {
|
||||||
if (this === other) return true
|
if (this === other) return true
|
||||||
if (other !is Object) return false
|
if (javaClass != other?.javaClass) return false
|
||||||
if (!super.equals(other)) return false
|
if (!super.equals(other)) return false
|
||||||
|
|
||||||
if (type != other.type) return false
|
other as Object
|
||||||
if (name != other.name) return false
|
|
||||||
if (actor != other.actor) return false
|
|
||||||
if (id != other.id) return false
|
|
||||||
|
|
||||||
return true
|
return type == other.type
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun hashCode(): Int {
|
override fun hashCode(): Int {
|
||||||
var result = super.hashCode()
|
var result = super.hashCode()
|
||||||
result = 31 * result + type.hashCode()
|
result = 31 * result + type.hashCode()
|
||||||
result = 31 * result + (name?.hashCode() ?: 0)
|
|
||||||
result = 31 * result + (actor?.hashCode() ?: 0)
|
|
||||||
result = 31 * result + (id?.hashCode() ?: 0)
|
|
||||||
return result
|
return result
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun toString(): String = "Object(type=$type, name=$name, actor=$actor, id=$id) ${super.toString()}"
|
override fun toString(): String = "Object(type=$type) ${super.toString()}"
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
@JvmStatic
|
@JvmStatic
|
||||||
|
|
|
@ -15,9 +15,6 @@ class ObjectDeserializer : JsonDeserializer<Object>() {
|
||||||
if (treeNode.isValueNode) {
|
if (treeNode.isValueNode) {
|
||||||
return ObjectValue(
|
return ObjectValue(
|
||||||
emptyList(),
|
emptyList(),
|
||||||
null,
|
|
||||||
null,
|
|
||||||
null,
|
|
||||||
treeNode.asText()
|
treeNode.asText()
|
||||||
)
|
)
|
||||||
} else if (treeNode.isObject) {
|
} else if (treeNode.isObject) {
|
||||||
|
@ -33,15 +30,8 @@ class ObjectDeserializer : JsonDeserializer<Object>() {
|
||||||
}
|
}
|
||||||
|
|
||||||
return when (activityType) {
|
return when (activityType) {
|
||||||
ExtendedActivityVocabulary.Follow -> {
|
ExtendedActivityVocabulary.Follow -> p.codec.treeToValue(treeNode, Follow::class.java)
|
||||||
val readValue = p.codec.treeToValue(treeNode, Follow::class.java)
|
ExtendedActivityVocabulary.Note -> p.codec.treeToValue(treeNode, Note::class.java)
|
||||||
readValue
|
|
||||||
}
|
|
||||||
|
|
||||||
ExtendedActivityVocabulary.Note -> {
|
|
||||||
p.codec.treeToValue(treeNode, Note::class.java)
|
|
||||||
}
|
|
||||||
|
|
||||||
ExtendedActivityVocabulary.Object -> p.codec.treeToValue(treeNode, Object::class.java)
|
ExtendedActivityVocabulary.Object -> p.codec.treeToValue(treeNode, Object::class.java)
|
||||||
ExtendedActivityVocabulary.Link -> TODO()
|
ExtendedActivityVocabulary.Link -> TODO()
|
||||||
ExtendedActivityVocabulary.Activity -> TODO()
|
ExtendedActivityVocabulary.Activity -> TODO()
|
||||||
|
|
|
@ -1,33 +1,27 @@
|
||||||
package dev.usbharu.hideout.activitypub.domain.model.objects
|
package dev.usbharu.hideout.activitypub.domain.model.objects
|
||||||
|
|
||||||
|
import com.fasterxml.jackson.annotation.JsonCreator
|
||||||
|
|
||||||
@Suppress("VariableNaming")
|
@Suppress("VariableNaming")
|
||||||
open class ObjectValue : Object {
|
open class ObjectValue @JsonCreator constructor(type: List<String>, var `object`: String) : Object(
|
||||||
|
type
|
||||||
var `object`: String? = null
|
) {
|
||||||
|
|
||||||
protected constructor() : super()
|
|
||||||
constructor(type: List<String>, name: String?, actor: String?, id: String?, `object`: String?) : super(
|
|
||||||
type,
|
|
||||||
name,
|
|
||||||
actor,
|
|
||||||
id
|
|
||||||
) {
|
|
||||||
this.`object` = `object`
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun equals(other: Any?): Boolean {
|
override fun equals(other: Any?): Boolean {
|
||||||
if (this === other) return true
|
if (this === other) return true
|
||||||
if (other !is ObjectValue) return false
|
if (javaClass != other?.javaClass) return false
|
||||||
if (!super.equals(other)) return false
|
if (!super.equals(other)) return false
|
||||||
|
|
||||||
|
other as ObjectValue
|
||||||
|
|
||||||
return `object` == other.`object`
|
return `object` == other.`object`
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun hashCode(): Int {
|
override fun hashCode(): Int {
|
||||||
var result = super.hashCode()
|
var result = super.hashCode()
|
||||||
result = 31 * result + (`object`?.hashCode() ?: 0)
|
result = 31 * result + `object`.hashCode()
|
||||||
return result
|
return result
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun toString(): String = "ObjectValue(`object`=$`object`) ${super.toString()}"
|
override fun toString(): String = "ObjectValue(`object`='$`object`') ${super.toString()}"
|
||||||
}
|
}
|
||||||
|
|
|
@ -64,7 +64,6 @@ class NoteQueryServiceImpl(private val postRepository: PostRepository, private v
|
||||||
this[Users.followers]
|
this[Users.followers]
|
||||||
)
|
)
|
||||||
return Note(
|
return Note(
|
||||||
name = "Post",
|
|
||||||
id = this[Posts.apId],
|
id = this[Posts.apId],
|
||||||
attributedTo = this[Users.url],
|
attributedTo = this[Users.url],
|
||||||
content = this[Posts.text],
|
content = this[Posts.text],
|
||||||
|
@ -80,7 +79,7 @@ class NoteQueryServiceImpl(private val postRepository: PostRepository, private v
|
||||||
private suspend fun Query.toNote(): Note {
|
private suspend fun Query.toNote(): Note {
|
||||||
return this.groupBy { it[Posts.id] }
|
return this.groupBy { it[Posts.id] }
|
||||||
.map { it.value }
|
.map { it.value }
|
||||||
.map { it.first().toNote(it.mapNotNull { it.toMediaOrNull() }) }
|
.map { it.first().toNote(it.mapNotNull { resultRow -> resultRow.toMediaOrNull() }) }
|
||||||
.singleOr { FailedToGetResourcesException("resource does not exist.") }
|
.singleOr { FailedToGetResourcesException("resource does not exist.") }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -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()}"
|
|
||||||
}
|
|
|
@ -15,6 +15,7 @@ interface InboxController {
|
||||||
"application/activity+json",
|
"application/activity+json",
|
||||||
"application/ld+json; profile=\"https://www.w3.org/ns/activitystreams\""
|
"application/ld+json; profile=\"https://www.w3.org/ns/activitystreams\""
|
||||||
],
|
],
|
||||||
|
consumes = ["application/json", "application/*+json"],
|
||||||
method = [RequestMethod.POST]
|
method = [RequestMethod.POST]
|
||||||
)
|
)
|
||||||
suspend fun inbox(@RequestBody string: String): ResponseEntity<Unit>
|
suspend fun inbox(@RequestBody string: String): ResponseEntity<Unit>
|
||||||
|
|
|
@ -1,16 +1,39 @@
|
||||||
package dev.usbharu.hideout.activitypub.interfaces.api.inbox
|
package dev.usbharu.hideout.activitypub.interfaces.api.inbox
|
||||||
|
|
||||||
import dev.usbharu.hideout.activitypub.service.common.APService
|
import dev.usbharu.hideout.activitypub.service.common.APService
|
||||||
|
import dev.usbharu.httpsignature.common.HttpHeaders
|
||||||
|
import dev.usbharu.httpsignature.common.HttpMethod
|
||||||
|
import dev.usbharu.httpsignature.common.HttpRequest
|
||||||
import org.slf4j.LoggerFactory
|
import org.slf4j.LoggerFactory
|
||||||
|
import org.springframework.http.HttpHeaders.WWW_AUTHENTICATE
|
||||||
import org.springframework.http.HttpStatus
|
import org.springframework.http.HttpStatus
|
||||||
import org.springframework.http.ResponseEntity
|
import org.springframework.http.ResponseEntity
|
||||||
import org.springframework.web.bind.annotation.RequestBody
|
import org.springframework.web.bind.annotation.RequestBody
|
||||||
import org.springframework.web.bind.annotation.RestController
|
import org.springframework.web.bind.annotation.RestController
|
||||||
|
import org.springframework.web.context.request.RequestContextHolder
|
||||||
|
import org.springframework.web.context.request.ServletRequestAttributes
|
||||||
|
import java.net.URL
|
||||||
|
|
||||||
@RestController
|
@RestController
|
||||||
class InboxControllerImpl(private val apService: APService) : InboxController {
|
class InboxControllerImpl(private val apService: APService) : InboxController {
|
||||||
@Suppress("TooGenericExceptionCaught")
|
@Suppress("TooGenericExceptionCaught")
|
||||||
override suspend fun inbox(@RequestBody string: String): ResponseEntity<Unit> {
|
override suspend fun inbox(
|
||||||
|
@RequestBody string: String
|
||||||
|
): ResponseEntity<Unit> {
|
||||||
|
val request = (requireNotNull(RequestContextHolder.getRequestAttributes()) as ServletRequestAttributes).request
|
||||||
|
|
||||||
|
val headersList = request.headerNames?.toList().orEmpty()
|
||||||
|
LOGGER.trace("Inbox Headers {}", headersList)
|
||||||
|
|
||||||
|
if (headersList.map { it.lowercase() }.contains("signature").not()) {
|
||||||
|
return ResponseEntity.status(HttpStatus.UNAUTHORIZED)
|
||||||
|
.header(
|
||||||
|
WWW_AUTHENTICATE,
|
||||||
|
"Signature realm=\"Example\",headers=\"(request-target) date host digest\""
|
||||||
|
)
|
||||||
|
.build()
|
||||||
|
}
|
||||||
|
|
||||||
val parseActivity = try {
|
val parseActivity = try {
|
||||||
apService.parseActivity(string)
|
apService.parseActivity(string)
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
|
@ -19,7 +42,29 @@ class InboxControllerImpl(private val apService: APService) : InboxController {
|
||||||
}
|
}
|
||||||
LOGGER.info("INBOX Processing Activity Type: {}", parseActivity)
|
LOGGER.info("INBOX Processing Activity Type: {}", parseActivity)
|
||||||
try {
|
try {
|
||||||
apService.processActivity(string, parseActivity)
|
val url = request.requestURL.toString()
|
||||||
|
|
||||||
|
val headers =
|
||||||
|
headersList.associateWith { header -> request.getHeaders(header)?.toList().orEmpty() }
|
||||||
|
|
||||||
|
val method = when (val method = request.method.lowercase()) {
|
||||||
|
"get" -> HttpMethod.GET
|
||||||
|
"post" -> HttpMethod.POST
|
||||||
|
else -> {
|
||||||
|
throw IllegalArgumentException("Unsupported method: $method")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
apService.processActivity(
|
||||||
|
string,
|
||||||
|
parseActivity,
|
||||||
|
HttpRequest(
|
||||||
|
URL(url + request.queryString.orEmpty()),
|
||||||
|
HttpHeaders(headers),
|
||||||
|
method
|
||||||
|
),
|
||||||
|
headers
|
||||||
|
)
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
LOGGER.warn("FAILED Process Activity $parseActivity", e)
|
LOGGER.warn("FAILED Process Activity $parseActivity", e)
|
||||||
return ResponseEntity(HttpStatus.ACCEPTED)
|
return ResponseEntity(HttpStatus.ACCEPTED)
|
||||||
|
|
|
@ -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)
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -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
|
||||||
|
}
|
|
@ -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)
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -33,7 +33,7 @@ class ApSendCreateServiceImpl(
|
||||||
val note = noteQueryService.findById(post.id).first
|
val note = noteQueryService.findById(post.id).first
|
||||||
val create = Create(
|
val create = Create(
|
||||||
name = "Create Note",
|
name = "Create Note",
|
||||||
`object` = note,
|
apObject = note,
|
||||||
actor = note.attributedTo,
|
actor = note.attributedTo,
|
||||||
id = "${applicationConfig.url}/create/note/${post.id}"
|
id = "${applicationConfig.url}/create/note/${post.id}"
|
||||||
)
|
)
|
||||||
|
|
|
@ -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
|
||||||
|
}
|
|
@ -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
|
||||||
|
}
|
|
@ -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
|
|
||||||
}
|
|
|
@ -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.")
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -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
|
||||||
|
}
|
|
@ -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)
|
||||||
|
}
|
||||||
|
}
|
|
@ -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>)
|
|
||||||
}
|
|
|
@ -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)
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -2,17 +2,14 @@ package dev.usbharu.hideout.activitypub.service.activity.follow
|
||||||
|
|
||||||
import com.fasterxml.jackson.databind.ObjectMapper
|
import com.fasterxml.jackson.databind.ObjectMapper
|
||||||
import dev.usbharu.hideout.activitypub.domain.model.Follow
|
import dev.usbharu.hideout.activitypub.domain.model.Follow
|
||||||
import dev.usbharu.hideout.activitypub.interfaces.api.common.ActivityPubResponse
|
|
||||||
import dev.usbharu.hideout.activitypub.interfaces.api.common.ActivityPubStringResponse
|
|
||||||
import dev.usbharu.hideout.core.external.job.ReceiveFollowJob
|
import dev.usbharu.hideout.core.external.job.ReceiveFollowJob
|
||||||
import dev.usbharu.hideout.core.service.job.JobQueueParentService
|
import dev.usbharu.hideout.core.service.job.JobQueueParentService
|
||||||
import io.ktor.http.*
|
|
||||||
import org.slf4j.LoggerFactory
|
import org.slf4j.LoggerFactory
|
||||||
import org.springframework.beans.factory.annotation.Qualifier
|
import org.springframework.beans.factory.annotation.Qualifier
|
||||||
import org.springframework.stereotype.Service
|
import org.springframework.stereotype.Service
|
||||||
|
|
||||||
interface APReceiveFollowService {
|
interface APReceiveFollowService {
|
||||||
suspend fun receiveFollow(follow: Follow): ActivityPubResponse
|
suspend fun receiveFollow(follow: Follow)
|
||||||
}
|
}
|
||||||
|
|
||||||
@Service
|
@Service
|
||||||
|
@ -20,14 +17,14 @@ class APReceiveFollowServiceImpl(
|
||||||
private val jobQueueParentService: JobQueueParentService,
|
private val jobQueueParentService: JobQueueParentService,
|
||||||
@Qualifier("activitypub") private val objectMapper: ObjectMapper
|
@Qualifier("activitypub") private val objectMapper: ObjectMapper
|
||||||
) : APReceiveFollowService {
|
) : APReceiveFollowService {
|
||||||
override suspend fun receiveFollow(follow: Follow): ActivityPubResponse {
|
override suspend fun receiveFollow(follow: Follow) {
|
||||||
logger.info("FOLLOW from: {} to: {}", follow.actor, follow.`object`)
|
logger.info("FOLLOW from: {} to: {}", follow.actor, follow.apObject)
|
||||||
jobQueueParentService.schedule(ReceiveFollowJob) {
|
jobQueueParentService.schedule(ReceiveFollowJob) {
|
||||||
props[ReceiveFollowJob.actor] = follow.actor
|
props[ReceiveFollowJob.actor] = follow.actor
|
||||||
props[ReceiveFollowJob.follow] = objectMapper.writeValueAsString(follow)
|
props[ReceiveFollowJob.follow] = objectMapper.writeValueAsString(follow)
|
||||||
props[ReceiveFollowJob.targetActor] = follow.`object`
|
props[ReceiveFollowJob.targetActor] = follow.apObject
|
||||||
}
|
}
|
||||||
return ActivityPubStringResponse(HttpStatusCode.OK, "{}", ContentType.Application.Json)
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
|
|
|
@ -15,8 +15,7 @@ class APSendFollowServiceImpl(
|
||||||
) : APSendFollowService {
|
) : APSendFollowService {
|
||||||
override suspend fun sendFollow(sendFollowDto: SendFollowDto) {
|
override suspend fun sendFollow(sendFollowDto: SendFollowDto) {
|
||||||
val follow = Follow(
|
val follow = Follow(
|
||||||
name = "Follow",
|
apObject = sendFollowDto.followTargetUserId.url,
|
||||||
`object` = sendFollowDto.followTargetUserId.url,
|
|
||||||
actor = sendFollowDto.userId.url
|
actor = sendFollowDto.userId.url
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
|
@ -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
|
||||||
|
}
|
|
@ -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)
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -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
|
||||||
|
}
|
|
@ -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>)
|
|
||||||
}
|
|
|
@ -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
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -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
|
||||||
|
}
|
|
@ -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
|
||||||
|
}
|
|
@ -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()
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -37,17 +37,31 @@ class APRequestServiceImpl(
|
||||||
logger.debug("START ActivityPub Request GET url: {}, signer: {}", url, signer?.url)
|
logger.debug("START ActivityPub Request GET url: {}, signer: {}", url, signer?.url)
|
||||||
val date = dateTimeFormatter.format(ZonedDateTime.now(ZoneId.of("GMT")))
|
val date = dateTimeFormatter.format(ZonedDateTime.now(ZoneId.of("GMT")))
|
||||||
val u = URL(url)
|
val u = URL(url)
|
||||||
if (signer?.privateKey == null) {
|
val httpResponse = if (signer?.privateKey == null) {
|
||||||
val bodyAsText = httpClient.get(url) {
|
apGetNotSign(url, date)
|
||||||
header("Accept", ContentType.Application.Activity)
|
} else {
|
||||||
header("Date", date)
|
apGetSign(date, u, signer, url)
|
||||||
}.bodyAsText()
|
|
||||||
logBody(bodyAsText, url)
|
|
||||||
return objectMapper.readValue(bodyAsText, responseClass)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
val bodyAsText = httpResponse.bodyAsText()
|
||||||
|
val readValue = objectMapper.readValue(bodyAsText, responseClass)
|
||||||
|
logger.debug(
|
||||||
|
"SUCCESS ActivityPub Request GET status: {} url: {}",
|
||||||
|
httpResponse.status,
|
||||||
|
httpResponse.request.url
|
||||||
|
)
|
||||||
|
logBody(bodyAsText, url)
|
||||||
|
return readValue
|
||||||
|
}
|
||||||
|
|
||||||
|
private suspend fun apGetSign(
|
||||||
|
date: String,
|
||||||
|
u: URL,
|
||||||
|
signer: User,
|
||||||
|
url: String
|
||||||
|
): HttpResponse {
|
||||||
val headers = headers {
|
val headers = headers {
|
||||||
append("Accept", ContentType.Application.Activity)
|
append("Accept", Activity)
|
||||||
append("Date", date)
|
append("Date", date)
|
||||||
append("Host", u.host)
|
append("Host", u.host)
|
||||||
}
|
}
|
||||||
|
@ -60,7 +74,7 @@ class APRequestServiceImpl(
|
||||||
),
|
),
|
||||||
privateKey = PrivateKey(
|
privateKey = PrivateKey(
|
||||||
keyId = "${signer.url}#pubkey",
|
keyId = "${signer.url}#pubkey",
|
||||||
privateKey = RsaUtil.decodeRsaPrivateKeyPem(signer.privateKey),
|
privateKey = RsaUtil.decodeRsaPrivateKeyPem(signer.privateKey!!),
|
||||||
),
|
),
|
||||||
signHeaders = listOf("(request-target)", "date", "host", "accept")
|
signHeaders = listOf("(request-target)", "date", "host", "accept")
|
||||||
)
|
)
|
||||||
|
@ -73,17 +87,14 @@ class APRequestServiceImpl(
|
||||||
remove("Host")
|
remove("Host")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
contentType(ContentType.Application.Activity)
|
contentType(Activity)
|
||||||
}
|
}
|
||||||
val bodyAsText = httpResponse.bodyAsText()
|
return httpResponse
|
||||||
val readValue = objectMapper.readValue(bodyAsText, responseClass)
|
}
|
||||||
logger.debug(
|
|
||||||
"SUCCESS ActivityPub Request GET status: {} url: {}",
|
private suspend fun apGetNotSign(url: String, date: String?) = httpClient.get(url) {
|
||||||
httpResponse.status,
|
header("Accept", Activity)
|
||||||
httpResponse.request.url
|
header("Date", date)
|
||||||
)
|
|
||||||
logBody(bodyAsText, url)
|
|
||||||
return readValue
|
|
||||||
}
|
}
|
||||||
|
|
||||||
override suspend fun <T : Object, R : Object> apPost(
|
override suspend fun <T : Object, R : Object> apPost(
|
||||||
|
@ -96,18 +107,9 @@ class APRequestServiceImpl(
|
||||||
return objectMapper.readValue(bodyAsText, responseClass)
|
return objectMapper.readValue(bodyAsText, responseClass)
|
||||||
}
|
}
|
||||||
|
|
||||||
@Suppress("LongMethod")
|
|
||||||
override suspend fun <T : Object> apPost(url: String, body: T?, signer: User?): String {
|
override suspend fun <T : Object> apPost(url: String, body: T?, signer: User?): String {
|
||||||
logger.debug("START ActivityPub Request POST url: {}, signer: {}", url, signer?.url)
|
logger.debug("START ActivityPub Request POST url: {}, signer: {}", url, signer?.url)
|
||||||
val requestBody = if (body != null) {
|
val requestBody = addContextIfNotNull(body)
|
||||||
val mutableListOf = mutableListOf<String>()
|
|
||||||
mutableListOf.add("https://www.w3.org/ns/activitystreams")
|
|
||||||
mutableListOf.addAll(body.context)
|
|
||||||
body.context = mutableListOf
|
|
||||||
objectMapper.writeValueAsString(body)
|
|
||||||
} else {
|
|
||||||
null
|
|
||||||
}
|
|
||||||
|
|
||||||
logger.trace(
|
logger.trace(
|
||||||
"""
|
"""
|
||||||
|
@ -129,22 +131,46 @@ class APRequestServiceImpl(
|
||||||
|
|
||||||
val date = dateTimeFormatter.format(ZonedDateTime.now(ZoneId.of("GMT")))
|
val date = dateTimeFormatter.format(ZonedDateTime.now(ZoneId.of("GMT")))
|
||||||
val u = URL(url)
|
val u = URL(url)
|
||||||
if (signer?.privateKey == null) {
|
val httpResponse = if (signer?.privateKey == null) {
|
||||||
val bodyAsText = httpClient.post(url) {
|
apPostNotSign(url, date, digest, requestBody)
|
||||||
accept(ContentType.Application.Activity)
|
} else {
|
||||||
header("Date", date)
|
apPostSign(date, u, digest, signer, requestBody)
|
||||||
header("Digest", "sha-256=$digest")
|
|
||||||
if (requestBody != null) {
|
|
||||||
setBody(requestBody)
|
|
||||||
contentType(ContentType.Application.Activity)
|
|
||||||
}
|
|
||||||
}.bodyAsText()
|
|
||||||
logBody(bodyAsText, url)
|
|
||||||
return 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 {
|
val headers = headers {
|
||||||
append("Accept", ContentType.Application.Activity)
|
append("Accept", Activity)
|
||||||
append("Date", date)
|
append("Date", date)
|
||||||
append("Host", u.host)
|
append("Host", u.host)
|
||||||
append("Digest", "sha-256=$digest")
|
append("Digest", "sha-256=$digest")
|
||||||
|
@ -158,30 +184,31 @@ class APRequestServiceImpl(
|
||||||
),
|
),
|
||||||
privateKey = PrivateKey(
|
privateKey = PrivateKey(
|
||||||
keyId = signer.keyId,
|
keyId = signer.keyId,
|
||||||
privateKey = RsaUtil.decodeRsaPrivateKeyPem(signer.privateKey)
|
privateKey = RsaUtil.decodeRsaPrivateKeyPem(signer.privateKey!!)
|
||||||
),
|
),
|
||||||
signHeaders = listOf("(request-target)", "date", "host", "digest")
|
signHeaders = listOf("(request-target)", "date", "host", "digest")
|
||||||
)
|
)
|
||||||
|
|
||||||
val httpResponse = httpClient.post(url) {
|
val httpResponse = httpClient.post(u) {
|
||||||
headers {
|
headers {
|
||||||
headers {
|
appendAll(headers)
|
||||||
appendAll(headers)
|
append("Signature", sign.signatureHeader)
|
||||||
append("Signature", sign.signatureHeader)
|
remove("Host")
|
||||||
remove("Host")
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
setBody(requestBody)
|
setBody(requestBody)
|
||||||
contentType(ContentType.Application.Activity)
|
contentType(Activity)
|
||||||
}
|
}
|
||||||
val bodyAsText = httpResponse.bodyAsText()
|
return httpResponse
|
||||||
logger.debug(
|
}
|
||||||
"SUCCESS ActivityPub Request POST status: {} url: {}",
|
|
||||||
httpResponse.status,
|
private fun <T : Object> addContextIfNotNull(body: T?) = if (body != null) {
|
||||||
httpResponse.request.url
|
val mutableListOf = mutableListOf<String>()
|
||||||
)
|
mutableListOf.add("https://www.w3.org/ns/activitystreams")
|
||||||
logBody(bodyAsText, url)
|
mutableListOf.addAll(body.context)
|
||||||
return bodyAsText
|
body.context = mutableListOf
|
||||||
|
objectMapper.writeValueAsString(body)
|
||||||
|
} else {
|
||||||
|
null
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun logBody(bodyAsText: String, url: String) {
|
private fun logBody(bodyAsText: String, url: String) {
|
||||||
|
|
|
@ -39,9 +39,8 @@ class APResourceResolveServiceImpl(
|
||||||
return (cacheManager.getOrWait(key) as APResolveResponse<T>).objects
|
return (cacheManager.getOrWait(key) as APResolveResponse<T>).objects
|
||||||
}
|
}
|
||||||
|
|
||||||
private suspend fun <T : Object> runResolve(url: String, singer: User?, clazz: Class<T>): ResolveResponse {
|
private suspend fun <T : Object> runResolve(url: String, singer: User?, clazz: Class<T>): ResolveResponse =
|
||||||
return APResolveResponse(apRequestService.apGet(url, singer, clazz))
|
APResolveResponse(apRequestService.apGet(url, singer, clazz))
|
||||||
}
|
|
||||||
|
|
||||||
private fun genCacheKey(url: String, singerId: Long?): String {
|
private fun genCacheKey(url: String, singerId: Long?): String {
|
||||||
if (singerId != null) {
|
if (singerId != null) {
|
||||||
|
|
|
@ -2,16 +2,10 @@ package dev.usbharu.hideout.activitypub.service.common
|
||||||
|
|
||||||
import com.fasterxml.jackson.databind.JsonNode
|
import com.fasterxml.jackson.databind.JsonNode
|
||||||
import com.fasterxml.jackson.databind.ObjectMapper
|
import com.fasterxml.jackson.databind.ObjectMapper
|
||||||
import com.fasterxml.jackson.module.kotlin.readValue
|
|
||||||
import dev.usbharu.hideout.activitypub.domain.exception.JsonParseException
|
import dev.usbharu.hideout.activitypub.domain.exception.JsonParseException
|
||||||
import dev.usbharu.hideout.activitypub.domain.model.Follow
|
import dev.usbharu.hideout.core.external.job.InboxJob
|
||||||
import dev.usbharu.hideout.activitypub.interfaces.api.common.ActivityPubResponse
|
import dev.usbharu.hideout.core.service.job.JobQueueParentService
|
||||||
import dev.usbharu.hideout.activitypub.service.activity.accept.APAcceptService
|
import dev.usbharu.httpsignature.common.HttpRequest
|
||||||
import dev.usbharu.hideout.activitypub.service.activity.create.APCreateService
|
|
||||||
import dev.usbharu.hideout.activitypub.service.activity.delete.APReceiveDeleteService
|
|
||||||
import dev.usbharu.hideout.activitypub.service.activity.follow.APReceiveFollowService
|
|
||||||
import dev.usbharu.hideout.activitypub.service.activity.like.APLikeService
|
|
||||||
import dev.usbharu.hideout.activitypub.service.activity.undo.APUndoService
|
|
||||||
import org.slf4j.Logger
|
import org.slf4j.Logger
|
||||||
import org.slf4j.LoggerFactory
|
import org.slf4j.LoggerFactory
|
||||||
import org.springframework.beans.factory.annotation.Qualifier
|
import org.springframework.beans.factory.annotation.Qualifier
|
||||||
|
@ -20,7 +14,12 @@ import org.springframework.stereotype.Service
|
||||||
interface APService {
|
interface APService {
|
||||||
fun parseActivity(json: String): ActivityType
|
fun parseActivity(json: String): ActivityType
|
||||||
|
|
||||||
suspend fun processActivity(json: String, type: ActivityType): ActivityPubResponse?
|
suspend fun processActivity(
|
||||||
|
json: String,
|
||||||
|
type: ActivityType,
|
||||||
|
httpRequest: HttpRequest,
|
||||||
|
map: Map<String, List<String>>
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
enum class ActivityType {
|
enum class ActivityType {
|
||||||
|
@ -176,13 +175,8 @@ enum class ExtendedVocabulary {
|
||||||
|
|
||||||
@Service
|
@Service
|
||||||
class APServiceImpl(
|
class APServiceImpl(
|
||||||
private val apReceiveFollowService: APReceiveFollowService,
|
@Qualifier("activitypub") private val objectMapper: ObjectMapper,
|
||||||
private val apUndoService: APUndoService,
|
private val jobQueueParentService: JobQueueParentService
|
||||||
private val apAcceptService: APAcceptService,
|
|
||||||
private val apCreateService: APCreateService,
|
|
||||||
private val apLikeService: APLikeService,
|
|
||||||
private val apReceiveDeleteService: APReceiveDeleteService,
|
|
||||||
@Qualifier("activitypub") private val objectMapper: ObjectMapper
|
|
||||||
) : APService {
|
) : APService {
|
||||||
|
|
||||||
val logger: Logger = LoggerFactory.getLogger(APServiceImpl::class.java)
|
val logger: Logger = LoggerFactory.getLogger(APServiceImpl::class.java)
|
||||||
|
@ -224,23 +218,21 @@ class APServiceImpl(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Suppress("CyclomaticComplexMethod", "NotImplementedDeclaration")
|
override suspend fun processActivity(
|
||||||
override suspend fun processActivity(json: String, type: ActivityType): ActivityPubResponse {
|
json: String,
|
||||||
|
type: ActivityType,
|
||||||
|
httpRequest: HttpRequest,
|
||||||
|
map: Map<String, List<String>>
|
||||||
|
) {
|
||||||
logger.debug("process activity: {}", type)
|
logger.debug("process activity: {}", type)
|
||||||
return when (type) {
|
jobQueueParentService.schedule(InboxJob) {
|
||||||
ActivityType.Accept -> apAcceptService.receiveAccept(objectMapper.readValue(json))
|
props[it.json] = json
|
||||||
ActivityType.Follow ->
|
props[it.type] = type.name
|
||||||
apReceiveFollowService
|
val writeValueAsString = objectMapper.writeValueAsString(httpRequest)
|
||||||
.receiveFollow(objectMapper.readValue(json, Follow::class.java))
|
println(writeValueAsString)
|
||||||
|
props[it.httpRequest] = writeValueAsString
|
||||||
ActivityType.Create -> apCreateService.receiveCreate(objectMapper.readValue(json))
|
props[it.headers] = objectMapper.writeValueAsString(map)
|
||||||
ActivityType.Like -> apLikeService.receiveLike(objectMapper.readValue(json))
|
|
||||||
ActivityType.Undo -> apUndoService.receiveUndo(objectMapper.readValue(json))
|
|
||||||
ActivityType.Delete -> apReceiveDeleteService.receiveDelete(objectMapper.readValue(json))
|
|
||||||
|
|
||||||
else -> {
|
|
||||||
throw IllegalArgumentException("$type is not supported.")
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
return
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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>)
|
||||||
|
}
|
|
@ -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
|
||||||
|
)
|
|
@ -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>
|
||||||
|
}
|
|
@ -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)
|
|
||||||
}
|
|
|
@ -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)
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -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")
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,7 +1,6 @@
|
||||||
package dev.usbharu.hideout.activitypub.service.objects.note
|
package dev.usbharu.hideout.activitypub.service.objects.note
|
||||||
|
|
||||||
import dev.usbharu.hideout.activitypub.domain.exception.FailedToGetActivityPubResourceException
|
import dev.usbharu.hideout.activitypub.domain.exception.FailedToGetActivityPubResourceException
|
||||||
import dev.usbharu.hideout.activitypub.domain.exception.IllegalActivityPubObjectException
|
|
||||||
import dev.usbharu.hideout.activitypub.domain.model.Note
|
import dev.usbharu.hideout.activitypub.domain.model.Note
|
||||||
import dev.usbharu.hideout.activitypub.query.NoteQueryService
|
import dev.usbharu.hideout.activitypub.query.NoteQueryService
|
||||||
import dev.usbharu.hideout.activitypub.service.common.APResourceResolveService
|
import dev.usbharu.hideout.activitypub.service.common.APResourceResolveService
|
||||||
|
@ -91,7 +90,7 @@ class APNoteServiceImpl(
|
||||||
requireNotNull(note.id) { "id is null" }
|
requireNotNull(note.id) { "id is null" }
|
||||||
|
|
||||||
return try {
|
return try {
|
||||||
noteQueryService.findByApid(note.id!!).first
|
noteQueryService.findByApid(note.id).first
|
||||||
} catch (_: FailedToGetResourcesException) {
|
} catch (_: FailedToGetResourcesException) {
|
||||||
saveNote(note, targetActor, url)
|
saveNote(note, targetActor, url)
|
||||||
}
|
}
|
||||||
|
@ -99,7 +98,7 @@ class APNoteServiceImpl(
|
||||||
|
|
||||||
private suspend fun saveNote(note: Note, targetActor: String?, url: String): Note {
|
private suspend fun saveNote(note: Note, targetActor: String?, url: String): Note {
|
||||||
val person = apUserService.fetchPersonWithEntity(
|
val person = apUserService.fetchPersonWithEntity(
|
||||||
note.attributedTo ?: throw IllegalActivityPubObjectException("note.attributedTo is null"),
|
note.attributedTo,
|
||||||
targetActor
|
targetActor
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -128,9 +127,9 @@ class APNoteServiceImpl(
|
||||||
.map {
|
.map {
|
||||||
mediaService.uploadRemoteMedia(
|
mediaService.uploadRemoteMedia(
|
||||||
RemoteMedia(
|
RemoteMedia(
|
||||||
(it.name ?: it.url)!!,
|
it.name,
|
||||||
it.url!!,
|
it.url,
|
||||||
it.mediaType ?: "application/octet-stream",
|
it.mediaType,
|
||||||
description = it.name
|
description = it.name
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
@ -142,13 +141,13 @@ class APNoteServiceImpl(
|
||||||
postBuilder.of(
|
postBuilder.of(
|
||||||
id = postRepository.generateId(),
|
id = postRepository.generateId(),
|
||||||
userId = person.second.id,
|
userId = person.second.id,
|
||||||
text = note.content.orEmpty(),
|
text = note.content,
|
||||||
createdAt = Instant.parse(note.published).toEpochMilli(),
|
createdAt = Instant.parse(note.published).toEpochMilli(),
|
||||||
visibility = visibility,
|
visibility = visibility,
|
||||||
url = note.id ?: url,
|
url = note.id,
|
||||||
replyId = reply?.id,
|
replyId = reply?.id,
|
||||||
sensitive = note.sensitive,
|
sensitive = note.sensitive,
|
||||||
apId = note.id ?: url,
|
apId = note.id,
|
||||||
mediaIds = mediaList
|
mediaIds = mediaList
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
@ -156,7 +155,7 @@ class APNoteServiceImpl(
|
||||||
}
|
}
|
||||||
|
|
||||||
override suspend fun fetchNote(note: Note, targetActor: String?): Note =
|
override suspend fun fetchNote(note: Note, targetActor: String?): Note =
|
||||||
saveIfMissing(note, targetActor, note.id ?: throw IllegalArgumentException("note.id is null"))
|
saveIfMissing(note, targetActor, note.id)
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
const val public: String = "https://www.w3.org/ns/activitystreams#Public"
|
const val public: String = "https://www.w3.org/ns/activitystreams#Public"
|
||||||
|
|
|
@ -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)
|
||||||
|
}
|
||||||
|
}
|
|
@ -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>)
|
|
||||||
}
|
|
|
@ -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)
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -4,6 +4,7 @@ import dev.usbharu.hideout.activitypub.domain.model.Note
|
||||||
import dev.usbharu.hideout.activitypub.query.NoteQueryService
|
import dev.usbharu.hideout.activitypub.query.NoteQueryService
|
||||||
import dev.usbharu.hideout.application.external.Transaction
|
import dev.usbharu.hideout.application.external.Transaction
|
||||||
import dev.usbharu.hideout.core.domain.exception.FailedToGetResourcesException
|
import dev.usbharu.hideout.core.domain.exception.FailedToGetResourcesException
|
||||||
|
import dev.usbharu.hideout.core.domain.model.post.Post
|
||||||
import dev.usbharu.hideout.core.domain.model.post.Visibility
|
import dev.usbharu.hideout.core.domain.model.post.Visibility
|
||||||
import dev.usbharu.hideout.core.query.FollowerQueryService
|
import dev.usbharu.hideout.core.query.FollowerQueryService
|
||||||
import org.slf4j.LoggerFactory
|
import org.slf4j.LoggerFactory
|
||||||
|
@ -28,20 +29,27 @@ class NoteApApiServiceImpl(
|
||||||
}
|
}
|
||||||
|
|
||||||
Visibility.FOLLOWERS -> {
|
Visibility.FOLLOWERS -> {
|
||||||
if (userId == null) {
|
return@transaction getFollowersNote(userId, findById)
|
||||||
return@transaction null
|
|
||||||
}
|
|
||||||
|
|
||||||
if (followerQueryService.alreadyFollow(findById.second.userId, userId).not()) {
|
|
||||||
return@transaction null
|
|
||||||
}
|
|
||||||
return@transaction findById.first
|
|
||||||
}
|
}
|
||||||
|
|
||||||
Visibility.DIRECT -> return@transaction null
|
Visibility.DIRECT -> return@transaction null
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private suspend fun getFollowersNote(
|
||||||
|
userId: Long?,
|
||||||
|
findById: Pair<Note, Post>
|
||||||
|
): Note? {
|
||||||
|
if (userId == null) {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
if (followerQueryService.alreadyFollow(findById.second.userId, userId)) {
|
||||||
|
return findById.first
|
||||||
|
}
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
private val logger = LoggerFactory.getLogger(NoteApApiServiceImpl::class.java)
|
private val logger = LoggerFactory.getLogger(NoteApApiServiceImpl::class.java)
|
||||||
}
|
}
|
||||||
|
|
|
@ -14,6 +14,7 @@ import dev.usbharu.hideout.core.query.UserQueryService
|
||||||
import dev.usbharu.hideout.core.service.user.RemoteUserCreateDto
|
import dev.usbharu.hideout.core.service.user.RemoteUserCreateDto
|
||||||
import dev.usbharu.hideout.core.service.user.UserService
|
import dev.usbharu.hideout.core.service.user.UserService
|
||||||
import org.springframework.stereotype.Service
|
import org.springframework.stereotype.Service
|
||||||
|
import org.springframework.transaction.annotation.Transactional
|
||||||
|
|
||||||
interface APUserService {
|
interface APUserService {
|
||||||
suspend fun getPersonByName(name: String): Person
|
suspend fun getPersonByName(name: String): Person
|
||||||
|
@ -57,13 +58,10 @@ class APUserServiceImpl(
|
||||||
url = userUrl,
|
url = userUrl,
|
||||||
icon = Image(
|
icon = Image(
|
||||||
type = emptyList(),
|
type = emptyList(),
|
||||||
name = "$userUrl/icon.png",
|
|
||||||
mediaType = "image/png",
|
mediaType = "image/png",
|
||||||
url = "$userUrl/icon.png"
|
url = "$userUrl/icon.png"
|
||||||
),
|
),
|
||||||
publicKey = Key(
|
publicKey = Key(
|
||||||
type = emptyList(),
|
|
||||||
name = "Public Key",
|
|
||||||
id = userEntity.keyId,
|
id = userEntity.keyId,
|
||||||
owner = userUrl,
|
owner = userUrl,
|
||||||
publicKeyPem = userEntity.publicKey
|
publicKeyPem = userEntity.publicKey
|
||||||
|
@ -77,52 +75,33 @@ class APUserServiceImpl(
|
||||||
override suspend fun fetchPerson(url: String, targetActor: String?): Person =
|
override suspend fun fetchPerson(url: String, targetActor: String?): Person =
|
||||||
fetchPersonWithEntity(url, targetActor).first
|
fetchPersonWithEntity(url, targetActor).first
|
||||||
|
|
||||||
|
@Transactional
|
||||||
override suspend fun fetchPersonWithEntity(url: String, targetActor: String?): Pair<Person, User> {
|
override suspend fun fetchPersonWithEntity(url: String, targetActor: String?): Pair<Person, User> {
|
||||||
return try {
|
return try {
|
||||||
val userEntity = userQueryService.findByUrl(url)
|
val userEntity = userQueryService.findByUrl(url)
|
||||||
return Person(
|
val id = userEntity.url
|
||||||
type = emptyList(),
|
return entityToPerson(userEntity, id) to userEntity
|
||||||
name = userEntity.name,
|
|
||||||
id = url,
|
|
||||||
preferredUsername = userEntity.name,
|
|
||||||
summary = userEntity.description,
|
|
||||||
inbox = "$url/inbox",
|
|
||||||
outbox = "$url/outbox",
|
|
||||||
url = url,
|
|
||||||
icon = Image(
|
|
||||||
type = emptyList(),
|
|
||||||
name = "$url/icon.png",
|
|
||||||
mediaType = "image/png",
|
|
||||||
url = "$url/icon.png"
|
|
||||||
),
|
|
||||||
publicKey = Key(
|
|
||||||
type = emptyList(),
|
|
||||||
name = "Public Key",
|
|
||||||
id = userEntity.keyId,
|
|
||||||
owner = url,
|
|
||||||
publicKeyPem = userEntity.publicKey
|
|
||||||
),
|
|
||||||
endpoints = mapOf("sharedInbox" to "${applicationConfig.url}/inbox"),
|
|
||||||
followers = userEntity.followers,
|
|
||||||
following = userEntity.following
|
|
||||||
) to userEntity
|
|
||||||
} catch (ignore: FailedToGetResourcesException) {
|
} catch (ignore: FailedToGetResourcesException) {
|
||||||
val person = apResourceResolveService.resolve<Person>(url, null as Long?)
|
val person = apResourceResolveService.resolve<Person>(url, null as Long?)
|
||||||
|
|
||||||
|
val id = person.id
|
||||||
|
try {
|
||||||
|
val userEntity = userQueryService.findByUrl(id)
|
||||||
|
return entityToPerson(userEntity, id) to userEntity
|
||||||
|
} catch (_: FailedToGetResourcesException) {
|
||||||
|
}
|
||||||
person to userService.createRemoteUser(
|
person to userService.createRemoteUser(
|
||||||
RemoteUserCreateDto(
|
RemoteUserCreateDto(
|
||||||
name = person.preferredUsername
|
name = person.preferredUsername
|
||||||
?: throw IllegalActivityPubObjectException("preferredUsername is null"),
|
?: throw IllegalActivityPubObjectException("preferredUsername is null"),
|
||||||
domain = url.substringAfter("://").substringBefore("/"),
|
domain = id.substringAfter("://").substringBefore("/"),
|
||||||
screenName = (person.name ?: person.preferredUsername)
|
screenName = person.name,
|
||||||
?: throw IllegalActivityPubObjectException("preferredUsername is null"),
|
|
||||||
description = person.summary.orEmpty(),
|
description = person.summary.orEmpty(),
|
||||||
inbox = person.inbox ?: throw IllegalActivityPubObjectException("inbox is null"),
|
inbox = person.inbox,
|
||||||
outbox = person.outbox ?: throw IllegalActivityPubObjectException("outbox is null"),
|
outbox = person.outbox,
|
||||||
url = url,
|
url = id,
|
||||||
publicKey = person.publicKey?.publicKeyPem
|
publicKey = person.publicKey.publicKeyPem,
|
||||||
?: throw IllegalActivityPubObjectException("publicKey is null"),
|
keyId = person.publicKey.id,
|
||||||
keyId = person.publicKey?.id ?: throw IllegalActivityPubObjectException("publicKey keyId is null"),
|
|
||||||
following = person.following,
|
following = person.following,
|
||||||
followers = person.followers,
|
followers = person.followers,
|
||||||
sharedInbox = person.endpoints["sharedInbox"]
|
sharedInbox = person.endpoints["sharedInbox"]
|
||||||
|
@ -130,4 +109,31 @@ class APUserServiceImpl(
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fun entityToPerson(
|
||||||
|
userEntity: User,
|
||||||
|
id: String
|
||||||
|
) = Person(
|
||||||
|
type = emptyList(),
|
||||||
|
name = userEntity.name,
|
||||||
|
id = id,
|
||||||
|
preferredUsername = userEntity.name,
|
||||||
|
summary = userEntity.description,
|
||||||
|
inbox = "$id/inbox",
|
||||||
|
outbox = "$id/outbox",
|
||||||
|
url = id,
|
||||||
|
icon = Image(
|
||||||
|
type = emptyList(),
|
||||||
|
mediaType = "image/png",
|
||||||
|
url = "$id/icon.png"
|
||||||
|
),
|
||||||
|
publicKey = Key(
|
||||||
|
id = userEntity.keyId,
|
||||||
|
owner = id,
|
||||||
|
publicKeyPem = userEntity.publicKey
|
||||||
|
),
|
||||||
|
endpoints = mapOf("sharedInbox" to "${applicationConfig.url}/inbox"),
|
||||||
|
followers = userEntity.followers,
|
||||||
|
following = userEntity.following
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
|
@ -7,6 +7,8 @@ import com.fasterxml.jackson.core.JsonParser
|
||||||
import com.fasterxml.jackson.databind.DeserializationFeature
|
import com.fasterxml.jackson.databind.DeserializationFeature
|
||||||
import com.fasterxml.jackson.databind.ObjectMapper
|
import com.fasterxml.jackson.databind.ObjectMapper
|
||||||
import com.fasterxml.jackson.module.kotlin.jacksonObjectMapper
|
import com.fasterxml.jackson.module.kotlin.jacksonObjectMapper
|
||||||
|
import dev.usbharu.hideout.core.infrastructure.httpsignature.HttpRequestMixIn
|
||||||
|
import dev.usbharu.httpsignature.common.HttpRequest
|
||||||
import dev.usbharu.httpsignature.sign.HttpSignatureSigner
|
import dev.usbharu.httpsignature.sign.HttpSignatureSigner
|
||||||
import dev.usbharu.httpsignature.sign.RsaSha256HttpSignatureSigner
|
import dev.usbharu.httpsignature.sign.RsaSha256HttpSignatureSigner
|
||||||
import org.springframework.beans.factory.annotation.Qualifier
|
import org.springframework.beans.factory.annotation.Qualifier
|
||||||
|
@ -24,11 +26,13 @@ class ActivityPubConfig {
|
||||||
val objectMapper = jacksonObjectMapper()
|
val objectMapper = jacksonObjectMapper()
|
||||||
.enable(DeserializationFeature.ACCEPT_SINGLE_VALUE_AS_ARRAY)
|
.enable(DeserializationFeature.ACCEPT_SINGLE_VALUE_AS_ARRAY)
|
||||||
.setSerializationInclusion(JsonInclude.Include.NON_EMPTY)
|
.setSerializationInclusion(JsonInclude.Include.NON_EMPTY)
|
||||||
.setDefaultSetterInfo(JsonSetter.Value.forContentNulls(Nulls.AS_EMPTY))
|
.setDefaultSetterInfo(JsonSetter.Value.forContentNulls(Nulls.SKIP))
|
||||||
|
.setDefaultSetterInfo(JsonSetter.Value.forValueNulls(Nulls.SKIP))
|
||||||
.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false)
|
.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false)
|
||||||
.configure(JsonParser.Feature.ALLOW_COMMENTS, true)
|
.configure(JsonParser.Feature.ALLOW_COMMENTS, true)
|
||||||
.configure(JsonParser.Feature.ALLOW_SINGLE_QUOTES, true)
|
.configure(JsonParser.Feature.ALLOW_SINGLE_QUOTES, true)
|
||||||
.configure(JsonParser.Feature.ALLOW_TRAILING_COMMA, true)
|
.configure(JsonParser.Feature.ALLOW_TRAILING_COMMA, true)
|
||||||
|
.addMixIn(HttpRequest::class.java, HttpRequestMixIn::class.java)
|
||||||
return objectMapper
|
return objectMapper
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue