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