diff --git a/.github/monorepo.json b/.github/monorepo.json new file mode 100644 index 00000000..36313cce --- /dev/null +++ b/.github/monorepo.json @@ -0,0 +1,8 @@ +{ + "projects": { + "./hideout-activitypub": "hideout-activitypub", + "./hideout-core": "hideout-core", + "./hideout-mastodon": "hideout-mastodon", + "./owl": "owl" + } +} \ No newline at end of file diff --git a/.github/workflows/master-publish-package.yaml b/.github/workflows/master-publish-package.yaml new file mode 100644 index 00000000..36abce0e --- /dev/null +++ b/.github/workflows/master-publish-package.yaml @@ -0,0 +1,78 @@ +name: master-publish-package.yaml +on: + pull_request: + branches: + - "master" + - "release-test-master" +jobs: + release-diff-check: + name: Release diff check + runs-on: ubuntu-latest + outputs: + diff: ${{ steps.check-diff.outputs.result }} + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: fetch git + run: git fetch --depth 1 origin + + - name: Check diff + id: check-diff + uses: actions/github-script@v3 + with: + result-encoding: 'json' + script: | + const fs = require('fs') + const {execSync} = require('child_process'); + const jsonData = JSON.parse(fs.readFileSync('./.github/monorepo.json', 'utf8')); + const baseRef = context.payload.pull_request.base.ref + console.log(baseRef) + var tags = [] + + for (let [key, value] of Object.entries(jsonData.projects)) { + console.log(execSync("git branch", {encoding: 'utf8'})) + const command = "git diff origin/" + baseRef + " -- HEAD --name-only --relative=" + key + "\n"; + const output = execSync(command, {encoding: 'utf8'}); + console.log(output) + if (output.trim() === '') { + tags.push(value) + } + } + return tags + + - name: show diff + env: + DIFF: ${{ steps.check-diff.outputs.result }} + run: echo "$DIFF" + + publish-package: + runs-on: ubuntu-latest + needs: release-diff-check + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Gradle Wrapper Validation + uses: gradle/actions/wrapper-validation@v4 + + - name: Set up JDK 21 + uses: actions/setup-java@v4 + with: + java-version: '21' + distribution: 'temurin' + + - name: Setup Gradle + uses: gradle/actions/setup-gradle@v4 + with: + cache-read-only: false + gradle-home-cache-cleanup: true + + - name: Publish OWL Local + if: ${{ github.base_ref == 'release-test-master' && contains( needs.release-diff-check.outputs.diff, 'owl') }} + run: ./owl/gradlew :owl:publishMavenPublicationToMavenLocal + + - name: Publish OWL Gitea + + if: ${{ github.base_ref == 'master' && contains( needs.release-diff-check.outputs.diff , 'owl')}} + run: ./owl/gradlew :owl:publishMavenPublicationToGiteaRepository \ No newline at end of file diff --git a/.github/workflows/pull-request-merge-check.yml b/.github/workflows/pull-request-merge-check.yml new file mode 100644 index 00000000..71055387 --- /dev/null +++ b/.github/workflows/pull-request-merge-check.yml @@ -0,0 +1,120 @@ +name: PullRequest Merge Check + +on: + pull_request: + paths-ignore: + - 'owl/**' + branches: + - "develop" + types: + - opened # default + - reopened # default + - synchronize # default + - ready_for_review # 必要 +concurrency: + group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }} + cancel-in-progress: true + +permissions: + contents: read + checks: write + id-token: write + pull-requests: write + +jobs: + setup: + name: Setup + if: github.event.pull_request.draft == false + runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Gradle Wrapper Validation + uses: gradle/actions/wrapper-validation@v4 + + - name: Set up JDK 21 + uses: actions/setup-java@v4 + with: + java-version: '21' + distribution: 'temurin' + + - name: Setup Gradle + uses: gradle/actions/setup-gradle@v4 + with: + cache-read-only: false + gradle-home-cache-cleanup: true + + - name: Build + run: ./gradlew :hideout-core:classes + + unit-test: + name: Unit Test + needs: [ setup ] + runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Set up JDK 21 + uses: actions/setup-java@v4 + with: + java-version: '21' + distribution: 'temurin' + + - name: Setup Gradle + uses: gradle/actions/setup-gradle@v4 + with: + cache-read-only: false + gradle-home-cache-cleanup: true + + - name: Unit Test + run: ./gradlew :hideout-core:koverXmlReport + + - name: Add coverage report to PR + if: always() + id: kover + uses: madrapps/jacoco-report@v1.6.1 + with: + paths: | + ${{ github.workspace }}/hideout-core/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 + + - name: JUnit Test Report + uses: mikepenz/action-junit-report@v4 + with: + report_paths: '**/TEST-*.xml' + + lint: + name: Lint + needs: [ setup ] + runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Set up JDK 21 + uses: actions/setup-java@v4 + with: + java-version: '21' + distribution: 'temurin' + + - name: Setup Gradle + uses: gradle/actions/setup-gradle@v4 + with: + cache-read-only: false + gradle-home-cache-cleanup: true + + - name: Build with Gradle + run: ./gradlew :hideout-core:detektMain + + - name: Auto Commit + if: ${{ always() }} + uses: stefanzweifel/git-auto-commit-action@v5 + with: + commit_message: "style: fix lint (CI)" \ No newline at end of file diff --git a/.gitignore b/.gitignore index fcf91b13..39e4144d 100644 --- a/.gitignore +++ b/.gitignore @@ -35,3 +35,16 @@ out/ ### VS Code ### .vscode/ *.db +/src/main/resources/static/ +/node_modules/ +/src/main/web/generated/ +/stats.html +/tomcat/ +/tomcat-e2e/ +/e2eTest.log +/files/ + +*.log +/hideout-core/files/ +/hideout-core/.kotlin/sessions/ +/hideout-mastodon/.kotlin/sessions/ diff --git a/LICENSE b/LICENSE new file mode 100644 index 00000000..823c1c8e --- /dev/null +++ b/LICENSE @@ -0,0 +1,202 @@ + + Apache License + Version 2.0, January 2004 + https://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "{}" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright {yyyy} {name of copyright owner} + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + https://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. \ No newline at end of file diff --git a/README.md b/README.md new file mode 100644 index 00000000..2356ce11 --- /dev/null +++ b/README.md @@ -0,0 +1,75 @@ +# Hideout + +HideoutはMastodon互換APIを備えたSNSで、ActivityPubに対応し、KotlinとSpring Frameworkを使用して制作されているソフトウェアです。現在は開発中で、SNSとして主要な機能を備えていますが、セキュリティの問題や不安定なテーブル定義などがあるためホストすることはおすすめしません。 + +## 特徴 + +### ActivityPubでつながるネットワーク + +ActivityPubを実装しているためMastodonやMisskey、Pleromaとつながることができます。また、ActivityPub以外の分散型の通信プロトコルを実装することがあるかもしれません。 + +### Mastodon互換API + +OAuth2プロバイダーを含めたほとんどのAPIがMastodonとの互換性を持っているため、既存のMastodon クライアントを使用することができます!また、今後Fedibirdやglitch-soc互換のAPIを実装するかもしれません。 + +## セルフホスト + +> [!CAUTION] +> **免責事項** +> +> 本ソフトウェアを利用して発生したすべての事象に対して開発者は責任を負いません。 +> 本ソフトウェアはApache License 2.0を採用しています。 + +現時点でセルフホストはおすすめしませんが、実験用としてホストすることはできます! + +### 使用技術 + +- **Kotlin** 強力な言語機能でアプリケーションの安全性を高めます。 +- **Spring Framework** (Spring Boot/Spring Security/Spring Web/Spring Data) 豊富な機能と堅牢なライブラリでソフトウェアの基幹部分を担います。 +- **OpenAPI** スキーマファーストのエンドポイント自動生成はAPIの安定性を高めます。 + +### 要件 + +#### 起動 + +- Java 21 +- PostgreSQL 12+ +- MongoDB(必須でない) 4.4.x+ + +#### ビルド + +- Java 21 +- Gradle 8+ + +実験用途として、PostgreSQLはH2DB(バンドル)のPostgreSQL互換モードでも問題ありません。 + +Java 17でもビルド/起動ができますが、サポートしません。 + +MongoDBを使用しない場合、構成で`hideout.use-mongodb`をfalseにする必要があります。 + +### ビルド + +今後のリリースでビルド済みjarなどを使う場合はスキップしてください。 + + +```bash +gradle bootJar +``` + +`build/libs/hideout-x.x.x.jar`が生成されます。 + +### 起動 + +適切に設定した`application-dev.yml`などをクラスパス上に準備します。 + +`dev`は`prod`などに置き換えたり、[複数指定すること](https://spring.pleiades.io/spring-boot/docs/current/reference/html/application-properties.html#application-properties.core.spring.profiles.active)もできます。 + +```bash +java -jar build/libs/hideout-x.x.x.jar --spring.profiles.active=dev +``` + +https://spring.pleiades.io/spring-boot/docs/current/reference/html/getting-started.html#getting-started.first-application.executable-jar.gradle + +### 注意事項 + +本ソフトウェアは開発中です。正常に機能しない場合があります。また、連合先に迷惑をかける事になる可能性があります。DB/設定ファイル/その他リソースなどの利用方法に破壊的な変更が入る可能性があります。 diff --git a/build.gradle.kts b/build.gradle.kts index 0dd14e61..2906c67f 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -1,100 +1,70 @@ -val ktor_version: String by project -val kotlin_version: String by project -val logback_version: String by project -val exposed_version: String by project -val h2_version: String by project -val koin_version: String by project +/* + * Copyright (C) 2024 usbharu + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ plugins { - kotlin("jvm") version "1.8.10" - id("io.ktor.plugin") version "2.2.4" -// id("org.jetbrains.kotlin.plugin.serialization") version "1.8.10" + alias(libs.plugins.kotlin.jvm) + alias(libs.plugins.spring.boot) + alias(libs.plugins.kotlin.spring) } -group = "dev.usbharu" -version = "0.0.1" -application { - mainClass.set("io.ktor.server.netty.EngineMain") - - val isDevelopment: Boolean = project.ext.has("development") - applicationDefaultJvmArgs = listOf("-Dio.ktor.development=$isDevelopment") -} - -tasks.withType { - useJUnitPlatform() +apply { + plugin("io.spring.dependency-management") } repositories { mavenCentral() -} + maven { + url = uri("https://git.usbharu.dev/api/packages/usbharu/maven") + } + maven { + name = "GitHubPackages" + url = uri("https://maven.pkg.github.com/usbharu/http-signature") + credentials { -kotlin { - target { - compilations.all { - kotlinOptions.jvmTarget = JavaVersion.VERSION_11.toString() + username = project.findProperty("gpr.user") as String? ?: System.getenv("USERNAME") + password = project.findProperty("gpr.key") as String? ?: System.getenv("TOKEN") } } + maven { + name = "GitHubPackages2" + url = uri("https://maven.pkg.github.com/multim-dev/emoji-kt") + credentials { + + username = project.findProperty("gpr.user") as String? ?: System.getenv("USERNAME") + password = project.findProperty("gpr.key") as String? ?: System.getenv("TOKEN") + } + } +} +configurations { + all { + exclude("org.springframework.boot", "spring-boot-starter-logging") + exclude("ch.qos.logback", "logback-classic") + } } dependencies { - implementation("io.ktor:ktor-server-core-jvm:$ktor_version") - implementation("io.ktor:ktor-server-auth-jvm:$ktor_version") - implementation("io.ktor:ktor-server-sessions-jvm:$ktor_version") - implementation("io.ktor:ktor-server-auto-head-response-jvm:$ktor_version") - implementation("io.ktor:ktor-server-cors-jvm:$ktor_version") - implementation("io.ktor:ktor-server-default-headers-jvm:$ktor_version") - implementation("io.ktor:ktor-server-forwarded-header-jvm:$ktor_version") - implementation("io.ktor:ktor-server-call-logging-jvm:$ktor_version") - implementation("io.ktor:ktor-server-content-negotiation-jvm:$ktor_version") - implementation("io.ktor:ktor-serialization-jackson:$ktor_version") - implementation("org.jetbrains.exposed:exposed-core:$exposed_version") - implementation("org.jetbrains.exposed:exposed-jdbc:$exposed_version") - implementation("com.h2database:h2:$h2_version") - implementation("org.xerial:sqlite-jdbc:3.40.1.0") - implementation("io.ktor:ktor-server-websockets-jvm:$ktor_version") - implementation("io.ktor:ktor-server-netty-jvm:$ktor_version") - implementation("ch.qos.logback:logback-classic:$logback_version") - - implementation("io.insert-koin:koin-core:$koin_version") - implementation("io.insert-koin:koin-ktor:$koin_version") - implementation("io.insert-koin:koin-logger-slf4j:$koin_version") - implementation("io.ktor:ktor-client-logging-jvm:2.2.4") - implementation("io.ktor:ktor-server-host-common-jvm:2.2.4") - implementation("io.ktor:ktor-server-status-pages-jvm:2.2.4") - - testImplementation("io.ktor:ktor-server-tests-jvm:$ktor_version") - testImplementation("org.jetbrains.kotlin:kotlin-test-junit:$kotlin_version") - testImplementation ("org.jetbrains.kotlinx:kotlinx-coroutines-test:1.6.4") - testImplementation("io.ktor:ktor-client-mock:$ktor_version") - - implementation("io.ktor:ktor-client-core:$ktor_version") - implementation("io.ktor:ktor-client-cio:$ktor_version") - implementation("io.ktor:ktor-client-content-negotiation:$ktor_version") - testImplementation("io.ktor:ktor-client-mock:$ktor_version") - implementation("tech.barbero.http-messages-signing:http-messages-signing-core:1.0.0") - - testImplementation("org.junit.jupiter:junit-jupiter:5.8.1") - testImplementation("org.mockito.kotlin:mockito-kotlin:4.1.0") - testImplementation("org.mockito:mockito-inline:5.2.0") - - - implementation("org.drewcarlson:kjob-core:0.6.0") - testImplementation("io.ktor:ktor-server-test-host-jvm:2.2.4") - - testImplementation("org.slf4j:slf4j-simple:2.0.7") - + implementation("dev.usbharu:hideout-core:0.0.1") + implementation("dev.usbharu:hideout-mastodon:1.0-SNAPSHOT") } -jib { - if (System.getProperty("os.name").toLowerCase().contains("windows")) { - dockerClient.environment = mapOf( - "DOCKER_HOST" to "localhost:2375" - ) - } +tasks.register("run") { + dependsOn(gradle.includedBuild("hideout-core").task(":run")) } -ktor { - docker { - localImageName.set("hideout") - } +springBoot { + mainClass = "dev.usbharu.hideout.SpringApplicationKt" } + diff --git a/detekt.yml b/detekt.yml new file mode 100644 index 00000000..1f64f96f --- /dev/null +++ b/detekt.yml @@ -0,0 +1,179 @@ +build: + maxIssues: 20 + weights: + Indentation: 0 + MagicNumber: 0 + EnumEntryNameCase: 0 + VariableNaming: 0 + NoNameShadowing: 0 + +style: + ClassOrdering: + active: true + + MandatoryBracesIfStatements: + active: true + + MandatoryBracesLoops: + active: true + + MultilineLambdaItParameter: + active: false + + UseEmptyCounterpart: + active: true + + ExpressionBodySyntax: + active: true + + WildcardImport: + active: false + + ReturnCount: + active: false + + MagicNumber: + ignorePropertyDeclaration: true + + ForbiddenComment: + active: false + + ThrowsCount: + active: false + + UseCheckOrError: + active: false + + UseRequire: + active: false + + VarCouldBeVal: + ignoreLateinitVar: true + +complexity: + CognitiveComplexMethod: + active: true + + ComplexCondition: + active: true + + ComplexInterface: + active: true + threshold: 30 + + LabeledExpression: + active: false + + NamedArguments: + active: true + ignoreArgumentsMatchingNames: true + threshold: 5 + + NestedBlockDepth: + active: true + + NestedScopeFunctions: + active: true + + ReplaceSafeCallChainWithRun: + active: false + + StringLiteralDuplication: + active: false + + LongParameterList: + constructorThreshold: 10 + + TooManyFunctions: + ignoreDeprecated: true + ignoreOverridden: true + ignorePrivate: true + + LongMethod: + active: true + excludes: + - "**/test/**" + +exceptions: + ExceptionRaisedInUnexpectedLocation: + active: true + + NotImplementedDeclaration: + active: false + + ObjectExtendsThrowable: + active: true + + ThrowingExceptionInMain: + active: true + + ThrowingExceptionsWithoutMessageOrCause: + active: true + + ThrowingNewInstanceOfSameException: + active: true + + TooGenericExceptionCaught: + active: true + + TooGenericExceptionThrown: + active: true + +formatting: + Indentation: + indentSize: 4 + + NoWildcardImports: + active: false + +naming: + FunctionMaxLength: + active: true + excludes: + - "**/test/**" + + FunctionMinLength: + ignoreFunction: + - of + active: true + + LambdaParameterNaming: + active: true + + ConstructorParameterNaming: + excludes: + - "**/domain/model/ap/*" + ignoreOverridden: true + + VariableNaming: + excludes: + - "**/domain/model/ap/*" + +performance: + UnnecessaryPartOfBinaryExpression: + active: true + + UnnecessaryTemporaryInstantiation: + active: true + + SpreadOperator: + active: false + +potential-bugs: + CastToNullableType: + active: true + + DontDowncastCollectionTypes: + active: true + + ElseCaseInsteadOfExhaustiveWhen: + active: true + + HasPlatformType: + active: false + +coroutines: + RedundantSuspendModifier: + active: false + InjectDispatcher: + active: false diff --git a/docker-compose.yml b/docker-compose.yml new file mode 100644 index 00000000..78204d19 --- /dev/null +++ b/docker-compose.yml @@ -0,0 +1,11 @@ +version: "3" + +services: + db: + image: postgres:16 + ports: + - "5432:5432" + environment: + POSTGRES_USER: "postgres" + POSTGRES_PASSWORD: "password" + POSTGRES_DB: "hideout" \ No newline at end of file diff --git a/gradle.properties b/gradle.properties index 73c5f36a..29326c5e 100644 --- a/gradle.properties +++ b/gradle.properties @@ -1,7 +1,4 @@ -ktor_version=2.2.4 -kotlin_version=1.8.10 -logback_version=1.4.6 -kotlin.code.style=official -exposed_version=0.41.1 -h2_version=2.1.214 -koin_version=3.3.1 +org.gradle.parallel=true +org.gradle.configureondemand=true +org.gradle.caching=true +org.gradle.jvmargs=-Xmx4096m -XX:+HeapDumpOnOutOfMemoryError -XX:+UseParallelGC --add-opens=java.base/java.util.concurrent.locks=ALL-UNNAMED -XX:TieredStopAtLevel=1 -noverify \ No newline at end of file diff --git a/gradle/wrapper/gradle-wrapper.jar b/gradle/wrapper/gradle-wrapper.jar index 7454180f..2c352119 100644 Binary files a/gradle/wrapper/gradle-wrapper.jar and b/gradle/wrapper/gradle-wrapper.jar differ diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties index ae04661e..09523c0e 100644 --- a/gradle/wrapper/gradle-wrapper.properties +++ b/gradle/wrapper/gradle-wrapper.properties @@ -1,5 +1,7 @@ distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-7.5.1-bin.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-8.9-bin.zip +networkTimeout=10000 +validateDistributionUrl=true zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists diff --git a/gradlew b/gradlew old mode 100644 new mode 100755 index 1b6c7873..f5feea6d --- a/gradlew +++ b/gradlew @@ -15,6 +15,8 @@ # See the License for the specific language governing permissions and # limitations under the License. # +# SPDX-License-Identifier: Apache-2.0 +# ############################################################################## # @@ -55,7 +57,7 @@ # Darwin, MinGW, and NonStop. # # (3) This script is generated from the Groovy template -# https://github.com/gradle/gradle/blob/master/subprojects/plugins/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt +# https://github.com/gradle/gradle/blob/HEAD/platforms/jvm/plugins-application/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt # within the Gradle project. # # You can find Gradle at https://github.com/gradle/gradle/. @@ -80,13 +82,12 @@ do esac done -APP_HOME=$( cd "${APP_HOME:-./}" && pwd -P ) || exit - -APP_NAME="Gradle" +# This is normally unused +# shellcheck disable=SC2034 APP_BASE_NAME=${0##*/} - -# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. -DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"' +# Discard cd standard output in case $CDPATH is set (https://github.com/gradle/gradle/issues/25036) +APP_HOME=$( cd -P "${APP_HOME:-./}" > /dev/null && printf '%s +' "$PWD" ) || exit # Use the maximum available, or set MAX_FD != -1 to use that value. MAX_FD=maximum @@ -133,22 +134,29 @@ location of your Java installation." fi else JAVACMD=java - which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. + if ! command -v java >/dev/null 2>&1 + then + die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. Please set the JAVA_HOME variable in your environment to match the location of your Java installation." + fi fi # Increase the maximum file descriptors if we can. if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then case $MAX_FD in #( max*) + # In POSIX sh, ulimit -H is undefined. That's why the result is checked to see if it worked. + # shellcheck disable=SC2039,SC3045 MAX_FD=$( ulimit -H -n ) || warn "Could not query maximum file descriptor limit" esac case $MAX_FD in #( '' | soft) :;; #( *) + # In POSIX sh, ulimit -n is undefined. That's why the result is checked to see if it worked. + # shellcheck disable=SC2039,SC3045 ulimit -n "$MAX_FD" || warn "Could not set maximum file descriptor limit to $MAX_FD" esac @@ -193,11 +201,15 @@ if "$cygwin" || "$msys" ; then done fi -# Collect all arguments for the java command; -# * $DEFAULT_JVM_OPTS, $JAVA_OPTS, and $GRADLE_OPTS can contain fragments of -# shell script including quotes and variable substitutions, so put them in -# double quotes to make sure that they get re-expanded; and -# * put everything else in single quotes, so that it's not re-expanded. + +# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"' + +# Collect all arguments for the java command: +# * DEFAULT_JVM_OPTS, JAVA_OPTS, JAVA_OPTS, and optsEnvironmentVar are not allowed to contain shell fragments, +# and any embedded shellness will be escaped. +# * For example: A user cannot expect ${Hostname} to be expanded, as it is an environment variable and will be +# treated as '${Hostname}' itself on the command line. set -- \ "-Dorg.gradle.appname=$APP_BASE_NAME" \ @@ -205,6 +217,12 @@ set -- \ org.gradle.wrapper.GradleWrapperMain \ "$@" +# Stop when "xargs" is not available. +if ! command -v xargs >/dev/null 2>&1 +then + die "xargs is not available" +fi + # Use "xargs" to parse quoted args. # # With -n1 it outputs one arg per line, with the quotes and backslashes removed. diff --git a/gradlew.bat b/gradlew.bat index 107acd32..9b42019c 100644 --- a/gradlew.bat +++ b/gradlew.bat @@ -1,89 +1,94 @@ -@rem -@rem Copyright 2015 the original author or authors. -@rem -@rem Licensed under the Apache License, Version 2.0 (the "License"); -@rem you may not use this file except in compliance with the License. -@rem You may obtain a copy of the License at -@rem -@rem https://www.apache.org/licenses/LICENSE-2.0 -@rem -@rem Unless required by applicable law or agreed to in writing, software -@rem distributed under the License is distributed on an "AS IS" BASIS, -@rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -@rem See the License for the specific language governing permissions and -@rem limitations under the License. -@rem - -@if "%DEBUG%" == "" @echo off -@rem ########################################################################## -@rem -@rem Gradle startup script for Windows -@rem -@rem ########################################################################## - -@rem Set local scope for the variables with windows NT shell -if "%OS%"=="Windows_NT" setlocal - -set DIRNAME=%~dp0 -if "%DIRNAME%" == "" set DIRNAME=. -set APP_BASE_NAME=%~n0 -set APP_HOME=%DIRNAME% - -@rem Resolve any "." and ".." in APP_HOME to make it shorter. -for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi - -@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. -set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m" - -@rem Find java.exe -if defined JAVA_HOME goto findJavaFromJavaHome - -set JAVA_EXE=java.exe -%JAVA_EXE% -version >NUL 2>&1 -if "%ERRORLEVEL%" == "0" goto execute - -echo. -echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. -echo. -echo Please set the JAVA_HOME variable in your environment to match the -echo location of your Java installation. - -goto fail - -:findJavaFromJavaHome -set JAVA_HOME=%JAVA_HOME:"=% -set JAVA_EXE=%JAVA_HOME%/bin/java.exe - -if exist "%JAVA_EXE%" goto execute - -echo. -echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% -echo. -echo Please set the JAVA_HOME variable in your environment to match the -echo location of your Java installation. - -goto fail - -:execute -@rem Setup the command line - -set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar - - -@rem Execute Gradle -"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %* - -:end -@rem End local scope for the variables with windows NT shell -if "%ERRORLEVEL%"=="0" goto mainEnd - -:fail -rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of -rem the _cmd.exe /c_ return code! -if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1 -exit /b 1 - -:mainEnd -if "%OS%"=="Windows_NT" endlocal - -:omega +@rem +@rem Copyright 2015 the original author or authors. +@rem +@rem Licensed under the Apache License, Version 2.0 (the "License"); +@rem you may not use this file except in compliance with the License. +@rem You may obtain a copy of the License at +@rem +@rem https://www.apache.org/licenses/LICENSE-2.0 +@rem +@rem Unless required by applicable law or agreed to in writing, software +@rem distributed under the License is distributed on an "AS IS" BASIS, +@rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +@rem See the License for the specific language governing permissions and +@rem limitations under the License. +@rem +@rem SPDX-License-Identifier: Apache-2.0 +@rem + +@if "%DEBUG%"=="" @echo off +@rem ########################################################################## +@rem +@rem Gradle startup script for Windows +@rem +@rem ########################################################################## + +@rem Set local scope for the variables with windows NT shell +if "%OS%"=="Windows_NT" setlocal + +set DIRNAME=%~dp0 +if "%DIRNAME%"=="" set DIRNAME=. +@rem This is normally unused +set APP_BASE_NAME=%~n0 +set APP_HOME=%DIRNAME% + +@rem Resolve any "." and ".." in APP_HOME to make it shorter. +for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi + +@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m" + +@rem Find java.exe +if defined JAVA_HOME goto findJavaFromJavaHome + +set JAVA_EXE=java.exe +%JAVA_EXE% -version >NUL 2>&1 +if %ERRORLEVEL% equ 0 goto execute + +echo. 1>&2 +echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 1>&2 +echo. 1>&2 +echo Please set the JAVA_HOME variable in your environment to match the 1>&2 +echo location of your Java installation. 1>&2 + +goto fail + +:findJavaFromJavaHome +set JAVA_HOME=%JAVA_HOME:"=% +set JAVA_EXE=%JAVA_HOME%/bin/java.exe + +if exist "%JAVA_EXE%" goto execute + +echo. 1>&2 +echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 1>&2 +echo. 1>&2 +echo Please set the JAVA_HOME variable in your environment to match the 1>&2 +echo location of your Java installation. 1>&2 + +goto fail + +:execute +@rem Setup the command line + +set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar + + +@rem Execute Gradle +"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %* + +:end +@rem End local scope for the variables with windows NT shell +if %ERRORLEVEL% equ 0 goto mainEnd + +:fail +rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of +rem the _cmd.exe /c_ return code! +set EXIT_CODE=%ERRORLEVEL% +if %EXIT_CODE% equ 0 set EXIT_CODE=1 +if not ""=="%GRADLE_EXIT_CONSOLE%" exit %EXIT_CODE% +exit /b %EXIT_CODE% + +:mainEnd +if "%OS%"=="Windows_NT" endlocal + +:omega diff --git a/hideout-activitypub/build.gradle.kts b/hideout-activitypub/build.gradle.kts new file mode 100644 index 00000000..36ec82b7 --- /dev/null +++ b/hideout-activitypub/build.gradle.kts @@ -0,0 +1,21 @@ +plugins { + kotlin("jvm") version "1.9.25" +} + +group = "dev.usbharu" +version = "1.0-SNAPSHOT" + +repositories { + mavenCentral() +} + +dependencies { + testImplementation(kotlin("test")) +} + +tasks.test { + useJUnitPlatform() +} +kotlin { + jvmToolchain(21) +} \ No newline at end of file diff --git a/hideout-activitypub/gradle/wrapper/gradle-wrapper.jar b/hideout-activitypub/gradle/wrapper/gradle-wrapper.jar new file mode 100644 index 00000000..2c352119 Binary files /dev/null and b/hideout-activitypub/gradle/wrapper/gradle-wrapper.jar differ diff --git a/hideout-activitypub/gradle/wrapper/gradle-wrapper.properties b/hideout-activitypub/gradle/wrapper/gradle-wrapper.properties new file mode 100644 index 00000000..09523c0e --- /dev/null +++ b/hideout-activitypub/gradle/wrapper/gradle-wrapper.properties @@ -0,0 +1,7 @@ +distributionBase=GRADLE_USER_HOME +distributionPath=wrapper/dists +distributionUrl=https\://services.gradle.org/distributions/gradle-8.9-bin.zip +networkTimeout=10000 +validateDistributionUrl=true +zipStoreBase=GRADLE_USER_HOME +zipStorePath=wrapper/dists diff --git a/hideout-activitypub/gradlew b/hideout-activitypub/gradlew new file mode 100644 index 00000000..f5feea6d --- /dev/null +++ b/hideout-activitypub/gradlew @@ -0,0 +1,252 @@ +#!/bin/sh + +# +# Copyright © 2015-2021 the original authors. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# https://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# +# SPDX-License-Identifier: Apache-2.0 +# + +############################################################################## +# +# Gradle start up script for POSIX generated by Gradle. +# +# Important for running: +# +# (1) You need a POSIX-compliant shell to run this script. If your /bin/sh is +# noncompliant, but you have some other compliant shell such as ksh or +# bash, then to run this script, type that shell name before the whole +# command line, like: +# +# ksh Gradle +# +# Busybox and similar reduced shells will NOT work, because this script +# requires all of these POSIX shell features: +# * functions; +# * expansions «$var», «${var}», «${var:-default}», «${var+SET}», +# «${var#prefix}», «${var%suffix}», and «$( cmd )»; +# * compound commands having a testable exit status, especially «case»; +# * various built-in commands including «command», «set», and «ulimit». +# +# Important for patching: +# +# (2) This script targets any POSIX shell, so it avoids extensions provided +# by Bash, Ksh, etc; in particular arrays are avoided. +# +# The "traditional" practice of packing multiple parameters into a +# space-separated string is a well documented source of bugs and security +# problems, so this is (mostly) avoided, by progressively accumulating +# options in "$@", and eventually passing that to Java. +# +# Where the inherited environment variables (DEFAULT_JVM_OPTS, JAVA_OPTS, +# and GRADLE_OPTS) rely on word-splitting, this is performed explicitly; +# see the in-line comments for details. +# +# There are tweaks for specific operating systems such as AIX, CygWin, +# Darwin, MinGW, and NonStop. +# +# (3) This script is generated from the Groovy template +# https://github.com/gradle/gradle/blob/HEAD/platforms/jvm/plugins-application/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt +# within the Gradle project. +# +# You can find Gradle at https://github.com/gradle/gradle/. +# +############################################################################## + +# Attempt to set APP_HOME + +# Resolve links: $0 may be a link +app_path=$0 + +# Need this for daisy-chained symlinks. +while + APP_HOME=${app_path%"${app_path##*/}"} # leaves a trailing /; empty if no leading path + [ -h "$app_path" ] +do + ls=$( ls -ld "$app_path" ) + link=${ls#*' -> '} + case $link in #( + /*) app_path=$link ;; #( + *) app_path=$APP_HOME$link ;; + esac +done + +# This is normally unused +# shellcheck disable=SC2034 +APP_BASE_NAME=${0##*/} +# Discard cd standard output in case $CDPATH is set (https://github.com/gradle/gradle/issues/25036) +APP_HOME=$( cd -P "${APP_HOME:-./}" > /dev/null && printf '%s +' "$PWD" ) || exit + +# Use the maximum available, or set MAX_FD != -1 to use that value. +MAX_FD=maximum + +warn () { + echo "$*" +} >&2 + +die () { + echo + echo "$*" + echo + exit 1 +} >&2 + +# OS specific support (must be 'true' or 'false'). +cygwin=false +msys=false +darwin=false +nonstop=false +case "$( uname )" in #( + CYGWIN* ) cygwin=true ;; #( + Darwin* ) darwin=true ;; #( + MSYS* | MINGW* ) msys=true ;; #( + NONSTOP* ) nonstop=true ;; +esac + +CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar + + +# Determine the Java command to use to start the JVM. +if [ -n "$JAVA_HOME" ] ; then + if [ -x "$JAVA_HOME/jre/sh/java" ] ; then + # IBM's JDK on AIX uses strange locations for the executables + JAVACMD=$JAVA_HOME/jre/sh/java + else + JAVACMD=$JAVA_HOME/bin/java + fi + if [ ! -x "$JAVACMD" ] ; then + die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." + fi +else + JAVACMD=java + if ! command -v java >/dev/null 2>&1 + then + die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." + fi +fi + +# Increase the maximum file descriptors if we can. +if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then + case $MAX_FD in #( + max*) + # In POSIX sh, ulimit -H is undefined. That's why the result is checked to see if it worked. + # shellcheck disable=SC2039,SC3045 + MAX_FD=$( ulimit -H -n ) || + warn "Could not query maximum file descriptor limit" + esac + case $MAX_FD in #( + '' | soft) :;; #( + *) + # In POSIX sh, ulimit -n is undefined. That's why the result is checked to see if it worked. + # shellcheck disable=SC2039,SC3045 + ulimit -n "$MAX_FD" || + warn "Could not set maximum file descriptor limit to $MAX_FD" + esac +fi + +# Collect all arguments for the java command, stacking in reverse order: +# * args from the command line +# * the main class name +# * -classpath +# * -D...appname settings +# * --module-path (only if needed) +# * DEFAULT_JVM_OPTS, JAVA_OPTS, and GRADLE_OPTS environment variables. + +# For Cygwin or MSYS, switch paths to Windows format before running java +if "$cygwin" || "$msys" ; then + APP_HOME=$( cygpath --path --mixed "$APP_HOME" ) + CLASSPATH=$( cygpath --path --mixed "$CLASSPATH" ) + + JAVACMD=$( cygpath --unix "$JAVACMD" ) + + # Now convert the arguments - kludge to limit ourselves to /bin/sh + for arg do + if + case $arg in #( + -*) false ;; # don't mess with options #( + /?*) t=${arg#/} t=/${t%%/*} # looks like a POSIX filepath + [ -e "$t" ] ;; #( + *) false ;; + esac + then + arg=$( cygpath --path --ignore --mixed "$arg" ) + fi + # Roll the args list around exactly as many times as the number of + # args, so each arg winds up back in the position where it started, but + # possibly modified. + # + # NB: a `for` loop captures its iteration list before it begins, so + # changing the positional parameters here affects neither the number of + # iterations, nor the values presented in `arg`. + shift # remove old arg + set -- "$@" "$arg" # push replacement arg + done +fi + + +# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"' + +# Collect all arguments for the java command: +# * DEFAULT_JVM_OPTS, JAVA_OPTS, JAVA_OPTS, and optsEnvironmentVar are not allowed to contain shell fragments, +# and any embedded shellness will be escaped. +# * For example: A user cannot expect ${Hostname} to be expanded, as it is an environment variable and will be +# treated as '${Hostname}' itself on the command line. + +set -- \ + "-Dorg.gradle.appname=$APP_BASE_NAME" \ + -classpath "$CLASSPATH" \ + org.gradle.wrapper.GradleWrapperMain \ + "$@" + +# Stop when "xargs" is not available. +if ! command -v xargs >/dev/null 2>&1 +then + die "xargs is not available" +fi + +# Use "xargs" to parse quoted args. +# +# With -n1 it outputs one arg per line, with the quotes and backslashes removed. +# +# In Bash we could simply go: +# +# readarray ARGS < <( xargs -n1 <<<"$var" ) && +# set -- "${ARGS[@]}" "$@" +# +# but POSIX shell has neither arrays nor command substitution, so instead we +# post-process each arg (as a line of input to sed) to backslash-escape any +# character that might be a shell metacharacter, then use eval to reverse +# that process (while maintaining the separation between arguments), and wrap +# the whole thing up as a single "set" statement. +# +# This will of course break if any of these variables contains a newline or +# an unmatched quote. +# + +eval "set -- $( + printf '%s\n' "$DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS" | + xargs -n1 | + sed ' s~[^-[:alnum:]+,./:=@_]~\\&~g; ' | + tr '\n' ' ' + )" '"$@"' + +exec "$JAVACMD" "$@" diff --git a/hideout-activitypub/gradlew.bat b/hideout-activitypub/gradlew.bat new file mode 100644 index 00000000..9b42019c --- /dev/null +++ b/hideout-activitypub/gradlew.bat @@ -0,0 +1,94 @@ +@rem +@rem Copyright 2015 the original author or authors. +@rem +@rem Licensed under the Apache License, Version 2.0 (the "License"); +@rem you may not use this file except in compliance with the License. +@rem You may obtain a copy of the License at +@rem +@rem https://www.apache.org/licenses/LICENSE-2.0 +@rem +@rem Unless required by applicable law or agreed to in writing, software +@rem distributed under the License is distributed on an "AS IS" BASIS, +@rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +@rem See the License for the specific language governing permissions and +@rem limitations under the License. +@rem +@rem SPDX-License-Identifier: Apache-2.0 +@rem + +@if "%DEBUG%"=="" @echo off +@rem ########################################################################## +@rem +@rem Gradle startup script for Windows +@rem +@rem ########################################################################## + +@rem Set local scope for the variables with windows NT shell +if "%OS%"=="Windows_NT" setlocal + +set DIRNAME=%~dp0 +if "%DIRNAME%"=="" set DIRNAME=. +@rem This is normally unused +set APP_BASE_NAME=%~n0 +set APP_HOME=%DIRNAME% + +@rem Resolve any "." and ".." in APP_HOME to make it shorter. +for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi + +@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m" + +@rem Find java.exe +if defined JAVA_HOME goto findJavaFromJavaHome + +set JAVA_EXE=java.exe +%JAVA_EXE% -version >NUL 2>&1 +if %ERRORLEVEL% equ 0 goto execute + +echo. 1>&2 +echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 1>&2 +echo. 1>&2 +echo Please set the JAVA_HOME variable in your environment to match the 1>&2 +echo location of your Java installation. 1>&2 + +goto fail + +:findJavaFromJavaHome +set JAVA_HOME=%JAVA_HOME:"=% +set JAVA_EXE=%JAVA_HOME%/bin/java.exe + +if exist "%JAVA_EXE%" goto execute + +echo. 1>&2 +echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 1>&2 +echo. 1>&2 +echo Please set the JAVA_HOME variable in your environment to match the 1>&2 +echo location of your Java installation. 1>&2 + +goto fail + +:execute +@rem Setup the command line + +set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar + + +@rem Execute Gradle +"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %* + +:end +@rem End local scope for the variables with windows NT shell +if %ERRORLEVEL% equ 0 goto mainEnd + +:fail +rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of +rem the _cmd.exe /c_ return code! +set EXIT_CODE=%ERRORLEVEL% +if %EXIT_CODE% equ 0 set EXIT_CODE=1 +if not ""=="%GRADLE_EXIT_CONSOLE%" exit %EXIT_CODE% +exit /b %EXIT_CODE% + +:mainEnd +if "%OS%"=="Windows_NT" endlocal + +:omega diff --git a/hideout-activitypub/settings.gradle.kts b/hideout-activitypub/settings.gradle.kts new file mode 100644 index 00000000..8f4bdd48 --- /dev/null +++ b/hideout-activitypub/settings.gradle.kts @@ -0,0 +1,5 @@ +plugins { + id("org.gradle.toolchains.foojay-resolver-convention") version "0.8.0" +} +rootProject.name = "hideout-activitypub" + diff --git a/hideout-activitypub/src/main/kotlin/Main.kt b/hideout-activitypub/src/main/kotlin/Main.kt new file mode 100644 index 00000000..27f6ee1a --- /dev/null +++ b/hideout-activitypub/src/main/kotlin/Main.kt @@ -0,0 +1,5 @@ +package dev.usbharu + +fun main() { + println("Hello World!") +} \ No newline at end of file diff --git a/hideout-core/allowed-licenses.json b/hideout-core/allowed-licenses.json new file mode 100644 index 00000000..64bcca39 --- /dev/null +++ b/hideout-core/allowed-licenses.json @@ -0,0 +1,25 @@ +{ + "allowedLicenses": [ + { + "moduleLicense": "Apache License, Version 2.0" + }, + { + "moduleLicense": "MIT License" + }, + { + "moduleLicense": "MIT-0" + }, + { + "moduleLicense": "The BSD License" + }, + { + "moduleLicense": "PUBLIC DOMAIN" + }, + { + "moduleLicense": "The 2-Clause BSD License" + }, + { + "moduleLicense": "GNU GENERAL PUBLIC LICENSE, Version 2 + Classpath Exception" + } + ] +} \ No newline at end of file diff --git a/hideout-core/build.gradle.kts b/hideout-core/build.gradle.kts new file mode 100644 index 00000000..2739b00b --- /dev/null +++ b/hideout-core/build.gradle.kts @@ -0,0 +1,245 @@ +import com.github.jk1.license.filter.DependencyFilter +import com.github.jk1.license.filter.LicenseBundleNormalizer +import com.github.jk1.license.importer.DependencyDataImporter +import com.github.jk1.license.importer.XmlReportImporter +import com.github.jk1.license.render.* +import org.jetbrains.kotlin.gradle.dsl.JvmTarget + +plugins { + alias(libs.plugins.kotlin.jvm) + alias(libs.plugins.detekt) + alias(libs.plugins.spring.boot) + alias(libs.plugins.kotlin.spring) + alias(libs.plugins.kover) + alias(libs.plugins.license.report) +} + +apply { + plugin("io.spring.dependency-management") +} + +group = "dev.usbharu" +version = "0.0.1" + + +tasks.withType { + useJUnitPlatform() + doFirst { + jvmArgs = arrayOf( + "--add-opens", "java.base/java.lang=ALL-UNNAMED", + "--add-opens", "java.base/java.util=ALL-UNNAMED", + "--add-opens", "java.naming/javax.naming=ALL-UNNAMED", + "--add-opens", "java.base/java.util.concurrent.locks=ALL-UNNAMED" + ).toMutableList() + } +} + +kotlin { + jvmToolchain(21) + compilerOptions { + freeCompilerArgs.add("-Xjsr305=strict") + jvmTarget = JvmTarget.JVM_21 + } +} + + +repositories { + mavenCentral() + maven { + url = uri("https://git.usbharu.dev/api/packages/usbharu/maven") + } + maven { + name = "GitHubPackages" + url = uri("https://maven.pkg.github.com/usbharu/http-signature") + credentials { + + username = project.findProperty("gpr.user") as String? ?: System.getenv("USERNAME") + password = project.findProperty("gpr.key") as String? ?: System.getenv("TOKEN") + } + } + maven { + name = "GitHubPackages2" + url = uri("https://maven.pkg.github.com/multim-dev/emoji-kt") + credentials { + + username = project.findProperty("gpr.user") as String? ?: System.getenv("USERNAME") + password = project.findProperty("gpr.key") as String? ?: System.getenv("TOKEN") + } + } +} + + +val os = org.gradle.nativeplatform.platform.internal + .DefaultNativePlatform.getCurrentOperatingSystem() + +dependencies { + developmentOnly(libs.h2db) + detektPlugins(libs.detekt.formatting) + + implementation(libs.bundles.exposed) + implementation(libs.bundles.coroutines) + implementation(libs.bundles.ktor.client) + implementation(libs.bundles.apache.tika) + implementation(libs.bundles.openapi) +// implementation(libs.bundles.owl.producer) +// implementation(libs.bundles.owl.broker) + implementation(libs.bundles.spring.boot.oauth2) + implementation(libs.bundles.spring.boot.data.mongodb) + implementation("org.springframework.boot:spring-boot-starter-actuator") + implementation("org.springframework.boot:spring-boot-starter-web") + implementation("org.springframework.boot:spring-boot-starter-security") + implementation("org.springframework.boot:spring-boot-starter-thymeleaf") + implementation("org.springframework.boot:spring-boot-starter-log4j2") + implementation("org.springframework.boot:spring-boot-starter-validation") + annotationProcessor("org.springframework:spring-context-indexer") + + implementation(libs.blurhash) + implementation(libs.aws.s3) + implementation(libs.jsoup) + implementation(libs.owasp.java.html.sanitizer) + implementation(libs.postgresql) + implementation(libs.imageio.webp) + implementation(libs.thumbnailator) + implementation(libs.flyway.core) + runtimeOnly(libs.flyway.postgresql) + +// implementation("dev.usbharu:owl-common-serialize-jackson:0.0.1") + + implementation(libs.javacv) { + exclude(module = "opencv") + exclude(module = "flycapture") + exclude(module = "artoolkitplus") + exclude(module = "libdc1394") + exclude(module = "librealsense") + exclude(module = "librealsense2") + exclude(module = "tesseract") + exclude(module = "libfreenect") + exclude(module = "libfreenect2") + } + if (os.isWindows) { + implementation(variantOf(libs.javacv.ffmpeg) { classifier("windows-x86_64") }) + } else { + implementation(variantOf(libs.javacv.ffmpeg) { classifier("linux-x86_64") }) + } + + implementation("dev.usbharu:http-signature:1.0.0") + implementation("dev.usbharu:emoji-kt:2.0.0") + + + + testImplementation("org.springframework.boot:spring-boot-starter-test") + testImplementation(libs.kotlin.junit) + testImplementation(libs.coroutines.test) + testImplementation(libs.ktor.client.mock) + testImplementation(libs.h2db) + testImplementation("org.mockito.kotlin:mockito-kotlin:5.4.0") + testImplementation("org.mockito:mockito-inline:5.2.0") + testImplementation("nl.jqno.equalsverifier:equalsverifier:3.16.1") + testImplementation("com.jparams:to-string-verifier:1.4.8") + +} + +detekt { + parallel = true + config = files("../detekt.yml") + buildUponDefaultConfig = true + basePath = "${rootDir.absolutePath}/src/main/kotlin" + autoCorrect = true +} + +configurations.matching { it.name == "detekt" }.all { + resolutionStrategy.eachDependency { + if (requested.group == "org.jetbrains.kotlin") { + useVersion(io.gitlab.arturbosch.detekt.getSupportedKotlinVersion()) + } + } +} + +tasks.withType { + exclude("**/generated/**") + doFirst { + + } + setSource("src/main/kotlin") + exclude("build/") +} + +tasks.withType().configureEach { + exclude("**/org/koin/ksp/generated/**", "**/generated/**") +} + +tasks.withType().configureEach { + exclude("**/org/koin/ksp/generated/**", "**/generated/**") +} + +configurations { + all { + exclude("org.springframework.boot", "spring-boot-starter-logging") + exclude("ch.qos.logback", "logback-classic") + } +} + +project.gradle.taskGraph.whenReady { + println(this.allTasks) + this.allTasks.map { println(it.name) } + if (this.hasTask(":koverGenerateArtifact")) { + println("has task") + val task = this.allTasks.find { it.name == "test" } + val verificationTask = task as VerificationTask + verificationTask.ignoreFailures = true + } +} + +kover { + currentProject { + sources { + excludedSourceSets.addAll( + "aot", "e2eTest", "intTest" + ) + + } + } + + reports { + filters { + excludes { + packages( + "dev.usbharu.hideout.activitypub.domain.exception", + "dev.usbharu.hideout.core.domain.exception", + "dev.usbharu.hideout.core.domain.exception.media", + "dev.usbharu.hideout.core.domain.exception.resource", + "dev.usbharu.hideout.core.domain.exception.resource.local" + ) + annotatedBy("org.springframework.context.annotation.Configuration") + annotatedBy("org.springframework.boot.context.properties.ConfigurationProperties") + packages( + "dev.usbharu.hideout.controller.mastodon.generated", + "dev.usbharu.hideout.domain.mastodon.model.generated" + ) + packages("org.springframework") + packages("org.jetbrains") + } + } + } +} + +springBoot { + buildInfo { + + } +} + +licenseReport { + excludeOwnGroup = true + + importers = arrayOf(XmlReportImporter("hideout", File("$projectDir/license-list.xml"))) + renderers = arrayOf( + InventoryHtmlReportRenderer(), + CsvReportRenderer(), + JsonReportRenderer(), + XmlReportRenderer() + ) + filters = arrayOf(LicenseBundleNormalizer("$projectDir/license-normalizer-bundle.json", true)) + allowedLicensesFile = File("$projectDir/allowed-licenses.json") + configurations = arrayOf("productionRuntimeClasspath") +} \ No newline at end of file diff --git a/hideout-core/gradle.properties b/hideout-core/gradle.properties new file mode 100644 index 00000000..f3b8b6f9 --- /dev/null +++ b/hideout-core/gradle.properties @@ -0,0 +1,22 @@ +# +# Copyright (C) 2024 usbharu +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# +kotlin.code.style=official +org.gradle.parallel=true +org.gradle.configureondemand=true +org.gradle.caching=true +org.gradle.jvmargs=-Xmx4096m -XX:+HeapDumpOnOutOfMemoryError -XX:+UseParallelGC +org.gradle.configuration-cache=true +org.gradle.configuration-cache.problems=warn \ No newline at end of file diff --git a/hideout-core/gradle/wrapper/gradle-wrapper.jar b/hideout-core/gradle/wrapper/gradle-wrapper.jar new file mode 100644 index 00000000..2c352119 Binary files /dev/null and b/hideout-core/gradle/wrapper/gradle-wrapper.jar differ diff --git a/hideout-core/gradle/wrapper/gradle-wrapper.properties b/hideout-core/gradle/wrapper/gradle-wrapper.properties new file mode 100644 index 00000000..09523c0e --- /dev/null +++ b/hideout-core/gradle/wrapper/gradle-wrapper.properties @@ -0,0 +1,7 @@ +distributionBase=GRADLE_USER_HOME +distributionPath=wrapper/dists +distributionUrl=https\://services.gradle.org/distributions/gradle-8.9-bin.zip +networkTimeout=10000 +validateDistributionUrl=true +zipStoreBase=GRADLE_USER_HOME +zipStorePath=wrapper/dists diff --git a/hideout-core/gradlew b/hideout-core/gradlew new file mode 100644 index 00000000..f5feea6d --- /dev/null +++ b/hideout-core/gradlew @@ -0,0 +1,252 @@ +#!/bin/sh + +# +# Copyright © 2015-2021 the original authors. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# https://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# +# SPDX-License-Identifier: Apache-2.0 +# + +############################################################################## +# +# Gradle start up script for POSIX generated by Gradle. +# +# Important for running: +# +# (1) You need a POSIX-compliant shell to run this script. If your /bin/sh is +# noncompliant, but you have some other compliant shell such as ksh or +# bash, then to run this script, type that shell name before the whole +# command line, like: +# +# ksh Gradle +# +# Busybox and similar reduced shells will NOT work, because this script +# requires all of these POSIX shell features: +# * functions; +# * expansions «$var», «${var}», «${var:-default}», «${var+SET}», +# «${var#prefix}», «${var%suffix}», and «$( cmd )»; +# * compound commands having a testable exit status, especially «case»; +# * various built-in commands including «command», «set», and «ulimit». +# +# Important for patching: +# +# (2) This script targets any POSIX shell, so it avoids extensions provided +# by Bash, Ksh, etc; in particular arrays are avoided. +# +# The "traditional" practice of packing multiple parameters into a +# space-separated string is a well documented source of bugs and security +# problems, so this is (mostly) avoided, by progressively accumulating +# options in "$@", and eventually passing that to Java. +# +# Where the inherited environment variables (DEFAULT_JVM_OPTS, JAVA_OPTS, +# and GRADLE_OPTS) rely on word-splitting, this is performed explicitly; +# see the in-line comments for details. +# +# There are tweaks for specific operating systems such as AIX, CygWin, +# Darwin, MinGW, and NonStop. +# +# (3) This script is generated from the Groovy template +# https://github.com/gradle/gradle/blob/HEAD/platforms/jvm/plugins-application/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt +# within the Gradle project. +# +# You can find Gradle at https://github.com/gradle/gradle/. +# +############################################################################## + +# Attempt to set APP_HOME + +# Resolve links: $0 may be a link +app_path=$0 + +# Need this for daisy-chained symlinks. +while + APP_HOME=${app_path%"${app_path##*/}"} # leaves a trailing /; empty if no leading path + [ -h "$app_path" ] +do + ls=$( ls -ld "$app_path" ) + link=${ls#*' -> '} + case $link in #( + /*) app_path=$link ;; #( + *) app_path=$APP_HOME$link ;; + esac +done + +# This is normally unused +# shellcheck disable=SC2034 +APP_BASE_NAME=${0##*/} +# Discard cd standard output in case $CDPATH is set (https://github.com/gradle/gradle/issues/25036) +APP_HOME=$( cd -P "${APP_HOME:-./}" > /dev/null && printf '%s +' "$PWD" ) || exit + +# Use the maximum available, or set MAX_FD != -1 to use that value. +MAX_FD=maximum + +warn () { + echo "$*" +} >&2 + +die () { + echo + echo "$*" + echo + exit 1 +} >&2 + +# OS specific support (must be 'true' or 'false'). +cygwin=false +msys=false +darwin=false +nonstop=false +case "$( uname )" in #( + CYGWIN* ) cygwin=true ;; #( + Darwin* ) darwin=true ;; #( + MSYS* | MINGW* ) msys=true ;; #( + NONSTOP* ) nonstop=true ;; +esac + +CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar + + +# Determine the Java command to use to start the JVM. +if [ -n "$JAVA_HOME" ] ; then + if [ -x "$JAVA_HOME/jre/sh/java" ] ; then + # IBM's JDK on AIX uses strange locations for the executables + JAVACMD=$JAVA_HOME/jre/sh/java + else + JAVACMD=$JAVA_HOME/bin/java + fi + if [ ! -x "$JAVACMD" ] ; then + die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." + fi +else + JAVACMD=java + if ! command -v java >/dev/null 2>&1 + then + die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." + fi +fi + +# Increase the maximum file descriptors if we can. +if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then + case $MAX_FD in #( + max*) + # In POSIX sh, ulimit -H is undefined. That's why the result is checked to see if it worked. + # shellcheck disable=SC2039,SC3045 + MAX_FD=$( ulimit -H -n ) || + warn "Could not query maximum file descriptor limit" + esac + case $MAX_FD in #( + '' | soft) :;; #( + *) + # In POSIX sh, ulimit -n is undefined. That's why the result is checked to see if it worked. + # shellcheck disable=SC2039,SC3045 + ulimit -n "$MAX_FD" || + warn "Could not set maximum file descriptor limit to $MAX_FD" + esac +fi + +# Collect all arguments for the java command, stacking in reverse order: +# * args from the command line +# * the main class name +# * -classpath +# * -D...appname settings +# * --module-path (only if needed) +# * DEFAULT_JVM_OPTS, JAVA_OPTS, and GRADLE_OPTS environment variables. + +# For Cygwin or MSYS, switch paths to Windows format before running java +if "$cygwin" || "$msys" ; then + APP_HOME=$( cygpath --path --mixed "$APP_HOME" ) + CLASSPATH=$( cygpath --path --mixed "$CLASSPATH" ) + + JAVACMD=$( cygpath --unix "$JAVACMD" ) + + # Now convert the arguments - kludge to limit ourselves to /bin/sh + for arg do + if + case $arg in #( + -*) false ;; # don't mess with options #( + /?*) t=${arg#/} t=/${t%%/*} # looks like a POSIX filepath + [ -e "$t" ] ;; #( + *) false ;; + esac + then + arg=$( cygpath --path --ignore --mixed "$arg" ) + fi + # Roll the args list around exactly as many times as the number of + # args, so each arg winds up back in the position where it started, but + # possibly modified. + # + # NB: a `for` loop captures its iteration list before it begins, so + # changing the positional parameters here affects neither the number of + # iterations, nor the values presented in `arg`. + shift # remove old arg + set -- "$@" "$arg" # push replacement arg + done +fi + + +# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"' + +# Collect all arguments for the java command: +# * DEFAULT_JVM_OPTS, JAVA_OPTS, JAVA_OPTS, and optsEnvironmentVar are not allowed to contain shell fragments, +# and any embedded shellness will be escaped. +# * For example: A user cannot expect ${Hostname} to be expanded, as it is an environment variable and will be +# treated as '${Hostname}' itself on the command line. + +set -- \ + "-Dorg.gradle.appname=$APP_BASE_NAME" \ + -classpath "$CLASSPATH" \ + org.gradle.wrapper.GradleWrapperMain \ + "$@" + +# Stop when "xargs" is not available. +if ! command -v xargs >/dev/null 2>&1 +then + die "xargs is not available" +fi + +# Use "xargs" to parse quoted args. +# +# With -n1 it outputs one arg per line, with the quotes and backslashes removed. +# +# In Bash we could simply go: +# +# readarray ARGS < <( xargs -n1 <<<"$var" ) && +# set -- "${ARGS[@]}" "$@" +# +# but POSIX shell has neither arrays nor command substitution, so instead we +# post-process each arg (as a line of input to sed) to backslash-escape any +# character that might be a shell metacharacter, then use eval to reverse +# that process (while maintaining the separation between arguments), and wrap +# the whole thing up as a single "set" statement. +# +# This will of course break if any of these variables contains a newline or +# an unmatched quote. +# + +eval "set -- $( + printf '%s\n' "$DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS" | + xargs -n1 | + sed ' s~[^-[:alnum:]+,./:=@_]~\\&~g; ' | + tr '\n' ' ' + )" '"$@"' + +exec "$JAVACMD" "$@" diff --git a/hideout-core/gradlew.bat b/hideout-core/gradlew.bat new file mode 100644 index 00000000..9b42019c --- /dev/null +++ b/hideout-core/gradlew.bat @@ -0,0 +1,94 @@ +@rem +@rem Copyright 2015 the original author or authors. +@rem +@rem Licensed under the Apache License, Version 2.0 (the "License"); +@rem you may not use this file except in compliance with the License. +@rem You may obtain a copy of the License at +@rem +@rem https://www.apache.org/licenses/LICENSE-2.0 +@rem +@rem Unless required by applicable law or agreed to in writing, software +@rem distributed under the License is distributed on an "AS IS" BASIS, +@rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +@rem See the License for the specific language governing permissions and +@rem limitations under the License. +@rem +@rem SPDX-License-Identifier: Apache-2.0 +@rem + +@if "%DEBUG%"=="" @echo off +@rem ########################################################################## +@rem +@rem Gradle startup script for Windows +@rem +@rem ########################################################################## + +@rem Set local scope for the variables with windows NT shell +if "%OS%"=="Windows_NT" setlocal + +set DIRNAME=%~dp0 +if "%DIRNAME%"=="" set DIRNAME=. +@rem This is normally unused +set APP_BASE_NAME=%~n0 +set APP_HOME=%DIRNAME% + +@rem Resolve any "." and ".." in APP_HOME to make it shorter. +for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi + +@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m" + +@rem Find java.exe +if defined JAVA_HOME goto findJavaFromJavaHome + +set JAVA_EXE=java.exe +%JAVA_EXE% -version >NUL 2>&1 +if %ERRORLEVEL% equ 0 goto execute + +echo. 1>&2 +echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 1>&2 +echo. 1>&2 +echo Please set the JAVA_HOME variable in your environment to match the 1>&2 +echo location of your Java installation. 1>&2 + +goto fail + +:findJavaFromJavaHome +set JAVA_HOME=%JAVA_HOME:"=% +set JAVA_EXE=%JAVA_HOME%/bin/java.exe + +if exist "%JAVA_EXE%" goto execute + +echo. 1>&2 +echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 1>&2 +echo. 1>&2 +echo Please set the JAVA_HOME variable in your environment to match the 1>&2 +echo location of your Java installation. 1>&2 + +goto fail + +:execute +@rem Setup the command line + +set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar + + +@rem Execute Gradle +"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %* + +:end +@rem End local scope for the variables with windows NT shell +if %ERRORLEVEL% equ 0 goto mainEnd + +:fail +rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of +rem the _cmd.exe /c_ return code! +set EXIT_CODE=%ERRORLEVEL% +if %EXIT_CODE% equ 0 set EXIT_CODE=1 +if not ""=="%GRADLE_EXIT_CONSOLE%" exit %EXIT_CODE% +exit /b %EXIT_CODE% + +:mainEnd +if "%OS%"=="Windows_NT" endlocal + +:omega diff --git a/hideout-core/license-normalizer-bundle.json b/hideout-core/license-normalizer-bundle.json new file mode 100644 index 00000000..fe85ed5f --- /dev/null +++ b/hideout-core/license-normalizer-bundle.json @@ -0,0 +1,56 @@ +{ + "bundles": [ + { + "bundleName": "Apache-2.0", + "licenseName": "Apache License, Version 2.0", + "licenseUrl": "https://www.apache.org/licenses/LICENSE-2.0.txt" + }, + { + "bundleName": "EPL-1.0", + "licenseName": "Eclipse Public License - v 1.0", + "licenseUrl": "http://www.eclipse.org/legal/epl-v10.html" + } + ], + "transformationRules": [ + { + "bundleName": "Apache-2.0", + "licenseNamePattern": ".*The Apache Software License, Version 2.0.*" + }, + { + "bundleName": "Apache-2.0", + "licenseNamePattern": "Apache 2" + }, + { + "bundleName": "Apache-2.0", + "licenseUrlPattern": "http://www.apache.org/licenses/LICENSE-2.0.txt" + }, + { + "bundleName": "Apache-2.0", + "licenseUrlPattern": "https://www.apache.org/licenses/LICENSE-2.0" + }, + { + "bundleName": "Apache-2.0", + "licenseUrlPattern": "https://aws.amazon.com/apache2.0" + }, + { + "bundleName": "Apache-2.0", + "licenseUrlPattern": "https://www.apache.org/licenses/LICENSE-2.0.txt" + }, + { + "bundleName": "Apache-2.0", + "licenseNamePattern": "Special Apache" + }, + { + "bundleName": "Apache-2.0", + "licenseNamePattern": "Keep this name" + }, + { + "bundleName": "Apache-2.0", + "licenseNamePattern": "The Apache Software License, Version 2.0" + }, + { + "bundleName": "EPL-1.0", + "licenseNamePattern": "EPL 1.0" + } + ] +} \ No newline at end of file diff --git a/hideout-core/settings.gradle.kts b/hideout-core/settings.gradle.kts new file mode 100644 index 00000000..81e7fae4 --- /dev/null +++ b/hideout-core/settings.gradle.kts @@ -0,0 +1,18 @@ +plugins { + id("org.gradle.toolchains.foojay-resolver-convention") version "0.8.0" +} +rootProject.name = "hideout-core" + +includeBuild("../owl") + +dependencyResolutionManagement { + repositories { + mavenCentral() + } + + versionCatalogs { + create("libs") { + from(files("../libs.versions.toml")) + } + } +} \ No newline at end of file diff --git a/hideout-core/src/main/kotlin/dev/usbharu/hideout/SpringApplication.kt b/hideout-core/src/main/kotlin/dev/usbharu/hideout/SpringApplication.kt new file mode 100644 index 00000000..bedc423c --- /dev/null +++ b/hideout-core/src/main/kotlin/dev/usbharu/hideout/SpringApplication.kt @@ -0,0 +1,32 @@ +/* + * Copyright (C) 2024 usbharu + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package dev.usbharu.hideout + +import org.springframework.boot.autoconfigure.SpringBootApplication +import org.springframework.boot.context.properties.ConfigurationPropertiesScan +import org.springframework.boot.runApplication +import org.springframework.cache.annotation.EnableCaching + +@SpringBootApplication +@ConfigurationPropertiesScan +@EnableCaching +class SpringApplication + +@Suppress("SpreadOperator") +fun main(args: Array) { + runApplication(*args) +} diff --git a/hideout-core/src/main/kotlin/dev/usbharu/hideout/core/application/actor/DeleteLocalActor.kt b/hideout-core/src/main/kotlin/dev/usbharu/hideout/core/application/actor/DeleteLocalActor.kt new file mode 100644 index 00000000..a00a4556 --- /dev/null +++ b/hideout-core/src/main/kotlin/dev/usbharu/hideout/core/application/actor/DeleteLocalActor.kt @@ -0,0 +1,5 @@ +package dev.usbharu.hideout.core.application.actor + +import dev.usbharu.hideout.core.domain.model.actor.ActorId + +data class DeleteLocalActor(val actorId: ActorId) diff --git a/hideout-core/src/main/kotlin/dev/usbharu/hideout/core/application/actor/GetUserDetail.kt b/hideout-core/src/main/kotlin/dev/usbharu/hideout/core/application/actor/GetUserDetail.kt new file mode 100644 index 00000000..16ece3c9 --- /dev/null +++ b/hideout-core/src/main/kotlin/dev/usbharu/hideout/core/application/actor/GetUserDetail.kt @@ -0,0 +1,19 @@ +/* + * Copyright (C) 2024 usbharu + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package dev.usbharu.hideout.core.application.actor + +data class GetUserDetail(val id: Long) diff --git a/hideout-core/src/main/kotlin/dev/usbharu/hideout/core/application/actor/GetUserDetailApplicationService.kt b/hideout-core/src/main/kotlin/dev/usbharu/hideout/core/application/actor/GetUserDetailApplicationService.kt new file mode 100644 index 00000000..6d990ee2 --- /dev/null +++ b/hideout-core/src/main/kotlin/dev/usbharu/hideout/core/application/actor/GetUserDetailApplicationService.kt @@ -0,0 +1,52 @@ +/* + * Copyright (C) 2024 usbharu + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package dev.usbharu.hideout.core.application.actor + +import dev.usbharu.hideout.core.application.exception.InternalServerException +import dev.usbharu.hideout.core.application.shared.AbstractApplicationService +import dev.usbharu.hideout.core.application.shared.Transaction +import dev.usbharu.hideout.core.domain.model.actor.ActorRepository +import dev.usbharu.hideout.core.domain.model.emoji.CustomEmojiRepository +import dev.usbharu.hideout.core.domain.model.support.principal.Principal +import dev.usbharu.hideout.core.domain.model.userdetails.UserDetailId +import dev.usbharu.hideout.core.domain.model.userdetails.UserDetailRepository +import org.slf4j.LoggerFactory +import org.springframework.stereotype.Service + +@Service +class GetUserDetailApplicationService( + private val actorRepository: ActorRepository, + private val userDetailRepository: UserDetailRepository, + private val customEmojiRepository: CustomEmojiRepository, + transaction: Transaction, +) : + AbstractApplicationService(transaction, Companion.logger) { + override suspend fun internalExecute(command: GetUserDetail, principal: Principal): UserDetail { + val userDetail = userDetailRepository.findById(UserDetailId(command.id)) + ?: throw IllegalArgumentException("User ${command.id} does not exist") + val actor = actorRepository.findById(userDetail.actorId) + ?: throw InternalServerException("Actor ${userDetail.actorId} not found") + + val emojis = customEmojiRepository.findByIds(actor.emojis.map { it.emojiId }) + + return UserDetail.of(actor, userDetail, emojis) + } + + companion object { + val logger = LoggerFactory.getLogger(GetUserDetailApplicationService::class.java) + } +} diff --git a/hideout-core/src/main/kotlin/dev/usbharu/hideout/core/application/actor/MigrationLocalActor.kt b/hideout-core/src/main/kotlin/dev/usbharu/hideout/core/application/actor/MigrationLocalActor.kt new file mode 100644 index 00000000..71f7fe96 --- /dev/null +++ b/hideout-core/src/main/kotlin/dev/usbharu/hideout/core/application/actor/MigrationLocalActor.kt @@ -0,0 +1,3 @@ +package dev.usbharu.hideout.core.application.actor + +data class MigrationLocalActor(val from: Long, val to: Long) diff --git a/hideout-core/src/main/kotlin/dev/usbharu/hideout/core/application/actor/MigrationLocalActorApplicationService.kt b/hideout-core/src/main/kotlin/dev/usbharu/hideout/core/application/actor/MigrationLocalActorApplicationService.kt new file mode 100644 index 00000000..9e0a01e6 --- /dev/null +++ b/hideout-core/src/main/kotlin/dev/usbharu/hideout/core/application/actor/MigrationLocalActorApplicationService.kt @@ -0,0 +1,74 @@ +/* + * Copyright (C) 2024 usbharu + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package dev.usbharu.hideout.core.application.actor + +import dev.usbharu.hideout.core.application.exception.InternalServerException +import dev.usbharu.hideout.core.application.exception.PermissionDeniedException +import dev.usbharu.hideout.core.application.shared.LocalUserAbstractApplicationService +import dev.usbharu.hideout.core.application.shared.Transaction +import dev.usbharu.hideout.core.domain.model.actor.ActorId +import dev.usbharu.hideout.core.domain.model.actor.ActorRepository +import dev.usbharu.hideout.core.domain.model.support.principal.FromApi +import dev.usbharu.hideout.core.domain.model.userdetails.UserDetailRepository +import dev.usbharu.hideout.core.domain.service.actor.local.AccountMigrationCheck.* +import dev.usbharu.hideout.core.domain.service.actor.local.LocalActorMigrationCheckDomainService +import org.slf4j.LoggerFactory +import org.springframework.stereotype.Service + +@Service +class MigrationLocalActorApplicationService( + private val actorRepository: ActorRepository, + private val localActorMigrationCheckDomainService: LocalActorMigrationCheckDomainService, + transaction: Transaction, + private val userDetailRepository: UserDetailRepository, +) : LocalUserAbstractApplicationService(transaction, logger) { + + override suspend fun internalExecute(command: MigrationLocalActor, principal: FromApi) { + if (command.from != principal.actorId.id) { + throw PermissionDeniedException() + } + + val userDetail = userDetailRepository.findById(principal.userDetailId) + ?: throw InternalServerException("User detail ${principal.userDetailId} not found.") + + val fromActorId = ActorId(command.from) + val toActorId = ActorId(command.to) + + val fromActor = + actorRepository.findById(fromActorId) ?: throw IllegalArgumentException("Actor ${command.from} not found.") + val toActor = + actorRepository.findById(toActorId) ?: throw IllegalArgumentException("Actor ${command.to} not found.") + + val canAccountMigration = + localActorMigrationCheckDomainService.canAccountMigration(userDetail, fromActor, toActor) + if (canAccountMigration.canMigration) { + fromActor.moveTo = toActorId + actorRepository.save(fromActor) + } else when (canAccountMigration) { + is AlreadyMoved -> throw IllegalArgumentException(canAccountMigration.message) + is CanAccountMigration -> throw InternalServerException() + is CircularReferences -> throw IllegalArgumentException(canAccountMigration.message) + is SelfReferences -> throw IllegalArgumentException("Self references are not supported") + is AlsoKnownAsNotFound -> throw IllegalArgumentException(canAccountMigration.message) + is MigrationCoolDown -> throw IllegalArgumentException(canAccountMigration.message) + } + } + + companion object { + private val logger = LoggerFactory.getLogger(MigrationLocalActorApplicationService::class.java) + } +} diff --git a/hideout-core/src/main/kotlin/dev/usbharu/hideout/core/application/actor/RegisterLocalActor.kt b/hideout-core/src/main/kotlin/dev/usbharu/hideout/core/application/actor/RegisterLocalActor.kt new file mode 100644 index 00000000..b54f2504 --- /dev/null +++ b/hideout-core/src/main/kotlin/dev/usbharu/hideout/core/application/actor/RegisterLocalActor.kt @@ -0,0 +1,22 @@ +/* + * Copyright (C) 2024 usbharu + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package dev.usbharu.hideout.core.application.actor + +data class RegisterLocalActor( + val name: String, + val password: String, +) diff --git a/hideout-core/src/main/kotlin/dev/usbharu/hideout/core/application/actor/RegisterLocalActorApplicationService.kt b/hideout-core/src/main/kotlin/dev/usbharu/hideout/core/application/actor/RegisterLocalActorApplicationService.kt new file mode 100644 index 00000000..27adac58 --- /dev/null +++ b/hideout-core/src/main/kotlin/dev/usbharu/hideout/core/application/actor/RegisterLocalActorApplicationService.kt @@ -0,0 +1,78 @@ +/* + * Copyright (C) 2024 usbharu + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package dev.usbharu.hideout.core.application.actor + +import dev.usbharu.hideout.core.application.exception.InternalServerException +import dev.usbharu.hideout.core.application.shared.AbstractApplicationService +import dev.usbharu.hideout.core.application.shared.Transaction +import dev.usbharu.hideout.core.config.ApplicationConfig +import dev.usbharu.hideout.core.domain.model.actor.ActorRepository +import dev.usbharu.hideout.core.domain.model.instance.InstanceRepository +import dev.usbharu.hideout.core.domain.model.support.principal.Principal +import dev.usbharu.hideout.core.domain.model.userdetails.UserDetail +import dev.usbharu.hideout.core.domain.model.userdetails.UserDetailId +import dev.usbharu.hideout.core.domain.model.userdetails.UserDetailRepository +import dev.usbharu.hideout.core.domain.service.actor.local.LocalActorDomainService +import dev.usbharu.hideout.core.domain.service.userdetail.UserDetailDomainService +import dev.usbharu.hideout.core.domain.shared.id.IdGenerateService +import dev.usbharu.hideout.core.infrastructure.factory.ActorFactoryImpl +import org.slf4j.LoggerFactory +import org.springframework.stereotype.Service +import java.net.URI + +@Service +class RegisterLocalActorApplicationService( + transaction: Transaction, + private val actorDomainService: LocalActorDomainService, + private val actorRepository: ActorRepository, + private val actorFactoryImpl: ActorFactoryImpl, + private val instanceRepository: InstanceRepository, + private val applicationConfig: ApplicationConfig, + private val userDetailDomainService: UserDetailDomainService, + private val userDetailRepository: UserDetailRepository, + private val idGenerateService: IdGenerateService, +) : AbstractApplicationService(transaction, Companion.logger) { + + override suspend fun internalExecute(command: RegisterLocalActor, principal: Principal): URI { + if (actorDomainService.usernameAlreadyUse(command.name)) { + throw IllegalArgumentException("Username already exists") + } + val instance = instanceRepository.findByUrl(applicationConfig.url.toURI()) + ?: throw InternalServerException("Local instance not found.") + + val actor = actorFactoryImpl.createLocal( + command.name, + actorDomainService.generateKeyPair(), + instance.id + ) + actorRepository.save(actor) + val userDetail = UserDetail.create( + id = UserDetailId(idGenerateService.generateId()), + actorId = actor.id, + password = userDetailDomainService.hashPassword(command.password), + autoAcceptFolloweeFollowRequest = false, + lastMigration = null, + homeTimelineId = null + ) + userDetailRepository.save(userDetail) + return actor.url + } + + companion object { + private val logger = LoggerFactory.getLogger(RegisterLocalActorApplicationService::class.java) + } +} diff --git a/hideout-core/src/main/kotlin/dev/usbharu/hideout/core/application/actor/SetAlsoKnownAsLocalActorApplicationService.kt b/hideout-core/src/main/kotlin/dev/usbharu/hideout/core/application/actor/SetAlsoKnownAsLocalActorApplicationService.kt new file mode 100644 index 00000000..3f676614 --- /dev/null +++ b/hideout-core/src/main/kotlin/dev/usbharu/hideout/core/application/actor/SetAlsoKnownAsLocalActorApplicationService.kt @@ -0,0 +1,8 @@ +package dev.usbharu.hideout.core.application.actor + +import org.springframework.stereotype.Service + +@Service +interface SetAlsoKnownAsLocalActorApplicationService { + suspend fun setAlsoKnownAs(actorId: Long, alsoKnownAs: List) +} diff --git a/hideout-core/src/main/kotlin/dev/usbharu/hideout/core/application/actor/StartDeleteLocalActorApplicationService.kt b/hideout-core/src/main/kotlin/dev/usbharu/hideout/core/application/actor/StartDeleteLocalActorApplicationService.kt new file mode 100644 index 00000000..9fd8669b --- /dev/null +++ b/hideout-core/src/main/kotlin/dev/usbharu/hideout/core/application/actor/StartDeleteLocalActorApplicationService.kt @@ -0,0 +1,46 @@ +/* + * Copyright (C) 2024 usbharu + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package dev.usbharu.hideout.core.application.actor + +import dev.usbharu.hideout.core.application.exception.InternalServerException +import dev.usbharu.hideout.core.application.exception.PermissionDeniedException +import dev.usbharu.hideout.core.application.shared.LocalUserAbstractApplicationService +import dev.usbharu.hideout.core.application.shared.Transaction +import dev.usbharu.hideout.core.domain.model.actor.ActorRepository +import dev.usbharu.hideout.core.domain.model.support.principal.FromApi +import org.slf4j.LoggerFactory +import org.springframework.stereotype.Service + +@Service +class StartDeleteLocalActorApplicationService( + transaction: Transaction, + private val actorRepository: ActorRepository, +) : LocalUserAbstractApplicationService(transaction, logger) { + override suspend fun internalExecute(command: DeleteLocalActor, principal: FromApi) { + if (command.actorId != principal.actorId) { + throw PermissionDeniedException() + } + val findById = actorRepository.findById(command.actorId) + ?: throw InternalServerException("Actor ${command.actorId} Not found") + findById.delete() + actorRepository.save(findById) + } + + companion object { + private val logger = LoggerFactory.getLogger(StartDeleteLocalActorApplicationService::class.java) + } +} diff --git a/hideout-core/src/main/kotlin/dev/usbharu/hideout/core/application/actor/SuspendLocalActorApplicationService.kt b/hideout-core/src/main/kotlin/dev/usbharu/hideout/core/application/actor/SuspendLocalActorApplicationService.kt new file mode 100644 index 00000000..809b3a36 --- /dev/null +++ b/hideout-core/src/main/kotlin/dev/usbharu/hideout/core/application/actor/SuspendLocalActorApplicationService.kt @@ -0,0 +1,37 @@ +/* + * Copyright (C) 2024 usbharu + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package dev.usbharu.hideout.core.application.actor + +import dev.usbharu.hideout.core.application.shared.Transaction +import dev.usbharu.hideout.core.domain.model.actor.ActorId +import dev.usbharu.hideout.core.domain.model.actor.ActorRepository +import org.springframework.stereotype.Service + +@Service +class SuspendLocalActorApplicationService( + private val transaction: Transaction, + private val actorRepository: ActorRepository, +) { + suspend fun suspend(actorId: Long, executor: ActorId) { + transaction.transaction { + val id = ActorId(actorId) + + val actor = actorRepository.findById(id)!! + actor.suspend = true + } + } +} diff --git a/hideout-core/src/main/kotlin/dev/usbharu/hideout/core/application/actor/UnsuspendLocalActorApplicationService.kt b/hideout-core/src/main/kotlin/dev/usbharu/hideout/core/application/actor/UnsuspendLocalActorApplicationService.kt new file mode 100644 index 00000000..baf0ab4b --- /dev/null +++ b/hideout-core/src/main/kotlin/dev/usbharu/hideout/core/application/actor/UnsuspendLocalActorApplicationService.kt @@ -0,0 +1,36 @@ +/* + * Copyright (C) 2024 usbharu + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package dev.usbharu.hideout.core.application.actor + +import dev.usbharu.hideout.core.application.shared.Transaction +import dev.usbharu.hideout.core.domain.model.actor.ActorId +import dev.usbharu.hideout.core.domain.model.actor.ActorRepository +import org.springframework.stereotype.Service + +@Service +class UnsuspendLocalActorApplicationService( + private val transaction: Transaction, + private val actorRepository: ActorRepository, +) { + suspend fun unsuspend(actorId: Long, executor: Long) { + transaction.transaction { + val findById = actorRepository.findById(ActorId(actorId))!! + + findById.suspend = false + } + } +} diff --git a/hideout-core/src/main/kotlin/dev/usbharu/hideout/core/application/actor/UserDetail.kt b/hideout-core/src/main/kotlin/dev/usbharu/hideout/core/application/actor/UserDetail.kt new file mode 100644 index 00000000..5b363296 --- /dev/null +++ b/hideout-core/src/main/kotlin/dev/usbharu/hideout/core/application/actor/UserDetail.kt @@ -0,0 +1,70 @@ +/* + * Copyright (C) 2024 usbharu + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package dev.usbharu.hideout.core.application.actor + +import dev.usbharu.hideout.core.domain.model.actor.Actor +import dev.usbharu.hideout.core.domain.model.emoji.CustomEmoji +import dev.usbharu.hideout.core.domain.model.userdetails.UserDetail +import java.time.Instant + +data class UserDetail( + val id: Long, + val userDetailId: Long, + val name: String, + val domain: String, + val screenName: String, + val url: String, + val iconUrl: String, + val description: String, + val locked: Boolean, + val emojis: List, + val createdAt: Instant, + val lastPostAt: Instant?, + val postsCount: Int, + val followingCount: Int?, + val followersCount: Int?, + val moveTo: Long?, + val suspend: Boolean, +) { + companion object { + fun of( + actor: Actor, + userDetail: UserDetail, + customEmojis: List, + ): dev.usbharu.hideout.core.application.actor.UserDetail { + return UserDetail( + id = actor.id.id, + userDetailId = userDetail.id.id, + name = actor.name.name, + domain = actor.domain.domain, + screenName = actor.screenName.screenName, + url = actor.url.toString(), + iconUrl = actor.url.toString(), + description = actor.description.description, + locked = actor.locked, + emojis = customEmojis, + createdAt = actor.createdAt, + lastPostAt = actor.lastPostAt, + postsCount = actor.postsCount.postsCount, + followingCount = actor.followingCount?.relationshipCount, + followersCount = actor.followersCount?.relationshipCount, + moveTo = actor.moveTo?.id, + suspend = actor.suspend + ) + } + } +} diff --git a/hideout-core/src/main/kotlin/dev/usbharu/hideout/core/application/application/RegisterApplication.kt b/hideout-core/src/main/kotlin/dev/usbharu/hideout/core/application/application/RegisterApplication.kt new file mode 100644 index 00000000..f39b957f --- /dev/null +++ b/hideout-core/src/main/kotlin/dev/usbharu/hideout/core/application/application/RegisterApplication.kt @@ -0,0 +1,26 @@ +/* + * Copyright (C) 2024 usbharu + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package dev.usbharu.hideout.core.application.application + +import java.net.URI + +data class RegisterApplication( + val name: String, + val redirectUris: Set, + val useRefreshToken: Boolean, + val scopes: Set, +) diff --git a/hideout-core/src/main/kotlin/dev/usbharu/hideout/core/application/application/RegisterApplicationApplicationService.kt b/hideout-core/src/main/kotlin/dev/usbharu/hideout/core/application/application/RegisterApplicationApplicationService.kt new file mode 100644 index 00000000..10890e85 --- /dev/null +++ b/hideout-core/src/main/kotlin/dev/usbharu/hideout/core/application/application/RegisterApplicationApplicationService.kt @@ -0,0 +1,98 @@ +/* + * Copyright (C) 2024 usbharu + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package dev.usbharu.hideout.core.application.application + +import dev.usbharu.hideout.core.application.shared.AbstractApplicationService +import dev.usbharu.hideout.core.application.shared.Transaction +import dev.usbharu.hideout.core.domain.model.application.Application +import dev.usbharu.hideout.core.domain.model.application.ApplicationId +import dev.usbharu.hideout.core.domain.model.application.ApplicationName +import dev.usbharu.hideout.core.domain.model.application.ApplicationRepository +import dev.usbharu.hideout.core.domain.model.support.principal.Principal +import dev.usbharu.hideout.core.domain.service.userdetail.PasswordEncoder +import dev.usbharu.hideout.core.domain.shared.id.IdGenerateService +import dev.usbharu.hideout.core.infrastructure.springframework.oauth2.SecureTokenGenerator +import org.slf4j.LoggerFactory +import org.springframework.security.oauth2.core.AuthorizationGrantType +import org.springframework.security.oauth2.core.ClientAuthenticationMethod +import org.springframework.security.oauth2.server.authorization.client.RegisteredClient +import org.springframework.security.oauth2.server.authorization.client.RegisteredClientRepository +import org.springframework.security.oauth2.server.authorization.settings.ClientSettings +import org.springframework.security.oauth2.server.authorization.settings.TokenSettings +import org.springframework.stereotype.Service +import java.time.Duration + +@Service +class RegisterApplicationApplicationService( + private val idGenerateService: IdGenerateService, + private val passwordEncoder: PasswordEncoder, + private val secureTokenGenerator: SecureTokenGenerator, + private val registeredClientRepository: RegisteredClientRepository, + transaction: Transaction, + private val applicationRepository: ApplicationRepository, +) : AbstractApplicationService(transaction, logger) { + + override suspend fun internalExecute(command: RegisterApplication, principal: Principal): RegisteredApplication { + val id = idGenerateService.generateId() + val clientSecret = secureTokenGenerator.generate() + val registeredClient = RegisteredClient + .withId(id.toString()) + .clientId(id.toString()) + .clientSecret(passwordEncoder.encode(clientSecret)) + .clientName(command.name) + .clientAuthenticationMethod(ClientAuthenticationMethod.CLIENT_SECRET_POST) + .clientAuthenticationMethod(ClientAuthenticationMethod.CLIENT_SECRET_JWT) + .clientAuthenticationMethod(ClientAuthenticationMethod.CLIENT_SECRET_BASIC) + .authorizationGrantType(AuthorizationGrantType.AUTHORIZATION_CODE) + .authorizationGrantType(AuthorizationGrantType.CLIENT_CREDENTIALS) + .apply { + if (command.useRefreshToken) { + authorizationGrantType(AuthorizationGrantType.REFRESH_TOKEN) + } else { + tokenSettings( + TokenSettings + .builder() + .accessTokenTimeToLive(Duration.ofSeconds(31536000000)) + .build() + ) + } + } + .redirectUris { set -> + set.addAll(command.redirectUris.map { it.toString() }) + } + .clientSettings(ClientSettings.builder().requireAuthorizationConsent(true).build()) + .scopes { it.addAll(command.scopes) } + .build() + registeredClientRepository.save(registeredClient) + + val application = Application(ApplicationId(id), ApplicationName(command.name)) + + applicationRepository.save(application) + return RegisteredApplication( + id = id, + name = command.name, + clientSecret = clientSecret, + clientId = id.toString(), + redirectUris = command.redirectUris + ) + } + + + companion object { + private val logger = LoggerFactory.getLogger(RegisterApplicationApplicationService::class.java) + } +} diff --git a/hideout-core/src/main/kotlin/dev/usbharu/hideout/core/application/application/RegisteredApplication.kt b/hideout-core/src/main/kotlin/dev/usbharu/hideout/core/application/application/RegisteredApplication.kt new file mode 100644 index 00000000..a5a18032 --- /dev/null +++ b/hideout-core/src/main/kotlin/dev/usbharu/hideout/core/application/application/RegisteredApplication.kt @@ -0,0 +1,27 @@ +/* + * Copyright (C) 2024 usbharu + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package dev.usbharu.hideout.core.application.application + +import java.net.URI + +data class RegisteredApplication( + val id: Long, + val name: String, + val redirectUris: Set, + val clientSecret: String, + val clientId: String, +) diff --git a/hideout-core/src/main/kotlin/dev/usbharu/hideout/core/application/domainevent/subscribers/DomainEventSubscriber.kt b/hideout-core/src/main/kotlin/dev/usbharu/hideout/core/application/domainevent/subscribers/DomainEventSubscriber.kt new file mode 100644 index 00000000..364bf92a --- /dev/null +++ b/hideout-core/src/main/kotlin/dev/usbharu/hideout/core/application/domainevent/subscribers/DomainEventSubscriber.kt @@ -0,0 +1,10 @@ +package dev.usbharu.hideout.core.application.domainevent.subscribers + +import dev.usbharu.hideout.core.domain.shared.domainevent.DomainEvent +import dev.usbharu.hideout.core.domain.shared.domainevent.DomainEventBody + +interface DomainEventSubscriber { + fun subscribe(eventName: String, domainEventConsumer: DomainEventConsumer) +} + +typealias DomainEventConsumer = suspend (DomainEvent) -> Unit diff --git a/hideout-core/src/main/kotlin/dev/usbharu/hideout/core/application/domainevent/subscribers/Subscriber.kt b/hideout-core/src/main/kotlin/dev/usbharu/hideout/core/application/domainevent/subscribers/Subscriber.kt new file mode 100644 index 00000000..a538b87a --- /dev/null +++ b/hideout-core/src/main/kotlin/dev/usbharu/hideout/core/application/domainevent/subscribers/Subscriber.kt @@ -0,0 +1,3 @@ +package dev.usbharu.hideout.core.application.domainevent.subscribers + +interface Subscriber diff --git a/hideout-core/src/main/kotlin/dev/usbharu/hideout/core/application/domainevent/subscribers/SubscriberRunner.kt b/hideout-core/src/main/kotlin/dev/usbharu/hideout/core/application/domainevent/subscribers/SubscriberRunner.kt new file mode 100644 index 00000000..b932b8dc --- /dev/null +++ b/hideout-core/src/main/kotlin/dev/usbharu/hideout/core/application/domainevent/subscribers/SubscriberRunner.kt @@ -0,0 +1,11 @@ +package dev.usbharu.hideout.core.application.domainevent.subscribers + +import org.springframework.boot.ApplicationArguments +import org.springframework.boot.ApplicationRunner +import org.springframework.stereotype.Component + +@Component +class SubscriberRunner(subscribers: List) : ApplicationRunner { + override fun run(args: ApplicationArguments?) { + } +} diff --git a/hideout-core/src/main/kotlin/dev/usbharu/hideout/core/application/domainevent/subscribers/TimelinePostCreateSubscriber.kt b/hideout-core/src/main/kotlin/dev/usbharu/hideout/core/application/domainevent/subscribers/TimelinePostCreateSubscriber.kt new file mode 100644 index 00000000..28b0004a --- /dev/null +++ b/hideout-core/src/main/kotlin/dev/usbharu/hideout/core/application/domainevent/subscribers/TimelinePostCreateSubscriber.kt @@ -0,0 +1,22 @@ +package dev.usbharu.hideout.core.application.domainevent.subscribers + +import dev.usbharu.hideout.core.domain.event.post.PostEvent +import dev.usbharu.hideout.core.domain.event.post.PostEventBody +import org.slf4j.LoggerFactory +import org.springframework.stereotype.Component + +@Component +class TimelinePostCreateSubscriber(domainEventSubscriber: DomainEventSubscriber) : Subscriber { + init { + domainEventSubscriber.subscribe(PostEvent.CREATE.eventName) { + val post = it.body.getPost() + val actor = it.body.getActor() + + logger.info("New Post! : {}", post) + } + } + + companion object { + private val logger = LoggerFactory.getLogger(TimelinePostCreateSubscriber::class.java) + } +} diff --git a/hideout-core/src/main/kotlin/dev/usbharu/hideout/core/application/domainevent/subscribers/TimelineRelationshipFollowSubscriber.kt b/hideout-core/src/main/kotlin/dev/usbharu/hideout/core/application/domainevent/subscribers/TimelineRelationshipFollowSubscriber.kt new file mode 100644 index 00000000..94aff5f3 --- /dev/null +++ b/hideout-core/src/main/kotlin/dev/usbharu/hideout/core/application/domainevent/subscribers/TimelineRelationshipFollowSubscriber.kt @@ -0,0 +1,50 @@ +package dev.usbharu.hideout.core.application.domainevent.subscribers + +import dev.usbharu.hideout.core.application.timeline.AddTimelineRelationship +import dev.usbharu.hideout.core.application.timeline.UserAddTimelineRelationshipApplicationService +import dev.usbharu.hideout.core.domain.event.relationship.RelationshipEvent +import dev.usbharu.hideout.core.domain.event.relationship.RelationshipEventBody +import dev.usbharu.hideout.core.domain.model.timelinerelationship.TimelineRelationship +import dev.usbharu.hideout.core.domain.model.timelinerelationship.TimelineRelationshipId +import dev.usbharu.hideout.core.domain.model.timelinerelationship.Visible +import dev.usbharu.hideout.core.domain.model.userdetails.UserDetailRepository +import dev.usbharu.hideout.core.domain.shared.id.IdGenerateService +import org.slf4j.LoggerFactory +import org.springframework.stereotype.Component + +@Component +class TimelineRelationshipFollowSubscriber( + private val userAddTimelineRelationshipApplicationService: UserAddTimelineRelationshipApplicationService, + private val idGenerateService: IdGenerateService, + private val userDetailRepository: UserDetailRepository, + domainEventSubscriber: DomainEventSubscriber +) : Subscriber { + + init { + domainEventSubscriber.subscribe(RelationshipEvent.FOLLOW.eventName) { + val relationship = it.body.getRelationship() + val userDetail = userDetailRepository.findByActorId(relationship.actorId.id) ?: throw Exception() + if (userDetail.homeTimelineId == null) { + logger.warn("Home timeline for ${relationship.actorId} is not found") + return@subscribe + } + userAddTimelineRelationshipApplicationService.execute( + AddTimelineRelationship( + TimelineRelationship( + TimelineRelationshipId(idGenerateService.generateId()), + userDetail.homeTimelineId, + relationship.targetActorId, + Visible.FOLLOWERS + ) + ), it.body.principal + ) + + + } + } + + companion object { + private val logger = LoggerFactory.getLogger(TimelineRelationshipFollowSubscriber::class.java) + } + +} \ No newline at end of file diff --git a/hideout-core/src/main/kotlin/dev/usbharu/hideout/core/application/exception/InternalServerException.kt b/hideout-core/src/main/kotlin/dev/usbharu/hideout/core/application/exception/InternalServerException.kt new file mode 100644 index 00000000..8acf6a72 --- /dev/null +++ b/hideout-core/src/main/kotlin/dev/usbharu/hideout/core/application/exception/InternalServerException.kt @@ -0,0 +1,14 @@ +package dev.usbharu.hideout.core.application.exception + +class InternalServerException : 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 + ) +} \ No newline at end of file diff --git a/hideout-core/src/main/kotlin/dev/usbharu/hideout/core/application/exception/PermissionDeniedException.kt b/hideout-core/src/main/kotlin/dev/usbharu/hideout/core/application/exception/PermissionDeniedException.kt new file mode 100644 index 00000000..c583ae0b --- /dev/null +++ b/hideout-core/src/main/kotlin/dev/usbharu/hideout/core/application/exception/PermissionDeniedException.kt @@ -0,0 +1,14 @@ +package dev.usbharu.hideout.core.application.exception + +class PermissionDeniedException : 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 + ) +} \ No newline at end of file diff --git a/hideout-core/src/main/kotlin/dev/usbharu/hideout/core/application/filter/DeleteFilter.kt b/hideout-core/src/main/kotlin/dev/usbharu/hideout/core/application/filter/DeleteFilter.kt new file mode 100644 index 00000000..52a2bf0a --- /dev/null +++ b/hideout-core/src/main/kotlin/dev/usbharu/hideout/core/application/filter/DeleteFilter.kt @@ -0,0 +1,19 @@ +/* + * Copyright (C) 2024 usbharu + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package dev.usbharu.hideout.core.application.filter + +data class DeleteFilter(val filterId: Long) diff --git a/hideout-core/src/main/kotlin/dev/usbharu/hideout/core/application/filter/Filter.kt b/hideout-core/src/main/kotlin/dev/usbharu/hideout/core/application/filter/Filter.kt new file mode 100644 index 00000000..e482991a --- /dev/null +++ b/hideout-core/src/main/kotlin/dev/usbharu/hideout/core/application/filter/Filter.kt @@ -0,0 +1,49 @@ +/* + * Copyright (C) 2024 usbharu + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package dev.usbharu.hideout.core.application.filter + +import dev.usbharu.hideout.core.domain.model.filter.Filter +import dev.usbharu.hideout.core.domain.model.filter.FilterAction +import dev.usbharu.hideout.core.domain.model.filter.FilterContext + +data class Filter( + val filterId: Long, + val userDetailId: Long, + val name: String, + val filterContext: Set, + val filterAction: FilterAction, + val filterKeywords: Set, +) { + companion object { + fun of(filter: Filter): dev.usbharu.hideout.core.application.filter.Filter { + return Filter( + filterId = filter.id.id, + userDetailId = filter.userDetailId.id, + name = filter.name.name, + filterContext = filter.filterContext, + filterAction = filter.filterAction, + filterKeywords = filter.filterKeywords.map { + FilterKeyword( + it.id.id, + it.keyword.keyword, + it.mode + ) + }.toSet() + ) + } + } +} diff --git a/hideout-core/src/main/kotlin/dev/usbharu/hideout/core/application/filter/FilterKeyword.kt b/hideout-core/src/main/kotlin/dev/usbharu/hideout/core/application/filter/FilterKeyword.kt new file mode 100644 index 00000000..bb58f6e6 --- /dev/null +++ b/hideout-core/src/main/kotlin/dev/usbharu/hideout/core/application/filter/FilterKeyword.kt @@ -0,0 +1,25 @@ +/* + * Copyright (C) 2024 usbharu + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package dev.usbharu.hideout.core.application.filter + +import dev.usbharu.hideout.core.domain.model.filter.FilterMode + +data class FilterKeyword( + val id: Long, + val keyword: String, + val filterMode: FilterMode, +) diff --git a/hideout-core/src/main/kotlin/dev/usbharu/hideout/core/application/filter/GetFilter.kt b/hideout-core/src/main/kotlin/dev/usbharu/hideout/core/application/filter/GetFilter.kt new file mode 100644 index 00000000..b9089ab0 --- /dev/null +++ b/hideout-core/src/main/kotlin/dev/usbharu/hideout/core/application/filter/GetFilter.kt @@ -0,0 +1,19 @@ +/* + * Copyright (C) 2024 usbharu + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package dev.usbharu.hideout.core.application.filter + +data class GetFilter(val filterId: Long) diff --git a/hideout-core/src/main/kotlin/dev/usbharu/hideout/core/application/filter/RegisterFilter.kt b/hideout-core/src/main/kotlin/dev/usbharu/hideout/core/application/filter/RegisterFilter.kt new file mode 100644 index 00000000..3fd9a35f --- /dev/null +++ b/hideout-core/src/main/kotlin/dev/usbharu/hideout/core/application/filter/RegisterFilter.kt @@ -0,0 +1,27 @@ +/* + * Copyright (C) 2024 usbharu + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package dev.usbharu.hideout.core.application.filter + +import dev.usbharu.hideout.core.domain.model.filter.FilterAction +import dev.usbharu.hideout.core.domain.model.filter.FilterContext + +data class RegisterFilter( + val filterName: String, + val filterContext: Set, + val filterAction: FilterAction, + val filterKeywords: Set, +) diff --git a/hideout-core/src/main/kotlin/dev/usbharu/hideout/core/application/filter/RegisterFilterKeyword.kt b/hideout-core/src/main/kotlin/dev/usbharu/hideout/core/application/filter/RegisterFilterKeyword.kt new file mode 100644 index 00000000..b6e3ed31 --- /dev/null +++ b/hideout-core/src/main/kotlin/dev/usbharu/hideout/core/application/filter/RegisterFilterKeyword.kt @@ -0,0 +1,24 @@ +/* + * Copyright (C) 2024 usbharu + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package dev.usbharu.hideout.core.application.filter + +import dev.usbharu.hideout.core.domain.model.filter.FilterMode + +data class RegisterFilterKeyword( + val keyword: String, + val filterMode: FilterMode, +) diff --git a/hideout-core/src/main/kotlin/dev/usbharu/hideout/core/application/filter/UserDeleteFilterApplicationService.kt b/hideout-core/src/main/kotlin/dev/usbharu/hideout/core/application/filter/UserDeleteFilterApplicationService.kt new file mode 100644 index 00000000..9333ec83 --- /dev/null +++ b/hideout-core/src/main/kotlin/dev/usbharu/hideout/core/application/filter/UserDeleteFilterApplicationService.kt @@ -0,0 +1,46 @@ +/* + * Copyright (C) 2024 usbharu + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package dev.usbharu.hideout.core.application.filter + +import dev.usbharu.hideout.core.application.exception.PermissionDeniedException +import dev.usbharu.hideout.core.application.shared.LocalUserAbstractApplicationService +import dev.usbharu.hideout.core.application.shared.Transaction +import dev.usbharu.hideout.core.domain.model.filter.FilterId +import dev.usbharu.hideout.core.domain.model.filter.FilterRepository +import dev.usbharu.hideout.core.domain.model.support.principal.FromApi +import org.slf4j.LoggerFactory +import org.springframework.stereotype.Service + +@Service +class UserDeleteFilterApplicationService(private val filterRepository: FilterRepository, transaction: Transaction) : + LocalUserAbstractApplicationService( + transaction, + logger + ) { + override suspend fun internalExecute(command: DeleteFilter, principal: FromApi) { + val filter = + filterRepository.findByFilterId(FilterId(command.filterId)) ?: throw IllegalArgumentException("not found") + if (filter.userDetailId != principal.userDetailId) { + throw PermissionDeniedException() + } + filterRepository.delete(filter) + } + + companion object { + private val logger = LoggerFactory.getLogger(UserDeleteFilterApplicationService::class.java) + } +} diff --git a/hideout-core/src/main/kotlin/dev/usbharu/hideout/core/application/filter/UserGetFilterApplicationService.kt b/hideout-core/src/main/kotlin/dev/usbharu/hideout/core/application/filter/UserGetFilterApplicationService.kt new file mode 100644 index 00000000..cecb5a5d --- /dev/null +++ b/hideout-core/src/main/kotlin/dev/usbharu/hideout/core/application/filter/UserGetFilterApplicationService.kt @@ -0,0 +1,46 @@ +/* + * Copyright (C) 2024 usbharu + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package dev.usbharu.hideout.core.application.filter + +import dev.usbharu.hideout.core.application.exception.PermissionDeniedException +import dev.usbharu.hideout.core.application.shared.LocalUserAbstractApplicationService +import dev.usbharu.hideout.core.application.shared.Transaction +import dev.usbharu.hideout.core.domain.model.filter.FilterId +import dev.usbharu.hideout.core.domain.model.filter.FilterRepository +import dev.usbharu.hideout.core.domain.model.support.principal.FromApi +import org.slf4j.LoggerFactory +import org.springframework.stereotype.Service + +@Service +class UserGetFilterApplicationService(private val filterRepository: FilterRepository, transaction: Transaction) : + LocalUserAbstractApplicationService( + transaction, + logger + ) { + override suspend fun internalExecute(command: GetFilter, principal: FromApi): Filter { + val filter = + filterRepository.findByFilterId(FilterId(command.filterId)) ?: throw IllegalArgumentException("Not Found") + if (filter.userDetailId != principal.userDetailId) { + throw PermissionDeniedException() + } + return Filter.of(filter) + } + + companion object { + private val logger = LoggerFactory.getLogger(UserGetFilterApplicationService::class.java) + } +} diff --git a/hideout-core/src/main/kotlin/dev/usbharu/hideout/core/application/filter/UserRegisterFilterApplicationService.kt b/hideout-core/src/main/kotlin/dev/usbharu/hideout/core/application/filter/UserRegisterFilterApplicationService.kt new file mode 100644 index 00000000..bf12c041 --- /dev/null +++ b/hideout-core/src/main/kotlin/dev/usbharu/hideout/core/application/filter/UserRegisterFilterApplicationService.kt @@ -0,0 +1,64 @@ +/* + * Copyright (C) 2024 usbharu + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package dev.usbharu.hideout.core.application.filter + +import dev.usbharu.hideout.core.application.shared.LocalUserAbstractApplicationService +import dev.usbharu.hideout.core.application.shared.Transaction +import dev.usbharu.hideout.core.domain.model.filter.* +import dev.usbharu.hideout.core.domain.model.filter.FilterKeyword +import dev.usbharu.hideout.core.domain.model.support.principal.FromApi +import dev.usbharu.hideout.core.domain.shared.id.IdGenerateService +import org.slf4j.LoggerFactory +import org.springframework.stereotype.Service + +@Service +class UserRegisterFilterApplicationService( + private val idGenerateService: IdGenerateService, + private val filterRepository: FilterRepository, + transaction: Transaction, +) : + LocalUserAbstractApplicationService( + transaction, + logger + ) { + + override suspend fun internalExecute(command: RegisterFilter, principal: FromApi): Filter { + + val filter = dev.usbharu.hideout.core.domain.model.filter.Filter.create( + id = FilterId(idGenerateService.generateId()), + userDetailId = principal.userDetailId, + name = FilterName(command.filterName), + filterContext = command.filterContext, + filterAction = command.filterAction, + filterKeywords = command.filterKeywords + .map { + FilterKeyword( + FilterKeywordId(idGenerateService.generateId()), + FilterKeywordKeyword(it.keyword), + it.filterMode + ) + }.toSet() + ) + + filterRepository.save(filter) + return Filter.of(filter) + } + + companion object { + private val logger = LoggerFactory.getLogger(UserRegisterFilterApplicationService::class.java) + } +} diff --git a/hideout-core/src/main/kotlin/dev/usbharu/hideout/core/application/instance/InitLocalInstanceApplicationService.kt b/hideout-core/src/main/kotlin/dev/usbharu/hideout/core/application/instance/InitLocalInstanceApplicationService.kt new file mode 100644 index 00000000..77d2e41b --- /dev/null +++ b/hideout-core/src/main/kotlin/dev/usbharu/hideout/core/application/instance/InitLocalInstanceApplicationService.kt @@ -0,0 +1,59 @@ +/* + * Copyright (C) 2024 usbharu + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package dev.usbharu.hideout.core.application.instance + +import dev.usbharu.hideout.core.application.shared.Transaction +import dev.usbharu.hideout.core.config.ApplicationConfig +import dev.usbharu.hideout.core.domain.model.instance.* +import dev.usbharu.hideout.core.domain.shared.id.IdGenerateService +import org.springframework.boot.context.event.ApplicationReadyEvent +import org.springframework.boot.info.BuildProperties +import org.springframework.context.event.EventListener +import org.springframework.stereotype.Service +import java.time.Instant + +@Service +class InitLocalInstanceApplicationService( + private val applicationConfig: ApplicationConfig, + private val instanceRepository: InstanceRepository, + private val idGenerateService: IdGenerateService, + private val buildProperties: BuildProperties, + private val transaction: Transaction, +) { + @EventListener(ApplicationReadyEvent::class) + suspend fun init() = transaction.transaction { + val findByUrl = instanceRepository.findByUrl(applicationConfig.url.toURI()) + + if (findByUrl == null) { + val instance = Instance( + id = InstanceId(idGenerateService.generateId()), + name = InstanceName(applicationConfig.url.host), + description = InstanceDescription(""), + url = applicationConfig.url.toURI(), + iconUrl = applicationConfig.url.toURI(), + sharedInbox = null, + software = InstanceSoftware("hideout"), + version = InstanceVersion(buildProperties.version), + isBlocked = false, + isMuted = false, + moderationNote = InstanceModerationNote(""), + createdAt = Instant.now(), + ) + instanceRepository.save(instance) + } + } +} diff --git a/hideout-core/src/main/kotlin/dev/usbharu/hideout/core/application/media/Media.kt b/hideout-core/src/main/kotlin/dev/usbharu/hideout/core/application/media/Media.kt new file mode 100644 index 00000000..5ff3e4a8 --- /dev/null +++ b/hideout-core/src/main/kotlin/dev/usbharu/hideout/core/application/media/Media.kt @@ -0,0 +1,50 @@ +/* + * Copyright (C) 2024 usbharu + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package dev.usbharu.hideout.core.application.media + +import dev.usbharu.hideout.core.domain.model.media.FileType +import dev.usbharu.hideout.core.domain.model.media.Media +import dev.usbharu.hideout.core.domain.model.media.MimeType +import java.net.URI + +data class Media( + val id: Long, + val name: String, + val url: URI, + val thumbprintURI: URI?, + val remoteURL: URI?, + val type: FileType, + val mimeType: MimeType, + val blurHash: String?, + val description: String? +) { + companion object { + fun of(media: Media): dev.usbharu.hideout.core.application.media.Media { + return Media( + id = media.id.id, + name = media.name.name, + url = media.url, + thumbprintURI = media.thumbnailUrl, + remoteURL = media.remoteUrl, + type = media.type, + mimeType = media.mimeType, + blurHash = media.blurHash?.hash, + description = media.description?.description + ) + } + } +} diff --git a/hideout-core/src/main/kotlin/dev/usbharu/hideout/core/application/media/UploadMedia.kt b/hideout-core/src/main/kotlin/dev/usbharu/hideout/core/application/media/UploadMedia.kt new file mode 100644 index 00000000..4a8af8d3 --- /dev/null +++ b/hideout-core/src/main/kotlin/dev/usbharu/hideout/core/application/media/UploadMedia.kt @@ -0,0 +1,22 @@ +/* + * Copyright (C) 2024 usbharu + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package dev.usbharu.hideout.core.application.media + +import java.net.URI +import java.nio.file.Path + +data class UploadMedia(val path: Path, val name: String, val remoteUri: URI?, val description: String?) diff --git a/hideout-core/src/main/kotlin/dev/usbharu/hideout/core/application/media/UploadMediaApplicationService.kt b/hideout-core/src/main/kotlin/dev/usbharu/hideout/core/application/media/UploadMediaApplicationService.kt new file mode 100644 index 00000000..4773f60f --- /dev/null +++ b/hideout-core/src/main/kotlin/dev/usbharu/hideout/core/application/media/UploadMediaApplicationService.kt @@ -0,0 +1,72 @@ +/* + * Copyright (C) 2024 usbharu + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package dev.usbharu.hideout.core.application.media + +import dev.usbharu.hideout.core.application.shared.LocalUserAbstractApplicationService +import dev.usbharu.hideout.core.application.shared.Transaction +import dev.usbharu.hideout.core.domain.model.media.* +import dev.usbharu.hideout.core.domain.model.support.principal.FromApi +import dev.usbharu.hideout.core.domain.shared.id.IdGenerateService +import dev.usbharu.hideout.core.external.media.MediaProcessor +import dev.usbharu.hideout.core.external.mediastore.MediaStore +import org.slf4j.LoggerFactory +import org.springframework.beans.factory.annotation.Qualifier +import org.springframework.stereotype.Service +import dev.usbharu.hideout.core.domain.model.media.Media as MediaModel + +@Service +class UploadMediaApplicationService( + @Qualifier("delegate") private val mediaProcessor: MediaProcessor, + private val mediaStore: MediaStore, + private val mediaRepository: MediaRepository, + private val idGenerateService: IdGenerateService, + transaction: Transaction +) : LocalUserAbstractApplicationService( + transaction, + logger +) { + override suspend fun internalExecute(command: UploadMedia, principal: FromApi): Media { + val process = mediaProcessor.process(command.path, command.name, null) + val id = idGenerateService.generateId() + val thumbnailUri = if (process.thumbnailPath != null) { + mediaStore.upload(process.thumbnailPath, "thumbnail-$id.${process.mimeType.subtype}") + } else { + null + } + val uri = mediaStore.upload(process.path, "$id.${process.mimeType.subtype}") + + val media = MediaModel( + id = MediaId(id), + name = MediaName(command.name), + url = uri, + remoteUrl = command.remoteUri, + thumbnailUrl = thumbnailUri, + type = process.fileType, + mimeType = process.mimeType, + blurHash = process.blurHash?.let { MediaBlurHash(it) }, + description = command.description?.let { MediaDescription(it) } + ) + + mediaRepository.save(media) + + return Media.of(media) + } + + companion object { + private val logger = LoggerFactory.getLogger(UploadMediaApplicationService::class.java) + } +} diff --git a/hideout-core/src/main/kotlin/dev/usbharu/hideout/core/application/post/DeleteLocalPost.kt b/hideout-core/src/main/kotlin/dev/usbharu/hideout/core/application/post/DeleteLocalPost.kt new file mode 100644 index 00000000..83da8dd1 --- /dev/null +++ b/hideout-core/src/main/kotlin/dev/usbharu/hideout/core/application/post/DeleteLocalPost.kt @@ -0,0 +1,3 @@ +package dev.usbharu.hideout.core.application.post + +data class DeleteLocalPost(val postId: Long) diff --git a/hideout-core/src/main/kotlin/dev/usbharu/hideout/core/application/post/DeleteLocalPostApplicationService.kt b/hideout-core/src/main/kotlin/dev/usbharu/hideout/core/application/post/DeleteLocalPostApplicationService.kt new file mode 100644 index 00000000..511f9629 --- /dev/null +++ b/hideout-core/src/main/kotlin/dev/usbharu/hideout/core/application/post/DeleteLocalPostApplicationService.kt @@ -0,0 +1,48 @@ +/* + * Copyright (C) 2024 usbharu + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package dev.usbharu.hideout.core.application.post + +import dev.usbharu.hideout.core.application.exception.PermissionDeniedException +import dev.usbharu.hideout.core.application.shared.LocalUserAbstractApplicationService +import dev.usbharu.hideout.core.application.shared.Transaction +import dev.usbharu.hideout.core.domain.model.actor.ActorRepository +import dev.usbharu.hideout.core.domain.model.post.PostId +import dev.usbharu.hideout.core.domain.model.post.PostRepository +import dev.usbharu.hideout.core.domain.model.support.principal.FromApi +import org.slf4j.LoggerFactory +import org.springframework.stereotype.Service + +@Service +class DeleteLocalPostApplicationService( + private val postRepository: PostRepository, + private val actorRepository: ActorRepository, transaction: Transaction, +) : LocalUserAbstractApplicationService(transaction, logger) { + + override suspend fun internalExecute(command: DeleteLocalPost, principal: FromApi) { + val findById = postRepository.findById(PostId(command.postId))!! + if (findById.actorId != principal.actorId) { + throw PermissionDeniedException() + } + val actor = actorRepository.findById(principal.actorId)!! + findById.delete(actor) + postRepository.save(findById) + } + + companion object { + private val logger = LoggerFactory.getLogger(DeleteLocalPostApplicationService::class.java) + } +} diff --git a/hideout-core/src/main/kotlin/dev/usbharu/hideout/core/application/post/GetPost.kt b/hideout-core/src/main/kotlin/dev/usbharu/hideout/core/application/post/GetPost.kt new file mode 100644 index 00000000..90ef8560 --- /dev/null +++ b/hideout-core/src/main/kotlin/dev/usbharu/hideout/core/application/post/GetPost.kt @@ -0,0 +1,21 @@ +/* + * Copyright (C) 2024 usbharu + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package dev.usbharu.hideout.core.application.post + +data class GetPost( + val postId: Long, +) diff --git a/hideout-core/src/main/kotlin/dev/usbharu/hideout/core/application/post/GetPostApplicationService.kt b/hideout-core/src/main/kotlin/dev/usbharu/hideout/core/application/post/GetPostApplicationService.kt new file mode 100644 index 00000000..6e1f0006 --- /dev/null +++ b/hideout-core/src/main/kotlin/dev/usbharu/hideout/core/application/post/GetPostApplicationService.kt @@ -0,0 +1,48 @@ +/* + * Copyright (C) 2024 usbharu + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package dev.usbharu.hideout.core.application.post + +import dev.usbharu.hideout.core.application.exception.PermissionDeniedException +import dev.usbharu.hideout.core.application.shared.AbstractApplicationService +import dev.usbharu.hideout.core.application.shared.Transaction +import dev.usbharu.hideout.core.domain.model.post.PostId +import dev.usbharu.hideout.core.domain.model.post.PostRepository +import dev.usbharu.hideout.core.domain.model.support.principal.Principal +import dev.usbharu.hideout.core.domain.service.post.IPostReadAccessControl +import org.slf4j.LoggerFactory +import org.springframework.stereotype.Service + +@Service +class GetPostApplicationService( + private val postRepository: PostRepository, + private val iPostReadAccessControl: IPostReadAccessControl, + transaction: Transaction +) : + AbstractApplicationService(transaction, logger) { + + override suspend fun internalExecute(command: GetPost, principal: Principal): Post { + val post = postRepository.findById(PostId(command.postId)) ?: throw IllegalArgumentException("Post not found") + if (iPostReadAccessControl.isAllow(post, principal).not()) { + throw PermissionDeniedException() + } + return Post.of(post) + } + + companion object { + private val logger = LoggerFactory.getLogger(GetPostApplicationService::class.java) + } +} diff --git a/hideout-core/src/main/kotlin/dev/usbharu/hideout/core/application/post/Post.kt b/hideout-core/src/main/kotlin/dev/usbharu/hideout/core/application/post/Post.kt new file mode 100644 index 00000000..2ed11666 --- /dev/null +++ b/hideout-core/src/main/kotlin/dev/usbharu/hideout/core/application/post/Post.kt @@ -0,0 +1,58 @@ +/* + * Copyright (C) 2024 usbharu + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package dev.usbharu.hideout.core.application.post + +import dev.usbharu.hideout.core.domain.model.post.Post +import dev.usbharu.hideout.core.domain.model.post.Visibility +import java.net.URI +import java.time.Instant + +data class Post( + val id: Long, + val actorId: Long, + val overview: String?, + val text: String, + val content: String, + val createdAt: Instant, + val visibility: Visibility, + val url: URI, + val repostId: Long?, + val replyId: Long?, + val sensitive: Boolean, + val mediaIds: List, + val moveTo: Long?, +) { + companion object { + fun of(post: Post): dev.usbharu.hideout.core.application.post.Post { + return Post( + id = post.id.id, + actorId = post.actorId.id, + overview = post.overview?.overview, + text = post.text, + content = post.content.content, + createdAt = post.createdAt, + visibility = post.visibility, + url = post.url, + repostId = post.repostId?.id, + replyId = post.replyId?.id, + sensitive = post.sensitive, + mediaIds = post.mediaIds.map { it.id }, + moveTo = post.moveTo?.id + ) + } + } +} diff --git a/hideout-core/src/main/kotlin/dev/usbharu/hideout/core/application/post/RegisterLocalPost.kt b/hideout-core/src/main/kotlin/dev/usbharu/hideout/core/application/post/RegisterLocalPost.kt new file mode 100644 index 00000000..b5fd2f71 --- /dev/null +++ b/hideout-core/src/main/kotlin/dev/usbharu/hideout/core/application/post/RegisterLocalPost.kt @@ -0,0 +1,29 @@ +/* + * Copyright (C) 2024 usbharu + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package dev.usbharu.hideout.core.application.post + +import dev.usbharu.hideout.core.domain.model.post.Visibility + +data class RegisterLocalPost( + val content: String, + val overview: String?, + val visibility: Visibility, + val repostId: Long?, + val replyId: Long?, + val sensitive: Boolean, + val mediaIds: List, +) diff --git a/hideout-core/src/main/kotlin/dev/usbharu/hideout/core/application/post/RegisterLocalPostApplicationService.kt b/hideout-core/src/main/kotlin/dev/usbharu/hideout/core/application/post/RegisterLocalPostApplicationService.kt new file mode 100644 index 00000000..0cc237da --- /dev/null +++ b/hideout-core/src/main/kotlin/dev/usbharu/hideout/core/application/post/RegisterLocalPostApplicationService.kt @@ -0,0 +1,66 @@ +/* + * Copyright (C) 2024 usbharu + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package dev.usbharu.hideout.core.application.post + +import dev.usbharu.hideout.core.application.exception.InternalServerException +import dev.usbharu.hideout.core.application.shared.LocalUserAbstractApplicationService +import dev.usbharu.hideout.core.application.shared.Transaction +import dev.usbharu.hideout.core.domain.model.actor.ActorRepository +import dev.usbharu.hideout.core.domain.model.media.MediaId +import dev.usbharu.hideout.core.domain.model.post.PostId +import dev.usbharu.hideout.core.domain.model.post.PostOverview +import dev.usbharu.hideout.core.domain.model.post.PostRepository +import dev.usbharu.hideout.core.domain.model.support.principal.FromApi +import dev.usbharu.hideout.core.infrastructure.factory.PostFactoryImpl +import org.slf4j.Logger +import org.slf4j.LoggerFactory +import org.springframework.stereotype.Service + +@Service +class RegisterLocalPostApplicationService( + private val postFactory: PostFactoryImpl, + private val actorRepository: ActorRepository, + private val postRepository: PostRepository, + transaction: Transaction, +) : LocalUserAbstractApplicationService(transaction, Companion.logger) { + + override suspend fun internalExecute(command: RegisterLocalPost, principal: FromApi): Long { + val actorId = principal.actorId + + val actor = actorRepository.findById(actorId) ?: throw InternalServerException("Actor $actorId not found.") + + val post = postFactory.createLocal( + actor = actor, + actorName = actor.name, + overview = command.overview?.let { PostOverview(it) }, + content = command.content, + visibility = command.visibility, + repostId = command.repostId?.let { PostId(it) }, + replyId = command.replyId?.let { PostId(it) }, + sensitive = command.sensitive, + mediaIds = command.mediaIds.map { MediaId(it) }, + ) + + postRepository.save(post) + + return post.id.id + } + + companion object { + val logger: Logger = LoggerFactory.getLogger(RegisterLocalPostApplicationService::class.java) + } +} diff --git a/hideout-core/src/main/kotlin/dev/usbharu/hideout/core/application/post/UpdateLocalNote.kt b/hideout-core/src/main/kotlin/dev/usbharu/hideout/core/application/post/UpdateLocalNote.kt new file mode 100644 index 00000000..64ce9831 --- /dev/null +++ b/hideout-core/src/main/kotlin/dev/usbharu/hideout/core/application/post/UpdateLocalNote.kt @@ -0,0 +1,25 @@ +/* + * Copyright (C) 2024 usbharu + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package dev.usbharu.hideout.core.application.post + +data class UpdateLocalNote( + val postId: Long, + val overview: String?, + val content: String, + val sensitive: Boolean, + val mediaIds: List +) diff --git a/hideout-core/src/main/kotlin/dev/usbharu/hideout/core/application/post/UpdateLocalNoteApplicationService.kt b/hideout-core/src/main/kotlin/dev/usbharu/hideout/core/application/post/UpdateLocalNoteApplicationService.kt new file mode 100644 index 00000000..17bafb70 --- /dev/null +++ b/hideout-core/src/main/kotlin/dev/usbharu/hideout/core/application/post/UpdateLocalNoteApplicationService.kt @@ -0,0 +1,66 @@ +/* + * Copyright (C) 2024 usbharu + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package dev.usbharu.hideout.core.application.post + +import dev.usbharu.hideout.core.application.exception.InternalServerException +import dev.usbharu.hideout.core.application.exception.PermissionDeniedException +import dev.usbharu.hideout.core.application.shared.LocalUserAbstractApplicationService +import dev.usbharu.hideout.core.application.shared.Transaction +import dev.usbharu.hideout.core.domain.model.actor.ActorRepository +import dev.usbharu.hideout.core.domain.model.media.MediaId +import dev.usbharu.hideout.core.domain.model.post.PostId +import dev.usbharu.hideout.core.domain.model.post.PostOverview +import dev.usbharu.hideout.core.domain.model.post.PostRepository +import dev.usbharu.hideout.core.domain.model.support.principal.FromApi +import dev.usbharu.hideout.core.domain.model.userdetails.UserDetailRepository +import dev.usbharu.hideout.core.infrastructure.factory.PostContentFactoryImpl +import org.slf4j.LoggerFactory +import org.springframework.stereotype.Service + +@Service +class UpdateLocalNoteApplicationService( + transaction: Transaction, + private val postRepository: PostRepository, + private val postContentFactoryImpl: PostContentFactoryImpl, + private val userDetailRepository: UserDetailRepository, + private val actorRepository: ActorRepository, +) : LocalUserAbstractApplicationService(transaction, logger) { + + override suspend fun internalExecute(command: UpdateLocalNote, principal: FromApi) { + val post = postRepository.findById(PostId(command.postId)) + ?: throw IllegalArgumentException("Post ${command.postId} not found.") + if (post.actorId != principal.actorId) { + throw PermissionDeniedException() + } + + val userDetail = userDetailRepository.findById(principal.userDetailId) + ?: throw InternalServerException("User detail ${principal.userDetailId} not found.") + val actor = actorRepository.findById(userDetail.actorId) + ?: throw InternalServerException("Actor ${principal.actorId} not found.") + + post.setContent(postContentFactoryImpl.create(command.content), actor) + post.setOverview(command.overview?.let { PostOverview(it) }, actor) + post.addMediaIds(command.mediaIds.map { MediaId(it) }, actor) + post.setSensitive(command.sensitive, actor) + + postRepository.save(post) + } + + companion object { + private val logger = LoggerFactory.getLogger(UpdateLocalNoteApplicationService::class.java) + } +} diff --git a/hideout-core/src/main/kotlin/dev/usbharu/hideout/core/application/relationship/acceptfollowrequest/AcceptFollowRequest.kt b/hideout-core/src/main/kotlin/dev/usbharu/hideout/core/application/relationship/acceptfollowrequest/AcceptFollowRequest.kt new file mode 100644 index 00000000..6c0d9f20 --- /dev/null +++ b/hideout-core/src/main/kotlin/dev/usbharu/hideout/core/application/relationship/acceptfollowrequest/AcceptFollowRequest.kt @@ -0,0 +1,19 @@ +/* + * Copyright (C) 2024 usbharu + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package dev.usbharu.hideout.core.application.relationship.acceptfollowrequest + +data class AcceptFollowRequest(val sourceActorId: Long) diff --git a/hideout-core/src/main/kotlin/dev/usbharu/hideout/core/application/relationship/acceptfollowrequest/UserAcceptFollowRequestApplicationService.kt b/hideout-core/src/main/kotlin/dev/usbharu/hideout/core/application/relationship/acceptfollowrequest/UserAcceptFollowRequestApplicationService.kt new file mode 100644 index 00000000..1f17a0df --- /dev/null +++ b/hideout-core/src/main/kotlin/dev/usbharu/hideout/core/application/relationship/acceptfollowrequest/UserAcceptFollowRequestApplicationService.kt @@ -0,0 +1,55 @@ +/* + * Copyright (C) 2024 usbharu + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package dev.usbharu.hideout.core.application.relationship.acceptfollowrequest + +import dev.usbharu.hideout.core.application.exception.InternalServerException +import dev.usbharu.hideout.core.application.relationship.block.UserBlockApplicationService +import dev.usbharu.hideout.core.application.shared.LocalUserAbstractApplicationService +import dev.usbharu.hideout.core.application.shared.Transaction +import dev.usbharu.hideout.core.domain.model.actor.ActorId +import dev.usbharu.hideout.core.domain.model.actor.ActorRepository +import dev.usbharu.hideout.core.domain.model.relationship.RelationshipRepository +import dev.usbharu.hideout.core.domain.model.support.principal.FromApi +import org.slf4j.LoggerFactory +import org.springframework.stereotype.Service + +@Service +class UserAcceptFollowRequestApplicationService( + private val relationshipRepository: RelationshipRepository, + transaction: Transaction, + private val actorRepository: ActorRepository, +) : + LocalUserAbstractApplicationService(transaction, logger) { + override suspend fun internalExecute(command: AcceptFollowRequest, principal: FromApi) { + + val actor = actorRepository.findById(principal.actorId) + ?: throw InternalServerException("Actor ${principal.actorId} not found") + + val targetId = ActorId(command.sourceActorId) + + val relationship = relationshipRepository.findByActorIdAndTargetId(targetId, actor.id) + ?: throw InternalServerException("Follow request not found") + + relationship.acceptFollowRequest() + + relationshipRepository.save(relationship) + } + + companion object { + private val logger = LoggerFactory.getLogger(UserBlockApplicationService::class.java) + } +} diff --git a/hideout-core/src/main/kotlin/dev/usbharu/hideout/core/application/relationship/block/Block.kt b/hideout-core/src/main/kotlin/dev/usbharu/hideout/core/application/relationship/block/Block.kt new file mode 100644 index 00000000..7a095b92 --- /dev/null +++ b/hideout-core/src/main/kotlin/dev/usbharu/hideout/core/application/relationship/block/Block.kt @@ -0,0 +1,19 @@ +/* + * Copyright (C) 2024 usbharu + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package dev.usbharu.hideout.core.application.relationship.block + +data class Block(val targetActorId: Long) diff --git a/hideout-core/src/main/kotlin/dev/usbharu/hideout/core/application/relationship/block/UserBlockApplicationService.kt b/hideout-core/src/main/kotlin/dev/usbharu/hideout/core/application/relationship/block/UserBlockApplicationService.kt new file mode 100644 index 00000000..91a99c4f --- /dev/null +++ b/hideout-core/src/main/kotlin/dev/usbharu/hideout/core/application/relationship/block/UserBlockApplicationService.kt @@ -0,0 +1,66 @@ +/* + * Copyright (C) 2024 usbharu + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package dev.usbharu.hideout.core.application.relationship.block + +import dev.usbharu.hideout.core.application.shared.LocalUserAbstractApplicationService +import dev.usbharu.hideout.core.application.shared.Transaction +import dev.usbharu.hideout.core.domain.model.actor.ActorId +import dev.usbharu.hideout.core.domain.model.actor.ActorRepository +import dev.usbharu.hideout.core.domain.model.relationship.Relationship +import dev.usbharu.hideout.core.domain.model.relationship.RelationshipRepository +import dev.usbharu.hideout.core.domain.model.support.principal.FromApi +import dev.usbharu.hideout.core.domain.model.userdetails.UserDetailRepository +import dev.usbharu.hideout.core.domain.service.relationship.RelationshipDomainService +import org.slf4j.LoggerFactory +import org.springframework.stereotype.Service + +@Service +class UserBlockApplicationService( + private val relationshipRepository: RelationshipRepository, + transaction: Transaction, + private val actorRepository: ActorRepository, + private val userDetailRepository: UserDetailRepository, + private val relationshipDomainService: RelationshipDomainService, +) : + LocalUserAbstractApplicationService(transaction, logger) { + override suspend fun internalExecute(command: Block, principal: FromApi) { + + val userDetail = userDetailRepository.findById(principal.userDetailId)!! + val actor = actorRepository.findById(userDetail.actorId)!! + + val targetId = ActorId(command.targetActorId) + val relationship = relationshipRepository.findByActorIdAndTargetId(actor.id, targetId) ?: Relationship.default( + actor.id, + targetId + ) + + val inverseRelationship = + relationshipRepository.findByActorIdAndTargetId(targetId, actor.id) ?: Relationship.default( + targetId, + actor.id + ) + + relationshipDomainService.block(relationship, inverseRelationship) + + relationshipRepository.save(relationship) + relationshipRepository.save(inverseRelationship) + } + + companion object { + private val logger = LoggerFactory.getLogger(UserBlockApplicationService::class.java) + } +} diff --git a/hideout-core/src/main/kotlin/dev/usbharu/hideout/core/application/relationship/followrequest/FollowRequest.kt b/hideout-core/src/main/kotlin/dev/usbharu/hideout/core/application/relationship/followrequest/FollowRequest.kt new file mode 100644 index 00000000..3f8de0a7 --- /dev/null +++ b/hideout-core/src/main/kotlin/dev/usbharu/hideout/core/application/relationship/followrequest/FollowRequest.kt @@ -0,0 +1,19 @@ +/* + * Copyright (C) 2024 usbharu + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package dev.usbharu.hideout.core.application.relationship.followrequest + +data class FollowRequest(val targetActorId: Long) diff --git a/hideout-core/src/main/kotlin/dev/usbharu/hideout/core/application/relationship/followrequest/UserFollowRequestApplicationService.kt b/hideout-core/src/main/kotlin/dev/usbharu/hideout/core/application/relationship/followrequest/UserFollowRequestApplicationService.kt new file mode 100644 index 00000000..0c9d967d --- /dev/null +++ b/hideout-core/src/main/kotlin/dev/usbharu/hideout/core/application/relationship/followrequest/UserFollowRequestApplicationService.kt @@ -0,0 +1,60 @@ +/* + * Copyright (C) 2024 usbharu + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package dev.usbharu.hideout.core.application.relationship.followrequest + +import dev.usbharu.hideout.core.application.shared.LocalUserAbstractApplicationService +import dev.usbharu.hideout.core.application.shared.Transaction +import dev.usbharu.hideout.core.domain.model.actor.ActorId +import dev.usbharu.hideout.core.domain.model.actor.ActorRepository +import dev.usbharu.hideout.core.domain.model.relationship.Relationship +import dev.usbharu.hideout.core.domain.model.relationship.RelationshipRepository +import dev.usbharu.hideout.core.domain.model.support.principal.FromApi +import dev.usbharu.hideout.core.domain.model.userdetails.UserDetailRepository +import org.slf4j.LoggerFactory +import org.springframework.stereotype.Service + +@Service +class UserFollowRequestApplicationService( + private val relationshipRepository: RelationshipRepository, + transaction: Transaction, + private val actorRepository: ActorRepository, + private val userDetailRepository: UserDetailRepository, +) : LocalUserAbstractApplicationService( + transaction, + logger +) { + + override suspend fun internalExecute(command: FollowRequest, principal: FromApi) { + + val userDetail = userDetailRepository.findById(principal.userDetailId)!! + val actor = actorRepository.findById(userDetail.actorId)!! + + val targetId = ActorId(command.targetActorId) + val relationship = relationshipRepository.findByActorIdAndTargetId(actor.id, targetId) ?: Relationship.default( + actor.id, + targetId + ) + + relationship.followRequest() + + relationshipRepository.save(relationship) + } + + companion object { + private val logger = LoggerFactory.getLogger(UserFollowRequestApplicationService::class.java) + } +} diff --git a/hideout-core/src/main/kotlin/dev/usbharu/hideout/core/application/relationship/get/GetRelationship.kt b/hideout-core/src/main/kotlin/dev/usbharu/hideout/core/application/relationship/get/GetRelationship.kt new file mode 100644 index 00000000..90df1b82 --- /dev/null +++ b/hideout-core/src/main/kotlin/dev/usbharu/hideout/core/application/relationship/get/GetRelationship.kt @@ -0,0 +1,19 @@ +/* + * Copyright (C) 2024 usbharu + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package dev.usbharu.hideout.core.application.relationship.get + +data class GetRelationship(val targetActorId: Long) diff --git a/hideout-core/src/main/kotlin/dev/usbharu/hideout/core/application/relationship/get/GetRelationshipApplicationService.kt b/hideout-core/src/main/kotlin/dev/usbharu/hideout/core/application/relationship/get/GetRelationshipApplicationService.kt new file mode 100644 index 00000000..b832710d --- /dev/null +++ b/hideout-core/src/main/kotlin/dev/usbharu/hideout/core/application/relationship/get/GetRelationshipApplicationService.kt @@ -0,0 +1,71 @@ +/* + * Copyright (C) 2024 usbharu + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package dev.usbharu.hideout.core.application.relationship.get + +import dev.usbharu.hideout.core.application.shared.LocalUserAbstractApplicationService +import dev.usbharu.hideout.core.application.shared.Transaction +import dev.usbharu.hideout.core.domain.model.actor.ActorId +import dev.usbharu.hideout.core.domain.model.actor.ActorRepository +import dev.usbharu.hideout.core.domain.model.actorinstancerelationship.ActorInstanceRelationship +import dev.usbharu.hideout.core.domain.model.actorinstancerelationship.ActorInstanceRelationshipRepository +import dev.usbharu.hideout.core.domain.model.relationship.RelationshipRepository +import dev.usbharu.hideout.core.domain.model.support.principal.FromApi +import dev.usbharu.hideout.core.domain.model.userdetails.UserDetailRepository +import org.slf4j.LoggerFactory +import org.springframework.stereotype.Service + +@Service +class GetRelationshipApplicationService( + private val relationshipRepository: RelationshipRepository, + private val actorRepository: ActorRepository, + private val userDetailRepository: UserDetailRepository, + private val actorInstanceRelationshipRepository: ActorInstanceRelationshipRepository, + transaction: Transaction, +) : + LocalUserAbstractApplicationService( + transaction, + logger + ) { + override suspend fun internalExecute(command: GetRelationship, principal: FromApi): Relationship { + val userDetail = userDetailRepository.findById(principal.userDetailId)!! + val actor = actorRepository.findById(userDetail.actorId)!! + val targetId = ActorId(command.targetActorId) + val target = actorRepository.findById(targetId)!! + val relationship = ( + relationshipRepository.findByActorIdAndTargetId(actor.id, targetId) + ?: dev.usbharu.hideout.core.domain.model.relationship.Relationship.default(actor.id, targetId) + ) + + val relationship1 = ( + relationshipRepository.findByActorIdAndTargetId(targetId, actor.id) + ?: dev.usbharu.hideout.core.domain.model.relationship.Relationship.default(targetId, actor.id) + ) + + val actorInstanceRelationship = + actorInstanceRelationshipRepository.findByActorIdAndInstanceId(actor.id, target.instance) + ?: ActorInstanceRelationship.default( + actor.id, + target.instance + ) + + return Relationship.of(relationship, relationship1, actorInstanceRelationship) + } + + companion object { + private val logger = LoggerFactory.getLogger(GetRelationshipApplicationService::class.java) + } +} diff --git a/hideout-core/src/main/kotlin/dev/usbharu/hideout/core/application/relationship/get/Relationship.kt b/hideout-core/src/main/kotlin/dev/usbharu/hideout/core/application/relationship/get/Relationship.kt new file mode 100644 index 00000000..03237ef4 --- /dev/null +++ b/hideout-core/src/main/kotlin/dev/usbharu/hideout/core/application/relationship/get/Relationship.kt @@ -0,0 +1,58 @@ +/* + * Copyright (C) 2024 usbharu + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package dev.usbharu.hideout.core.application.relationship.get + +import dev.usbharu.hideout.core.domain.model.actorinstancerelationship.ActorInstanceRelationship +import dev.usbharu.hideout.core.domain.model.relationship.Relationship + +data class Relationship( + val actorId: Long, + val targetId: Long, + val following: Boolean, + val followedBy: Boolean, + val blocking: Boolean, + val blockedBy: Boolean, + val muting: Boolean, + val followRequesting: Boolean, + val followRequestedBy: Boolean, + val domainBlocking: Boolean, + val domainMuting: Boolean, + val domainDoNotSendPrivate: Boolean, +) { + companion object { + fun of( + relationship: Relationship, + relationship2: Relationship, + actorInstanceRelationship: ActorInstanceRelationship, + ): dev.usbharu.hideout.core.application.relationship.get.Relationship { + return Relationship( + actorId = relationship.actorId.id, + targetId = relationship.targetActorId.id, + following = relationship.following, + followedBy = relationship2.following, + blocking = relationship.blocking, + blockedBy = relationship2.blocking, + muting = relationship.muting, + followRequesting = relationship.followRequesting, + followRequestedBy = relationship2.followRequesting, + domainBlocking = actorInstanceRelationship.blocking, + domainMuting = actorInstanceRelationship.muting, + domainDoNotSendPrivate = actorInstanceRelationship.doNotSendPrivate + ) + } + } +} diff --git a/hideout-core/src/main/kotlin/dev/usbharu/hideout/core/application/relationship/mute/Mute.kt b/hideout-core/src/main/kotlin/dev/usbharu/hideout/core/application/relationship/mute/Mute.kt new file mode 100644 index 00000000..79a56830 --- /dev/null +++ b/hideout-core/src/main/kotlin/dev/usbharu/hideout/core/application/relationship/mute/Mute.kt @@ -0,0 +1,19 @@ +/* + * Copyright (C) 2024 usbharu + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package dev.usbharu.hideout.core.application.relationship.mute + +data class Mute(val targetActorId: Long) diff --git a/hideout-core/src/main/kotlin/dev/usbharu/hideout/core/application/relationship/mute/UserMuteApplicationService.kt b/hideout-core/src/main/kotlin/dev/usbharu/hideout/core/application/relationship/mute/UserMuteApplicationService.kt new file mode 100644 index 00000000..7c611d24 --- /dev/null +++ b/hideout-core/src/main/kotlin/dev/usbharu/hideout/core/application/relationship/mute/UserMuteApplicationService.kt @@ -0,0 +1,58 @@ +/* + * Copyright (C) 2024 usbharu + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package dev.usbharu.hideout.core.application.relationship.mute + +import dev.usbharu.hideout.core.application.relationship.block.UserBlockApplicationService +import dev.usbharu.hideout.core.application.shared.LocalUserAbstractApplicationService +import dev.usbharu.hideout.core.application.shared.Transaction +import dev.usbharu.hideout.core.domain.model.actor.ActorId +import dev.usbharu.hideout.core.domain.model.actor.ActorRepository +import dev.usbharu.hideout.core.domain.model.relationship.Relationship +import dev.usbharu.hideout.core.domain.model.relationship.RelationshipRepository +import dev.usbharu.hideout.core.domain.model.support.principal.FromApi +import dev.usbharu.hideout.core.domain.model.userdetails.UserDetailRepository +import org.slf4j.LoggerFactory +import org.springframework.stereotype.Service + +@Service +class UserMuteApplicationService( + private val relationshipRepository: RelationshipRepository, + transaction: Transaction, + private val actorRepository: ActorRepository, + private val userDetailRepository: UserDetailRepository, +) : + LocalUserAbstractApplicationService(transaction, logger) { + override suspend fun internalExecute(command: Mute, principal: FromApi) { + + val userDetail = userDetailRepository.findById(principal.userDetailId)!! + val actor = actorRepository.findById(userDetail.actorId)!! + + val targetId = ActorId(command.targetActorId) + val relationship = relationshipRepository.findByActorIdAndTargetId(actor.id, targetId) ?: Relationship.default( + actor.id, + targetId + ) + + relationship.mute() + + relationshipRepository.save(relationship) + } + + companion object { + private val logger = LoggerFactory.getLogger(UserBlockApplicationService::class.java) + } +} diff --git a/hideout-core/src/main/kotlin/dev/usbharu/hideout/core/application/relationship/rejectfollowrequest/RejectFollowRequest.kt b/hideout-core/src/main/kotlin/dev/usbharu/hideout/core/application/relationship/rejectfollowrequest/RejectFollowRequest.kt new file mode 100644 index 00000000..4662eff1 --- /dev/null +++ b/hideout-core/src/main/kotlin/dev/usbharu/hideout/core/application/relationship/rejectfollowrequest/RejectFollowRequest.kt @@ -0,0 +1,19 @@ +/* + * Copyright (C) 2024 usbharu + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package dev.usbharu.hideout.core.application.relationship.rejectfollowrequest + +data class RejectFollowRequest(val sourceActorId: Long) diff --git a/hideout-core/src/main/kotlin/dev/usbharu/hideout/core/application/relationship/rejectfollowrequest/UserRejectFollowRequestApplicationService.kt b/hideout-core/src/main/kotlin/dev/usbharu/hideout/core/application/relationship/rejectfollowrequest/UserRejectFollowRequestApplicationService.kt new file mode 100644 index 00000000..97adbdd0 --- /dev/null +++ b/hideout-core/src/main/kotlin/dev/usbharu/hideout/core/application/relationship/rejectfollowrequest/UserRejectFollowRequestApplicationService.kt @@ -0,0 +1,56 @@ +/* + * Copyright (C) 2024 usbharu + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package dev.usbharu.hideout.core.application.relationship.rejectfollowrequest + +import dev.usbharu.hideout.core.application.relationship.block.UserBlockApplicationService +import dev.usbharu.hideout.core.application.shared.LocalUserAbstractApplicationService +import dev.usbharu.hideout.core.application.shared.Transaction +import dev.usbharu.hideout.core.domain.model.actor.ActorId +import dev.usbharu.hideout.core.domain.model.actor.ActorRepository +import dev.usbharu.hideout.core.domain.model.relationship.RelationshipRepository +import dev.usbharu.hideout.core.domain.model.support.principal.FromApi +import dev.usbharu.hideout.core.domain.model.userdetails.UserDetailRepository +import org.slf4j.LoggerFactory +import org.springframework.stereotype.Service + +@Service +class UserRejectFollowRequestApplicationService( + private val relationshipRepository: RelationshipRepository, + transaction: Transaction, + private val actorRepository: ActorRepository, + private val userDetailRepository: UserDetailRepository, +) : + LocalUserAbstractApplicationService(transaction, logger) { + override suspend fun internalExecute(command: RejectFollowRequest, principal: FromApi) { + + val userDetail = userDetailRepository.findById(principal.userDetailId)!! + val actor = actorRepository.findById(userDetail.actorId)!! + + val targetId = ActorId(command.sourceActorId) + + val relationship = relationshipRepository.findByActorIdAndTargetId(targetId, actor.id) + ?: throw Exception("Follow request not found") + + relationship.rejectFollowRequest() + + relationshipRepository.save(relationship) + } + + companion object { + private val logger = LoggerFactory.getLogger(UserBlockApplicationService::class.java) + } +} diff --git a/hideout-core/src/main/kotlin/dev/usbharu/hideout/core/application/relationship/removefromfollowers/RemoveFromFollowers.kt b/hideout-core/src/main/kotlin/dev/usbharu/hideout/core/application/relationship/removefromfollowers/RemoveFromFollowers.kt new file mode 100644 index 00000000..f9642099 --- /dev/null +++ b/hideout-core/src/main/kotlin/dev/usbharu/hideout/core/application/relationship/removefromfollowers/RemoveFromFollowers.kt @@ -0,0 +1,19 @@ +/* + * Copyright (C) 2024 usbharu + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package dev.usbharu.hideout.core.application.relationship.removefromfollowers + +data class RemoveFromFollowers(val targetActorId: Long) diff --git a/hideout-core/src/main/kotlin/dev/usbharu/hideout/core/application/relationship/removefromfollowers/UserRemoveFromFollowersApplicationService.kt b/hideout-core/src/main/kotlin/dev/usbharu/hideout/core/application/relationship/removefromfollowers/UserRemoveFromFollowersApplicationService.kt new file mode 100644 index 00000000..d3e50e50 --- /dev/null +++ b/hideout-core/src/main/kotlin/dev/usbharu/hideout/core/application/relationship/removefromfollowers/UserRemoveFromFollowersApplicationService.kt @@ -0,0 +1,58 @@ +/* + * Copyright (C) 2024 usbharu + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package dev.usbharu.hideout.core.application.relationship.removefromfollowers + +import dev.usbharu.hideout.core.application.relationship.block.UserBlockApplicationService +import dev.usbharu.hideout.core.application.shared.LocalUserAbstractApplicationService +import dev.usbharu.hideout.core.application.shared.Transaction +import dev.usbharu.hideout.core.domain.model.actor.ActorId +import dev.usbharu.hideout.core.domain.model.actor.ActorRepository +import dev.usbharu.hideout.core.domain.model.relationship.Relationship +import dev.usbharu.hideout.core.domain.model.relationship.RelationshipRepository +import dev.usbharu.hideout.core.domain.model.support.principal.FromApi +import dev.usbharu.hideout.core.domain.model.userdetails.UserDetailRepository +import org.slf4j.LoggerFactory +import org.springframework.stereotype.Service + +@Service +class UserRemoveFromFollowersApplicationService( + private val relationshipRepository: RelationshipRepository, + transaction: Transaction, + private val actorRepository: ActorRepository, + private val userDetailRepository: UserDetailRepository, +) : + LocalUserAbstractApplicationService(transaction, logger) { + override suspend fun internalExecute(command: RemoveFromFollowers, principal: FromApi) { + + val userDetail = userDetailRepository.findById(principal.userDetailId)!! + val actor = actorRepository.findById(userDetail.actorId)!! + + val targetId = ActorId(command.targetActorId) + val relationship = relationshipRepository.findByActorIdAndTargetId(targetId, actor.id) ?: Relationship.default( + targetId, + actor.id + ) + + relationship.unfollow() + + relationshipRepository.save(relationship) + } + + companion object { + private val logger = LoggerFactory.getLogger(UserBlockApplicationService::class.java) + } +} diff --git a/hideout-core/src/main/kotlin/dev/usbharu/hideout/core/application/relationship/unblock/Unblock.kt b/hideout-core/src/main/kotlin/dev/usbharu/hideout/core/application/relationship/unblock/Unblock.kt new file mode 100644 index 00000000..7b85c603 --- /dev/null +++ b/hideout-core/src/main/kotlin/dev/usbharu/hideout/core/application/relationship/unblock/Unblock.kt @@ -0,0 +1,19 @@ +/* + * Copyright (C) 2024 usbharu + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package dev.usbharu.hideout.core.application.relationship.unblock + +data class Unblock(val targetActorId: Long) diff --git a/hideout-core/src/main/kotlin/dev/usbharu/hideout/core/application/relationship/unblock/UserUnblockApplicationService.kt b/hideout-core/src/main/kotlin/dev/usbharu/hideout/core/application/relationship/unblock/UserUnblockApplicationService.kt new file mode 100644 index 00000000..d5417799 --- /dev/null +++ b/hideout-core/src/main/kotlin/dev/usbharu/hideout/core/application/relationship/unblock/UserUnblockApplicationService.kt @@ -0,0 +1,58 @@ +/* + * Copyright (C) 2024 usbharu + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package dev.usbharu.hideout.core.application.relationship.unblock + +import dev.usbharu.hideout.core.application.relationship.block.UserBlockApplicationService +import dev.usbharu.hideout.core.application.shared.LocalUserAbstractApplicationService +import dev.usbharu.hideout.core.application.shared.Transaction +import dev.usbharu.hideout.core.domain.model.actor.ActorId +import dev.usbharu.hideout.core.domain.model.actor.ActorRepository +import dev.usbharu.hideout.core.domain.model.relationship.Relationship +import dev.usbharu.hideout.core.domain.model.relationship.RelationshipRepository +import dev.usbharu.hideout.core.domain.model.support.principal.FromApi +import dev.usbharu.hideout.core.domain.model.userdetails.UserDetailRepository +import org.slf4j.LoggerFactory +import org.springframework.stereotype.Service + +@Service +class UserUnblockApplicationService( + private val relationshipRepository: RelationshipRepository, + transaction: Transaction, + private val actorRepository: ActorRepository, + private val userDetailRepository: UserDetailRepository, +) : + LocalUserAbstractApplicationService(transaction, logger) { + override suspend fun internalExecute(command: Unblock, principal: FromApi) { + + val userDetail = userDetailRepository.findById(principal.userDetailId)!! + val actor = actorRepository.findById(userDetail.actorId)!! + + val targetId = ActorId(command.targetActorId) + val relationship = relationshipRepository.findByActorIdAndTargetId(actor.id, targetId) ?: Relationship.default( + actor.id, + targetId + ) + + relationship.unblock() + + relationshipRepository.save(relationship) + } + + companion object { + private val logger = LoggerFactory.getLogger(UserBlockApplicationService::class.java) + } +} diff --git a/hideout-core/src/main/kotlin/dev/usbharu/hideout/core/application/relationship/unfollow/Unfollow.kt b/hideout-core/src/main/kotlin/dev/usbharu/hideout/core/application/relationship/unfollow/Unfollow.kt new file mode 100644 index 00000000..60190dab --- /dev/null +++ b/hideout-core/src/main/kotlin/dev/usbharu/hideout/core/application/relationship/unfollow/Unfollow.kt @@ -0,0 +1,19 @@ +/* + * Copyright (C) 2024 usbharu + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package dev.usbharu.hideout.core.application.relationship.unfollow + +data class Unfollow(val targetActorId: Long) diff --git a/hideout-core/src/main/kotlin/dev/usbharu/hideout/core/application/relationship/unfollow/UserUnfollowApplicationService.kt b/hideout-core/src/main/kotlin/dev/usbharu/hideout/core/application/relationship/unfollow/UserUnfollowApplicationService.kt new file mode 100644 index 00000000..a7068d00 --- /dev/null +++ b/hideout-core/src/main/kotlin/dev/usbharu/hideout/core/application/relationship/unfollow/UserUnfollowApplicationService.kt @@ -0,0 +1,58 @@ +/* + * Copyright (C) 2024 usbharu + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package dev.usbharu.hideout.core.application.relationship.unfollow + +import dev.usbharu.hideout.core.application.relationship.block.UserBlockApplicationService +import dev.usbharu.hideout.core.application.shared.LocalUserAbstractApplicationService +import dev.usbharu.hideout.core.application.shared.Transaction +import dev.usbharu.hideout.core.domain.model.actor.ActorId +import dev.usbharu.hideout.core.domain.model.actor.ActorRepository +import dev.usbharu.hideout.core.domain.model.relationship.Relationship +import dev.usbharu.hideout.core.domain.model.relationship.RelationshipRepository +import dev.usbharu.hideout.core.domain.model.support.principal.FromApi +import dev.usbharu.hideout.core.domain.model.userdetails.UserDetailRepository +import org.slf4j.LoggerFactory +import org.springframework.stereotype.Service + +@Service +class UserUnfollowApplicationService( + private val relationshipRepository: RelationshipRepository, + transaction: Transaction, + private val actorRepository: ActorRepository, + private val userDetailRepository: UserDetailRepository, +) : + LocalUserAbstractApplicationService(transaction, logger) { + override suspend fun internalExecute(command: Unfollow, principal: FromApi) { + + val userDetail = userDetailRepository.findById(principal.userDetailId)!! + val actor = actorRepository.findById(userDetail.actorId)!! + + val targetId = ActorId(command.targetActorId) + val relationship = relationshipRepository.findByActorIdAndTargetId(actor.id, targetId) ?: Relationship.default( + actor.id, + targetId + ) + + relationship.unfollow() + + relationshipRepository.save(relationship) + } + + companion object { + private val logger = LoggerFactory.getLogger(UserBlockApplicationService::class.java) + } +} diff --git a/hideout-core/src/main/kotlin/dev/usbharu/hideout/core/application/relationship/unmute/Unmute.kt b/hideout-core/src/main/kotlin/dev/usbharu/hideout/core/application/relationship/unmute/Unmute.kt new file mode 100644 index 00000000..1939ee25 --- /dev/null +++ b/hideout-core/src/main/kotlin/dev/usbharu/hideout/core/application/relationship/unmute/Unmute.kt @@ -0,0 +1,19 @@ +/* + * Copyright (C) 2024 usbharu + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package dev.usbharu.hideout.core.application.relationship.unmute + +data class Unmute(val targetActorId: Long) diff --git a/hideout-core/src/main/kotlin/dev/usbharu/hideout/core/application/relationship/unmute/UserUnmuteApplicationService.kt b/hideout-core/src/main/kotlin/dev/usbharu/hideout/core/application/relationship/unmute/UserUnmuteApplicationService.kt new file mode 100644 index 00000000..b90ba059 --- /dev/null +++ b/hideout-core/src/main/kotlin/dev/usbharu/hideout/core/application/relationship/unmute/UserUnmuteApplicationService.kt @@ -0,0 +1,58 @@ +/* + * Copyright (C) 2024 usbharu + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package dev.usbharu.hideout.core.application.relationship.unmute + +import dev.usbharu.hideout.core.application.relationship.block.UserBlockApplicationService +import dev.usbharu.hideout.core.application.shared.LocalUserAbstractApplicationService +import dev.usbharu.hideout.core.application.shared.Transaction +import dev.usbharu.hideout.core.domain.model.actor.ActorId +import dev.usbharu.hideout.core.domain.model.actor.ActorRepository +import dev.usbharu.hideout.core.domain.model.relationship.Relationship +import dev.usbharu.hideout.core.domain.model.relationship.RelationshipRepository +import dev.usbharu.hideout.core.domain.model.support.principal.FromApi +import dev.usbharu.hideout.core.domain.model.userdetails.UserDetailRepository +import org.slf4j.LoggerFactory +import org.springframework.stereotype.Service + +@Service +class UserUnmuteApplicationService( + private val relationshipRepository: RelationshipRepository, + transaction: Transaction, + private val actorRepository: ActorRepository, + private val userDetailRepository: UserDetailRepository, +) : + LocalUserAbstractApplicationService(transaction, logger) { + companion object { + private val logger = LoggerFactory.getLogger(UserBlockApplicationService::class.java) + } + + override suspend fun internalExecute(command: Unmute, principal: FromApi) { + + val userDetail = userDetailRepository.findById(principal.userDetailId)!! + val actor = actorRepository.findById(userDetail.actorId)!! + + val targetId = ActorId(command.targetActorId) + val relationship = relationshipRepository.findByActorIdAndTargetId(actor.id, targetId) ?: Relationship.default( + actor.id, + targetId + ) + + relationship.unmute() + + relationshipRepository.save(relationship) + } +} diff --git a/hideout-core/src/main/kotlin/dev/usbharu/hideout/core/application/shared/AbstractApplicationService.kt b/hideout-core/src/main/kotlin/dev/usbharu/hideout/core/application/shared/AbstractApplicationService.kt new file mode 100644 index 00000000..88f5de32 --- /dev/null +++ b/hideout-core/src/main/kotlin/dev/usbharu/hideout/core/application/shared/AbstractApplicationService.kt @@ -0,0 +1,45 @@ +/* + * Copyright (C) 2024 usbharu + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package dev.usbharu.hideout.core.application.shared + +import dev.usbharu.hideout.core.domain.model.support.principal.Principal +import kotlinx.coroutines.CancellationException +import org.slf4j.Logger + +abstract class AbstractApplicationService( + protected val transaction: Transaction, + protected val logger: Logger, +) : ApplicationService { + override suspend fun execute(command: T, principal: Principal): R { + return try { + logger.debug("START {}", command::class.simpleName) + val response = transaction.transaction { + internalExecute(command, principal) + } + logger.info("SUCCESS ${command::class.simpleName}") + response + } catch (e: CancellationException) { + logger.debug("Coroutine canceled", e) + throw e + } catch (e: Exception) { + logger.warn("Command execution error", e) + throw e + } + } + + protected abstract suspend fun internalExecute(command: T, principal: Principal): R +} diff --git a/hideout-core/src/main/kotlin/dev/usbharu/hideout/core/application/shared/ApplicationService.kt b/hideout-core/src/main/kotlin/dev/usbharu/hideout/core/application/shared/ApplicationService.kt new file mode 100644 index 00000000..6b2b3f02 --- /dev/null +++ b/hideout-core/src/main/kotlin/dev/usbharu/hideout/core/application/shared/ApplicationService.kt @@ -0,0 +1,23 @@ +/* + * Copyright (C) 2024 usbharu + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package dev.usbharu.hideout.core.application.shared + +import dev.usbharu.hideout.core.domain.model.support.principal.Principal + +interface ApplicationService { + suspend fun execute(command: T, principal: Principal): R +} diff --git a/hideout-core/src/main/kotlin/dev/usbharu/hideout/core/application/shared/LocalUserAbstractApplicationService.kt b/hideout-core/src/main/kotlin/dev/usbharu/hideout/core/application/shared/LocalUserAbstractApplicationService.kt new file mode 100644 index 00000000..cc4ddef8 --- /dev/null +++ b/hideout-core/src/main/kotlin/dev/usbharu/hideout/core/application/shared/LocalUserAbstractApplicationService.kt @@ -0,0 +1,15 @@ +package dev.usbharu.hideout.core.application.shared + +import dev.usbharu.hideout.core.domain.model.support.principal.FromApi +import dev.usbharu.hideout.core.domain.model.support.principal.Principal +import org.slf4j.Logger + +abstract class LocalUserAbstractApplicationService(transaction: Transaction, logger: Logger) : + AbstractApplicationService(transaction, logger) { + override suspend fun internalExecute(command: T, principal: Principal): R { + require(principal is FromApi) + return internalExecute(command, principal) + } + + protected abstract suspend fun internalExecute(command: T, principal: FromApi): R +} \ No newline at end of file diff --git a/hideout-core/src/main/kotlin/dev/usbharu/hideout/core/application/shared/Transaction.kt b/hideout-core/src/main/kotlin/dev/usbharu/hideout/core/application/shared/Transaction.kt new file mode 100644 index 00000000..5dc4df1b --- /dev/null +++ b/hideout-core/src/main/kotlin/dev/usbharu/hideout/core/application/shared/Transaction.kt @@ -0,0 +1,25 @@ +/* + * Copyright (C) 2024 usbharu + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package dev.usbharu.hideout.core.application.shared + +import org.springframework.stereotype.Service + +@Service +interface Transaction { + suspend fun transaction(block: suspend () -> T): T + suspend fun transaction(transactionLevel: Int, block: suspend () -> T): T +} diff --git a/hideout-core/src/main/kotlin/dev/usbharu/hideout/core/application/timeline/AddTimelineRelationship.kt b/hideout-core/src/main/kotlin/dev/usbharu/hideout/core/application/timeline/AddTimelineRelationship.kt new file mode 100644 index 00000000..e8c7bda4 --- /dev/null +++ b/hideout-core/src/main/kotlin/dev/usbharu/hideout/core/application/timeline/AddTimelineRelationship.kt @@ -0,0 +1,7 @@ +package dev.usbharu.hideout.core.application.timeline + +import dev.usbharu.hideout.core.domain.model.timelinerelationship.TimelineRelationship + +data class AddTimelineRelationship( + val timelineRelationship: TimelineRelationship +) diff --git a/hideout-core/src/main/kotlin/dev/usbharu/hideout/core/application/timeline/UserAddTimelineRelationshipApplicationService.kt b/hideout-core/src/main/kotlin/dev/usbharu/hideout/core/application/timeline/UserAddTimelineRelationshipApplicationService.kt new file mode 100644 index 00000000..5bd0721f --- /dev/null +++ b/hideout-core/src/main/kotlin/dev/usbharu/hideout/core/application/timeline/UserAddTimelineRelationshipApplicationService.kt @@ -0,0 +1,26 @@ +package dev.usbharu.hideout.core.application.timeline + +import dev.usbharu.hideout.core.application.shared.AbstractApplicationService +import dev.usbharu.hideout.core.application.shared.Transaction +import dev.usbharu.hideout.core.domain.model.support.principal.Principal +import dev.usbharu.hideout.core.domain.model.timelinerelationship.TimelineRelationshipRepository +import org.slf4j.LoggerFactory +import org.springframework.stereotype.Service + +@Service +class UserAddTimelineRelationshipApplicationService( + private val timelineRelationshipRepository: TimelineRelationshipRepository, + transaction: Transaction +) : + AbstractApplicationService( + transaction, logger + ) { + override suspend fun internalExecute(command: AddTimelineRelationship, principal: Principal) { + timelineRelationshipRepository.save(command.timelineRelationship) + + } + + companion object { + private val logger = LoggerFactory.getLogger(UserAddTimelineRelationshipApplicationService::class.java) + } +} \ No newline at end of file diff --git a/hideout-core/src/main/kotlin/dev/usbharu/hideout/core/config/ApplicationConfig.kt b/hideout-core/src/main/kotlin/dev/usbharu/hideout/core/config/ApplicationConfig.kt new file mode 100644 index 00000000..5cb5941c --- /dev/null +++ b/hideout-core/src/main/kotlin/dev/usbharu/hideout/core/config/ApplicationConfig.kt @@ -0,0 +1,27 @@ +/* + * Copyright (C) 2024 usbharu + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package dev.usbharu.hideout.core.config + +import org.springframework.boot.context.properties.ConfigurationProperties +import java.net.URL + +@ConfigurationProperties("hideout") +data class ApplicationConfig( + val url: URL, + val private: Boolean = true, + val keySize: Int = 2048, +) diff --git a/hideout-core/src/main/kotlin/dev/usbharu/hideout/core/config/DefaultTimelineStoreConfig.kt b/hideout-core/src/main/kotlin/dev/usbharu/hideout/core/config/DefaultTimelineStoreConfig.kt new file mode 100644 index 00000000..a6db6b29 --- /dev/null +++ b/hideout-core/src/main/kotlin/dev/usbharu/hideout/core/config/DefaultTimelineStoreConfig.kt @@ -0,0 +1,8 @@ +package dev.usbharu.hideout.core.config + +import org.springframework.boot.context.properties.ConfigurationProperties + +@ConfigurationProperties("hideout.timeline.default") +data class DefaultTimelineStoreConfig( + val actorPostsCount: Int = 500 +) diff --git a/hideout-core/src/main/kotlin/dev/usbharu/hideout/core/config/FFmpegVideoConfig.kt b/hideout-core/src/main/kotlin/dev/usbharu/hideout/core/config/FFmpegVideoConfig.kt new file mode 100644 index 00000000..65e5f9be --- /dev/null +++ b/hideout-core/src/main/kotlin/dev/usbharu/hideout/core/config/FFmpegVideoConfig.kt @@ -0,0 +1,17 @@ +package dev.usbharu.hideout.core.config + +import org.bytedeco.ffmpeg.global.avcodec +import org.springframework.boot.context.properties.ConfigurationProperties + +@ConfigurationProperties("hideout.media.video.ffmpeg") +data class FFmpegVideoConfig( + val frameRate: Int = 60, + val maxWidth: Int = 1920, + val maxHeight: Int = 1080, + val format: String = "mp4", + val videoCodec: Int = avcodec.AV_CODEC_ID_H264, + val audioCodec: Int = avcodec.AV_CODEC_ID_AAC, + val videoQuality: Double = 1.0, + val videoOption: List = listOf("preset", "ultrafast"), + val maxBitrate: Int = 1300000, +) diff --git a/hideout-core/src/main/kotlin/dev/usbharu/hideout/core/config/FlywayConfig.kt b/hideout-core/src/main/kotlin/dev/usbharu/hideout/core/config/FlywayConfig.kt new file mode 100644 index 00000000..15133cdb --- /dev/null +++ b/hideout-core/src/main/kotlin/dev/usbharu/hideout/core/config/FlywayConfig.kt @@ -0,0 +1,16 @@ +package dev.usbharu.hideout.core.config + +import org.springframework.boot.autoconfigure.flyway.FlywayMigrationStrategy +import org.springframework.context.annotation.Bean +import org.springframework.context.annotation.Configuration + +@Configuration +class FlywayConfig { + @Bean + fun cleanMigrateStrategy(): FlywayMigrationStrategy { + return FlywayMigrationStrategy { migrate -> + migrate.repair() + migrate.migrate() + } + } +} diff --git a/hideout-core/src/main/kotlin/dev/usbharu/hideout/core/config/HtmlSanitizeConfig.kt b/hideout-core/src/main/kotlin/dev/usbharu/hideout/core/config/HtmlSanitizeConfig.kt new file mode 100644 index 00000000..e55bc8fe --- /dev/null +++ b/hideout-core/src/main/kotlin/dev/usbharu/hideout/core/config/HtmlSanitizeConfig.kt @@ -0,0 +1,37 @@ +/* + * Copyright (C) 2024 usbharu + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package dev.usbharu.hideout.core.config + +import org.owasp.html.HtmlPolicyBuilder +import org.owasp.html.PolicyFactory +import org.springframework.context.annotation.Bean +import org.springframework.context.annotation.Configuration + +@Configuration +class HtmlSanitizeConfig { + @Bean + fun policy(): PolicyFactory { + return HtmlPolicyBuilder() + .allowElements("p") + .allowElements("a") + .allowElements("br") + .allowAttributes("href").onElements("a") + .allowUrlProtocols("http", "https") + .allowElements({ _, _ -> return@allowElements "p" }, "h1", "h2", "h3", "h4", "h5", "h6") + .toFactory() + } +} diff --git a/hideout-core/src/main/kotlin/dev/usbharu/hideout/core/config/ImageIOImageConfig.kt b/hideout-core/src/main/kotlin/dev/usbharu/hideout/core/config/ImageIOImageConfig.kt new file mode 100644 index 00000000..03e8bfcc --- /dev/null +++ b/hideout-core/src/main/kotlin/dev/usbharu/hideout/core/config/ImageIOImageConfig.kt @@ -0,0 +1,10 @@ +package dev.usbharu.hideout.core.config + +import org.springframework.boot.context.properties.ConfigurationProperties + +@ConfigurationProperties("hideout.media.image.imageio") +data class ImageIOImageConfig( + val thumbnailsWidth: Int = 1000, + val thumbnailsHeight: Int = 1000, + val format: String = "jpeg" +) diff --git a/hideout-core/src/main/kotlin/dev/usbharu/hideout/core/config/LocalStorageConfig.kt b/hideout-core/src/main/kotlin/dev/usbharu/hideout/core/config/LocalStorageConfig.kt new file mode 100644 index 00000000..48962c11 --- /dev/null +++ b/hideout-core/src/main/kotlin/dev/usbharu/hideout/core/config/LocalStorageConfig.kt @@ -0,0 +1,17 @@ +package dev.usbharu.hideout.core.config + +import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty +import org.springframework.boot.context.properties.ConfigurationProperties + +/** + * メディアの保存にローカルファイルシステムを使用する際のコンフィグ + * + * @property path フォゾンする場所へのパス。 /から始めると絶対パスとなります。 + * @property publicUrl 公開用URL 省略可能 指定するとHideoutがファイルを配信しなくなります。 + */ +@ConfigurationProperties("hideout.storage.local") +@ConditionalOnProperty("hideout.storage.type", havingValue = "local", matchIfMissing = true) +data class LocalStorageConfig( + val path: String = "files", + val publicUrl: String? +) diff --git a/hideout-core/src/main/kotlin/dev/usbharu/hideout/core/config/S3StorageConfig.kt b/hideout-core/src/main/kotlin/dev/usbharu/hideout/core/config/S3StorageConfig.kt new file mode 100644 index 00000000..1a8b5677 --- /dev/null +++ b/hideout-core/src/main/kotlin/dev/usbharu/hideout/core/config/S3StorageConfig.kt @@ -0,0 +1,34 @@ +package dev.usbharu.hideout.core.config + +import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty +import org.springframework.boot.context.properties.ConfigurationProperties +import org.springframework.context.annotation.Bean +import org.springframework.context.annotation.Configuration +import software.amazon.awssdk.auth.credentials.AwsBasicCredentials +import software.amazon.awssdk.regions.Region +import software.amazon.awssdk.services.s3.S3Client +import java.net.URI + +@ConfigurationProperties("hideout.storage.s3") +@ConditionalOnProperty("hideout.storage.type", havingValue = "s3") +data class S3StorageConfig( + val endpoint: String, + val publicUrl: String, + val bucket: String, + val region: String, + val accessKey: String, + val secretKey: String +) + +@Configuration +class AwsConfig { + @Bean + @ConditionalOnProperty("hideout.storage.type", havingValue = "s3") + fun s3Client(awsConfig: S3StorageConfig): S3Client { + return S3Client.builder() + .endpointOverride(URI.create(awsConfig.endpoint)) + .region(Region.of(awsConfig.region)) + .credentialsProvider { AwsBasicCredentials.create(awsConfig.accessKey, awsConfig.secretKey) } + .build() + } +} diff --git a/hideout-core/src/main/kotlin/dev/usbharu/hideout/core/config/SecurityConfig.kt b/hideout-core/src/main/kotlin/dev/usbharu/hideout/core/config/SecurityConfig.kt new file mode 100644 index 00000000..17320fb8 --- /dev/null +++ b/hideout-core/src/main/kotlin/dev/usbharu/hideout/core/config/SecurityConfig.kt @@ -0,0 +1,194 @@ +/* + * Copyright (C) 2024 usbharu + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package dev.usbharu.hideout.core.config + +import com.nimbusds.jose.jwk.JWKSet +import com.nimbusds.jose.jwk.RSAKey +import com.nimbusds.jose.jwk.source.ImmutableJWKSet +import com.nimbusds.jose.jwk.source.JWKSource +import com.nimbusds.jose.proc.SecurityContext +import dev.usbharu.hideout.core.infrastructure.springframework.oauth2.HideoutUserDetails +import dev.usbharu.hideout.util.RsaUtil +import org.springframework.boot.context.properties.ConfigurationProperties +import org.springframework.context.annotation.Bean +import org.springframework.context.annotation.Configuration +import org.springframework.core.annotation.Order +import org.springframework.http.HttpMethod.GET +import org.springframework.http.HttpMethod.POST +import org.springframework.jdbc.core.JdbcOperations +import org.springframework.security.access.hierarchicalroles.RoleHierarchy +import org.springframework.security.access.hierarchicalroles.RoleHierarchyImpl +import org.springframework.security.config.annotation.web.builders.HttpSecurity +import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity +import org.springframework.security.config.annotation.web.invoke +import org.springframework.security.core.Authentication +import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder +import org.springframework.security.crypto.password.PasswordEncoder +import org.springframework.security.oauth2.core.AuthorizationGrantType +import org.springframework.security.oauth2.server.authorization.JdbcOAuth2AuthorizationConsentService +import org.springframework.security.oauth2.server.authorization.OAuth2AuthorizationConsentService +import org.springframework.security.oauth2.server.authorization.OAuth2TokenType +import org.springframework.security.oauth2.server.authorization.client.JdbcRegisteredClientRepository +import org.springframework.security.oauth2.server.authorization.client.RegisteredClientRepository +import org.springframework.security.oauth2.server.authorization.config.annotation.web.configuration.OAuth2AuthorizationServerConfiguration +import org.springframework.security.oauth2.server.authorization.settings.AuthorizationServerSettings +import org.springframework.security.oauth2.server.authorization.token.JwtEncodingContext +import org.springframework.security.oauth2.server.authorization.token.OAuth2TokenCustomizer +import org.springframework.security.web.SecurityFilterChain +import org.springframework.security.web.authentication.LoginUrlAuthenticationEntryPoint + +@Configuration +@EnableWebSecurity(debug = true) +class SecurityConfig { + @Bean + fun passwordEncoder(): PasswordEncoder = BCryptPasswordEncoder() + + @Bean + @Order(1) + fun oauth2Provider(http: HttpSecurity): SecurityFilterChain { + OAuth2AuthorizationServerConfiguration.applyDefaultSecurity(http) + http { + exceptionHandling { + authenticationEntryPoint = LoginUrlAuthenticationEntryPoint("/login") + } + } + return http.build() + } + + @Bean + @Order(3) + fun httpSecurityFilterChain(http: HttpSecurity): SecurityFilterChain { + http { + authorizeHttpRequests { + authorize("/error", permitAll) + authorize("/login", permitAll) + authorize(GET, "/.well-known/**", permitAll) + authorize(GET, "/nodeinfo/2.0", permitAll) + + authorize(GET, "/auth/sign_up", hasRole("ANONYMOUS")) + authorize(POST, "/auth/sign_up", permitAll) + + authorize(anyRequest, authenticated) + } + formLogin { + } + } + return http.build() + } + + @Bean + fun registeredClientRepository(jdbcOperations: JdbcOperations): RegisteredClientRepository = + JdbcRegisteredClientRepository(jdbcOperations) + + @Bean + @Suppress("FunctionMaxLength") + fun oauth2AuthorizationConsentService( + jdbcOperations: JdbcOperations, + registeredClientRepository: RegisteredClientRepository, + ): OAuth2AuthorizationConsentService = + JdbcOAuth2AuthorizationConsentService(jdbcOperations, registeredClientRepository) + + @Bean + fun authorizationServerSettings(): AuthorizationServerSettings { + return AuthorizationServerSettings.builder().authorizationEndpoint("/oauth/authorize") + .tokenEndpoint("/oauth/token").tokenRevocationEndpoint("/oauth/revoke").build() + } + + @Bean + fun jwtTokenCustomizer(): OAuth2TokenCustomizer { + return OAuth2TokenCustomizer { context: JwtEncodingContext -> + + if (OAuth2TokenType.ACCESS_TOKEN == context.tokenType && + context.authorization?.authorizationGrantType == AuthorizationGrantType.AUTHORIZATION_CODE + ) { + val userDetailsImpl = context.getPrincipal().principal as HideoutUserDetails + context.claims.claim("uid", userDetailsImpl.userDetailsId.toString()) + } + } + } + + @Bean + fun loadJwkSource(jwkConfig: JwkConfig): JWKSource { + val rsaKey = RSAKey.Builder(RsaUtil.decodeRsaPublicKey(jwkConfig.publicKey)) + .privateKey(RsaUtil.decodeRsaPrivateKey(jwkConfig.privateKey)).keyID(jwkConfig.keyId).build() + return ImmutableJWKSet(JWKSet(rsaKey)) + } + + @ConfigurationProperties("hideout.security.jwt") + data class JwkConfig( + val keyId: String, + val publicKey: String, + val privateKey: String, + ) + + @Bean + fun roleHierarchy(): RoleHierarchy { + val roleHierarchyImpl = RoleHierarchyImpl.fromHierarchy( + """ + SCOPE_read > SCOPE_read:accounts + SCOPE_read > SCOPE_read:accounts + SCOPE_read > SCOPE_read:blocks + SCOPE_read > SCOPE_read:bookmarks + SCOPE_read > SCOPE_read:favourites + SCOPE_read > SCOPE_read:filters + SCOPE_read > SCOPE_read:follows + SCOPE_read > SCOPE_read:lists + SCOPE_read > SCOPE_read:mutes + SCOPE_read > SCOPE_read:notifications + SCOPE_read > SCOPE_read:search + SCOPE_read > SCOPE_read:statuses + SCOPE_write > SCOPE_write:accounts + SCOPE_write > SCOPE_write:blocks + SCOPE_write > SCOPE_write:bookmarks + SCOPE_write > SCOPE_write:conversations + SCOPE_write > SCOPE_write:favourites + SCOPE_write > SCOPE_write:filters + SCOPE_write > SCOPE_write:follows + SCOPE_write > SCOPE_write:lists + SCOPE_write > SCOPE_write:media + SCOPE_write > SCOPE_write:mutes + SCOPE_write > SCOPE_write:notifications + SCOPE_write > SCOPE_write:reports + SCOPE_write > SCOPE_write:statuses + SCOPE_follow > SCOPE_write:blocks + SCOPE_follow > SCOPE_write:follows + SCOPE_follow > SCOPE_write:mutes + SCOPE_follow > SCOPE_read:blocks + SCOPE_follow > SCOPE_read:follows + SCOPE_follow > SCOPE_read:mutes + SCOPE_admin > SCOPE_admin:read + SCOPE_admin > SCOPE_admin:write + SCOPE_admin:read > SCOPE_admin:read:accounts + SCOPE_admin:read > SCOPE_admin:read:reports + SCOPE_admin:read > SCOPE_admin:read:domain_allows + SCOPE_admin:read > SCOPE_admin:read:domain_blocks + SCOPE_admin:read > SCOPE_admin:read:ip_blocks + SCOPE_admin:read > SCOPE_admin:read:email_domain_blocks + SCOPE_admin:read > SCOPE_admin:read:canonical_email_blocks + SCOPE_admin:write > SCOPE_admin:write:accounts + SCOPE_admin:write > SCOPE_admin:write:reports + SCOPE_admin:write > SCOPE_admin:write:domain_allows + SCOPE_admin:write > SCOPE_admin:write:domain_blocks + SCOPE_admin:write > SCOPE_admin:write:ip_blocks + SCOPE_admin:write > SCOPE_admin:write:email_domain_blocks + SCOPE_admin:write > SCOPE_admin:write:canonical_email_blocks + """.trimIndent() + ) + + return roleHierarchyImpl + } +} diff --git a/hideout-core/src/main/kotlin/dev/usbharu/hideout/core/config/SpringMvcConfig.kt b/hideout-core/src/main/kotlin/dev/usbharu/hideout/core/config/SpringMvcConfig.kt new file mode 100644 index 00000000..ee75a0ef --- /dev/null +++ b/hideout-core/src/main/kotlin/dev/usbharu/hideout/core/config/SpringMvcConfig.kt @@ -0,0 +1,46 @@ +/* + * Copyright (C) 2024 usbharu + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package dev.usbharu.hideout.core.config + +import dev.usbharu.hideout.generate.JsonOrFormModelMethodProcessor +import org.springframework.context.annotation.Bean +import org.springframework.context.annotation.Configuration +import org.springframework.http.converter.HttpMessageConverter +import org.springframework.web.method.support.HandlerMethodArgumentResolver +import org.springframework.web.servlet.config.annotation.WebMvcConfigurer +import org.springframework.web.servlet.mvc.method.annotation.RequestResponseBodyMethodProcessor +import org.springframework.web.servlet.mvc.method.annotation.ServletModelAttributeMethodProcessor + +@Configuration +class MvcConfigurer(private val jsonOrFormModelMethodProcessor: JsonOrFormModelMethodProcessor) : WebMvcConfigurer { + override fun addArgumentResolvers(resolvers: MutableList) { + resolvers.add(jsonOrFormModelMethodProcessor) + } +} + +@Configuration +class JsonOrFormModelMethodProcessorConfig { + @Bean + fun jsonOrFormModelMethodProcessor(converter: List>): JsonOrFormModelMethodProcessor { + return JsonOrFormModelMethodProcessor( + ServletModelAttributeMethodProcessor(true), + RequestResponseBodyMethodProcessor( + converter + ) + ) + } +} diff --git a/hideout-core/src/main/kotlin/dev/usbharu/hideout/core/domain/event/actor/ActorEvent.kt b/hideout-core/src/main/kotlin/dev/usbharu/hideout/core/domain/event/actor/ActorEvent.kt new file mode 100644 index 00000000..98f2918a --- /dev/null +++ b/hideout-core/src/main/kotlin/dev/usbharu/hideout/core/domain/event/actor/ActorEvent.kt @@ -0,0 +1,46 @@ +/* + * Copyright (C) 2024 usbharu + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package dev.usbharu.hideout.core.domain.event.actor + +import dev.usbharu.hideout.core.domain.model.actor.Actor +import dev.usbharu.hideout.core.domain.shared.domainevent.DomainEvent +import dev.usbharu.hideout.core.domain.shared.domainevent.DomainEventBody + +class ActorDomainEventFactory(private val actor: Actor) { + fun createEvent(actorEvent: ActorEvent): DomainEvent { + return DomainEvent.create( + actorEvent.eventName, + ActorEventBody(actor), + actorEvent.collectable + ) + } +} + +class ActorEventBody(actor: Actor) : DomainEventBody( + mapOf( + "actor" to actor + ), +) + +enum class ActorEvent(val eventName: String, val collectable: Boolean = true) { + UPDATE("ActorUpdate"), + DELETE("ActorDelete"), + CHECK_UPDATE("ActorCheckUpdate"), + MOVE("ActorMove"), + ACTOR_SUSPEND("ActorSuspend"), + ACTOR_UNSUSPEND("ActorUnsuspend"), +} diff --git a/hideout-core/src/main/kotlin/dev/usbharu/hideout/core/domain/event/actorinstancerelationship/ActorInstanceRelationshipEvent.kt b/hideout-core/src/main/kotlin/dev/usbharu/hideout/core/domain/event/actorinstancerelationship/ActorInstanceRelationshipEvent.kt new file mode 100644 index 00000000..ba92c969 --- /dev/null +++ b/hideout-core/src/main/kotlin/dev/usbharu/hideout/core/domain/event/actorinstancerelationship/ActorInstanceRelationshipEvent.kt @@ -0,0 +1,49 @@ +/* + * Copyright (C) 2024 usbharu + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package dev.usbharu.hideout.core.domain.event.actorinstancerelationship + +import dev.usbharu.hideout.core.domain.model.actorinstancerelationship.ActorInstanceRelationship +import dev.usbharu.hideout.core.domain.shared.domainevent.DomainEvent +import dev.usbharu.hideout.core.domain.shared.domainevent.DomainEventBody + +class ActorInstanceRelationshipDomainEventFactory(private val actorInstanceRelationship: ActorInstanceRelationship) { + fun createEvent( + actorInstanceRelationshipEvent: ActorInstanceRelationshipEvent + ): DomainEvent { + return DomainEvent.create( + actorInstanceRelationshipEvent.eventName, + ActorInstanceRelationshipEventBody(actorInstanceRelationship) + ) + } +} + +class ActorInstanceRelationshipEventBody(actorInstanceRelationship: ActorInstanceRelationship) : + DomainEventBody( + mapOf( + "actorId" to actorInstanceRelationship.actorId, + "instanceId" to actorInstanceRelationship.instanceId, + "muting" to actorInstanceRelationship.muting, + "blocking" to actorInstanceRelationship.blocking, + "doNotSendPrivate" to actorInstanceRelationship.doNotSendPrivate, + ), + ) + +enum class ActorInstanceRelationshipEvent(val eventName: String) { + BLOCK("ActorInstanceBlock"), + MUTE("ActorInstanceMute"), + UNMUTE("ActorInstanceUnmute"), +} diff --git a/hideout-core/src/main/kotlin/dev/usbharu/hideout/core/domain/event/instance/InstanceEvent.kt b/hideout-core/src/main/kotlin/dev/usbharu/hideout/core/domain/event/instance/InstanceEvent.kt new file mode 100644 index 00000000..3f09a2c7 --- /dev/null +++ b/hideout-core/src/main/kotlin/dev/usbharu/hideout/core/domain/event/instance/InstanceEvent.kt @@ -0,0 +1,36 @@ +/* + * Copyright (C) 2024 usbharu + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package dev.usbharu.hideout.core.domain.event.instance + +import dev.usbharu.hideout.core.domain.model.instance.Instance +import dev.usbharu.hideout.core.domain.shared.domainevent.DomainEvent +import dev.usbharu.hideout.core.domain.shared.domainevent.DomainEventBody + +class InstanceEventFactory(private val instance: Instance) { + fun createEvent(event: InstanceEvent): DomainEvent { + return DomainEvent.create( + event.eventName, + InstanceEventBody(instance) + ) + } +} + +class InstanceEventBody(instance: Instance) : DomainEventBody(mapOf("instance" to instance)) + +enum class InstanceEvent(val eventName: String) { + UPDATE("InstanceUpdate") +} diff --git a/hideout-core/src/main/kotlin/dev/usbharu/hideout/core/domain/event/post/PostEvent.kt b/hideout-core/src/main/kotlin/dev/usbharu/hideout/core/domain/event/post/PostEvent.kt new file mode 100644 index 00000000..52423afd --- /dev/null +++ b/hideout-core/src/main/kotlin/dev/usbharu/hideout/core/domain/event/post/PostEvent.kt @@ -0,0 +1,43 @@ +/* + * Copyright (C) 2024 usbharu + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package dev.usbharu.hideout.core.domain.event.post + +import dev.usbharu.hideout.core.domain.model.actor.Actor +import dev.usbharu.hideout.core.domain.model.post.Post +import dev.usbharu.hideout.core.domain.shared.domainevent.DomainEvent +import dev.usbharu.hideout.core.domain.shared.domainevent.DomainEventBody + +class PostDomainEventFactory(private val post: Post, private val actor: Actor? = null) { + fun createEvent(postEvent: PostEvent): DomainEvent { + return DomainEvent.create( + postEvent.eventName, + PostEventBody(post, actor) + ) + } +} + +class PostEventBody(post: Post, actor: Actor?) : DomainEventBody(mapOf("post" to post, "actor" to actor)) { + fun getPost(): Post = toMap()["post"] as Post + fun getActor(): Actor? = toMap()["actor"] as Actor? +} + +enum class PostEvent(val eventName: String) { + DELETE("PostDelete"), + UPDATE("PostUpdate"), + CREATE("PostCreate"), + CHECK_UPDATE("PostCheckUpdate"), +} diff --git a/hideout-core/src/main/kotlin/dev/usbharu/hideout/core/domain/event/relationship/RelationshipEvent.kt b/hideout-core/src/main/kotlin/dev/usbharu/hideout/core/domain/event/relationship/RelationshipEvent.kt new file mode 100644 index 00000000..0681d9b7 --- /dev/null +++ b/hideout-core/src/main/kotlin/dev/usbharu/hideout/core/domain/event/relationship/RelationshipEvent.kt @@ -0,0 +1,48 @@ +/* + * Copyright (C) 2024 usbharu + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package dev.usbharu.hideout.core.domain.event.relationship + +import dev.usbharu.hideout.core.domain.model.relationship.Relationship +import dev.usbharu.hideout.core.domain.model.support.principal.Anonymous +import dev.usbharu.hideout.core.domain.model.support.principal.Principal +import dev.usbharu.hideout.core.domain.shared.domainevent.DomainEvent +import dev.usbharu.hideout.core.domain.shared.domainevent.DomainEventBody + +class RelationshipEventFactory(private val relationship: Relationship, private val principal: Principal = Anonymous) { + fun createEvent(relationshipEvent: RelationshipEvent): DomainEvent = + DomainEvent.create(relationshipEvent.eventName, RelationshipEventBody(relationship, principal)) +} + +class RelationshipEventBody( + relationship: Relationship, + override val principal: Principal +) : DomainEventBody(mapOf("relationship" to relationship), principal) { + fun getRelationship(): Relationship { + return toMap()["relationship"] as Relationship + } +} + +enum class RelationshipEvent(val eventName: String) { + FOLLOW("RelationshipFollow"), + UNFOLLOW("RelationshipUnfollow"), + BLOCK("RelationshipBlock"), + UNBLOCK("RelationshipUnblock"), + MUTE("RelationshipMute"), + UNMUTE("RelationshipUnmute"), + FOLLOW_REQUEST("RelationshipFollowRequest"), + UNFOLLOW_REQUEST("RelationshipUnfollowRequest"), +} diff --git a/hideout-core/src/main/kotlin/dev/usbharu/hideout/core/domain/event/timeline/TimelineEvent.kt b/hideout-core/src/main/kotlin/dev/usbharu/hideout/core/domain/event/timeline/TimelineEvent.kt new file mode 100644 index 00000000..a6d53cf6 --- /dev/null +++ b/hideout-core/src/main/kotlin/dev/usbharu/hideout/core/domain/event/timeline/TimelineEvent.kt @@ -0,0 +1,16 @@ +package dev.usbharu.hideout.core.domain.event.timeline + +import dev.usbharu.hideout.core.domain.model.timeline.Timeline +import dev.usbharu.hideout.core.domain.shared.domainevent.DomainEvent +import dev.usbharu.hideout.core.domain.shared.domainevent.DomainEventBody + +class TimelineEventFactory(private val timeline: Timeline) { + fun createEvent(timelineEvent: TimelineEvent): DomainEvent = + DomainEvent.create(timelineEvent.eventName, TimelineEventBody(timeline)) +} + +class TimelineEventBody(timeline: Timeline) : DomainEventBody(mapOf("timeline" to timeline)) + +enum class TimelineEvent(val eventName: String) { + CHANGE_VISIBILITY("ChangeVisibility") +} diff --git a/hideout-core/src/main/kotlin/dev/usbharu/hideout/core/domain/exception/HideoutException.kt b/hideout-core/src/main/kotlin/dev/usbharu/hideout/core/domain/exception/HideoutException.kt new file mode 100644 index 00000000..46174918 --- /dev/null +++ b/hideout-core/src/main/kotlin/dev/usbharu/hideout/core/domain/exception/HideoutException.kt @@ -0,0 +1,37 @@ +/* + * Copyright (C) 2024 usbharu + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package dev.usbharu.hideout.core.domain.exception + +import java.io.Serial + +open class HideoutException : 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 = 8506638570017469956L + } +} diff --git a/hideout-core/src/main/kotlin/dev/usbharu/hideout/core/domain/exception/SQLExceptionTranslator.kt b/hideout-core/src/main/kotlin/dev/usbharu/hideout/core/domain/exception/SQLExceptionTranslator.kt new file mode 100644 index 00000000..a86ee9fd --- /dev/null +++ b/hideout-core/src/main/kotlin/dev/usbharu/hideout/core/domain/exception/SQLExceptionTranslator.kt @@ -0,0 +1,23 @@ +/* + * Copyright (C) 2024 usbharu + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package dev.usbharu.hideout.core.domain.exception + +import dev.usbharu.hideout.core.domain.exception.resource.ResourceAccessException + +interface SQLExceptionTranslator { + fun translate(message: String, sql: String? = null, exception: Exception): ResourceAccessException +} diff --git a/hideout-core/src/main/kotlin/dev/usbharu/hideout/core/domain/exception/SpringDataAccessExceptionSQLExceptionTranslator.kt b/hideout-core/src/main/kotlin/dev/usbharu/hideout/core/domain/exception/SpringDataAccessExceptionSQLExceptionTranslator.kt new file mode 100644 index 00000000..163474af --- /dev/null +++ b/hideout-core/src/main/kotlin/dev/usbharu/hideout/core/domain/exception/SpringDataAccessExceptionSQLExceptionTranslator.kt @@ -0,0 +1,35 @@ +/* + * Copyright (C) 2024 usbharu + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package dev.usbharu.hideout.core.domain.exception + +import dev.usbharu.hideout.core.domain.exception.resource.DuplicateException +import dev.usbharu.hideout.core.domain.exception.resource.ResourceAccessException +import org.springframework.dao.DataAccessException +import org.springframework.dao.DuplicateKeyException + +class SpringDataAccessExceptionSQLExceptionTranslator : SQLExceptionTranslator { + override fun translate(message: String, sql: String?, exception: Exception): ResourceAccessException { + if (exception !is DataAccessException) { + throw IllegalArgumentException("exception must be DataAccessException.") + } + + return when (exception) { + is DuplicateKeyException -> DuplicateException(message, exception.rootCause) + else -> ResourceAccessException(message, exception.rootCause) + } + } +} diff --git a/hideout-core/src/main/kotlin/dev/usbharu/hideout/core/domain/exception/resource/DuplicateException.kt b/hideout-core/src/main/kotlin/dev/usbharu/hideout/core/domain/exception/resource/DuplicateException.kt new file mode 100644 index 00000000..7fc70340 --- /dev/null +++ b/hideout-core/src/main/kotlin/dev/usbharu/hideout/core/domain/exception/resource/DuplicateException.kt @@ -0,0 +1,37 @@ +/* + * Copyright (C) 2024 usbharu + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package dev.usbharu.hideout.core.domain.exception.resource + +import java.io.Serial + +class DuplicateException : ResourceAccessException { + 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 = 7092046653037974417L + } +} diff --git a/hideout-core/src/main/kotlin/dev/usbharu/hideout/core/domain/exception/resource/NotFoundException.kt b/hideout-core/src/main/kotlin/dev/usbharu/hideout/core/domain/exception/resource/NotFoundException.kt new file mode 100644 index 00000000..c2d53ef5 --- /dev/null +++ b/hideout-core/src/main/kotlin/dev/usbharu/hideout/core/domain/exception/resource/NotFoundException.kt @@ -0,0 +1,30 @@ +/* + * Copyright (C) 2024 usbharu + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package dev.usbharu.hideout.core.domain.exception.resource + +open class NotFoundException : ResourceAccessException { + 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 + ) +} diff --git a/hideout-core/src/main/kotlin/dev/usbharu/hideout/core/domain/exception/resource/ResourceAccessException.kt b/hideout-core/src/main/kotlin/dev/usbharu/hideout/core/domain/exception/resource/ResourceAccessException.kt new file mode 100644 index 00000000..6f4e979e --- /dev/null +++ b/hideout-core/src/main/kotlin/dev/usbharu/hideout/core/domain/exception/resource/ResourceAccessException.kt @@ -0,0 +1,32 @@ +/* + * Copyright (C) 2024 usbharu + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package dev.usbharu.hideout.core.domain.exception.resource + +import dev.usbharu.hideout.core.domain.exception.HideoutException + +open class ResourceAccessException : HideoutException { + 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 + ) +} diff --git a/hideout-core/src/main/kotlin/dev/usbharu/hideout/core/domain/model/actor/Actor.kt b/hideout-core/src/main/kotlin/dev/usbharu/hideout/core/domain/model/actor/Actor.kt new file mode 100644 index 00000000..3b282c34 --- /dev/null +++ b/hideout-core/src/main/kotlin/dev/usbharu/hideout/core/domain/model/actor/Actor.kt @@ -0,0 +1,138 @@ +/* + * Copyright (C) 2024 usbharu + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package dev.usbharu.hideout.core.domain.model.actor + +import dev.usbharu.hideout.core.domain.event.actor.ActorDomainEventFactory +import dev.usbharu.hideout.core.domain.event.actor.ActorEvent.* +import dev.usbharu.hideout.core.domain.model.emoji.EmojiId +import dev.usbharu.hideout.core.domain.model.instance.InstanceId +import dev.usbharu.hideout.core.domain.model.media.MediaId +import dev.usbharu.hideout.core.domain.model.support.domain.Domain +import dev.usbharu.hideout.core.domain.shared.domainevent.DomainEventStorable +import java.net.URI +import java.time.Instant + +@Suppress("LongParameterList", "ClassOrdering") +class Actor( + val id: ActorId, + val name: ActorName, + val domain: Domain, + screenName: ActorScreenName, + description: ActorDescription, + val inbox: URI, + val outbox: URI, + val url: URI, + val publicKey: ActorPublicKey, + val privateKey: ActorPrivateKey? = null, + val createdAt: Instant, + val keyId: ActorKeyId, + val followersEndpoint: URI?, + val followingEndpoint: URI?, + val instance: InstanceId, + var locked: Boolean, + var followersCount: ActorRelationshipCount?, + var followingCount: ActorRelationshipCount?, + var postsCount: ActorPostsCount, + var lastPostAt: Instant? = null, + suspend: Boolean, + var lastUpdateAt: Instant = createdAt, + alsoKnownAs: Set = emptySet(), + moveTo: ActorId? = null, + emojiIds: Set, + deleted: Boolean, + icon: MediaId?, + banner: MediaId?, +) : DomainEventStorable() { + + var banner = banner + private set + + fun setBannerUrl(banner: MediaId?) { + addDomainEvent(ActorDomainEventFactory(this).createEvent(UPDATE)) + this.banner = banner + } + + var icon = icon + private set + + fun setIconUrl(icon: MediaId?) { + addDomainEvent(ActorDomainEventFactory(this).createEvent(UPDATE)) + this.icon = icon + } + + var suspend = suspend + set(value) { + if (field != value && value) { + addDomainEvent(ActorDomainEventFactory(this).createEvent(ACTOR_SUSPEND)) + } else if (field != value && !value) { + addDomainEvent(ActorDomainEventFactory(this).createEvent(ACTOR_UNSUSPEND)) + } + field = value + } + + var alsoKnownAs = alsoKnownAs + set(value) { + require(value.none { it == id }) + field = value + } + + var moveTo = moveTo + set(value) { + require(value != id) + addDomainEvent(ActorDomainEventFactory(this).createEvent(MOVE)) + field = value + } + + var emojis = emojiIds + private set + + var description = description + set(value) { + addDomainEvent(ActorDomainEventFactory(this).createEvent(UPDATE)) + field = value + } + var screenName = screenName + set(value) { + addDomainEvent(ActorDomainEventFactory(this).createEvent(UPDATE)) + field = value + } + + var deleted = deleted + private set + + fun delete() { + if (deleted.not()) { + addDomainEvent(ActorDomainEventFactory(this).createEvent(DELETE)) + screenName = ActorScreenName.empty + description = ActorDescription.empty + emojis = emptySet() + lastPostAt = null + postsCount = ActorPostsCount.ZERO + followersCount = null + followingCount = null + } + } + + fun restore() { + deleted = false + checkUpdate() + } + + fun checkUpdate() { + addDomainEvent(ActorDomainEventFactory(this).createEvent(CHECK_UPDATE)) + } +} diff --git a/hideout-core/src/main/kotlin/dev/usbharu/hideout/core/domain/model/actor/ActorDescription.kt b/hideout-core/src/main/kotlin/dev/usbharu/hideout/core/domain/model/actor/ActorDescription.kt new file mode 100644 index 00000000..84c60eff --- /dev/null +++ b/hideout-core/src/main/kotlin/dev/usbharu/hideout/core/domain/model/actor/ActorDescription.kt @@ -0,0 +1,26 @@ +/* + * Copyright (C) 2024 usbharu + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package dev.usbharu.hideout.core.domain.model.actor + +class ActorDescription(description: String) { + val description: String = description.take(LENGTH) + + companion object { + const val LENGTH = 10000 + val empty = ActorDescription("") + } +} diff --git a/hideout-core/src/main/kotlin/dev/usbharu/hideout/core/domain/model/actor/ActorId.kt b/hideout-core/src/main/kotlin/dev/usbharu/hideout/core/domain/model/actor/ActorId.kt new file mode 100644 index 00000000..19e295f4 --- /dev/null +++ b/hideout-core/src/main/kotlin/dev/usbharu/hideout/core/domain/model/actor/ActorId.kt @@ -0,0 +1,27 @@ +/* + * Copyright (C) 2024 usbharu + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package dev.usbharu.hideout.core.domain.model.actor + +@JvmInline +value class ActorId(val id: Long) { + init { + require(0 <= id) + } + companion object { + val ghost = ActorId(0L) + } +} diff --git a/hideout-core/src/main/kotlin/dev/usbharu/hideout/core/domain/model/actor/ActorKeyId.kt b/hideout-core/src/main/kotlin/dev/usbharu/hideout/core/domain/model/actor/ActorKeyId.kt new file mode 100644 index 00000000..3bc3e643 --- /dev/null +++ b/hideout-core/src/main/kotlin/dev/usbharu/hideout/core/domain/model/actor/ActorKeyId.kt @@ -0,0 +1,24 @@ +/* + * Copyright (C) 2024 usbharu + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package dev.usbharu.hideout.core.domain.model.actor + +@JvmInline +value class ActorKeyId(val keyId: String) { + init { + require(keyId.isNotBlank()) + } +} diff --git a/hideout-core/src/main/kotlin/dev/usbharu/hideout/core/domain/model/actor/ActorName.kt b/hideout-core/src/main/kotlin/dev/usbharu/hideout/core/domain/model/actor/ActorName.kt new file mode 100644 index 00000000..a2e6697e --- /dev/null +++ b/hideout-core/src/main/kotlin/dev/usbharu/hideout/core/domain/model/actor/ActorName.kt @@ -0,0 +1,31 @@ +/* + * Copyright (C) 2024 usbharu + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package dev.usbharu.hideout.core.domain.model.actor + +@JvmInline +value class ActorName(val name: String) { + init { + require(name.isNotBlank()) + require(name.length <= LENGTH) + require(regex.matches(name)) + } + + companion object { + const val LENGTH = 300 + private val regex = Regex("^[a-zA-Z0-9_-]{1,$LENGTH}\$") + } +} diff --git a/hideout-core/src/main/kotlin/dev/usbharu/hideout/core/domain/model/actor/ActorPostsCount.kt b/hideout-core/src/main/kotlin/dev/usbharu/hideout/core/domain/model/actor/ActorPostsCount.kt new file mode 100644 index 00000000..8c344c05 --- /dev/null +++ b/hideout-core/src/main/kotlin/dev/usbharu/hideout/core/domain/model/actor/ActorPostsCount.kt @@ -0,0 +1,28 @@ +/* + * Copyright (C) 2024 usbharu + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package dev.usbharu.hideout.core.domain.model.actor + +@JvmInline +value class ActorPostsCount(val postsCount: Int) { + init { + require(0 <= this.postsCount) { "Posts count must be greater than 0" } + } + + companion object { + val ZERO = ActorPostsCount(0) + } +} diff --git a/hideout-core/src/main/kotlin/dev/usbharu/hideout/core/domain/model/actor/ActorPrivateKey.kt b/hideout-core/src/main/kotlin/dev/usbharu/hideout/core/domain/model/actor/ActorPrivateKey.kt new file mode 100644 index 00000000..fd683f2b --- /dev/null +++ b/hideout-core/src/main/kotlin/dev/usbharu/hideout/core/domain/model/actor/ActorPrivateKey.kt @@ -0,0 +1,37 @@ +/* + * Copyright (C) 2024 usbharu + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package dev.usbharu.hideout.core.domain.model.actor + +import java.security.PrivateKey +import java.util.* + +@JvmInline +value class ActorPrivateKey(val privateKey: String) { + companion object { + fun create(privateKey: PrivateKey): ActorPrivateKey { + return ActorPrivateKey( + "-----BEGIN PRIVATE KEY-----\n" + + Base64 + .getEncoder() + .encodeToString(privateKey.encoded) + .chunked(64) + .joinToString("\n") + + "\n-----END PRIVATE KEY-----" + ) + } + } +} diff --git a/hideout-core/src/main/kotlin/dev/usbharu/hideout/core/domain/model/actor/ActorPublicKey.kt b/hideout-core/src/main/kotlin/dev/usbharu/hideout/core/domain/model/actor/ActorPublicKey.kt new file mode 100644 index 00000000..f3419d91 --- /dev/null +++ b/hideout-core/src/main/kotlin/dev/usbharu/hideout/core/domain/model/actor/ActorPublicKey.kt @@ -0,0 +1,37 @@ +/* + * Copyright (C) 2024 usbharu + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package dev.usbharu.hideout.core.domain.model.actor + +import java.security.PublicKey +import java.util.* + +@JvmInline +value class ActorPublicKey(val publicKey: String) { + companion object { + fun create(publicKey: PublicKey): ActorPublicKey { + return ActorPublicKey( + "-----BEGIN PUBLIC KEY-----\n" + + Base64 + .getEncoder() + .encodeToString(publicKey.encoded) + .chunked(64) + .joinToString("\n") + + "\n-----END PUBLIC KEY-----" + ) + } + } +} diff --git a/hideout-core/src/main/kotlin/dev/usbharu/hideout/core/domain/model/actor/ActorRelationshipCount.kt b/hideout-core/src/main/kotlin/dev/usbharu/hideout/core/domain/model/actor/ActorRelationshipCount.kt new file mode 100644 index 00000000..e21b75f4 --- /dev/null +++ b/hideout-core/src/main/kotlin/dev/usbharu/hideout/core/domain/model/actor/ActorRelationshipCount.kt @@ -0,0 +1,24 @@ +/* + * Copyright (C) 2024 usbharu + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package dev.usbharu.hideout.core.domain.model.actor + +@JvmInline +value class ActorRelationshipCount(val relationshipCount: Int) { + init { + require(0 <= relationshipCount) { "Followers count must be > 0" } + } +} diff --git a/hideout-core/src/main/kotlin/dev/usbharu/hideout/core/domain/model/actor/ActorRepository.kt b/hideout-core/src/main/kotlin/dev/usbharu/hideout/core/domain/model/actor/ActorRepository.kt new file mode 100644 index 00000000..53c8789c --- /dev/null +++ b/hideout-core/src/main/kotlin/dev/usbharu/hideout/core/domain/model/actor/ActorRepository.kt @@ -0,0 +1,25 @@ +/* + * Copyright (C) 2024 usbharu + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package dev.usbharu.hideout.core.domain.model.actor + +interface ActorRepository { + suspend fun save(actor: Actor): Actor + suspend fun delete(actor: Actor) + suspend fun findById(id: ActorId): Actor? + suspend fun findByNameAndDomain(name: String, domain: String): Actor? + suspend fun findAllById(actorIds: List): List +} diff --git a/hideout-core/src/main/kotlin/dev/usbharu/hideout/core/domain/model/actor/ActorScreenName.kt b/hideout-core/src/main/kotlin/dev/usbharu/hideout/core/domain/model/actor/ActorScreenName.kt new file mode 100644 index 00000000..57866d77 --- /dev/null +++ b/hideout-core/src/main/kotlin/dev/usbharu/hideout/core/domain/model/actor/ActorScreenName.kt @@ -0,0 +1,27 @@ +/* + * Copyright (C) 2024 usbharu + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package dev.usbharu.hideout.core.domain.model.actor + +class ActorScreenName(screenName: String) { + + val screenName: String = screenName.take(LENGTH) + + companion object { + const val LENGTH = 300 + val empty = ActorScreenName("") + } +} diff --git a/hideout-core/src/main/kotlin/dev/usbharu/hideout/core/domain/model/actor/Role.kt b/hideout-core/src/main/kotlin/dev/usbharu/hideout/core/domain/model/actor/Role.kt new file mode 100644 index 00000000..7c697abf --- /dev/null +++ b/hideout-core/src/main/kotlin/dev/usbharu/hideout/core/domain/model/actor/Role.kt @@ -0,0 +1,24 @@ +/* + * Copyright (C) 2024 usbharu + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package dev.usbharu.hideout.core.domain.model.actor + +enum class Role { + LOCAL, + MODERATOR, + ADMINISTRATOR, + REMOTE +} diff --git a/hideout-core/src/main/kotlin/dev/usbharu/hideout/core/domain/model/actorinstancerelationship/ActorInstanceRelationship.kt b/hideout-core/src/main/kotlin/dev/usbharu/hideout/core/domain/model/actorinstancerelationship/ActorInstanceRelationship.kt new file mode 100644 index 00000000..4585ddc0 --- /dev/null +++ b/hideout-core/src/main/kotlin/dev/usbharu/hideout/core/domain/model/actorinstancerelationship/ActorInstanceRelationship.kt @@ -0,0 +1,111 @@ +/* + * Copyright (C) 2024 usbharu + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package dev.usbharu.hideout.core.domain.model.actorinstancerelationship + +import dev.usbharu.hideout.core.domain.event.actorinstancerelationship.ActorInstanceRelationshipDomainEventFactory +import dev.usbharu.hideout.core.domain.event.actorinstancerelationship.ActorInstanceRelationshipEvent.* +import dev.usbharu.hideout.core.domain.model.actor.ActorId +import dev.usbharu.hideout.core.domain.model.instance.InstanceId +import dev.usbharu.hideout.core.domain.shared.domainevent.DomainEventStorable + +class ActorInstanceRelationship( + val actorId: ActorId, + val instanceId: InstanceId, + blocking: Boolean = false, + muting: Boolean = false, + doNotSendPrivate: Boolean = false, +) : DomainEventStorable() { + var doNotSendPrivate = doNotSendPrivate + private set + var muting = muting + private set + var blocking = blocking + private set + + fun block(): ActorInstanceRelationship { + addDomainEvent(ActorInstanceRelationshipDomainEventFactory(this).createEvent(BLOCK)) + blocking = true + return this + } + + fun unblock(): ActorInstanceRelationship { + blocking = false + return this + } + + fun mute(): ActorInstanceRelationship { + addDomainEvent(ActorInstanceRelationshipDomainEventFactory(this).createEvent(MUTE)) + muting = true + return this + } + + fun unmute(): ActorInstanceRelationship { + addDomainEvent(ActorInstanceRelationshipDomainEventFactory(this).createEvent(UNMUTE)) + muting = false + return this + } + + fun doNotSendPrivate(): ActorInstanceRelationship { + doNotSendPrivate = true + return this + } + + fun doSendPrivate(): ActorInstanceRelationship { + doNotSendPrivate = false + return this + } + + override fun equals(other: Any?): Boolean { + if (this === other) return true + if (javaClass != other?.javaClass) return false + + other as ActorInstanceRelationship + + if (actorId != other.actorId) return false + if (instanceId != other.instanceId) return false + + return true + } + + override fun hashCode(): Int { + var result = actorId.hashCode() + result = 31 * result + instanceId.hashCode() + return result + } + + override fun toString(): String { + return "ActorInstanceRelationship(" + + "actorId=$actorId, " + + "instanceId=$instanceId, " + + "blocking=$blocking, " + + "muting=$muting, " + + "doNotSendPrivate=$doNotSendPrivate" + + ")" + } + + companion object { + fun default(actorId: ActorId, instanceId: InstanceId): ActorInstanceRelationship { + return ActorInstanceRelationship( + actorId = actorId, + instanceId = instanceId, + blocking = false, + muting = false, + doNotSendPrivate = false + ) + } + } +} diff --git a/hideout-core/src/main/kotlin/dev/usbharu/hideout/core/domain/model/actorinstancerelationship/ActorInstanceRelationshipRepository.kt b/hideout-core/src/main/kotlin/dev/usbharu/hideout/core/domain/model/actorinstancerelationship/ActorInstanceRelationshipRepository.kt new file mode 100644 index 00000000..ea2bba54 --- /dev/null +++ b/hideout-core/src/main/kotlin/dev/usbharu/hideout/core/domain/model/actorinstancerelationship/ActorInstanceRelationshipRepository.kt @@ -0,0 +1,26 @@ +/* + * Copyright (C) 2024 usbharu + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package dev.usbharu.hideout.core.domain.model.actorinstancerelationship + +import dev.usbharu.hideout.core.domain.model.actor.ActorId +import dev.usbharu.hideout.core.domain.model.instance.InstanceId + +interface ActorInstanceRelationshipRepository { + suspend fun save(actorInstanceRelationship: ActorInstanceRelationship): ActorInstanceRelationship + suspend fun delete(actorInstanceRelationship: ActorInstanceRelationship) + suspend fun findByActorIdAndInstanceId(actorId: ActorId, instanceId: InstanceId): ActorInstanceRelationship? +} diff --git a/hideout-core/src/main/kotlin/dev/usbharu/hideout/core/domain/model/application/Application.kt b/hideout-core/src/main/kotlin/dev/usbharu/hideout/core/domain/model/application/Application.kt new file mode 100644 index 00000000..968bb90e --- /dev/null +++ b/hideout-core/src/main/kotlin/dev/usbharu/hideout/core/domain/model/application/Application.kt @@ -0,0 +1,22 @@ +/* + * Copyright (C) 2024 usbharu + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package dev.usbharu.hideout.core.domain.model.application + +class Application( + val applicationId: ApplicationId, + val name: ApplicationName, +) diff --git a/hideout-core/src/main/kotlin/dev/usbharu/hideout/core/domain/model/application/ApplicationId.kt b/hideout-core/src/main/kotlin/dev/usbharu/hideout/core/domain/model/application/ApplicationId.kt new file mode 100644 index 00000000..41007237 --- /dev/null +++ b/hideout-core/src/main/kotlin/dev/usbharu/hideout/core/domain/model/application/ApplicationId.kt @@ -0,0 +1,24 @@ +/* + * Copyright (C) 2024 usbharu + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package dev.usbharu.hideout.core.domain.model.application + +@JvmInline +value class ApplicationId(val id: Long) { + init { + require(0 <= id) + } +} diff --git a/hideout-core/src/main/kotlin/dev/usbharu/hideout/core/domain/model/application/ApplicationName.kt b/hideout-core/src/main/kotlin/dev/usbharu/hideout/core/domain/model/application/ApplicationName.kt new file mode 100644 index 00000000..37007ecd --- /dev/null +++ b/hideout-core/src/main/kotlin/dev/usbharu/hideout/core/domain/model/application/ApplicationName.kt @@ -0,0 +1,28 @@ +/* + * Copyright (C) 2024 usbharu + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package dev.usbharu.hideout.core.domain.model.application + +@JvmInline +value class ApplicationName(val name: String) { + init { + require(name.length <= LENGTH) + } + + companion object { + const val LENGTH = 300 + } +} diff --git a/hideout-core/src/main/kotlin/dev/usbharu/hideout/core/domain/model/application/ApplicationRepository.kt b/hideout-core/src/main/kotlin/dev/usbharu/hideout/core/domain/model/application/ApplicationRepository.kt new file mode 100644 index 00000000..0150e266 --- /dev/null +++ b/hideout-core/src/main/kotlin/dev/usbharu/hideout/core/domain/model/application/ApplicationRepository.kt @@ -0,0 +1,22 @@ +/* + * Copyright (C) 2024 usbharu + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package dev.usbharu.hideout.core.domain.model.application + +interface ApplicationRepository { + suspend fun save(application: Application): Application + suspend fun delete(application: Application) +} diff --git a/hideout-core/src/main/kotlin/dev/usbharu/hideout/core/domain/model/emoji/CustomEmoji.kt b/hideout-core/src/main/kotlin/dev/usbharu/hideout/core/domain/model/emoji/CustomEmoji.kt new file mode 100644 index 00000000..e0e29b17 --- /dev/null +++ b/hideout-core/src/main/kotlin/dev/usbharu/hideout/core/domain/model/emoji/CustomEmoji.kt @@ -0,0 +1,55 @@ +/* + * Copyright (C) 2024 usbharu + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package dev.usbharu.hideout.core.domain.model.emoji + +import dev.usbharu.hideout.core.domain.model.instance.InstanceId +import dev.usbharu.hideout.core.domain.model.support.domain.Domain +import java.net.URI +import java.time.Instant + +sealed class Emoji { + abstract val domain: Domain + abstract val name: String + + @Suppress("FunctionMinLength") + abstract fun id(): String + override fun toString(): String { + return "Emoji(" + + "domain='$domain', " + + "name='$name'" + + ")" + } +} + +data class CustomEmoji( + val id: EmojiId, + override val name: String, + override val domain: Domain, + val instanceId: InstanceId, + val url: URI, + val category: String?, + val createdAt: Instant, +) : Emoji() { + override fun id(): String = id.toString() +} + +data class UnicodeEmoji( + override val name: String +) : Emoji() { + override val domain: Domain = Domain("unicode.org") + override fun id(): String = name +} diff --git a/hideout-core/src/main/kotlin/dev/usbharu/hideout/core/domain/model/emoji/CustomEmojiRepository.kt b/hideout-core/src/main/kotlin/dev/usbharu/hideout/core/domain/model/emoji/CustomEmojiRepository.kt new file mode 100644 index 00000000..1eb20f2c --- /dev/null +++ b/hideout-core/src/main/kotlin/dev/usbharu/hideout/core/domain/model/emoji/CustomEmojiRepository.kt @@ -0,0 +1,25 @@ +/* + * Copyright (C) 2024 usbharu + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package dev.usbharu.hideout.core.domain.model.emoji + +interface CustomEmojiRepository { + suspend fun save(customEmoji: CustomEmoji): CustomEmoji + suspend fun findById(id: Long): CustomEmoji? + suspend fun delete(customEmoji: CustomEmoji) + suspend fun findByNamesAndDomain(names: List, domain: String): List + suspend fun findByIds(ids: List): List +} diff --git a/hideout-core/src/main/kotlin/dev/usbharu/hideout/core/domain/model/emoji/EmojiId.kt b/hideout-core/src/main/kotlin/dev/usbharu/hideout/core/domain/model/emoji/EmojiId.kt new file mode 100644 index 00000000..5cf4284d --- /dev/null +++ b/hideout-core/src/main/kotlin/dev/usbharu/hideout/core/domain/model/emoji/EmojiId.kt @@ -0,0 +1,24 @@ +/* + * Copyright (C) 2024 usbharu + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package dev.usbharu.hideout.core.domain.model.emoji + +@JvmInline +value class EmojiId(val emojiId: Long) { + init { + require(0 <= emojiId) + } +} diff --git a/hideout-core/src/main/kotlin/dev/usbharu/hideout/core/domain/model/filter/Filter.kt b/hideout-core/src/main/kotlin/dev/usbharu/hideout/core/domain/model/filter/Filter.kt new file mode 100644 index 00000000..9a41fa9e --- /dev/null +++ b/hideout-core/src/main/kotlin/dev/usbharu/hideout/core/domain/model/filter/Filter.kt @@ -0,0 +1,103 @@ +package dev.usbharu.hideout.core.domain.model.filter + +import dev.usbharu.hideout.core.domain.model.filter.Filter.Companion.Action.SET_KEYWORDS +import dev.usbharu.hideout.core.domain.model.filter.FilterMode.* +import dev.usbharu.hideout.core.domain.model.userdetails.UserDetail +import dev.usbharu.hideout.core.domain.model.userdetails.UserDetailId + +class Filter( + val id: FilterId, + val userDetailId: UserDetailId, + var name: FilterName, + val filterContext: Set, + val filterAction: FilterAction, + filterKeywords: Set, +) { + var filterKeywords = filterKeywords + private set + + fun setFilterKeywords(filterKeywords: Set, user: UserDetail) { + require(isAllow(user, SET_KEYWORDS, this)) + this.filterKeywords = filterKeywords + } + + /** + * フィルターを正規表現として表現したものを返します + * + * @return フィルターの正規表現 + */ + fun compileFilter(): Regex { + val words = mutableListOf() + val wholeWords = mutableListOf() + val regexes = mutableListOf() + + for (filterKeyword in filterKeywords) { + when (filterKeyword.mode) { + WHOLE_WORD -> wholeWords.add(filterKeyword.keyword.keyword) + REGEX -> regexes.add(filterKeyword.keyword.keyword) + NONE -> words.add(filterKeyword.keyword.keyword) + } + } + val wholeWordsRegex = wholeWords.takeIf { it.isNotEmpty() }?.joinToString("|", "\\b(", ")\\b") + val noneWordsRegex = words.takeIf { it.isNotEmpty() }?.joinToString("|", "(", ")") + val regex = regexes.takeIf { it.isNotEmpty() }?.joinToString("|", "(", ")") + + return listOfNotNull(wholeWordsRegex, noneWordsRegex, regex).joinToString("|").toRegex() + } + + fun reconstructWith(filterKeywords: Set): Filter { + return Filter( + id = this.id, + userDetailId = this.userDetailId, + name = this.name, + filterContext = this.filterContext, + filterAction = this.filterAction, + filterKeywords = filterKeywords + ) + } + + override fun equals(other: Any?): Boolean { + if (this === other) return true + if (javaClass != other?.javaClass) return false + + other as Filter + + return id == other.id + } + + override fun hashCode(): Int { + return id.hashCode() + } + + + companion object { + fun isAllow(user: UserDetail, action: Action, resource: Filter): Boolean { + return when (action) { + SET_KEYWORDS -> resource.userDetailId == user.id + } + } + + enum class Action { + SET_KEYWORDS + } + + @Suppress("LongParameterList") + fun create( + id: FilterId, + userDetailId: UserDetailId, + name: FilterName, + filterContext: Set, + filterAction: FilterAction, + filterKeywords: Set, + ): Filter { + return Filter( + id, + userDetailId, + name, + filterContext, + filterAction, + filterKeywords + ) + } + } +} diff --git a/hideout-core/src/main/kotlin/dev/usbharu/hideout/core/domain/model/filter/FilterAction.kt b/hideout-core/src/main/kotlin/dev/usbharu/hideout/core/domain/model/filter/FilterAction.kt new file mode 100644 index 00000000..d19c8db8 --- /dev/null +++ b/hideout-core/src/main/kotlin/dev/usbharu/hideout/core/domain/model/filter/FilterAction.kt @@ -0,0 +1,6 @@ +package dev.usbharu.hideout.core.domain.model.filter + +enum class FilterAction { + WARN, + HIDE +} diff --git a/hideout-core/src/main/kotlin/dev/usbharu/hideout/core/domain/model/filter/FilterContext.kt b/hideout-core/src/main/kotlin/dev/usbharu/hideout/core/domain/model/filter/FilterContext.kt new file mode 100644 index 00000000..df987e3d --- /dev/null +++ b/hideout-core/src/main/kotlin/dev/usbharu/hideout/core/domain/model/filter/FilterContext.kt @@ -0,0 +1,9 @@ +package dev.usbharu.hideout.core.domain.model.filter + +enum class FilterContext { + HOME, + NOTIFICATION, + PUBLIC, + THREAD, + ACCOUNT +} diff --git a/hideout-core/src/main/kotlin/dev/usbharu/hideout/core/domain/model/filter/FilterId.kt b/hideout-core/src/main/kotlin/dev/usbharu/hideout/core/domain/model/filter/FilterId.kt new file mode 100644 index 00000000..a9ee5ce4 --- /dev/null +++ b/hideout-core/src/main/kotlin/dev/usbharu/hideout/core/domain/model/filter/FilterId.kt @@ -0,0 +1,8 @@ +package dev.usbharu.hideout.core.domain.model.filter + +@JvmInline +value class FilterId(val id: Long) { + init { + require(0 <= id) + } +} diff --git a/hideout-core/src/main/kotlin/dev/usbharu/hideout/core/domain/model/filter/FilterKeyword.kt b/hideout-core/src/main/kotlin/dev/usbharu/hideout/core/domain/model/filter/FilterKeyword.kt new file mode 100644 index 00000000..333eb8ae --- /dev/null +++ b/hideout-core/src/main/kotlin/dev/usbharu/hideout/core/domain/model/filter/FilterKeyword.kt @@ -0,0 +1,7 @@ +package dev.usbharu.hideout.core.domain.model.filter + +class FilterKeyword( + val id: FilterKeywordId, + var keyword: FilterKeywordKeyword, + val mode: FilterMode +) diff --git a/hideout-core/src/main/kotlin/dev/usbharu/hideout/core/domain/model/filter/FilterKeywordId.kt b/hideout-core/src/main/kotlin/dev/usbharu/hideout/core/domain/model/filter/FilterKeywordId.kt new file mode 100644 index 00000000..4f1b2df5 --- /dev/null +++ b/hideout-core/src/main/kotlin/dev/usbharu/hideout/core/domain/model/filter/FilterKeywordId.kt @@ -0,0 +1,4 @@ +package dev.usbharu.hideout.core.domain.model.filter + +@JvmInline +value class FilterKeywordId(val id: Long) diff --git a/hideout-core/src/main/kotlin/dev/usbharu/hideout/core/domain/model/filter/FilterKeywordKeyword.kt b/hideout-core/src/main/kotlin/dev/usbharu/hideout/core/domain/model/filter/FilterKeywordKeyword.kt new file mode 100644 index 00000000..89e959b3 --- /dev/null +++ b/hideout-core/src/main/kotlin/dev/usbharu/hideout/core/domain/model/filter/FilterKeywordKeyword.kt @@ -0,0 +1,4 @@ +package dev.usbharu.hideout.core.domain.model.filter + +@JvmInline +value class FilterKeywordKeyword(val keyword: String) diff --git a/hideout-core/src/main/kotlin/dev/usbharu/hideout/core/domain/model/filter/FilterMode.kt b/hideout-core/src/main/kotlin/dev/usbharu/hideout/core/domain/model/filter/FilterMode.kt new file mode 100644 index 00000000..57e38fb7 --- /dev/null +++ b/hideout-core/src/main/kotlin/dev/usbharu/hideout/core/domain/model/filter/FilterMode.kt @@ -0,0 +1,7 @@ +package dev.usbharu.hideout.core.domain.model.filter + +enum class FilterMode { + WHOLE_WORD, + REGEX, + NONE +} diff --git a/hideout-core/src/main/kotlin/dev/usbharu/hideout/core/domain/model/filter/FilterName.kt b/hideout-core/src/main/kotlin/dev/usbharu/hideout/core/domain/model/filter/FilterName.kt new file mode 100644 index 00000000..bd6596f3 --- /dev/null +++ b/hideout-core/src/main/kotlin/dev/usbharu/hideout/core/domain/model/filter/FilterName.kt @@ -0,0 +1,10 @@ +package dev.usbharu.hideout.core.domain.model.filter + +class FilterName(name: String) { + + val name = name.take(LENGTH) + + companion object { + const val LENGTH = 300 + } +} diff --git a/hideout-core/src/main/kotlin/dev/usbharu/hideout/core/domain/model/filter/FilterRepository.kt b/hideout-core/src/main/kotlin/dev/usbharu/hideout/core/domain/model/filter/FilterRepository.kt new file mode 100644 index 00000000..fe65b133 --- /dev/null +++ b/hideout-core/src/main/kotlin/dev/usbharu/hideout/core/domain/model/filter/FilterRepository.kt @@ -0,0 +1,13 @@ +package dev.usbharu.hideout.core.domain.model.filter + +import dev.usbharu.hideout.core.domain.model.userdetails.UserDetailId + +interface FilterRepository { + suspend fun save(filter: Filter): Filter + suspend fun delete(filter: Filter) + + suspend fun findByFilterKeywordId(filterKeywordId: FilterKeywordId): Filter? + suspend fun findByFilterId(filterId: FilterId): Filter? + + suspend fun findByUserDetailId(userDetailId: UserDetailId): List +} diff --git a/hideout-core/src/main/kotlin/dev/usbharu/hideout/core/domain/model/filter/FilterResult.kt b/hideout-core/src/main/kotlin/dev/usbharu/hideout/core/domain/model/filter/FilterResult.kt new file mode 100644 index 00000000..478f05e5 --- /dev/null +++ b/hideout-core/src/main/kotlin/dev/usbharu/hideout/core/domain/model/filter/FilterResult.kt @@ -0,0 +1,3 @@ +package dev.usbharu.hideout.core.domain.model.filter + +class FilterResult(val filter: Filter, val matchedKeyword: String) diff --git a/hideout-core/src/main/kotlin/dev/usbharu/hideout/core/domain/model/filter/FilteredPost.kt b/hideout-core/src/main/kotlin/dev/usbharu/hideout/core/domain/model/filter/FilteredPost.kt new file mode 100644 index 00000000..80226401 --- /dev/null +++ b/hideout-core/src/main/kotlin/dev/usbharu/hideout/core/domain/model/filter/FilteredPost.kt @@ -0,0 +1,5 @@ +package dev.usbharu.hideout.core.domain.model.filter + +import dev.usbharu.hideout.core.domain.model.post.Post + +class FilteredPost(val post: Post, val filterResults: List) diff --git a/hideout-core/src/main/kotlin/dev/usbharu/hideout/core/domain/model/followtimeline/FollowTimeline.kt b/hideout-core/src/main/kotlin/dev/usbharu/hideout/core/domain/model/followtimeline/FollowTimeline.kt new file mode 100644 index 00000000..c5340618 --- /dev/null +++ b/hideout-core/src/main/kotlin/dev/usbharu/hideout/core/domain/model/followtimeline/FollowTimeline.kt @@ -0,0 +1,6 @@ +package dev.usbharu.hideout.core.domain.model.followtimeline + +import dev.usbharu.hideout.core.domain.model.timeline.TimelineId +import dev.usbharu.hideout.core.domain.model.userdetails.UserDetailId + +class FollowTimeline(val userDetailId: UserDetailId, val timelineId: TimelineId) diff --git a/hideout-core/src/main/kotlin/dev/usbharu/hideout/core/domain/model/followtimeline/FollowTimelineRepository.kt b/hideout-core/src/main/kotlin/dev/usbharu/hideout/core/domain/model/followtimeline/FollowTimelineRepository.kt new file mode 100644 index 00000000..9c7bc104 --- /dev/null +++ b/hideout-core/src/main/kotlin/dev/usbharu/hideout/core/domain/model/followtimeline/FollowTimelineRepository.kt @@ -0,0 +1,6 @@ +package dev.usbharu.hideout.core.domain.model.followtimeline + +interface FollowTimelineRepository { + suspend fun save(followTimeline: FollowTimeline): FollowTimeline + suspend fun delete(followTimeline: FollowTimeline) +} diff --git a/hideout-core/src/main/kotlin/dev/usbharu/hideout/core/domain/model/instance/Instance.kt b/hideout-core/src/main/kotlin/dev/usbharu/hideout/core/domain/model/instance/Instance.kt new file mode 100644 index 00000000..2d3a6dbd --- /dev/null +++ b/hideout-core/src/main/kotlin/dev/usbharu/hideout/core/domain/model/instance/Instance.kt @@ -0,0 +1,57 @@ +/* + * Copyright (C) 2024 usbharu + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package dev.usbharu.hideout.core.domain.model.instance + +import dev.usbharu.hideout.core.domain.event.instance.InstanceEvent +import dev.usbharu.hideout.core.domain.event.instance.InstanceEventFactory +import dev.usbharu.hideout.core.domain.shared.domainevent.DomainEventStorable +import java.net.URI +import java.time.Instant + +@Suppress("LongParameterList") +class Instance( + val id: InstanceId, + var name: InstanceName, + var description: InstanceDescription, + val url: URI, + iconUrl: URI, + var sharedInbox: URI?, + var software: InstanceSoftware, + var version: InstanceVersion, + var isBlocked: Boolean, + var isMuted: Boolean, + var moderationNote: InstanceModerationNote, + val createdAt: Instant, +) : DomainEventStorable() { + + var iconUrl = iconUrl + set(value) { + addDomainEvent(InstanceEventFactory(this).createEvent(InstanceEvent.UPDATE)) + field = value + } + + override fun equals(other: Any?): Boolean { + if (this === other) return true + if (javaClass != other?.javaClass) return false + + other as Instance + + return id == other.id + } + + override fun hashCode(): Int = id.hashCode() +} diff --git a/hideout-core/src/main/kotlin/dev/usbharu/hideout/core/domain/model/instance/InstanceDescription.kt b/hideout-core/src/main/kotlin/dev/usbharu/hideout/core/domain/model/instance/InstanceDescription.kt new file mode 100644 index 00000000..8a6f2084 --- /dev/null +++ b/hideout-core/src/main/kotlin/dev/usbharu/hideout/core/domain/model/instance/InstanceDescription.kt @@ -0,0 +1,20 @@ +/* + * Copyright (C) 2024 usbharu + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package dev.usbharu.hideout.core.domain.model.instance + +@JvmInline +value class InstanceDescription(val description: String) diff --git a/hideout-core/src/main/kotlin/dev/usbharu/hideout/core/domain/model/instance/InstanceId.kt b/hideout-core/src/main/kotlin/dev/usbharu/hideout/core/domain/model/instance/InstanceId.kt new file mode 100644 index 00000000..66ed056b --- /dev/null +++ b/hideout-core/src/main/kotlin/dev/usbharu/hideout/core/domain/model/instance/InstanceId.kt @@ -0,0 +1,20 @@ +/* + * Copyright (C) 2024 usbharu + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package dev.usbharu.hideout.core.domain.model.instance + +@JvmInline +value class InstanceId(val instanceId: Long) diff --git a/hideout-core/src/main/kotlin/dev/usbharu/hideout/core/domain/model/instance/InstanceModerationNote.kt b/hideout-core/src/main/kotlin/dev/usbharu/hideout/core/domain/model/instance/InstanceModerationNote.kt new file mode 100644 index 00000000..6439f75c --- /dev/null +++ b/hideout-core/src/main/kotlin/dev/usbharu/hideout/core/domain/model/instance/InstanceModerationNote.kt @@ -0,0 +1,20 @@ +/* + * Copyright (C) 2024 usbharu + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package dev.usbharu.hideout.core.domain.model.instance + +@JvmInline +value class InstanceModerationNote(val note: String) diff --git a/hideout-core/src/main/kotlin/dev/usbharu/hideout/core/domain/model/instance/InstanceName.kt b/hideout-core/src/main/kotlin/dev/usbharu/hideout/core/domain/model/instance/InstanceName.kt new file mode 100644 index 00000000..5133566e --- /dev/null +++ b/hideout-core/src/main/kotlin/dev/usbharu/hideout/core/domain/model/instance/InstanceName.kt @@ -0,0 +1,20 @@ +/* + * Copyright (C) 2024 usbharu + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package dev.usbharu.hideout.core.domain.model.instance + +@JvmInline +value class InstanceName(val name: String) diff --git a/hideout-core/src/main/kotlin/dev/usbharu/hideout/core/domain/model/instance/InstanceRepository.kt b/hideout-core/src/main/kotlin/dev/usbharu/hideout/core/domain/model/instance/InstanceRepository.kt new file mode 100644 index 00000000..5446d3bd --- /dev/null +++ b/hideout-core/src/main/kotlin/dev/usbharu/hideout/core/domain/model/instance/InstanceRepository.kt @@ -0,0 +1,26 @@ +/* + * Copyright (C) 2024 usbharu + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package dev.usbharu.hideout.core.domain.model.instance + +import java.net.URI + +interface InstanceRepository { + suspend fun save(instance: Instance): Instance + suspend fun findById(id: InstanceId): Instance? + suspend fun delete(instance: Instance) + suspend fun findByUrl(url: URI): Instance? +} diff --git a/hideout-core/src/main/kotlin/dev/usbharu/hideout/core/domain/model/instance/InstanceSoftware.kt b/hideout-core/src/main/kotlin/dev/usbharu/hideout/core/domain/model/instance/InstanceSoftware.kt new file mode 100644 index 00000000..30d06746 --- /dev/null +++ b/hideout-core/src/main/kotlin/dev/usbharu/hideout/core/domain/model/instance/InstanceSoftware.kt @@ -0,0 +1,20 @@ +/* + * Copyright (C) 2024 usbharu + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package dev.usbharu.hideout.core.domain.model.instance + +@JvmInline +value class InstanceSoftware(val software: String) diff --git a/hideout-core/src/main/kotlin/dev/usbharu/hideout/core/domain/model/instance/InstanceVersion.kt b/hideout-core/src/main/kotlin/dev/usbharu/hideout/core/domain/model/instance/InstanceVersion.kt new file mode 100644 index 00000000..b8770133 --- /dev/null +++ b/hideout-core/src/main/kotlin/dev/usbharu/hideout/core/domain/model/instance/InstanceVersion.kt @@ -0,0 +1,20 @@ +/* + * Copyright (C) 2024 usbharu + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package dev.usbharu.hideout.core.domain.model.instance + +@JvmInline +value class InstanceVersion(val version: String) diff --git a/hideout-core/src/main/kotlin/dev/usbharu/hideout/core/domain/model/media/FileType.kt b/hideout-core/src/main/kotlin/dev/usbharu/hideout/core/domain/model/media/FileType.kt new file mode 100644 index 00000000..9d2b8837 --- /dev/null +++ b/hideout-core/src/main/kotlin/dev/usbharu/hideout/core/domain/model/media/FileType.kt @@ -0,0 +1,24 @@ +/* + * Copyright (C) 2024 usbharu + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package dev.usbharu.hideout.core.domain.model.media + +enum class FileType { + Image, + Video, + Audio, + Unknown +} diff --git a/hideout-core/src/main/kotlin/dev/usbharu/hideout/core/domain/model/media/Media.kt b/hideout-core/src/main/kotlin/dev/usbharu/hideout/core/domain/model/media/Media.kt new file mode 100644 index 00000000..44c75088 --- /dev/null +++ b/hideout-core/src/main/kotlin/dev/usbharu/hideout/core/domain/model/media/Media.kt @@ -0,0 +1,51 @@ +/* + * Copyright (C) 2024 usbharu + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package dev.usbharu.hideout.core.domain.model.media + +import java.net.URI + +class Media( + val id: MediaId, + val name: MediaName, + url: URI, + val remoteUrl: URI?, + val thumbnailUrl: URI?, + val type: FileType, + val mimeType: MimeType, + val blurHash: MediaBlurHash?, + val description: MediaDescription? = null, +) { + var url = url + private set + + fun setUrl(url: URI) { + this.url = url + } + override fun toString(): String { + return "Media(" + + "id=$id, " + + "name=$name, " + + "url=$url, " + + "remoteUrl=$remoteUrl, " + + "thumbnailUrl=$thumbnailUrl, " + + "type=$type, " + + "mimeType=$mimeType, " + + "blurHash=$blurHash, " + + "description=$description" + + ")" + } +} diff --git a/hideout-core/src/main/kotlin/dev/usbharu/hideout/core/domain/model/media/MediaBlurHash.kt b/hideout-core/src/main/kotlin/dev/usbharu/hideout/core/domain/model/media/MediaBlurHash.kt new file mode 100644 index 00000000..1b1dd054 --- /dev/null +++ b/hideout-core/src/main/kotlin/dev/usbharu/hideout/core/domain/model/media/MediaBlurHash.kt @@ -0,0 +1,20 @@ +/* + * Copyright (C) 2024 usbharu + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package dev.usbharu.hideout.core.domain.model.media + +@JvmInline +value class MediaBlurHash(val hash: String) diff --git a/hideout-core/src/main/kotlin/dev/usbharu/hideout/core/domain/model/media/MediaDescription.kt b/hideout-core/src/main/kotlin/dev/usbharu/hideout/core/domain/model/media/MediaDescription.kt new file mode 100644 index 00000000..c99345b0 --- /dev/null +++ b/hideout-core/src/main/kotlin/dev/usbharu/hideout/core/domain/model/media/MediaDescription.kt @@ -0,0 +1,20 @@ +/* + * Copyright (C) 2024 usbharu + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package dev.usbharu.hideout.core.domain.model.media + +@JvmInline +value class MediaDescription(val description: String) diff --git a/hideout-core/src/main/kotlin/dev/usbharu/hideout/core/domain/model/media/MediaId.kt b/hideout-core/src/main/kotlin/dev/usbharu/hideout/core/domain/model/media/MediaId.kt new file mode 100644 index 00000000..5003f164 --- /dev/null +++ b/hideout-core/src/main/kotlin/dev/usbharu/hideout/core/domain/model/media/MediaId.kt @@ -0,0 +1,20 @@ +/* + * Copyright (C) 2024 usbharu + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package dev.usbharu.hideout.core.domain.model.media + +@JvmInline +value class MediaId(val id: Long) diff --git a/hideout-core/src/main/kotlin/dev/usbharu/hideout/core/domain/model/media/MediaName.kt b/hideout-core/src/main/kotlin/dev/usbharu/hideout/core/domain/model/media/MediaName.kt new file mode 100644 index 00000000..58c483ac --- /dev/null +++ b/hideout-core/src/main/kotlin/dev/usbharu/hideout/core/domain/model/media/MediaName.kt @@ -0,0 +1,20 @@ +/* + * Copyright (C) 2024 usbharu + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package dev.usbharu.hideout.core.domain.model.media + +@JvmInline +value class MediaName(val name: String) diff --git a/hideout-core/src/main/kotlin/dev/usbharu/hideout/core/domain/model/media/MediaRepository.kt b/hideout-core/src/main/kotlin/dev/usbharu/hideout/core/domain/model/media/MediaRepository.kt new file mode 100644 index 00000000..cc5321b8 --- /dev/null +++ b/hideout-core/src/main/kotlin/dev/usbharu/hideout/core/domain/model/media/MediaRepository.kt @@ -0,0 +1,23 @@ +/* + * Copyright (C) 2024 usbharu + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package dev.usbharu.hideout.core.domain.model.media + +interface MediaRepository { + suspend fun save(media: Media): Media + suspend fun findById(id: MediaId): Media? + suspend fun delete(media: Media) +} diff --git a/hideout-core/src/main/kotlin/dev/usbharu/hideout/core/domain/model/media/MimeType.kt b/hideout-core/src/main/kotlin/dev/usbharu/hideout/core/domain/model/media/MimeType.kt new file mode 100644 index 00000000..95cdcd99 --- /dev/null +++ b/hideout-core/src/main/kotlin/dev/usbharu/hideout/core/domain/model/media/MimeType.kt @@ -0,0 +1,19 @@ +/* + * Copyright (C) 2024 usbharu + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package dev.usbharu.hideout.core.domain.model.media + +data class MimeType(val type: String, val subtype: String, val fileType: FileType) diff --git a/hideout-core/src/main/kotlin/dev/usbharu/hideout/core/domain/model/post/Post.kt b/hideout-core/src/main/kotlin/dev/usbharu/hideout/core/domain/model/post/Post.kt new file mode 100644 index 00000000..d7d3f8e3 --- /dev/null +++ b/hideout-core/src/main/kotlin/dev/usbharu/hideout/core/domain/model/post/Post.kt @@ -0,0 +1,318 @@ +/* + * Copyright (C) 2024 usbharu + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package dev.usbharu.hideout.core.domain.model.post + +import dev.usbharu.hideout.core.domain.event.post.PostDomainEventFactory +import dev.usbharu.hideout.core.domain.event.post.PostEvent +import dev.usbharu.hideout.core.domain.model.actor.Actor +import dev.usbharu.hideout.core.domain.model.actor.ActorId +import dev.usbharu.hideout.core.domain.model.emoji.EmojiId +import dev.usbharu.hideout.core.domain.model.instance.InstanceId +import dev.usbharu.hideout.core.domain.model.media.MediaId +import dev.usbharu.hideout.core.domain.shared.domainevent.DomainEventStorable +import java.net.URI +import java.time.Instant + +@Suppress("LongParameterList", "TooManyFunctions", "ClassOrdering") +class Post( + val id: PostId, + actorId: ActorId, + val instanceId: InstanceId, + overview: PostOverview?, + content: PostContent, + val createdAt: Instant, + visibility: Visibility, + val url: URI, + val repostId: PostId?, + val replyId: PostId?, + sensitive: Boolean, + val apId: URI, + deleted: Boolean, + mediaIds: List, + visibleActors: Set, + hide: Boolean, + moveTo: PostId?, +) : DomainEventStorable() { + + val actorId = actorId + get() { + if (deleted) { + return ActorId.ghost + } + return field + } + + var visibility = visibility + private set + + fun setVisibility(visibility: Visibility, actor: Actor) { + require(this.visibility != Visibility.DIRECT) + require(visibility != Visibility.DIRECT) + require(this.visibility.ordinal >= visibility.ordinal) + + require(deleted.not()) + + if (this.visibility != visibility) { + addDomainEvent(PostDomainEventFactory(this, actor).createEvent(PostEvent.UPDATE)) + } + this.visibility = visibility + } + + var visibleActors = visibleActors + private set + + fun setVisibleActors(visibleActors: Set, actor: Actor) { + require(deleted.not()) + if (visibility == Visibility.DIRECT) { + addDomainEvent(PostDomainEventFactory(this, actor).createEvent(PostEvent.UPDATE)) + this.visibleActors = this.visibleActors.plus(visibleActors) + } + } + + var content = content + get() { + if (hide) { + return PostContent.empty + } + return field + } + private set + + fun setContent(content: PostContent, actor: Actor) { + require(deleted.not()) + if (this.content != content) { + addDomainEvent(PostDomainEventFactory(this, actor).createEvent(PostEvent.UPDATE)) + } + this.content = content + } + + var overview = overview + get() { + if (hide) { + return null + } + return field + } + private set + + fun setOverview(overview: PostOverview?, actor: Actor) { + require(deleted.not()) + if (this.overview != overview) { + addDomainEvent(PostDomainEventFactory(this, actor).createEvent(PostEvent.UPDATE)) + } + this.overview = overview + } + + var sensitive = sensitive + private set + + fun setSensitive(sensitive: Boolean, actor: Actor) { + require(deleted.not()) + if (this.sensitive != sensitive) { + addDomainEvent(PostDomainEventFactory(this, actor).createEvent(PostEvent.UPDATE)) + } + this.sensitive = sensitive + } + + val text: String + get() { + if (hide) { + return PostContent.empty.text + } + return content.text + } + + val emojiIds: List + get() { + if (hide) { + return PostContent.empty.emojiIds + } + return content.emojiIds + } + + var mediaIds = mediaIds + get() { + if (hide) { + return emptyList() + } + return field + } + private set + + fun addMediaIds(mediaIds: List, actor: Actor) { + require(deleted.not()) + addDomainEvent(PostDomainEventFactory(this, actor).createEvent(PostEvent.UPDATE)) + this.mediaIds = this.mediaIds.plus(mediaIds).distinct() + } + + var deleted = deleted + private set + + fun delete(actor: Actor) { + if (deleted.not()) { + addDomainEvent(PostDomainEventFactory(this, actor).createEvent(PostEvent.DELETE)) + content = PostContent.empty + overview = null + mediaIds = emptyList() + } + deleted = true + } + + fun checkUpdate() { + addDomainEvent(PostDomainEventFactory(this).createEvent(PostEvent.CHECK_UPDATE)) + } + + fun restore(content: PostContent, overview: PostOverview?, mediaIds: List) { + require(deleted) + deleted = false + this.content = content + this.overview = overview + this.mediaIds = mediaIds + checkUpdate() + } + + var hide = hide + private set + + fun hide() { + hide = true + } + + fun show() { + hide = false + } + + var moveTo = moveTo + private set + + fun moveTo(moveTo: PostId, actor: Actor) { + require(this.moveTo == null) + this.moveTo = moveTo + } + + override fun equals(other: Any?): Boolean { + if (this === other) return true + if (javaClass != other?.javaClass) return false + + other as Post + + return id == other.id + } + + override fun hashCode(): Int = id.hashCode() + + fun reconstructWith(mediaIds: List, emojis: List, visibleActors: Set): Post { + return Post( + id = id, + actorId = actorId, + instanceId = instanceId, + overview = overview, + content = PostContent(this.content.text, this.content.content, emojis), + createdAt = createdAt, + visibility = visibility, + url = url, + repostId = repostId, + replyId = replyId, + sensitive = sensitive, + apId = apId, + deleted = deleted, + mediaIds = mediaIds, + visibleActors = visibleActors, + hide = hide, + moveTo = moveTo + ) + } + + override fun toString(): String { + return "Post(" + + "id=$id, " + + "createdAt=$createdAt, " + + "url=$url, " + + "repostId=$repostId, " + + "replyId=$replyId, " + + "apId=$apId, " + + "actorId=$actorId, " + + "visibility=$visibility, " + + "visibleActors=$visibleActors, " + + "content=$content, " + + "overview=$overview, " + + "sensitive=$sensitive, " + + "text='$text', " + + "emojiIds=$emojiIds, " + + "mediaIds=$mediaIds, " + + "deleted=$deleted, " + + "hide=$hide, " + + "moveTo=$moveTo" + + ")" + } + + companion object { + @Suppress("LongParameterList") + fun create( + id: PostId, + actorId: ActorId, + instanceId: InstanceId, + overview: PostOverview? = null, + content: PostContent, + createdAt: Instant, + visibility: Visibility, + url: URI, + repostId: PostId?, + replyId: PostId?, + sensitive: Boolean, + apId: URI, + deleted: Boolean, + mediaIds: List, + visibleActors: Set = emptySet(), + hide: Boolean = false, + moveTo: PostId? = null, + actor: Actor, + ): Post { + require(actor.deleted.not()) + require(actor.moveTo == null) + + val visibility1 = if (actor.suspend && visibility == Visibility.PUBLIC) { + Visibility.UNLISTED + } else { + visibility + } + + val post = Post( + id = id, + actorId = actorId, + instanceId = instanceId, + overview = overview, + content = content, + createdAt = createdAt, + visibility = visibility1, + url = url, + repostId = repostId, + replyId = replyId, + sensitive = sensitive, + apId = apId, + deleted = deleted, + mediaIds = mediaIds, + visibleActors = visibleActors, + hide = hide, + moveTo = moveTo + ) + post.addDomainEvent(PostDomainEventFactory(post).createEvent(PostEvent.CREATE)) + return post + } + + } +} diff --git a/hideout-core/src/main/kotlin/dev/usbharu/hideout/core/domain/model/post/PostContent.kt b/hideout-core/src/main/kotlin/dev/usbharu/hideout/core/domain/model/post/PostContent.kt new file mode 100644 index 00000000..2d787e75 --- /dev/null +++ b/hideout-core/src/main/kotlin/dev/usbharu/hideout/core/domain/model/post/PostContent.kt @@ -0,0 +1,28 @@ +/* + * Copyright (C) 2024 usbharu + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package dev.usbharu.hideout.core.domain.model.post + +import dev.usbharu.hideout.core.domain.model.emoji.EmojiId + +data class PostContent(val text: String, val content: String, val emojiIds: List) { + + companion object { + val empty = PostContent("", "", emptyList()) + const val CONTENT_LENGTH = 5000 + const val TEXT_LENGTH = 3000 + } +} diff --git a/hideout-core/src/main/kotlin/dev/usbharu/hideout/core/domain/model/post/PostId.kt b/hideout-core/src/main/kotlin/dev/usbharu/hideout/core/domain/model/post/PostId.kt new file mode 100644 index 00000000..e4682ff1 --- /dev/null +++ b/hideout-core/src/main/kotlin/dev/usbharu/hideout/core/domain/model/post/PostId.kt @@ -0,0 +1,20 @@ +/* + * Copyright (C) 2024 usbharu + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package dev.usbharu.hideout.core.domain.model.post + +@JvmInline +value class PostId(val id: Long) diff --git a/hideout-core/src/main/kotlin/dev/usbharu/hideout/core/domain/model/post/PostOverview.kt b/hideout-core/src/main/kotlin/dev/usbharu/hideout/core/domain/model/post/PostOverview.kt new file mode 100644 index 00000000..0793febe --- /dev/null +++ b/hideout-core/src/main/kotlin/dev/usbharu/hideout/core/domain/model/post/PostOverview.kt @@ -0,0 +1,24 @@ +/* + * Copyright (C) 2024 usbharu + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package dev.usbharu.hideout.core.domain.model.post + +@JvmInline +value class PostOverview(val overview: String) { + companion object { + const val LENGTH = 100 + } +} diff --git a/hideout-core/src/main/kotlin/dev/usbharu/hideout/core/domain/model/post/PostRepository.kt b/hideout-core/src/main/kotlin/dev/usbharu/hideout/core/domain/model/post/PostRepository.kt new file mode 100644 index 00000000..674bf8bc --- /dev/null +++ b/hideout-core/src/main/kotlin/dev/usbharu/hideout/core/domain/model/post/PostRepository.kt @@ -0,0 +1,35 @@ +/* + * Copyright (C) 2024 usbharu + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package dev.usbharu.hideout.core.domain.model.post + +import dev.usbharu.hideout.core.domain.model.actor.ActorId +import dev.usbharu.hideout.core.domain.model.support.page.Page +import dev.usbharu.hideout.core.domain.model.support.page.PaginationList + +interface PostRepository { + suspend fun save(post: Post): Post + suspend fun saveAll(posts: List): List + suspend fun findById(id: PostId): Post? + suspend fun findAllById(ids: List): List + suspend fun findByActorId(id: ActorId, page: Page? = null): PaginationList + suspend fun delete(post: Post) + suspend fun findByActorIdAndVisibilityInList( + actorId: ActorId, + visibilityList: List, + of: Page? = null + ): PaginationList +} diff --git a/hideout-core/src/main/kotlin/dev/usbharu/hideout/core/domain/model/post/Visibility.kt b/hideout-core/src/main/kotlin/dev/usbharu/hideout/core/domain/model/post/Visibility.kt new file mode 100644 index 00000000..70acb6d6 --- /dev/null +++ b/hideout-core/src/main/kotlin/dev/usbharu/hideout/core/domain/model/post/Visibility.kt @@ -0,0 +1,24 @@ +/* + * Copyright (C) 2024 usbharu + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package dev.usbharu.hideout.core.domain.model.post + +enum class Visibility { + PUBLIC, + UNLISTED, + FOLLOWERS, + DIRECT +} diff --git a/hideout-core/src/main/kotlin/dev/usbharu/hideout/core/domain/model/relationship/Relationship.kt b/hideout-core/src/main/kotlin/dev/usbharu/hideout/core/domain/model/relationship/Relationship.kt new file mode 100644 index 00000000..af32b48d --- /dev/null +++ b/hideout-core/src/main/kotlin/dev/usbharu/hideout/core/domain/model/relationship/Relationship.kt @@ -0,0 +1,136 @@ +/* + * Copyright (C) 2024 usbharu + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package dev.usbharu.hideout.core.domain.model.relationship + +import dev.usbharu.hideout.core.domain.event.relationship.RelationshipEvent +import dev.usbharu.hideout.core.domain.event.relationship.RelationshipEventFactory +import dev.usbharu.hideout.core.domain.model.actor.ActorId +import dev.usbharu.hideout.core.domain.shared.domainevent.DomainEventStorable + +@Suppress("TooManyFunctions") +class Relationship( + val actorId: ActorId, + val targetActorId: ActorId, + following: Boolean, + blocking: Boolean, + muting: Boolean, + followRequesting: Boolean, + mutingFollowRequest: Boolean, +) : DomainEventStorable() { + + var following: Boolean = following + private set + var blocking: Boolean = blocking + private set + + var muting: Boolean = muting + private set + var followRequesting: Boolean = followRequesting + private set + var mutingFollowRequest: Boolean = mutingFollowRequest + private set + + fun follow() { + require(blocking.not()) + following = true + addDomainEvent(RelationshipEventFactory(this).createEvent(RelationshipEvent.FOLLOW)) + } + + fun unfollow() { + following = false + addDomainEvent(RelationshipEventFactory(this).createEvent(RelationshipEvent.UNFOLLOW)) + } + + fun block() { + require(following.not()) + blocking = true + addDomainEvent(RelationshipEventFactory(this).createEvent(RelationshipEvent.BLOCK)) + } + + fun unblock() { + blocking = false + addDomainEvent(RelationshipEventFactory(this).createEvent(RelationshipEvent.UNBLOCK)) + } + + fun mute() { + muting = true + addDomainEvent(RelationshipEventFactory(this).createEvent(RelationshipEvent.MUTE)) + } + + fun unmute() { + muting = false + addDomainEvent(RelationshipEventFactory(this).createEvent(RelationshipEvent.UNMUTE)) + } + + fun muteFollowRequest() { + mutingFollowRequest = true + } + + fun unmuteFollowRequest() { + mutingFollowRequest = false + } + + fun followRequest() { + require(blocking.not()) + followRequesting = true + addDomainEvent(RelationshipEventFactory(this).createEvent(RelationshipEvent.FOLLOW_REQUEST)) + } + + fun unfollowRequest() { + followRequesting = false + addDomainEvent(RelationshipEventFactory(this).createEvent(RelationshipEvent.UNFOLLOW_REQUEST)) + } + + fun acceptFollowRequest() { + follow() + followRequesting = false + } + + fun rejectFollowRequest() { + followRequesting = false + } + + override fun equals(other: Any?): Boolean { + if (this === other) return true + if (javaClass != other?.javaClass) return false + + other as Relationship + + if (actorId != other.actorId) return false + if (targetActorId != other.targetActorId) return false + + return true + } + + override fun hashCode(): Int { + var result = actorId.hashCode() + result = 31 * result + targetActorId.hashCode() + return result + } + + companion object { + fun default(actorId: ActorId, targetActorId: ActorId): Relationship = Relationship( + actorId = actorId, + targetActorId = targetActorId, + following = false, + blocking = false, + muting = false, + followRequesting = false, + mutingFollowRequest = false + ) + } +} diff --git a/hideout-core/src/main/kotlin/dev/usbharu/hideout/core/domain/model/relationship/RelationshipRepository.kt b/hideout-core/src/main/kotlin/dev/usbharu/hideout/core/domain/model/relationship/RelationshipRepository.kt new file mode 100644 index 00000000..1e854326 --- /dev/null +++ b/hideout-core/src/main/kotlin/dev/usbharu/hideout/core/domain/model/relationship/RelationshipRepository.kt @@ -0,0 +1,38 @@ +/* + * Copyright (C) 2024 usbharu + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package dev.usbharu.hideout.core.domain.model.relationship + +import dev.usbharu.hideout.core.domain.model.actor.ActorId + +interface RelationshipRepository { + suspend fun save(relationship: Relationship): Relationship + suspend fun delete(relationship: Relationship) + suspend fun findByActorIdAndTargetId(actorId: ActorId, targetId: ActorId): Relationship? + suspend fun findByTargetId( + targetId: ActorId, + option: FindRelationshipOption? = null, + inverseOption: FindRelationshipOption? = null + ): List +} + +data class FindRelationshipOption( + val follow: Boolean? = null, + val block: Boolean? = null, + val mute: Boolean? = null, + val followRequest: Boolean? = null, + val muteFollowRequest: Boolean? = null +) diff --git a/hideout-core/src/main/kotlin/dev/usbharu/hideout/core/domain/model/support/acct/Acct.kt b/hideout-core/src/main/kotlin/dev/usbharu/hideout/core/domain/model/support/acct/Acct.kt new file mode 100644 index 00000000..52394510 --- /dev/null +++ b/hideout-core/src/main/kotlin/dev/usbharu/hideout/core/domain/model/support/acct/Acct.kt @@ -0,0 +1,10 @@ +package dev.usbharu.hideout.core.domain.model.support.acct + +data class Acct( + val userpart: String, + val host: String +) { + override fun toString(): String { + return "acct:$userpart@$host" + } +} diff --git a/hideout-core/src/main/kotlin/dev/usbharu/hideout/core/domain/model/support/domain/Domain.kt b/hideout-core/src/main/kotlin/dev/usbharu/hideout/core/domain/model/support/domain/Domain.kt new file mode 100644 index 00000000..b17ab7bd --- /dev/null +++ b/hideout-core/src/main/kotlin/dev/usbharu/hideout/core/domain/model/support/domain/Domain.kt @@ -0,0 +1,28 @@ +/* + * Copyright (C) 2024 usbharu + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package dev.usbharu.hideout.core.domain.model.support.domain + +@JvmInline +value class Domain(val domain: String) { + init { + require(domain.length <= LENGTH) + } + + companion object { + const val LENGTH = 1000 + } +} diff --git a/hideout-core/src/main/kotlin/dev/usbharu/hideout/core/domain/model/support/page/Page.kt b/hideout-core/src/main/kotlin/dev/usbharu/hideout/core/domain/model/support/page/Page.kt new file mode 100644 index 00000000..88ef63a5 --- /dev/null +++ b/hideout-core/src/main/kotlin/dev/usbharu/hideout/core/domain/model/support/page/Page.kt @@ -0,0 +1,46 @@ +package dev.usbharu.hideout.core.domain.model.support.page + +sealed class Page { + abstract val maxId: Long? + abstract val sinceId: Long? + abstract val minId: Long? + abstract val limit: Int? + + data class PageByMaxId( + override val maxId: Long?, + override val sinceId: Long?, + override val limit: Int? + ) : Page() { + override val minId: Long? = null + } + + data class PageByMinId( + override val maxId: Long?, + override val minId: Long?, + override val limit: Int? + ) : Page() { + override val sinceId: Long? = null + } + + companion object { + fun of( + maxId: Long? = null, + sinceId: Long? = null, + minId: Long? = null, + limit: Int? = null + ): Page = + if (minId != null) { + PageByMinId( + maxId, + minId, + limit + ) + } else { + PageByMaxId( + maxId, + sinceId, + limit + ) + } + } +} diff --git a/hideout-core/src/main/kotlin/dev/usbharu/hideout/core/domain/model/support/page/PaginationList.kt b/hideout-core/src/main/kotlin/dev/usbharu/hideout/core/domain/model/support/page/PaginationList.kt new file mode 100644 index 00000000..617ceecb --- /dev/null +++ b/hideout-core/src/main/kotlin/dev/usbharu/hideout/core/domain/model/support/page/PaginationList.kt @@ -0,0 +1,3 @@ +package dev.usbharu.hideout.core.domain.model.support.page + +class PaginationList(list: List, val next: ID?, val prev: ID?) : List by list diff --git a/hideout-core/src/main/kotlin/dev/usbharu/hideout/core/domain/model/support/postdetail/PostDetail.kt b/hideout-core/src/main/kotlin/dev/usbharu/hideout/core/domain/model/support/postdetail/PostDetail.kt new file mode 100644 index 00000000..5126aa35 --- /dev/null +++ b/hideout-core/src/main/kotlin/dev/usbharu/hideout/core/domain/model/support/postdetail/PostDetail.kt @@ -0,0 +1,22 @@ +package dev.usbharu.hideout.core.domain.model.support.postdetail + +import dev.usbharu.hideout.core.domain.model.actor.Actor +import dev.usbharu.hideout.core.domain.model.post.Post + +data class PostDetail( + val post: Post, + val reply: Post? = null, + val repost: Post? = null, + val postActor: Actor, + val replyActor: Actor? = null, + val repostActor: Actor? = null +) { + init { + require(post.replyId == reply?.id) + require(post.repostId == repost?.id) + + require(post.actorId == postActor.id) + require(reply?.actorId == replyActor?.id) + require(repost?.actorId == repostActor?.id) + } +} diff --git a/hideout-core/src/main/kotlin/dev/usbharu/hideout/core/domain/model/support/principal/Anonymous.kt b/hideout-core/src/main/kotlin/dev/usbharu/hideout/core/domain/model/support/principal/Anonymous.kt new file mode 100644 index 00000000..9bcc6cbb --- /dev/null +++ b/hideout-core/src/main/kotlin/dev/usbharu/hideout/core/domain/model/support/principal/Anonymous.kt @@ -0,0 +1,5 @@ +package dev.usbharu.hideout.core.domain.model.support.principal + +import dev.usbharu.hideout.core.domain.model.actor.ActorId + +data object Anonymous : Principal(ActorId.ghost, null, null) \ No newline at end of file diff --git a/hideout-core/src/main/kotlin/dev/usbharu/hideout/core/domain/model/support/principal/FromApi.kt b/hideout-core/src/main/kotlin/dev/usbharu/hideout/core/domain/model/support/principal/FromApi.kt new file mode 100644 index 00000000..2ce5f785 --- /dev/null +++ b/hideout-core/src/main/kotlin/dev/usbharu/hideout/core/domain/model/support/principal/FromApi.kt @@ -0,0 +1,15 @@ +package dev.usbharu.hideout.core.domain.model.support.principal + +import dev.usbharu.hideout.core.domain.model.actor.ActorId +import dev.usbharu.hideout.core.domain.model.support.acct.Acct +import dev.usbharu.hideout.core.domain.model.userdetails.UserDetailId + +class FromApi( + actorId: ActorId, + override val userDetailId: UserDetailId, + override val acct: Acct +) : Principal( + actorId, + userDetailId, + acct +) \ No newline at end of file diff --git a/hideout-core/src/main/kotlin/dev/usbharu/hideout/core/domain/model/support/principal/Principal.kt b/hideout-core/src/main/kotlin/dev/usbharu/hideout/core/domain/model/support/principal/Principal.kt new file mode 100644 index 00000000..6e7b939f --- /dev/null +++ b/hideout-core/src/main/kotlin/dev/usbharu/hideout/core/domain/model/support/principal/Principal.kt @@ -0,0 +1,7 @@ +package dev.usbharu.hideout.core.domain.model.support.principal + +import dev.usbharu.hideout.core.domain.model.actor.ActorId +import dev.usbharu.hideout.core.domain.model.support.acct.Acct +import dev.usbharu.hideout.core.domain.model.userdetails.UserDetailId + +sealed class Principal(open val actorId: ActorId, open val userDetailId: UserDetailId?, open val acct: Acct?) \ No newline at end of file diff --git a/hideout-core/src/main/kotlin/dev/usbharu/hideout/core/domain/model/support/principal/PrincipalContextHolder.kt b/hideout-core/src/main/kotlin/dev/usbharu/hideout/core/domain/model/support/principal/PrincipalContextHolder.kt new file mode 100644 index 00000000..464363ce --- /dev/null +++ b/hideout-core/src/main/kotlin/dev/usbharu/hideout/core/domain/model/support/principal/PrincipalContextHolder.kt @@ -0,0 +1,5 @@ +package dev.usbharu.hideout.core.domain.model.support.principal + +interface PrincipalContextHolder { + suspend fun getPrincipal(): Principal +} \ No newline at end of file diff --git a/hideout-core/src/main/kotlin/dev/usbharu/hideout/core/domain/model/support/timelineobjectdetail/TimelineObjectDetail.kt b/hideout-core/src/main/kotlin/dev/usbharu/hideout/core/domain/model/support/timelineobjectdetail/TimelineObjectDetail.kt new file mode 100644 index 00000000..a3039380 --- /dev/null +++ b/hideout-core/src/main/kotlin/dev/usbharu/hideout/core/domain/model/support/timelineobjectdetail/TimelineObjectDetail.kt @@ -0,0 +1,56 @@ +package dev.usbharu.hideout.core.domain.model.support.timelineobjectdetail + +import dev.usbharu.hideout.core.domain.model.actor.Actor +import dev.usbharu.hideout.core.domain.model.post.Post +import dev.usbharu.hideout.core.domain.model.post.PostId +import dev.usbharu.hideout.core.domain.model.timelineobject.TimelineObject +import dev.usbharu.hideout.core.domain.model.timelineobject.TimelineObjectId +import dev.usbharu.hideout.core.domain.model.timelineobject.TimelineObjectWarnFilter +import dev.usbharu.hideout.core.domain.model.userdetails.UserDetail +import java.time.Instant + +data class TimelineObjectDetail( + val id: TimelineObjectId, + val postId: PostId, + val timelineUserDetail: UserDetail, + val post: Post, + val postActor: Actor, + val replyPost: Post?, + val replyPostActor: Actor?, + val repostPost: Post?, + val repostPostActor: Actor?, + val isPureRepost: Boolean, + val lastUpdateAt: Instant, + val hasMediaInRepost: Boolean, + val warnFilter: List +) { + companion object { + fun of( + timelineObject: TimelineObject, + timelineUserDetail: UserDetail, + post: Post, + postActor: Actor, + replyPost: Post?, + replyPostActor: Actor?, + repostPost: Post?, + repostPostActor: Actor?, + warnFilter: List + ): TimelineObjectDetail { + return TimelineObjectDetail( + timelineObject.id, + post.id, + timelineUserDetail, + post, + postActor, + replyPost, + replyPostActor, + repostPost, + repostPostActor, + timelineObject.isPureRepost, + timelineObject.lastUpdatedAt, + timelineObject.hasMediaInRepost, + warnFilter + ) + } + } +} diff --git a/hideout-core/src/main/kotlin/dev/usbharu/hideout/core/domain/model/timeline/Timeline.kt b/hideout-core/src/main/kotlin/dev/usbharu/hideout/core/domain/model/timeline/Timeline.kt new file mode 100644 index 00000000..955b1621 --- /dev/null +++ b/hideout-core/src/main/kotlin/dev/usbharu/hideout/core/domain/model/timeline/Timeline.kt @@ -0,0 +1,28 @@ +package dev.usbharu.hideout.core.domain.model.timeline + +import dev.usbharu.hideout.core.domain.event.timeline.TimelineEvent +import dev.usbharu.hideout.core.domain.event.timeline.TimelineEventFactory +import dev.usbharu.hideout.core.domain.model.userdetails.UserDetail +import dev.usbharu.hideout.core.domain.model.userdetails.UserDetailId +import dev.usbharu.hideout.core.domain.shared.domainevent.DomainEventStorable + +class Timeline( + val id: TimelineId, + val userDetailId: UserDetailId, + name: TimelineName, + visibility: TimelineVisibility, + val isSystem: Boolean +) : DomainEventStorable() { + var visibility = visibility + private set + + fun setVisibility(visibility: TimelineVisibility, userDetail: UserDetail) { + check(isSystem.not()) + require(userDetailId == userDetail.id) + this.visibility = visibility + addDomainEvent(TimelineEventFactory(this).createEvent(TimelineEvent.CHANGE_VISIBILITY)) + } + + var name = name + private set +} diff --git a/hideout-core/src/main/kotlin/dev/usbharu/hideout/core/domain/model/timeline/TimelineId.kt b/hideout-core/src/main/kotlin/dev/usbharu/hideout/core/domain/model/timeline/TimelineId.kt new file mode 100644 index 00000000..c93738d8 --- /dev/null +++ b/hideout-core/src/main/kotlin/dev/usbharu/hideout/core/domain/model/timeline/TimelineId.kt @@ -0,0 +1,4 @@ +package dev.usbharu.hideout.core.domain.model.timeline + +@JvmInline +value class TimelineId(val value: Long) diff --git a/hideout-core/src/main/kotlin/dev/usbharu/hideout/core/domain/model/timeline/TimelineName.kt b/hideout-core/src/main/kotlin/dev/usbharu/hideout/core/domain/model/timeline/TimelineName.kt new file mode 100644 index 00000000..a8dd2f87 --- /dev/null +++ b/hideout-core/src/main/kotlin/dev/usbharu/hideout/core/domain/model/timeline/TimelineName.kt @@ -0,0 +1,4 @@ +package dev.usbharu.hideout.core.domain.model.timeline + +@JvmInline +value class TimelineName(val value: String) diff --git a/hideout-core/src/main/kotlin/dev/usbharu/hideout/core/domain/model/timeline/TimelineRepository.kt b/hideout-core/src/main/kotlin/dev/usbharu/hideout/core/domain/model/timeline/TimelineRepository.kt new file mode 100644 index 00000000..28fdb0f1 --- /dev/null +++ b/hideout-core/src/main/kotlin/dev/usbharu/hideout/core/domain/model/timeline/TimelineRepository.kt @@ -0,0 +1,10 @@ +package dev.usbharu.hideout.core.domain.model.timeline + +interface TimelineRepository { + suspend fun save(timeline: Timeline): Timeline + suspend fun delete(timeline: Timeline) + + suspend fun findByIds(ids: List): List + + suspend fun findById(id: TimelineId): Timeline? +} diff --git a/hideout-core/src/main/kotlin/dev/usbharu/hideout/core/domain/model/timeline/TimelineVisibility.kt b/hideout-core/src/main/kotlin/dev/usbharu/hideout/core/domain/model/timeline/TimelineVisibility.kt new file mode 100644 index 00000000..506dd3af --- /dev/null +++ b/hideout-core/src/main/kotlin/dev/usbharu/hideout/core/domain/model/timeline/TimelineVisibility.kt @@ -0,0 +1,7 @@ +package dev.usbharu.hideout.core.domain.model.timeline + +enum class TimelineVisibility { + PRIVATE, + UNLISTED, + PUBLIC +} diff --git a/hideout-core/src/main/kotlin/dev/usbharu/hideout/core/domain/model/timelineobject/TimelineObject.kt b/hideout-core/src/main/kotlin/dev/usbharu/hideout/core/domain/model/timelineobject/TimelineObject.kt new file mode 100644 index 00000000..171fcc01 --- /dev/null +++ b/hideout-core/src/main/kotlin/dev/usbharu/hideout/core/domain/model/timelineobject/TimelineObject.kt @@ -0,0 +1,140 @@ +package dev.usbharu.hideout.core.domain.model.timelineobject + +import dev.usbharu.hideout.core.domain.model.actor.ActorId +import dev.usbharu.hideout.core.domain.model.emoji.EmojiId +import dev.usbharu.hideout.core.domain.model.filter.FilterResult +import dev.usbharu.hideout.core.domain.model.media.MediaId +import dev.usbharu.hideout.core.domain.model.post.Post +import dev.usbharu.hideout.core.domain.model.post.PostContent +import dev.usbharu.hideout.core.domain.model.post.PostId +import dev.usbharu.hideout.core.domain.model.post.Visibility +import dev.usbharu.hideout.core.domain.model.timeline.Timeline +import dev.usbharu.hideout.core.domain.model.timeline.TimelineId +import dev.usbharu.hideout.core.domain.model.userdetails.UserDetailId +import java.time.Instant + +class TimelineObject( + val id: TimelineObjectId, + val userDetailId: UserDetailId, + val timelineId: TimelineId, + val postId: PostId, + val postActorId: ActorId, + val postCreatedAt: Instant, + val replyId: PostId?, + val replyActorId: ActorId?, + val repostId: PostId?, + val repostActorId: ActorId?, + visibility: Visibility, + isPureRepost: Boolean, + mediaIds: List, + emojiIds: List, + visibleActors: List, + hasMediaInRepost: Boolean, + lastUpdatedAt: Instant, + var warnFilters: List, + + ) { + var isPureRepost = isPureRepost + private set + var visibleActors = visibleActors + private set + var hasMediaInRepost = hasMediaInRepost + private set + val hasMedia + get() = mediaIds.isNotEmpty() + + var lastUpdatedAt = lastUpdatedAt + private set + var visibility = visibility + private set + var mediaIds = mediaIds + private set + var emojiIds = emojiIds + private set + + fun updateWith(post: Post, filterResults: List) { + visibleActors = post.visibleActors.toList() + visibility = post.visibility + mediaIds = post.mediaIds.toList() + emojiIds = post.emojiIds.toList() + lastUpdatedAt = Instant.now() + isPureRepost = + post.repostId != null && post.replyId == null && post.text.isEmpty() && post.overview?.overview.isNullOrEmpty() + warnFilters = filterResults.map { TimelineObjectWarnFilter(it.filter.id, it.matchedKeyword) } + } + + fun updateWith(post: Post, repost: Post, filterResults: List) { + require(repost.id == post.repostId) + require(repostId == post.repostId) + + updateWith(post, filterResults) + hasMediaInRepost = repost.mediaIds.isNotEmpty() + } + + companion object { + + fun create( + timelineObjectId: TimelineObjectId, + timeline: Timeline, + post: Post, + replyActorId: ActorId?, + filterResults: List + ): TimelineObject { + return TimelineObject( + id = timelineObjectId, + userDetailId = timeline.userDetailId, + timelineId = timeline.id, + postId = post.id, + postActorId = post.actorId, + postCreatedAt = post.createdAt, + replyId = post.replyId, + replyActorId = replyActorId, + repostId = null, + repostActorId = null, + visibility = post.visibility, + isPureRepost = true, + mediaIds = post.mediaIds, + emojiIds = post.emojiIds, + visibleActors = post.visibleActors.toList(), + hasMediaInRepost = false, + lastUpdatedAt = Instant.now(), + warnFilters = filterResults.map { TimelineObjectWarnFilter(it.filter.id, it.matchedKeyword) } + ) + } + + fun create( + timelineObjectId: TimelineObjectId, + timeline: Timeline, + post: Post, + replyActorId: ActorId?, + repost: Post, + filterResults: List + ): TimelineObject { + require(post.repostId == repost.id) + + return TimelineObject( + id = timelineObjectId, + userDetailId = timeline.userDetailId, + timelineId = timeline.id, + postId = post.id, + postActorId = post.actorId, + postCreatedAt = post.createdAt, + replyId = post.replyId, + replyActorId = replyActorId, + repostId = repost.id, + repostActorId = repost.actorId, + visibility = post.visibility, + isPureRepost = repost.mediaIds.isEmpty() && + repost.overview == null && + repost.content == PostContent.empty && + repost.replyId == null, + mediaIds = post.mediaIds, + emojiIds = post.emojiIds, + visibleActors = post.visibleActors.toList(), + hasMediaInRepost = repost.mediaIds.isNotEmpty(), + lastUpdatedAt = Instant.now(), + warnFilters = filterResults.map { TimelineObjectWarnFilter(it.filter.id, it.matchedKeyword) } + ) + } + } +} diff --git a/hideout-core/src/main/kotlin/dev/usbharu/hideout/core/domain/model/timelineobject/TimelineObjectId.kt b/hideout-core/src/main/kotlin/dev/usbharu/hideout/core/domain/model/timelineobject/TimelineObjectId.kt new file mode 100644 index 00000000..95e24c40 --- /dev/null +++ b/hideout-core/src/main/kotlin/dev/usbharu/hideout/core/domain/model/timelineobject/TimelineObjectId.kt @@ -0,0 +1,4 @@ +package dev.usbharu.hideout.core.domain.model.timelineobject + +@JvmInline +value class TimelineObjectId(val value: Long) diff --git a/hideout-core/src/main/kotlin/dev/usbharu/hideout/core/domain/model/timelineobject/TimelineObjectWarnFilter.kt b/hideout-core/src/main/kotlin/dev/usbharu/hideout/core/domain/model/timelineobject/TimelineObjectWarnFilter.kt new file mode 100644 index 00000000..bdd0764d --- /dev/null +++ b/hideout-core/src/main/kotlin/dev/usbharu/hideout/core/domain/model/timelineobject/TimelineObjectWarnFilter.kt @@ -0,0 +1,5 @@ +package dev.usbharu.hideout.core.domain.model.timelineobject + +import dev.usbharu.hideout.core.domain.model.filter.FilterId + +data class TimelineObjectWarnFilter(val filterId: FilterId, val matchedKeyword: String) diff --git a/hideout-core/src/main/kotlin/dev/usbharu/hideout/core/domain/model/timelinerelationship/TimelineRelationship.kt b/hideout-core/src/main/kotlin/dev/usbharu/hideout/core/domain/model/timelinerelationship/TimelineRelationship.kt new file mode 100644 index 00000000..df2cf099 --- /dev/null +++ b/hideout-core/src/main/kotlin/dev/usbharu/hideout/core/domain/model/timelinerelationship/TimelineRelationship.kt @@ -0,0 +1,18 @@ +package dev.usbharu.hideout.core.domain.model.timelinerelationship + +import dev.usbharu.hideout.core.domain.model.actor.ActorId +import dev.usbharu.hideout.core.domain.model.timeline.TimelineId + +class TimelineRelationship( + val id: TimelineRelationshipId, + val timelineId: TimelineId, + val actorId: ActorId, + val visible: Visible +) + +enum class Visible { + PUBLIC, + UNLISTED, + FOLLOWERS, + DIRECT +} diff --git a/hideout-core/src/main/kotlin/dev/usbharu/hideout/core/domain/model/timelinerelationship/TimelineRelationshipId.kt b/hideout-core/src/main/kotlin/dev/usbharu/hideout/core/domain/model/timelinerelationship/TimelineRelationshipId.kt new file mode 100644 index 00000000..5de526a7 --- /dev/null +++ b/hideout-core/src/main/kotlin/dev/usbharu/hideout/core/domain/model/timelinerelationship/TimelineRelationshipId.kt @@ -0,0 +1,4 @@ +package dev.usbharu.hideout.core.domain.model.timelinerelationship + +@JvmInline +value class TimelineRelationshipId(val value: Long) diff --git a/hideout-core/src/main/kotlin/dev/usbharu/hideout/core/domain/model/timelinerelationship/TimelineRelationshipRepository.kt b/hideout-core/src/main/kotlin/dev/usbharu/hideout/core/domain/model/timelinerelationship/TimelineRelationshipRepository.kt new file mode 100644 index 00000000..ccf1c463 --- /dev/null +++ b/hideout-core/src/main/kotlin/dev/usbharu/hideout/core/domain/model/timelinerelationship/TimelineRelationshipRepository.kt @@ -0,0 +1,10 @@ +package dev.usbharu.hideout.core.domain.model.timelinerelationship + +import dev.usbharu.hideout.core.domain.model.actor.ActorId + +interface TimelineRelationshipRepository { + suspend fun save(timelineRelationship: TimelineRelationship): TimelineRelationship + suspend fun delete(timelineRelationship: TimelineRelationship) + + suspend fun findByActorId(actorId: ActorId): List +} diff --git a/hideout-core/src/main/kotlin/dev/usbharu/hideout/core/domain/model/userdetails/UserDetail.kt b/hideout-core/src/main/kotlin/dev/usbharu/hideout/core/domain/model/userdetails/UserDetail.kt new file mode 100644 index 00000000..a272176f --- /dev/null +++ b/hideout-core/src/main/kotlin/dev/usbharu/hideout/core/domain/model/userdetails/UserDetail.kt @@ -0,0 +1,62 @@ +/* + * Copyright (C) 2024 usbharu + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package dev.usbharu.hideout.core.domain.model.userdetails + +import dev.usbharu.hideout.core.domain.model.actor.ActorId +import dev.usbharu.hideout.core.domain.model.timeline.TimelineId +import java.time.Instant + +class UserDetail private constructor( + val id: UserDetailId, + val actorId: ActorId, + var password: UserDetailHashedPassword, + var autoAcceptFolloweeFollowRequest: Boolean, + var lastMigration: Instant? = null, + val homeTimelineId: TimelineId? +) { + + override fun equals(other: Any?): Boolean { + if (this === other) return true + if (javaClass != other?.javaClass) return false + + other as UserDetail + + return id == other.id + } + + override fun hashCode(): Int = id.hashCode() + + companion object { + fun create( + id: UserDetailId, + actorId: ActorId, + password: UserDetailHashedPassword, + autoAcceptFolloweeFollowRequest: Boolean = false, + lastMigration: Instant? = null, + homeTimelineId: TimelineId? = null + ): UserDetail { + return UserDetail( + id, + actorId, + password, + autoAcceptFolloweeFollowRequest, + lastMigration, + homeTimelineId + ) + } + } +} diff --git a/hideout-core/src/main/kotlin/dev/usbharu/hideout/core/domain/model/userdetails/UserDetailHashedPassword.kt b/hideout-core/src/main/kotlin/dev/usbharu/hideout/core/domain/model/userdetails/UserDetailHashedPassword.kt new file mode 100644 index 00000000..f0dc4399 --- /dev/null +++ b/hideout-core/src/main/kotlin/dev/usbharu/hideout/core/domain/model/userdetails/UserDetailHashedPassword.kt @@ -0,0 +1,20 @@ +/* + * Copyright (C) 2024 usbharu + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package dev.usbharu.hideout.core.domain.model.userdetails + +@JvmInline +value class UserDetailHashedPassword(val password: String) diff --git a/hideout-core/src/main/kotlin/dev/usbharu/hideout/core/domain/model/userdetails/UserDetailId.kt b/hideout-core/src/main/kotlin/dev/usbharu/hideout/core/domain/model/userdetails/UserDetailId.kt new file mode 100644 index 00000000..cc048546 --- /dev/null +++ b/hideout-core/src/main/kotlin/dev/usbharu/hideout/core/domain/model/userdetails/UserDetailId.kt @@ -0,0 +1,20 @@ +/* + * Copyright (C) 2024 usbharu + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package dev.usbharu.hideout.core.domain.model.userdetails + +@JvmInline +value class UserDetailId(val id: Long) diff --git a/hideout-core/src/main/kotlin/dev/usbharu/hideout/core/domain/model/userdetails/UserDetailRepository.kt b/hideout-core/src/main/kotlin/dev/usbharu/hideout/core/domain/model/userdetails/UserDetailRepository.kt new file mode 100644 index 00000000..8951b70a --- /dev/null +++ b/hideout-core/src/main/kotlin/dev/usbharu/hideout/core/domain/model/userdetails/UserDetailRepository.kt @@ -0,0 +1,25 @@ +/* + * Copyright (C) 2024 usbharu + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package dev.usbharu.hideout.core.domain.model.userdetails + +interface UserDetailRepository { + suspend fun save(userDetail: UserDetail): UserDetail + suspend fun delete(userDetail: UserDetail) + suspend fun findByActorId(actorId: Long): UserDetail? + suspend fun findById(userDetailId: UserDetailId): UserDetail? + suspend fun findAllById(idList: List): List +} diff --git a/hideout-core/src/main/kotlin/dev/usbharu/hideout/core/domain/service/actor/RemoteActorCheckDomainService.kt b/hideout-core/src/main/kotlin/dev/usbharu/hideout/core/domain/service/actor/RemoteActorCheckDomainService.kt new file mode 100644 index 00000000..a2f9b6fd --- /dev/null +++ b/hideout-core/src/main/kotlin/dev/usbharu/hideout/core/domain/service/actor/RemoteActorCheckDomainService.kt @@ -0,0 +1,30 @@ +/* + * Copyright (C) 2024 usbharu + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package dev.usbharu.hideout.core.domain.service.actor + +import dev.usbharu.hideout.core.config.ApplicationConfig +import dev.usbharu.hideout.core.domain.model.actor.Actor +import org.springframework.stereotype.Service + +interface IRemoteActorCheckDomainService { + fun isRemoteActor(actor: Actor): Boolean +} + +@Service +class RemoteActorCheckDomainService(private val applicationConfig: ApplicationConfig) : IRemoteActorCheckDomainService { + override fun isRemoteActor(actor: Actor): Boolean = actor.domain.domain != applicationConfig.url.host +} diff --git a/hideout-core/src/main/kotlin/dev/usbharu/hideout/core/domain/service/actor/local/LocalActorDomainService.kt b/hideout-core/src/main/kotlin/dev/usbharu/hideout/core/domain/service/actor/local/LocalActorDomainService.kt new file mode 100644 index 00000000..1dafb03d --- /dev/null +++ b/hideout-core/src/main/kotlin/dev/usbharu/hideout/core/domain/service/actor/local/LocalActorDomainService.kt @@ -0,0 +1,25 @@ +/* + * Copyright (C) 2024 usbharu + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package dev.usbharu.hideout.core.domain.service.actor.local + +import dev.usbharu.hideout.core.domain.model.actor.ActorPrivateKey +import dev.usbharu.hideout.core.domain.model.actor.ActorPublicKey + +interface LocalActorDomainService { + suspend fun usernameAlreadyUse(name: String): Boolean + suspend fun generateKeyPair(): Pair +} diff --git a/hideout-core/src/main/kotlin/dev/usbharu/hideout/core/domain/service/actor/local/LocalActorDomainServiceImpl.kt b/hideout-core/src/main/kotlin/dev/usbharu/hideout/core/domain/service/actor/local/LocalActorDomainServiceImpl.kt new file mode 100644 index 00000000..6f0dd6de --- /dev/null +++ b/hideout-core/src/main/kotlin/dev/usbharu/hideout/core/domain/service/actor/local/LocalActorDomainServiceImpl.kt @@ -0,0 +1,41 @@ +/* + * Copyright (C) 2024 usbharu + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package dev.usbharu.hideout.core.domain.service.actor.local + +import dev.usbharu.hideout.core.config.ApplicationConfig +import dev.usbharu.hideout.core.domain.model.actor.ActorPrivateKey +import dev.usbharu.hideout.core.domain.model.actor.ActorPublicKey +import dev.usbharu.hideout.core.domain.model.actor.ActorRepository +import org.springframework.stereotype.Service +import java.security.KeyPairGenerator + +@Service +class LocalActorDomainServiceImpl( + private val actorRepository: ActorRepository, + private val applicationConfig: ApplicationConfig, +) : LocalActorDomainService { + override suspend fun usernameAlreadyUse(name: String): Boolean = + actorRepository.findByNameAndDomain(name, applicationConfig.url.host) != null + + override suspend fun generateKeyPair(): Pair { + val keyPairGenerator = KeyPairGenerator.getInstance("RSA") + keyPairGenerator.initialize(applicationConfig.keySize) + val generateKeyPair = keyPairGenerator.generateKeyPair() + + return ActorPublicKey.create(generateKeyPair.public) to ActorPrivateKey.create(generateKeyPair.private) + } +} diff --git a/hideout-core/src/main/kotlin/dev/usbharu/hideout/core/domain/service/actor/local/LocalActorMigrationCheckDomainService.kt b/hideout-core/src/main/kotlin/dev/usbharu/hideout/core/domain/service/actor/local/LocalActorMigrationCheckDomainService.kt new file mode 100644 index 00000000..c9eb3338 --- /dev/null +++ b/hideout-core/src/main/kotlin/dev/usbharu/hideout/core/domain/service/actor/local/LocalActorMigrationCheckDomainService.kt @@ -0,0 +1,40 @@ +/* + * Copyright (C) 2024 usbharu + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package dev.usbharu.hideout.core.domain.service.actor.local + +import dev.usbharu.hideout.core.domain.model.actor.Actor +import dev.usbharu.hideout.core.domain.model.userdetails.UserDetail + +interface LocalActorMigrationCheckDomainService { + suspend fun canAccountMigration(userDetail: UserDetail, from: Actor, to: Actor): AccountMigrationCheck +} + +sealed class AccountMigrationCheck( + val canMigration: Boolean, +) { + class CanAccountMigration : AccountMigrationCheck(true) + + class CircularReferences(val message: String) : AccountMigrationCheck(false) + + class SelfReferences : AccountMigrationCheck(false) + + class AlreadyMoved(val message: String) : AccountMigrationCheck(false) + + class AlsoKnownAsNotFound(val message: String) : AccountMigrationCheck(false) + + class MigrationCoolDown(val message: String) : AccountMigrationCheck(false) +} diff --git a/hideout-core/src/main/kotlin/dev/usbharu/hideout/core/domain/service/actor/local/LocalActorMigrationCheckDomainServiceImpl.kt b/hideout-core/src/main/kotlin/dev/usbharu/hideout/core/domain/service/actor/local/LocalActorMigrationCheckDomainServiceImpl.kt new file mode 100644 index 00000000..4b295484 --- /dev/null +++ b/hideout-core/src/main/kotlin/dev/usbharu/hideout/core/domain/service/actor/local/LocalActorMigrationCheckDomainServiceImpl.kt @@ -0,0 +1,55 @@ +/* + * Copyright (C) 2024 usbharu + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package dev.usbharu.hideout.core.domain.service.actor.local + +import dev.usbharu.hideout.core.domain.model.actor.Actor +import dev.usbharu.hideout.core.domain.model.userdetails.UserDetail +import org.springframework.stereotype.Service +import java.time.Instant +import kotlin.time.Duration.Companion.days +import kotlin.time.toJavaDuration + +@Service +class LocalActorMigrationCheckDomainServiceImpl : LocalActorMigrationCheckDomainService { + override suspend fun canAccountMigration(userDetail: UserDetail, from: Actor, to: Actor): AccountMigrationCheck { + val lastMigration = userDetail.lastMigration + if (lastMigration != null) { + val instant = lastMigration.plus(30.days.toJavaDuration()) + if (instant.isAfter(Instant.now())) { + return AccountMigrationCheck.MigrationCoolDown("You can migration at $instant.") + } + } + + if (to == from) { + return AccountMigrationCheck.SelfReferences() + } + + if (to.moveTo != null) { + return AccountMigrationCheck.AlreadyMoved("${to.name}@${to.domain} was move to ${to.moveTo}") + } + + if (from.moveTo != null) { + return AccountMigrationCheck.AlreadyMoved("${from.name}@${from.domain} was move to ${from.moveTo}") + } + + if (to.alsoKnownAs.contains(from.id).not()) { + return AccountMigrationCheck.AlsoKnownAsNotFound("${to.id} has ${to.alsoKnownAs}") + } + + return AccountMigrationCheck.CanAccountMigration() + } +} diff --git a/hideout-core/src/main/kotlin/dev/usbharu/hideout/core/domain/service/actorinstancerelationship/ActorInstanceRelationshipDomainService.kt b/hideout-core/src/main/kotlin/dev/usbharu/hideout/core/domain/service/actorinstancerelationship/ActorInstanceRelationshipDomainService.kt new file mode 100644 index 00000000..d47ef39a --- /dev/null +++ b/hideout-core/src/main/kotlin/dev/usbharu/hideout/core/domain/service/actorinstancerelationship/ActorInstanceRelationshipDomainService.kt @@ -0,0 +1,21 @@ +/* + * Copyright (C) 2024 usbharu + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package dev.usbharu.hideout.core.domain.service.actorinstancerelationship + +interface ActorInstanceRelationshipDomainService { + suspend fun block() +} diff --git a/hideout-core/src/main/kotlin/dev/usbharu/hideout/core/domain/service/filter/FilterDomainService.kt b/hideout-core/src/main/kotlin/dev/usbharu/hideout/core/domain/service/filter/FilterDomainService.kt new file mode 100644 index 00000000..97ec2cfb --- /dev/null +++ b/hideout-core/src/main/kotlin/dev/usbharu/hideout/core/domain/service/filter/FilterDomainService.kt @@ -0,0 +1,67 @@ +package dev.usbharu.hideout.core.domain.service.filter + +import dev.usbharu.hideout.core.domain.model.filter.Filter +import dev.usbharu.hideout.core.domain.model.filter.FilterContext +import dev.usbharu.hideout.core.domain.model.filter.FilterResult +import dev.usbharu.hideout.core.domain.model.filter.FilteredPost +import dev.usbharu.hideout.core.domain.model.post.Post +import org.springframework.stereotype.Service + +interface IFilterDomainService { + fun apply(post: Post, context: FilterContext, filters: List): FilteredPost + fun applyAll(postList: List, context: FilterContext, filters: List): List +} + +@Service +class FilterDomainService : IFilterDomainService { + override fun apply(post: Post, context: FilterContext, filters: List): FilteredPost { + val filterResults = filters + .filter { it.filterContext.contains(context) } + .flatMap { filter -> + val regex = filter.compileFilter() + post + .overview + ?.overview + ?.let { it1 -> regex.findAll(it1) } + .orEmpty() + .toList() + .map { FilterResult(filter, it.value) } + .plus( + post + .text + .let { regex.findAll(it) } + .toList() + .map { FilterResult(filter, it.value) } + ) + } + return FilteredPost(post, filterResults) + } + + override fun applyAll(postList: List, context: FilterContext, filters: List): List { + return filters + .filter { it.filterContext.contains(context) } + .map { it to it.compileFilter() } + .flatMap { compiledFilter -> + postList + .map { post -> + val filterResults = post + .overview + ?.overview + ?.let { overview -> compiledFilter.second.findAll(overview) } + .orEmpty() + .toList() + .map { FilterResult(compiledFilter.first, it.value) } + .plus( + post + .text + .let { compiledFilter.second.findAll(it) } + .toList() + .map { FilterResult(compiledFilter.first, it.value) } + ) + + post to filterResults + } + } + .map { FilteredPost(it.first, it.second) } + } +} diff --git a/hideout-core/src/main/kotlin/dev/usbharu/hideout/core/domain/service/post/DefaultPostReadAccessControl.kt b/hideout-core/src/main/kotlin/dev/usbharu/hideout/core/domain/service/post/DefaultPostReadAccessControl.kt new file mode 100644 index 00000000..4e7fba5e --- /dev/null +++ b/hideout-core/src/main/kotlin/dev/usbharu/hideout/core/domain/service/post/DefaultPostReadAccessControl.kt @@ -0,0 +1,54 @@ +package dev.usbharu.hideout.core.domain.service.post + +import dev.usbharu.hideout.core.domain.model.post.Post +import dev.usbharu.hideout.core.domain.model.post.Visibility +import dev.usbharu.hideout.core.domain.model.relationship.Relationship +import dev.usbharu.hideout.core.domain.model.relationship.RelationshipRepository +import dev.usbharu.hideout.core.domain.model.support.principal.Anonymous +import dev.usbharu.hideout.core.domain.model.support.principal.Principal +import org.springframework.stereotype.Component + +interface IPostReadAccessControl { + suspend fun isAllow(post: Post, principal: Principal): Boolean +} + +@Component +class DefaultPostReadAccessControl(private val relationshipRepository: RelationshipRepository) : + IPostReadAccessControl { + override suspend fun isAllow(post: Post, principal: Principal): Boolean { + val relationship = (relationshipRepository.findByActorIdAndTargetId(post.actorId, principal.actorId) + ?: Relationship.default(post.actorId, principal.actorId)) + + //ブロックされてたら見れない + if (relationship.blocking) { + return false + } + + //PublicかUnlistedなら見れる + if (post.visibility == Visibility.PUBLIC || post.visibility == Visibility.UNLISTED) { + return true + } + + //principalがAnonymousなら見れない + if (principal is Anonymous) { + return false + } + + //DirectでvisibleActorsに含まれていたら見れる + if (post.visibility == Visibility.DIRECT && post.visibleActors.contains(principal.actorId)) { + return true + } + + //Followersでフォロワーなら見れる + if (post.visibility == Visibility.FOLLOWERS) { + val inverseRelationship = + relationshipRepository.findByActorIdAndTargetId(principal.actorId, post.actorId) ?: return false + + return inverseRelationship.following + } + + //その他の場合は見れない + return false + } + +} \ No newline at end of file diff --git a/hideout-core/src/main/kotlin/dev/usbharu/hideout/core/domain/service/post/PostContentFormatter.kt b/hideout-core/src/main/kotlin/dev/usbharu/hideout/core/domain/service/post/PostContentFormatter.kt new file mode 100644 index 00000000..e74b5f64 --- /dev/null +++ b/hideout-core/src/main/kotlin/dev/usbharu/hideout/core/domain/service/post/PostContentFormatter.kt @@ -0,0 +1,26 @@ +/* + * Copyright (C) 2024 usbharu + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package dev.usbharu.hideout.core.domain.service.post + +interface PostContentFormatter { + fun format(content: String): FormattedPostContent +} + +data class FormattedPostContent( + val html: String, + val content: String, +) diff --git a/hideout-core/src/main/kotlin/dev/usbharu/hideout/core/domain/service/relationship/RelationshipDomainService.kt b/hideout-core/src/main/kotlin/dev/usbharu/hideout/core/domain/service/relationship/RelationshipDomainService.kt new file mode 100644 index 00000000..b59c0319 --- /dev/null +++ b/hideout-core/src/main/kotlin/dev/usbharu/hideout/core/domain/service/relationship/RelationshipDomainService.kt @@ -0,0 +1,33 @@ +/* + * Copyright (C) 2024 usbharu + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package dev.usbharu.hideout.core.domain.service.relationship + +import dev.usbharu.hideout.core.domain.model.relationship.Relationship +import org.springframework.stereotype.Service + +@Service +class RelationshipDomainService { + fun block(relationship: Relationship, inverseRelationship: Relationship) { + require(relationship != inverseRelationship) + require(relationship.actorId == inverseRelationship.targetActorId) + require(relationship.targetActorId == inverseRelationship.actorId) + + relationship.block() + inverseRelationship.unfollow() + inverseRelationship.unfollowRequest() + } +} diff --git a/hideout-core/src/main/kotlin/dev/usbharu/hideout/core/domain/service/userdetail/PasswordEncoder.kt b/hideout-core/src/main/kotlin/dev/usbharu/hideout/core/domain/service/userdetail/PasswordEncoder.kt new file mode 100644 index 00000000..979bc1e0 --- /dev/null +++ b/hideout-core/src/main/kotlin/dev/usbharu/hideout/core/domain/service/userdetail/PasswordEncoder.kt @@ -0,0 +1,21 @@ +/* + * Copyright (C) 2024 usbharu + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package dev.usbharu.hideout.core.domain.service.userdetail + +interface PasswordEncoder { + suspend fun encode(input: String): String +} diff --git a/hideout-core/src/main/kotlin/dev/usbharu/hideout/core/domain/service/userdetail/UserDetailDomainService.kt b/hideout-core/src/main/kotlin/dev/usbharu/hideout/core/domain/service/userdetail/UserDetailDomainService.kt new file mode 100644 index 00000000..cb3fb074 --- /dev/null +++ b/hideout-core/src/main/kotlin/dev/usbharu/hideout/core/domain/service/userdetail/UserDetailDomainService.kt @@ -0,0 +1,25 @@ +/* + * Copyright (C) 2024 usbharu + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package dev.usbharu.hideout.core.domain.service.userdetail + +import dev.usbharu.hideout.core.domain.model.userdetails.UserDetailHashedPassword +import org.springframework.stereotype.Service + +@Service +class UserDetailDomainService(private val passwordEncoder: PasswordEncoder) { + suspend fun hashPassword(password: String) = UserDetailHashedPassword(passwordEncoder.encode(password)) +} diff --git a/hideout-core/src/main/kotlin/dev/usbharu/hideout/core/domain/shared/domainevent/DomainEvent.kt b/hideout-core/src/main/kotlin/dev/usbharu/hideout/core/domain/shared/domainevent/DomainEvent.kt new file mode 100644 index 00000000..0b5f77fc --- /dev/null +++ b/hideout-core/src/main/kotlin/dev/usbharu/hideout/core/domain/shared/domainevent/DomainEvent.kt @@ -0,0 +1,50 @@ +/* + * Copyright (C) 2024 usbharu + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package dev.usbharu.hideout.core.domain.shared.domainevent + +import java.time.Instant +import java.util.* + +/** + * エンティティで発生したドメインイベント + * + * @property id ID + * @property name ドメインイベント名 + * @property occurredOn 発生時刻 + * @property body ドメインイベントのボディ + * @property collectable trueで同じドメインイベント名でをまとめる + */ +data class DomainEvent( + val id: String, + val name: String, + val occurredOn: Instant, + val body: T, + val collectable: Boolean = false +) { + companion object { + fun create(name: String, body: T, collectable: Boolean = false): DomainEvent = + DomainEvent(UUID.randomUUID().toString(), name, Instant.now(), body, collectable) + + fun reconstruct( + id: String, + name: String, + occurredOn: Instant, + body: T, + collectable: Boolean + ): DomainEvent = DomainEvent(id, name, occurredOn, body, collectable) + } +} diff --git a/hideout-core/src/main/kotlin/dev/usbharu/hideout/core/domain/shared/domainevent/DomainEventBody.kt b/hideout-core/src/main/kotlin/dev/usbharu/hideout/core/domain/shared/domainevent/DomainEventBody.kt new file mode 100644 index 00000000..bcee190b --- /dev/null +++ b/hideout-core/src/main/kotlin/dev/usbharu/hideout/core/domain/shared/domainevent/DomainEventBody.kt @@ -0,0 +1,24 @@ +/* + * Copyright (C) 2024 usbharu + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package dev.usbharu.hideout.core.domain.shared.domainevent + +import dev.usbharu.hideout.core.domain.model.support.principal.Principal + +@Suppress("UnnecessaryAbstractClass") +abstract class DomainEventBody(private val map: Map, open val principal: Principal? = null) { + fun toMap(): Map = map +} diff --git a/hideout-core/src/main/kotlin/dev/usbharu/hideout/core/domain/shared/domainevent/DomainEventPublisher.kt b/hideout-core/src/main/kotlin/dev/usbharu/hideout/core/domain/shared/domainevent/DomainEventPublisher.kt new file mode 100644 index 00000000..cf784458 --- /dev/null +++ b/hideout-core/src/main/kotlin/dev/usbharu/hideout/core/domain/shared/domainevent/DomainEventPublisher.kt @@ -0,0 +1,5 @@ +package dev.usbharu.hideout.core.domain.shared.domainevent + +interface DomainEventPublisher { + suspend fun publishEvent(domainEvent: DomainEvent<*>) +} diff --git a/hideout-core/src/main/kotlin/dev/usbharu/hideout/core/domain/shared/domainevent/DomainEventStorable.kt b/hideout-core/src/main/kotlin/dev/usbharu/hideout/core/domain/shared/domainevent/DomainEventStorable.kt new file mode 100644 index 00000000..849267b0 --- /dev/null +++ b/hideout-core/src/main/kotlin/dev/usbharu/hideout/core/domain/shared/domainevent/DomainEventStorable.kt @@ -0,0 +1,30 @@ +/* + * Copyright (C) 2024 usbharu + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package dev.usbharu.hideout.core.domain.shared.domainevent + +@Suppress("UnnecessaryAbstractClass") +abstract class DomainEventStorable { + private val domainEvents: MutableList> = mutableListOf() + + protected fun addDomainEvent(domainEvent: DomainEvent<*>) { + domainEvents.add(domainEvent) + } + + fun clearDomainEvents() = domainEvents.clear() + + fun getDomainEvents(): List> = domainEvents.toList() +} diff --git a/hideout-core/src/main/kotlin/dev/usbharu/hideout/core/domain/shared/id/IdGenerateService.kt b/hideout-core/src/main/kotlin/dev/usbharu/hideout/core/domain/shared/id/IdGenerateService.kt new file mode 100644 index 00000000..91991d71 --- /dev/null +++ b/hideout-core/src/main/kotlin/dev/usbharu/hideout/core/domain/shared/id/IdGenerateService.kt @@ -0,0 +1,21 @@ +/* + * Copyright (C) 2024 usbharu + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package dev.usbharu.hideout.core.domain.shared.id + +interface IdGenerateService { + suspend fun generateId(): Long +} diff --git a/hideout-core/src/main/kotlin/dev/usbharu/hideout/core/domain/shared/repository/DomainEventPublishableRepository.kt b/hideout-core/src/main/kotlin/dev/usbharu/hideout/core/domain/shared/repository/DomainEventPublishableRepository.kt new file mode 100644 index 00000000..3b385984 --- /dev/null +++ b/hideout-core/src/main/kotlin/dev/usbharu/hideout/core/domain/shared/repository/DomainEventPublishableRepository.kt @@ -0,0 +1,22 @@ +package dev.usbharu.hideout.core.domain.shared.repository + +import dev.usbharu.hideout.core.domain.shared.domainevent.DomainEventPublisher +import dev.usbharu.hideout.core.domain.shared.domainevent.DomainEventStorable +import org.springframework.stereotype.Repository + +@Repository +interface DomainEventPublishableRepository { + val domainEventPublisher: DomainEventPublisher + suspend fun update(entity: T) { + entity.getDomainEvents().distinctBy { + if (it.collectable) { + it.name + } else { + it.id + } + }.forEach { + domainEventPublisher.publishEvent(it) + } + entity.clearDomainEvents() + } +} diff --git a/hideout-core/src/main/kotlin/dev/usbharu/hideout/core/external/media/DelegateMediaProcessor.kt b/hideout-core/src/main/kotlin/dev/usbharu/hideout/core/external/media/DelegateMediaProcessor.kt new file mode 100644 index 00000000..65cbc14d --- /dev/null +++ b/hideout-core/src/main/kotlin/dev/usbharu/hideout/core/external/media/DelegateMediaProcessor.kt @@ -0,0 +1,20 @@ +package dev.usbharu.hideout.core.external.media + +import dev.usbharu.hideout.core.domain.model.media.MimeType +import org.springframework.beans.factory.annotation.Qualifier +import org.springframework.stereotype.Component +import java.nio.file.Path + +@Component +@Qualifier("delegate") +class DelegateMediaProcessor( + private val fileTypeDeterminer: FileTypeDeterminer, + private val mediaProcessors: List +) : MediaProcessor { + override fun isSupported(mimeType: MimeType): Boolean = true + + override suspend fun process(path: Path, filename: String, mimeType: MimeType?): ProcessedMedia { + val fileType = fileTypeDeterminer.fileType(path, filename) + return mediaProcessors.first { it.isSupported(fileType) }.process(path, filename, fileType) + } +} diff --git a/hideout-core/src/main/kotlin/dev/usbharu/hideout/core/external/media/FileTypeDeterminer.kt b/hideout-core/src/main/kotlin/dev/usbharu/hideout/core/external/media/FileTypeDeterminer.kt new file mode 100644 index 00000000..83270bb6 --- /dev/null +++ b/hideout-core/src/main/kotlin/dev/usbharu/hideout/core/external/media/FileTypeDeterminer.kt @@ -0,0 +1,8 @@ +package dev.usbharu.hideout.core.external.media + +import dev.usbharu.hideout.core.domain.model.media.MimeType +import java.nio.file.Path + +interface FileTypeDeterminer { + fun fileType(path: Path, filename: String): MimeType +} diff --git a/hideout-core/src/main/kotlin/dev/usbharu/hideout/core/external/media/MediaProcessor.kt b/hideout-core/src/main/kotlin/dev/usbharu/hideout/core/external/media/MediaProcessor.kt new file mode 100644 index 00000000..687aa718 --- /dev/null +++ b/hideout-core/src/main/kotlin/dev/usbharu/hideout/core/external/media/MediaProcessor.kt @@ -0,0 +1,25 @@ +/* + * Copyright (C) 2024 usbharu + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package dev.usbharu.hideout.core.external.media + +import dev.usbharu.hideout.core.domain.model.media.MimeType +import java.nio.file.Path + +interface MediaProcessor { + fun isSupported(mimeType: MimeType): Boolean + suspend fun process(path: Path, filename: String, mimeType: MimeType?): ProcessedMedia +} diff --git a/hideout-core/src/main/kotlin/dev/usbharu/hideout/core/external/media/ProcessedMedia.kt b/hideout-core/src/main/kotlin/dev/usbharu/hideout/core/external/media/ProcessedMedia.kt new file mode 100644 index 00000000..b69ece17 --- /dev/null +++ b/hideout-core/src/main/kotlin/dev/usbharu/hideout/core/external/media/ProcessedMedia.kt @@ -0,0 +1,29 @@ +/* + * Copyright (C) 2024 usbharu + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package dev.usbharu.hideout.core.external.media + +import dev.usbharu.hideout.core.domain.model.media.FileType +import dev.usbharu.hideout.core.domain.model.media.MimeType +import java.nio.file.Path + +data class ProcessedMedia( + val path: Path, + val thumbnailPath: Path?, + val fileType: FileType, + val mimeType: MimeType, + val blurHash: String?, +) diff --git a/hideout-core/src/main/kotlin/dev/usbharu/hideout/core/external/media/TikaFileTypeDeterminer.kt b/hideout-core/src/main/kotlin/dev/usbharu/hideout/core/external/media/TikaFileTypeDeterminer.kt new file mode 100644 index 00000000..393798b7 --- /dev/null +++ b/hideout-core/src/main/kotlin/dev/usbharu/hideout/core/external/media/TikaFileTypeDeterminer.kt @@ -0,0 +1,51 @@ +package dev.usbharu.hideout.core.external.media + +import dev.usbharu.hideout.core.domain.model.media.FileType +import dev.usbharu.hideout.core.domain.model.media.MimeType +import org.apache.tika.Tika +import org.slf4j.LoggerFactory +import org.springframework.stereotype.Component +import java.nio.file.Path + +@Component +class TikaFileTypeDeterminer : FileTypeDeterminer { + override fun fileType(path: Path, filename: String): MimeType { + logger.info("START Detect file type name: {}", filename) + + val tika = Tika() + + val detect = try { + tika.detect(path) + } catch (e: IllegalStateException) { + logger.warn("FAILED Detect file type", e) + "application/octet-stream" + } + + val type = detect.substringBefore("/") + val fileType = when (type) { + "image" -> { + FileType.Image + } + + "video" -> { + FileType.Video + } + + "audio" -> { + FileType.Audio + } + + else -> { + FileType.Unknown + } + } + val mimeType = MimeType(type, detect.substringAfter("/"), fileType) + + logger.info("SUCCESS Detect file type name: {},MimeType: {}", filename, mimeType) + return mimeType + } + + companion object { + private val logger = LoggerFactory.getLogger(TikaFileTypeDeterminer::class.java) + } +} diff --git a/hideout-core/src/main/kotlin/dev/usbharu/hideout/core/external/mediastore/MediaStore.kt b/hideout-core/src/main/kotlin/dev/usbharu/hideout/core/external/mediastore/MediaStore.kt new file mode 100644 index 00000000..d2ee30d1 --- /dev/null +++ b/hideout-core/src/main/kotlin/dev/usbharu/hideout/core/external/mediastore/MediaStore.kt @@ -0,0 +1,8 @@ +package dev.usbharu.hideout.core.external.mediastore + +import java.net.URI +import java.nio.file.Path + +interface MediaStore { + suspend fun upload(path: Path, id: String): URI +} diff --git a/hideout-core/src/main/kotlin/dev/usbharu/hideout/core/external/timeline/ReadTimelineOption.kt b/hideout-core/src/main/kotlin/dev/usbharu/hideout/core/external/timeline/ReadTimelineOption.kt new file mode 100644 index 00000000..5cee660f --- /dev/null +++ b/hideout-core/src/main/kotlin/dev/usbharu/hideout/core/external/timeline/ReadTimelineOption.kt @@ -0,0 +1,7 @@ +package dev.usbharu.hideout.core.external.timeline + +data class ReadTimelineOption( + val mediaOnly: Boolean = false, + val local: Boolean = false, + val remote: Boolean = false, +) diff --git a/hideout-core/src/main/kotlin/dev/usbharu/hideout/core/external/timeline/TimelineStore.kt b/hideout-core/src/main/kotlin/dev/usbharu/hideout/core/external/timeline/TimelineStore.kt new file mode 100644 index 00000000..a9536a6c --- /dev/null +++ b/hideout-core/src/main/kotlin/dev/usbharu/hideout/core/external/timeline/TimelineStore.kt @@ -0,0 +1,27 @@ +package dev.usbharu.hideout.core.external.timeline + +import dev.usbharu.hideout.core.domain.model.post.Post +import dev.usbharu.hideout.core.domain.model.post.PostId +import dev.usbharu.hideout.core.domain.model.support.page.Page +import dev.usbharu.hideout.core.domain.model.support.page.PaginationList +import dev.usbharu.hideout.core.domain.model.support.timelineobjectdetail.TimelineObjectDetail +import dev.usbharu.hideout.core.domain.model.timeline.Timeline +import dev.usbharu.hideout.core.domain.model.timelinerelationship.TimelineRelationship + +interface TimelineStore { + suspend fun addPost(post: Post) + suspend fun updatePost(post: Post) + suspend fun removePost(post: Post) + suspend fun addTimelineRelationship(timelineRelationship: TimelineRelationship) + suspend fun removeTimelineRelationship(timelineRelationship: TimelineRelationship) + + suspend fun updateTimelineRelationship(timelineRelationship: TimelineRelationship) + suspend fun addTimeline(timeline: Timeline, timelineRelationshipList: List) + suspend fun removeTimeline(timeline: Timeline) + + suspend fun readTimeline( + timeline: Timeline, + option: ReadTimelineOption? = null, + page: Page? = null + ): PaginationList +} diff --git a/hideout-core/src/main/kotlin/dev/usbharu/hideout/core/infrastructure/awss3/AWSS3MediaStore.kt b/hideout-core/src/main/kotlin/dev/usbharu/hideout/core/infrastructure/awss3/AWSS3MediaStore.kt new file mode 100644 index 00000000..47e30858 --- /dev/null +++ b/hideout-core/src/main/kotlin/dev/usbharu/hideout/core/infrastructure/awss3/AWSS3MediaStore.kt @@ -0,0 +1,50 @@ +package dev.usbharu.hideout.core.infrastructure.awss3 + +import dev.usbharu.hideout.core.config.S3StorageConfig +import dev.usbharu.hideout.core.external.mediastore.MediaStore +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.withContext +import org.slf4j.LoggerFactory +import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty +import org.springframework.stereotype.Component +import software.amazon.awssdk.core.sync.RequestBody +import software.amazon.awssdk.services.s3.S3Client +import software.amazon.awssdk.services.s3.model.PutObjectRequest +import java.net.URI +import java.nio.file.Path + +@Component +@ConditionalOnProperty("hideout.storage.type", havingValue = "s3") +class AWSS3MediaStore( + private val s3StorageConfig: S3StorageConfig, + private val s3Client: S3Client +) : MediaStore { + override suspend fun upload(path: Path, id: String): URI { + logger.info("MEDIA upload. {}", id) + + val fileUploadRequest = PutObjectRequest.builder() + .bucket(s3StorageConfig.bucket) + .key(id) + .build() + + logger.info("MEDIA upload. bucket: {} key: {}", s3StorageConfig.bucket, id) + + withContext(Dispatchers.IO) { + s3Client.putObject(fileUploadRequest, RequestBody.fromFile(path)) + } + val successSavedMedia = URI.create("${s3StorageConfig.publicUrl}/${s3StorageConfig.bucket}/$id") + + logger.info("SUCCESS Media upload. {}", id) + logger.debug( + "name: {} url: {}", + id, + successSavedMedia, + ) + + return successSavedMedia + } + + companion object { + private val logger = LoggerFactory.getLogger(AWSS3MediaStore::class.java) + } +} diff --git a/hideout-core/src/main/kotlin/dev/usbharu/hideout/core/infrastructure/exposed/ActorQueryMapper.kt b/hideout-core/src/main/kotlin/dev/usbharu/hideout/core/infrastructure/exposed/ActorQueryMapper.kt new file mode 100644 index 00000000..e3bcab78 --- /dev/null +++ b/hideout-core/src/main/kotlin/dev/usbharu/hideout/core/infrastructure/exposed/ActorQueryMapper.kt @@ -0,0 +1,54 @@ +/* + * Copyright (C) 2024 usbharu + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package dev.usbharu.hideout.core.infrastructure.exposed + +import dev.usbharu.hideout.core.domain.model.actor.Actor +import dev.usbharu.hideout.core.domain.model.actor.ActorId +import dev.usbharu.hideout.core.infrastructure.exposedrepository.Actors +import dev.usbharu.hideout.core.infrastructure.exposedrepository.ActorsAlsoKnownAs +import org.jetbrains.exposed.sql.Query +import org.jetbrains.exposed.sql.ResultRow +import org.springframework.stereotype.Component + +@Component +class ActorQueryMapper(private val actorResultRowMapper: ResultRowMapper) : QueryMapper { + override fun map(query: Query): List { + return query + .groupBy { it[Actors.id] } + .map { it.value } + .map { + it + .first() + .let(actorResultRowMapper::map) + .apply { + alsoKnownAs = buildAlsoKnownAs(it) + clearDomainEvents() + } + } + } + + private fun buildAlsoKnownAs(it: List) = + it.mapNotNull { resultRow: ResultRow -> + resultRow.getOrNull( + ActorsAlsoKnownAs.alsoKnownAs + )?.let { actorId -> + ActorId( + actorId + ) + } + }.toSet() +} diff --git a/hideout-core/src/main/kotlin/dev/usbharu/hideout/core/infrastructure/exposed/ActorResultRowMapper.kt b/hideout-core/src/main/kotlin/dev/usbharu/hideout/core/infrastructure/exposed/ActorResultRowMapper.kt new file mode 100644 index 00000000..59cb72fc --- /dev/null +++ b/hideout-core/src/main/kotlin/dev/usbharu/hideout/core/infrastructure/exposed/ActorResultRowMapper.kt @@ -0,0 +1,67 @@ +/* + * Copyright (C) 2024 usbharu + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package dev.usbharu.hideout.core.infrastructure.exposed + +import dev.usbharu.hideout.core.domain.model.actor.* +import dev.usbharu.hideout.core.domain.model.emoji.EmojiId +import dev.usbharu.hideout.core.domain.model.instance.InstanceId +import dev.usbharu.hideout.core.domain.model.media.MediaId +import dev.usbharu.hideout.core.domain.model.support.domain.Domain +import dev.usbharu.hideout.core.infrastructure.exposedrepository.Actors +import org.jetbrains.exposed.sql.ResultRow +import org.springframework.stereotype.Component +import java.net.URI + +@Component +class ActorResultRowMapper : ResultRowMapper { + override fun map(resultRow: ResultRow): Actor { + return Actor( + id = ActorId(resultRow[Actors.id]), + name = ActorName(resultRow[Actors.name]), + domain = Domain(resultRow[Actors.domain]), + screenName = ActorScreenName(resultRow[Actors.screenName]), + description = ActorDescription(resultRow[Actors.description]), + inbox = URI.create(resultRow[Actors.inbox]), + outbox = URI.create(resultRow[Actors.outbox]), + url = URI.create(resultRow[Actors.url]), + publicKey = ActorPublicKey(resultRow[Actors.publicKey]), + privateKey = resultRow[Actors.privateKey]?.let { ActorPrivateKey(it) }, + createdAt = resultRow[Actors.createdAt], + keyId = ActorKeyId(resultRow[Actors.keyId]), + followersEndpoint = resultRow[Actors.followers]?.let { URI.create(it) }, + followingEndpoint = resultRow[Actors.following]?.let { URI.create(it) }, + instance = InstanceId(resultRow[Actors.instance]), + locked = resultRow[Actors.locked], + followersCount = resultRow[Actors.followersCount]?.let { ActorRelationshipCount(it) }, + followingCount = resultRow[Actors.followingCount]?.let { ActorRelationshipCount(it) }, + postsCount = ActorPostsCount(resultRow[Actors.postsCount]), + lastPostAt = resultRow[Actors.lastPostAt], + suspend = resultRow[Actors.suspend], + lastUpdateAt = resultRow[Actors.lastUpdateAt], + alsoKnownAs = emptySet(), + moveTo = resultRow[Actors.moveTo]?.let { ActorId(it) }, + emojiIds = resultRow[Actors.emojis] + .split(",") + .filter { it.isNotEmpty() } + .map { EmojiId(it.toLong()) } + .toSet(), + deleted = resultRow[Actors.deleted], + icon = resultRow[Actors.icon]?.let { MediaId(it) }, + banner = resultRow[Actors.banner]?.let { MediaId(it) } + ) + } +} diff --git a/hideout-core/src/main/kotlin/dev/usbharu/hideout/core/infrastructure/exposed/ExposedTransaction.kt b/hideout-core/src/main/kotlin/dev/usbharu/hideout/core/infrastructure/exposed/ExposedTransaction.kt new file mode 100644 index 00000000..7dc419f3 --- /dev/null +++ b/hideout-core/src/main/kotlin/dev/usbharu/hideout/core/infrastructure/exposed/ExposedTransaction.kt @@ -0,0 +1,47 @@ +/* + * Copyright (C) 2024 usbharu + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package dev.usbharu.hideout.core.infrastructure.exposed + +import dev.usbharu.hideout.core.application.shared.Transaction +import kotlinx.coroutines.slf4j.MDCContext +import org.jetbrains.exposed.sql.Slf4jSqlDebugLogger +import org.jetbrains.exposed.sql.addLogger +import org.jetbrains.exposed.sql.transactions.experimental.newSuspendedTransaction +import org.springframework.stereotype.Component +import java.sql.Connection + +@Component +class ExposedTransaction : Transaction { + override suspend fun transaction(block: suspend () -> T): T { + return newSuspendedTransaction( + transactionIsolation = Connection.TRANSACTION_READ_COMMITTED, + context = MDCContext() + ) { + debug = true + warnLongQueriesDuration = 1000 + addLogger(Slf4jSqlDebugLogger) + block() + } + } + + override suspend fun transaction(transactionLevel: Int, block: suspend () -> T): T { + return newSuspendedTransaction(MDCContext(), transactionIsolation = transactionLevel) { + addLogger(Slf4jSqlDebugLogger) + block() + } + } +} diff --git a/hideout-core/src/main/kotlin/dev/usbharu/hideout/core/infrastructure/exposed/FilterQueryMapper.kt b/hideout-core/src/main/kotlin/dev/usbharu/hideout/core/infrastructure/exposed/FilterQueryMapper.kt new file mode 100644 index 00000000..5b6039c4 --- /dev/null +++ b/hideout-core/src/main/kotlin/dev/usbharu/hideout/core/infrastructure/exposed/FilterQueryMapper.kt @@ -0,0 +1,53 @@ +/* + * Copyright (C) 2024 usbharu + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package dev.usbharu.hideout.core.infrastructure.exposed + +import dev.usbharu.hideout.core.domain.model.filter.* +import dev.usbharu.hideout.core.infrastructure.exposedrepository.FilterKeywords +import dev.usbharu.hideout.core.infrastructure.exposedrepository.Filters +import org.jetbrains.exposed.sql.Query +import org.jetbrains.exposed.sql.ResultRow +import org.springframework.stereotype.Component + +@Component +class FilterQueryMapper(private val filterResultRowMapper: ResultRowMapper) : QueryMapper { + override fun map(query: Query): List { + return query + .groupBy { it[Filters.id] } + .map { it.value } + .map { + it + .first() + .let(filterResultRowMapper::map) + .apply { + reconstructWith( + it.mapNotNull { resultRow: ResultRow -> + FilterKeyword( + FilterKeywordId(resultRow.getOrNull(FilterKeywords.id) ?: return@mapNotNull null), + FilterKeywordKeyword( + resultRow.getOrNull(FilterKeywords.keyword) ?: return@mapNotNull null + ), + FilterMode.valueOf( + resultRow.getOrNull(FilterKeywords.mode) ?: return@mapNotNull null + ) + ) + }.toSet() + ) + } + } + } +} diff --git a/hideout-core/src/main/kotlin/dev/usbharu/hideout/core/infrastructure/exposed/FilterResultRowMapper.kt b/hideout-core/src/main/kotlin/dev/usbharu/hideout/core/infrastructure/exposed/FilterResultRowMapper.kt new file mode 100644 index 00000000..5b18bb91 --- /dev/null +++ b/hideout-core/src/main/kotlin/dev/usbharu/hideout/core/infrastructure/exposed/FilterResultRowMapper.kt @@ -0,0 +1,37 @@ +/* + * Copyright (C) 2024 usbharu + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package dev.usbharu.hideout.core.infrastructure.exposed + +import dev.usbharu.hideout.core.domain.model.filter.* +import dev.usbharu.hideout.core.domain.model.userdetails.UserDetailId +import dev.usbharu.hideout.core.infrastructure.exposedrepository.Filters +import org.jetbrains.exposed.sql.ResultRow +import org.springframework.stereotype.Component + +@Component +class FilterResultRowMapper : ResultRowMapper { + override fun map(resultRow: ResultRow): Filter = Filter( + id = FilterId(resultRow[Filters.id]), + userDetailId = UserDetailId(resultRow[Filters.userId]), + name = FilterName(resultRow[Filters.name]), + filterContext = resultRow[Filters.context].split(",").filter { + it.isNotEmpty() + }.map { FilterContext.valueOf(it) }.toSet(), + filterAction = FilterAction.valueOf(resultRow[Filters.filterAction]), + filterKeywords = emptySet() + ) +} diff --git a/hideout-core/src/main/kotlin/dev/usbharu/hideout/core/infrastructure/exposed/PostQueryMapper.kt b/hideout-core/src/main/kotlin/dev/usbharu/hideout/core/infrastructure/exposed/PostQueryMapper.kt new file mode 100644 index 00000000..f3cf3407 --- /dev/null +++ b/hideout-core/src/main/kotlin/dev/usbharu/hideout/core/infrastructure/exposed/PostQueryMapper.kt @@ -0,0 +1,67 @@ +/* + * Copyright (C) 2024 usbharu + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package dev.usbharu.hideout.core.infrastructure.exposed + +import dev.usbharu.hideout.core.domain.model.actor.ActorId +import dev.usbharu.hideout.core.domain.model.emoji.EmojiId +import dev.usbharu.hideout.core.domain.model.media.MediaId +import dev.usbharu.hideout.core.domain.model.post.Post +import dev.usbharu.hideout.core.infrastructure.exposedrepository.Posts +import dev.usbharu.hideout.core.infrastructure.exposedrepository.PostsEmojis +import dev.usbharu.hideout.core.infrastructure.exposedrepository.PostsMedia +import dev.usbharu.hideout.core.infrastructure.exposedrepository.PostsVisibleActors +import org.jetbrains.exposed.sql.Query +import org.jetbrains.exposed.sql.ResultRow +import org.springframework.stereotype.Component + +@Component +class PostQueryMapper(private val postResultRowMapper: ResultRowMapper) : QueryMapper { + override fun map(query: Query): List { + return query + .groupBy { it[Posts.id] } + .map { it.value } + .map { + it + .first() + .let(postResultRowMapper::map) + .apply { + buildPost(it) + } + } + } + + private fun Post.buildPost(it: List) { + reconstructWith( + mediaIds = it.mapNotNull { resultRow: ResultRow -> + resultRow + .getOrNull(PostsMedia.mediaId) + ?.let { mediaId -> MediaId(mediaId) } + }, + emojis = it + .mapNotNull { resultRow: ResultRow -> + resultRow + .getOrNull(PostsEmojis.emojiId) + ?.let { emojiId -> EmojiId(emojiId) } + }, + visibleActors = it.mapNotNull { resultRow: ResultRow -> + resultRow + .getOrNull(PostsVisibleActors.actorId) + ?.let { actorId -> ActorId(actorId) } + }.toSet() + ) + } +} diff --git a/hideout-core/src/main/kotlin/dev/usbharu/hideout/core/infrastructure/exposed/PostResultRowMapper.kt b/hideout-core/src/main/kotlin/dev/usbharu/hideout/core/infrastructure/exposed/PostResultRowMapper.kt new file mode 100644 index 00000000..fccc7166 --- /dev/null +++ b/hideout-core/src/main/kotlin/dev/usbharu/hideout/core/infrastructure/exposed/PostResultRowMapper.kt @@ -0,0 +1,50 @@ +/* + * Copyright (C) 2024 usbharu + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package dev.usbharu.hideout.core.infrastructure.exposed + +import dev.usbharu.hideout.core.domain.model.actor.ActorId +import dev.usbharu.hideout.core.domain.model.instance.InstanceId +import dev.usbharu.hideout.core.domain.model.post.* +import dev.usbharu.hideout.core.infrastructure.exposedrepository.Posts +import org.jetbrains.exposed.sql.ResultRow +import org.springframework.stereotype.Component +import java.net.URI + +@Component +class PostResultRowMapper : ResultRowMapper { + override fun map(resultRow: ResultRow): Post { + return Post( + id = PostId(resultRow[Posts.id]), + actorId = ActorId(resultRow[Posts.actorId]), + instanceId = InstanceId(resultRow[Posts.instanceId]), + overview = resultRow[Posts.overview]?.let { PostOverview(it) }, + content = PostContent(resultRow[Posts.text], resultRow[Posts.content], emptyList()), + createdAt = resultRow[Posts.createdAt], + visibility = Visibility.valueOf(resultRow[Posts.visibility]), + url = URI.create(resultRow[Posts.url]), + repostId = resultRow[Posts.repostId]?.let { PostId(it) }, + replyId = resultRow[Posts.replyId]?.let { PostId(it) }, + sensitive = resultRow[Posts.sensitive], + apId = URI.create(resultRow[Posts.apId]), + deleted = resultRow[Posts.deleted], + mediaIds = emptyList(), + visibleActors = emptySet(), + hide = resultRow[Posts.hide], + moveTo = resultRow[Posts.moveTo]?.let { PostId(it) } + ) + } +} diff --git a/hideout-core/src/main/kotlin/dev/usbharu/hideout/core/infrastructure/exposed/QueryMapper.kt b/hideout-core/src/main/kotlin/dev/usbharu/hideout/core/infrastructure/exposed/QueryMapper.kt new file mode 100644 index 00000000..a817b9b2 --- /dev/null +++ b/hideout-core/src/main/kotlin/dev/usbharu/hideout/core/infrastructure/exposed/QueryMapper.kt @@ -0,0 +1,23 @@ +/* + * Copyright (C) 2024 usbharu + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package dev.usbharu.hideout.core.infrastructure.exposed + +import org.jetbrains.exposed.sql.Query + +interface QueryMapper { + fun map(query: Query): List +} diff --git a/hideout-core/src/main/kotlin/dev/usbharu/hideout/core/infrastructure/exposed/ResultRowMapper.kt b/hideout-core/src/main/kotlin/dev/usbharu/hideout/core/infrastructure/exposed/ResultRowMapper.kt new file mode 100644 index 00000000..7f4b3261 --- /dev/null +++ b/hideout-core/src/main/kotlin/dev/usbharu/hideout/core/infrastructure/exposed/ResultRowMapper.kt @@ -0,0 +1,23 @@ +/* + * Copyright (C) 2024 usbharu + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package dev.usbharu.hideout.core.infrastructure.exposed + +import org.jetbrains.exposed.sql.ResultRow + +interface ResultRowMapper { + fun map(resultRow: ResultRow): T +} diff --git a/hideout-core/src/main/kotlin/dev/usbharu/hideout/core/infrastructure/exposedquery/ExposedPrincipalQueryService.kt b/hideout-core/src/main/kotlin/dev/usbharu/hideout/core/infrastructure/exposedquery/ExposedPrincipalQueryService.kt new file mode 100644 index 00000000..413ccc67 --- /dev/null +++ b/hideout-core/src/main/kotlin/dev/usbharu/hideout/core/infrastructure/exposedquery/ExposedPrincipalQueryService.kt @@ -0,0 +1,37 @@ +package dev.usbharu.hideout.core.infrastructure.exposedquery + +import dev.usbharu.hideout.core.domain.model.actor.ActorId +import dev.usbharu.hideout.core.domain.model.userdetails.UserDetailId +import dev.usbharu.hideout.core.infrastructure.exposedrepository.AbstractRepository +import dev.usbharu.hideout.core.infrastructure.exposedrepository.Actors +import dev.usbharu.hideout.core.infrastructure.exposedrepository.UserDetails +import dev.usbharu.hideout.core.query.principal.PrincipalDTO +import dev.usbharu.hideout.core.query.principal.PrincipalQueryService +import org.jetbrains.exposed.sql.selectAll +import org.slf4j.Logger +import org.slf4j.LoggerFactory +import org.springframework.stereotype.Repository + +@Repository +class ExposedPrincipalQueryService : PrincipalQueryService, AbstractRepository() { + override suspend fun findByUserDetailId(userDetailId: UserDetailId): PrincipalDTO { + return query { + UserDetails.leftJoin(Actors).selectAll().where { UserDetails.id eq userDetailId.id }.single() + .let { + PrincipalDTO( + UserDetailId(it[UserDetails.id]), + ActorId(it[UserDetails.actorId]), + it[Actors.name], + it[Actors.domain] + ) + } + } + } + + override val logger: Logger + get() = Companion.logger + + companion object { + private val logger: Logger = LoggerFactory.getLogger(ExposedPrincipalQueryService::class.java) + } +} \ No newline at end of file diff --git a/hideout-core/src/main/kotlin/dev/usbharu/hideout/core/infrastructure/exposedrepository/AbstractRepository.kt b/hideout-core/src/main/kotlin/dev/usbharu/hideout/core/infrastructure/exposedrepository/AbstractRepository.kt new file mode 100644 index 00000000..4d578f90 --- /dev/null +++ b/hideout-core/src/main/kotlin/dev/usbharu/hideout/core/infrastructure/exposedrepository/AbstractRepository.kt @@ -0,0 +1,81 @@ +/* + * Copyright (C) 2024 usbharu + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package dev.usbharu.hideout.core.infrastructure.exposedrepository + +import dev.usbharu.hideout.core.domain.exception.SpringDataAccessExceptionSQLExceptionTranslator +import org.jetbrains.exposed.sql.transactions.TransactionManager +import org.slf4j.Logger +import org.springframework.beans.factory.annotation.Value +import org.springframework.jdbc.support.SQLErrorCodeSQLExceptionTranslator +import java.sql.SQLException + +@Suppress("VarCouldBeVal") +abstract class AbstractRepository { + protected abstract val logger: Logger + private val sqlErrorCodeSQLExceptionTranslator = SQLErrorCodeSQLExceptionTranslator() + private val springDataAccessExceptionSQLExceptionTranslator = SpringDataAccessExceptionSQLExceptionTranslator() + + @Value("\${hideout.debug.trace-query-exception:false}") + private var traceQueryException: Boolean = false + + @Value("\${hideout.debug.trace-query-call:false}") + private var traceQueryCall: Boolean = false + + protected suspend fun query(block: () -> T): T = try { + if (traceQueryCall) { + @Suppress("ThrowingExceptionsWithoutMessageOrCause") + logger.trace( + """ +***** QUERY CALL STACK TRACE ***** + +${Throwable().stackTrace.joinToString("\n")} + +***** QUERY CALL STACK TRACE ***** +""" + ) + } + + block.invoke() + } catch (e: SQLException) { + if (traceQueryException) { + logger.trace("FAILED EXECUTE SQL", e) + } + TransactionManager.currentOrNull()?.rollback() + if (e.cause !is SQLException) { + throw e + } + + val dataAccessException = + sqlErrorCodeSQLExceptionTranslator.translate("Failed to persist entity", null, e.cause as SQLException) + ?: throw e + + if (traceQueryException) { + logger.trace("EXCEPTION TRANSLATED TO", dataAccessException) + } + + val translate = springDataAccessExceptionSQLExceptionTranslator.translate( + "Failed to persist entity", + null, + dataAccessException + ) + + if (traceQueryException) { + logger.trace("EXCEPTION TRANSLATED TO", translate) + } + throw translate + } +} diff --git a/hideout-core/src/main/kotlin/dev/usbharu/hideout/core/infrastructure/exposedrepository/CustomEmojiRepositoryImpl.kt b/hideout-core/src/main/kotlin/dev/usbharu/hideout/core/infrastructure/exposedrepository/CustomEmojiRepositoryImpl.kt new file mode 100644 index 00000000..56fcc6d1 --- /dev/null +++ b/hideout-core/src/main/kotlin/dev/usbharu/hideout/core/infrastructure/exposedrepository/CustomEmojiRepositoryImpl.kt @@ -0,0 +1,132 @@ +/* + * Copyright (C) 2024 usbharu + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package dev.usbharu.hideout.core.infrastructure.exposedrepository + +import dev.usbharu.hideout.core.domain.model.emoji.CustomEmoji +import dev.usbharu.hideout.core.domain.model.emoji.CustomEmojiRepository +import dev.usbharu.hideout.core.domain.model.emoji.EmojiId +import dev.usbharu.hideout.core.domain.model.instance.InstanceId +import dev.usbharu.hideout.core.domain.model.support.domain.Domain +import org.jetbrains.exposed.sql.* +import org.jetbrains.exposed.sql.SqlExpressionBuilder.eq +import org.jetbrains.exposed.sql.javatime.CurrentTimestamp +import org.jetbrains.exposed.sql.javatime.timestamp +import org.slf4j.Logger +import org.slf4j.LoggerFactory +import org.springframework.stereotype.Repository +import java.net.URI + +@Repository +class CustomEmojiRepositoryImpl : CustomEmojiRepository, + AbstractRepository() { + override val logger: Logger + get() = Companion.logger + + override suspend fun save(customEmoji: CustomEmoji): CustomEmoji = query { + val singleOrNull = + CustomEmojis.selectAll().where { CustomEmojis.id eq customEmoji.id.emojiId }.forUpdate().singleOrNull() + if (singleOrNull == null) { + CustomEmojis.insert { + it[id] = customEmoji.id.emojiId + it[name] = customEmoji.name + it[domain] = customEmoji.domain.domain + it[instanceId] = customEmoji.instanceId.instanceId + it[url] = customEmoji.url.toString() + it[category] = customEmoji.category + it[createdAt] = customEmoji.createdAt + } + } else { + CustomEmojis.update({ CustomEmojis.id eq customEmoji.id.emojiId }) { + it[name] = customEmoji.name + it[domain] = customEmoji.domain.domain + it[instanceId] = customEmoji.instanceId.instanceId + it[url] = customEmoji.url.toString() + it[category] = customEmoji.category + it[createdAt] = customEmoji.createdAt + } + } + return@query customEmoji + } + + override suspend fun findById(id: Long): CustomEmoji? = query { + return@query CustomEmojis.selectAll().where { CustomEmojis.id eq id }.singleOrNull()?.toCustomEmoji() + } + + override suspend fun delete(customEmoji: CustomEmoji): Unit = query { + CustomEmojis.deleteWhere { id eq customEmoji.id.emojiId } + } + + override suspend fun findByNamesAndDomain(names: List, domain: String): List = query { + return@query CustomEmojis + .selectAll() + .where { + CustomEmojis.name inList names and (CustomEmojis.domain eq domain) + } + .map { it.toCustomEmoji() } + } + + override suspend fun findByIds(ids: List): List = query { + return@query CustomEmojis + .selectAll() + .where { + CustomEmojis.id inList ids + } + .map { it.toCustomEmoji() } + } + + companion object { + private val logger = LoggerFactory.getLogger(CustomEmojiRepositoryImpl::class.java) + } +} + +fun ResultRow.toCustomEmoji(): CustomEmoji = CustomEmoji( + id = EmojiId(this[CustomEmojis.id]), + name = this[CustomEmojis.name], + domain = Domain(this[CustomEmojis.domain]), + instanceId = InstanceId(this[CustomEmojis.instanceId]), + url = URI.create(this[CustomEmojis.url]), + category = this[CustomEmojis.category], + createdAt = this[CustomEmojis.createdAt] +) + +fun ResultRow.toCustomEmojiOrNull(): CustomEmoji? { + return CustomEmoji( + id = EmojiId(this.getOrNull(CustomEmojis.id) ?: return null), + name = this.getOrNull(CustomEmojis.name) ?: return null, + domain = Domain(this.getOrNull(CustomEmojis.domain) ?: return null), + instanceId = InstanceId(this.getOrNull(CustomEmojis.instanceId) ?: return null), + url = URI.create(this.getOrNull(CustomEmojis.url) ?: return null), + category = this[CustomEmojis.category], + createdAt = this.getOrNull(CustomEmojis.createdAt) ?: return null + ) +} + +object CustomEmojis : Table("emojis") { + val id = long("id") + val name = varchar("name", 1000) + val domain = varchar("domain", 1000) + val instanceId = long("instance_id").references(Instance.id) + val url = varchar("url", 255).uniqueIndex() + val category = varchar("category", 255).nullable() + val createdAt = timestamp("created_at").defaultExpression(CurrentTimestamp) + + override val primaryKey: PrimaryKey = PrimaryKey(id) + + init { + uniqueIndex(name, instanceId) + } +} diff --git a/hideout-core/src/main/kotlin/dev/usbharu/hideout/core/infrastructure/exposedrepository/ExposedActorInstanceRelationshipRepository.kt b/hideout-core/src/main/kotlin/dev/usbharu/hideout/core/infrastructure/exposedrepository/ExposedActorInstanceRelationshipRepository.kt new file mode 100644 index 00000000..61bccf73 --- /dev/null +++ b/hideout-core/src/main/kotlin/dev/usbharu/hideout/core/infrastructure/exposedrepository/ExposedActorInstanceRelationshipRepository.kt @@ -0,0 +1,100 @@ +/* + * Copyright (C) 2024 usbharu + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package dev.usbharu.hideout.core.infrastructure.exposedrepository + +import dev.usbharu.hideout.core.domain.model.actor.ActorId +import dev.usbharu.hideout.core.domain.model.actorinstancerelationship.ActorInstanceRelationship +import dev.usbharu.hideout.core.domain.model.actorinstancerelationship.ActorInstanceRelationshipRepository +import dev.usbharu.hideout.core.domain.model.instance.InstanceId +import dev.usbharu.hideout.core.domain.shared.domainevent.DomainEventPublisher +import dev.usbharu.hideout.core.domain.shared.repository.DomainEventPublishableRepository +import org.jetbrains.exposed.sql.* +import org.jetbrains.exposed.sql.SqlExpressionBuilder.eq +import org.slf4j.Logger +import org.slf4j.LoggerFactory +import org.springframework.stereotype.Repository + +@Repository +class ExposedActorInstanceRelationshipRepository(override val domainEventPublisher: DomainEventPublisher) : + ActorInstanceRelationshipRepository, + AbstractRepository(), + DomainEventPublishableRepository { + override val logger: Logger + get() = Companion.logger + + override suspend fun save(actorInstanceRelationship: ActorInstanceRelationship): ActorInstanceRelationship { + query { + ActorInstanceRelationships.upsert { + it[actorId] = actorInstanceRelationship.actorId.id + it[instanceId] = actorInstanceRelationship.instanceId.instanceId + it[blocking] = actorInstanceRelationship.blocking + it[muting] = actorInstanceRelationship.muting + it[doNotSendPrivate] = actorInstanceRelationship.doNotSendPrivate + } + } + update(actorInstanceRelationship) + return actorInstanceRelationship + } + + override suspend fun delete(actorInstanceRelationship: ActorInstanceRelationship) { + query { + ActorInstanceRelationships.deleteWhere { + actorId eq actorInstanceRelationship.actorId.id and + (instanceId eq actorInstanceRelationship.instanceId.instanceId) + } + } + update(actorInstanceRelationship) + } + + override suspend fun findByActorIdAndInstanceId( + actorId: ActorId, + instanceId: InstanceId, + ): ActorInstanceRelationship? = query { + ActorInstanceRelationships + .selectAll() + .where { + ActorInstanceRelationships.actorId eq actorId.id and + (ActorInstanceRelationships.instanceId eq instanceId.instanceId) + } + .singleOrNull() + ?.toActorInstanceRelationship() + } + + companion object { + private val logger = LoggerFactory.getLogger(ExposedActorInstanceRelationshipRepository::class.java) + } +} + +private fun ResultRow.toActorInstanceRelationship(): ActorInstanceRelationship { + return ActorInstanceRelationship( + actorId = ActorId(this[ActorInstanceRelationships.actorId]), + instanceId = InstanceId(this[ActorInstanceRelationships.instanceId]), + blocking = this[ActorInstanceRelationships.blocking], + muting = this[ActorInstanceRelationships.muting], + doNotSendPrivate = this[ActorInstanceRelationships.doNotSendPrivate], + ) +} + +object ActorInstanceRelationships : Table("actor_instance_relationships") { + val actorId = long("actor_id").references(Actors.id) + val instanceId = long("instance_id").references(Instance.id) + val blocking = bool("blocking") + val muting = bool("muting") + val doNotSendPrivate = bool("do_not_send_private") + + override val primaryKey: PrimaryKey = PrimaryKey(actorId, instanceId) +} diff --git a/hideout-core/src/main/kotlin/dev/usbharu/hideout/core/infrastructure/exposedrepository/ExposedActorRepository.kt b/hideout-core/src/main/kotlin/dev/usbharu/hideout/core/infrastructure/exposedrepository/ExposedActorRepository.kt new file mode 100644 index 00000000..96e663e2 --- /dev/null +++ b/hideout-core/src/main/kotlin/dev/usbharu/hideout/core/infrastructure/exposedrepository/ExposedActorRepository.kt @@ -0,0 +1,160 @@ +package dev.usbharu.hideout.core.infrastructure.exposedrepository + +import dev.usbharu.hideout.core.domain.model.actor.* +import dev.usbharu.hideout.core.domain.model.support.domain.Domain +import dev.usbharu.hideout.core.domain.shared.domainevent.DomainEventPublisher +import dev.usbharu.hideout.core.domain.shared.repository.DomainEventPublishableRepository +import dev.usbharu.hideout.core.infrastructure.exposed.QueryMapper +import org.jetbrains.exposed.sql.* +import org.jetbrains.exposed.sql.SqlExpressionBuilder.eq +import org.jetbrains.exposed.sql.javatime.timestamp +import org.slf4j.Logger +import org.slf4j.LoggerFactory +import org.springframework.stereotype.Repository + +@Repository +class ExposedActorRepository( + private val actorQueryMapper: QueryMapper, + override val domainEventPublisher: DomainEventPublisher, +) : AbstractRepository(), + DomainEventPublishableRepository, + ActorRepository { + override val logger: Logger + get() = Companion.logger + + override suspend fun save(actor: Actor): Actor { + query { + Actors.upsert { + it[id] = actor.id.id + it[name] = actor.name.name + it[domain] = actor.domain.domain + it[screenName] = actor.screenName.screenName + it[description] = actor.description.description + it[inbox] = actor.inbox.toString() + it[outbox] = actor.outbox.toString() + it[url] = actor.outbox.toString() + it[publicKey] = actor.publicKey.publicKey + it[privateKey] = actor.privateKey?.privateKey + it[createdAt] = actor.createdAt + it[keyId] = actor.keyId.keyId + it[following] = actor.followingEndpoint?.toString() + it[followers] = actor.followersEndpoint?.toString() + it[instance] = actor.instance.instanceId + it[locked] = actor.locked + it[followingCount] = actor.followingCount?.relationshipCount + it[followersCount] = actor.followersCount?.relationshipCount + it[postsCount] = actor.postsCount.postsCount + it[lastPostAt] = actor.lastPostAt + it[lastUpdateAt] = actor.lastUpdateAt + it[suspend] = actor.suspend + it[moveTo] = actor.moveTo?.id + it[emojis] = actor.emojis.joinToString(",") + it[icon] = actor.icon?.id + it[banner] = actor.banner?.id + } + ActorsAlsoKnownAs.deleteWhere { + actorId eq actor.id.id + } + ActorsAlsoKnownAs.batchInsert(actor.alsoKnownAs) { + this[ActorsAlsoKnownAs.actorId] = actor.id.id + this[ActorsAlsoKnownAs.alsoKnownAs] = it.id + } + } + update(actor) + return actor + } + + override suspend fun delete(actor: Actor) { + query { + Actors.deleteWhere { id eq actor.id.id } + ActorsAlsoKnownAs.deleteWhere { actorId eq actor.id.id } + } + update(actor) + } + + override suspend fun findById(id: ActorId): Actor? { + return query { + Actors + .leftJoin(ActorsAlsoKnownAs, onColumn = { Actors.id }, otherColumn = { actorId }) + .selectAll() + .where { + Actors.id eq id.id + } + .let(actorQueryMapper::map) + .firstOrNull() + } + } + + override suspend fun findByNameAndDomain(name: String, domain: String): Actor? { + return query { + Actors + .leftJoin(ActorsAlsoKnownAs, onColumn = { id }, otherColumn = { actorId }) + .selectAll() + .where { + Actors.name eq name and (Actors.domain eq domain) + } + .let(actorQueryMapper::map) + .firstOrNull() + } + } + + override suspend fun findAllById(actorIds: List): List { + return query { + Actors + .leftJoin(ActorsAlsoKnownAs, onColumn = { id }, otherColumn = { actorId }) + .selectAll() + .where { + Actors.id inList actorIds.map { it.id } + } + .let(actorQueryMapper::map) + } + } + + companion object { + private val logger = LoggerFactory.getLogger(ExposedActorRepository::class.java) + } +} + +object Actors : Table("actors") { + val id = long("id") + val name = varchar("name", ActorName.LENGTH) + val domain = varchar("domain", Domain.LENGTH) + val screenName = varchar("screen_name", ActorScreenName.LENGTH) + val description = varchar("description", ActorDescription.LENGTH) + val inbox = varchar("inbox", 1000).uniqueIndex() + val outbox = varchar("outbox", 1000).uniqueIndex() + val url = varchar("url", 1000).uniqueIndex() + val publicKey = varchar("public_key", 10000) + val privateKey = varchar("private_key", 100000).nullable() + val createdAt = timestamp("created_at") + val keyId = varchar("key_id", 1000) + val following = varchar("following", 1000).nullable() + val followers = varchar("followers", 1000).nullable() + val instance = long("instance").references(Instance.id) + val locked = bool("locked") + val followingCount = integer("following_count").nullable() + val followersCount = integer("followers_count").nullable() + val postsCount = integer("posts_count") + val lastPostAt = timestamp("last_post_at").nullable() + val lastUpdateAt = timestamp("last_update_at") + val suspend = bool("suspend") + val moveTo = long("move_to").references(id).nullable() + val emojis = varchar("emojis", 3000) + val deleted = bool("deleted") + val banner = long("banner").references(Media.id).nullable() + val icon = long("icon").references(Media.id).nullable() + + override val primaryKey = PrimaryKey(id) + + init { + uniqueIndex(name, domain) + } +} + +object ActorsAlsoKnownAs : Table("actor_alsoknownas") { + val actorId = + long("actor_id").references(Actors.id, onDelete = ReferenceOption.CASCADE, onUpdate = ReferenceOption.CASCADE) + val alsoKnownAs = long("also_known_as").references(Actors.id, ReferenceOption.CASCADE, ReferenceOption.CASCADE) + + override val primaryKey: PrimaryKey = PrimaryKey(actorId, alsoKnownAs) +} diff --git a/hideout-core/src/main/kotlin/dev/usbharu/hideout/core/infrastructure/exposedrepository/ExposedApplicationRepository.kt b/hideout-core/src/main/kotlin/dev/usbharu/hideout/core/infrastructure/exposedrepository/ExposedApplicationRepository.kt new file mode 100644 index 00000000..42d5c460 --- /dev/null +++ b/hideout-core/src/main/kotlin/dev/usbharu/hideout/core/infrastructure/exposedrepository/ExposedApplicationRepository.kt @@ -0,0 +1,55 @@ +/* + * Copyright (C) 2024 usbharu + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package dev.usbharu.hideout.core.infrastructure.exposedrepository + +import dev.usbharu.hideout.core.domain.model.application.Application +import dev.usbharu.hideout.core.domain.model.application.ApplicationRepository +import org.jetbrains.exposed.sql.SqlExpressionBuilder.eq +import org.jetbrains.exposed.sql.Table +import org.jetbrains.exposed.sql.deleteWhere +import org.jetbrains.exposed.sql.upsert +import org.slf4j.Logger +import org.slf4j.LoggerFactory +import org.springframework.stereotype.Repository + +@Repository +class ExposedApplicationRepository : ApplicationRepository, AbstractRepository() { + override val logger: Logger + get() = Companion.logger + + override suspend fun save(application: Application) = query { + Applications.upsert { + it[id] = application.applicationId.id + it[name] = application.name.name + } + application + } + + override suspend fun delete(application: Application): Unit = query { + Applications.deleteWhere { id eq application.applicationId.id } + } + + companion object { + private val logger = LoggerFactory.getLogger(ExposedApplicationRepository::class.java) + } +} + +object Applications : Table("applications") { + val id = long("id") + val name = varchar("name", 500) + override val primaryKey: PrimaryKey = PrimaryKey(id) +} diff --git a/hideout-core/src/main/kotlin/dev/usbharu/hideout/core/infrastructure/exposedrepository/ExposedFilterRepository.kt b/hideout-core/src/main/kotlin/dev/usbharu/hideout/core/infrastructure/exposedrepository/ExposedFilterRepository.kt new file mode 100644 index 00000000..ae06094f --- /dev/null +++ b/hideout-core/src/main/kotlin/dev/usbharu/hideout/core/infrastructure/exposedrepository/ExposedFilterRepository.kt @@ -0,0 +1,103 @@ +/* + * Copyright (C) 2024 usbharu + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package dev.usbharu.hideout.core.infrastructure.exposedrepository + +import dev.usbharu.hideout.core.domain.model.filter.Filter +import dev.usbharu.hideout.core.domain.model.filter.FilterId +import dev.usbharu.hideout.core.domain.model.filter.FilterKeywordId +import dev.usbharu.hideout.core.domain.model.filter.FilterRepository +import dev.usbharu.hideout.core.domain.model.userdetails.UserDetailId +import dev.usbharu.hideout.core.infrastructure.exposed.QueryMapper +import org.jetbrains.exposed.sql.* +import org.jetbrains.exposed.sql.SqlExpressionBuilder.eq +import org.slf4j.Logger +import org.slf4j.LoggerFactory +import org.springframework.stereotype.Repository + +@Repository +class ExposedFilterRepository(private val filterQueryMapper: QueryMapper) : FilterRepository, + AbstractRepository() { + override val logger: Logger + get() = Companion.logger + + override suspend fun save(filter: Filter): Filter = query { + Filters.upsert { upsertStatement -> + upsertStatement[id] = filter.id.id + upsertStatement[userId] = filter.userDetailId.id + upsertStatement[name] = filter.name.name + upsertStatement[context] = filter.filterContext.joinToString(",") { it.name } + upsertStatement[filterAction] = filter.filterAction.name + } + FilterKeywords.deleteWhere { + filterId eq filter.id.id + } + FilterKeywords.batchUpsert(filter.filterKeywords) { + this[FilterKeywords.id] = it.id.id + this[FilterKeywords.filterId] = filter.id.id + this[FilterKeywords.keyword] = it.keyword.keyword + this[FilterKeywords.mode] = it.mode.name + } + filter + } + + override suspend fun delete(filter: Filter): Unit = query { + FilterKeywords.deleteWhere { filterId eq filter.id.id } + Filters.deleteWhere { id eq filter.id.id } + } + + override suspend fun findByFilterKeywordId(filterKeywordId: FilterKeywordId): Filter? { + val filterId = FilterKeywords + .selectAll() + .where { FilterKeywords.id eq filterKeywordId.id } + .firstOrNull()?.get(FilterKeywords.filterId) ?: return null + val where = Filters.selectAll().where { Filters.id eq filterId } + return filterQueryMapper.map(where).firstOrNull() + } + + override suspend fun findByFilterId(filterId: FilterId): Filter? { + val where = Filters.selectAll().where { Filters.id eq filterId.id } + return filterQueryMapper.map(where).firstOrNull() + } + + override suspend fun findByUserDetailId(userDetailId: UserDetailId): List { + return Filters.selectAll().where { Filters.userId eq userDetailId.id }.let(filterQueryMapper::map) + } + + companion object { + private val logger = LoggerFactory.getLogger(ExposedFilterRepository::class.java) + } +} + +object Filters : Table("filters") { + val id = long("id") + val userId = long("user_id").references(UserDetails.id, ReferenceOption.CASCADE, ReferenceOption.CASCADE) + val name = varchar("name", 255) + val context = varchar("context", 500) + val filterAction = varchar("action", 255) + + override val primaryKey: PrimaryKey = PrimaryKey(id) +} + +object FilterKeywords : Table("filter_keywords") { + val id = long("id") + val filterId = + long("filter_id").references(Filters.id, onDelete = ReferenceOption.CASCADE, onUpdate = ReferenceOption.CASCADE) + val keyword = varchar("keyword", 1000) + val mode = varchar("mode", 100) + + override val primaryKey: PrimaryKey = PrimaryKey(id) +} diff --git a/hideout-core/src/main/kotlin/dev/usbharu/hideout/core/infrastructure/exposedrepository/ExposedPostRepository.kt b/hideout-core/src/main/kotlin/dev/usbharu/hideout/core/infrastructure/exposedrepository/ExposedPostRepository.kt new file mode 100644 index 00000000..e9b09eba --- /dev/null +++ b/hideout-core/src/main/kotlin/dev/usbharu/hideout/core/infrastructure/exposedrepository/ExposedPostRepository.kt @@ -0,0 +1,257 @@ +/* + * Copyright (C) 2024 usbharu + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package dev.usbharu.hideout.core.infrastructure.exposedrepository + +import dev.usbharu.hideout.core.domain.model.actor.ActorId +import dev.usbharu.hideout.core.domain.model.post.* +import dev.usbharu.hideout.core.domain.model.support.page.Page +import dev.usbharu.hideout.core.domain.model.support.page.PaginationList +import dev.usbharu.hideout.core.domain.shared.domainevent.DomainEventPublisher +import dev.usbharu.hideout.core.domain.shared.repository.DomainEventPublishableRepository +import dev.usbharu.hideout.core.infrastructure.exposed.QueryMapper +import dev.usbharu.hideout.core.infrastructure.exposedrepository.Posts.actorId +import dev.usbharu.hideout.core.infrastructure.exposedrepository.Posts.apId +import dev.usbharu.hideout.core.infrastructure.exposedrepository.Posts.content +import dev.usbharu.hideout.core.infrastructure.exposedrepository.Posts.createdAt +import dev.usbharu.hideout.core.infrastructure.exposedrepository.Posts.deleted +import dev.usbharu.hideout.core.infrastructure.exposedrepository.Posts.hide +import dev.usbharu.hideout.core.infrastructure.exposedrepository.Posts.id +import dev.usbharu.hideout.core.infrastructure.exposedrepository.Posts.moveTo +import dev.usbharu.hideout.core.infrastructure.exposedrepository.Posts.overview +import dev.usbharu.hideout.core.infrastructure.exposedrepository.Posts.replyId +import dev.usbharu.hideout.core.infrastructure.exposedrepository.Posts.repostId +import dev.usbharu.hideout.core.infrastructure.exposedrepository.Posts.sensitive +import dev.usbharu.hideout.core.infrastructure.exposedrepository.Posts.text +import dev.usbharu.hideout.core.infrastructure.exposedrepository.Posts.url +import dev.usbharu.hideout.core.infrastructure.exposedrepository.Posts.visibility +import org.jetbrains.exposed.sql.* +import org.jetbrains.exposed.sql.SqlExpressionBuilder.eq +import org.jetbrains.exposed.sql.SqlExpressionBuilder.inList +import org.jetbrains.exposed.sql.javatime.timestamp +import org.slf4j.Logger +import org.slf4j.LoggerFactory +import org.springframework.stereotype.Repository + +@Repository +class ExposedPostRepository( + private val postQueryMapper: QueryMapper, + override val domainEventPublisher: DomainEventPublisher, +) : + PostRepository, + AbstractRepository(), + DomainEventPublishableRepository { + override val logger: Logger = Companion.logger + + override suspend fun save(post: Post): Post { + query { + Posts.upsert { + it[id] = post.id.id + it[actorId] = post.actorId.id + it[overview] = post.overview?.overview + it[content] = post.content.content + it[text] = post.content.text + it[createdAt] = post.createdAt + it[visibility] = post.visibility.name + it[url] = post.url.toString() + it[repostId] = post.repostId?.id + it[replyId] = post.replyId?.id + it[sensitive] = post.sensitive + it[apId] = post.apId.toString() + it[deleted] = post.deleted + it[hide] = post.hide + it[moveTo] = post.moveTo?.id + } + PostsMedia.deleteWhere { + postId eq post.id.id + } + PostsEmojis.deleteWhere { + postId eq post.id.id + } + PostsVisibleActors.deleteWhere { + postId eq post.id.id + } + PostsMedia.batchInsert(post.mediaIds) { + this[PostsMedia.postId] = post.id.id + this[PostsMedia.mediaId] = it.id + } + PostsEmojis.batchInsert(post.emojiIds) { + this[PostsEmojis.postId] = post.id.id + this[PostsEmojis.emojiId] = it.emojiId + } + PostsVisibleActors.batchInsert(post.visibleActors) { + this[PostsVisibleActors.postId] = post.id.id + this[PostsVisibleActors.actorId] = it.id + } + } + update(post) + return post + } + + override suspend fun saveAll(posts: List): List { + query { + Posts.batchUpsert(posts, id) { + this[id] = it.id.id + this[actorId] = it.actorId.id + this[overview] = it.overview?.overview + this[content] = it.content.content + this[text] = it.content.text + this[createdAt] = it.createdAt + this[visibility] = it.visibility.name + this[url] = it.url.toString() + this[repostId] = it.repostId?.id + this[replyId] = it.replyId?.id + this[sensitive] = it.sensitive + this[apId] = it.apId.toString() + this[deleted] = it.deleted + this[hide] = it.hide + this[moveTo] = it.moveTo?.id + } + val mediaIds = posts.flatMap { post -> post.mediaIds.map { post.id.id to it.id } } + val postsIds = posts.map { it.id.id } + PostsMedia.deleteWhere { + postId inList postsIds + } + PostsMedia.batchInsert(mediaIds) { + this[PostsMedia.postId] = it.first + this[PostsMedia.mediaId] = it.second + } + val emojiIds = posts.flatMap { post -> post.emojiIds.map { post.id.id to it.emojiId } } + PostsEmojis.deleteWhere { + postId inList postsIds + } + PostsEmojis.batchInsert(emojiIds) { + this[PostsEmojis.postId] = it.first + this[PostsEmojis.emojiId] = it.second + } + val visibleActors = posts.flatMap { post -> post.visibleActors.map { post.id.id to it.id } } + PostsVisibleActors.deleteWhere { + postId inList postsIds + } + PostsVisibleActors.batchInsert(visibleActors) { + this[PostsVisibleActors.postId] = it.first + this[PostsVisibleActors.actorId] = it.second + } + } + posts.forEach { + update(it) + } + return posts + } + + override suspend fun findById(id: PostId): Post? = query { + Posts + .selectAll() + .where { + Posts.id eq id.id + } + .let(postQueryMapper::map) + .first() + } + + override suspend fun findAllById(ids: List): List { + return query { + Posts + .selectAll() + .where { + Posts.id inList ids.map { it.id } + } + .let(postQueryMapper::map) + } + } + + override suspend fun findByActorId(id: ActorId, page: Page?): PaginationList = PaginationList( + query { + Posts + .selectAll() + .where { + actorId eq actorId + } + .let(postQueryMapper::map) + }, + null, + null + ) + + override suspend fun delete(post: Post) { + query { + Posts.deleteWhere { + id eq post.id.id + } + } + update(post) + } + + override suspend fun findByActorIdAndVisibilityInList( + actorId: ActorId, + visibilityList: List, + of: Page? + ): PaginationList { + return PaginationList( + query { + Posts + .selectAll() + .where { + Posts.actorId eq actorId.id and (visibility inList visibilityList.map { it.name }) + } + .let(postQueryMapper::map) + }, + null, + null + ) + } + + companion object { + private val logger = LoggerFactory.getLogger(ExposedPostRepository::class.java) + } +} + +object Posts : Table("posts") { + val id = long("id") + val actorId = long("actor_id").references(Actors.id) + val instanceId = long("instance_id").references(Instance.id) + val overview = varchar("overview", PostOverview.LENGTH).nullable() + val content = varchar("content", PostContent.CONTENT_LENGTH) + val text = varchar("text", PostContent.TEXT_LENGTH) + val createdAt = timestamp("created_at") + val visibility = varchar("visibility", 100) + val url = varchar("url", 1000) + val repostId = long("repost_id").references(id).nullable() + val replyId = long("reply_id").references(id).nullable() + val sensitive = bool("sensitive") + val apId = varchar("ap_id", 1000) + val deleted = bool("deleted") + val hide = bool("hide") + val moveTo = long("move_to").references(id).nullable() + override val primaryKey: PrimaryKey = PrimaryKey(id) +} + +object PostsMedia : Table("posts_media") { + val postId = long("post_id").references(id, ReferenceOption.CASCADE, ReferenceOption.CASCADE) + val mediaId = long("media_id").references(Media.id, ReferenceOption.CASCADE, ReferenceOption.CASCADE) + override val primaryKey = PrimaryKey(postId, mediaId) +} + +object PostsEmojis : Table("posts_emojis") { + val postId = long("post_id").references(id) + val emojiId = long("emoji_id").references(CustomEmojis.id) + override val primaryKey: PrimaryKey = PrimaryKey(postId, emojiId) +} + +object PostsVisibleActors : Table("posts_visible_actors") { + val postId = long("post_id").references(id) + val actorId = long("actor_id").references(Actors.id) +} diff --git a/hideout-core/src/main/kotlin/dev/usbharu/hideout/core/infrastructure/exposedrepository/ExposedRelationshipRepository.kt b/hideout-core/src/main/kotlin/dev/usbharu/hideout/core/infrastructure/exposedrepository/ExposedRelationshipRepository.kt new file mode 100644 index 00000000..8de364aa --- /dev/null +++ b/hideout-core/src/main/kotlin/dev/usbharu/hideout/core/infrastructure/exposedrepository/ExposedRelationshipRepository.kt @@ -0,0 +1,129 @@ +/* + * Copyright (C) 2024 usbharu + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package dev.usbharu.hideout.core.infrastructure.exposedrepository + +import dev.usbharu.hideout.core.domain.model.actor.ActorId +import dev.usbharu.hideout.core.domain.model.relationship.FindRelationshipOption +import dev.usbharu.hideout.core.domain.model.relationship.Relationship +import dev.usbharu.hideout.core.domain.model.relationship.RelationshipRepository +import dev.usbharu.hideout.core.domain.shared.domainevent.DomainEventPublisher +import dev.usbharu.hideout.core.domain.shared.repository.DomainEventPublishableRepository +import org.jetbrains.exposed.sql.* +import org.jetbrains.exposed.sql.SqlExpressionBuilder.eq +import org.slf4j.Logger +import org.slf4j.LoggerFactory +import org.springframework.stereotype.Repository + +@Repository +class ExposedRelationshipRepository(override val domainEventPublisher: DomainEventPublisher) : + RelationshipRepository, + AbstractRepository(), + DomainEventPublishableRepository { + override val logger: Logger + get() = Companion.logger + + override suspend fun save(relationship: Relationship): Relationship { + query { + Relationships.upsert { + it[actorId] = relationship.actorId.id + it[targetActorId] = relationship.targetActorId.id + it[following] = relationship.following + it[blocking] = relationship.blocking + it[muting] = relationship.muting + it[followRequesting] = relationship.followRequesting + it[mutingFollowRequest] = relationship.mutingFollowRequest + } + } + update(relationship) + return relationship + } + + override suspend fun delete(relationship: Relationship) { + query { + Relationships.deleteWhere { + actorId eq relationship.actorId.id and (targetActorId eq relationship.targetActorId.id) + } + } + update(relationship) + } + + override suspend fun findByActorIdAndTargetId(actorId: ActorId, targetId: ActorId): Relationship? = query { + Relationships.selectAll().where { + Relationships.actorId eq actorId.id and (Relationships.targetActorId eq targetId.id) + }.singleOrNull()?.toRelationships() + } + + override suspend fun findByTargetId( + targetId: ActorId, + option: FindRelationshipOption?, + inverseOption: FindRelationshipOption? + ): List { + val query1 = Relationships.selectAll().where { Relationships.actorId eq targetId.id } + inverseOption.apply(query1) + // todo 逆のほうがいいかも + val query = query1.alias("INV").selectAll().where { + Relationships.targetActorId eq targetId.id + } + option.apply(query) + + return query.map(ResultRow::toRelationships) + } + + companion object { + private val logger = LoggerFactory.getLogger(ExposedRelationshipRepository::class.java) + } +} + +fun FindRelationshipOption?.apply(query: Query) { + if (this?.follow != null) { + query.andWhere { Relationships.following eq this@apply.follow } + } + if (this?.mute != null) { + query.andWhere { Relationships.muting eq this@apply.mute } + } + if (this?.block != null) { + query.andWhere { Relationships.blocking eq this@apply.block } + } + if (this?.followRequest != null) { + query.andWhere { Relationships.followRequesting eq this@apply.followRequest } + } + if (this?.muteFollowRequest != null) { + query.andWhere { Relationships.mutingFollowRequest eq this@apply.muteFollowRequest } + } +} + +fun ResultRow.toRelationships(): Relationship = Relationship( + actorId = ActorId(this[Relationships.actorId]), + targetActorId = ActorId(this[Relationships.targetActorId]), + following = this[Relationships.following], + blocking = this[Relationships.blocking], + muting = this[Relationships.muting], + followRequesting = this[Relationships.followRequesting], + mutingFollowRequest = this[Relationships.mutingFollowRequest] +) + +object Relationships : Table("relationships") { + val actorId = long("actor_id").references(Actors.id) + val targetActorId = long("target_actor_id").references(Actors.id) + val following = bool("following") + val blocking = bool("blocking") + val muting = bool("muting") + val followRequesting = bool("follow_requesting") + val mutingFollowRequest = bool("muting_follow_request") + + override val primaryKey: PrimaryKey = PrimaryKey(actorId, targetActorId) +} diff --git a/hideout-core/src/main/kotlin/dev/usbharu/hideout/core/infrastructure/exposedrepository/ExposedTimelineRelationshipRepository.kt b/hideout-core/src/main/kotlin/dev/usbharu/hideout/core/infrastructure/exposedrepository/ExposedTimelineRelationshipRepository.kt new file mode 100644 index 00000000..e501f9eb --- /dev/null +++ b/hideout-core/src/main/kotlin/dev/usbharu/hideout/core/infrastructure/exposedrepository/ExposedTimelineRelationshipRepository.kt @@ -0,0 +1,68 @@ +package dev.usbharu.hideout.core.infrastructure.exposedrepository + +import dev.usbharu.hideout.core.domain.model.actor.ActorId +import dev.usbharu.hideout.core.domain.model.timeline.TimelineId +import dev.usbharu.hideout.core.domain.model.timelinerelationship.TimelineRelationship +import dev.usbharu.hideout.core.domain.model.timelinerelationship.TimelineRelationshipId +import dev.usbharu.hideout.core.domain.model.timelinerelationship.TimelineRelationshipRepository +import dev.usbharu.hideout.core.domain.model.timelinerelationship.Visible +import org.jetbrains.exposed.sql.* +import org.jetbrains.exposed.sql.SqlExpressionBuilder.eq +import org.slf4j.Logger +import org.slf4j.LoggerFactory +import org.springframework.stereotype.Repository + +@Repository +class ExposedTimelineRelationshipRepository : AbstractRepository(), TimelineRelationshipRepository { + override val logger: Logger + get() = Companion.logger + + override suspend fun save(timelineRelationship: TimelineRelationship): TimelineRelationship { + query { + TimelineRelationships.insert { + it[id] = timelineRelationship.id.value + it[timelineId] = timelineRelationship.timelineId.value + it[actorId] = timelineRelationship.actorId.id + it[visible] = timelineRelationship.visible.name + } + } + return timelineRelationship + } + + override suspend fun delete(timelineRelationship: TimelineRelationship) { + query { + TimelineRelationships.deleteWhere { + TimelineRelationships.id eq timelineRelationship.id.value + } + } + } + + override suspend fun findByActorId(actorId: ActorId): List { + return query { + TimelineRelationships.selectAll().where { + TimelineRelationships.actorId eq actorId.id + }.map { it.toTimelineRelationship() } + } + } + + companion object { + private val logger = LoggerFactory.getLogger(ExposedTimelineRelationshipRepository::class.java) + } +} + +fun ResultRow.toTimelineRelationship(): TimelineRelationship { + return TimelineRelationship( + TimelineRelationshipId(this[TimelineRelationships.id]), + TimelineId(this[TimelineRelationships.timelineId]), + ActorId(this[TimelineRelationships.actorId]), + Visible.valueOf(this[TimelineRelationships.visible]) + ) +} + +object TimelineRelationships : Table("timeline_relationships") { + val id = long("id") + val timelineId = long("timeline_id").references(Timelines.id) + val actorId = long("actor_id").references(Actors.id) + val visible = varchar("visible", 100) + override val primaryKey: PrimaryKey = PrimaryKey(id) +} diff --git a/hideout-core/src/main/kotlin/dev/usbharu/hideout/core/infrastructure/exposedrepository/ExposedTimelineRepository.kt b/hideout-core/src/main/kotlin/dev/usbharu/hideout/core/infrastructure/exposedrepository/ExposedTimelineRepository.kt new file mode 100644 index 00000000..ecf93bb1 --- /dev/null +++ b/hideout-core/src/main/kotlin/dev/usbharu/hideout/core/infrastructure/exposedrepository/ExposedTimelineRepository.kt @@ -0,0 +1,79 @@ +package dev.usbharu.hideout.core.infrastructure.exposedrepository + +import dev.usbharu.hideout.core.domain.model.timeline.* +import dev.usbharu.hideout.core.domain.model.userdetails.UserDetailId +import dev.usbharu.hideout.core.domain.shared.domainevent.DomainEventPublisher +import dev.usbharu.hideout.core.domain.shared.repository.DomainEventPublishableRepository +import org.jetbrains.exposed.sql.* +import org.jetbrains.exposed.sql.SqlExpressionBuilder.eq +import org.slf4j.Logger +import org.slf4j.LoggerFactory +import org.springframework.stereotype.Repository + +@Repository +class ExposedTimelineRepository(override val domainEventPublisher: DomainEventPublisher) : + TimelineRepository, + AbstractRepository(), + DomainEventPublishableRepository { + override suspend fun save(timeline: Timeline): Timeline { + query { + Timelines.insert { + it[id] = timeline.id.value + it[userDetailId] = timeline.userDetailId.id + it[name] = timeline.name.value + it[visibility] = timeline.visibility.name + it[isSystem] = timeline.isSystem + } + } + update(timeline) + return timeline + } + + override suspend fun delete(timeline: Timeline) { + query { + Timelines.deleteWhere { + Timelines.id eq timeline.id.value + } + } + update(timeline) + } + + override suspend fun findByIds(ids: List): List { + return query { + Timelines.selectAll().where { Timelines.id inList ids.map { it.value } }.map { it.toTimeline() } + } + } + + override suspend fun findById(id: TimelineId): Timeline? { + return query { + Timelines.selectAll().where { Timelines.id eq id.value }.firstOrNull()?.toTimeline() + } + } + + companion object { + private val logger = LoggerFactory.getLogger(ExposedTimelineRepository::class.java.name) + } + + override val logger: Logger + get() = Companion.logger +} + +fun ResultRow.toTimeline(): Timeline { + return Timeline( + TimelineId(this[Timelines.id]), + UserDetailId(this[Timelines.userDetailId]), + TimelineName(this[Timelines.name]), + TimelineVisibility.valueOf(this[Timelines.visibility]), + this[Timelines.isSystem] + ) +} + +object Timelines : Table("timelines") { + val id = long("id") + val userDetailId = long("user_detail_id").references(UserDetails.id) + val name = varchar("name", 300) + val visibility = varchar("visibility", 100) + val isSystem = bool("is_system") + + override val primaryKey: PrimaryKey = PrimaryKey(id) +} diff --git a/hideout-core/src/main/kotlin/dev/usbharu/hideout/core/infrastructure/exposedrepository/InstanceRepositoryImpl.kt b/hideout-core/src/main/kotlin/dev/usbharu/hideout/core/infrastructure/exposedrepository/InstanceRepositoryImpl.kt new file mode 100644 index 00000000..76844bed --- /dev/null +++ b/hideout-core/src/main/kotlin/dev/usbharu/hideout/core/infrastructure/exposedrepository/InstanceRepositoryImpl.kt @@ -0,0 +1,119 @@ +/* + * Copyright (C) 2024 usbharu + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package dev.usbharu.hideout.core.infrastructure.exposedrepository + +import dev.usbharu.hideout.core.domain.model.instance.* +import org.jetbrains.exposed.sql.* +import org.jetbrains.exposed.sql.SqlExpressionBuilder.eq +import org.jetbrains.exposed.sql.javatime.timestamp +import org.slf4j.Logger +import org.slf4j.LoggerFactory +import org.springframework.stereotype.Repository +import java.net.URI +import dev.usbharu.hideout.core.domain.model.instance.Instance as InstanceEntity + +@Repository +class InstanceRepositoryImpl : InstanceRepository, + AbstractRepository() { + override val logger: Logger + get() = Companion.logger + + override suspend fun save(instance: InstanceEntity): InstanceEntity = query { + if (Instance.selectAll().where { Instance.id.eq(instance.id.instanceId) }.forUpdate().empty()) { + Instance.insert { + it[id] = instance.id.instanceId + it[name] = instance.name.name + it[description] = instance.description.description + it[url] = instance.url.toString() + it[iconUrl] = instance.iconUrl.toString() + it[sharedInbox] = instance.sharedInbox?.toString() + it[software] = instance.software.software + it[version] = instance.version.version + it[isBlocked] = instance.isBlocked + it[isMuted] = instance.isMuted + it[moderationNote] = instance.moderationNote.note + it[createdAt] = instance.createdAt + } + } else { + Instance.update({ Instance.id eq instance.id.instanceId }) { + it[name] = instance.name.name + it[description] = instance.description.description + it[url] = instance.url.toString() + it[iconUrl] = instance.iconUrl.toString() + it[sharedInbox] = instance.sharedInbox?.toString() + it[software] = instance.software.software + it[version] = instance.version.version + it[isBlocked] = instance.isBlocked + it[isMuted] = instance.isMuted + it[moderationNote] = instance.moderationNote.note + it[createdAt] = instance.createdAt + } + } + return@query instance + } + + override suspend fun findById(id: InstanceId): InstanceEntity? = query { + return@query Instance.selectAll().where { Instance.id eq id.instanceId } + .singleOrNull()?.toInstance() + } + + override suspend fun delete(instance: InstanceEntity): Unit = query { + Instance.deleteWhere { id eq instance.id.instanceId } + } + + override suspend fun findByUrl(url: URI): dev.usbharu.hideout.core.domain.model.instance.Instance? = query { + return@query Instance.selectAll().where { Instance.url eq url.toString() }.singleOrNull()?.toInstance() + } + + companion object { + private val logger = LoggerFactory.getLogger(InstanceRepositoryImpl::class.java) + } +} + +fun ResultRow.toInstance(): InstanceEntity { + return InstanceEntity( + id = InstanceId(this[Instance.id]), + name = InstanceName(this[Instance.name]), + description = InstanceDescription(this[Instance.description]), + url = URI.create(this[Instance.url]), + iconUrl = URI.create(this[Instance.iconUrl]), + sharedInbox = this[Instance.sharedInbox]?.let { URI.create(it) }, + software = InstanceSoftware(this[Instance.software]), + version = InstanceVersion(this[Instance.version]), + isBlocked = this[Instance.isBlocked], + isMuted = this[Instance.isMuted], + moderationNote = InstanceModerationNote(this[Instance.moderationNote]), + createdAt = this[Instance.createdAt] + ) +} + +object Instance : Table("instance") { + val id = long("id") + val name = varchar("name", 1000) + val description = varchar("description", 5000) + val url = varchar("url", 255).uniqueIndex() + val iconUrl = varchar("icon_url", 255) + val sharedInbox = varchar("shared_inbox", 255).nullable().uniqueIndex() + val software = varchar("software", 255) + val version = varchar("version", 255) + val isBlocked = bool("is_blocked") + val isMuted = bool("is_muted") + val moderationNote = varchar("moderation_note", 10000) + val createdAt = timestamp("created_at") + + override val primaryKey: PrimaryKey = PrimaryKey(id) +} diff --git a/hideout-core/src/main/kotlin/dev/usbharu/hideout/core/infrastructure/exposedrepository/MediaRepositoryImpl.kt b/hideout-core/src/main/kotlin/dev/usbharu/hideout/core/infrastructure/exposedrepository/MediaRepositoryImpl.kt new file mode 100644 index 00000000..b9ab43de --- /dev/null +++ b/hideout-core/src/main/kotlin/dev/usbharu/hideout/core/infrastructure/exposedrepository/MediaRepositoryImpl.kt @@ -0,0 +1,109 @@ +/* + * Copyright (C) 2024 usbharu + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package dev.usbharu.hideout.core.infrastructure.exposedrepository + +import dev.usbharu.hideout.core.domain.model.media.* +import org.jetbrains.exposed.sql.* +import org.jetbrains.exposed.sql.SqlExpressionBuilder.eq +import org.slf4j.Logger +import org.slf4j.LoggerFactory +import org.springframework.stereotype.Repository +import java.net.URI +import dev.usbharu.hideout.core.domain.model.media.Media as EntityMedia + +@Repository +class MediaRepositoryImpl : MediaRepository, AbstractRepository() { + override val logger: Logger + get() = Companion.logger + + override suspend fun findById(id: MediaId): dev.usbharu.hideout.core.domain.model.media.Media? { + return query { + return@query Media + .selectAll().where { Media.id eq id.id } + .singleOrNull() + ?.toMedia() + } + } + + override suspend fun delete(media: dev.usbharu.hideout.core.domain.model.media.Media): Unit = query { + Media.deleteWhere { + id eq id + } + } + + override suspend fun save(media: EntityMedia): EntityMedia = query { + if (Media.selectAll().where { Media.id eq media.id.id }.forUpdate().singleOrNull() != null + ) { + Media.update({ Media.id eq media.id.id }) { + it[name] = media.name.name + it[url] = media.url.toString() + it[remoteUrl] = media.remoteUrl?.toString() + it[thumbnailUrl] = media.thumbnailUrl?.toString() + it[type] = media.type.name + it[blurhash] = media.blurHash?.hash + it[mimeType] = media.mimeType.type + "/" + media.mimeType.subtype + it[description] = media.description?.description + } + } else { + Media.insert { + it[id] = media.id.id + it[name] = media.name.name + it[url] = media.url.toString() + it[remoteUrl] = media.remoteUrl?.toString() + it[thumbnailUrl] = media.thumbnailUrl?.toString() + it[type] = media.type.name + it[blurhash] = media.blurHash?.hash + it[mimeType] = media.mimeType.type + "/" + media.mimeType.subtype + it[description] = media.description?.description + } + } + return@query media + } + + companion object { + private val logger = LoggerFactory.getLogger(MediaRepositoryImpl::class.java) + } +} + +fun ResultRow.toMedia(): EntityMedia { + val fileType = FileType.valueOf(this[Media.type]) + val mimeType = this[Media.mimeType] + return EntityMedia( + id = MediaId(this[Media.id]), + name = MediaName(this[Media.name]), + url = URI.create(this[Media.url]), + remoteUrl = this[Media.remoteUrl]?.let { URI.create(it) }, + thumbnailUrl = this[Media.thumbnailUrl]?.let { URI.create(it) }, + type = fileType, + blurHash = this[Media.blurhash]?.let { MediaBlurHash(it) }, + mimeType = MimeType(mimeType.substringBefore("/"), mimeType.substringAfter("/"), fileType), + description = this[Media.description]?.let { MediaDescription(it) } + ) +} + +object Media : Table("media") { + val id = long("id") + val name = varchar("name", 255) + val url = varchar("url", 255).uniqueIndex() + val remoteUrl = varchar("remote_url", 255).uniqueIndex().nullable() + val thumbnailUrl = varchar("thumbnail_url", 255).uniqueIndex().nullable() + val type = varchar("type", 100) + val blurhash = varchar("blurhash", 255).nullable() + val mimeType = varchar("mime_type", 255) + val description = varchar("description", 4000).nullable() + override val primaryKey = PrimaryKey(id) +} diff --git a/hideout-core/src/main/kotlin/dev/usbharu/hideout/core/infrastructure/exposedrepository/UserDetailRepositoryImpl.kt b/hideout-core/src/main/kotlin/dev/usbharu/hideout/core/infrastructure/exposedrepository/UserDetailRepositoryImpl.kt new file mode 100644 index 00000000..dc9f5908 --- /dev/null +++ b/hideout-core/src/main/kotlin/dev/usbharu/hideout/core/infrastructure/exposedrepository/UserDetailRepositoryImpl.kt @@ -0,0 +1,114 @@ +/* + * Copyright (C) 2024 usbharu + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package dev.usbharu.hideout.core.infrastructure.exposedrepository + +import dev.usbharu.hideout.core.domain.model.actor.ActorId +import dev.usbharu.hideout.core.domain.model.timeline.TimelineId +import dev.usbharu.hideout.core.domain.model.userdetails.UserDetail +import dev.usbharu.hideout.core.domain.model.userdetails.UserDetailHashedPassword +import dev.usbharu.hideout.core.domain.model.userdetails.UserDetailId +import dev.usbharu.hideout.core.domain.model.userdetails.UserDetailRepository +import org.jetbrains.exposed.sql.* +import org.jetbrains.exposed.sql.SqlExpressionBuilder.eq +import org.jetbrains.exposed.sql.javatime.timestamp +import org.slf4j.Logger +import org.slf4j.LoggerFactory +import org.springframework.stereotype.Repository + +@Repository +class UserDetailRepositoryImpl : UserDetailRepository, AbstractRepository() { + override val logger: Logger + get() = Companion.logger + + override suspend fun save(userDetail: UserDetail): UserDetail = query { + val singleOrNull = + UserDetails.selectAll().where { UserDetails.id eq userDetail.id.id }.forUpdate().singleOrNull() + if (singleOrNull == null) { + UserDetails.insert { + it[id] = userDetail.id.id + it[actorId] = userDetail.actorId.id + it[password] = userDetail.password.password + it[autoAcceptFolloweeFollowRequest] = userDetail.autoAcceptFolloweeFollowRequest + it[lastMigration] = userDetail.lastMigration + } + } else { + UserDetails.update({ UserDetails.id eq userDetail.id.id }) { + it[actorId] = userDetail.actorId.id + it[password] = userDetail.password.password + it[autoAcceptFolloweeFollowRequest] = userDetail.autoAcceptFolloweeFollowRequest + it[lastMigration] = userDetail.lastMigration + } + } + return@query userDetail + } + + override suspend fun delete(userDetail: UserDetail): Unit = query { + UserDetails.deleteWhere { id eq userDetail.id.id } + } + + override suspend fun findByActorId(actorId: Long): UserDetail? = query { + return@query UserDetails + .selectAll().where { UserDetails.actorId eq actorId } + .singleOrNull() + ?.let { + userDetail(it) + } + } + + override suspend fun findById(id: UserDetailId): UserDetail? = query { + UserDetails + .selectAll().where { UserDetails.id eq id.id } + .singleOrNull() + ?.let { + userDetail(it) + } + } + + override suspend fun findAllById(idList: List): List { + return query { + UserDetails + .selectAll() + .where { UserDetails.id inList idList.map { it.id } } + .map { + userDetail(it) + } + } + } + + private fun userDetail(it: ResultRow) = UserDetail.create( + UserDetailId(it[UserDetails.id]), + ActorId(it[UserDetails.actorId]), + UserDetailHashedPassword(it[UserDetails.password]), + it[UserDetails.autoAcceptFolloweeFollowRequest], + it[UserDetails.lastMigration], + it[UserDetails.homeTimelineId]?.let { it1 -> TimelineId(it1) } + ) + + companion object { + private val logger = LoggerFactory.getLogger(UserDetailRepositoryImpl::class.java) + } +} + +object UserDetails : Table("user_details") { + val id = long("id") + val actorId = long("actor_id").references(Actors.id) + val password = varchar("password", 255) + val autoAcceptFolloweeFollowRequest = bool("auto_accept_followee_follow_request") + val lastMigration = timestamp("last_migration").nullable() + val homeTimelineId = long("home_timeline_id").references(Timelines.id).nullable() + override val primaryKey: PrimaryKey = PrimaryKey(id) +} diff --git a/hideout-core/src/main/kotlin/dev/usbharu/hideout/core/infrastructure/factory/ActorFactoryImpl.kt b/hideout-core/src/main/kotlin/dev/usbharu/hideout/core/infrastructure/factory/ActorFactoryImpl.kt new file mode 100644 index 00000000..9c41792b --- /dev/null +++ b/hideout-core/src/main/kotlin/dev/usbharu/hideout/core/infrastructure/factory/ActorFactoryImpl.kt @@ -0,0 +1,70 @@ +/* + * Copyright (C) 2024 usbharu + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package dev.usbharu.hideout.core.infrastructure.factory + +import dev.usbharu.hideout.core.config.ApplicationConfig +import dev.usbharu.hideout.core.domain.model.actor.* +import dev.usbharu.hideout.core.domain.model.instance.InstanceId +import dev.usbharu.hideout.core.domain.model.support.domain.Domain +import dev.usbharu.hideout.core.domain.shared.id.IdGenerateService +import org.springframework.stereotype.Component +import java.net.URI +import java.time.Instant + +@Component +class ActorFactoryImpl( + private val idGenerateService: IdGenerateService, + private val applicationConfig: ApplicationConfig, +) { + suspend fun createLocal( + name: String, + keyPair: Pair, + instanceId: InstanceId, + ): Actor { + val actorName = ActorName(name) + val userUrl = "${applicationConfig.url}/users/${actorName.name}" + return Actor( + id = ActorId(idGenerateService.generateId()), + name = actorName, + domain = Domain(applicationConfig.url.host), + screenName = ActorScreenName(name), + description = ActorDescription(""), + inbox = URI.create("$userUrl/inbox"), + outbox = URI.create("$userUrl/outbox"), + url = URI.create(userUrl), + publicKey = keyPair.first, + privateKey = keyPair.second, + createdAt = Instant.now(), + keyId = ActorKeyId("$userUrl#main-key"), + followersEndpoint = URI.create("$userUrl/followers"), + followingEndpoint = URI.create("$userUrl/following"), + instance = instanceId, + locked = false, + followersCount = ActorRelationshipCount(0), + followingCount = ActorRelationshipCount(0), + postsCount = ActorPostsCount(0), + lastPostAt = null, + suspend = false, + emojiIds = emptySet(), + deleted = false, + banner = null, + icon = null + ) + } +} + +// todo なんか色々おかしいので直す diff --git a/hideout-core/src/main/kotlin/dev/usbharu/hideout/core/infrastructure/factory/PostContentFactoryImpl.kt b/hideout-core/src/main/kotlin/dev/usbharu/hideout/core/infrastructure/factory/PostContentFactoryImpl.kt new file mode 100644 index 00000000..a59af46c --- /dev/null +++ b/hideout-core/src/main/kotlin/dev/usbharu/hideout/core/infrastructure/factory/PostContentFactoryImpl.kt @@ -0,0 +1,35 @@ +/* + * Copyright (C) 2024 usbharu + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package dev.usbharu.hideout.core.infrastructure.factory + +import dev.usbharu.hideout.core.domain.model.post.PostContent +import dev.usbharu.hideout.core.domain.service.post.PostContentFormatter +import org.springframework.stereotype.Component + +@Component +class PostContentFactoryImpl( + private val postContentFormatter: PostContentFormatter, +) { + suspend fun create(content: String): PostContent { + val format = postContentFormatter.format(content) + return PostContent( + format.content, + format.html, + emptyList() + ) + } +} diff --git a/hideout-core/src/main/kotlin/dev/usbharu/hideout/core/infrastructure/factory/PostFactoryImpl.kt b/hideout-core/src/main/kotlin/dev/usbharu/hideout/core/infrastructure/factory/PostFactoryImpl.kt new file mode 100644 index 00000000..d831b5be --- /dev/null +++ b/hideout-core/src/main/kotlin/dev/usbharu/hideout/core/infrastructure/factory/PostFactoryImpl.kt @@ -0,0 +1,70 @@ +/* + * Copyright (C) 2024 usbharu + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package dev.usbharu.hideout.core.infrastructure.factory + +import dev.usbharu.hideout.core.config.ApplicationConfig +import dev.usbharu.hideout.core.domain.model.actor.Actor +import dev.usbharu.hideout.core.domain.model.actor.ActorName +import dev.usbharu.hideout.core.domain.model.media.MediaId +import dev.usbharu.hideout.core.domain.model.post.Post +import dev.usbharu.hideout.core.domain.model.post.PostId +import dev.usbharu.hideout.core.domain.model.post.PostOverview +import dev.usbharu.hideout.core.domain.model.post.Visibility +import dev.usbharu.hideout.core.domain.shared.id.IdGenerateService +import org.springframework.stereotype.Component +import java.net.URI +import java.time.Instant + +@Component +class PostFactoryImpl( + private val idGenerateService: IdGenerateService, + private val postContentFactoryImpl: PostContentFactoryImpl, + private val applicationConfig: ApplicationConfig, +) { + @Suppress("LongParameterList") + suspend fun createLocal( + actor: Actor, + actorName: ActorName, + overview: PostOverview?, + content: String, + visibility: Visibility, + repostId: PostId?, + replyId: PostId?, + sensitive: Boolean, + mediaIds: List, + ): Post { + val id = idGenerateService.generateId() + val url = URI.create(applicationConfig.url.toString() + "/users/" + actorName.name + "/posts/" + id) + return Post.create( + id = PostId(id), + actorId = actor.id, + instanceId = actor.instance, + overview = overview, + content = postContentFactoryImpl.create(content), + createdAt = Instant.now(), + visibility = visibility, + url = url, + repostId = repostId, + replyId = replyId, + sensitive = sensitive, + apId = url, + deleted = false, + mediaIds = mediaIds, + actor = actor, + ) + } +} diff --git a/hideout-core/src/main/kotlin/dev/usbharu/hideout/core/infrastructure/localfilesystem/LocalFileSystemMediaStore.kt b/hideout-core/src/main/kotlin/dev/usbharu/hideout/core/infrastructure/localfilesystem/LocalFileSystemMediaStore.kt new file mode 100644 index 00000000..607ab397 --- /dev/null +++ b/hideout-core/src/main/kotlin/dev/usbharu/hideout/core/infrastructure/localfilesystem/LocalFileSystemMediaStore.kt @@ -0,0 +1,46 @@ +package dev.usbharu.hideout.core.infrastructure.localfilesystem + +import dev.usbharu.hideout.core.config.ApplicationConfig +import dev.usbharu.hideout.core.config.LocalStorageConfig +import dev.usbharu.hideout.core.external.mediastore.MediaStore +import org.slf4j.LoggerFactory +import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty +import org.springframework.stereotype.Component +import java.net.URI +import java.nio.file.Path +import kotlin.io.path.copyTo + +@Component +@ConditionalOnProperty("hideout.storage.type", havingValue = "local", matchIfMissing = true) +class LocalFileSystemMediaStore( + localStorageConfig: LocalStorageConfig, + applicationConfig: ApplicationConfig +) : + MediaStore { + + private val publicUrl = localStorageConfig.publicUrl ?: "${applicationConfig.url}/files/" + override suspend fun upload(path: Path, id: String): URI { + logger.info("START Media upload. {}", id) + val fileSavePath = buildSavePath(path, id) + + val fileSavePathString = fileSavePath.toAbsolutePath().toString() + logger.info("MEDIA save. path: {}", fileSavePathString) + + @Suppress("TooGenericExceptionCaught") + try { + path.copyTo(fileSavePath) + } catch (e: Exception) { + logger.warn("FAILED to Save the media.", e) + throw e + } + + logger.info("SUCCESS Media upload. {}", id) + return URI.create(publicUrl).resolve(id) + } + + private fun buildSavePath(savePath: Path, name: String): Path = savePath.resolve(name) + + companion object { + private val logger = LoggerFactory.getLogger(LocalFileSystemMediaStore::class.java) + } +} diff --git a/hideout-core/src/main/kotlin/dev/usbharu/hideout/core/infrastructure/media/common/GenerateBlurhash.kt b/hideout-core/src/main/kotlin/dev/usbharu/hideout/core/infrastructure/media/common/GenerateBlurhash.kt new file mode 100644 index 00000000..6ee2893d --- /dev/null +++ b/hideout-core/src/main/kotlin/dev/usbharu/hideout/core/infrastructure/media/common/GenerateBlurhash.kt @@ -0,0 +1,7 @@ +package dev.usbharu.hideout.core.infrastructure.media.common + +import java.awt.image.BufferedImage + +interface GenerateBlurhash { + fun generateBlurhash(bufferedImage: BufferedImage): String +} diff --git a/hideout-core/src/main/kotlin/dev/usbharu/hideout/core/infrastructure/media/common/GenerateBlurhashImpl.kt b/hideout-core/src/main/kotlin/dev/usbharu/hideout/core/infrastructure/media/common/GenerateBlurhashImpl.kt new file mode 100644 index 00000000..cb269364 --- /dev/null +++ b/hideout-core/src/main/kotlin/dev/usbharu/hideout/core/infrastructure/media/common/GenerateBlurhashImpl.kt @@ -0,0 +1,10 @@ +package dev.usbharu.hideout.core.infrastructure.media.common + +import io.trbl.blurhash.BlurHash +import org.springframework.stereotype.Component +import java.awt.image.BufferedImage + +@Component +class GenerateBlurhashImpl : GenerateBlurhash { + override fun generateBlurhash(bufferedImage: BufferedImage): String = BlurHash.encode(bufferedImage) +} diff --git a/hideout-core/src/main/kotlin/dev/usbharu/hideout/core/infrastructure/media/image/ImageIOImageProcessor.kt b/hideout-core/src/main/kotlin/dev/usbharu/hideout/core/infrastructure/media/image/ImageIOImageProcessor.kt new file mode 100644 index 00000000..b297ff51 --- /dev/null +++ b/hideout-core/src/main/kotlin/dev/usbharu/hideout/core/infrastructure/media/image/ImageIOImageProcessor.kt @@ -0,0 +1,78 @@ +package dev.usbharu.hideout.core.infrastructure.media.image + +import dev.usbharu.hideout.core.config.ImageIOImageConfig +import dev.usbharu.hideout.core.domain.model.media.FileType +import dev.usbharu.hideout.core.domain.model.media.MimeType +import dev.usbharu.hideout.core.external.media.MediaProcessor +import dev.usbharu.hideout.core.external.media.ProcessedMedia +import dev.usbharu.hideout.core.infrastructure.media.common.GenerateBlurhash +import net.coobird.thumbnailator.Thumbnails +import org.slf4j.LoggerFactory +import org.springframework.beans.factory.annotation.Qualifier +import org.springframework.stereotype.Component +import java.awt.Color +import java.awt.image.BufferedImage +import java.nio.file.Files +import java.nio.file.Path +import java.util.* +import javax.imageio.ImageIO +import kotlin.io.path.inputStream +import kotlin.io.path.outputStream + +@Component +@Qualifier("image") +class ImageIOImageProcessor( + private val imageIOImageConfig: ImageIOImageConfig, + private val blurhash: GenerateBlurhash +) : MediaProcessor { + override fun isSupported(mimeType: MimeType): Boolean = + mimeType.fileType == FileType.Image || mimeType.type == "image" + + override suspend fun process(path: Path, filename: String, mimeType: MimeType?): ProcessedMedia { + val read = ImageIO.read(path.inputStream()) + + val bufferedImage = BufferedImage(read.width, read.height, BufferedImage.TYPE_INT_RGB) + + val graphics = bufferedImage.createGraphics() + + graphics.drawImage(read, 0, 0, Color.BLACK, null) + + val tempFileName = UUID.randomUUID().toString() + val tempFile = Files.createTempFile(tempFileName, "tmp") + + val thumbnailPath = run { + val tempThumbnailFile = Files.createTempFile("thumbnail-$tempFileName", ".tmp") + + tempThumbnailFile.outputStream().use { + val write = ImageIO.write( + Thumbnails.of(bufferedImage) + .size(imageIOImageConfig.thumbnailsWidth, imageIOImageConfig.thumbnailsHeight) + .imageType(BufferedImage.TYPE_INT_RGB) + .asBufferedImage(), + imageIOImageConfig.format, + it + ) + tempThumbnailFile.takeIf { write } + } + } + + tempFile.outputStream().use { + if (ImageIO.write(bufferedImage, imageIOImageConfig.format, it).not()) { + logger.warn("Failed to save a temporary file. type: {} ,path: {}", imageIOImageConfig.format, tempFile) + throw Exception("Failed to save a temporary file.") + } + } + + return ProcessedMedia( + tempFile, + thumbnailPath, + FileType.Image, + MimeType("image", imageIOImageConfig.format, FileType.Image), + blurhash.generateBlurhash(bufferedImage) + ) + } + + companion object { + private val logger = LoggerFactory.getLogger(ImageIOImageProcessor::class.java) + } +} diff --git a/hideout-core/src/main/kotlin/dev/usbharu/hideout/core/infrastructure/media/video/FFmpegVideoProcessor.kt b/hideout-core/src/main/kotlin/dev/usbharu/hideout/core/infrastructure/media/video/FFmpegVideoProcessor.kt new file mode 100644 index 00000000..9625c2c1 --- /dev/null +++ b/hideout-core/src/main/kotlin/dev/usbharu/hideout/core/infrastructure/media/video/FFmpegVideoProcessor.kt @@ -0,0 +1,116 @@ +package dev.usbharu.hideout.core.infrastructure.media.video + +import dev.usbharu.hideout.core.config.FFmpegVideoConfig +import dev.usbharu.hideout.core.domain.model.media.FileType +import dev.usbharu.hideout.core.domain.model.media.MimeType +import dev.usbharu.hideout.core.external.media.MediaProcessor +import dev.usbharu.hideout.core.external.media.ProcessedMedia +import dev.usbharu.hideout.core.infrastructure.media.common.GenerateBlurhash +import org.bytedeco.javacv.FFmpegFrameFilter +import org.bytedeco.javacv.FFmpegFrameGrabber +import org.bytedeco.javacv.FFmpegFrameRecorder +import org.bytedeco.javacv.Java2DFrameConverter +import org.slf4j.LoggerFactory +import org.springframework.beans.factory.annotation.Qualifier +import org.springframework.stereotype.Component +import java.awt.image.BufferedImage +import java.nio.file.Files +import java.nio.file.Path +import javax.imageio.ImageIO +import kotlin.math.min + +@Component +@Qualifier("video") +class FFmpegVideoProcessor( + private val fFmpegVideoConfig: FFmpegVideoConfig, + private val generateBlurhash: GenerateBlurhash +) : MediaProcessor { + override fun isSupported(mimeType: MimeType): Boolean = + mimeType.fileType == FileType.Video || mimeType.type == "video" + + @Suppress("LongMethod", "CyclomaticComplexMethod", "CognitiveComplexMethod", "NestedBlockDepth") + override suspend fun process(path: Path, filename: String, mimeType: MimeType?): ProcessedMedia { + val tempFile = Files.createTempFile("hideout-movie-processor-", ".tmp") + val thumbnailFile = Files.createTempFile("hideout-movie-thumbnail-generate-", ".tmp") + logger.info("START Convert Movie Media {}", filename) + var bufferedImage: BufferedImage? = null + FFmpegFrameGrabber(path.toFile()).use { grabber -> + grabber.start() + val width = min(fFmpegVideoConfig.maxWidth, grabber.imageWidth) + val height = min(fFmpegVideoConfig.maxHeight, grabber.imageHeight) + val frameRate = fFmpegVideoConfig.frameRate + + logger.debug("Movie Media Width {}, Height {}", width, height) + + FFmpegFrameFilter( + "fps=fps=$frameRate", + "anull", + width, + height, + grabber.audioChannels + ).use { filter -> + + filter.sampleFormat = grabber.sampleFormat + filter.sampleRate = grabber.sampleRate + filter.pixelFormat = grabber.pixelFormat + filter.frameRate = grabber.frameRate + filter.start() + + val videoBitRate = min(fFmpegVideoConfig.maxBitrate, (width * height * frameRate * 1 * 0.07).toInt()) + + logger.debug("Movie Media BitRate {}", videoBitRate) + + FFmpegFrameRecorder(tempFile.toFile(), width, height, grabber.audioChannels).use { + it.sampleRate = grabber.sampleRate + it.format = fFmpegVideoConfig.format + it.videoCodec = fFmpegVideoConfig.videoCodec + it.audioCodec = fFmpegVideoConfig.audioCodec + it.audioChannels = grabber.audioChannels + it.videoQuality = fFmpegVideoConfig.videoQuality + it.frameRate = frameRate.toDouble() + it.setVideoOption("preset", "ultrafast") + it.timestamp = 0 + it.gopSize = frameRate + it.videoBitrate = videoBitRate + it.start() + + val frameConverter = Java2DFrameConverter() + + while (true) { + val grab = grabber.grab() ?: break + + if (bufferedImage == null) { + bufferedImage = frameConverter.convert(grab) + } + + if (grab.image != null || grab.samples != null) { + filter.push(grab) + } + while (true) { + val frame = filter.pull() ?: break + it.record(frame) + } + } + + if (bufferedImage != null) { + ImageIO.write(bufferedImage, "jpeg", thumbnailFile.toFile()) + } + } + } + } + + logger.info("SUCCESS Convert Movie Media {}", filename) + + return ProcessedMedia( + tempFile, + thumbnailFile, + FileType.Video, + MimeType("video", fFmpegVideoConfig.format, FileType.Video), + bufferedImage?.let { generateBlurhash.generateBlurhash(it) } + ) + } + + companion object { + private val logger = LoggerFactory.getLogger(FFmpegVideoProcessor::class.java) + } +} diff --git a/hideout-core/src/main/kotlin/dev/usbharu/hideout/core/infrastructure/mongorepository/MongoInternalTimelineObjectRepository.kt b/hideout-core/src/main/kotlin/dev/usbharu/hideout/core/infrastructure/mongorepository/MongoInternalTimelineObjectRepository.kt new file mode 100644 index 00000000..6f77882c --- /dev/null +++ b/hideout-core/src/main/kotlin/dev/usbharu/hideout/core/infrastructure/mongorepository/MongoInternalTimelineObjectRepository.kt @@ -0,0 +1,197 @@ +package dev.usbharu.hideout.core.infrastructure.mongorepository + +import dev.usbharu.hideout.core.domain.model.actor.ActorId +import dev.usbharu.hideout.core.domain.model.emoji.EmojiId +import dev.usbharu.hideout.core.domain.model.filter.FilterId +import dev.usbharu.hideout.core.domain.model.media.MediaId +import dev.usbharu.hideout.core.domain.model.post.PostId +import dev.usbharu.hideout.core.domain.model.post.Visibility +import dev.usbharu.hideout.core.domain.model.support.page.Page +import dev.usbharu.hideout.core.domain.model.support.page.PaginationList +import dev.usbharu.hideout.core.domain.model.timeline.TimelineId +import dev.usbharu.hideout.core.domain.model.timelineobject.TimelineObject +import dev.usbharu.hideout.core.domain.model.timelineobject.TimelineObjectId +import dev.usbharu.hideout.core.domain.model.timelineobject.TimelineObjectWarnFilter +import dev.usbharu.hideout.core.domain.model.userdetails.UserDetailId +import dev.usbharu.hideout.core.infrastructure.timeline.InternalTimelineObjectOption +import dev.usbharu.hideout.core.infrastructure.timeline.InternalTimelineObjectRepository +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.collect +import kotlinx.coroutines.flow.map +import kotlinx.coroutines.flow.toList +import org.springframework.data.domain.Sort +import org.springframework.data.mongodb.core.MongoTemplate +import org.springframework.data.mongodb.core.mapping.Document +import org.springframework.data.mongodb.core.query.Criteria +import org.springframework.data.mongodb.core.query.Query +import org.springframework.data.repository.kotlin.CoroutineCrudRepository +import org.springframework.stereotype.Repository +import java.time.Instant + +@Repository +class MongoInternalTimelineObjectRepository( + private val springDataMongoTimelineObjectRepository: SpringDataMongoTimelineObjectRepository, + private val mongoTemplate: MongoTemplate +) : + InternalTimelineObjectRepository { + override suspend fun save(timelineObject: TimelineObject): TimelineObject { + springDataMongoTimelineObjectRepository.save(SpringDataMongoTimelineObject.of(timelineObject)) + return timelineObject + } + + override suspend fun saveAll(timelineObjectList: List): List { + springDataMongoTimelineObjectRepository.saveAll(timelineObjectList.map { SpringDataMongoTimelineObject.of(it) }) + .collect() + return timelineObjectList + } + + override suspend fun findByPostId(postId: PostId): List { + return springDataMongoTimelineObjectRepository.findByPostId(postId.id).map { it.toTimelineObject() }.toList() + } + + override suspend fun deleteByPostId(postId: PostId) { + springDataMongoTimelineObjectRepository.deleteByPostId(postId.id) + } + + override suspend fun deleteByTimelineIdAndActorId(timelineId: TimelineId, actorId: ActorId) { + springDataMongoTimelineObjectRepository.deleteByTimelineIdAndPostActorId(timelineId.value, actorId.id) + } + + override suspend fun deleteByTimelineId(timelineId: TimelineId) { + springDataMongoTimelineObjectRepository.deleteByTimelineId(timelineId.value) + } + + override suspend fun findByTimelineId( + timelineId: TimelineId, + internalTimelineObjectOption: InternalTimelineObjectOption?, + page: Page? + ): PaginationList { + val query = Query() + + if (page?.minId != null) { + query.with(Sort.by(Sort.Direction.ASC, "postCreatedAt")) + page.minId?.let { query.addCriteria(Criteria.where("id").gt(it)) } + page.maxId?.let { query.addCriteria(Criteria.where("id").lt(it)) } + } else { + query.with(Sort.by(Sort.Direction.DESC, "postCreatedAt")) + page?.sinceId?.let { query.addCriteria(Criteria.where("id").gt(it)) } + page?.maxId?.let { query.addCriteria(Criteria.where("id").lt(it)) } + } + + page?.limit?.let { query.limit(it) } + + val timelineObjects = + mongoTemplate.find(query, SpringDataMongoTimelineObject::class.java).map { it.toTimelineObject() } + + return PaginationList( + timelineObjects, + timelineObjects.lastOrNull()?.postId, + timelineObjects.firstOrNull()?.postId + ) + } +} + +@Document +data class SpringDataMongoTimelineObject( + val id: Long, + val userDetailId: Long, + val timelineId: Long, + val postId: Long, + val postActorId: Long, + val postCreatedAt: Long, + val replyId: Long?, + val replyActorId: Long?, + val repostId: Long?, + val repostActorId: Long?, + val visibility: Visibility, + val isPureRepost: Boolean, + val mediaIds: List, + val emojiIds: List, + val visibleActors: List, + val hasMediaInRepost: Boolean, + val lastUpdatedAt: Long, + val warnFilters: List +) { + + fun toTimelineObject(): TimelineObject { + return TimelineObject( + TimelineObjectId(id), + UserDetailId(userDetailId), + TimelineId(timelineId), + PostId(postId), + ActorId(postActorId), + Instant.ofEpochSecond(postCreatedAt), + replyId?.let { PostId(it) }, + replyActorId?.let { ActorId(it) }, + repostId?.let { PostId(it) }, + repostActorId?.let { ActorId(it) }, + visibility, + isPureRepost, + mediaIds.map { MediaId(it) }, + emojiIds.map { EmojiId(it) }, + visibleActors.map { ActorId(it) }, + hasMediaInRepost, + Instant.ofEpochSecond(lastUpdatedAt), + warnFilters.map { it.toTimelineObjectWarnFilter() } + ) + } + + companion object { + fun of(timelineObject: TimelineObject): SpringDataMongoTimelineObject { + return SpringDataMongoTimelineObject( + timelineObject.id.value, + timelineObject.userDetailId.id, + timelineObject.timelineId.value, + timelineObject.postId.id, + timelineObject.postActorId.id, + timelineObject.postCreatedAt.epochSecond, + timelineObject.replyId?.id, + timelineObject.replyActorId?.id, + timelineObject.repostId?.id, + timelineObject.repostActorId?.id, + timelineObject.visibility, + timelineObject.isPureRepost, + timelineObject.mediaIds.map { it.id }, + timelineObject.emojiIds.map { it.emojiId }, + timelineObject.visibleActors.map { it.id }, + timelineObject.hasMediaInRepost, + timelineObject.lastUpdatedAt.epochSecond, + timelineObject.warnFilters.map { SpringDataMongoTimelineObjectWarnFilter.of(it) } + ) + } + } +} + +data class SpringDataMongoTimelineObjectWarnFilter( + val filterId: Long, + val matchedKeyword: String +) { + + fun toTimelineObjectWarnFilter(): TimelineObjectWarnFilter { + return TimelineObjectWarnFilter( + FilterId(filterId), + matchedKeyword + ) + } + + companion object { + fun of(timelineObjectWarnFilter: TimelineObjectWarnFilter): SpringDataMongoTimelineObjectWarnFilter { + return SpringDataMongoTimelineObjectWarnFilter( + timelineObjectWarnFilter.filterId.id, + timelineObjectWarnFilter.matchedKeyword + ) + } + } +} + +interface SpringDataMongoTimelineObjectRepository : CoroutineCrudRepository { + fun findByPostId(postId: Long): Flow + + suspend fun deleteByPostId(postId: Long) + + suspend fun deleteByTimelineIdAndPostActorId(timelineId: Long, postActorId: Long) + + suspend fun deleteByTimelineId(timelineId: Long) + + suspend fun findByTimelineId(timelineId: TimelineId): Flow +} diff --git a/hideout-core/src/main/kotlin/dev/usbharu/hideout/core/infrastructure/other/DefaultPostContentFormatter.kt b/hideout-core/src/main/kotlin/dev/usbharu/hideout/core/infrastructure/other/DefaultPostContentFormatter.kt new file mode 100644 index 00000000..0998a495 --- /dev/null +++ b/hideout-core/src/main/kotlin/dev/usbharu/hideout/core/infrastructure/other/DefaultPostContentFormatter.kt @@ -0,0 +1,118 @@ +/* + * Copyright (C) 2024 usbharu + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package dev.usbharu.hideout.core.infrastructure.other + +import dev.usbharu.hideout.core.domain.service.post.FormattedPostContent +import dev.usbharu.hideout.core.domain.service.post.PostContentFormatter +import org.jsoup.Jsoup +import org.jsoup.nodes.Document +import org.jsoup.nodes.Element +import org.jsoup.nodes.TextNode +import org.jsoup.select.Elements +import org.owasp.html.PolicyFactory +import org.springframework.stereotype.Service + +@Service +class DefaultPostContentFormatter(private val policyFactory: PolicyFactory) : PostContentFormatter { + override fun format(content: String): FormattedPostContent { + // まず不正なHTMLを整形する + val document = Jsoup.parseBodyFragment(content) + val outputSettings = Document.OutputSettings() + outputSettings.prettyPrint(false) + + document.outputSettings(outputSettings) + + val unsafeElement = document.getElementsByTag("body").first() ?: return FormattedPostContent( + "", + "" + ) + + // 文字だけのHTMLなどはここでpタグで囲む + val flattenHtml = unsafeElement.childNodes().mapNotNull { + if (it is Element) { + it + } else if (it is TextNode) { + Element("p").appendText(it.text()) + } else { + null + } + }.filter { it.text().isNotBlank() } + + // HTMLのサニタイズをする + val unsafeHtml = Elements(flattenHtml).outerHtml() + + val safeHtml = policyFactory.sanitize(unsafeHtml) + + val safeDocument = + Jsoup.parseBodyFragment(safeHtml).getElementsByTag("body").first() ?: return FormattedPostContent("", "") + + val formattedHtml = mutableListOf() + + // 連続するbrタグを段落に変換する + for (element in safeDocument.children()) { + var brCount = 0 + var prevIndex = 0 + val childNodes = element.childNodes() + for ((index, childNode) in childNodes.withIndex()) { + if (childNode is Element && childNode.tagName() == "br") { + brCount++ + } else if (brCount >= 2) { + formattedHtml.add( + Element(element.tag(), element.baseUri(), element.attributes()).appendChildren( + childNodes.subList( + prevIndex, + index - brCount + ) + ) + ) + prevIndex = index + } + } + formattedHtml.add( + Element(element.tag(), element.baseUri(), element.attributes()).appendChildren( + childNodes.subList( + prevIndex, + childNodes.size + ) + ) + ) + } + + val elements = Elements(formattedHtml) + val document1 = Document("") + document1.outputSettings().syntax(Document.OutputSettings.Syntax.xml) + document1.insertChildren(0, elements) + + return FormattedPostContent(elements.outerHtml().replace("\n", ""), printHtml(elements)) + } + + private fun printHtml(element: Elements): String { + return element.joinToString("\n\n") { + it.childNodes().joinToString("") { node -> + if (node is Element && node.tagName() == "br") { + "\n" + } else if (node is Element) { + node.text() + } else if (node is TextNode) { + node.text() + } else { + "" + } + } + } + } +} diff --git a/hideout-core/src/main/kotlin/dev/usbharu/hideout/core/infrastructure/other/SnowflakeIdGenerateService.kt b/hideout-core/src/main/kotlin/dev/usbharu/hideout/core/infrastructure/other/SnowflakeIdGenerateService.kt new file mode 100644 index 00000000..3b82b1cc --- /dev/null +++ b/hideout-core/src/main/kotlin/dev/usbharu/hideout/core/infrastructure/other/SnowflakeIdGenerateService.kt @@ -0,0 +1,84 @@ +/* + * Copyright (C) 2024 usbharu + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package dev.usbharu.hideout.core.infrastructure.other + +import dev.usbharu.hideout.core.domain.shared.id.IdGenerateService +import kotlinx.coroutines.delay +import kotlinx.coroutines.sync.Mutex +import kotlinx.coroutines.sync.withLock +import java.time.Instant + +@Suppress("MagicNumber") +open class SnowflakeIdGenerateService(private val baseTime: Long) : IdGenerateService { + var lastTimeStamp: Long = -1 + var sequenceId: Int = 0 + val mutex = Mutex() + + @Throws(IllegalStateException::class) + override suspend fun generateId(): Long { + return mutex.withLock { + var timestamp = getTime() + if (timestamp < lastTimeStamp) { + timestamp = wait(timestamp) + // throw IllegalStateException(" $lastTimeStamp $timestamp ${lastTimeStamp-timestamp} ") + } + if (timestamp == lastTimeStamp) { + sequenceId++ + if (sequenceId >= 4096) { + timestamp = wait(timestamp) + sequenceId = 0 + } + } else { + sequenceId = 0 + } + lastTimeStamp = timestamp + return@withLock (timestamp - baseTime).shl(22).or(1L.shl(12)).or(sequenceId.toLong()) + } + } + + private suspend fun wait(timestamp: Long): Long { + var timestamp1 = timestamp + while (timestamp1 <= lastTimeStamp) { + delay(1L) + timestamp1 = getTime() + } + return timestamp1 + } + + private fun getTime(): Long = Instant.now().toEpochMilli() + override fun equals(other: Any?): Boolean { + if (this === other) return true + if (javaClass != other?.javaClass) return false + + other as SnowflakeIdGenerateService + + if (baseTime != other.baseTime) return false + if (lastTimeStamp != other.lastTimeStamp) return false + if (sequenceId != other.sequenceId) return false + if (mutex != other.mutex) return false + + return true + } + + override fun hashCode(): Int { + var result = baseTime.hashCode() + result = 31 * result + lastTimeStamp.hashCode() + result = 31 * result + sequenceId + result = 31 * result + mutex.hashCode() + return result + } +} diff --git a/hideout-core/src/main/kotlin/dev/usbharu/hideout/core/infrastructure/other/TwitterSnowflakeIdGenerateService.kt b/hideout-core/src/main/kotlin/dev/usbharu/hideout/core/infrastructure/other/TwitterSnowflakeIdGenerateService.kt new file mode 100644 index 00000000..dcefc995 --- /dev/null +++ b/hideout-core/src/main/kotlin/dev/usbharu/hideout/core/infrastructure/other/TwitterSnowflakeIdGenerateService.kt @@ -0,0 +1,26 @@ +/* + * Copyright (C) 2024 usbharu + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package dev.usbharu.hideout.core.infrastructure.other + +import org.springframework.context.annotation.Primary +import org.springframework.stereotype.Service + +// 2010-11-04T01:42:54.657 +@Suppress("MagicNumber") +@Service +@Primary +object TwitterSnowflakeIdGenerateService : SnowflakeIdGenerateService(1288834974657L) diff --git a/hideout-core/src/main/kotlin/dev/usbharu/hideout/core/infrastructure/springframework/SpringSecurityPasswordEncoder.kt b/hideout-core/src/main/kotlin/dev/usbharu/hideout/core/infrastructure/springframework/SpringSecurityPasswordEncoder.kt new file mode 100644 index 00000000..799a8d44 --- /dev/null +++ b/hideout-core/src/main/kotlin/dev/usbharu/hideout/core/infrastructure/springframework/SpringSecurityPasswordEncoder.kt @@ -0,0 +1,28 @@ +/* + * Copyright (C) 2024 usbharu + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package dev.usbharu.hideout.core.infrastructure.springframework + +import dev.usbharu.hideout.core.domain.service.userdetail.PasswordEncoder +import org.springframework.stereotype.Component + +@Component +class SpringSecurityPasswordEncoder( + private val passwordEncoder: org.springframework.security.crypto.password.PasswordEncoder, +) : + PasswordEncoder { + override suspend fun encode(input: String): String = passwordEncoder.encode(input) +} diff --git a/hideout-core/src/main/kotlin/dev/usbharu/hideout/core/infrastructure/springframework/domainevent/SpringFrameworkDomainEventPublisher.kt b/hideout-core/src/main/kotlin/dev/usbharu/hideout/core/infrastructure/springframework/domainevent/SpringFrameworkDomainEventPublisher.kt new file mode 100644 index 00000000..40794c1e --- /dev/null +++ b/hideout-core/src/main/kotlin/dev/usbharu/hideout/core/infrastructure/springframework/domainevent/SpringFrameworkDomainEventPublisher.kt @@ -0,0 +1,30 @@ +/* + * Copyright (C) 2024 usbharu + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package dev.usbharu.hideout.core.infrastructure.springframework.domainevent + +import dev.usbharu.hideout.core.domain.shared.domainevent.DomainEvent +import dev.usbharu.hideout.core.domain.shared.domainevent.DomainEventPublisher +import org.springframework.context.ApplicationEventPublisher +import org.springframework.stereotype.Component + +@Component +class SpringFrameworkDomainEventPublisher(private val applicationEventPublisher: ApplicationEventPublisher) : + DomainEventPublisher { + override suspend fun publishEvent(domainEvent: DomainEvent<*>) { + applicationEventPublisher.publishEvent(domainEvent) + } +} diff --git a/hideout-core/src/main/kotlin/dev/usbharu/hideout/core/infrastructure/springframework/domainevent/SpringFrameworkDomainEventSubscriber.kt b/hideout-core/src/main/kotlin/dev/usbharu/hideout/core/infrastructure/springframework/domainevent/SpringFrameworkDomainEventSubscriber.kt new file mode 100644 index 00000000..6c5a148d --- /dev/null +++ b/hideout-core/src/main/kotlin/dev/usbharu/hideout/core/infrastructure/springframework/domainevent/SpringFrameworkDomainEventSubscriber.kt @@ -0,0 +1,28 @@ +package dev.usbharu.hideout.core.infrastructure.springframework.domainevent + +import dev.usbharu.hideout.core.application.domainevent.subscribers.DomainEventConsumer +import dev.usbharu.hideout.core.application.domainevent.subscribers.DomainEventSubscriber +import dev.usbharu.hideout.core.domain.shared.domainevent.DomainEvent +import dev.usbharu.hideout.core.domain.shared.domainevent.DomainEventBody +import org.springframework.context.event.EventListener +import org.springframework.stereotype.Component + +@Component +class SpringFrameworkDomainEventSubscriber : DomainEventSubscriber { + + val map = mutableMapOf>>() + + override fun subscribe(eventName: String, domainEventConsumer: DomainEventConsumer) { + map.getOrPut(eventName) { mutableListOf() }.add(domainEventConsumer as DomainEventConsumer<*>) + } + + @EventListener + suspend fun onDomainEventPublished(domainEvent: DomainEvent<*>) { + map[domainEvent.name]?.forEach { + try { + it.invoke(domainEvent) + } catch (e: Exception) { + } + } + } +} diff --git a/hideout-core/src/main/kotlin/dev/usbharu/hideout/core/infrastructure/springframework/oauth2/HideoutJdbcOauth2AuthorizationService.kt b/hideout-core/src/main/kotlin/dev/usbharu/hideout/core/infrastructure/springframework/oauth2/HideoutJdbcOauth2AuthorizationService.kt new file mode 100644 index 00000000..2044962e --- /dev/null +++ b/hideout-core/src/main/kotlin/dev/usbharu/hideout/core/infrastructure/springframework/oauth2/HideoutJdbcOauth2AuthorizationService.kt @@ -0,0 +1,46 @@ +/* + * Copyright (C) 2024 usbharu + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package dev.usbharu.hideout.core.infrastructure.springframework.oauth2 + +import org.springframework.beans.factory.annotation.Autowired +import org.springframework.jdbc.core.JdbcOperations +import org.springframework.jdbc.support.lob.DefaultLobHandler +import org.springframework.jdbc.support.lob.LobHandler +import org.springframework.security.oauth2.server.authorization.JdbcOAuth2AuthorizationService +import org.springframework.security.oauth2.server.authorization.client.RegisteredClientRepository +import org.springframework.stereotype.Component + +@Component +class HideoutJdbcOauth2AuthorizationService( + registeredClientRepository: RegisteredClientRepository, + jdbcOperations: JdbcOperations, + @Autowired(required = false) lobHandler: LobHandler = DefaultLobHandler(), +) : JdbcOAuth2AuthorizationService(jdbcOperations, registeredClientRepository, lobHandler) { + + init { + super.setAuthorizationRowMapper( + HideoutOAuth2AuthorizationRowMapper(registeredClientRepository = registeredClientRepository) + ) + } + + class HideoutOAuth2AuthorizationRowMapper(registeredClientRepository: RegisteredClientRepository?) : + OAuth2AuthorizationRowMapper(registeredClientRepository) { + init { + objectMapper.addMixIn(HideoutUserDetails::class.java, UserDetailsMixin::class.java) + } + } +} diff --git a/hideout-core/src/main/kotlin/dev/usbharu/hideout/core/infrastructure/springframework/oauth2/HideoutUserDetails.kt b/hideout-core/src/main/kotlin/dev/usbharu/hideout/core/infrastructure/springframework/oauth2/HideoutUserDetails.kt new file mode 100644 index 00000000..cbaf2540 --- /dev/null +++ b/hideout-core/src/main/kotlin/dev/usbharu/hideout/core/infrastructure/springframework/oauth2/HideoutUserDetails.kt @@ -0,0 +1,128 @@ +/* + * Copyright (C) 2024 usbharu + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package dev.usbharu.hideout.core.infrastructure.springframework.oauth2 + +import com.fasterxml.jackson.annotation.JsonAutoDetect +import com.fasterxml.jackson.annotation.JsonIgnoreProperties +import com.fasterxml.jackson.annotation.JsonSubTypes +import com.fasterxml.jackson.annotation.JsonTypeInfo +import com.fasterxml.jackson.core.JsonParser +import com.fasterxml.jackson.core.type.TypeReference +import com.fasterxml.jackson.databind.DeserializationContext +import com.fasterxml.jackson.databind.JsonDeserializer +import com.fasterxml.jackson.databind.JsonNode +import com.fasterxml.jackson.databind.ObjectMapper +import com.fasterxml.jackson.databind.annotation.JsonDeserialize +import org.springframework.security.core.GrantedAuthority +import org.springframework.security.core.authority.SimpleGrantedAuthority +import org.springframework.security.core.userdetails.UserDetails +import java.io.Serial +import java.util.* + +class HideoutUserDetails( + authorities: Set, + private val password: String, + private val username: String, + val userDetailsId: Long, +) : UserDetails { + private val authorities: MutableSet = Collections.unmodifiableSet(authorities) + override fun getAuthorities(): MutableSet = authorities + + override fun getPassword(): String = password + + override fun getUsername(): String = username + + override fun equals(other: Any?): Boolean { + if (this === other) return true + if (javaClass != other?.javaClass) return false + + other as HideoutUserDetails + + if (authorities != other.authorities) return false + if (password != other.password) return false + if (username != other.username) return false + if (userDetailsId != other.userDetailsId) return false + + return true + } + + override fun hashCode(): Int { + var result = authorities.hashCode() + result = 31 * result + password.hashCode() + result = 31 * result + username.hashCode() + result = 31 * result + userDetailsId.hashCode() + return result + } + + override fun toString(): String { + return "HideoutUserDetails(" + + "password='$password', " + + "username='$username', " + + "userDetailsId=$userDetailsId, " + + "authorities=$authorities" + + ")" + } + + companion object { + @Serial + private const val serialVersionUID = -899168205656607781L + } +} + +@JsonTypeInfo(use = JsonTypeInfo.Id.CLASS, include = JsonTypeInfo.As.PROPERTY) +@JsonDeserialize(using = UserDetailsDeserializer::class) +@JsonAutoDetect( + fieldVisibility = JsonAutoDetect.Visibility.ANY, + getterVisibility = JsonAutoDetect.Visibility.NONE, + isGetterVisibility = JsonAutoDetect.Visibility.NONE, + creatorVisibility = JsonAutoDetect.Visibility.NONE +) +@JsonIgnoreProperties(ignoreUnknown = true) +@JsonSubTypes +@Suppress("UnnecessaryAbstractClass") +abstract class UserDetailsMixin + +class UserDetailsDeserializer : JsonDeserializer() { + + override fun deserialize(p: JsonParser, ctxt: DeserializationContext): HideoutUserDetails { + val mapper = p.codec as ObjectMapper + val jsonNode: JsonNode = mapper.readTree(p) + val authorities: Set = mapper.convertValue( + jsonNode["authorities"], + SIMPLE_GRANTED_AUTHORITY_SET + ) + + val password = jsonNode.readText("password") + return HideoutUserDetails( + userDetailsId = jsonNode["userDetailsId"].longValue(), + username = jsonNode.readText("username"), + password = password, + authorities = authorities + ) + } + + fun JsonNode.readText(field: String, defaultValue: String = ""): String { + return when { + has(field) -> get(field).asText(defaultValue) + else -> defaultValue + } + } + + companion object { + private val SIMPLE_GRANTED_AUTHORITY_SET = object : TypeReference>() {} + } +} diff --git a/hideout-core/src/main/kotlin/dev/usbharu/hideout/core/infrastructure/springframework/oauth2/SecureTokenGenerator.kt b/hideout-core/src/main/kotlin/dev/usbharu/hideout/core/infrastructure/springframework/oauth2/SecureTokenGenerator.kt new file mode 100644 index 00000000..fda64d39 --- /dev/null +++ b/hideout-core/src/main/kotlin/dev/usbharu/hideout/core/infrastructure/springframework/oauth2/SecureTokenGenerator.kt @@ -0,0 +1,24 @@ +/* + * Copyright (C) 2024 usbharu + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package dev.usbharu.hideout.core.infrastructure.springframework.oauth2 + +import org.springframework.stereotype.Component + +@Component +interface SecureTokenGenerator { + fun generate(): String +} diff --git a/hideout-core/src/main/kotlin/dev/usbharu/hideout/core/infrastructure/springframework/oauth2/SecureTokenGeneratorImpl.kt b/hideout-core/src/main/kotlin/dev/usbharu/hideout/core/infrastructure/springframework/oauth2/SecureTokenGeneratorImpl.kt new file mode 100644 index 00000000..b4f5aac7 --- /dev/null +++ b/hideout-core/src/main/kotlin/dev/usbharu/hideout/core/infrastructure/springframework/oauth2/SecureTokenGeneratorImpl.kt @@ -0,0 +1,32 @@ +/* + * Copyright (C) 2024 usbharu + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package dev.usbharu.hideout.core.infrastructure.springframework.oauth2 + +import org.springframework.stereotype.Component +import java.security.SecureRandom +import java.util.* + +@Component +class SecureTokenGeneratorImpl : SecureTokenGenerator { + override fun generate(): String { + val byteArray = ByteArray(16) + val secureRandom = SecureRandom() + secureRandom.nextBytes(byteArray) + + return Base64.getUrlEncoder().encodeToString(byteArray) + } +} diff --git a/hideout-core/src/main/kotlin/dev/usbharu/hideout/core/infrastructure/springframework/oauth2/SpringSecurityOauth2PrincipalContextHolder.kt b/hideout-core/src/main/kotlin/dev/usbharu/hideout/core/infrastructure/springframework/oauth2/SpringSecurityOauth2PrincipalContextHolder.kt new file mode 100644 index 00000000..68341349 --- /dev/null +++ b/hideout-core/src/main/kotlin/dev/usbharu/hideout/core/infrastructure/springframework/oauth2/SpringSecurityOauth2PrincipalContextHolder.kt @@ -0,0 +1,27 @@ +package dev.usbharu.hideout.core.infrastructure.springframework.oauth2 + +import dev.usbharu.hideout.core.domain.model.support.acct.Acct +import dev.usbharu.hideout.core.domain.model.support.principal.FromApi +import dev.usbharu.hideout.core.domain.model.support.principal.PrincipalContextHolder +import dev.usbharu.hideout.core.domain.model.userdetails.UserDetailId +import dev.usbharu.hideout.core.query.principal.PrincipalQueryService +import org.springframework.security.core.context.SecurityContextHolder +import org.springframework.security.oauth2.jwt.Jwt +import org.springframework.stereotype.Component + +@Component +class SpringSecurityOauth2PrincipalContextHolder(private val principalQueryService: PrincipalQueryService) : + PrincipalContextHolder { + override suspend fun getPrincipal(): FromApi { + val principal = SecurityContextHolder.getContext().authentication?.principal as Jwt + + val id = principal.getClaim("uid").toLong() + val userDetail = principalQueryService.findByUserDetailId(UserDetailId(id)) + + return FromApi( + userDetail.actorId, + userDetail.userDetailId, + Acct(userDetail.username, userDetail.host) + ) + } +} \ No newline at end of file diff --git a/hideout-core/src/main/kotlin/dev/usbharu/hideout/core/infrastructure/springframework/oauth2/UserDetailsServiceImpl.kt b/hideout-core/src/main/kotlin/dev/usbharu/hideout/core/infrastructure/springframework/oauth2/UserDetailsServiceImpl.kt new file mode 100644 index 00000000..5ca4964e --- /dev/null +++ b/hideout-core/src/main/kotlin/dev/usbharu/hideout/core/infrastructure/springframework/oauth2/UserDetailsServiceImpl.kt @@ -0,0 +1,53 @@ +/* + * Copyright (C) 2024 usbharu + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package dev.usbharu.hideout.core.infrastructure.springframework.oauth2 + +import dev.usbharu.hideout.core.application.shared.Transaction +import dev.usbharu.hideout.core.config.ApplicationConfig +import dev.usbharu.hideout.core.domain.model.actor.ActorRepository +import dev.usbharu.hideout.core.domain.model.userdetails.UserDetailRepository +import kotlinx.coroutines.runBlocking +import org.springframework.security.core.userdetails.UserDetails +import org.springframework.security.core.userdetails.UserDetailsService +import org.springframework.security.core.userdetails.UsernameNotFoundException +import org.springframework.stereotype.Component + +@Component +class UserDetailsServiceImpl( + private val actorRepository: ActorRepository, + private val userDetailRepository: UserDetailRepository, + private val applicationConfig: ApplicationConfig, + private val transaction: Transaction, +) : UserDetailsService { + override fun loadUserByUsername(username: String?): UserDetails = runBlocking { + if (username == null) { + throw UsernameNotFoundException("Username not found") + } + transaction.transaction { + val actor = actorRepository.findByNameAndDomain(username, applicationConfig.url.host) + ?: throw UsernameNotFoundException("$username not found") + val userDetail = userDetailRepository.findByActorId(actor.id.id) + ?: throw UsernameNotFoundException("${actor.id.id} not found") + HideoutUserDetails( + authorities = HashSet(), + password = userDetail.password.password, + actor.name.name, + userDetailsId = userDetail.id.id + ) + } + } +} diff --git a/hideout-core/src/main/kotlin/dev/usbharu/hideout/core/infrastructure/timeline/AbstractTimelineStore.kt b/hideout-core/src/main/kotlin/dev/usbharu/hideout/core/infrastructure/timeline/AbstractTimelineStore.kt new file mode 100644 index 00000000..564f188a --- /dev/null +++ b/hideout-core/src/main/kotlin/dev/usbharu/hideout/core/infrastructure/timeline/AbstractTimelineStore.kt @@ -0,0 +1,269 @@ +package dev.usbharu.hideout.core.infrastructure.timeline + +import dev.usbharu.hideout.core.domain.model.actor.Actor +import dev.usbharu.hideout.core.domain.model.actor.ActorId +import dev.usbharu.hideout.core.domain.model.filter.Filter +import dev.usbharu.hideout.core.domain.model.filter.FilteredPost +import dev.usbharu.hideout.core.domain.model.post.Post +import dev.usbharu.hideout.core.domain.model.post.PostId +import dev.usbharu.hideout.core.domain.model.post.Visibility +import dev.usbharu.hideout.core.domain.model.support.page.Page +import dev.usbharu.hideout.core.domain.model.support.page.PaginationList +import dev.usbharu.hideout.core.domain.model.support.timelineobjectdetail.TimelineObjectDetail +import dev.usbharu.hideout.core.domain.model.timeline.Timeline +import dev.usbharu.hideout.core.domain.model.timeline.TimelineId +import dev.usbharu.hideout.core.domain.model.timeline.TimelineVisibility +import dev.usbharu.hideout.core.domain.model.timelineobject.TimelineObject +import dev.usbharu.hideout.core.domain.model.timelineobject.TimelineObjectId +import dev.usbharu.hideout.core.domain.model.timelineobject.TimelineObjectWarnFilter +import dev.usbharu.hideout.core.domain.model.timelinerelationship.TimelineRelationship +import dev.usbharu.hideout.core.domain.model.timelinerelationship.Visible +import dev.usbharu.hideout.core.domain.model.userdetails.UserDetail +import dev.usbharu.hideout.core.domain.model.userdetails.UserDetailId +import dev.usbharu.hideout.core.domain.shared.id.IdGenerateService +import dev.usbharu.hideout.core.external.timeline.ReadTimelineOption +import dev.usbharu.hideout.core.external.timeline.TimelineStore +import java.time.Instant + +abstract class AbstractTimelineStore(private val idGenerateService: IdGenerateService) : TimelineStore { + override suspend fun addPost(post: Post) { + val timelineList = getTimelines(post.actorId) + + val repost = post.repostId?.let { getPost(it) } + val replyActorId = post.replyId?.let { getPost(it)?.actorId } + + val timelineObjectList = timelineList.mapNotNull { + createTimelineObject(post, replyActorId, repost, it) + } + + insertTimelineObject(timelineObjectList) + } + + protected abstract suspend fun getTimelines(actorId: ActorId): List + + protected abstract suspend fun getTimeline(timelineId: TimelineId): Timeline? + + protected suspend fun createTimelineObject( + post: Post, + replyActorId: ActorId?, + repost: Post?, + timeline: Timeline + ): TimelineObject? { + if (post.visibility == Visibility.DIRECT) { + return null + } + if (timeline.visibility == TimelineVisibility.PUBLIC && post.visibility != Visibility.PUBLIC) { + return null + } + if (timeline.visibility == TimelineVisibility.UNLISTED && (post.visibility != Visibility.PUBLIC || post.visibility != Visibility.UNLISTED)) { + return null + } + + val filters = getFilters(timeline.userDetailId) + + val applyFilters = applyFilters(post, filters) + + if (repost != null) { + return TimelineObject.create( + TimelineObjectId(idGenerateService.generateId()), + timeline, + post, + replyActorId, + repost, + applyFilters.filterResults + ) + } + + return TimelineObject.create( + TimelineObjectId(idGenerateService.generateId()), + timeline, + post, + replyActorId, + applyFilters.filterResults + ) + } + + protected abstract suspend fun getFilters(userDetailId: UserDetailId): List + + protected abstract suspend fun getNewerFilters(userDetailId: UserDetailId, lastUpdateAt: Instant): List + + protected abstract suspend fun applyFilters(post: Post, filters: List): FilteredPost + + protected abstract suspend fun getPost(postId: PostId): Post? + + protected abstract suspend fun insertTimelineObject(timelineObjectList: List) + + protected abstract suspend fun updateTimelineObject(timelineObjectList: List) + + protected abstract suspend fun getTimelineObjectByPostId(postId: PostId): List + + protected abstract suspend fun removeTimelineObject(postId: PostId) + + protected abstract suspend fun removeTimelineObject(timelineId: TimelineId, actorId: ActorId) + + protected abstract suspend fun removeTimelineObject(timelineId: TimelineId) + + protected abstract suspend fun getPostsByTimelineRelationshipList(timelineRelationshipList: List): List + + protected abstract suspend fun getPostsByPostId(postIds: List): List + + protected abstract suspend fun getTimelineObject( + timelineId: TimelineId, + readTimelineOption: ReadTimelineOption?, + page: Page? + ): PaginationList + + override suspend fun updatePost(post: Post) { + val timelineObjectByPostId = getTimelineObjectByPostId(post.id) + + val repost = post.repostId?.let { getPost(it) } + + val timelineObjectList = if (repost != null) { + timelineObjectByPostId.map { + val filters = getFilters(it.userDetailId) + val applyFilters = applyFilters(post, filters) + it.updateWith(post, repost, applyFilters.filterResults) + it + } + } else { + timelineObjectByPostId.map { + val filters = getFilters(it.userDetailId) + val applyFilters = applyFilters(post, filters) + it.updateWith(post, applyFilters.filterResults) + it + } + } + + updateTimelineObject(timelineObjectList) + } + + protected abstract suspend fun getActorPost(actorId: ActorId, visibilityList: List): List + + override suspend fun removePost(post: Post) { + removeTimelineObject(post.id) + } + + override suspend fun addTimelineRelationship(timelineRelationship: TimelineRelationship) { + val visibilityList = visibilities(timelineRelationship) + val postList = getActorPost(timelineRelationship.actorId, visibilityList) + val timeline = getTimeline(timelineRelationship.timelineId) ?: return + val timelineObjects = postList.mapNotNull { post -> + val repost = post.repostId?.let { getPost(it) } + val replyActorId = post.replyId?.let { getPost(it)?.actorId } + createTimelineObject(post, replyActorId, repost, timeline) + } + + insertTimelineObject(timelineObjects) + } + + protected fun visibilities(timelineRelationship: TimelineRelationship): List { + val visibilityList = when (timelineRelationship.visible) { + Visible.PUBLIC -> { + listOf(Visibility.PUBLIC) + } + + Visible.UNLISTED -> { + listOf(Visibility.PUBLIC, Visibility.UNLISTED) + } + + Visible.FOLLOWERS -> { + listOf(Visibility.PUBLIC, Visibility.UNLISTED, Visibility.FOLLOWERS) + } + + Visible.DIRECT -> { + listOf(Visibility.PUBLIC, Visibility.UNLISTED, Visibility.FOLLOWERS, Visibility.DIRECT) + } + } + return visibilityList + } + + override suspend fun removeTimelineRelationship(timelineRelationship: TimelineRelationship) { + removeTimelineObject(timelineRelationship.timelineId, timelineRelationship.actorId) + } + + override suspend fun updateTimelineRelationship(timelineRelationship: TimelineRelationship) { + removeTimelineRelationship(timelineRelationship) + addTimelineRelationship(timelineRelationship) + } + + override suspend fun addTimeline(timeline: Timeline, timelineRelationshipList: List) { + val postList = getPostsByTimelineRelationshipList(timelineRelationshipList) + + val timelineObjectList = postList.mapNotNull { post -> + val repost = post.repostId?.let { getPost(it) } + val replyActorId = post.replyId?.let { getPost(it)?.actorId } + createTimelineObject(post, replyActorId, repost, timeline) + } + + insertTimelineObject(timelineObjectList) + } + + override suspend fun removeTimeline(timeline: Timeline) { + removeTimelineObject(timeline.id) + } + + override suspend fun readTimeline( + timeline: Timeline, + option: ReadTimelineOption?, + page: Page? + ): PaginationList { + val timelineObjectList = getTimelineObject(timeline.id, option, page) + val lastUpdatedAt = timelineObjectList.minBy { it.lastUpdatedAt }.lastUpdatedAt + + val newerFilters = getNewerFilters(timeline.userDetailId, lastUpdatedAt) + + val posts = + getPostsByPostId( + timelineObjectList.map { + it.postId + } + timelineObjectList.mapNotNull { it.repostId } + timelineObjectList.mapNotNull { it.replyId } + ) + + val userDetails = getUserDetails(timelineObjectList.map { it.userDetailId }) + + val actors = + getActors( + timelineObjectList.map { + it.postActorId + } + timelineObjectList.mapNotNull { it.repostActorId } + timelineObjectList.mapNotNull { it.replyActorId } + ) + + val postMap = posts.associate { post -> + post.id to applyFilters(post, newerFilters) + } + + return PaginationList( + timelineObjectList.mapNotNull { + val timelineUserDetail = userDetails[it.userDetailId] ?: return@mapNotNull null + val actor = actors[it.postActorId] ?: return@mapNotNull null + val post = postMap[it.postId] ?: return@mapNotNull null + val reply = postMap[it.replyId] + val replyActor = actors[it.replyActorId] + val repost = postMap[it.repostId] + val repostActor = actors[it.repostActorId] + TimelineObjectDetail.of( + timelineObject = it, + timelineUserDetail = timelineUserDetail, + post = post.post, + postActor = actor, + replyPost = reply?.post, + replyPostActor = replyActor, + repostPost = repost?.post, + repostPostActor = repostActor, + warnFilter = it.warnFilters + post.filterResults.map { + TimelineObjectWarnFilter( + it.filter.id, + it.matchedKeyword + ) + } + ) + }, + timelineObjectList.lastOrNull()?.postId, + timelineObjectList.firstOrNull()?.postId + ) + } + + protected abstract suspend fun getActors(actorIds: List): Map + + protected abstract suspend fun getUserDetails(userDetailIdList: List): Map +} diff --git a/hideout-core/src/main/kotlin/dev/usbharu/hideout/core/infrastructure/timeline/DefaultTimelineStore.kt b/hideout-core/src/main/kotlin/dev/usbharu/hideout/core/infrastructure/timeline/DefaultTimelineStore.kt new file mode 100644 index 00000000..95c0b11a --- /dev/null +++ b/hideout-core/src/main/kotlin/dev/usbharu/hideout/core/infrastructure/timeline/DefaultTimelineStore.kt @@ -0,0 +1,137 @@ +package dev.usbharu.hideout.core.infrastructure.timeline + +import dev.usbharu.hideout.core.config.DefaultTimelineStoreConfig +import dev.usbharu.hideout.core.domain.model.actor.Actor +import dev.usbharu.hideout.core.domain.model.actor.ActorId +import dev.usbharu.hideout.core.domain.model.actor.ActorRepository +import dev.usbharu.hideout.core.domain.model.filter.Filter +import dev.usbharu.hideout.core.domain.model.filter.FilterContext +import dev.usbharu.hideout.core.domain.model.filter.FilterRepository +import dev.usbharu.hideout.core.domain.model.filter.FilteredPost +import dev.usbharu.hideout.core.domain.model.post.Post +import dev.usbharu.hideout.core.domain.model.post.PostId +import dev.usbharu.hideout.core.domain.model.post.PostRepository +import dev.usbharu.hideout.core.domain.model.post.Visibility +import dev.usbharu.hideout.core.domain.model.support.page.Page +import dev.usbharu.hideout.core.domain.model.support.page.PaginationList +import dev.usbharu.hideout.core.domain.model.timeline.Timeline +import dev.usbharu.hideout.core.domain.model.timeline.TimelineId +import dev.usbharu.hideout.core.domain.model.timeline.TimelineRepository +import dev.usbharu.hideout.core.domain.model.timelineobject.TimelineObject +import dev.usbharu.hideout.core.domain.model.timelinerelationship.TimelineRelationship +import dev.usbharu.hideout.core.domain.model.timelinerelationship.TimelineRelationshipRepository +import dev.usbharu.hideout.core.domain.model.userdetails.UserDetail +import dev.usbharu.hideout.core.domain.model.userdetails.UserDetailId +import dev.usbharu.hideout.core.domain.model.userdetails.UserDetailRepository +import dev.usbharu.hideout.core.domain.service.filter.FilterDomainService +import dev.usbharu.hideout.core.domain.shared.id.IdGenerateService +import dev.usbharu.hideout.core.external.timeline.ReadTimelineOption +import org.springframework.stereotype.Component +import java.time.Instant + +@Component +open class DefaultTimelineStore( + private val timelineRepository: TimelineRepository, + private val timelineRelationshipRepository: TimelineRelationshipRepository, + private val filterRepository: FilterRepository, + private val postRepository: PostRepository, + private val filterDomainService: FilterDomainService, + idGenerateService: IdGenerateService, + private val defaultTimelineStoreConfig: DefaultTimelineStoreConfig, + private val internalTimelineObjectRepository: InternalTimelineObjectRepository, + private val userDetailRepository: UserDetailRepository, + private val actorRepository: ActorRepository +) : AbstractTimelineStore(idGenerateService) { + override suspend fun getTimelines(actorId: ActorId): List { + return timelineRepository.findByIds( + timelineRelationshipRepository + .findByActorId( + actorId + ).map { it.timelineId } + ) + } + + override suspend fun getTimeline(timelineId: TimelineId): Timeline? { + return timelineRepository.findById(timelineId) + } + + override suspend fun getFilters(userDetailId: UserDetailId): List { + return filterRepository.findByUserDetailId(userDetailId) + } + + override suspend fun getNewerFilters(userDetailId: UserDetailId, lastUpdateAt: Instant): List { + TODO("Not yet implemented") + } + + override suspend fun applyFilters(post: Post, filters: List): FilteredPost { + return filterDomainService.apply(post, FilterContext.HOME, filters) + } + + override suspend fun getPost(postId: PostId): Post? { + return postRepository.findById(postId) + } + + override suspend fun insertTimelineObject(timelineObjectList: List) { + internalTimelineObjectRepository.saveAll(timelineObjectList) + } + + override suspend fun updateTimelineObject(timelineObjectList: List) { + internalTimelineObjectRepository.saveAll(timelineObjectList) + } + + override suspend fun getTimelineObjectByPostId(postId: PostId): List { + return internalTimelineObjectRepository.findByPostId(postId) + } + + override suspend fun removeTimelineObject(postId: PostId) { + internalTimelineObjectRepository.deleteByPostId(postId) + } + + override suspend fun removeTimelineObject(timelineId: TimelineId, actorId: ActorId) { + internalTimelineObjectRepository.deleteByTimelineIdAndActorId(timelineId, actorId) + } + + override suspend fun removeTimelineObject(timelineId: TimelineId) { + internalTimelineObjectRepository.deleteByTimelineId(timelineId) + } + + override suspend fun getPostsByTimelineRelationshipList(timelineRelationshipList: List): List { + return timelineRelationshipList.flatMap { getActorPost(it.actorId, visibilities(it)) } + } + + override suspend fun getPostsByPostId(postIds: List): List { + return postRepository.findAllById(postIds) + } + + override suspend fun getTimelineObject( + timelineId: TimelineId, + readTimelineOption: ReadTimelineOption?, + page: Page? + ): PaginationList { + return internalTimelineObjectRepository.findByTimelineId( + timelineId, + InternalTimelineObjectOption( + readTimelineOption?.local, + readTimelineOption?.remote, + readTimelineOption?.mediaOnly + ), + page + ) + } + + override suspend fun getActorPost(actorId: ActorId, visibilityList: List): List { + return postRepository.findByActorIdAndVisibilityInList( + actorId, + visibilityList, + Page.of(limit = defaultTimelineStoreConfig.actorPostsCount) + ) + } + + override suspend fun getActors(actorIds: List): Map { + return actorRepository.findAllById(actorIds).associateBy { it.id } + } + + override suspend fun getUserDetails(userDetailIdList: List): Map { + return userDetailRepository.findAllById(userDetailIdList).associateBy { it.id } + } +} diff --git a/hideout-core/src/main/kotlin/dev/usbharu/hideout/core/infrastructure/timeline/InternalTimelineObjectRepository.kt b/hideout-core/src/main/kotlin/dev/usbharu/hideout/core/infrastructure/timeline/InternalTimelineObjectRepository.kt new file mode 100644 index 00000000..490be0ab --- /dev/null +++ b/hideout-core/src/main/kotlin/dev/usbharu/hideout/core/infrastructure/timeline/InternalTimelineObjectRepository.kt @@ -0,0 +1,33 @@ +package dev.usbharu.hideout.core.infrastructure.timeline + +import dev.usbharu.hideout.core.domain.model.actor.ActorId +import dev.usbharu.hideout.core.domain.model.post.PostId +import dev.usbharu.hideout.core.domain.model.support.page.Page +import dev.usbharu.hideout.core.domain.model.support.page.PaginationList +import dev.usbharu.hideout.core.domain.model.timeline.TimelineId +import dev.usbharu.hideout.core.domain.model.timelineobject.TimelineObject + +interface InternalTimelineObjectRepository { + suspend fun save(timelineObject: TimelineObject): TimelineObject + + suspend fun saveAll(timelineObjectList: List): List + + suspend fun findByPostId(postId: PostId): List + + suspend fun deleteByPostId(postId: PostId) + + suspend fun deleteByTimelineIdAndActorId(timelineId: TimelineId, actorId: ActorId) + + suspend fun deleteByTimelineId(timelineId: TimelineId) + suspend fun findByTimelineId( + timelineId: TimelineId, + internalTimelineObjectOption: InternalTimelineObjectOption? = null, + page: Page? = null + ): PaginationList +} + +data class InternalTimelineObjectOption( + val localOnly: Boolean? = null, + val remoteOnly: Boolean? = null, + val mediaOnly: Boolean? = null +) \ No newline at end of file diff --git a/hideout-core/src/main/kotlin/dev/usbharu/hideout/core/interfaces/api/auth/AuthController.kt b/hideout-core/src/main/kotlin/dev/usbharu/hideout/core/interfaces/api/auth/AuthController.kt new file mode 100644 index 00000000..8872d60d --- /dev/null +++ b/hideout-core/src/main/kotlin/dev/usbharu/hideout/core/interfaces/api/auth/AuthController.kt @@ -0,0 +1,46 @@ +/* + * Copyright (C) 2024 usbharu + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package dev.usbharu.hideout.core.interfaces.api.auth + +import dev.usbharu.hideout.core.application.actor.RegisterLocalActor +import dev.usbharu.hideout.core.application.actor.RegisterLocalActorApplicationService +import dev.usbharu.hideout.core.domain.model.support.principal.Anonymous +import jakarta.servlet.http.HttpServletRequest +import org.springframework.stereotype.Controller +import org.springframework.validation.annotation.Validated +import org.springframework.web.bind.annotation.GetMapping +import org.springframework.web.bind.annotation.ModelAttribute +import org.springframework.web.bind.annotation.PostMapping + +@Controller +class AuthController( + private val registerLocalActorApplicationService: RegisterLocalActorApplicationService, +) { + @GetMapping("/auth/sign_up") + @Suppress("FunctionOnlyReturningConstant") + fun signUp(): String = "sign_up" + + @PostMapping("/auth/sign_up") + suspend fun signUp(@Validated @ModelAttribute signUpForm: SignUpForm, request: HttpServletRequest): String { + val registerLocalActor = RegisterLocalActor(signUpForm.username, signUpForm.password) + val uri = registerLocalActorApplicationService.execute( + registerLocalActor, Anonymous + ) + request.login(signUpForm.username, signUpForm.password) + return "redirect:$uri" + } +} diff --git a/hideout-core/src/main/kotlin/dev/usbharu/hideout/core/interfaces/api/auth/SignUpForm.kt b/hideout-core/src/main/kotlin/dev/usbharu/hideout/core/interfaces/api/auth/SignUpForm.kt new file mode 100644 index 00000000..d70eb9c2 --- /dev/null +++ b/hideout-core/src/main/kotlin/dev/usbharu/hideout/core/interfaces/api/auth/SignUpForm.kt @@ -0,0 +1,7 @@ +package dev.usbharu.hideout.core.interfaces.api.auth + +data class SignUpForm( + val username: String, + val password: String, +// val recaptchaResponse: String +) diff --git a/hideout-core/src/main/kotlin/dev/usbharu/hideout/core/interfaces/api/media/LocalFileController.kt b/hideout-core/src/main/kotlin/dev/usbharu/hideout/core/interfaces/api/media/LocalFileController.kt new file mode 100644 index 00000000..d41762c0 --- /dev/null +++ b/hideout-core/src/main/kotlin/dev/usbharu/hideout/core/interfaces/api/media/LocalFileController.kt @@ -0,0 +1,44 @@ +/* + * Copyright (C) 2024 usbharu + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package dev.usbharu.hideout.core.interfaces.api.media + +import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty +import org.springframework.core.io.ClassPathResource +import org.springframework.core.io.Resource +import org.springframework.http.MediaType +import org.springframework.http.ResponseEntity +import org.springframework.stereotype.Controller +import org.springframework.web.bind.annotation.GetMapping +import org.springframework.web.bind.annotation.PathVariable + +@Controller +@ConditionalOnProperty("hideout.storage.type", havingValue = "local", matchIfMissing = true) +interface LocalFileController { + + @GetMapping("/files/{id}") + fun files(@PathVariable("id") id: String): ResponseEntity + + @GetMapping("/users/{user}/icon.jpg", "/users/{user}/header.jpg") + fun icons(): ResponseEntity { + val pathResource = ClassPathResource("icon.png") + return ResponseEntity + .ok() + .contentType(MediaType.IMAGE_PNG) + .contentLength(pathResource.contentLength()) + .body(pathResource) + } +} diff --git a/hideout-core/src/main/kotlin/dev/usbharu/hideout/core/query/principal/PrincipalDTO.kt b/hideout-core/src/main/kotlin/dev/usbharu/hideout/core/query/principal/PrincipalDTO.kt new file mode 100644 index 00000000..40b0cb90 --- /dev/null +++ b/hideout-core/src/main/kotlin/dev/usbharu/hideout/core/query/principal/PrincipalDTO.kt @@ -0,0 +1,6 @@ +package dev.usbharu.hideout.core.query.principal + +import dev.usbharu.hideout.core.domain.model.actor.ActorId +import dev.usbharu.hideout.core.domain.model.userdetails.UserDetailId + +data class PrincipalDTO(val userDetailId: UserDetailId, val actorId: ActorId, val username: String, val host: String) \ No newline at end of file diff --git a/hideout-core/src/main/kotlin/dev/usbharu/hideout/core/query/principal/PrincipalQueryService.kt b/hideout-core/src/main/kotlin/dev/usbharu/hideout/core/query/principal/PrincipalQueryService.kt new file mode 100644 index 00000000..3aa01531 --- /dev/null +++ b/hideout-core/src/main/kotlin/dev/usbharu/hideout/core/query/principal/PrincipalQueryService.kt @@ -0,0 +1,7 @@ +package dev.usbharu.hideout.core.query.principal + +import dev.usbharu.hideout.core.domain.model.userdetails.UserDetailId + +interface PrincipalQueryService { + suspend fun findByUserDetailId(userDetailId: UserDetailId): PrincipalDTO +} \ No newline at end of file diff --git a/hideout-core/src/main/kotlin/dev/usbharu/hideout/generate/JsonOrFormBind.kt b/hideout-core/src/main/kotlin/dev/usbharu/hideout/generate/JsonOrFormBind.kt new file mode 100644 index 00000000..4a74319e --- /dev/null +++ b/hideout-core/src/main/kotlin/dev/usbharu/hideout/generate/JsonOrFormBind.kt @@ -0,0 +1,22 @@ +/* + * Copyright (C) 2024 usbharu + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package dev.usbharu.hideout.generate + +@MustBeDocumented +@Retention(AnnotationRetention.RUNTIME) +@Target(AnnotationTarget.VALUE_PARAMETER) +annotation class JsonOrFormBind diff --git a/hideout-core/src/main/kotlin/dev/usbharu/hideout/generate/JsonOrFormModelMethodProcessor.kt b/hideout-core/src/main/kotlin/dev/usbharu/hideout/generate/JsonOrFormModelMethodProcessor.kt new file mode 100644 index 00000000..d7a97736 --- /dev/null +++ b/hideout-core/src/main/kotlin/dev/usbharu/hideout/generate/JsonOrFormModelMethodProcessor.kt @@ -0,0 +1,78 @@ +/* + * Copyright (C) 2024 usbharu + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package dev.usbharu.hideout.generate + +import org.slf4j.Logger +import org.slf4j.LoggerFactory +import org.springframework.core.MethodParameter +import org.springframework.validation.BindException +import org.springframework.web.bind.support.WebDataBinderFactory +import org.springframework.web.context.request.NativeWebRequest +import org.springframework.web.method.annotation.ModelAttributeMethodProcessor +import org.springframework.web.method.support.HandlerMethodArgumentResolver +import org.springframework.web.method.support.ModelAndViewContainer +import org.springframework.web.servlet.mvc.method.annotation.RequestResponseBodyMethodProcessor + +@Suppress("TooGenericExceptionCaught") +class JsonOrFormModelMethodProcessor( + private val modelAttributeMethodProcessor: ModelAttributeMethodProcessor, + private val requestResponseBodyMethodProcessor: RequestResponseBodyMethodProcessor, +) : HandlerMethodArgumentResolver { + private val isJsonRegex = Regex("application/((\\w*)\\+)?json") + + override fun supportsParameter(parameter: MethodParameter): Boolean = + parameter.hasParameterAnnotation(JsonOrFormBind::class.java) + + override fun resolveArgument( + parameter: MethodParameter, + mavContainer: ModelAndViewContainer?, + webRequest: NativeWebRequest, + binderFactory: WebDataBinderFactory?, + ): Any? { + val contentType = webRequest.getHeader("Content-Type").orEmpty() + logger.trace("ContentType is {}", contentType) + if (contentType.contains(isJsonRegex)) { + logger.trace("Determine content type as json.") + return requestResponseBodyMethodProcessor.resolveArgument( + parameter, + mavContainer, + webRequest, + binderFactory + ) + } + + return try { + modelAttributeMethodProcessor.resolveArgument(parameter, mavContainer, webRequest, binderFactory) + } catch (e: BindException) { + throw e + } catch (exception: Exception) { + try { + requestResponseBodyMethodProcessor.resolveArgument(parameter, mavContainer, webRequest, binderFactory) + } catch (e: BindException) { + throw e + } catch (e: Exception) { + logger.warn("Failed to bind request (1)", exception) + logger.warn("Failed to bind request (2)", e) + throw IllegalArgumentException("Failed to bind request.") + } + } + } + + companion object { + val logger: Logger = LoggerFactory.getLogger(JsonOrFormModelMethodProcessor::class.java) + } +} diff --git a/hideout-core/src/main/kotlin/dev/usbharu/hideout/util/Base64Util.kt b/hideout-core/src/main/kotlin/dev/usbharu/hideout/util/Base64Util.kt new file mode 100644 index 00000000..b7b83f73 --- /dev/null +++ b/hideout-core/src/main/kotlin/dev/usbharu/hideout/util/Base64Util.kt @@ -0,0 +1,25 @@ +/* + * Copyright (C) 2024 usbharu + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package dev.usbharu.hideout.util + +import java.util.* + +object Base64Util { + fun decode(str: String): ByteArray = Base64.getDecoder().decode(str) + + fun encode(bytes: ByteArray): String = Base64.getEncoder().encodeToString(bytes) +} diff --git a/hideout-core/src/main/kotlin/dev/usbharu/hideout/util/EmojiUtil.kt b/hideout-core/src/main/kotlin/dev/usbharu/hideout/util/EmojiUtil.kt new file mode 100644 index 00000000..8c044db2 --- /dev/null +++ b/hideout-core/src/main/kotlin/dev/usbharu/hideout/util/EmojiUtil.kt @@ -0,0 +1,30 @@ +/* + * Copyright (C) 2024 usbharu + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package dev.usbharu.hideout.util + +import Emojis + +object EmojiUtil { + + val emojiMap by lazy { + Emojis.allEmojis + .associate { it.code.replace(" ", "-") to it.char } + .filterValues { it != "™" } + } + + fun isEmoji(string: String): Boolean = emojiMap.any { it.value == string } +} diff --git a/hideout-core/src/main/kotlin/dev/usbharu/hideout/util/RsaUtil.kt b/hideout-core/src/main/kotlin/dev/usbharu/hideout/util/RsaUtil.kt new file mode 100644 index 00000000..56b20ac3 --- /dev/null +++ b/hideout-core/src/main/kotlin/dev/usbharu/hideout/util/RsaUtil.kt @@ -0,0 +1,47 @@ +/* + * Copyright (C) 2024 usbharu + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package dev.usbharu.hideout.util + +import java.security.KeyFactory +import java.security.interfaces.RSAPrivateKey +import java.security.interfaces.RSAPublicKey +import java.security.spec.PKCS8EncodedKeySpec +import java.security.spec.X509EncodedKeySpec + +object RsaUtil { + private val replaceHeaderAndFooterRegex = Regex("-----.*?-----") + + fun decodeRsaPublicKey(byteArray: ByteArray): RSAPublicKey { + val x509EncodedKeySpec = X509EncodedKeySpec(byteArray) + return KeyFactory.getInstance("RSA").generatePublic(x509EncodedKeySpec) as RSAPublicKey + } + + fun decodeRsaPublicKey(encoded: String): RSAPublicKey = decodeRsaPublicKey(Base64Util.decode(encoded)) + + fun decodeRsaPublicKeyPem(pem: String): RSAPublicKey { + val replace = pem.replace(replaceHeaderAndFooterRegex, "") + .replace("\n", "") + return decodeRsaPublicKey(replace) + } + + fun decodeRsaPrivateKey(byteArray: ByteArray): RSAPrivateKey { + val pkcS8EncodedKeySpec = PKCS8EncodedKeySpec(byteArray) + return KeyFactory.getInstance("RSA").generatePrivate(pkcS8EncodedKeySpec) as RSAPrivateKey + } + + fun decodeRsaPrivateKey(encoded: String): RSAPrivateKey = decodeRsaPrivateKey(Base64Util.decode(encoded)) +} diff --git a/hideout-core/src/main/resources/META-INF/native-image/jni-config.json b/hideout-core/src/main/resources/META-INF/native-image/jni-config.json new file mode 100644 index 00000000..c89c44cc --- /dev/null +++ b/hideout-core/src/main/resources/META-INF/native-image/jni-config.json @@ -0,0 +1,31 @@ +[ + { + "name": "sun.management.VMManagementImpl", + "fields": [ + { + "name": "compTimeMonitoringSupport" + }, + { + "name": "currentThreadCpuTimeSupport" + }, + { + "name": "objectMonitorUsageSupport" + }, + { + "name": "otherThreadCpuTimeSupport" + }, + { + "name": "remoteDiagnosticCommandsSupport" + }, + { + "name": "synchronizerUsageSupport" + }, + { + "name": "threadAllocatedMemorySupport" + }, + { + "name": "threadContentionMonitoringSupport" + } + ] + } +] diff --git a/hideout-core/src/main/resources/META-INF/native-image/predefined-classes-config.json b/hideout-core/src/main/resources/META-INF/native-image/predefined-classes-config.json new file mode 100644 index 00000000..9587dc34 --- /dev/null +++ b/hideout-core/src/main/resources/META-INF/native-image/predefined-classes-config.json @@ -0,0 +1,7 @@ +[ + { + "type": "agent-extracted", + "classes": [ + ] + } +] diff --git a/hideout-core/src/main/resources/META-INF/native-image/proxy-config.json b/hideout-core/src/main/resources/META-INF/native-image/proxy-config.json new file mode 100644 index 00000000..0d4f101c --- /dev/null +++ b/hideout-core/src/main/resources/META-INF/native-image/proxy-config.json @@ -0,0 +1,2 @@ +[ +] diff --git a/hideout-core/src/main/resources/META-INF/native-image/reflect-config.json b/hideout-core/src/main/resources/META-INF/native-image/reflect-config.json new file mode 100644 index 00000000..33c8e95d --- /dev/null +++ b/hideout-core/src/main/resources/META-INF/native-image/reflect-config.json @@ -0,0 +1,1294 @@ +[ + { + "name": "[Lcom.fasterxml.jackson.databind.deser.Deserializers;" + }, + { + "name": "[Lcom.fasterxml.jackson.databind.deser.KeyDeserializers;" + }, + { + "name": "[Lcom.fasterxml.jackson.databind.deser.ValueInstantiators;" + }, + { + "name": "[Lcom.fasterxml.jackson.databind.ser.Serializers;" + }, + { + "name": "[Ljava.lang.String;" + }, + { + "name": "android.os.Build$VERSION" + }, + { + "name": "ch.qos.logback.classic.encoder.PatternLayoutEncoder", + "queryAllPublicMethods": true, + "methods": [ + { + "name": "", + "parameterTypes": [ + ] + } + ] + }, + { + "name": "ch.qos.logback.classic.pattern.DateConverter", + "methods": [ + { + "name": "", + "parameterTypes": [ + ] + } + ] + }, + { + "name": "ch.qos.logback.classic.pattern.LevelConverter", + "methods": [ + { + "name": "", + "parameterTypes": [ + ] + } + ] + }, + { + "name": "ch.qos.logback.classic.pattern.LineSeparatorConverter", + "methods": [ + { + "name": "", + "parameterTypes": [ + ] + } + ] + }, + { + "name": "ch.qos.logback.classic.pattern.LoggerConverter", + "methods": [ + { + "name": "", + "parameterTypes": [ + ] + } + ] + }, + { + "name": "ch.qos.logback.classic.pattern.MessageConverter", + "methods": [ + { + "name": "", + "parameterTypes": [ + ] + } + ] + }, + { + "name": "ch.qos.logback.classic.pattern.ThreadConverter", + "methods": [ + { + "name": "", + "parameterTypes": [ + ] + } + ] + }, + { + "name": "ch.qos.logback.core.ConsoleAppender", + "queryAllPublicMethods": true, + "methods": [ + { + "name": "", + "parameterTypes": [ + ] + } + ] + }, + { + "name": "ch.qos.logback.core.OutputStreamAppender", + "methods": [ + { + "name": "setEncoder", + "parameterTypes": [ + "ch.qos.logback.core.encoder.Encoder" + ] + } + ] + }, + { + "name": "ch.qos.logback.core.encoder.LayoutWrappingEncoder", + "methods": [ + { + "name": "setParent", + "parameterTypes": [ + "ch.qos.logback.core.spi.ContextAware" + ] + } + ] + }, + { + "name": "ch.qos.logback.core.pattern.PatternLayoutEncoderBase", + "methods": [ + { + "name": "setPattern", + "parameterTypes": [ + ] + }, + { + "name": "setPattern", + "parameterTypes": [ + "java.lang.String" + ] + } + ] + }, + { + "name": "com.fasterxml.jackson.databind.ext.Java7SupportImpl", + "methods": [ + { + "name": "", + "parameterTypes": [ + ] + } + ] + }, + { + "name": "com.ibm.icu.text.Collator" + }, + { + "name": "com.sun.org.apache.xerces.internal.jaxp.SAXParserFactoryImpl", + "methods": [ + { + "name": "", + "parameterTypes": [ + ] + } + ] + }, + { + "name": "dev.usbharu.hideout.ApplicationKt", + "queryAllPublicMethods": true, + "methods": [ + { + "name": "main", + "parameterTypes": [ + ] + }, + { + "name": "parent", + "parameterTypes": [ + "io.ktor.server.application.Application" + ] + }, + { + "name": "worker", + "parameterTypes": [ + "io.ktor.server.application.Application" + ] + } + ] + }, + { + "name": "io.ktor.http.HttpStatusCode" + }, + { + "name": "io.ktor.http.Parameters" + }, + { + "name": "io.ktor.server.application.Application" + }, + { + "name": "javax.smartcardio.CardPermission" + }, + { + "name": "kotlin.Any" + }, + { + "name": "kotlin.Array" + }, + { + "name": "kotlin.ExtensionFunctionType" + }, + { + "name": "kotlin.Function2" + }, + { + "name": "kotlin.Metadata", + "queryAllDeclaredMethods": true, + "methods": [ + { + "name": "bv", + "parameterTypes": [ + ] + }, + { + "name": "d1", + "parameterTypes": [ + ] + }, + { + "name": "d2", + "parameterTypes": [ + ] + }, + { + "name": "k", + "parameterTypes": [ + ] + }, + { + "name": "mv", + "parameterTypes": [ + ] + }, + { + "name": "pn", + "parameterTypes": [ + ] + }, + { + "name": "xi", + "parameterTypes": [ + ] + }, + { + "name": "xs", + "parameterTypes": [ + ] + } + ] + }, + { + "name": "kotlin.ParameterName" + }, + { + "name": "kotlin.String" + }, + { + "name": "kotlin.Unit" + }, + { + "name": "kotlin.internal.jdk8.JDK8PlatformImplementations", + "methods": [ + { + "name": "", + "parameterTypes": [ + ] + } + ] + }, + { + "name": "kotlin.jvm.internal.DefaultConstructorMarker" + }, + { + "name": "kotlin.reflect.jvm.internal.ReflectionFactoryImpl", + "methods": [ + { + "name": "", + "parameterTypes": [ + ] + } + ] + }, + { + "name": "kotlin.reflect.jvm.internal.impl.resolve.scopes.DescriptorKindFilter", + "allPublicFields": true + }, + { + "name": "org.h2.Driver", + "methods": [ + { + "name": "", + "parameterTypes": [ + ] + } + ] + }, + { + "name": "org.h2.mvstore.db.LobStorageMap$BlobMeta$Type", + "fields": [ + { + "name": "INSTANCE" + } + ] + }, + { + "name": "org.h2.mvstore.db.LobStorageMap$BlobReference$Type", + "fields": [ + { + "name": "INSTANCE" + } + ] + }, + { + "name": "org.h2.mvstore.db.NullValueDataType", + "fields": [ + { + "name": "INSTANCE" + } + ] + }, + { + "name": "org.h2.mvstore.db.RowDataType$Factory", + "methods": [ + { + "name": "", + "parameterTypes": [ + ] + } + ] + }, + { + "name": "org.h2.mvstore.tx.VersionedValueType$Factory", + "methods": [ + { + "name": "", + "parameterTypes": [ + ] + } + ] + }, + { + "name": "org.h2.mvstore.type.ByteArrayDataType", + "fields": [ + { + "name": "INSTANCE" + } + ] + }, + { + "name": "org.h2.mvstore.type.LongDataType", + "fields": [ + { + "name": "INSTANCE" + } + ] + }, + { + "name": "org.h2.store.fs.async.FilePathAsync", + "methods": [ + { + "name": "", + "parameterTypes": [ + ] + } + ] + }, + { + "name": "org.h2.store.fs.disk.FilePathDisk", + "methods": [ + { + "name": "", + "parameterTypes": [ + ] + } + ] + }, + { + "name": "org.h2.store.fs.mem.FilePathMem", + "methods": [ + { + "name": "", + "parameterTypes": [ + ] + } + ] + }, + { + "name": "org.h2.store.fs.mem.FilePathMemLZF", + "methods": [ + { + "name": "", + "parameterTypes": [ + ] + } + ] + }, + { + "name": "org.h2.store.fs.niomapped.FilePathNioMapped", + "methods": [ + { + "name": "", + "parameterTypes": [ + ] + } + ] + }, + { + "name": "org.h2.store.fs.niomem.FilePathNioMem", + "methods": [ + { + "name": "", + "parameterTypes": [ + ] + } + ] + }, + { + "name": "org.h2.store.fs.niomem.FilePathNioMemLZF", + "methods": [ + { + "name": "", + "parameterTypes": [ + ] + } + ] + }, + { + "name": "org.h2.store.fs.retry.FilePathRetryOnInterrupt", + "methods": [ + { + "name": "", + "parameterTypes": [ + ] + } + ] + }, + { + "name": "org.h2.store.fs.split.FilePathSplit", + "methods": [ + { + "name": "", + "parameterTypes": [ + ] + } + ] + }, + { + "name": "org.h2.store.fs.zip.FilePathZip", + "methods": [ + { + "name": "", + "parameterTypes": [ + ] + } + ] + }, + { + "name": "org.locationtech.jts.geom.Geometry" + }, + { + "name": "sun.security.provider.DRBG", + "methods": [ + { + "name": "", + "parameterTypes": [ + ] + } + ] + }, + { + "name": "sun.security.provider.SHA", + "methods": [ + { + "name": "", + "parameterTypes": [ + ] + } + ] + }, + { + "name": "sun.security.provider.SHA2$SHA256", + "methods": [ + { + "name": "", + "parameterTypes": [ + ] + } + ] + }, + { + "name": "sun.security.rsa.RSAKeyPairGenerator$Legacy", + "methods": [ + { + "name": "", + "parameterTypes": [ + ] + } + ] + }, + { + "name": "kotlin.reflect.jvm.internal.ReflectionFactoryImpl", + "allDeclaredConstructors": true + }, + { + "name": "kotlin.KotlinVersion", + "allPublicMethods": true, + "allDeclaredFields": true, + "allDeclaredMethods": true, + "allDeclaredConstructors": true + }, + { + "name": "kotlin.KotlinVersion[]" + }, + { + "name": "kotlin.KotlinVersion$Companion" + }, + { + "name": "kotlin.KotlinVersion$Companion[]" + }, + { + "name": "kotlin.internal.jdk8.JDK8PlatformImplementations", + "allPublicMethods": true, + "allDeclaredFields": true, + "allDeclaredMethods": true, + "allDeclaredConstructors": true + }, + { + "name": "kotlin", + "allPublicMethods": true, + "allDeclaredFields": true, + "allDeclaredMethods": true, + "allDeclaredConstructors": true + }, + { + "name": "io.igx.kotlin.model.Driver", + "allDeclaredConstructors": true, + "allPublicConstructors": true, + "allDeclaredMethods": true, + "allPublicMethods": true, + "fields": [ + { + "name": "id" + }, + { + "name": "firstName" + }, + { + "name": "lastName" + }, + { + "name": "nationality" + } + ] + }, + { + "name": "java.lang.Integer", + "methods": [ + { + "name": "parseInt", + "parameterTypes": [ + "java.lang.String" + ] + } + ] + }, + { + "name": "java.lang.Long", + "methods": [ + { + "name": "parseLong", + "parameterTypes": [ + "java.lang.String" + ] + } + ] + }, + { + "name": "java.lang.Boolean", + "methods": [ + { + "name": "parseBoolean", + "parameterTypes": [ + "java.lang.String" + ] + } + ] + }, + { + "name": "java.lang.Byte", + "methods": [ + { + "name": "parseByte", + "parameterTypes": [ + "java.lang.String" + ] + } + ] + }, + { + "name": "java.lang.Short", + "methods": [ + { + "name": "parseShort", + "parameterTypes": [ + "java.lang.String" + ] + } + ] + }, + { + "name": "java.lang.Float", + "methods": [ + { + "name": "parseFloat", + "parameterTypes": [ + "java.lang.String" + ] + } + ] + }, + { + "name": "java.lang.Double", + "methods": [ + { + "name": "parseDouble", + "parameterTypes": [ + "java.lang.String" + ] + } + ] + }, + { + "name": "dev.usbharu.hideout.domain.model.api.StatusForPost", + "allPublicMethods": true, + "allDeclaredFields": true, + "allDeclaredMethods": true, + "allDeclaredConstructors": true + }, + { + "name": "dev.usbharu.hideout.domain.model.ap.Follow", + "allPublicMethods": true, + "allDeclaredFields": true, + "allDeclaredMethods": true, + "allDeclaredConstructors": true + }, + { + "name": "dev.usbharu.hideout.EmptyKt", + "allDeclaredClasses": true, + "queryAllDeclaredMethods": true, + "queryAllPublicMethods": true, + "methods": [ + { + "name": "empty", + "parameterTypes": [ + "io.ktor.server.application.Application" + ] + } + ] + }, + { + "name": "dev.usbharu.hideout.domain.model.PostEntity", + "queryAllDeclaredMethods": true, + "allDeclaredFields": true, + "queryAllDeclaredConstructors": true, + "methods": [ + { + "name": "", + "parameterTypes": [ + "long", + "long", + "long", + "int" + ] + }, + { + "name": "", + "parameterTypes": [ + "long", + "long", + "long", + "int", + "int", + "kotlin.jvm.internal.DefaultConstructorMarker" + ] + }, + { + "name": "getCreatedAt", + "parameterTypes": [ + ] + }, + { + "name": "getId", + "parameterTypes": [ + ] + }, + { + "name": "getOverview", + "parameterTypes": [ + ] + }, + { + "name": "getReplyId", + "parameterTypes": [ + ] + }, + { + "name": "getRepostId", + "parameterTypes": [ + ] + }, + { + "name": "getText", + "parameterTypes": [ + ] + }, + { + "name": "getUrl", + "parameterTypes": [ + ] + }, + { + "name": "getUserId", + "parameterTypes": [ + ] + }, + { + "name": "getVisibility", + "parameterTypes": [ + ] + } + ] + }, + { + "name": "dev.usbharu.hideout.domain.model.ap.Accept", + "queryAllDeclaredMethods": true, + "allDeclaredFields": true, + "queryAllDeclaredConstructors": true, + "methods": [ + { + "name": "", + "parameterTypes": [ + ] + }, + { + "name": "", + "parameterTypes": [ + "dev.usbharu.hideout.domain.model.ap.Object" + ] + }, + { + "name": "getActor", + "parameterTypes": [ + ] + }, + { + "name": "getObject", + "parameterTypes": [ + ] + }, + { + "name": "setActor", + "parameterTypes": [ + ] + }, + { + "name": "setObject", + "parameterTypes": [ + "dev.usbharu.hideout.domain.model.ap.Object" + ] + } + ] + }, + { + "name": "dev.usbharu.hideout.domain.model.ap.ContextDeserializer", + "methods": [ + { + "name": "", + "parameterTypes": [ + ] + } + ] + }, + { + "name": "dev.usbharu.hideout.domain.model.ap.ContextSerializer", + "methods": [ + { + "name": "", + "parameterTypes": [ + ] + } + ] + }, + { + "name": "dev.usbharu.hideout.domain.model.ap.Create", + "queryAllDeclaredMethods": true, + "allDeclaredFields": true, + "queryAllDeclaredConstructors": true, + "methods": [ + { + "name": "", + "parameterTypes": [ + ] + }, + { + "name": "", + "parameterTypes": [ + "dev.usbharu.hideout.domain.model.ap.Object" + ] + }, + { + "name": "getObject", + "parameterTypes": [ + ] + } + ] + }, + { + "name": "dev.usbharu.hideout.domain.model.ap.Follow", + "queryAllDeclaredMethods": true, + "allDeclaredFields": true, + "queryAllDeclaredConstructors": true, + "methods": [ + { + "name": "", + "parameterTypes": [ + ] + }, + { + "name": "", + "parameterTypes": [ + ] + }, + { + "name": "getActor", + "parameterTypes": [ + ] + }, + { + "name": "getObject", + "parameterTypes": [ + ] + }, + { + "name": "setActor", + "parameterTypes": [ + ] + }, + { + "name": "setObject", + "parameterTypes": [ + ] + } + ] + }, + { + "name": "dev.usbharu.hideout.domain.model.ap.Image", + "queryAllDeclaredMethods": true, + "allDeclaredFields": true, + "queryAllDeclaredConstructors": true, + "methods": [ + { + "name": "", + "parameterTypes": [ + ] + }, + { + "name": "", + "parameterTypes": [ + ] + } + ] + }, + { + "name": "dev.usbharu.hideout.domain.model.ap.JsonLd", + "queryAllDeclaredMethods": true, + "allDeclaredFields": true, + "queryAllDeclaredConstructors": true, + "methods": [ + { + "name": "", + "parameterTypes": [ + ] + }, + { + "name": "getContext", + "parameterTypes": [ + ] + }, + { + "name": "setContext", + "parameterTypes": [ + ] + } + ] + }, + { + "name": "dev.usbharu.hideout.domain.model.ap.Key", + "queryAllDeclaredMethods": true, + "allDeclaredFields": true, + "queryAllDeclaredConstructors": true, + "methods": [ + { + "name": "", + "parameterTypes": [ + ] + }, + { + "name": "", + "parameterTypes": [ + ] + }, + { + "name": "getId", + "parameterTypes": [ + ] + }, + { + "name": "getOwner", + "parameterTypes": [ + ] + }, + { + "name": "getPublicKeyPem", + "parameterTypes": [ + ] + }, + { + "name": "setId", + "parameterTypes": [ + ] + }, + { + "name": "setOwner", + "parameterTypes": [ + ] + }, + { + "name": "setPublicKeyPem", + "parameterTypes": [ + ] + } + ] + }, + { + "name": "dev.usbharu.hideout.domain.model.ap.Note", + "queryAllDeclaredMethods": true, + "allDeclaredFields": true, + "queryAllDeclaredConstructors": true, + "methods": [ + { + "name": "", + "parameterTypes": [ + ] + }, + { + "name": "", + "parameterTypes": [ + ] + }, + { + "name": "getAttributedTo", + "parameterTypes": [ + ] + }, + { + "name": "getContent", + "parameterTypes": [ + ] + }, + { + "name": "getId", + "parameterTypes": [ + ] + }, + { + "name": "getPublished", + "parameterTypes": [ + ] + }, + { + "name": "getTo", + "parameterTypes": [ + ] + }, + { + "name": "setAttributedTo", + "parameterTypes": [ + ] + }, + { + "name": "setContent", + "parameterTypes": [ + ] + }, + { + "name": "setId", + "parameterTypes": [ + ] + }, + { + "name": "setPublished", + "parameterTypes": [ + ] + } + ] + }, + { + "name": "dev.usbharu.hideout.domain.model.ap.Object", + "queryAllDeclaredMethods": true, + "allDeclaredFields": true, + "queryAllDeclaredConstructors": true, + "methods": [ + { + "name": "", + "parameterTypes": [ + ] + }, + { + "name": "", + "parameterTypes": [ + ] + }, + { + "name": "getName", + "parameterTypes": [ + ] + }, + { + "name": "setName", + "parameterTypes": [ + ] + } + ] + }, + { + "name": "dev.usbharu.hideout.domain.model.ap.Object$Companion", + "methods": [ + { + "name": "add", + "parameterTypes": [ + ] + } + ] + }, + { + "name": "dev.usbharu.hideout.domain.model.ap.Person", + "queryAllDeclaredMethods": true, + "allDeclaredFields": true, + "queryAllDeclaredConstructors": true, + "methods": [ + { + "name": "", + "parameterTypes": [ + ] + }, + { + "name": "", + "parameterTypes": [ + "dev.usbharu.hideout.domain.model.ap.Image", + "dev.usbharu.hideout.domain.model.ap.Key" + ] + }, + { + "name": "getInbox", + "parameterTypes": [ + ] + }, + { + "name": "getOutbox", + "parameterTypes": [ + ] + }, + { + "name": "getPreferredUsername", + "parameterTypes": [ + ] + }, + { + "name": "getPublicKey", + "parameterTypes": [ + ] + }, + { + "name": "getSummary", + "parameterTypes": [ + ] + }, + { + "name": "setInbox", + "parameterTypes": [ + ] + }, + { + "name": "setOutbox", + "parameterTypes": [ + ] + }, + { + "name": "setPreferredUsername", + "parameterTypes": [ + ] + }, + { + "name": "setPublicKey", + "parameterTypes": [ + "dev.usbharu.hideout.domain.model.ap.Key" + ] + }, + { + "name": "setSummary", + "parameterTypes": [ + ] + } + ] + }, + { + "name": "dev.usbharu.hideout.domain.model.ap.TypeSerializer", + "methods": [ + { + "name": "", + "parameterTypes": [ + ] + } + ] + }, + { + "name": "dev.usbharu.hideout.exception.JsonParseException", + "allDeclaredFields": true, + "queryAllPublicConstructors": true, + "methods": [ + { + "name": "", + "parameterTypes": [ + ] + } + ] + }, + { + "allDeclaredClasses": true, + "queryAllDeclaredConstructors": true, + "methods": [ + { + "name": "create", + "parameterTypes": [ + "dev.usbharu.hideout.domain.model.Post", + "kotlin.coroutines.Continuation" + ] + } + ], + "name": "dev.usbharu.hideout.service.IPostService", + "queryAllDeclaredMethods": true, + "allDeclaredFields": true, + "queryAllPublicMethods": true + }, + { + "allDeclaredClasses": true, + "queryAllDeclaredConstructors": true, + "methods": [ + { + "name": "parseActivity", + "parameterTypes": [ + ] + }, + { + "name": "processActivity", + "parameterTypes": [ + "dev.usbharu.hideout.service.activitypub.ActivityType", + "kotlin.coroutines.Continuation" + ] + }, + { + "name": "processActivity", + "parameterTypes": [ + "kjob.core.dsl.JobContextWithProps", + "dev.usbharu.hideout.domain.model.job.HideoutJob", + "kotlin.coroutines.Continuation" + ] + } + ], + "name": "dev.usbharu.hideout.service.activitypub.ActivityPubService", + "queryAllDeclaredMethods": true, + "allDeclaredFields": true, + "queryAllPublicMethods": true + }, + { + "allDeclaredClasses": true, + "queryAllDeclaredConstructors": true, + "methods": [ + { + "name": "fetchPerson", + "parameterTypes": [ + "kotlin.coroutines.Continuation" + ] + }, + { + "name": "getPersonByName", + "parameterTypes": [ + "kotlin.coroutines.Continuation" + ] + } + ], + "name": "dev.usbharu.hideout.service.activitypub.ActivityPubUserService", + "queryAllDeclaredMethods": true, + "allDeclaredFields": true, + "queryAllPublicMethods": true + }, + { + "name": "dev.usbharu.hideout.service.impl.UserService", + "allDeclaredClasses": true, + "queryAllDeclaredMethods": true, + "allDeclaredFields": true, + "queryAllDeclaredConstructors": true, + "methods": [ + { + "name": "", + "parameterTypes": [ + "dev.usbharu.hideout.repository.IUserRepository" + ] + }, + { + "name": "addFollowers", + "parameterTypes": [ + "long", + "long", + "kotlin.coroutines.Continuation" + ] + }, + { + "name": "findById", + "parameterTypes": [ + "long", + "kotlin.coroutines.Continuation" + ] + }, + { + "name": "findByUrls", + "parameterTypes": [ + "kotlin.coroutines.Continuation" + ] + }, + { + "name": "findFollowersById", + "parameterTypes": [ + "long", + "kotlin.coroutines.Continuation" + ] + } + ] + }, + { + "allDeclaredClasses": true, + "queryAllDeclaredConstructors": true, + "methods": [ + { + "name": "init", + "parameterTypes": [ + ] + }, + { + "name": "schedule", + "parameterTypes": [ + "kjob.core.Job", + "kotlin.jvm.functions.Function2", + "kotlin.coroutines.Continuation" + ] + } + ], + "name": "dev.usbharu.hideout.service.job.JobQueueParentService", + "queryAllDeclaredMethods": true, + "allDeclaredFields": true, + "queryAllPublicMethods": true + }, + { + "allDeclaredClasses": true, + "queryAllDeclaredConstructors": true, + "methods": [ + { + "name": "verify", + "parameterTypes": [ + "io.ktor.http.Headers" + ] + } + ], + "name": "dev.usbharu.hideout.service.signature.HttpSignatureVerifyService", + "queryAllDeclaredMethods": true, + "allDeclaredFields": true, + "queryAllPublicMethods": true + } +] diff --git a/hideout-core/src/main/resources/META-INF/native-image/resource-config.json b/hideout-core/src/main/resources/META-INF/native-image/resource-config.json new file mode 100644 index 00000000..72f8cf9c --- /dev/null +++ b/hideout-core/src/main/resources/META-INF/native-image/resource-config.json @@ -0,0 +1,53 @@ +{ + "resources": { + "includes": [ + { + "pattern": "\\QMETA-INF/services/ch.qos.logback.classic.spi.Configurator\\E" + }, + { + "pattern": "\\QMETA-INF/services/io.ktor.server.config.ConfigLoader\\E" + }, + { + "pattern": "\\QMETA-INF/services/java.sql.Driver\\E" + }, + { + "pattern": "\\QMETA-INF/services/kotlin.reflect.jvm.internal.impl.builtins.BuiltInsLoader\\E" + }, + { + "pattern": "\\QMETA-INF/services/kotlin.reflect.jvm.internal.impl.resolve.ExternalOverridabilityCondition\\E" + }, + { + "pattern": "\\QMETA-INF/services/org.jetbrains.exposed.sql.DatabaseConnectionAutoRegistration\\E" + }, + { + "pattern": "\\QMETA-INF/services/org.slf4j.spi.SLF4JServiceProvider\\E" + }, + { + "pattern": "\\Qapplication.conf\\E" + }, + { + "pattern": "\\Qlogback.xml\\E" + }, + { + "pattern": "\\Qorg/fusesource/jansi/internal/native/Windows/x86_64/jansi.dll\\E" + }, + { + "pattern": "\\Qorg/fusesource/jansi/jansi.properties\\E" + }, + { + "pattern": "\\Qorg/h2/util/data.zip\\E" + }, + { + "pattern": "\\Qstatic/assets/index-c7cbea7a.js\\E" + }, + { + "pattern": "\\Qstatic/index.html\\E" + }, + { + "pattern": "\\Qkotlin/kotlin.kotlin_builtins\\E" + } + ] + }, + "bundles": [ + ] +} diff --git a/hideout-core/src/main/resources/META-INF/native-image/serialization-config.json b/hideout-core/src/main/resources/META-INF/native-image/serialization-config.json new file mode 100644 index 00000000..5e42b093 --- /dev/null +++ b/hideout-core/src/main/resources/META-INF/native-image/serialization-config.json @@ -0,0 +1,8 @@ +{ + "lambdaCapturingTypes": [ + ], + "types": [ + ], + "proxies": [ + ] +} diff --git a/hideout-core/src/main/resources/application.yml b/hideout-core/src/main/resources/application.yml new file mode 100644 index 00000000..29c5fb19 --- /dev/null +++ b/hideout-core/src/main/resources/application.yml @@ -0,0 +1,55 @@ +#file: noinspection SpellCheckingInspection,SpellCheckingInspection,SpellCheckingInspection,SpellCheckingInspection,SpellCheckingInspection,SpellCheckingInspection,SpellCheckingInspection,SpellCheckingInspection,SpellCheckingInspection,SpellCheckingInspection,SpellCheckingInspection,SpellCheckingInspection,SpellCheckingInspection,SpellCheckingInspection,SpellCheckingInspection,SpellCheckingInspection,SpellCheckingInspection,SpellCheckingInspection,SpellCheckingInspection,SpellCheckingInspection,SpellCheckingInspection,SpellCheckingInspection,SpellCheckingInspection,SpellCheckingInspection,SpellCheckingInspection,SpellCheckingInspection,SpellCheckingInspection,SpellCheckingInspection,SpellCheckingInspection,SpellCheckingInspection,SpellCheckingInspection,SpellCheckingInspection,SpellCheckingInspection,SpellCheckingInspection,SpellCheckingInspection,SpellCheckingInspection,SpellCheckingInspection,SpellCheckingInspection,SpellCheckingInspection,SpellCheckingInspection,SpellCheckingInspection,SpellCheckingInspection,SpellCheckingInspection,SpellCheckingInspection,SpellCheckingInspection,SpellCheckingInspection,SpellCheckingInspection,SpellCheckingInspection,SpellCheckingInspection,SpellCheckingInspection,SpellCheckingInspection,SpellCheckingInspection,SpellCheckingInspection,SpellCheckingInspection,SpellCheckingInspection,SpellCheckingInspection,SpellCheckingInspection,SpellCheckingInspection,SpellCheckingInspection,SpellCheckingInspection,SpellCheckingInspection,SpellCheckingInspection,SpellCheckingInspection,SpellCheckingInspection,SpellCheckingInspection,SpellCheckingInspection,SpellCheckingInspection,SpellCheckingInspection,SpellCheckingInspection,SpellCheckingInspection,SpellCheckingInspection,SpellCheckingInspection,SpellCheckingInspection,SpellCheckingInspection,SpellCheckingInspection,SpellCheckingInspection,SpellCheckingInspection,SpellCheckingInspection,SpellCheckingInspection,SpellCheckingInspection,SpellCheckingInspection,SpellCheckingInspection +hideout: + url: "https://test-hideout-dev.usbharu.dev" + use-mongodb: true + owl: + producer: + standalone: embedded + 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" + private: true + + + + + +spring: + jmx: + enabled: false + jackson: + serialization: + WRITE_DATES_AS_TIMESTAMPS: false + default-property-inclusion: always + datasource: + driver-class-name: org.postgresql.Driver + url: "jdbc:postgresql:hideout" + username: "postgres" + password: "password" + data: + mongodb: + auto-index-creation: true + host: localhost + port: 27017 + database: hideout + servlet: + multipart: + max-file-size: 40MB + max-request-size: 40MB + h2: + console: + enabled: true + threads: + virtual: + enabled: true +server: + tomcat: + basedir: tomcat + accesslog: + enabled: true + max-http-form-post-size: 40MB + max-swallow-size: 40MB + port: 8081 \ No newline at end of file diff --git a/hideout-core/src/main/resources/db/migration/V1__Init_DB.sql b/hideout-core/src/main/resources/db/migration/V1__Init_DB.sql new file mode 100644 index 00000000..d6f0c0f4 --- /dev/null +++ b/hideout-core/src/main/resources/db/migration/V1__Init_DB.sql @@ -0,0 +1,273 @@ +create table if not exists emojis +( + id bigint primary key, + "name" varchar(1000) not null, + domain varchar(1000) not null, + instance_id bigint null, + url varchar(255) not null unique, + category varchar(255), + created_at timestamp not null default current_timestamp, + unique ("name", instance_id) +); + +create table if not exists instance +( + id bigint primary key, + "name" varchar(1000) not null, + description varchar(5000) not null, + url varchar(255) not null unique, + icon_url varchar(255) not null, + shared_inbox varchar(255) null unique, + software varchar(255) not null, + version varchar(255) not null, + is_blocked boolean not null, + is_muted boolean not null, + moderation_note varchar(10000) not null, + created_at timestamp not null +); + +alter table emojis + add constraint fk_emojis_instance_id__id foreign key (instance_id) references instance (id) on delete cascade on update cascade; + +create table if not exists actors +( + id bigint primary key, + "name" varchar(300) not null, + "domain" varchar(1000) not null, + screen_name varchar(300) not null, + description varchar(10000) not null, + inbox varchar(1000) not null unique, + outbox varchar(1000) not null unique, + url varchar(1000) not null unique, + public_key varchar(10000) not null, + private_key varchar(10000) null, + created_at timestamp not null, + key_id varchar(1000) not null, + "following" varchar(1000) null, + "followers" varchar(1000) null, + "instance" bigint not null, + locked boolean not null, + following_count int null, + followers_count int null, + posts_count int not null, + last_post_at timestamp null default null, + last_update_at timestamp not null, + suspend boolean not null, + "move_to" bigint null default null, + emojis varchar(3000) not null default '', + deleted boolean not null default false, + "icon" bigint null, + "banner" bigint null, + unique ("name", "domain"), + constraint fk_actors_instance__id foreign key ("instance") references instance (id) on delete restrict on update restrict, + constraint fk_actors_actors__move_to foreign key ("move_to") references actors (id) on delete restrict on update restrict +); + +create table if not exists actor_alsoknownas +( + "actor_id" bigint not null, + "also_known_as" bigint not null, + constraint fk_actor_alsoknownas_actors__actor_id foreign key ("actor_id") references actors (id) on delete cascade on update cascade, + constraint fk_actor_alsoknownas_actors__also_known_as foreign key ("also_known_as") references actors (id) on delete cascade on update cascade +); + +create table if not exists user_details +( + id bigserial primary key, + actor_id bigint not null unique, + password varchar(255) not null, + auto_accept_followee_follow_request boolean not null, + last_migration timestamp null default null, + constraint fk_user_details_actor_id__id foreign key (actor_id) references actors (id) on delete restrict on update restrict +); + +create table if not exists media +( + id bigint primary key, + "name" varchar(255) not null, + url varchar(255) not null unique, + remote_url varchar(255) null unique, + thumbnail_url varchar(255) null unique, + "type" varchar(100) not null, + blurhash varchar(255) null, + mime_type varchar(255) not null, + description varchar(4000) null +); + +alter table actors + add constraint fk_actors_media__icon foreign key ("icon") references media (id) on delete cascade on update cascade; +alter table actors + add constraint fk_actors_media__banner foreign key ("banner") references media (id) on delete cascade on update cascade; + + +create table if not exists posts +( + id bigint primary key, + actor_id bigint not null, + overview varchar(100) null, + content varchar(5000) not null, + text varchar(3000) not null, + created_at timestamp not null, + visibility varchar(100) not null, + url varchar(500) not null, + repost_id bigint null, + reply_id bigint null, + "sensitive" boolean default false not null, + ap_id varchar(100) not null unique, + deleted boolean default false not null, + hide boolean default false not null, + move_to bigint default null null +); +alter table posts + add constraint fk_posts_actor_id__id foreign key (actor_id) references actors (id) on delete restrict on update restrict; +alter table posts + add constraint fk_posts_repostid__id foreign key (repost_id) references posts (id) on delete restrict on update restrict; +alter table posts + add constraint fk_posts_replyid__id foreign key (reply_id) references posts (id) on delete restrict on update restrict; +alter table posts + add constraint fk_posts_move_to__id foreign key (move_to) references posts (id) on delete CASCADE on update cascade; + +create table if not exists posts_media +( + post_id bigint, + media_id bigint, + constraint pk_postsmedia primary key (post_id, media_id) +); +alter table posts_media + add constraint fk_posts_media_post_id__id foreign key (post_id) references posts (id) on delete cascade on update cascade; +alter table posts_media + add constraint fk_posts_media_media_id__id foreign key (media_id) references media (id) on delete cascade on update cascade; + +create table if not exists posts_emojis +( + post_id bigint not null, + emoji_id bigint not null, + constraint pk_postsemoji primary key (post_id, emoji_id) +); + +alter table posts_emojis + add constraint fk_posts_emojis_post_id__id foreign key (post_id) references posts (id) on delete cascade on update cascade; +alter table posts_emojis + add constraint fk_posts_emojis_emoji_id__id foreign key (emoji_id) references emojis (id) on delete cascade on update cascade; + + +create table if not exists posts_visible_actors +( + post_id bigint not null, + actor_id bigint not null, + constraint pk_postsvisibleactors primary key (post_id, actor_id) +); + +alter table posts_visible_actors + add constraint fk_posts_visible_actors_post_id__id foreign key (post_id) references posts (id) on delete cascade on update cascade; +alter table posts_visible_actors + add constraint fk_posts_visible_actors_actor_id__id foreign key (actor_id) references actors (id) on delete cascade on update cascade; + + +create table if not exists relationships +( + id bigserial primary key, + actor_id bigint not null, + target_actor_id bigint not null, + following boolean not null, + blocking boolean not null, + muting boolean not null, + follow_request boolean not null, + ignore_follow_request boolean not null, + constraint fk_relationships_actor_id__id foreign key (actor_id) references actors (id) on delete restrict on update restrict, + constraint fk_relationships_target_actor_id__id foreign key (target_actor_id) references actors (id) on delete restrict on update restrict, + unique (actor_id, target_actor_id) +); + +insert into instance (id, "name", description, url, icon_url, shared_inbox, software, version, is_blocked, is_muted, + moderation_note, created_at) +values (0, 'system', '', '', '', null, '', '', false, false, '', current_timestamp); + +insert into actors (id, "name", "domain", screen_name, description, inbox, outbox, url, public_key, private_key, + created_at, + key_id, "following", "followers", "instance", locked, following_count, followers_count, posts_count, + last_post_at, last_update_at, suspend, "move_to", emojis) +values (0, '', '', '', '', '', '', '', '', null, current_timestamp, '', null, null, 0, true, null, null, 0, null, + current_timestamp, false, null, ''); + +create table if not exists applications +( + id bigint primary key, + name varchar(500) not null +); + +create table if not exists oauth2_registered_client +( + id varchar(100) NOT NULL, + client_id varchar(100) NOT NULL, + client_id_issued_at timestamp DEFAULT CURRENT_TIMESTAMP NOT NULL, + client_secret varchar(200) DEFAULT NULL, + client_secret_expires_at timestamp DEFAULT NULL, + client_name varchar(200) NOT NULL, + client_authentication_methods varchar(1000) NOT NULL, + authorization_grant_types varchar(1000) NOT NULL, + redirect_uris varchar(1000) DEFAULT NULL, + post_logout_redirect_uris varchar(1000) DEFAULT NULL, + scopes varchar(1000) NOT NULL, + client_settings varchar(2000) NOT NULL, + token_settings varchar(2000) NOT NULL, + PRIMARY KEY (id) +); + +CREATE TABLE if not exists oauth2_authorization_consent +( + registered_client_id varchar(100) NOT NULL, + principal_name varchar(200) NOT NULL, + authorities varchar(1000) NOT NULL, + PRIMARY KEY (registered_client_id, principal_name) +); + +CREATE TABLE oauth2_authorization +( + id varchar(100) NOT NULL, + registered_client_id varchar(100) NOT NULL, + principal_name varchar(200) NOT NULL, + authorization_grant_type varchar(100) NOT NULL, + authorized_scopes varchar(1000) DEFAULT NULL, + attributes varchar(4000) DEFAULT NULL, + state varchar(500) DEFAULT NULL, + authorization_code_value varchar(4000) DEFAULT NULL, + authorization_code_issued_at timestamp DEFAULT NULL, + authorization_code_expires_at timestamp DEFAULT NULL, + authorization_code_metadata varchar(4000) DEFAULT NULL, + access_token_value varchar(4000) DEFAULT NULL, + access_token_issued_at timestamp DEFAULT NULL, + access_token_expires_at timestamp DEFAULT NULL, + access_token_metadata varchar(4000) DEFAULT NULL, + access_token_type varchar(100) DEFAULT NULL, + access_token_scopes varchar(1000) DEFAULT NULL, + oidc_id_token_value varchar(4000) DEFAULT NULL, + oidc_id_token_issued_at timestamp DEFAULT NULL, + oidc_id_token_expires_at timestamp DEFAULT NULL, + oidc_id_token_metadata varchar(4000) DEFAULT NULL, + refresh_token_value varchar(4000) DEFAULT NULL, + refresh_token_issued_at timestamp DEFAULT NULL, + refresh_token_expires_at timestamp DEFAULT NULL, + refresh_token_metadata varchar(4000) DEFAULT NULL, + user_code_value varchar(4000) DEFAULT NULL, + user_code_issued_at timestamp DEFAULT NULL, + user_code_expires_at timestamp DEFAULT NULL, + user_code_metadata varchar(4000) DEFAULT NULL, + device_code_value varchar(4000) DEFAULT NULL, + device_code_issued_at timestamp DEFAULT NULL, + device_code_expires_at timestamp DEFAULT NULL, + device_code_metadata varchar(4000) DEFAULT NULL, + PRIMARY KEY (id) +); + +create table if not exists actor_instance_relationships +( + actor_id bigint not null, + instance_id bigint not null, + blocking boolean not null, + muting boolean not null, + do_not_send_private boolean not null, + PRIMARY KEY (actor_id, instance_id), + constraint fk_actor_instance_relationships_actor_id__id foreign key (actor_id) references actors (id) on delete cascade on update cascade, + constraint fk_actor_instance_relationships_instance_id__id foreign key (instance_id) references instance (id) on delete cascade on update cascade +); \ No newline at end of file diff --git a/src/main/resources/icon.png b/hideout-core/src/main/resources/icon.png similarity index 100% rename from src/main/resources/icon.png rename to hideout-core/src/main/resources/icon.png diff --git a/hideout-core/src/main/resources/log4j2.xml b/hideout-core/src/main/resources/log4j2.xml new file mode 100644 index 00000000..4a2ec926 --- /dev/null +++ b/hideout-core/src/main/resources/log4j2.xml @@ -0,0 +1,16 @@ + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/hideout-core/src/main/resources/logback.xml b/hideout-core/src/main/resources/logback.xml new file mode 100644 index 00000000..ea1c0ecd --- /dev/null +++ b/hideout-core/src/main/resources/logback.xml @@ -0,0 +1,35 @@ + + + logFile.log + + UTF-8 + %d{YYYY-MM-dd HH:mm:ss.SSS} [%thread] %-5level [%X{x-request-id}] %logger{36} - %msg%n + + + + logFile.%d{yyyy-MM-dd_HH}.log + + + 30 + + + + + %d{YYYY-MM-dd HH:mm:ss.SSS} [%thread] %-5level [%X{x-request-id},%X{x-job-id}] %logger{36} - + %msg%n + + + + + + + + + + + + + + + + diff --git a/hideout-core/src/main/resources/templates/sign_up.html b/hideout-core/src/main/resources/templates/sign_up.html new file mode 100644 index 00000000..63f57c5a --- /dev/null +++ b/hideout-core/src/main/resources/templates/sign_up.html @@ -0,0 +1,15 @@ + + + + + SignUp + + + +
+ + + +
+ + diff --git a/hideout-core/src/test/kotlin/dev/usbharu/hideout/EqualsAndToStringTest.kt b/hideout-core/src/test/kotlin/dev/usbharu/hideout/EqualsAndToStringTest.kt new file mode 100644 index 00000000..d6325826 --- /dev/null +++ b/hideout-core/src/test/kotlin/dev/usbharu/hideout/EqualsAndToStringTest.kt @@ -0,0 +1,131 @@ +/* + * Copyright (C) 2024 usbharu + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package dev.usbharu.hideout + +import com.fasterxml.jackson.module.kotlin.isKotlinClass +import com.jparams.verifier.tostring.ToStringVerifier +import com.jparams.verifier.tostring.preset.Presets +import nl.jqno.equalsverifier.EqualsVerifier +import nl.jqno.equalsverifier.Warning +import nl.jqno.equalsverifier.internal.reflection.PackageScanner +import org.junit.jupiter.api.DynamicTest +import org.junit.jupiter.api.DynamicTest.dynamicTest +import org.junit.jupiter.api.TestFactory +import org.springframework.context.annotation.Configuration +import org.springframework.stereotype.Component +import org.springframework.stereotype.Controller +import org.springframework.stereotype.Repository +import org.springframework.stereotype.Service +import org.springframework.web.bind.annotation.RestController +import java.lang.reflect.Modifier + +class EqualsAndToStringTest { + @TestFactory + fun equalsTest(): List { + + val classes = PackageScanner.getClassesIn("dev.usbharu.hideout", null, true) + + return classes + .asSequence() + .filter { + it.getAnnotation(Service::class.java) == null + } + .filter { + it.getAnnotation(Repository::class.java) == null + } + .filter { + it.getAnnotation(Component::class.java) == null + } + .filter { + it.getAnnotation(Controller::class.java) == null + } + .filter { + it.getAnnotation(RestController::class.java) == null + } + .filter { + it.getAnnotation(Configuration::class.java) == null + } + .filterNot { + it.packageName.startsWith("dev.usbharu.hideout.domain.mastodon.model.generated") + } + .filterNot { + Throwable::class.java.isAssignableFrom(it) + } + .filterNot { + Modifier.isAbstract(it.modifiers) + } + .filter { + try { + it.kotlin.objectInstance == null + } catch (_: Exception) { + true + } + + } + .filter { + it.superclass == Any::class.java || it.superclass?.packageName?.startsWith("dev.usbharu") ?: true + } + .map { + dynamicTest(it.name) { + if (it.isKotlinClass()) { + println(" at ${it.name}.toString(${it.simpleName}.kt:1)") + } + try { + EqualsVerifier.simple() + .suppress(Warning.INHERITED_DIRECTLY_FROM_OBJECT, Warning.TRANSIENT_FIELDS) + .forClass(it) + .verify() + } catch (e: AssertionError) { + e.printStackTrace() + } + } + } + .toList() + } + + @TestFactory + fun toStringTest(): List { + + return PackageScanner.getClassesIn("dev.usbharu.hideout", null, true) + .filter { + it != null && !it.isEnum && !it.isInterface && !Modifier.isAbstract(it.modifiers) + } + .filter { + val clazz = it.getMethod(it::toString.name).declaringClass + clazz != Any::class.java && clazz != Throwable::class.java + } + .filter { + it.superclass == Any::class.java || it.superclass?.packageName?.startsWith("dev.usbharu") ?: true + } + .filterNot { + it.superclass.isSealed + } + .map { + + dynamicTest(it.name) { + if (it.isKotlinClass()) { + println(" at ${it.name}.toString(${it.simpleName}.kt:1)") + } + try { + ToStringVerifier.forClass(it).withPreset(Presets.INTELLI_J).verify() + } catch (e: Throwable) { + e.printStackTrace() + } + } + } + } +} diff --git a/hideout-core/src/test/kotlin/dev/usbharu/hideout/core/application/actor/GetUserDetailApplicationServiceTest.kt b/hideout-core/src/test/kotlin/dev/usbharu/hideout/core/application/actor/GetUserDetailApplicationServiceTest.kt new file mode 100644 index 00000000..494a381c --- /dev/null +++ b/hideout-core/src/test/kotlin/dev/usbharu/hideout/core/application/actor/GetUserDetailApplicationServiceTest.kt @@ -0,0 +1,78 @@ +package dev.usbharu.hideout.core.application.actor + +import dev.usbharu.hideout.core.application.exception.InternalServerException +import dev.usbharu.hideout.core.domain.model.actor.ActorId +import dev.usbharu.hideout.core.domain.model.actor.ActorRepository +import dev.usbharu.hideout.core.domain.model.actor.TestActorFactory +import dev.usbharu.hideout.core.domain.model.emoji.CustomEmojiRepository +import dev.usbharu.hideout.core.domain.model.support.principal.Anonymous +import dev.usbharu.hideout.core.domain.model.userdetails.UserDetail +import dev.usbharu.hideout.core.domain.model.userdetails.UserDetailHashedPassword +import dev.usbharu.hideout.core.domain.model.userdetails.UserDetailId +import dev.usbharu.hideout.core.domain.model.userdetails.UserDetailRepository +import kotlinx.coroutines.test.runTest +import org.junit.jupiter.api.Test +import org.junit.jupiter.api.assertThrows +import org.junit.jupiter.api.extension.ExtendWith +import org.mockito.InjectMocks +import org.mockito.Mock +import org.mockito.Spy +import org.mockito.junit.jupiter.MockitoExtension +import org.mockito.kotlin.any +import org.mockito.kotlin.doReturn +import org.mockito.kotlin.whenever +import utils.TestTransaction + +@ExtendWith(MockitoExtension::class) +class GetUserDetailApplicationServiceTest { + @InjectMocks + lateinit var service: GetUserDetailApplicationService + + @Mock + lateinit var actorRepository: ActorRepository + + @Mock + lateinit var userDetailRepository: UserDetailRepository + + @Mock + lateinit var customEmojiRepository: CustomEmojiRepository + + @Spy + val transaction = TestTransaction + + @Test + fun userDetailを取得できる() = runTest { + whenever(userDetailRepository.findById(UserDetailId(1))).doReturn( + UserDetail.create( + UserDetailId(1), ActorId(1), + UserDetailHashedPassword("") + ) + ) + whenever(actorRepository.findById(ActorId(1))).doReturn(TestActorFactory.create(1)) + whenever(customEmojiRepository.findByIds(any())).doReturn(listOf()) + + service.execute(GetUserDetail(1), Anonymous) + } + + @Test + fun userDetailが存在しない場合失敗() = runTest { + + assertThrows { + service.execute(GetUserDetail(2), Anonymous) + } + } + + @Test + fun userDetailが存在するけどActorが存在しない場合はInternalServerException() = runTest { + whenever(userDetailRepository.findById(UserDetailId(2))).doReturn( + UserDetail.create( + UserDetailId(2), ActorId(2), + UserDetailHashedPassword("") + ) + ) + + assertThrows { + service.execute(GetUserDetail(2), Anonymous) + } + } +} \ No newline at end of file diff --git a/hideout-core/src/test/kotlin/dev/usbharu/hideout/core/application/actor/MigrationLocalActorApplicationServiceTest.kt b/hideout-core/src/test/kotlin/dev/usbharu/hideout/core/application/actor/MigrationLocalActorApplicationServiceTest.kt new file mode 100644 index 00000000..3148980d --- /dev/null +++ b/hideout-core/src/test/kotlin/dev/usbharu/hideout/core/application/actor/MigrationLocalActorApplicationServiceTest.kt @@ -0,0 +1,171 @@ +package dev.usbharu.hideout.core.application.actor + +import dev.usbharu.hideout.core.application.exception.InternalServerException +import dev.usbharu.hideout.core.application.exception.PermissionDeniedException +import dev.usbharu.hideout.core.domain.model.actor.Actor +import dev.usbharu.hideout.core.domain.model.actor.ActorId +import dev.usbharu.hideout.core.domain.model.actor.ActorRepository +import dev.usbharu.hideout.core.domain.model.actor.TestActorFactory +import dev.usbharu.hideout.core.domain.model.support.acct.Acct +import dev.usbharu.hideout.core.domain.model.support.principal.FromApi +import dev.usbharu.hideout.core.domain.model.userdetails.UserDetail +import dev.usbharu.hideout.core.domain.model.userdetails.UserDetailHashedPassword +import dev.usbharu.hideout.core.domain.model.userdetails.UserDetailId +import dev.usbharu.hideout.core.domain.model.userdetails.UserDetailRepository +import dev.usbharu.hideout.core.domain.service.actor.local.AccountMigrationCheck +import dev.usbharu.hideout.core.domain.service.actor.local.LocalActorMigrationCheckDomainService +import kotlinx.coroutines.test.runTest +import org.junit.jupiter.api.Assertions.assertEquals +import org.junit.jupiter.api.Test +import org.junit.jupiter.api.assertThrows +import org.junit.jupiter.api.extension.ExtendWith +import org.mockito.InjectMocks +import org.mockito.Mock +import org.mockito.Spy +import org.mockito.junit.jupiter.MockitoExtension +import org.mockito.kotlin.* +import utils.TestTransaction + +@ExtendWith(MockitoExtension::class) +class MigrationLocalActorApplicationServiceTest { + @InjectMocks + lateinit var service: MigrationLocalActorApplicationService + + @Mock + lateinit var actorRepository: ActorRepository + + @Mock + lateinit var localActorMigrationCheckDomainService: LocalActorMigrationCheckDomainService + + @Mock + lateinit var userDetailRepository: UserDetailRepository + + @Spy + val transaction = TestTransaction + + @Test + fun pricinpalのactorとfromのactorが違うと失敗() = runTest { + assertThrows { + service.execute( + MigrationLocalActor(1, 2), + FromApi(ActorId(3), UserDetailId(3), Acct("test", "example.com")) + ) + } + } + + @Test + fun fromのactorが見つからなかったら失敗() = runTest { + val userDetail = UserDetail.create( + UserDetailId(1), + ActorId(1), UserDetailHashedPassword + ("") + ) + whenever(userDetailRepository.findById(UserDetailId(1))).doReturn(userDetail) + assertThrows { + service.execute( + MigrationLocalActor(1, 2), + FromApi(ActorId(1), UserDetailId(1), Acct("test", "example.com")) + ) + } + } + + @Test + fun toのactorが見つからなかったら失敗() = runTest { + val userDetail = UserDetail.create( + UserDetailId(1), + ActorId(1), UserDetailHashedPassword + ("") + ) + whenever(actorRepository.findById(ActorId(1))).doReturn(TestActorFactory.create(1)) + whenever(userDetailRepository.findById(UserDetailId(1))).doReturn(userDetail) + assertThrows { + service.execute( + MigrationLocalActor(1, 2), + FromApi(ActorId(1), UserDetailId(1), Acct("test", "example.com")) + ) + } + } + + @Test + fun userDetailが見つからなかったら失敗() = runTest { + assertThrows { + service.execute( + MigrationLocalActor(1, 2), + FromApi(ActorId(1), UserDetailId(1), Acct("test", "example.com")) + ) + } + } + + @Test + fun canMigrationがtrueならmoveToを書き込む() = runTest { + val from = TestActorFactory.create(1) + val to = TestActorFactory.create(2) + val userDetail = UserDetail.create( + UserDetailId(1), + ActorId(1), UserDetailHashedPassword + ("") + ) + whenever(actorRepository.findById(ActorId(1))).doReturn(from) + whenever(actorRepository.findById(ActorId(2))).doReturn(to) + whenever(userDetailRepository.findById(UserDetailId(1))).doReturn(userDetail) + + whenever( + localActorMigrationCheckDomainService.canAccountMigration( + userDetail, + from, + to + ) + ).doReturn(AccountMigrationCheck.CanAccountMigration()) + + service.execute( + MigrationLocalActor(1, 2), + FromApi(ActorId(1), UserDetailId(1), Acct("test", "example.com")) + ) + + argumentCaptor { + verify(actorRepository, times(1)).save(capture()) + val first = allValues.first() + + assertEquals(first.moveTo, to.id) + } + } + + @Test + fun canMigrationがfalseなら例外() = runTest { + val from = TestActorFactory.create(1) + val to = TestActorFactory.create(2) + val userDetail = UserDetail.create( + UserDetailId(1), + ActorId(1), UserDetailHashedPassword + ("") + ) + + whenever(actorRepository.findById(ActorId(1))).doReturn(from) + whenever(actorRepository.findById(ActorId(2))).doReturn(to) + whenever(userDetailRepository.findById(UserDetailId(1))).doReturn(userDetail) + whenever( + localActorMigrationCheckDomainService.canAccountMigration( + userDetail, + from, + to + ) + ).doReturn( + AccountMigrationCheck.AlreadyMoved("Message"), + AccountMigrationCheck.CircularReferences("Message"), + AccountMigrationCheck.SelfReferences(), + AccountMigrationCheck.AlsoKnownAsNotFound("Message"), + AccountMigrationCheck.MigrationCoolDown("Message") + ) + + repeat(5) { + assertThrows { + service.execute( + MigrationLocalActor(1, 2), + FromApi(ActorId(1), UserDetailId(1), Acct("test", "example.com")) + ) + } + } + + verify(actorRepository, never()).save(any()) + } +} \ No newline at end of file diff --git a/hideout-core/src/test/kotlin/dev/usbharu/hideout/core/application/actor/RegisterLocalActorApplicationServiceTest.kt b/hideout-core/src/test/kotlin/dev/usbharu/hideout/core/application/actor/RegisterLocalActorApplicationServiceTest.kt new file mode 100644 index 00000000..e0087535 --- /dev/null +++ b/hideout-core/src/test/kotlin/dev/usbharu/hideout/core/application/actor/RegisterLocalActorApplicationServiceTest.kt @@ -0,0 +1,78 @@ +package dev.usbharu.hideout.core.application.actor + +import dev.usbharu.hideout.core.application.exception.InternalServerException +import dev.usbharu.hideout.core.config.ApplicationConfig +import dev.usbharu.hideout.core.domain.model.actor.ActorRepository +import dev.usbharu.hideout.core.domain.model.instance.InstanceRepository +import dev.usbharu.hideout.core.domain.model.support.principal.Anonymous +import dev.usbharu.hideout.core.domain.model.userdetails.UserDetailRepository +import dev.usbharu.hideout.core.domain.service.actor.local.LocalActorDomainService +import dev.usbharu.hideout.core.domain.service.userdetail.UserDetailDomainService +import dev.usbharu.hideout.core.infrastructure.factory.ActorFactoryImpl +import dev.usbharu.hideout.core.infrastructure.other.TwitterSnowflakeIdGenerateService +import kotlinx.coroutines.test.runTest +import org.junit.jupiter.api.Test +import org.junit.jupiter.api.assertThrows +import org.junit.jupiter.api.extension.ExtendWith +import org.mockito.InjectMocks +import org.mockito.Mock +import org.mockito.Spy +import org.mockito.junit.jupiter.MockitoExtension +import org.mockito.kotlin.doReturn +import org.mockito.kotlin.eq +import org.mockito.kotlin.whenever +import utils.TestTransaction +import java.net.URL + +@ExtendWith(MockitoExtension::class) +class RegisterLocalActorApplicationServiceTest { + @InjectMocks + lateinit var service: RegisterLocalActorApplicationService + + @Mock + lateinit var actorDomainService: LocalActorDomainService + + @Mock + lateinit var actorRepository: ActorRepository + + @Mock + lateinit var actorFactoryImpl: ActorFactoryImpl + + @Mock + lateinit var instanceRepository: InstanceRepository + + @Mock + lateinit var userDetailDomainService: UserDetailDomainService + + @Mock + lateinit var userDetailRepository: UserDetailRepository + + @Spy + val transaction = TestTransaction + + @Spy + val applicationConfig = ApplicationConfig(URL("http://example.com")) + + @Spy + val idGenerateService = TwitterSnowflakeIdGenerateService + + @Test + fun usernameがすでに使われていた場合失敗() = runTest { + whenever(actorDomainService.usernameAlreadyUse(eq("test"))).doReturn(true) + + assertThrows { + service.execute(RegisterLocalActor("test", "password"), Anonymous) + } + } + + @Test + fun ローカルインスタンスが見つからない場合失敗() = runTest { + whenever(actorDomainService.usernameAlreadyUse(eq("test"))).doReturn(false) + + assertThrows { + service.execute(RegisterLocalActor("test", "password"), Anonymous) + } + } + + +} \ No newline at end of file diff --git a/hideout-core/src/test/kotlin/dev/usbharu/hideout/core/application/actor/StartDeleteLocalActorApplicationServiceTest.kt b/hideout-core/src/test/kotlin/dev/usbharu/hideout/core/application/actor/StartDeleteLocalActorApplicationServiceTest.kt new file mode 100644 index 00000000..f99645bf --- /dev/null +++ b/hideout-core/src/test/kotlin/dev/usbharu/hideout/core/application/actor/StartDeleteLocalActorApplicationServiceTest.kt @@ -0,0 +1,64 @@ +package dev.usbharu.hideout.core.application.actor + +import dev.usbharu.hideout.core.application.exception.InternalServerException +import dev.usbharu.hideout.core.application.exception.PermissionDeniedException +import dev.usbharu.hideout.core.domain.model.actor.ActorId +import dev.usbharu.hideout.core.domain.model.actor.ActorRepository +import dev.usbharu.hideout.core.domain.model.actor.TestActorFactory +import dev.usbharu.hideout.core.domain.model.support.acct.Acct +import dev.usbharu.hideout.core.domain.model.support.principal.FromApi +import dev.usbharu.hideout.core.domain.model.userdetails.UserDetailId +import kotlinx.coroutines.test.runTest +import org.junit.jupiter.api.Test +import org.junit.jupiter.api.assertThrows +import org.junit.jupiter.api.extension.ExtendWith +import org.mockito.InjectMocks +import org.mockito.Mock +import org.mockito.Spy +import org.mockito.junit.jupiter.MockitoExtension +import org.mockito.kotlin.doReturn +import org.mockito.kotlin.whenever +import utils.TestTransaction + +@ExtendWith(MockitoExtension::class) +class StartDeleteLocalActorApplicationServiceTest { + + @InjectMocks + lateinit var service: StartDeleteLocalActorApplicationService + + @Mock + lateinit var actorRepository: ActorRepository + + @Spy + val transaction = TestTransaction + + @Test + fun ローカルActorを削除できる() = runTest { + whenever(actorRepository.findById(ActorId(1))).doReturn(TestActorFactory.create(1)) + + service.execute( + DeleteLocalActor(ActorId(1)), + FromApi(ActorId(1), UserDetailId((1)), Acct("test", "example.com")) + ) + } + + @Test + fun ログイン中のユーザーと一致しない場合失敗() = runTest { + assertThrows { + service.execute( + DeleteLocalActor(ActorId(2)), + FromApi(ActorId(1), UserDetailId((1)), Acct("test", "example.com")) + ) + } + } + + @Test + fun ユーザーが存在しない場合失敗() = runTest { + assertThrows { + service.execute( + DeleteLocalActor(ActorId(1)), + FromApi(ActorId(1), UserDetailId((1)), Acct("test", "example.com")) + ) + } + } +} \ No newline at end of file diff --git a/hideout-core/src/test/kotlin/dev/usbharu/hideout/core/application/application/RegisterApplicationApplicationServiceTest.kt b/hideout-core/src/test/kotlin/dev/usbharu/hideout/core/application/application/RegisterApplicationApplicationServiceTest.kt new file mode 100644 index 00000000..e1f73b83 --- /dev/null +++ b/hideout-core/src/test/kotlin/dev/usbharu/hideout/core/application/application/RegisterApplicationApplicationServiceTest.kt @@ -0,0 +1,84 @@ +package dev.usbharu.hideout.core.application.application + +import dev.usbharu.hideout.core.domain.model.application.ApplicationRepository +import dev.usbharu.hideout.core.domain.model.support.principal.Anonymous +import dev.usbharu.hideout.core.infrastructure.other.TwitterSnowflakeIdGenerateService +import dev.usbharu.hideout.core.infrastructure.springframework.SpringSecurityPasswordEncoder +import dev.usbharu.hideout.core.infrastructure.springframework.oauth2.SecureTokenGenerator +import kotlinx.coroutines.test.runTest +import org.assertj.core.api.Assertions.assertThat +import org.junit.jupiter.api.Test +import org.junit.jupiter.api.extension.ExtendWith +import org.mockito.InjectMocks +import org.mockito.Mock +import org.mockito.Spy +import org.mockito.junit.jupiter.MockitoExtension +import org.mockito.kotlin.argumentCaptor +import org.mockito.kotlin.doReturn +import org.mockito.kotlin.verify +import org.mockito.kotlin.whenever +import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder +import org.springframework.security.oauth2.core.AuthorizationGrantType +import org.springframework.security.oauth2.server.authorization.client.RegisteredClient +import org.springframework.security.oauth2.server.authorization.client.RegisteredClientRepository +import utils.TestTransaction +import java.net.URI +import java.time.Duration + +@ExtendWith(MockitoExtension::class) +class RegisterApplicationApplicationServiceTest { + @InjectMocks + lateinit var service: RegisterApplicationApplicationService + + @Mock + lateinit var secureTokenGenerator: SecureTokenGenerator + + @Mock + lateinit var registeredClientRepository: RegisteredClientRepository + + @Mock + lateinit var applicationRepository: ApplicationRepository + + @Spy + val idGenerateService = TwitterSnowflakeIdGenerateService + + @Spy + val passwordEncoder = SpringSecurityPasswordEncoder(BCryptPasswordEncoder()) + + @Spy + val transaction = TestTransaction + + @Test + fun applicationを作成できる() = runTest { + whenever(secureTokenGenerator.generate()).doReturn("very-secure-token") + + service.execute( + RegisterApplication("test", setOf(URI.create("https://example.com")), false, setOf("write")), + Anonymous + ) + + argumentCaptor { + verify(registeredClientRepository).save(capture()) + val first = allValues.first() + assertThat(first.tokenSettings.accessTokenTimeToLive).isGreaterThanOrEqualTo(Duration.ofSeconds(31536000000)) + + } + } + + @Test + fun refreshTokenを有効化してapplicationを作成できる() = runTest { + whenever(secureTokenGenerator.generate()).doReturn("very-secure-token") + + service.execute( + RegisterApplication("test", setOf(URI.create("https://example.com")), true, setOf("write")), + Anonymous + ) + + argumentCaptor { + verify(registeredClientRepository).save(capture()) + val first = allValues.first() + assertThat(first.authorizationGrantTypes).contains(AuthorizationGrantType.REFRESH_TOKEN) + + } + } +} \ No newline at end of file diff --git a/hideout-core/src/test/kotlin/dev/usbharu/hideout/core/application/filter/UserDeleteFilterApplicationServiceTest.kt b/hideout-core/src/test/kotlin/dev/usbharu/hideout/core/application/filter/UserDeleteFilterApplicationServiceTest.kt new file mode 100644 index 00000000..3928ea45 --- /dev/null +++ b/hideout-core/src/test/kotlin/dev/usbharu/hideout/core/application/filter/UserDeleteFilterApplicationServiceTest.kt @@ -0,0 +1,89 @@ +package dev.usbharu.hideout.core.application.filter + +import dev.usbharu.hideout.core.application.exception.PermissionDeniedException +import dev.usbharu.hideout.core.domain.model.actor.ActorId +import dev.usbharu.hideout.core.domain.model.filter.* +import dev.usbharu.hideout.core.domain.model.filter.Filter +import dev.usbharu.hideout.core.domain.model.filter.FilterKeyword +import dev.usbharu.hideout.core.domain.model.support.acct.Acct +import dev.usbharu.hideout.core.domain.model.support.principal.FromApi +import dev.usbharu.hideout.core.domain.model.userdetails.UserDetailId +import kotlinx.coroutines.test.runTest +import org.junit.jupiter.api.Test +import org.junit.jupiter.api.assertThrows +import org.junit.jupiter.api.extension.ExtendWith +import org.mockito.InjectMocks +import org.mockito.Mock +import org.mockito.Spy +import org.mockito.junit.jupiter.MockitoExtension +import org.mockito.kotlin.* +import utils.TestTransaction + +@ExtendWith(MockitoExtension::class) +class UserDeleteFilterApplicationServiceTest { + + @InjectMocks + lateinit var service: UserDeleteFilterApplicationService + + @Spy + val transaction = TestTransaction + + @Mock + lateinit var filterRepository: FilterRepository + + @Test + fun フィルターを削除できる() = runTest { + val filter = Filter( + FilterId(1), UserDetailId(1), FilterName("filter"), setOf(FilterContext.HOME), FilterAction.HIDE, setOf( + FilterKeyword( + FilterKeywordId(1), FilterKeywordKeyword("aaa"), FilterMode.NONE + ) + ) + ) + whenever(filterRepository.findByFilterId(FilterId(1))).doReturn(filter) + + service.execute( + DeleteFilter(1), FromApi( + ActorId(1), UserDetailId(1), + Acct("test", "example.com") + ) + ) + + verify(filterRepository, times(1)).delete(eq(filter)) + } + + @Test + fun フィルターが見つからない場合失敗() = runTest { + assertThrows { + service.execute( + DeleteFilter(1), FromApi( + ActorId(1), UserDetailId(1), + Acct("test", "example.com") + ) + ) + } + } + + @Test + fun フィルターのオーナー以外は失敗() = runTest { + val filter = Filter( + FilterId(1), UserDetailId(1), FilterName("filter"), setOf(FilterContext.HOME), FilterAction.HIDE, setOf( + FilterKeyword( + FilterKeywordId(1), FilterKeywordKeyword("aaa"), FilterMode.NONE + ) + ) + ) + whenever(filterRepository.findByFilterId(FilterId(1))).doReturn(filter) + + assertThrows { + service.execute( + DeleteFilter(1), FromApi( + ActorId(3), UserDetailId(3), + Acct("test", "example.com") + ) + ) + } + + verify(filterRepository, never()).delete(any()) + } +} \ No newline at end of file diff --git a/hideout-core/src/test/kotlin/dev/usbharu/hideout/core/application/filter/UserGetFilterApplicationServiceTest.kt b/hideout-core/src/test/kotlin/dev/usbharu/hideout/core/application/filter/UserGetFilterApplicationServiceTest.kt new file mode 100644 index 00000000..b3b2db82 --- /dev/null +++ b/hideout-core/src/test/kotlin/dev/usbharu/hideout/core/application/filter/UserGetFilterApplicationServiceTest.kt @@ -0,0 +1,86 @@ +package dev.usbharu.hideout.core.application.filter + +import dev.usbharu.hideout.core.application.exception.PermissionDeniedException +import dev.usbharu.hideout.core.domain.model.actor.ActorId +import dev.usbharu.hideout.core.domain.model.filter.* +import dev.usbharu.hideout.core.domain.model.filter.Filter +import dev.usbharu.hideout.core.domain.model.filter.FilterKeyword +import dev.usbharu.hideout.core.domain.model.support.acct.Acct +import dev.usbharu.hideout.core.domain.model.support.principal.FromApi +import dev.usbharu.hideout.core.domain.model.userdetails.UserDetailId +import kotlinx.coroutines.test.runTest +import org.junit.jupiter.api.Test +import org.junit.jupiter.api.assertThrows +import org.junit.jupiter.api.extension.ExtendWith +import org.mockito.InjectMocks +import org.mockito.Mock +import org.mockito.Spy +import org.mockito.junit.jupiter.MockitoExtension +import org.mockito.kotlin.doReturn +import org.mockito.kotlin.whenever +import utils.TestTransaction + +@ExtendWith(MockitoExtension::class) +class UserGetFilterApplicationServiceTest { + @InjectMocks + lateinit var service: UserGetFilterApplicationService + + @Mock + lateinit var filterRepository: FilterRepository + + @Spy + val transaction = TestTransaction + + @Test + fun オーナーのみ取得できる() = runTest { + val filter = Filter( + FilterId(1), UserDetailId(1), FilterName("filter"), setOf(FilterContext.HOME), FilterAction.HIDE, setOf( + FilterKeyword( + FilterKeywordId(1), FilterKeywordKeyword("aaa"), FilterMode.NONE + ) + ) + ) + whenever(filterRepository.findByFilterId(FilterId(1))).doReturn(filter) + + service.execute( + GetFilter(1), FromApi( + ActorId(1), UserDetailId(1), + Acct("test", "example.com") + ) + ) + } + + @Test + fun オーナー以外は失敗() = runTest { + val filter = Filter( + FilterId(1), UserDetailId(1), FilterName("filter"), setOf(FilterContext.HOME), FilterAction.HIDE, setOf( + FilterKeyword( + FilterKeywordId(1), FilterKeywordKeyword("aaa"), FilterMode.NONE + ) + ) + ) + whenever(filterRepository.findByFilterId(FilterId(1))).doReturn(filter) + + + assertThrows { + service.execute( + GetFilter(1), FromApi( + ActorId(3), UserDetailId(3), + Acct("test", "example.com") + ) + ) + } + } + + @Test + fun フィルターが見つからない場合失敗() = runTest { + assertThrows { + service.execute( + GetFilter(1), FromApi( + ActorId(3), UserDetailId(3), + Acct("test", "example.com") + ) + ) + } + } +} \ No newline at end of file diff --git a/hideout-core/src/test/kotlin/dev/usbharu/hideout/core/application/post/DeleteLocalPostApplicationServiceTest.kt b/hideout-core/src/test/kotlin/dev/usbharu/hideout/core/application/post/DeleteLocalPostApplicationServiceTest.kt new file mode 100644 index 00000000..b4c747ba --- /dev/null +++ b/hideout-core/src/test/kotlin/dev/usbharu/hideout/core/application/post/DeleteLocalPostApplicationServiceTest.kt @@ -0,0 +1,55 @@ +package dev.usbharu.hideout.core.application.post + +import dev.usbharu.hideout.core.application.exception.PermissionDeniedException +import dev.usbharu.hideout.core.domain.model.actor.ActorId +import dev.usbharu.hideout.core.domain.model.actor.ActorRepository +import dev.usbharu.hideout.core.domain.model.actor.TestActorFactory +import dev.usbharu.hideout.core.domain.model.post.PostId +import dev.usbharu.hideout.core.domain.model.post.PostRepository +import dev.usbharu.hideout.core.domain.model.post.TestPostFactory +import dev.usbharu.hideout.core.domain.model.support.acct.Acct +import dev.usbharu.hideout.core.domain.model.support.principal.FromApi +import dev.usbharu.hideout.core.domain.model.userdetails.UserDetailId +import kotlinx.coroutines.test.runTest +import org.junit.jupiter.api.Test +import org.junit.jupiter.api.assertThrows +import org.junit.jupiter.api.extension.ExtendWith +import org.mockito.InjectMocks +import org.mockito.Mock +import org.mockito.Spy +import org.mockito.junit.jupiter.MockitoExtension +import org.mockito.kotlin.doReturn +import org.mockito.kotlin.whenever +import utils.TestTransaction + +@ExtendWith(MockitoExtension::class) +class DeleteLocalPostApplicationServiceTest { + @InjectMocks + lateinit var service: DeleteLocalPostApplicationService + + @Mock + lateinit var postRepository: PostRepository + + @Mock + lateinit var actorRepository: ActorRepository + + @Spy + val transaction = TestTransaction + + @Test + fun Post主はローカルPostを削除できる() = runTest { + whenever(postRepository.findById(PostId(1))).doReturn(TestPostFactory.create(actorId = 2)) + whenever(actorRepository.findById(ActorId(2))).doReturn(TestActorFactory.create(id = 2)) + + service.execute(DeleteLocalPost(1), FromApi(ActorId(2), UserDetailId(2), Acct("test", "example.com"))) + } + + @Test + fun Post主以外はローカルPostを削除できない() = runTest { + whenever(postRepository.findById(PostId(1))).doReturn(TestPostFactory.create(actorId = 2)) + + assertThrows { + service.execute(DeleteLocalPost(1), FromApi(ActorId(3), UserDetailId(3), Acct("test", "example.com"))) + } + } +} \ No newline at end of file diff --git a/hideout-core/src/test/kotlin/dev/usbharu/hideout/core/application/post/GetPostApplicationServiceTest.kt b/hideout-core/src/test/kotlin/dev/usbharu/hideout/core/application/post/GetPostApplicationServiceTest.kt new file mode 100644 index 00000000..c8cf9561 --- /dev/null +++ b/hideout-core/src/test/kotlin/dev/usbharu/hideout/core/application/post/GetPostApplicationServiceTest.kt @@ -0,0 +1,64 @@ +package dev.usbharu.hideout.core.application.post + +import dev.usbharu.hideout.core.application.exception.PermissionDeniedException +import dev.usbharu.hideout.core.domain.model.post.PostId +import dev.usbharu.hideout.core.domain.model.post.PostRepository +import dev.usbharu.hideout.core.domain.model.post.TestPostFactory +import dev.usbharu.hideout.core.domain.model.support.principal.Anonymous +import dev.usbharu.hideout.core.domain.service.post.IPostReadAccessControl +import kotlinx.coroutines.test.runTest +import org.junit.jupiter.api.Assertions.assertEquals +import org.junit.jupiter.api.Test +import org.junit.jupiter.api.assertThrows +import org.junit.jupiter.api.extension.ExtendWith +import org.mockito.InjectMocks +import org.mockito.Mock +import org.mockito.Spy +import org.mockito.junit.jupiter.MockitoExtension +import org.mockito.kotlin.any +import org.mockito.kotlin.doReturn +import org.mockito.kotlin.whenever +import utils.TestTransaction + +@ExtendWith(MockitoExtension::class) +class GetPostApplicationServiceTest { + @InjectMocks + lateinit var service: GetPostApplicationService + + @Mock + lateinit var postRepository: PostRepository + + @Mock + lateinit var iPostReadAccessControl: IPostReadAccessControl + + @Spy + val transaction = TestTransaction + + @Test + fun postReadAccessControlがtrueを返したらPostが返ってくる() = runTest { + val post = TestPostFactory.create(id = 1) + whenever(postRepository.findById(PostId(1))).doReturn(post) + whenever(iPostReadAccessControl.isAllow(any(), any())).doReturn(true) + + val actual = service.execute(GetPost(1), Anonymous) + assertEquals(Post.of(post), actual) + } + + @Test + fun postが見つからない場合失敗() = runTest { + assertThrows { + service.execute(GetPost(2), Anonymous) + } + } + + @Test + fun postReadAccessControlがfalseを返したら失敗() = runTest { + val post = TestPostFactory.create(id = 1) + whenever(postRepository.findById(PostId(1))).doReturn(post) + whenever(iPostReadAccessControl.isAllow(any(), any())).doReturn(false) + assertThrows { + service.execute(GetPost(1), Anonymous) + } + + } +} \ No newline at end of file diff --git a/hideout-core/src/test/kotlin/dev/usbharu/hideout/core/application/post/RegisterLocalPostApplicationServiceTest.kt b/hideout-core/src/test/kotlin/dev/usbharu/hideout/core/application/post/RegisterLocalPostApplicationServiceTest.kt new file mode 100644 index 00000000..40cc7162 --- /dev/null +++ b/hideout-core/src/test/kotlin/dev/usbharu/hideout/core/application/post/RegisterLocalPostApplicationServiceTest.kt @@ -0,0 +1,80 @@ +package dev.usbharu.hideout.core.application.post + +import dev.usbharu.hideout.core.application.exception.InternalServerException +import dev.usbharu.hideout.core.domain.model.actor.ActorId +import dev.usbharu.hideout.core.domain.model.actor.ActorRepository +import dev.usbharu.hideout.core.domain.model.actor.TestActorFactory +import dev.usbharu.hideout.core.domain.model.post.PostRepository +import dev.usbharu.hideout.core.domain.model.post.TestPostFactory +import dev.usbharu.hideout.core.domain.model.post.Visibility +import dev.usbharu.hideout.core.domain.model.support.acct.Acct +import dev.usbharu.hideout.core.domain.model.support.principal.FromApi +import dev.usbharu.hideout.core.domain.model.userdetails.UserDetailId +import dev.usbharu.hideout.core.infrastructure.factory.PostFactoryImpl +import kotlinx.coroutines.test.runTest +import org.junit.jupiter.api.Test +import org.junit.jupiter.api.assertThrows +import org.junit.jupiter.api.extension.ExtendWith +import org.mockito.InjectMocks +import org.mockito.Mock +import org.mockito.Spy +import org.mockito.junit.jupiter.MockitoExtension +import org.mockito.kotlin.* +import utils.TestTransaction + +@ExtendWith(MockitoExtension::class) +class RegisterLocalPostApplicationServiceTest { + @InjectMocks + lateinit var service: RegisterLocalPostApplicationService + + @Mock + lateinit var actorRepository: ActorRepository + + @Mock + lateinit var postRepository: PostRepository + + @Mock + lateinit var postFactoryImpl: PostFactoryImpl + + @Spy + val transaction = TestTransaction + + @Test + fun postを作成できる() = runTest { + val actor = TestActorFactory.create(id = 1) + val post = TestPostFactory.create() + whenever(actorRepository.findById(ActorId(1))) doReturn actor + whenever( + postFactoryImpl.createLocal( + eq(actor), + anyValueClass(), + isNull(), + eq("content test"), + eq(Visibility.PUBLIC), + isNull(), + isNull(), + eq(false), + eq(emptyList()) + ) + ).doReturn(post) + + service.execute( + RegisterLocalPost("content test", null, Visibility.PUBLIC, null, null, false, emptyList()), FromApi( + ActorId(1), UserDetailId(1), Acct("test", "example.com") + ) + ) + + verify(postRepository, times(1)).save(eq(post)) + } + + @Test + fun actorが見つからないと失敗() = runTest { + assertThrows { + service.execute( + RegisterLocalPost("content test", null, Visibility.PUBLIC, null, null, false, emptyList()), FromApi( + ActorId(1), UserDetailId(1), Acct("test", "example.com") + ) + ) + } + } +} \ No newline at end of file diff --git a/hideout-core/src/test/kotlin/dev/usbharu/hideout/core/application/post/UpdateLocalNoteApplicationServiceTest.kt b/hideout-core/src/test/kotlin/dev/usbharu/hideout/core/application/post/UpdateLocalNoteApplicationServiceTest.kt new file mode 100644 index 00000000..25c8524b --- /dev/null +++ b/hideout-core/src/test/kotlin/dev/usbharu/hideout/core/application/post/UpdateLocalNoteApplicationServiceTest.kt @@ -0,0 +1,152 @@ +package dev.usbharu.hideout.core.application.post + +import dev.usbharu.hideout.core.application.exception.InternalServerException +import dev.usbharu.hideout.core.application.exception.PermissionDeniedException +import dev.usbharu.hideout.core.domain.model.actor.ActorId +import dev.usbharu.hideout.core.domain.model.actor.ActorRepository +import dev.usbharu.hideout.core.domain.model.actor.TestActorFactory +import dev.usbharu.hideout.core.domain.model.post.* +import dev.usbharu.hideout.core.domain.model.post.Post +import dev.usbharu.hideout.core.domain.model.support.acct.Acct +import dev.usbharu.hideout.core.domain.model.support.principal.FromApi +import dev.usbharu.hideout.core.domain.model.userdetails.UserDetail +import dev.usbharu.hideout.core.domain.model.userdetails.UserDetailHashedPassword +import dev.usbharu.hideout.core.domain.model.userdetails.UserDetailId +import dev.usbharu.hideout.core.domain.model.userdetails.UserDetailRepository +import dev.usbharu.hideout.core.infrastructure.factory.PostContentFactoryImpl +import kotlinx.coroutines.test.runTest +import org.junit.jupiter.api.Assertions.assertEquals +import org.junit.jupiter.api.Test +import org.junit.jupiter.api.assertThrows +import org.junit.jupiter.api.extension.ExtendWith +import org.mockito.InjectMocks +import org.mockito.Mock +import org.mockito.Spy +import org.mockito.junit.jupiter.MockitoExtension +import org.mockito.kotlin.* +import utils.TestTransaction + +@ExtendWith(MockitoExtension::class) +class UpdateLocalNoteApplicationServiceTest { + @InjectMocks + lateinit var service: UpdateLocalNoteApplicationService + + @Mock + lateinit var postRepository: PostRepository + + @Mock + lateinit var userDetailRepository: UserDetailRepository + + @Mock + lateinit var actorRepository: ActorRepository + + @Mock + lateinit var postContentFactoryImpl: PostContentFactoryImpl + + @Spy + val transaction = TestTransaction + + + @Test + fun Post主はPostを編集できる() = runTest { + val post = TestPostFactory.create() + + whenever(postRepository.findById(post.id)).doReturn(post) + whenever(userDetailRepository.findById(UserDetailId(1))).doReturn( + UserDetail.create( + UserDetailId(1), post.actorId, + UserDetailHashedPassword("") + ) + ) + whenever(actorRepository.findById(post.actorId)).doReturn(TestActorFactory.create(id = post.actorId.id)) + val content = PostContent("

test

", "test", emptyList()) + whenever(postContentFactoryImpl.create(eq("test"))).doReturn(content) + + service.execute( + UpdateLocalNote(post.id.id, null, "test", false, emptyList()), FromApi( + post.actorId, + UserDetailId(1), + Acct("test", "example.com") + ) + ) + + argumentCaptor { + verify(postRepository, times(1)).save(capture()) + val first = allValues.first() + + assertEquals( + content, first.content + ) + } + } + + @Test + fun postが見つからない場合失敗() = runTest { + assertThrows { + service.execute( + UpdateLocalNote(1, null, "test", false, emptyList()), FromApi( + ActorId(1), + UserDetailId(1), Acct("test", "example.com") + ) + ) + } + } + + @Test + fun post主じゃない場合失敗() = runTest { + whenever(postRepository.findById(PostId(1))).doReturn(TestPostFactory.create(id = 1, actorId = 3)) + + assertThrows { + service.execute( + UpdateLocalNote(1, null, "test", false, emptyList()), FromApi( + ActorId(1), + UserDetailId(1), Acct("test", "example.com") + ) + ) + } + } + + @Test + fun userDetailが見つからない場合失敗() = runTest { + whenever(postRepository.findById(PostId(1))).doReturn(TestPostFactory.create(id = 1, actorId = 1)) + + assertThrows { + service.execute( + UpdateLocalNote(1, null, "test", false, emptyList()), FromApi( + ActorId(1), + UserDetailId(1), Acct("test", "example.com") + ) + ) + } + + verify(userDetailRepository, times(1)).findById(UserDetailId(1)) + verify(actorRepository, never()).findById(any()) + } + + @Test + fun actorが見つからない場合失敗() = runTest { + val post = TestPostFactory.create() + + whenever(postRepository.findById(post.id)).doReturn(post) + whenever(userDetailRepository.findById(UserDetailId(1))).doReturn( + UserDetail.create( + UserDetailId(1), post.actorId, + UserDetailHashedPassword("") + ) + ) + + + assertThrows { + service.execute( + UpdateLocalNote(post.id.id, null, "test", false, emptyList()), FromApi( + post.actorId, + UserDetailId(1), + Acct("test", "example.com") + ) + ) + } + verify(userDetailRepository, times(1)).findById(UserDetailId(1)) + verify(actorRepository, times(1)).findById(ActorId(1)) + verify(postRepository, never()).save(any()) + } +} \ No newline at end of file diff --git a/hideout-core/src/test/kotlin/dev/usbharu/hideout/core/application/relationship/acceptfollowrequest/UserAcceptFollowRequestApplicationServiceTest.kt b/hideout-core/src/test/kotlin/dev/usbharu/hideout/core/application/relationship/acceptfollowrequest/UserAcceptFollowRequestApplicationServiceTest.kt new file mode 100644 index 00000000..8f47c41f --- /dev/null +++ b/hideout-core/src/test/kotlin/dev/usbharu/hideout/core/application/relationship/acceptfollowrequest/UserAcceptFollowRequestApplicationServiceTest.kt @@ -0,0 +1,81 @@ +package dev.usbharu.hideout.core.application.relationship.acceptfollowrequest + +import dev.usbharu.hideout.core.application.exception.InternalServerException +import dev.usbharu.hideout.core.domain.model.actor.ActorId +import dev.usbharu.hideout.core.domain.model.actor.ActorRepository +import dev.usbharu.hideout.core.domain.model.actor.TestActorFactory +import dev.usbharu.hideout.core.domain.model.relationship.Relationship +import dev.usbharu.hideout.core.domain.model.relationship.RelationshipRepository +import dev.usbharu.hideout.core.domain.model.support.acct.Acct +import dev.usbharu.hideout.core.domain.model.support.principal.FromApi +import dev.usbharu.hideout.core.domain.model.userdetails.UserDetailId +import kotlinx.coroutines.test.runTest +import org.junit.jupiter.api.Assertions.assertFalse +import org.junit.jupiter.api.Assertions.assertTrue +import org.junit.jupiter.api.Test +import org.junit.jupiter.api.assertThrows +import org.junit.jupiter.api.extension.ExtendWith +import org.mockito.InjectMocks +import org.mockito.Mock +import org.mockito.Spy +import org.mockito.junit.jupiter.MockitoExtension +import org.mockito.kotlin.argumentCaptor +import org.mockito.kotlin.doReturn +import org.mockito.kotlin.verify +import org.mockito.kotlin.whenever +import utils.TestTransaction + +@ExtendWith(MockitoExtension::class) +class UserAcceptFollowRequestApplicationServiceTest { + @InjectMocks + lateinit var service: UserAcceptFollowRequestApplicationService + + @Mock + lateinit var relationshipRepository: RelationshipRepository + + @Mock + lateinit var actorRepository: ActorRepository + + @Spy + val transaction = TestTransaction + + @Test + fun actorが見つからない場合失敗() = runTest { + assertThrows { + service.execute(AcceptFollowRequest(1), FromApi(ActorId(2), UserDetailId(2), Acct("test", "example.com"))) + } + } + + @Test + fun relationshipが見つからない場合失敗() = runTest { + whenever(actorRepository.findById(ActorId(2))).doReturn(TestActorFactory.create(id = 2)) + + assertThrows { + service.execute(AcceptFollowRequest(1), FromApi(ActorId(2), UserDetailId(2), Acct("test", "example.com"))) + } + } + + @Test + fun フォローリクエストを承認できる() = runTest { + whenever(actorRepository.findById(ActorId(2))).doReturn(TestActorFactory.create(id = 2)) + whenever(relationshipRepository.findByActorIdAndTargetId(ActorId(1), ActorId(2))).doReturn( + Relationship( + actorId = ActorId(1), targetActorId = ActorId + (2), + following = false, + blocking = false, + muting = false, + followRequesting = true, mutingFollowRequest = false + ) + ) + service.execute(AcceptFollowRequest(1), FromApi(ActorId(2), UserDetailId(2), Acct("test", "example.com"))) + + argumentCaptor { + verify(relationshipRepository).save(capture()) + val first = allValues.first() + + assertFalse(first.followRequesting) + assertTrue(first.following) + } + } +} \ No newline at end of file diff --git a/hideout-core/src/test/kotlin/dev/usbharu/hideout/core/application/shared/LocalUserAbstractApplicationServiceTest.kt b/hideout-core/src/test/kotlin/dev/usbharu/hideout/core/application/shared/LocalUserAbstractApplicationServiceTest.kt new file mode 100644 index 00000000..a52a29d7 --- /dev/null +++ b/hideout-core/src/test/kotlin/dev/usbharu/hideout/core/application/shared/LocalUserAbstractApplicationServiceTest.kt @@ -0,0 +1,24 @@ +package dev.usbharu.hideout.core.application.shared + +import dev.usbharu.hideout.core.domain.model.support.principal.Anonymous +import dev.usbharu.hideout.core.domain.model.support.principal.FromApi +import kotlinx.coroutines.test.runTest +import org.junit.jupiter.api.Test +import org.slf4j.LoggerFactory +import utils.TestTransaction + +class LocalUserAbstractApplicationServiceTest { + @Test + fun requireFromAPI() = runTest { + val logger = LoggerFactory.getLogger(javaClass) + val value = object : LocalUserAbstractApplicationService(TestTransaction, logger) { + override suspend fun internalExecute(command: Unit, principal: FromApi) { + + } + } + + org.junit.jupiter.api.assertThrows { + value.execute(Unit, Anonymous) + } + } +} \ No newline at end of file diff --git a/hideout-core/src/test/kotlin/dev/usbharu/hideout/core/domain/model/actor/ActorDescriptionTest.kt b/hideout-core/src/test/kotlin/dev/usbharu/hideout/core/domain/model/actor/ActorDescriptionTest.kt new file mode 100644 index 00000000..c61538b0 --- /dev/null +++ b/hideout-core/src/test/kotlin/dev/usbharu/hideout/core/domain/model/actor/ActorDescriptionTest.kt @@ -0,0 +1,13 @@ +package dev.usbharu.hideout.core.domain.model.actor + +import org.junit.jupiter.api.Test +import kotlin.test.assertEquals + +class ActorDescriptionTest { + @Test + fun actorDescriptionがlength以上なら無視される() { + val actorScreenName = ActorDescription("a".repeat(100000)) + + assertEquals(ActorDescription.LENGTH, actorScreenName.description.length) + } +} \ No newline at end of file diff --git a/hideout-core/src/test/kotlin/dev/usbharu/hideout/core/domain/model/actor/ActorIdTest.kt b/hideout-core/src/test/kotlin/dev/usbharu/hideout/core/domain/model/actor/ActorIdTest.kt new file mode 100644 index 00000000..12c57c0c --- /dev/null +++ b/hideout-core/src/test/kotlin/dev/usbharu/hideout/core/domain/model/actor/ActorIdTest.kt @@ -0,0 +1,12 @@ +package dev.usbharu.hideout.core.domain.model.actor + +import org.junit.jupiter.api.Test + +class ActorIdTest { + @Test + fun idを負の数にすることはできない() { + org.junit.jupiter.api.assertThrows { + ActorId(-1) + } + } +} \ No newline at end of file diff --git a/hideout-core/src/test/kotlin/dev/usbharu/hideout/core/domain/model/actor/ActorKeyIdTest.kt b/hideout-core/src/test/kotlin/dev/usbharu/hideout/core/domain/model/actor/ActorKeyIdTest.kt new file mode 100644 index 00000000..8de564da --- /dev/null +++ b/hideout-core/src/test/kotlin/dev/usbharu/hideout/core/domain/model/actor/ActorKeyIdTest.kt @@ -0,0 +1,25 @@ +package dev.usbharu.hideout.core.domain.model.actor + +import org.junit.jupiter.api.Assertions.assertDoesNotThrow +import org.junit.jupiter.api.Test +import org.junit.jupiter.api.assertThrows + +class ActorKeyIdTest { + @Test + fun keyIdはblankではいけない() { + assertThrows { + ActorKeyId("") + } + + assertThrows { + ActorKeyId(" ") + } + } + + @Test + fun keyIdがblankでなければ作成できる() { + assertDoesNotThrow { + ActorKeyId("aiueo") + } + } +} \ No newline at end of file diff --git a/hideout-core/src/test/kotlin/dev/usbharu/hideout/core/domain/model/actor/ActorNameTest.kt b/hideout-core/src/test/kotlin/dev/usbharu/hideout/core/domain/model/actor/ActorNameTest.kt new file mode 100644 index 00000000..477f799d --- /dev/null +++ b/hideout-core/src/test/kotlin/dev/usbharu/hideout/core/domain/model/actor/ActorNameTest.kt @@ -0,0 +1,35 @@ +package dev.usbharu.hideout.core.domain.model.actor + +import org.junit.jupiter.api.Test +import org.junit.jupiter.api.assertDoesNotThrow +import org.junit.jupiter.api.assertThrows + +class ActorNameTest { + @Test + fun blankはダメ() { + assertThrows { + ActorName("") + } + } + + @Test + fun 長過ぎるとダメ() { + assertThrows { + ActorName("a".repeat(1000)) + } + } + + @Test + fun 指定外の文字は使えない() { + assertThrows { + ActorName("あ") + } + } + + @Test + fun 普通に作成できる() { + assertDoesNotThrow { + ActorName("test-user") + } + } +} \ No newline at end of file diff --git a/hideout-core/src/test/kotlin/dev/usbharu/hideout/core/domain/model/actor/ActorPostsCountTest.kt b/hideout-core/src/test/kotlin/dev/usbharu/hideout/core/domain/model/actor/ActorPostsCountTest.kt new file mode 100644 index 00000000..451913b5 --- /dev/null +++ b/hideout-core/src/test/kotlin/dev/usbharu/hideout/core/domain/model/actor/ActorPostsCountTest.kt @@ -0,0 +1,20 @@ +package dev.usbharu.hideout.core.domain.model.actor + +import org.junit.jupiter.api.Assertions.assertDoesNotThrow +import org.junit.jupiter.api.Test + +class ActorPostsCountTest { + @Test + fun postsCountが負になることはない() { + org.junit.jupiter.api.assertThrows { + ActorPostsCount(-1) + } + } + + @Test + fun postsCountが正の数値なら設定できる() { + assertDoesNotThrow { + ActorPostsCount(1) + } + } +} \ No newline at end of file diff --git a/hideout-core/src/test/kotlin/dev/usbharu/hideout/core/domain/model/actor/ActorPrivateKeyTest.kt b/hideout-core/src/test/kotlin/dev/usbharu/hideout/core/domain/model/actor/ActorPrivateKeyTest.kt new file mode 100644 index 00000000..68ff6877 --- /dev/null +++ b/hideout-core/src/test/kotlin/dev/usbharu/hideout/core/domain/model/actor/ActorPrivateKeyTest.kt @@ -0,0 +1,18 @@ +package dev.usbharu.hideout.core.domain.model.actor + +import dev.usbharu.hideout.util.Base64Util +import org.junit.jupiter.api.Assertions.assertEquals +import org.junit.jupiter.api.Test +import java.security.KeyPairGenerator + +class ActorPrivateKeyTest { + @Test + fun privateKeyから生成できる() { + val genKeyPair = KeyPairGenerator.getInstance("RSA").genKeyPair() + val actorPrivateKey = ActorPrivateKey.create(genKeyPair.private) + val key = "-----BEGIN PRIVATE KEY-----\n" + + Base64Util.encode(genKeyPair.private.encoded).chunked(64) + .joinToString("\n") + "\n-----END PRIVATE KEY-----" + assertEquals(key, actorPrivateKey.privateKey) + } +} \ No newline at end of file diff --git a/hideout-core/src/test/kotlin/dev/usbharu/hideout/core/domain/model/actor/ActorPublicKeyTest.kt b/hideout-core/src/test/kotlin/dev/usbharu/hideout/core/domain/model/actor/ActorPublicKeyTest.kt new file mode 100644 index 00000000..a2ec7f79 --- /dev/null +++ b/hideout-core/src/test/kotlin/dev/usbharu/hideout/core/domain/model/actor/ActorPublicKeyTest.kt @@ -0,0 +1,18 @@ +package dev.usbharu.hideout.core.domain.model.actor + +import dev.usbharu.hideout.util.Base64Util +import org.junit.jupiter.api.Assertions.assertEquals +import org.junit.jupiter.api.Test +import java.security.KeyPairGenerator + +class ActorPublicKeyTest { + @Test + fun publicKeyから生成できる() { + val genKeyPair = KeyPairGenerator.getInstance("RSA").genKeyPair() + val actorPublicKey = ActorPublicKey.create(genKeyPair.public) + val key = "-----BEGIN PUBLIC KEY-----\n" + + Base64Util.encode(genKeyPair.public.encoded).chunked(64) + .joinToString("\n") + "\n-----END PUBLIC KEY-----" + assertEquals(key, actorPublicKey.publicKey) + } +} \ No newline at end of file diff --git a/hideout-core/src/test/kotlin/dev/usbharu/hideout/core/domain/model/actor/ActorRelationshipCountTest.kt b/hideout-core/src/test/kotlin/dev/usbharu/hideout/core/domain/model/actor/ActorRelationshipCountTest.kt new file mode 100644 index 00000000..340d1563 --- /dev/null +++ b/hideout-core/src/test/kotlin/dev/usbharu/hideout/core/domain/model/actor/ActorRelationshipCountTest.kt @@ -0,0 +1,21 @@ +package dev.usbharu.hideout.core.domain.model.actor + +import org.junit.jupiter.api.Assertions.assertDoesNotThrow +import org.junit.jupiter.api.Test +import org.junit.jupiter.api.assertThrows + +class ActorRelationshipCountTest { + @Test + fun relationshipCountが負になることはない() { + assertThrows { + ActorRelationshipCount(-1) + } + } + + @Test + fun relationshipCountが正の数値なら設定できる() { + assertDoesNotThrow { + ActorRelationshipCount(1) + } + } +} \ No newline at end of file diff --git a/hideout-core/src/test/kotlin/dev/usbharu/hideout/core/domain/model/actor/ActorScreenNameTest.kt b/hideout-core/src/test/kotlin/dev/usbharu/hideout/core/domain/model/actor/ActorScreenNameTest.kt new file mode 100644 index 00000000..4c4fa1f2 --- /dev/null +++ b/hideout-core/src/test/kotlin/dev/usbharu/hideout/core/domain/model/actor/ActorScreenNameTest.kt @@ -0,0 +1,13 @@ +package dev.usbharu.hideout.core.domain.model.actor + +import org.junit.jupiter.api.Test +import kotlin.test.assertEquals + +class ActorScreenNameTest { + @Test + fun screenNameがlengthを超えると無視される() { + val actorScreenName = ActorScreenName("a".repeat(1000)) + + assertEquals(ActorScreenName.LENGTH, actorScreenName.screenName.length) + } +} \ No newline at end of file diff --git a/hideout-core/src/test/kotlin/dev/usbharu/hideout/core/domain/model/actor/ActorsTest.kt b/hideout-core/src/test/kotlin/dev/usbharu/hideout/core/domain/model/actor/ActorsTest.kt new file mode 100644 index 00000000..d4f55ff9 --- /dev/null +++ b/hideout-core/src/test/kotlin/dev/usbharu/hideout/core/domain/model/actor/ActorsTest.kt @@ -0,0 +1,149 @@ +package dev.usbharu.hideout.core.domain.model.actor + +import dev.usbharu.hideout.core.domain.event.actor.ActorEvent +import dev.usbharu.hideout.core.domain.model.media.MediaId +import org.junit.jupiter.api.Test +import org.junit.jupiter.api.assertThrows +import utils.AssertDomainEvent.assertContainsEvent +import utils.AssertDomainEvent.assertEmpty +import kotlin.test.assertEquals +import kotlin.test.assertFalse +import kotlin.test.assertNull + +class ActorsTest { + @Test + fun suspendがtrueのときactorSuspendイベントが発生する() { + val actor = TestActorFactory.create(publicKey = ActorPublicKey("")) + + actor.suspend = true + + assertContainsEvent(actor, ActorEvent.ACTOR_SUSPEND.eventName) + } + + @Test + fun suspendがfalseになったときactorUnsuspendイベントが発生する() { + val actor = TestActorFactory.create(publicKey = ActorPublicKey(""), suspend = true) + + actor.suspend = false + + assertContainsEvent(actor, ActorEvent.ACTOR_UNSUSPEND.eventName) + } + + @Test + fun alsoKnownAsに自分自身が含まれない場合更新される() { + val actor = TestActorFactory.create(publicKey = ActorPublicKey("")) + + val actorIds = setOf(ActorId(100), ActorId(200)) + actor.alsoKnownAs = actorIds + + assertEquals(actorIds, actor.alsoKnownAs) + } + + @Test + fun moveToに自分自身が設定された場合moveイベントが発生し更新される() { + val actor = TestActorFactory.create(publicKey = ActorPublicKey("")) + + + actor.moveTo = ActorId(100) + + assertContainsEvent(actor, ActorEvent.MOVE.eventName) + } + + @Test + fun alsoKnownAsに自分自身が含まれてはいけない() { + val actor = TestActorFactory.create(publicKey = ActorPublicKey("")) + + assertThrows { + actor.alsoKnownAs = setOf(actor.id) + } + } + + @Test + fun moveToに自分自身が設定されてはいけない() { + val actor = TestActorFactory.create(publicKey = ActorPublicKey("")) + + assertThrows { + actor.moveTo = actor.id + } + } + + @Test + fun descriptionが更新されたときupdateイベントが発生する() { + val actor = TestActorFactory.create(publicKey = ActorPublicKey("")) + + actor.description = ActorDescription("hoge fuga") + + assertContainsEvent(actor, ActorEvent.UPDATE.eventName) + } + + @Test + fun screenNameが更新されたときupdateイベントが発生する() { + val actor = TestActorFactory.create(publicKey = ActorPublicKey("")) + + actor.screenName = ActorScreenName("fuga hoge") + + assertContainsEvent(actor, ActorEvent.UPDATE.eventName) + } + + @Test + fun deleteが実行されたときすでにdeletedがtrueなら何もしない() { + val actor = TestActorFactory.create(publicKey = ActorPublicKey(""), deleted = true) + + actor.delete() + + assertEmpty(actor) + } + + @Test + fun deleteが実行されたときdeletedがfalseならdeleteイベントが発生する() { + val actor = TestActorFactory.create(publicKey = ActorPublicKey("")) + + actor.delete() + + assertEquals(ActorScreenName.empty, actor.screenName) + assertEquals(ActorDescription.empty, actor.description) + assertEquals(emptySet(), actor.emojis) + assertNull(actor.lastPostAt) + assertEquals(ActorPostsCount.ZERO, actor.postsCount) + assertNull(actor.followersCount) + assertNull(actor.followingCount) + assertContainsEvent(actor, ActorEvent.DELETE.eventName) + } + + @Test + fun restoreが実行されたときcheckUpdateイベントが発生する() { + val actor = TestActorFactory.create(publicKey = ActorPublicKey(""), deleted = true) + + actor.restore() + + assertFalse(actor.deleted) + assertContainsEvent(actor, ActorEvent.CHECK_UPDATE.eventName) + } + + @Test + fun checkUpdateが実行されたときcheckUpdateイベントがh() { + val actor = TestActorFactory.create(publicKey = ActorPublicKey("")) + + actor.checkUpdate() + + assertContainsEvent(actor, ActorEvent.CHECK_UPDATE.eventName) + } + + @Test + fun bannerが設定されたらupdateイベントが発生する() { + val actor = TestActorFactory.create(publicKey = ActorPublicKey("")) + + actor.setBannerUrl(MediaId(1)) + + assertContainsEvent(actor, ActorEvent.UPDATE.eventName) + } + + @Test + fun iconが設定されたらupdateイベントが発生する() { + val actor = TestActorFactory.create(publicKey = ActorPublicKey("")) + + actor.setIconUrl(MediaId(1)) + + assertContainsEvent(actor, ActorEvent.UPDATE.eventName) + } +} \ No newline at end of file diff --git a/hideout-core/src/test/kotlin/dev/usbharu/hideout/core/domain/model/actor/TestActorFactory.kt b/hideout-core/src/test/kotlin/dev/usbharu/hideout/core/domain/model/actor/TestActorFactory.kt new file mode 100644 index 00000000..3286ed74 --- /dev/null +++ b/hideout-core/src/test/kotlin/dev/usbharu/hideout/core/domain/model/actor/TestActorFactory.kt @@ -0,0 +1,79 @@ +package dev.usbharu.hideout.core.domain.model.actor + +import dev.usbharu.hideout.core.domain.model.emoji.EmojiId +import dev.usbharu.hideout.core.domain.model.instance.InstanceId +import dev.usbharu.hideout.core.domain.model.support.domain.Domain +import dev.usbharu.hideout.core.infrastructure.other.TwitterSnowflakeIdGenerateService +import kotlinx.coroutines.runBlocking +import java.net.URI +import java.time.Instant + +object TestActorFactory { + private val idGenerateService = TwitterSnowflakeIdGenerateService + + fun create( + id: Long = generateId(), + actorName: String = "test-$id", + domain: String = "example.com", + actorScreenName: String = actorName, + description: String = "test description", + inbox: URI = URI.create("https://example.com/$id/inbox"), + outbox: URI = URI.create("https://example.com/$id/outbox"), + uri: URI = URI.create("https://example.com/$id"), + publicKey: ActorPublicKey = ActorPublicKey(""), + privateKey: ActorPrivateKey? = null, + createdAt: Instant = Instant.now(), + keyId: String = "https://example.com/$id#key-id", + followersEndpoint: URI = URI.create("https://example.com/$id/followers"), + followingEndpoint: URI = URI.create("https://example.com/$id/following"), + instanceId: Long = 1L, + locked: Boolean = false, + followersCount: Int = 0, + followingCount: Int = 0, + postCount: Int = 0, + lastPostDate: Instant? = null, + suspend: Boolean = false, + alsoKnownAs: Set = emptySet(), + moveTo: Long? = null, + emojiIds: Set = emptySet(), + deleted: Boolean = false, + roles: Set = emptySet(), + ): Actor { + return runBlocking { + Actor( + id = ActorId(id), + name = ActorName(actorName), + domain = Domain(domain), + screenName = ActorScreenName(actorScreenName), + description = ActorDescription(description), + inbox = inbox, + outbox = outbox, + url = uri, + publicKey = publicKey, + privateKey = privateKey, + createdAt = createdAt, + keyId = ActorKeyId(keyId), + followersEndpoint = followersEndpoint, + followingEndpoint = followingEndpoint, + instance = InstanceId(instanceId), + locked = locked, + followersCount = ActorRelationshipCount(followersCount), + followingCount = ActorRelationshipCount(followingCount), + postsCount = ActorPostsCount(postCount), + lastPostAt = lastPostDate, + suspend = suspend, + alsoKnownAs = alsoKnownAs, + moveTo = moveTo?.let { ActorId(it) }, + emojiIds = emojiIds, + deleted = deleted, + icon = null, + banner = null, + + ) + } + } + + private fun generateId(): Long = runBlocking { + idGenerateService.generateId() + } +} \ No newline at end of file diff --git a/hideout-core/src/test/kotlin/dev/usbharu/hideout/core/domain/model/actorinstancerelationship/ActorInstanceRelationshipTest.kt b/hideout-core/src/test/kotlin/dev/usbharu/hideout/core/domain/model/actorinstancerelationship/ActorInstanceRelationshipTest.kt new file mode 100644 index 00000000..c24c559d --- /dev/null +++ b/hideout-core/src/test/kotlin/dev/usbharu/hideout/core/domain/model/actorinstancerelationship/ActorInstanceRelationshipTest.kt @@ -0,0 +1,77 @@ +package dev.usbharu.hideout.core.domain.model.actorinstancerelationship + +import dev.usbharu.hideout.core.domain.event.actorinstancerelationship.ActorInstanceRelationshipEvent +import dev.usbharu.hideout.core.domain.model.actor.ActorId +import dev.usbharu.hideout.core.domain.model.instance.InstanceId +import org.junit.jupiter.api.Assertions.assertFalse +import org.junit.jupiter.api.Assertions.assertTrue +import org.junit.jupiter.api.Test +import utils.AssertDomainEvent.assertContainsEvent + +class ActorInstanceRelationshipTest { + @Test + fun blockするとBLOCKイベントが発生する() { + val actorInstanceRelationship = ActorInstanceRelationship(ActorId(1), InstanceId(2), false) + + actorInstanceRelationship.block() + + assertContainsEvent(actorInstanceRelationship, ActorInstanceRelationshipEvent.BLOCK.eventName) + assertTrue(actorInstanceRelationship.blocking) + } + + @Test + fun muteするとMUTEイベントが発生する() { + val actorInstanceRelationship = ActorInstanceRelationship(ActorId(1), InstanceId(2), false) + + actorInstanceRelationship.mute() + + assertContainsEvent(actorInstanceRelationship, ActorInstanceRelationshipEvent.MUTE.eventName) + assertTrue(actorInstanceRelationship.muting) + } + + @Test + fun unmuteするとUNMUTEイベントが発生する() { + val actorInstanceRelationship = ActorInstanceRelationship(ActorId(1), InstanceId(2), muting = true) + + actorInstanceRelationship.unmute() + + assertContainsEvent(actorInstanceRelationship, ActorInstanceRelationshipEvent.UNMUTE.eventName) + assertFalse(actorInstanceRelationship.muting) + } + + @Test + fun unblockで解除される() { + val actorInstanceRelationship = ActorInstanceRelationship(ActorId(1), InstanceId(2), true) + + actorInstanceRelationship.unblock() + + assertFalse(actorInstanceRelationship.blocking) + } + + @Test + fun doNotSendPrivateで設定される() { + val actorInstanceRelationship = ActorInstanceRelationship(ActorId(1), InstanceId(2)) + + actorInstanceRelationship.doNotSendPrivate() + + assertTrue(actorInstanceRelationship.doNotSendPrivate) + } + + @Test + fun doSendPrivateで解除される() { + val actorInstanceRelationship = ActorInstanceRelationship(ActorId(1), InstanceId(2), doNotSendPrivate = true) + + actorInstanceRelationship.doSendPrivate() + + assertFalse(actorInstanceRelationship.doNotSendPrivate) + } + + @Test + fun defaultで全部falseが作られる() { + val default = ActorInstanceRelationship.default(ActorId(1), InstanceId(2)) + + assertFalse(default.muting) + assertFalse(default.blocking) + assertFalse(default.doNotSendPrivate) + } +} \ No newline at end of file diff --git a/hideout-core/src/test/kotlin/dev/usbharu/hideout/core/domain/model/application/ApplicationIdTest.kt b/hideout-core/src/test/kotlin/dev/usbharu/hideout/core/domain/model/application/ApplicationIdTest.kt new file mode 100644 index 00000000..e8c08c9d --- /dev/null +++ b/hideout-core/src/test/kotlin/dev/usbharu/hideout/core/domain/model/application/ApplicationIdTest.kt @@ -0,0 +1,20 @@ +package dev.usbharu.hideout.core.domain.model.application + +import org.junit.jupiter.api.Assertions.assertDoesNotThrow +import org.junit.jupiter.api.Test + +class ApplicationIdTest { + @Test + fun applicationIdは0以上である必要がある() { + org.junit.jupiter.api.assertThrows { + ApplicationId(-1) + } + } + + @Test + fun applicationIdが0以上なら設定できる() { + assertDoesNotThrow { + ApplicationId(1) + } + } +} \ No newline at end of file diff --git a/hideout-core/src/test/kotlin/dev/usbharu/hideout/core/domain/model/application/ApplicationNameTest.kt b/hideout-core/src/test/kotlin/dev/usbharu/hideout/core/domain/model/application/ApplicationNameTest.kt new file mode 100644 index 00000000..a9fddfa1 --- /dev/null +++ b/hideout-core/src/test/kotlin/dev/usbharu/hideout/core/domain/model/application/ApplicationNameTest.kt @@ -0,0 +1,20 @@ +package dev.usbharu.hideout.core.domain.model.application + +import org.junit.jupiter.api.Assertions.assertDoesNotThrow +import org.junit.jupiter.api.Test + +class ApplicationNameTest { + @Test + fun applicationNameがlength以上の時エラー() { + org.junit.jupiter.api.assertThrows { + ApplicationName("a".repeat(1000)) + } + } + + @Test + fun applicationNameがlength未満の時設定できる() { + assertDoesNotThrow { + ApplicationName("a".repeat(100)) + } + } +} diff --git a/hideout-core/src/test/kotlin/dev/usbharu/hideout/core/domain/model/application/ApplicationTest.kt b/hideout-core/src/test/kotlin/dev/usbharu/hideout/core/domain/model/application/ApplicationTest.kt new file mode 100644 index 00000000..af51c836 --- /dev/null +++ b/hideout-core/src/test/kotlin/dev/usbharu/hideout/core/domain/model/application/ApplicationTest.kt @@ -0,0 +1,13 @@ +package dev.usbharu.hideout.core.domain.model.application + +import org.junit.jupiter.api.Test + +class ApplicationTest { + @Test + fun インスタンスを生成できる() { + Application( + ApplicationId(1), + ApplicationName("aiueo") + ) + } +} \ No newline at end of file diff --git a/hideout-core/src/test/kotlin/dev/usbharu/hideout/core/domain/model/emoji/EmojiIdTest.kt b/hideout-core/src/test/kotlin/dev/usbharu/hideout/core/domain/model/emoji/EmojiIdTest.kt new file mode 100644 index 00000000..c304294b --- /dev/null +++ b/hideout-core/src/test/kotlin/dev/usbharu/hideout/core/domain/model/emoji/EmojiIdTest.kt @@ -0,0 +1,20 @@ +package dev.usbharu.hideout.core.domain.model.emoji + +import org.junit.jupiter.api.Assertions.assertDoesNotThrow +import org.junit.jupiter.api.Test + +class EmojiIdTest { + @Test + fun emojiIdは0以上である必要がある() { + org.junit.jupiter.api.assertThrows { + EmojiId(-1) + } + } + + @Test + fun emojiIdは0以上なら設定できる() { + assertDoesNotThrow { + EmojiId(1) + } + } +} \ No newline at end of file diff --git a/hideout-core/src/test/kotlin/dev/usbharu/hideout/core/domain/model/filter/FilterIdTest.kt b/hideout-core/src/test/kotlin/dev/usbharu/hideout/core/domain/model/filter/FilterIdTest.kt new file mode 100644 index 00000000..f91e4d5f --- /dev/null +++ b/hideout-core/src/test/kotlin/dev/usbharu/hideout/core/domain/model/filter/FilterIdTest.kt @@ -0,0 +1,20 @@ +package dev.usbharu.hideout.core.domain.model.filter + +import org.junit.jupiter.api.Assertions.assertDoesNotThrow +import org.junit.jupiter.api.Test + +class FilterIdTest { + @Test + fun filterIdは0以上である必要がある() { + org.junit.jupiter.api.assertThrows { + FilterId(-1) + } + } + + @Test + fun filterIdが0以上なら設定できる() { + assertDoesNotThrow { + FilterId(1) + } + } +} \ No newline at end of file diff --git a/hideout-core/src/test/kotlin/dev/usbharu/hideout/core/domain/model/filter/FilterNameTest.kt b/hideout-core/src/test/kotlin/dev/usbharu/hideout/core/domain/model/filter/FilterNameTest.kt new file mode 100644 index 00000000..00bd6c84 --- /dev/null +++ b/hideout-core/src/test/kotlin/dev/usbharu/hideout/core/domain/model/filter/FilterNameTest.kt @@ -0,0 +1,13 @@ +package dev.usbharu.hideout.core.domain.model.filter + +import org.junit.jupiter.api.Test +import kotlin.test.assertEquals + +class FilterNameTest { + @Test + fun FilterNameがlength以上のときは無視される() { + val filterName = FilterName("a".repeat(1000)) + + assertEquals(FilterName.LENGTH, filterName.name.length) + } +} \ No newline at end of file diff --git a/hideout-core/src/test/kotlin/dev/usbharu/hideout/core/domain/model/filter/FilterTest.kt b/hideout-core/src/test/kotlin/dev/usbharu/hideout/core/domain/model/filter/FilterTest.kt new file mode 100644 index 00000000..48d26605 --- /dev/null +++ b/hideout-core/src/test/kotlin/dev/usbharu/hideout/core/domain/model/filter/FilterTest.kt @@ -0,0 +1,139 @@ +package dev.usbharu.hideout.core.domain.model.filter + +import dev.usbharu.hideout.core.domain.model.actor.ActorId +import dev.usbharu.hideout.core.domain.model.userdetails.UserDetail +import dev.usbharu.hideout.core.domain.model.userdetails.UserDetailHashedPassword +import dev.usbharu.hideout.core.domain.model.userdetails.UserDetailId +import org.junit.jupiter.api.Assertions.assertDoesNotThrow +import org.junit.jupiter.api.Test + +class FilterTest { + @Test + fun `setFilterKeywords 所有者のみ変更できる`() { + val filter = Filter.create( + id = FilterId(1), + userDetailId = UserDetailId(1), + name = FilterName("aiueo"), + filterContext = setOf(), + filterAction = FilterAction.HIDE, + filterKeywords = setOf() + ) + + val userDetail = UserDetail.create( + id = UserDetailId(1), + actorId = ActorId(1), + password = UserDetailHashedPassword(""), + autoAcceptFolloweeFollowRequest = false, + lastMigration = null, + null + ) + + assertDoesNotThrow { + filter.setFilterKeywords( + setOf( + FilterKeyword( + FilterKeywordId(1), + FilterKeywordKeyword("keyword"), + FilterMode.NONE + ) + ), userDetail + ) + } + + } + + @Test + fun compileFilterで正規表現として表すことができるNONE() { + val filter = Filter( + id = FilterId(1), + userDetailId = UserDetailId(1), + name = FilterName("aiueo"), + filterContext = setOf(), + filterAction = FilterAction.HIDE, + filterKeywords = setOf( + FilterKeyword( + FilterKeywordId(1), + FilterKeywordKeyword("hoge"), + FilterMode.NONE + ) + ) + ) + + kotlin.test.assertEquals("(hoge)", filter.compileFilter().pattern) + + } + + @Test + fun compileFilterで正規表現として表すことができるWHOLE_WORD() { + val filter = Filter( + id = FilterId(1), + userDetailId = UserDetailId(1), + name = FilterName("aiueo"), + filterContext = setOf(), + filterAction = FilterAction.HIDE, + filterKeywords = setOf( + FilterKeyword( + FilterKeywordId(1), + FilterKeywordKeyword("hoge"), + FilterMode.WHOLE_WORD + ) + ) + ) + + kotlin.test.assertEquals("\\b(hoge)\\b", filter.compileFilter().pattern) + + } + + @Test + fun compileFilterで正規表現として表すことができるREGEX() { + val filter = Filter( + id = FilterId(1), + userDetailId = UserDetailId(1), + name = FilterName("aiueo"), + filterContext = setOf(), + filterAction = FilterAction.HIDE, + filterKeywords = setOf( + FilterKeyword( + FilterKeywordId(1), + FilterKeywordKeyword("hoge"), + FilterMode.REGEX + ) + ) + ) + + kotlin.test.assertEquals("(hoge)", filter.compileFilter().pattern) + + } + + @Test + fun compileFilterで正規表現として表すことができる() { + val filter = Filter( + id = FilterId(1), + userDetailId = UserDetailId(1), + name = FilterName("aiueo"), + filterContext = setOf(), + filterAction = FilterAction.HIDE, + filterKeywords = setOf( + FilterKeyword( + FilterKeywordId(1), + FilterKeywordKeyword("hoge"), + FilterMode.WHOLE_WORD + ), + FilterKeyword( + FilterKeywordId(2), + FilterKeywordKeyword("hoge"), + FilterMode.REGEX + ), + FilterKeyword( + FilterKeywordId(3), + FilterKeywordKeyword("hoge"), + FilterMode.NONE + ) + ) + ) + + kotlin.test.assertEquals("\\b(hoge)\\b|(hoge)|(hoge)", filter.compileFilter().pattern) + + } +} + diff --git a/hideout-core/src/test/kotlin/dev/usbharu/hideout/core/domain/model/post/PostTest.kt b/hideout-core/src/test/kotlin/dev/usbharu/hideout/core/domain/model/post/PostTest.kt new file mode 100644 index 00000000..69fb7cca --- /dev/null +++ b/hideout-core/src/test/kotlin/dev/usbharu/hideout/core/domain/model/post/PostTest.kt @@ -0,0 +1,695 @@ +package dev.usbharu.hideout.core.domain.model.post + +import dev.usbharu.hideout.core.domain.event.post.PostEvent +import dev.usbharu.hideout.core.domain.model.actor.ActorId +import dev.usbharu.hideout.core.domain.model.actor.ActorPublicKey +import dev.usbharu.hideout.core.domain.model.actor.TestActorFactory +import dev.usbharu.hideout.core.domain.model.emoji.EmojiId +import dev.usbharu.hideout.core.domain.model.media.MediaId +import org.junit.jupiter.api.Assertions.assertTrue +import org.junit.jupiter.api.Test +import org.junit.jupiter.api.assertDoesNotThrow +import org.junit.jupiter.api.assertThrows +import utils.AssertDomainEvent.assertContainsEvent +import utils.AssertDomainEvent.assertEmpty +import java.net.URI +import java.time.Instant +import kotlin.test.assertEquals +import kotlin.test.assertFalse +import kotlin.test.assertNull + +class PostTest { + @Test + fun deletedがtrueのときghostのidが返される() { + val post = TestPostFactory.create(deleted = true) + + assertEquals(ActorId.ghost, post.actorId) + } + + @Test + fun deletedがfalseの時actorのIDが返される() { + val post = TestPostFactory.create(deleted = false, actorId = 100) + + assertEquals(ActorId(100), post.actorId) + } + + @Test + fun visibilityがDIRECTのとき変更できない() { + val post = TestPostFactory.create(visibility = Visibility.DIRECT) + + val actor = TestActorFactory.create(id = post.actorId.id, publicKey = ActorPublicKey("")) + + assertThrows { + post.setVisibility(Visibility.PUBLIC, actor) + } + assertThrows { + post.setVisibility(Visibility.UNLISTED, actor) + } + assertThrows { + post.setVisibility(Visibility.FOLLOWERS, actor) + } + } + + @Test + fun visibilityを小さくすることはできないPUBLIC() { + val post = TestPostFactory.create(visibility = Visibility.PUBLIC) + val actor = TestActorFactory.create(id = post.actorId.id, publicKey = ActorPublicKey("")) + assertThrows { + post.setVisibility(Visibility.DIRECT, actor) + } + assertThrows { + post.setVisibility(Visibility.UNLISTED, actor) + } + assertThrows { + post.setVisibility(Visibility.FOLLOWERS, actor) + } + } + + @Test + fun visibilityを小さくすることはできないUNLISTED() { + val post = TestPostFactory.create(visibility = Visibility.UNLISTED) + val actor = TestActorFactory.create(id = post.actorId.id, publicKey = ActorPublicKey("")) + assertThrows { + post.setVisibility(Visibility.DIRECT, actor) + } + assertThrows { + post.setVisibility(Visibility.FOLLOWERS, actor) + } + } + + @Test + fun visibilityを小さくすることはできないFOLLOWERS() { + val post = TestPostFactory.create(visibility = Visibility.FOLLOWERS) + val actor = TestActorFactory.create(id = post.actorId.id, publicKey = ActorPublicKey("")) + assertThrows { + post.setVisibility(Visibility.DIRECT, actor) + } + } + + @Test + fun visibilityをDIRECTにあとからすることはできない() { + val post = TestPostFactory.create(visibility = Visibility.DIRECT) + val actor = TestActorFactory.create(id = post.actorId.id, publicKey = ActorPublicKey("")) + assertThrows { + post.setVisibility(Visibility.DIRECT, actor) + } + } + + @Test + fun visibilityを大きくすることができるFOLLOWERS() { + val post = TestPostFactory.create(visibility = Visibility.FOLLOWERS) + val actor = TestActorFactory.create(id = post.actorId.id, publicKey = ActorPublicKey("")) + assertDoesNotThrow { + post.setVisibility(Visibility.UNLISTED, actor) + } + + val post2 = TestPostFactory.create(visibility = Visibility.FOLLOWERS) + + assertDoesNotThrow { + post2.setVisibility(Visibility.PUBLIC, actor) + } + } + + @Test + fun visibilityを大きくすることができるUNLISTED() { + val post = TestPostFactory.create(visibility = Visibility.UNLISTED) + val actor = TestActorFactory.create(id = post.actorId.id, publicKey = ActorPublicKey("")) + assertDoesNotThrow { + post.setVisibility(Visibility.PUBLIC, actor) + } + } + + @Test + fun deletedがtrueのときvisibilityを変更できない() { + val post = TestPostFactory.create(visibility = Visibility.UNLISTED, deleted = true) + val actor = TestActorFactory.create(id = post.actorId.id, publicKey = ActorPublicKey("")) + assertThrows { + post.setVisibility(Visibility.PUBLIC, actor) + } + } + + @Test + fun visibilityが変更されない限りドメインイベントは発生しない() { + val post = TestPostFactory.create(visibility = Visibility.UNLISTED) + val actor = TestActorFactory.create(id = post.actorId.id, publicKey = ActorPublicKey("")) + post.setVisibility(Visibility.UNLISTED, actor) + assertEmpty(post) + + } + + @Test + fun visibilityが変更されるとupdateイベントが発生する() { + val post = TestPostFactory.create(visibility = Visibility.UNLISTED) + val actor = TestActorFactory.create(id = post.actorId.id, publicKey = ActorPublicKey("")) + post.setVisibility(Visibility.PUBLIC, actor) + + assertContainsEvent(post, PostEvent.UPDATE.eventName) + } + + @Test + fun deletedがtrueのときvisibleActorsを変更できない() { + val post = TestPostFactory.create(deleted = true) + val actor = TestActorFactory.create(id = post.actorId.id, publicKey = ActorPublicKey("")) + assertThrows { + post.setVisibleActors(setOf(ActorId(100)), actor) + } + } + + @Test + fun ゔvisibilityがDIRECT以外の時visibleActorsを変更できない() { + val post = TestPostFactory.create(visibility = Visibility.FOLLOWERS) + val actor = TestActorFactory.create(id = post.actorId.id, publicKey = ActorPublicKey("")) + post.setVisibleActors(setOf(ActorId(100)), actor) + assertEmpty(post) + + val post2 = TestPostFactory.create(visibility = Visibility.UNLISTED) + + post2.setVisibleActors(setOf(ActorId(100)), actor) + assertEmpty(post2) + + val post3 = TestPostFactory.create(visibility = Visibility.PUBLIC) + + post3.setVisibleActors(setOf(ActorId(100)), actor) + assertEmpty(post3) + } + + @Test + fun visibilityがDIRECTの時visibleActorsを変更できる() { + val post = TestPostFactory.create(visibility = Visibility.DIRECT) + val actor = TestActorFactory.create(id = post.actorId.id, publicKey = ActorPublicKey("")) + post.setVisibleActors(setOf(ActorId(100)), actor) + assertEquals(setOf(ActorId(100)), post.visibleActors) + } + + @Test + fun visibleActorsから削除されることはない() { + val post = TestPostFactory.create(visibility = Visibility.DIRECT, visibleActors = listOf(100)) + val actor = TestActorFactory.create(id = post.actorId.id, publicKey = ActorPublicKey("")) + post.setVisibleActors(setOf(ActorId(200)), actor) + assertEquals(setOf(ActorId(100), ActorId(200)), post.visibleActors) + } + + @Test + fun visibleActorsに追加された時updateイベントが発生する() { + val post = TestPostFactory.create(visibility = Visibility.DIRECT) + val actor = TestActorFactory.create(id = post.actorId.id, publicKey = ActorPublicKey("")) + post.setVisibleActors(setOf(ActorId(100)), actor) + + assertContainsEvent(post, PostEvent.UPDATE.eventName) + } + + @Test + fun hideがtrueのときcontentがemptyを返す() { + val post = TestPostFactory.create(hide = true) + + assertEquals(PostContent.empty, post.content) + } + + @Test + fun deletedがtrueの時contentをセットできない() { + val post = TestPostFactory.create(deleted = true) + val actor = TestActorFactory.create(id = post.actorId.id, publicKey = ActorPublicKey("")) + assertThrows { + post.setContent(PostContent("test", "test", emptyList()), actor) + } + } + + @Test + fun contentの内容が変更されたらupdateイベントが発生する() { + val post = TestPostFactory.create() + val actor = TestActorFactory.create(id = post.actorId.id, publicKey = ActorPublicKey("")) + post.setContent(PostContent("test", "test", emptyList()), actor) + assertContainsEvent(post, PostEvent.UPDATE.eventName) + } + + @Test + fun hideがtrueの時nullを返す() { + val post = TestPostFactory.create(hide = true, overview = "aaaa") + + assertNull(post.overview) + } + + @Test + fun hideがfalseの時overviewを返す() { + val post = TestPostFactory.create(hide = false, overview = "aaaa") + + assertEquals(PostOverview("aaaa"), post.overview) + } + + @Test + fun deletedがtrueのときセットできない() { + val post = TestPostFactory.create(deleted = true) + val actor = TestActorFactory.create(id = post.actorId.id, publicKey = ActorPublicKey("")) + assertThrows { + post.setOverview(PostOverview("aaaa"), actor) + } + } + + @Test + fun deletedがfalseのときセットできる() { + val post = TestPostFactory.create(deleted = false) + val actor = TestActorFactory.create(id = post.actorId.id, publicKey = ActorPublicKey("")) + val overview = PostOverview("aaaa") + assertDoesNotThrow { + post.setOverview(overview, actor) + } + assertEquals(overview, post.overview) + + assertContainsEvent(post, PostEvent.UPDATE.eventName) + } + + @Test + fun overviewの内容が更新されなかった時イベントが発生しない() { + val post = TestPostFactory.create(overview = "aaaa") + val actor = TestActorFactory.create(id = post.actorId.id, publicKey = ActorPublicKey("")) + post.setOverview(PostOverview("aaaa"), actor) + assertEmpty(post) + } + + @Test + fun sensitiveが変更されるとupdateイベントが発生する() { + val post = TestPostFactory.create() + val actor = TestActorFactory.create(id = post.actorId.id, publicKey = ActorPublicKey("")) + post.setSensitive(true, actor) + assertContainsEvent(post, PostEvent.UPDATE.eventName) + } + + @Test + fun 削除されている場合sensitiveを変更できない() { + val post = TestPostFactory.create(deleted = true) + val actor = TestActorFactory.create(id = post.actorId.id, publicKey = ActorPublicKey("")) + assertThrows { + post.setSensitive(true, actor) + } + } + + @Test + fun sensitiveが変更されなかった場合イベントが発生しない() { + val post = TestPostFactory.create(overview = "aaaa") + val actor = TestActorFactory.create(id = post.actorId.id, publicKey = ActorPublicKey("")) + post.setSensitive(false, actor) + assertEmpty(post) + } + + @Test + fun hideがtrueの時emptyが帰る() { + val post = TestPostFactory.create(hide = true) + + assertEquals(PostContent.empty.text, post.text) + } + + @Test + fun hideがfalseの時textが返る() { + val post = TestPostFactory.create(hide = false, content = "aaaa") + + assertEquals("aaaa", post.text) + } + + @Test + fun `create actorが削除済みの時作成できない`() { + val actor = TestActorFactory.create(deleted = true) + assertThrows { + Post.create( + id = PostId(1), + actorId = actor.id, + instanceId = actor.instance, + overview = null, + content = PostContent.empty, + createdAt = Instant.now(), + visibility = Visibility.PUBLIC, + url = URI.create("https://example.com"), + repostId = null, + replyId = null, + sensitive = false, + apId = URI.create("https://example.com"), + deleted = false, + mediaIds = emptyList(), + visibleActors = emptySet(), + hide = false, + moveTo = null, + actor = actor + ) + } + } + + @Test + fun `create actorがsuspedの時visibilityがpublicの登校はunlistedになる`() { + val actor = TestActorFactory.create(suspend = true) + + val post = Post.create( + id = PostId(1), + actorId = actor.id, + instanceId = actor.instance, + overview = null, + content = PostContent.empty, + createdAt = Instant.now(), + visibility = Visibility.PUBLIC, + url = URI.create("https://example.com"), + repostId = null, + replyId = null, + sensitive = false, + apId = URI.create("https://example.com"), + deleted = false, + mediaIds = emptyList(), + visibleActors = emptySet(), + hide = false, + moveTo = null, + actor = actor + ) + + assertEquals(Visibility.UNLISTED, post.visibility) + } + + @Test + fun `create actorがsuspedの時visibilityがunlistedの登校は変わらない`() { + val actor = TestActorFactory.create(suspend = true) + + val post = Post.create( + id = PostId(1), + actorId = actor.id, + instanceId = actor.instance, + overview = null, + content = PostContent.empty, + createdAt = Instant.now(), + visibility = Visibility.UNLISTED, + url = URI.create("https://example.com"), + repostId = null, + replyId = null, + sensitive = false, + apId = URI.create("https://example.com"), + deleted = false, + mediaIds = emptyList(), + visibleActors = emptySet(), + hide = false, + moveTo = null, + actor = actor + ) + + assertEquals(Visibility.UNLISTED, post.visibility) + } + + @Test + fun `create 作成できる`() { + val actor = TestActorFactory.create(suspend = true) + + + assertDoesNotThrow { + + Post.create( + id = PostId(1), + actorId = actor.id, + instanceId = actor.instance, + overview = null, + content = PostContent.empty, + createdAt = Instant.now(), + visibility = Visibility.PUBLIC, + url = URI.create("https://example.com"), + repostId = null, + replyId = null, + sensitive = false, + apId = URI.create("https://example.com"), + deleted = false, + mediaIds = emptyList(), + visibleActors = emptySet(), + hide = false, + moveTo = null, + actor = actor + ) + } + } + + @Test + fun `create 作成できる2`() { + val actor = TestActorFactory.create(suspend = true) + + + assertDoesNotThrow { + + Post.create( + id = PostId(1), + actorId = actor.id, + instanceId = actor.instance, + content = PostContent.empty, + createdAt = Instant.now(), + visibility = Visibility.PUBLIC, + url = URI.create("https://example.com"), + repostId = null, + replyId = null, + sensitive = false, + apId = URI.create("https://example.com"), + deleted = false, + mediaIds = emptyList(), + actor = actor + ) + } + } + + @Test + fun `emojiIds hideがtrueの時empty`() { + val actor = TestActorFactory.create() + val emojiIds = listOf(EmojiId(1), EmojiId(2)) + val post = Post.create( + id = PostId(1), + actorId = actor.id, + instanceId = actor.instance, + content = PostContent("aaa", "aaa", emojiIds), + createdAt = Instant.now(), + visibility = Visibility.PUBLIC, + url = URI.create("https://example.com"), + repostId = null, + replyId = null, + sensitive = false, + apId = URI.create("https://example.com"), + deleted = false, + mediaIds = emptyList(), + actor = actor, + hide = true + ) + + assertEquals(PostContent.empty.emojiIds, post.emojiIds) + + } + + @Test + fun `emojiIds hideがfalseの時中身が返される`() { + val actor = TestActorFactory.create() + val emojiIds = listOf(EmojiId(1), EmojiId(2)) + val post = Post.create( + id = PostId(1), + actorId = actor.id, + instanceId = actor.instance, + content = PostContent("aaa", "aaa", emojiIds), + createdAt = Instant.now(), + visibility = Visibility.PUBLIC, + url = URI.create("https://example.com"), + repostId = null, + replyId = null, + sensitive = false, + apId = URI.create("https://example.com"), + deleted = false, + mediaIds = emptyList(), + actor = actor, + hide = false + ) + + assertEquals(emojiIds, post.emojiIds) + } + + @Test + fun `reconstructWith 与えた引数で上書きされる`() { + val post = TestPostFactory.create() + val mediaIds = listOf(MediaId(1)) + val visibleActors = setOf((ActorId(2))) + val emojis = listOf(EmojiId(3)) + val reconstructWith = post.reconstructWith(mediaIds, emojis, visibleActors) + + assertEquals(mediaIds, reconstructWith.mediaIds) + assertEquals(visibleActors, reconstructWith.visibleActors) + assertEquals(emojis, reconstructWith.emojiIds) + } + + @Test + fun `mediaIds hideがtrueの時emptyが返される`() { + val actor = TestActorFactory.create() + val emojiIds = listOf(EmojiId(1), EmojiId(2)) + val mediaIds = listOf(MediaId(1)) + val post = Post.create( + id = PostId(1), + actorId = actor.id, + instanceId = actor.instance, + content = PostContent("aaa", "aaa", emojiIds), + createdAt = Instant.now(), + visibility = Visibility.PUBLIC, + url = URI.create("https://example.com"), + repostId = null, + replyId = null, + sensitive = false, + apId = URI.create("https://example.com"), + deleted = false, + mediaIds = mediaIds, + actor = actor, + hide = true + ) + + assertEquals(emptyList(), post.mediaIds) + + } + + @Test + fun `mediaIds hideがfalseの時中身が返される`() { + val actor = TestActorFactory.create() + val emojiIds = listOf(EmojiId(1), EmojiId(2)) + val mediaIds = listOf(MediaId(2)) + val post = Post.create( + id = PostId(1), + actorId = actor.id, + instanceId = actor.instance, + content = PostContent("aaa", "aaa", emojiIds), + createdAt = Instant.now(), + visibility = Visibility.PUBLIC, + url = URI.create("https://example.com"), + repostId = null, + replyId = null, + sensitive = false, + apId = URI.create("https://example.com"), + deleted = false, + mediaIds = mediaIds, + actor = actor, + hide = false + ) + + assertEquals(mediaIds, post.mediaIds) + } + + @Test + fun `delete deleteイベントが発生する`() { + + val actor = TestActorFactory.create() + val post = TestPostFactory.create(deleted = false, actorId = actor.id.id) + post.delete(actor) + assertContainsEvent(post, PostEvent.DELETE.eventName) + } + + @Test + fun `delete すでにdeletedがtrueの時deleteイベントは発生しない`() { + + val actor = TestActorFactory.create() + val post = TestPostFactory.create(deleted = true, actorId = actor.id.id) + post.delete(actor) + assertEmpty(post) + } + + @Test + fun `delete contentがemptyにoverviewがnullにmediaIdsがemptyになる`() { + val actor = TestActorFactory.create() + val post = TestPostFactory.create(deleted = false, actorId = actor.id.id) + post.delete(actor) + assertEquals(PostContent.empty, post.content) + assertNull(post.overview) + assertEquals(emptyList(), post.mediaIds) + assertTrue(post.deleted) + } + + @Test + fun `checkUpdate CHECKUPDATEイベントが発生する`() { + val post = TestPostFactory.create() + + post.checkUpdate() + + assertContainsEvent(post, PostEvent.CHECK_UPDATE.eventName) + } + + @Test + fun `restore 指定された引数で再構成されCHECKUPDATEイベントが発生する`() { + val post = TestPostFactory.create(deleted = true) + + val postContent = PostContent("aiueo", "aiueo", listOf(EmojiId(1))) + val overview = PostOverview("overview") + val mediaIds = listOf(MediaId(1)) + post.restore( + postContent, + overview, + mediaIds + ) + + assertContainsEvent(post, PostEvent.CHECK_UPDATE.eventName) + assertEquals(postContent, post.content) + assertEquals(overview, post.overview) + assertEquals(mediaIds, post.mediaIds) + } + + @Test + fun deletedがfalseの時失敗する() { + val post = TestPostFactory.create(deleted = false) + + val postContent = PostContent("aiueo", "aiueo", listOf(EmojiId(1))) + val overview = PostOverview("overview") + val mediaIds = listOf(MediaId(1)) + assertThrows { + post.restore( + postContent, + overview, + mediaIds + ) + } + } + + @Test + fun `addMediaIds deletedがtrueの時失敗する`() { + val post = TestPostFactory.create(deleted = true) + val actor = TestActorFactory.create(id = post.actorId.id) + + assertThrows { + post.addMediaIds(listOf(MediaId(1)), actor) + } + } + + @Test + fun `addMediaIds updateイベントが発生する`() { + val post = TestPostFactory.create(deleted = false) + val actor = TestActorFactory.create(id = post.actorId.id) + + post.addMediaIds(listOf(MediaId(2)), actor) + assertContainsEvent(post, PostEvent.UPDATE.eventName) + } + + @Test + fun `hide hideがtrueになる`() { + val post = TestPostFactory.create(hide = false) + + post.hide() + + assertTrue(post.hide) + } + + @Test + fun `show hideがfalseになる`() { + val post = TestPostFactory.create(hide = true) + + post.show() + + assertFalse(post.hide) + } + + @Test + fun `moveTo すでに設定されている場合は失敗する`() { + val post = TestPostFactory.create(moveTo = 100) + val actor = TestActorFactory.create(post.actorId.id) + + assertThrows { + post.moveTo(PostId(2), actor) + } + } + + @Test + fun `moveTo moveToが設定される`() { + val post = TestPostFactory.create(moveTo = null) + val actor = TestActorFactory.create(post.actorId.id) + + assertDoesNotThrow { + post.moveTo(PostId(2), actor) + } + + assertEquals(PostId(2), post.moveTo) + } +} diff --git a/hideout-core/src/test/kotlin/dev/usbharu/hideout/core/domain/model/post/TestPostFactory.kt b/hideout-core/src/test/kotlin/dev/usbharu/hideout/core/domain/model/post/TestPostFactory.kt new file mode 100644 index 00000000..388aaca0 --- /dev/null +++ b/hideout-core/src/test/kotlin/dev/usbharu/hideout/core/domain/model/post/TestPostFactory.kt @@ -0,0 +1,57 @@ +package dev.usbharu.hideout.core.domain.model.post + +import dev.usbharu.hideout.core.domain.model.actor.ActorId +import dev.usbharu.hideout.core.domain.model.instance.InstanceId +import dev.usbharu.hideout.core.domain.model.media.MediaId +import dev.usbharu.hideout.core.infrastructure.other.TwitterSnowflakeIdGenerateService +import kotlinx.coroutines.runBlocking +import java.net.URI +import java.time.Instant + +object TestPostFactory { + private val idGenerateService = TwitterSnowflakeIdGenerateService + + fun create( + id: Long = generateId(), + actorId: Long = 1, + instanceId: Long = 1, + overview: String? = null, + content: String = "This is test content", + createdAt: Instant = Instant.now(), + visibility: Visibility = Visibility.PUBLIC, + url: URI = URI.create("https://example.com/$actorId/posts/$id"), + repostId: Long? = null, + replyId: Long? = null, + sensitive: Boolean = false, + apId: URI = URI.create("https://example.com/$actorId/posts/$id"), + deleted: Boolean = false, + mediaIds: List = emptyList(), + visibleActors: List = emptyList(), + hide: Boolean = false, + moveTo: Long? = null, + ): Post { + return Post( + PostId(id), + ActorId(actorId), + instanceId = InstanceId(instanceId), + overview = overview?.let { PostOverview(it) }, + content = PostContent(content, content, emptyList()), + createdAt = createdAt, + visibility = visibility, + url = url, + repostId = repostId?.let { PostId(it) }, + replyId = replyId?.let { PostId(it) }, + sensitive = sensitive, + apId = apId, + deleted = deleted, + mediaIds = mediaIds.map { MediaId(it) }, + visibleActors = visibleActors.map { ActorId(it) }.toSet(), + hide = hide, + moveTo = moveTo?.let { PostId(it) } + ) + } + + private fun generateId(): Long = runBlocking { + idGenerateService.generateId() + } +} \ No newline at end of file diff --git a/hideout-core/src/test/kotlin/dev/usbharu/hideout/core/domain/model/support/domain/DomainTest.kt b/hideout-core/src/test/kotlin/dev/usbharu/hideout/core/domain/model/support/domain/DomainTest.kt new file mode 100644 index 00000000..861d2857 --- /dev/null +++ b/hideout-core/src/test/kotlin/dev/usbharu/hideout/core/domain/model/support/domain/DomainTest.kt @@ -0,0 +1,14 @@ +package dev.usbharu.hideout.core.domain.model.support.domain + +import org.junit.jupiter.api.Test +import org.junit.jupiter.api.assertThrows + + +class DomainTest { + @Test + fun `1000超過の長さは失敗`() { + assertThrows { + Domain("a".repeat(1001)) + } + } +} \ No newline at end of file diff --git a/hideout-core/src/test/kotlin/dev/usbharu/hideout/core/domain/service/actor/RemoteActorCheckDomainServiceTest.kt b/hideout-core/src/test/kotlin/dev/usbharu/hideout/core/domain/service/actor/RemoteActorCheckDomainServiceTest.kt new file mode 100644 index 00000000..0b7acd6d --- /dev/null +++ b/hideout-core/src/test/kotlin/dev/usbharu/hideout/core/domain/service/actor/RemoteActorCheckDomainServiceTest.kt @@ -0,0 +1,41 @@ +package dev.usbharu.hideout.core.domain.service.actor + +import dev.usbharu.hideout.core.config.ApplicationConfig +import dev.usbharu.hideout.core.domain.model.actor.ActorPublicKey +import dev.usbharu.hideout.core.domain.model.actor.TestActorFactory +import org.junit.jupiter.api.Test +import java.net.URI +import kotlin.test.assertFalse +import kotlin.test.assertTrue + +class RemoteActorCheckDomainServiceTest { + @Test + fun リモートのドメインならtrueを返す() { + val actor = TestActorFactory.create(publicKey = ActorPublicKey("")) + + val remoteActor = RemoteActorCheckDomainService( + ApplicationConfig( + URI.create("https://local.example.com").toURL() + ) + ).isRemoteActor( + actor + ) + + assertTrue(remoteActor) + } + + @Test + fun ローカルのActorならfalseを返す() { + val actor = TestActorFactory.create(domain = "local.example.com", publicKey = ActorPublicKey("")) + + val localActor = RemoteActorCheckDomainService( + ApplicationConfig( + URI.create("https://local.example.com").toURL() + ) + ).isRemoteActor( + actor + ) + + assertFalse(localActor) + } +} \ No newline at end of file diff --git a/hideout-core/src/test/kotlin/dev/usbharu/hideout/core/domain/service/actor/local/LocalActorDomainServiceImplTest.kt b/hideout-core/src/test/kotlin/dev/usbharu/hideout/core/domain/service/actor/local/LocalActorDomainServiceImplTest.kt new file mode 100644 index 00000000..a5fcf1a1 --- /dev/null +++ b/hideout-core/src/test/kotlin/dev/usbharu/hideout/core/domain/service/actor/local/LocalActorDomainServiceImplTest.kt @@ -0,0 +1,51 @@ +package dev.usbharu.hideout.core.domain.service.actor.local + +import dev.usbharu.hideout.core.config.ApplicationConfig +import dev.usbharu.hideout.core.domain.model.actor.ActorRepository +import dev.usbharu.hideout.core.domain.model.actor.TestActorFactory +import kotlinx.coroutines.test.runTest +import org.junit.jupiter.api.Assertions.assertFalse +import org.junit.jupiter.api.Assertions.assertTrue +import org.junit.jupiter.api.Test +import org.junit.jupiter.api.extension.ExtendWith +import org.mockito.InjectMocks +import org.mockito.Mock +import org.mockito.Spy +import org.mockito.junit.jupiter.MockitoExtension +import org.mockito.kotlin.doReturn +import org.mockito.kotlin.eq +import org.mockito.kotlin.whenever +import java.net.URL + +@ExtendWith(MockitoExtension::class) +class LocalActorDomainServiceImplTest { + @InjectMocks + lateinit var service: LocalActorDomainServiceImpl + + @Mock + lateinit var actorRepository: ActorRepository + + @Spy + val applicationConfig = ApplicationConfig(URL("http://example.com")) + + @Test + fun findByNameAndDomainがnullならfalse() = runTest { + val actual = service.usernameAlreadyUse("test") + + assertFalse(actual) + } + + @Test + fun findByNameAndDomainがnullならtrue() = runTest { + whenever(actorRepository.findByNameAndDomain(eq("test"), eq("example.com"))).doReturn(TestActorFactory.create()) + + val actual = service.usernameAlreadyUse("test") + + assertTrue(actual) + } + + @Test + fun generateKeyPair() = runTest { + service.generateKeyPair() + } +} \ No newline at end of file diff --git a/hideout-core/src/test/kotlin/dev/usbharu/hideout/core/domain/service/actor/local/LocalActorMigrationCheckDomainServiceImplTest.kt b/hideout-core/src/test/kotlin/dev/usbharu/hideout/core/domain/service/actor/local/LocalActorMigrationCheckDomainServiceImplTest.kt new file mode 100644 index 00000000..3617fae9 --- /dev/null +++ b/hideout-core/src/test/kotlin/dev/usbharu/hideout/core/domain/service/actor/local/LocalActorMigrationCheckDomainServiceImplTest.kt @@ -0,0 +1,126 @@ +package dev.usbharu.hideout.core.domain.service.actor.local + +import dev.usbharu.hideout.core.domain.model.actor.ActorId +import dev.usbharu.hideout.core.domain.model.actor.TestActorFactory +import dev.usbharu.hideout.core.domain.model.userdetails.UserDetail +import dev.usbharu.hideout.core.domain.model.userdetails.UserDetailHashedPassword +import dev.usbharu.hideout.core.domain.model.userdetails.UserDetailId +import kotlinx.coroutines.test.runTest +import org.junit.jupiter.api.Assertions.assertInstanceOf +import org.junit.jupiter.api.Test +import java.time.Instant +import kotlin.time.Duration.Companion.days +import kotlin.time.toJavaDuration + +class LocalActorMigrationCheckDomainServiceImplTest { + + @Test + fun 最終お引越しから30日以内だと失敗() = runTest { + val from = TestActorFactory.create() + val to = TestActorFactory.create() + val userDetail = UserDetail.create( + UserDetailId(1), + ActorId(1), UserDetailHashedPassword("") + ) + userDetail.lastMigration = Instant.now().minusSeconds(100) + + val localActorMigrationCheckDomainServiceImpl = LocalActorMigrationCheckDomainServiceImpl() + + val canAccountMigration = localActorMigrationCheckDomainServiceImpl.canAccountMigration(userDetail, to, from) + + assertInstanceOf(AccountMigrationCheck.MigrationCoolDown::class.java, canAccountMigration) + } + + @Test + fun 自分自身に引っ越しできない(): Unit = runTest { + + val from = TestActorFactory.create() + val to = TestActorFactory.create() + val userDetail = UserDetail.create( + UserDetailId(1), + ActorId(1), UserDetailHashedPassword("") + ) + val localActorMigrationCheckDomainServiceImpl = LocalActorMigrationCheckDomainServiceImpl() + + val canAccountMigration = localActorMigrationCheckDomainServiceImpl.canAccountMigration(userDetail, from, from) + + assertInstanceOf(AccountMigrationCheck.SelfReferences::class.java, canAccountMigration) + } + + @Test + fun 引越し先が引っ越している場合は引っ越しできない(): Unit = runTest { + + val from = TestActorFactory.create() + val to = TestActorFactory.create(moveTo = 100) + val userDetail = UserDetail.create( + UserDetailId(1), + ActorId(1), UserDetailHashedPassword("") + ) + val localActorMigrationCheckDomainServiceImpl = LocalActorMigrationCheckDomainServiceImpl() + + val canAccountMigration = localActorMigrationCheckDomainServiceImpl.canAccountMigration(userDetail, from, to) + + assertInstanceOf(AccountMigrationCheck.AlreadyMoved::class.java, canAccountMigration) + } + + @Test + fun 自分自身が引っ越している場合は引っ越しできない() = runTest { + val from = TestActorFactory.create(moveTo = 100) + val to = TestActorFactory.create() + val userDetail = UserDetail.create( + UserDetailId(1), + ActorId(1), UserDetailHashedPassword("") + ) + val localActorMigrationCheckDomainServiceImpl = LocalActorMigrationCheckDomainServiceImpl() + + val canAccountMigration = localActorMigrationCheckDomainServiceImpl.canAccountMigration(userDetail, from, to) + + assertInstanceOf(AccountMigrationCheck.AlreadyMoved::class.java, canAccountMigration) + } + + @Test + fun 引越し先のalsoKnownAsに引越し元が含まれてない場合失敗する() = runTest { + val from = TestActorFactory.create() + val to = TestActorFactory.create(alsoKnownAs = setOf(ActorId(100))) + val userDetail = UserDetail.create( + UserDetailId(1), + ActorId(1), UserDetailHashedPassword("") + ) + val localActorMigrationCheckDomainServiceImpl = LocalActorMigrationCheckDomainServiceImpl() + + val canAccountMigration = localActorMigrationCheckDomainServiceImpl.canAccountMigration(userDetail, from, to) + + assertInstanceOf(AccountMigrationCheck.AlsoKnownAsNotFound::class.java, canAccountMigration) + } + + @Test + fun 正常に設定されている場合は成功する() = runTest { + val from = TestActorFactory.create() + val to = TestActorFactory.create(alsoKnownAs = setOf(from.id, ActorId(100))) + val userDetail = UserDetail.create( + UserDetailId(1), + ActorId(1), UserDetailHashedPassword("") + ) + val localActorMigrationCheckDomainServiceImpl = LocalActorMigrationCheckDomainServiceImpl() + + val canAccountMigration = localActorMigrationCheckDomainServiceImpl.canAccountMigration(userDetail, from, to) + + assertInstanceOf(AccountMigrationCheck.CanAccountMigration::class.java, canAccountMigration) + } + + @Test + fun お引越し履歴があっても30日以上経っていたら成功する() = runTest { + val from = TestActorFactory.create() + val to = TestActorFactory.create(alsoKnownAs = setOf(from.id, ActorId(100))) + val userDetail = UserDetail.create( + UserDetailId(1), + ActorId(1), UserDetailHashedPassword("") + ) + userDetail.lastMigration = Instant.now().minus(31.days.toJavaDuration()) + val localActorMigrationCheckDomainServiceImpl = LocalActorMigrationCheckDomainServiceImpl() + + val canAccountMigration = localActorMigrationCheckDomainServiceImpl.canAccountMigration(userDetail, from, to) + + assertInstanceOf(AccountMigrationCheck.CanAccountMigration::class.java, canAccountMigration) + } +} \ No newline at end of file diff --git a/hideout-core/src/test/kotlin/dev/usbharu/hideout/core/domain/service/post/DefaultPostReadAccessControlTest.kt b/hideout-core/src/test/kotlin/dev/usbharu/hideout/core/domain/service/post/DefaultPostReadAccessControlTest.kt new file mode 100644 index 00000000..2826cb58 --- /dev/null +++ b/hideout-core/src/test/kotlin/dev/usbharu/hideout/core/domain/service/post/DefaultPostReadAccessControlTest.kt @@ -0,0 +1,158 @@ +package dev.usbharu.hideout.core.domain.service.post + +import dev.usbharu.hideout.core.domain.model.actor.ActorId +import dev.usbharu.hideout.core.domain.model.post.TestPostFactory +import dev.usbharu.hideout.core.domain.model.post.Visibility +import dev.usbharu.hideout.core.domain.model.relationship.Relationship +import dev.usbharu.hideout.core.domain.model.relationship.RelationshipRepository +import dev.usbharu.hideout.core.domain.model.support.acct.Acct +import dev.usbharu.hideout.core.domain.model.support.principal.Anonymous +import dev.usbharu.hideout.core.domain.model.support.principal.FromApi +import dev.usbharu.hideout.core.domain.model.userdetails.UserDetailId +import kotlinx.coroutines.test.runTest +import org.junit.jupiter.api.Assertions.assertFalse +import org.junit.jupiter.api.Assertions.assertTrue +import org.junit.jupiter.api.Test +import org.junit.jupiter.api.extension.ExtendWith +import org.mockito.InjectMocks +import org.mockito.Mock +import org.mockito.junit.jupiter.MockitoExtension +import org.mockito.kotlin.doReturn +import org.mockito.kotlin.whenever + +@ExtendWith(MockitoExtension::class) +class DefaultPostReadAccessControlTest { + @InjectMocks + lateinit var service: DefaultPostReadAccessControl + + @Mock + lateinit var relationshipRepository: RelationshipRepository + + @Test + fun ブロックされてたら見れない() = runTest { + whenever(relationshipRepository.findByActorIdAndTargetId(ActorId(1), ActorId(2))).doReturn( + Relationship( + actorId = ActorId(1), + targetActorId = ActorId(2), + following = false, + blocking = true, + muting = false, + followRequesting = false, + mutingFollowRequest = false, + ) + ) + + val actual = service.isAllow( + TestPostFactory.create(actorId = 1), + FromApi(ActorId(2), UserDetailId(2), Acct("test", "example.com")) + ) + + assertFalse(actual) + } + + @Test + fun PublicかUnlistedなら見れる() = runTest { + val actual = service.isAllow(TestPostFactory.create(visibility = Visibility.PUBLIC), Anonymous) + assertTrue(actual) + + val actual2 = service.isAllow(TestPostFactory.create(visibility = Visibility.UNLISTED), Anonymous) + assertTrue(actual2) + } + + @Test + fun FollowersかDirecのときAnonymousなら見れない() = runTest { + val actual = service.isAllow(TestPostFactory.create(visibility = Visibility.FOLLOWERS), Anonymous) + assertFalse(actual) + + val actual2 = service.isAllow(TestPostFactory.create(visibility = Visibility.DIRECT), Anonymous) + assertFalse(actual2) + } + + @Test + fun DirectでvisibleActorsに含まれていたら見れる() = runTest { + val actual = service.isAllow( + TestPostFactory.create(actorId = 1, visibility = Visibility.DIRECT, visibleActors = listOf(2)), + FromApi(ActorId(2), UserDetailId(2), Acct("test", "example.com")) + ) + + assertTrue(actual) + } + + @Test + fun DirectでvisibleActorsに含まれていなかったら見れない() = runTest { + val actual = service.isAllow( + TestPostFactory.create(actorId = 1, visibility = Visibility.DIRECT, visibleActors = listOf(3)), + FromApi(ActorId(2), UserDetailId(2), Acct("test", "example.com")) + ) + + assertFalse(actual) + } + + @Test + fun Followersでフォロワーなら見れる() = runTest { + whenever(relationshipRepository.findByActorIdAndTargetId(ActorId(1), ActorId(2))).doReturn( + Relationship.default( + actorId = ActorId(1), + targetActorId = ActorId(2) + ) + ) + whenever(relationshipRepository.findByActorIdAndTargetId(ActorId(2), ActorId(1))).doReturn( + Relationship( + actorId = ActorId(2), + targetActorId = ActorId(1), + following = true, + blocking = false, + muting = false, + followRequesting = false, + mutingFollowRequest = false + ) + ) + + + val actual = service.isAllow( + TestPostFactory.create(actorId = 1, visibility = Visibility.FOLLOWERS), + FromApi(ActorId(2), UserDetailId(2), Acct("test", "example.com")) + ) + + assertTrue(actual) + } + + @Test + fun relationshipが見つからない場合見れない() = runTest { + val actual = service.isAllow( + TestPostFactory.create(actorId = 1, visibility = Visibility.FOLLOWERS), + FromApi(ActorId(2), UserDetailId(2), Acct("test", "example.com")) + ) + + assertFalse(actual) + } + + @Test + fun フォロワーじゃない場合は見れない() = runTest { + whenever(relationshipRepository.findByActorIdAndTargetId(ActorId(1), ActorId(2))).doReturn( + Relationship.default( + actorId = ActorId(1), + targetActorId = ActorId(2) + ) + ) + whenever(relationshipRepository.findByActorIdAndTargetId(ActorId(2), ActorId(1))).doReturn( + Relationship( + actorId = ActorId(2), + targetActorId = ActorId(1), + following = false, + blocking = false, + muting = false, + followRequesting = false, + mutingFollowRequest = false + ) + ) + + + val actual = service.isAllow( + TestPostFactory.create(actorId = 1, visibility = Visibility.FOLLOWERS), + FromApi(ActorId(2), UserDetailId(2), Acct("test", "example.com")) + ) + + assertFalse(actual) + } +} \ No newline at end of file diff --git a/hideout-core/src/test/kotlin/dev/usbharu/hideout/core/infrastructure/other/DefaultPostContentFormatterTest.kt b/hideout-core/src/test/kotlin/dev/usbharu/hideout/core/infrastructure/other/DefaultPostContentFormatterTest.kt new file mode 100644 index 00000000..91c6413f --- /dev/null +++ b/hideout-core/src/test/kotlin/dev/usbharu/hideout/core/infrastructure/other/DefaultPostContentFormatterTest.kt @@ -0,0 +1,75 @@ +package dev.usbharu.hideout.core.infrastructure.other + +import dev.usbharu.hideout.core.config.HtmlSanitizeConfig +import dev.usbharu.hideout.core.domain.service.post.FormattedPostContent +import org.junit.jupiter.api.Test +import org.junit.jupiter.api.extension.ExtendWith +import org.mockito.InjectMocks +import org.mockito.Spy +import org.mockito.junit.jupiter.MockitoExtension +import kotlin.test.assertEquals + +@ExtendWith(MockitoExtension::class) +class DefaultPostContentFormatterTest { + @InjectMocks + lateinit var formatter: DefaultPostContentFormatter + + @Spy + val policyFactory = HtmlSanitizeConfig().policy() + + @Test + fun 文字だけのHTMLをPで囲む() { + val actual = formatter.format("a") + + assertEquals(FormattedPostContent("

a

", "a"), actual) + } + + @Test + fun エレメントはそのまま() { + val actual = formatter.format("

a

") + + assertEquals(FormattedPostContent("

a

", "a"), actual) + } + + @Test + fun コメントは無視() { + val actual = formatter.format("") + + assertEquals(FormattedPostContent("", ""), actual) + } + + @Test + fun brタグを改行に() { + val actual = formatter.format("

a

") + + assertEquals(FormattedPostContent("

a

", "a\n"), actual) + } + + @Test + fun brタグ2連続を段落に() { + val format = formatter.format("

a

a

") + + assertEquals(FormattedPostContent("

a

a

", "a\n\na"), format) + } + + @Test + fun brタグ3連続以上を段落にして改行2つに変換() { + val format = formatter.format("

a


a

") + + assertEquals(FormattedPostContent("

a

a

", "a\n\na"), format) + } + + @Test + fun aタグは許可される() { + val format = formatter.format("p") + + assertEquals(FormattedPostContent("p", "p"), format) + } + + @Test + fun pの中のaタグも許可される() { + val actual = formatter.format("

a

") + + assertEquals(FormattedPostContent("

a

", "a"), actual) + } +} \ No newline at end of file diff --git a/hideout-core/src/test/kotlin/dev/usbharu/hideout/core/infrastructure/other/TwitterSnowflakeIdGenerateServiceTest.kt b/hideout-core/src/test/kotlin/dev/usbharu/hideout/core/infrastructure/other/TwitterSnowflakeIdGenerateServiceTest.kt new file mode 100644 index 00000000..98855ca8 --- /dev/null +++ b/hideout-core/src/test/kotlin/dev/usbharu/hideout/core/infrastructure/other/TwitterSnowflakeIdGenerateServiceTest.kt @@ -0,0 +1,30 @@ +package dev.usbharu.hideout.core.infrastructure.other + +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.coroutineScope +import kotlinx.coroutines.launch +import kotlinx.coroutines.runBlocking +import kotlinx.coroutines.sync.Mutex +import kotlinx.coroutines.sync.withLock +import org.junit.jupiter.api.Assertions.assertEquals +import org.junit.jupiter.api.Test + +class TwitterSnowflakeIdGenerateServiceTest { + @Test + fun noDuplicateTest() = runBlocking { + val mutex = Mutex() + val mutableListOf = mutableListOf() + coroutineScope { + repeat(500000) { + launch(Dispatchers.IO) { + val id = TwitterSnowflakeIdGenerateService.generateId() + mutex.withLock { + mutableListOf.add(id) + } + } + } + } + + assertEquals(0, mutableListOf.size - mutableListOf.toSet().size) + } +} \ No newline at end of file diff --git a/hideout-core/src/test/kotlin/dev/usbharu/hideout/core/infrastructure/springframework/oauth2/UserDetailsServiceImplTest.kt b/hideout-core/src/test/kotlin/dev/usbharu/hideout/core/infrastructure/springframework/oauth2/UserDetailsServiceImplTest.kt new file mode 100644 index 00000000..bb253ef6 --- /dev/null +++ b/hideout-core/src/test/kotlin/dev/usbharu/hideout/core/infrastructure/springframework/oauth2/UserDetailsServiceImplTest.kt @@ -0,0 +1,91 @@ +package dev.usbharu.hideout.core.infrastructure.springframework.oauth2 + +import dev.usbharu.hideout.core.config.ApplicationConfig +import dev.usbharu.hideout.core.domain.model.actor.ActorId +import dev.usbharu.hideout.core.domain.model.actor.ActorRepository +import dev.usbharu.hideout.core.domain.model.actor.TestActorFactory +import dev.usbharu.hideout.core.domain.model.userdetails.UserDetail +import dev.usbharu.hideout.core.domain.model.userdetails.UserDetailHashedPassword +import dev.usbharu.hideout.core.domain.model.userdetails.UserDetailId +import dev.usbharu.hideout.core.domain.model.userdetails.UserDetailRepository +import kotlinx.coroutines.test.runTest +import org.junit.jupiter.api.Test +import org.junit.jupiter.api.assertThrows +import org.junit.jupiter.api.extension.ExtendWith +import org.mockito.InjectMocks +import org.mockito.Mock +import org.mockito.Spy +import org.mockito.junit.jupiter.MockitoExtension +import org.mockito.kotlin.* +import org.springframework.security.core.userdetails.UsernameNotFoundException +import utils.TestTransaction +import java.net.URL +import kotlin.test.assertEquals + +@ExtendWith(MockitoExtension::class) +class UserDetailsServiceImplTest { + @InjectMocks + lateinit var service: UserDetailsServiceImpl + + @Mock + lateinit var actorRepository: ActorRepository + + @Mock + lateinit var userDetailRepository: UserDetailRepository + + @Spy + val applicationConfig = ApplicationConfig(URL("http://example.com")) + + @Spy + val transaction = TestTransaction + + @Test + fun usernameがnullなら失敗() = runTest { + assertThrows { + service.loadUserByUsername(null) + } + verify(actorRepository, never()).findByNameAndDomain(any(), any()) + } + + @Test + fun actorが見つからない場合失敗() = runTest { + assertThrows { + service.loadUserByUsername("test") + } + verify(actorRepository, times(1)).findByNameAndDomain(eq("test"), eq("example.com")) + } + + @Test + fun userDetailが見つからない場合失敗() = runTest { + whenever(actorRepository.findByNameAndDomain(eq("test"), eq("example.com"))).doReturn( + TestActorFactory.create( + actorName = "test", id = 1 + ) + ) + assertThrows { + service.loadUserByUsername("test") + } + verify(actorRepository, times(1)).findByNameAndDomain(eq("test"), eq("example.com")) + verify(userDetailRepository, times(1)).findByActorId(eq(1)) + } + + @Test + fun 全部見つかったら成功() = runTest { + whenever( + actorRepository.findByNameAndDomain( + eq("test"), + eq("example.com") + ) + ).doReturn(TestActorFactory.create(id = 1)) + whenever(userDetailRepository.findByActorId(eq(1))).doReturn( + UserDetail.create( + UserDetailId(1), + ActorId(1), UserDetailHashedPassword("") + ) + ) + + val actual = service.loadUserByUsername("test") + + assertEquals(HideoutUserDetails(HashSet(), "", "test-1", 1), actual) + } +} \ No newline at end of file diff --git a/hideout-core/src/test/kotlin/dev/usbharu/hideout/core/infrastructure/timeline/DefaultTimelineStoreTest.kt b/hideout-core/src/test/kotlin/dev/usbharu/hideout/core/infrastructure/timeline/DefaultTimelineStoreTest.kt new file mode 100644 index 00000000..ca243c63 --- /dev/null +++ b/hideout-core/src/test/kotlin/dev/usbharu/hideout/core/infrastructure/timeline/DefaultTimelineStoreTest.kt @@ -0,0 +1,494 @@ +package dev.usbharu.hideout.core.infrastructure.timeline + +import dev.usbharu.hideout.core.config.DefaultTimelineStoreConfig +import dev.usbharu.hideout.core.domain.model.actor.ActorRepository +import dev.usbharu.hideout.core.domain.model.filter.* +import dev.usbharu.hideout.core.domain.model.post.PostRepository +import dev.usbharu.hideout.core.domain.model.post.TestPostFactory +import dev.usbharu.hideout.core.domain.model.post.Visibility +import dev.usbharu.hideout.core.domain.model.timeline.* +import dev.usbharu.hideout.core.domain.model.timelineobject.TimelineObject +import dev.usbharu.hideout.core.domain.model.timelineobject.TimelineObjectWarnFilter +import dev.usbharu.hideout.core.domain.model.timelinerelationship.TimelineRelationship +import dev.usbharu.hideout.core.domain.model.timelinerelationship.TimelineRelationshipId +import dev.usbharu.hideout.core.domain.model.timelinerelationship.TimelineRelationshipRepository +import dev.usbharu.hideout.core.domain.model.timelinerelationship.Visible +import dev.usbharu.hideout.core.domain.model.userdetails.UserDetailId +import dev.usbharu.hideout.core.domain.model.userdetails.UserDetailRepository +import dev.usbharu.hideout.core.domain.service.filter.FilterDomainService +import dev.usbharu.hideout.core.infrastructure.other.TwitterSnowflakeIdGenerateService +import kotlinx.coroutines.test.runTest +import org.assertj.core.api.Assertions.assertThat +import org.junit.jupiter.api.Test +import org.junit.jupiter.api.extension.ExtendWith +import org.mockito.InjectMocks +import org.mockito.Mock +import org.mockito.Spy +import org.mockito.junit.jupiter.MockitoExtension +import org.mockito.kotlin.* + +@ExtendWith(MockitoExtension::class) +class DefaultTimelineStoreTest { + @InjectMocks + lateinit var timelineStore: DefaultTimelineStore + + @Mock + lateinit var timelineRepository: TimelineRepository + + @Mock + lateinit var timelineRelationshipRepository: TimelineRelationshipRepository + + @Mock + lateinit var filterRepository: FilterRepository + + @Mock + lateinit var postRepository: PostRepository + + @Mock + lateinit var filterDomainService: FilterDomainService + + @Mock + lateinit var internalTimelineObjectRepository: InternalTimelineObjectRepository + + @Mock + lateinit var userDetailRepository: UserDetailRepository + + @Mock + lateinit var actorRepository: ActorRepository + + @Spy + val defaultTimelineStoreConfig = DefaultTimelineStoreConfig(500) + + @Spy + val idGenerateService = TwitterSnowflakeIdGenerateService + + @Test + fun addPost() = runTest { + val post = TestPostFactory.create() + whenever(timelineRelationshipRepository.findByActorId(post.actorId)).doReturn( + listOf( + TimelineRelationship( + TimelineRelationshipId(1), + TimelineId(12), + post.actorId, + Visible.PUBLIC + ) + ) + ) + + whenever(timelineRepository.findByIds(listOf(TimelineId(12)))).doReturn( + listOf( + Timeline( + id = TimelineId(12), + userDetailId = UserDetailId(post.actorId.id), + name = TimelineName("timeline"), + visibility = TimelineVisibility.PUBLIC, + isSystem = false + ) + ) + ) + + val filters = listOf( + Filter( + id = FilterId(13), + userDetailId = UserDetailId(post.actorId.id), + name = FilterName("filter"), + filterContext = setOf(FilterContext.HOME), + filterAction = FilterAction.HIDE, + filterKeywords = setOf( + FilterKeyword(FilterKeywordId(14), FilterKeywordKeyword("aa"), FilterMode.NONE) + ) + ) + ) + + whenever(filterRepository.findByUserDetailId(UserDetailId(post.actorId.id))).doReturn(filters) + + whenever(filterDomainService.apply(post, FilterContext.HOME, filters)).doReturn( + FilteredPost( + post, listOf( + FilterResult(filters.first(), "aaa") + ) + ) + ) + + timelineStore.addPost(post) + + argumentCaptor> { + verify(internalTimelineObjectRepository, times(1)).saveAll(capture()) + val timelineObjectList = allValues.first() + + assertThat(timelineObjectList).allSatisfy { + assertThat(it.postId).isEqualTo(post.id) + assertThat(it.postActorId).isEqualTo(post.actorId) + assertThat(it.replyId).isNull() + assertThat(it.replyActorId).isNull() + assertThat(it.repostId).isNull() + assertThat(it.repostActorId).isNull() + + assertThat(it.userDetailId).isEqualTo(UserDetailId(post.actorId.id)) + assertThat(it.timelineId).isEqualTo(TimelineId(12)) + assertThat(it.warnFilters).contains(TimelineObjectWarnFilter(FilterId(13), "aaa")) + } + } + } + + @Test + fun `addPost direct投稿は追加されない`() = runTest { + val post = TestPostFactory.create(visibility = Visibility.DIRECT) + whenever(timelineRelationshipRepository.findByActorId(post.actorId)).doReturn( + listOf( + TimelineRelationship( + TimelineRelationshipId(1), + TimelineId(12), + post.actorId, + Visible.PUBLIC + ) + ) + ) + + whenever(timelineRepository.findByIds(listOf(TimelineId(12)))).doReturn( + listOf( + Timeline( + id = TimelineId(12), + userDetailId = UserDetailId(post.actorId.id), + name = TimelineName("timeline"), + visibility = TimelineVisibility.PUBLIC, + isSystem = false + ) + ) + ) + + timelineStore.addPost(post) + + argumentCaptor> { + verify(internalTimelineObjectRepository, times(1)).saveAll(capture()) + val timelineObjectList = allValues.first() + + assertThat(timelineObjectList).isEmpty() + } + } + + @Test + fun timelineがpublicでpostがUNLISTEDの時追加されない() = runTest { + val post = TestPostFactory.create(visibility = Visibility.UNLISTED) + whenever(timelineRelationshipRepository.findByActorId(post.actorId)).doReturn( + listOf( + TimelineRelationship( + TimelineRelationshipId(1), + TimelineId(12), + post.actorId, + Visible.PUBLIC + ) + ) + ) + + whenever(timelineRepository.findByIds(listOf(TimelineId(12)))).doReturn( + listOf( + Timeline( + id = TimelineId(12), + userDetailId = UserDetailId(post.actorId.id), + name = TimelineName("timeline"), + visibility = TimelineVisibility.PUBLIC, + isSystem = false + ) + ) + ) + + timelineStore.addPost(post) + + argumentCaptor> { + verify(internalTimelineObjectRepository, times(1)).saveAll(capture()) + val timelineObjectList = allValues.first() + + assertThat(timelineObjectList).isEmpty() + } + } + + @Test + fun timelineがpublicでpostがFOLLOWERSの時追加されない() = runTest { + val post = TestPostFactory.create(visibility = Visibility.FOLLOWERS) + whenever(timelineRelationshipRepository.findByActorId(post.actorId)).doReturn( + listOf( + TimelineRelationship( + TimelineRelationshipId(1), + TimelineId(12), + post.actorId, + Visible.PUBLIC + ) + ) + ) + + whenever(timelineRepository.findByIds(listOf(TimelineId(12)))).doReturn( + listOf( + Timeline( + id = TimelineId(12), + userDetailId = UserDetailId(post.actorId.id), + name = TimelineName("timeline"), + visibility = TimelineVisibility.PUBLIC, + isSystem = false + ) + ) + ) + + timelineStore.addPost(post) + + argumentCaptor> { + verify(internalTimelineObjectRepository, times(1)).saveAll(capture()) + val timelineObjectList = allValues.first() + + assertThat(timelineObjectList).isEmpty() + } + } + + @Test + fun timelineがUNLISTEDでpostがFOLLOWERSの時追加されない() = runTest { + val post = TestPostFactory.create(visibility = Visibility.FOLLOWERS) + whenever(timelineRelationshipRepository.findByActorId(post.actorId)).doReturn( + listOf( + TimelineRelationship( + TimelineRelationshipId(1), + TimelineId(12), + post.actorId, + Visible.PUBLIC + ) + ) + ) + + whenever(timelineRepository.findByIds(listOf(TimelineId(12)))).doReturn( + listOf( + Timeline( + id = TimelineId(12), + userDetailId = UserDetailId(post.actorId.id), + name = TimelineName("timeline"), + visibility = TimelineVisibility.UNLISTED, + isSystem = false + ) + ) + ) + + timelineStore.addPost(post) + + argumentCaptor> { + verify(internalTimelineObjectRepository, times(1)).saveAll(capture()) + val timelineObjectList = allValues.first() + + assertThat(timelineObjectList).isEmpty() + } + } + + @Test + fun timelineがPRIVATEでpostがFOLLOWERSの時追加される() = runTest { + val post = TestPostFactory.create(visibility = Visibility.FOLLOWERS) + whenever(timelineRelationshipRepository.findByActorId(post.actorId)).doReturn( + listOf( + TimelineRelationship( + TimelineRelationshipId(1), + TimelineId(12), + post.actorId, + Visible.PUBLIC + ) + ) + ) + + whenever(timelineRepository.findByIds(listOf(TimelineId(12)))).doReturn( + listOf( + Timeline( + id = TimelineId(12), + userDetailId = UserDetailId(post.actorId.id), + name = TimelineName("timeline"), + visibility = TimelineVisibility.PRIVATE, + isSystem = false + ) + ) + ) + + val filters = listOf( + Filter( + id = FilterId(13), + userDetailId = UserDetailId(post.actorId.id), + name = FilterName("filter"), + filterContext = setOf(FilterContext.HOME), + filterAction = FilterAction.HIDE, + filterKeywords = setOf( + FilterKeyword(FilterKeywordId(14), FilterKeywordKeyword("aa"), FilterMode.NONE) + ) + ) + ) + + whenever(filterRepository.findByUserDetailId(UserDetailId(post.actorId.id))).doReturn(filters) + + whenever(filterDomainService.apply(post, FilterContext.HOME, filters)).doReturn( + FilteredPost( + post, listOf( + FilterResult(filters.first(), "aaa") + ) + ) + ) + + timelineStore.addPost(post) + + argumentCaptor> { + verify(internalTimelineObjectRepository, times(1)).saveAll(capture()) + val timelineObjectList = allValues.first() + + assertThat(timelineObjectList).allSatisfy { + assertThat(it.postId).isEqualTo(post.id) + assertThat(it.postActorId).isEqualTo(post.actorId) + assertThat(it.replyId).isNull() + assertThat(it.replyActorId).isNull() + assertThat(it.repostId).isNull() + assertThat(it.repostActorId).isNull() + + assertThat(it.userDetailId).isEqualTo(UserDetailId(post.actorId.id)) + assertThat(it.timelineId).isEqualTo(TimelineId(12)) + assertThat(it.warnFilters).contains(TimelineObjectWarnFilter(FilterId(13), "aaa")) + } + } + } + + @Test + fun repostがあるときはRepostを含めたTimelineObjectが作成される() = runTest { + val repost = TestPostFactory.create() + val post = TestPostFactory.create(repostId = repost.id.id) + + whenever(postRepository.findById(repost.id)).doReturn(repost) + whenever(timelineRelationshipRepository.findByActorId(post.actorId)).doReturn( + listOf( + TimelineRelationship( + TimelineRelationshipId(1), + TimelineId(12), + post.actorId, + Visible.PUBLIC + ) + ) + ) + + whenever(timelineRepository.findByIds(listOf(TimelineId(12)))).doReturn( + listOf( + Timeline( + id = TimelineId(12), + userDetailId = UserDetailId(post.actorId.id), + name = TimelineName("timeline"), + visibility = TimelineVisibility.PUBLIC, + isSystem = false + ) + ) + ) + + val filters = listOf( + Filter( + id = FilterId(13), + userDetailId = UserDetailId(post.actorId.id), + name = FilterName("filter"), + filterContext = setOf(FilterContext.HOME), + filterAction = FilterAction.HIDE, + filterKeywords = setOf( + FilterKeyword(FilterKeywordId(14), FilterKeywordKeyword("aa"), FilterMode.NONE) + ) + ) + ) + + whenever(filterRepository.findByUserDetailId(UserDetailId(post.actorId.id))).doReturn(filters) + + whenever(filterDomainService.apply(post, FilterContext.HOME, filters)).doReturn( + FilteredPost( + post, listOf( + FilterResult(filters.first(), "aaa") + ) + ) + ) + + timelineStore.addPost(post) + + argumentCaptor> { + verify(internalTimelineObjectRepository, times(1)).saveAll(capture()) + val timelineObjectList = allValues.first() + + assertThat(timelineObjectList).allSatisfy { + assertThat(it.postId).isEqualTo(post.id) + assertThat(it.postActorId).isEqualTo(post.actorId) + assertThat(it.replyId).isNull() + assertThat(it.replyActorId).isNull() + assertThat(it.repostId).isEqualTo(repost.id) + assertThat(it.repostActorId).isEqualTo(repost.actorId) + + assertThat(it.userDetailId).isEqualTo(UserDetailId(post.actorId.id)) + assertThat(it.timelineId).isEqualTo(TimelineId(12)) + assertThat(it.warnFilters).contains(TimelineObjectWarnFilter(FilterId(13), "aaa")) + } + } + } + + @Test + fun replyがあるときはReplyを含めたTimeineObjectが作成される() = runTest { + val reply = TestPostFactory.create() + val post = TestPostFactory.create(replyId = reply.id.id) + + whenever(postRepository.findById(reply.id)).doReturn(reply) + whenever(timelineRelationshipRepository.findByActorId(post.actorId)).doReturn( + listOf( + TimelineRelationship( + TimelineRelationshipId(1), + TimelineId(12), + post.actorId, + Visible.PUBLIC + ) + ) + ) + + whenever(timelineRepository.findByIds(listOf(TimelineId(12)))).doReturn( + listOf( + Timeline( + id = TimelineId(12), + userDetailId = UserDetailId(post.actorId.id), + name = TimelineName("timeline"), + visibility = TimelineVisibility.PUBLIC, + isSystem = false + ) + ) + ) + + val filters = listOf( + Filter( + id = FilterId(13), + userDetailId = UserDetailId(post.actorId.id), + name = FilterName("filter"), + filterContext = setOf(FilterContext.HOME), + filterAction = FilterAction.HIDE, + filterKeywords = setOf( + FilterKeyword(FilterKeywordId(14), FilterKeywordKeyword("aa"), FilterMode.NONE) + ) + ) + ) + + whenever(filterRepository.findByUserDetailId(UserDetailId(post.actorId.id))).doReturn(filters) + + whenever(filterDomainService.apply(post, FilterContext.HOME, filters)).doReturn( + FilteredPost( + post, listOf( + FilterResult(filters.first(), "aaa") + ) + ) + ) + + timelineStore.addPost(post) + + argumentCaptor> { + verify(internalTimelineObjectRepository, times(1)).saveAll(capture()) + val timelineObjectList = allValues.first() + + assertThat(timelineObjectList).allSatisfy { + assertThat(it.postId).isEqualTo(post.id) + assertThat(it.postActorId).isEqualTo(post.actorId) + assertThat(it.repostId).isNull() + assertThat(it.repostActorId).isNull() + assertThat(it.replyId).isEqualTo(reply.id) + assertThat(it.replyActorId).isEqualTo(reply.actorId) + + assertThat(it.userDetailId).isEqualTo(UserDetailId(post.actorId.id)) + assertThat(it.timelineId).isEqualTo(TimelineId(12)) + assertThat(it.warnFilters).contains(TimelineObjectWarnFilter(FilterId(13), "aaa")) + } + } + } +} \ No newline at end of file diff --git a/hideout-core/src/test/kotlin/utils/AssertDomainEvent.kt b/hideout-core/src/test/kotlin/utils/AssertDomainEvent.kt new file mode 100644 index 00000000..d1d2c049 --- /dev/null +++ b/hideout-core/src/test/kotlin/utils/AssertDomainEvent.kt @@ -0,0 +1,35 @@ +/* + * Copyright (C) 2024 usbharu + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package utils + +import dev.usbharu.hideout.core.domain.shared.domainevent.DomainEventStorable + +object AssertDomainEvent { + fun assertContainsEvent(domainEventStorable: DomainEventStorable, eventName: String) { + val find = domainEventStorable.getDomainEvents().find { it.name == eventName } + + if (find == null) { + throw AssertionError("Domain Event not found: $eventName") + } + } + + fun assertEmpty(domainEventStorable: DomainEventStorable) { + if (domainEventStorable.getDomainEvents().isNotEmpty()) { + throw AssertionError("Domain Event found") + } + } +} \ No newline at end of file diff --git a/hideout-core/src/test/kotlin/utils/TestTransaction.kt b/hideout-core/src/test/kotlin/utils/TestTransaction.kt new file mode 100644 index 00000000..a32bf303 --- /dev/null +++ b/hideout-core/src/test/kotlin/utils/TestTransaction.kt @@ -0,0 +1,24 @@ +/* + * Copyright (C) 2024 usbharu + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package utils + +import dev.usbharu.hideout.core.application.shared.Transaction + +object TestTransaction : Transaction { + override suspend fun transaction(block: suspend () -> T): T = block() + override suspend fun transaction(transactionLevel: Int, block: suspend () -> T): T = block() +} diff --git a/hideout-core/src/test/resources/400x400.png b/hideout-core/src/test/resources/400x400.png new file mode 100644 index 00000000..0d2e71be Binary files /dev/null and b/hideout-core/src/test/resources/400x400.png differ diff --git a/src/test/resources/empty.conf b/hideout-core/src/test/resources/empty.conf similarity index 82% rename from src/test/resources/empty.conf rename to hideout-core/src/test/resources/empty.conf index ba691e1c..3c142bff 100644 --- a/src/test/resources/empty.conf +++ b/hideout-core/src/test/resources/empty.conf @@ -10,8 +10,7 @@ ktor { } hideout { - hostname = "https://localhost:8080" - hostname = ${?HOSTNAME} + url = "http://localhost:8080" database { url = "jdbc:h2:./test;MODE=POSTGRESQL" driver = "org.h2.Driver" diff --git a/hideout-core/src/test/resources/junit-platform.properties b/hideout-core/src/test/resources/junit-platform.properties new file mode 100644 index 00000000..acfa9e5a --- /dev/null +++ b/hideout-core/src/test/resources/junit-platform.properties @@ -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 diff --git a/hideout-mastodon/build.gradle.kts b/hideout-mastodon/build.gradle.kts new file mode 100644 index 00000000..597af703 --- /dev/null +++ b/hideout-mastodon/build.gradle.kts @@ -0,0 +1,86 @@ +import org.openapitools.generator.gradle.plugin.tasks.GenerateTask + +plugins { + alias(libs.plugins.kotlin.jvm) + alias(libs.plugins.openapi.generator) + alias(libs.plugins.spring.boot) + alias(libs.plugins.kotlin.spring) +} + + +apply { + plugin("io.spring.dependency-management") +} + +group = "dev.usbharu" +version = "1.0-SNAPSHOT" + +repositories { + mavenCentral() +} + +configurations { + all { + exclude("org.springframework.boot", "spring-boot-starter-logging") + exclude("ch.qos.logback", "logback-classic") + } +} + +dependencies { + implementation("org.springframework.boot:spring-boot-starter-web") + implementation("org.springframework.boot:spring-boot-starter-security") + implementation("com.fasterxml.jackson.datatype:jackson-datatype-jsr310") + implementation("org.springframework.boot:spring-boot-starter-log4j2") + + implementation("dev.usbharu:hideout-core:0.0.1") + + implementation(libs.jackson.databind) + implementation(libs.jackson.module.kotlin) + implementation(libs.jakarta.annotation) + implementation(libs.jakarta.validation) + + implementation(libs.bundles.exposed) + implementation(libs.bundles.openapi) + implementation(libs.bundles.coroutines) +} + +tasks { + test { + useJUnitPlatform() + } + + compileKotlin { + dependsOn("openApiGenerateMastodonCompatibleApi") + mustRunAfter("openApiGenerateMastodonCompatibleApi") + } + + create("openApiGenerateMastodonCompatibleApi") { + generatorName.set("kotlin-spring") + inputSpec.set("$rootDir/src/main/resources/openapi/mastodon.yaml") + outputDir.set("$buildDir/generated/sources/mastodon") + apiPackage.set("dev.usbharu.hideout.mastodon.interfaces.api.generated") + modelPackage.set("dev.usbharu.hideout.mastodon.interfaces.api.generated.model") + configOptions.put("interfaceOnly", "true") + configOptions.put("useSpringBoot3", "true") + configOptions.put("reactive", "true") + configOptions.put("gradleBuildFile", "false") + configOptions.put("useSwaggerUI", "false") + configOptions.put("enumPropertyNaming", "UPPERCASE") + additionalProperties.put("useTags", "true") + + importMappings.put("org.springframework.core.io.Resource", "org.springframework.web.multipart.MultipartFile") + typeMappings.put("org.springframework.core.io.Resource", "org.springframework.web.multipart.MultipartFile") + templateDir.set("$rootDir/templates") + } +} + +kotlin { + jvmToolchain(21) +} + +sourceSets.main { + kotlin.srcDirs( + "$buildDir/generated/sources/mastodon/src/main/kotlin" + ) +} + diff --git a/hideout-mastodon/gradle/wrapper/gradle-wrapper.jar b/hideout-mastodon/gradle/wrapper/gradle-wrapper.jar new file mode 100644 index 00000000..2c352119 Binary files /dev/null and b/hideout-mastodon/gradle/wrapper/gradle-wrapper.jar differ diff --git a/hideout-mastodon/gradle/wrapper/gradle-wrapper.properties b/hideout-mastodon/gradle/wrapper/gradle-wrapper.properties new file mode 100644 index 00000000..09523c0e --- /dev/null +++ b/hideout-mastodon/gradle/wrapper/gradle-wrapper.properties @@ -0,0 +1,7 @@ +distributionBase=GRADLE_USER_HOME +distributionPath=wrapper/dists +distributionUrl=https\://services.gradle.org/distributions/gradle-8.9-bin.zip +networkTimeout=10000 +validateDistributionUrl=true +zipStoreBase=GRADLE_USER_HOME +zipStorePath=wrapper/dists diff --git a/hideout-mastodon/gradlew b/hideout-mastodon/gradlew new file mode 100644 index 00000000..f5feea6d --- /dev/null +++ b/hideout-mastodon/gradlew @@ -0,0 +1,252 @@ +#!/bin/sh + +# +# Copyright © 2015-2021 the original authors. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# https://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# +# SPDX-License-Identifier: Apache-2.0 +# + +############################################################################## +# +# Gradle start up script for POSIX generated by Gradle. +# +# Important for running: +# +# (1) You need a POSIX-compliant shell to run this script. If your /bin/sh is +# noncompliant, but you have some other compliant shell such as ksh or +# bash, then to run this script, type that shell name before the whole +# command line, like: +# +# ksh Gradle +# +# Busybox and similar reduced shells will NOT work, because this script +# requires all of these POSIX shell features: +# * functions; +# * expansions «$var», «${var}», «${var:-default}», «${var+SET}», +# «${var#prefix}», «${var%suffix}», and «$( cmd )»; +# * compound commands having a testable exit status, especially «case»; +# * various built-in commands including «command», «set», and «ulimit». +# +# Important for patching: +# +# (2) This script targets any POSIX shell, so it avoids extensions provided +# by Bash, Ksh, etc; in particular arrays are avoided. +# +# The "traditional" practice of packing multiple parameters into a +# space-separated string is a well documented source of bugs and security +# problems, so this is (mostly) avoided, by progressively accumulating +# options in "$@", and eventually passing that to Java. +# +# Where the inherited environment variables (DEFAULT_JVM_OPTS, JAVA_OPTS, +# and GRADLE_OPTS) rely on word-splitting, this is performed explicitly; +# see the in-line comments for details. +# +# There are tweaks for specific operating systems such as AIX, CygWin, +# Darwin, MinGW, and NonStop. +# +# (3) This script is generated from the Groovy template +# https://github.com/gradle/gradle/blob/HEAD/platforms/jvm/plugins-application/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt +# within the Gradle project. +# +# You can find Gradle at https://github.com/gradle/gradle/. +# +############################################################################## + +# Attempt to set APP_HOME + +# Resolve links: $0 may be a link +app_path=$0 + +# Need this for daisy-chained symlinks. +while + APP_HOME=${app_path%"${app_path##*/}"} # leaves a trailing /; empty if no leading path + [ -h "$app_path" ] +do + ls=$( ls -ld "$app_path" ) + link=${ls#*' -> '} + case $link in #( + /*) app_path=$link ;; #( + *) app_path=$APP_HOME$link ;; + esac +done + +# This is normally unused +# shellcheck disable=SC2034 +APP_BASE_NAME=${0##*/} +# Discard cd standard output in case $CDPATH is set (https://github.com/gradle/gradle/issues/25036) +APP_HOME=$( cd -P "${APP_HOME:-./}" > /dev/null && printf '%s +' "$PWD" ) || exit + +# Use the maximum available, or set MAX_FD != -1 to use that value. +MAX_FD=maximum + +warn () { + echo "$*" +} >&2 + +die () { + echo + echo "$*" + echo + exit 1 +} >&2 + +# OS specific support (must be 'true' or 'false'). +cygwin=false +msys=false +darwin=false +nonstop=false +case "$( uname )" in #( + CYGWIN* ) cygwin=true ;; #( + Darwin* ) darwin=true ;; #( + MSYS* | MINGW* ) msys=true ;; #( + NONSTOP* ) nonstop=true ;; +esac + +CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar + + +# Determine the Java command to use to start the JVM. +if [ -n "$JAVA_HOME" ] ; then + if [ -x "$JAVA_HOME/jre/sh/java" ] ; then + # IBM's JDK on AIX uses strange locations for the executables + JAVACMD=$JAVA_HOME/jre/sh/java + else + JAVACMD=$JAVA_HOME/bin/java + fi + if [ ! -x "$JAVACMD" ] ; then + die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." + fi +else + JAVACMD=java + if ! command -v java >/dev/null 2>&1 + then + die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." + fi +fi + +# Increase the maximum file descriptors if we can. +if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then + case $MAX_FD in #( + max*) + # In POSIX sh, ulimit -H is undefined. That's why the result is checked to see if it worked. + # shellcheck disable=SC2039,SC3045 + MAX_FD=$( ulimit -H -n ) || + warn "Could not query maximum file descriptor limit" + esac + case $MAX_FD in #( + '' | soft) :;; #( + *) + # In POSIX sh, ulimit -n is undefined. That's why the result is checked to see if it worked. + # shellcheck disable=SC2039,SC3045 + ulimit -n "$MAX_FD" || + warn "Could not set maximum file descriptor limit to $MAX_FD" + esac +fi + +# Collect all arguments for the java command, stacking in reverse order: +# * args from the command line +# * the main class name +# * -classpath +# * -D...appname settings +# * --module-path (only if needed) +# * DEFAULT_JVM_OPTS, JAVA_OPTS, and GRADLE_OPTS environment variables. + +# For Cygwin or MSYS, switch paths to Windows format before running java +if "$cygwin" || "$msys" ; then + APP_HOME=$( cygpath --path --mixed "$APP_HOME" ) + CLASSPATH=$( cygpath --path --mixed "$CLASSPATH" ) + + JAVACMD=$( cygpath --unix "$JAVACMD" ) + + # Now convert the arguments - kludge to limit ourselves to /bin/sh + for arg do + if + case $arg in #( + -*) false ;; # don't mess with options #( + /?*) t=${arg#/} t=/${t%%/*} # looks like a POSIX filepath + [ -e "$t" ] ;; #( + *) false ;; + esac + then + arg=$( cygpath --path --ignore --mixed "$arg" ) + fi + # Roll the args list around exactly as many times as the number of + # args, so each arg winds up back in the position where it started, but + # possibly modified. + # + # NB: a `for` loop captures its iteration list before it begins, so + # changing the positional parameters here affects neither the number of + # iterations, nor the values presented in `arg`. + shift # remove old arg + set -- "$@" "$arg" # push replacement arg + done +fi + + +# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"' + +# Collect all arguments for the java command: +# * DEFAULT_JVM_OPTS, JAVA_OPTS, JAVA_OPTS, and optsEnvironmentVar are not allowed to contain shell fragments, +# and any embedded shellness will be escaped. +# * For example: A user cannot expect ${Hostname} to be expanded, as it is an environment variable and will be +# treated as '${Hostname}' itself on the command line. + +set -- \ + "-Dorg.gradle.appname=$APP_BASE_NAME" \ + -classpath "$CLASSPATH" \ + org.gradle.wrapper.GradleWrapperMain \ + "$@" + +# Stop when "xargs" is not available. +if ! command -v xargs >/dev/null 2>&1 +then + die "xargs is not available" +fi + +# Use "xargs" to parse quoted args. +# +# With -n1 it outputs one arg per line, with the quotes and backslashes removed. +# +# In Bash we could simply go: +# +# readarray ARGS < <( xargs -n1 <<<"$var" ) && +# set -- "${ARGS[@]}" "$@" +# +# but POSIX shell has neither arrays nor command substitution, so instead we +# post-process each arg (as a line of input to sed) to backslash-escape any +# character that might be a shell metacharacter, then use eval to reverse +# that process (while maintaining the separation between arguments), and wrap +# the whole thing up as a single "set" statement. +# +# This will of course break if any of these variables contains a newline or +# an unmatched quote. +# + +eval "set -- $( + printf '%s\n' "$DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS" | + xargs -n1 | + sed ' s~[^-[:alnum:]+,./:=@_]~\\&~g; ' | + tr '\n' ' ' + )" '"$@"' + +exec "$JAVACMD" "$@" diff --git a/hideout-mastodon/gradlew.bat b/hideout-mastodon/gradlew.bat new file mode 100644 index 00000000..9b42019c --- /dev/null +++ b/hideout-mastodon/gradlew.bat @@ -0,0 +1,94 @@ +@rem +@rem Copyright 2015 the original author or authors. +@rem +@rem Licensed under the Apache License, Version 2.0 (the "License"); +@rem you may not use this file except in compliance with the License. +@rem You may obtain a copy of the License at +@rem +@rem https://www.apache.org/licenses/LICENSE-2.0 +@rem +@rem Unless required by applicable law or agreed to in writing, software +@rem distributed under the License is distributed on an "AS IS" BASIS, +@rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +@rem See the License for the specific language governing permissions and +@rem limitations under the License. +@rem +@rem SPDX-License-Identifier: Apache-2.0 +@rem + +@if "%DEBUG%"=="" @echo off +@rem ########################################################################## +@rem +@rem Gradle startup script for Windows +@rem +@rem ########################################################################## + +@rem Set local scope for the variables with windows NT shell +if "%OS%"=="Windows_NT" setlocal + +set DIRNAME=%~dp0 +if "%DIRNAME%"=="" set DIRNAME=. +@rem This is normally unused +set APP_BASE_NAME=%~n0 +set APP_HOME=%DIRNAME% + +@rem Resolve any "." and ".." in APP_HOME to make it shorter. +for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi + +@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m" + +@rem Find java.exe +if defined JAVA_HOME goto findJavaFromJavaHome + +set JAVA_EXE=java.exe +%JAVA_EXE% -version >NUL 2>&1 +if %ERRORLEVEL% equ 0 goto execute + +echo. 1>&2 +echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 1>&2 +echo. 1>&2 +echo Please set the JAVA_HOME variable in your environment to match the 1>&2 +echo location of your Java installation. 1>&2 + +goto fail + +:findJavaFromJavaHome +set JAVA_HOME=%JAVA_HOME:"=% +set JAVA_EXE=%JAVA_HOME%/bin/java.exe + +if exist "%JAVA_EXE%" goto execute + +echo. 1>&2 +echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 1>&2 +echo. 1>&2 +echo Please set the JAVA_HOME variable in your environment to match the 1>&2 +echo location of your Java installation. 1>&2 + +goto fail + +:execute +@rem Setup the command line + +set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar + + +@rem Execute Gradle +"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %* + +:end +@rem End local scope for the variables with windows NT shell +if %ERRORLEVEL% equ 0 goto mainEnd + +:fail +rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of +rem the _cmd.exe /c_ return code! +set EXIT_CODE=%ERRORLEVEL% +if %EXIT_CODE% equ 0 set EXIT_CODE=1 +if not ""=="%GRADLE_EXIT_CONSOLE%" exit %EXIT_CODE% +exit /b %EXIT_CODE% + +:mainEnd +if "%OS%"=="Windows_NT" endlocal + +:omega diff --git a/hideout-mastodon/settings.gradle.kts b/hideout-mastodon/settings.gradle.kts new file mode 100644 index 00000000..67719be1 --- /dev/null +++ b/hideout-mastodon/settings.gradle.kts @@ -0,0 +1,18 @@ +plugins { + id("org.gradle.toolchains.foojay-resolver-convention") version "0.8.0" +} +rootProject.name = "hideout-mastodon" + +includeBuild("../hideout-core") + +dependencyResolutionManagement { + repositories { + mavenCentral() + } + + versionCatalogs { + create("libs") { + from(files("../libs.versions.toml")) + } + } +} \ No newline at end of file diff --git a/hideout-mastodon/src/main/kotlin/dev/usbharu/hideout/mastodon/application/accounts/GetAccount.kt b/hideout-mastodon/src/main/kotlin/dev/usbharu/hideout/mastodon/application/accounts/GetAccount.kt new file mode 100644 index 00000000..340607b9 --- /dev/null +++ b/hideout-mastodon/src/main/kotlin/dev/usbharu/hideout/mastodon/application/accounts/GetAccount.kt @@ -0,0 +1,19 @@ +/* + * Copyright (C) 2024 usbharu + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package dev.usbharu.hideout.mastodon.application.accounts + +data class GetAccount(val accountId: String) diff --git a/hideout-mastodon/src/main/kotlin/dev/usbharu/hideout/mastodon/application/accounts/GetAccountApplicationService.kt b/hideout-mastodon/src/main/kotlin/dev/usbharu/hideout/mastodon/application/accounts/GetAccountApplicationService.kt new file mode 100644 index 00000000..d2611534 --- /dev/null +++ b/hideout-mastodon/src/main/kotlin/dev/usbharu/hideout/mastodon/application/accounts/GetAccountApplicationService.kt @@ -0,0 +1,40 @@ +/* + * Copyright (C) 2024 usbharu + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package dev.usbharu.hideout.mastodon.application.accounts + +import dev.usbharu.hideout.core.application.shared.AbstractApplicationService +import dev.usbharu.hideout.core.application.shared.Transaction +import dev.usbharu.hideout.core.domain.model.support.principal.Principal +import dev.usbharu.hideout.mastodon.interfaces.api.generated.model.Account +import dev.usbharu.hideout.mastodon.query.AccountQueryService +import org.slf4j.LoggerFactory +import org.springframework.stereotype.Service + +@Service +class GetAccountApplicationService(private val accountQueryService: AccountQueryService, transaction: Transaction) : + AbstractApplicationService( + transaction, + logger + ) { + override suspend fun internalExecute(command: GetAccount, principal: Principal): Account { + return accountQueryService.findById(command.accountId.toLong()) ?: throw Exception("Account not found") + } + + companion object { + private val logger = LoggerFactory.getLogger(GetAccountApplicationService::class.java) + } +} \ No newline at end of file diff --git a/hideout-mastodon/src/main/kotlin/dev/usbharu/hideout/mastodon/application/filter/DeleteFilterV1.kt b/hideout-mastodon/src/main/kotlin/dev/usbharu/hideout/mastodon/application/filter/DeleteFilterV1.kt new file mode 100644 index 00000000..6baf4976 --- /dev/null +++ b/hideout-mastodon/src/main/kotlin/dev/usbharu/hideout/mastodon/application/filter/DeleteFilterV1.kt @@ -0,0 +1,19 @@ +/* + * Copyright (C) 2024 usbharu + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package dev.usbharu.hideout.mastodon.application.filter + +data class DeleteFilterV1(val filterKeywordId: Long) diff --git a/hideout-mastodon/src/main/kotlin/dev/usbharu/hideout/mastodon/application/filter/DeleteFilterV1ApplicationService.kt b/hideout-mastodon/src/main/kotlin/dev/usbharu/hideout/mastodon/application/filter/DeleteFilterV1ApplicationService.kt new file mode 100644 index 00000000..2a722f01 --- /dev/null +++ b/hideout-mastodon/src/main/kotlin/dev/usbharu/hideout/mastodon/application/filter/DeleteFilterV1ApplicationService.kt @@ -0,0 +1,41 @@ +/* + * Copyright (C) 2024 usbharu + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package dev.usbharu.hideout.mastodon.application.filter + +import dev.usbharu.hideout.core.application.shared.AbstractApplicationService +import dev.usbharu.hideout.core.application.shared.Transaction +import dev.usbharu.hideout.core.domain.model.filter.FilterKeywordId +import dev.usbharu.hideout.core.domain.model.filter.FilterRepository +import dev.usbharu.hideout.core.domain.model.support.principal.Principal +import org.slf4j.LoggerFactory +import org.springframework.stereotype.Service + +@Service +class DeleteFilterV1ApplicationService(private val filterRepository: FilterRepository, transaction: Transaction) : + AbstractApplicationService( + transaction, logger + ) { + companion object { + private val logger = LoggerFactory.getLogger(DeleteFilterV1ApplicationService::class.java) + } + + override suspend fun internalExecute(command: DeleteFilterV1, principal: Principal) { + val filter = filterRepository.findByFilterKeywordId(FilterKeywordId(command.filterKeywordId)) + ?: throw Exception("Not Found") + filterRepository.delete(filter) + } +} \ No newline at end of file diff --git a/hideout-mastodon/src/main/kotlin/dev/usbharu/hideout/mastodon/application/filter/GetFilterV1.kt b/hideout-mastodon/src/main/kotlin/dev/usbharu/hideout/mastodon/application/filter/GetFilterV1.kt new file mode 100644 index 00000000..e6d1c26c --- /dev/null +++ b/hideout-mastodon/src/main/kotlin/dev/usbharu/hideout/mastodon/application/filter/GetFilterV1.kt @@ -0,0 +1,19 @@ +/* + * Copyright (C) 2024 usbharu + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package dev.usbharu.hideout.mastodon.application.filter + +data class GetFilterV1(val filterKeywordId: Long) \ No newline at end of file diff --git a/hideout-mastodon/src/main/kotlin/dev/usbharu/hideout/mastodon/application/filter/GetFilterV1ApplicationService.kt b/hideout-mastodon/src/main/kotlin/dev/usbharu/hideout/mastodon/application/filter/GetFilterV1ApplicationService.kt new file mode 100644 index 00000000..057b7b1a --- /dev/null +++ b/hideout-mastodon/src/main/kotlin/dev/usbharu/hideout/mastodon/application/filter/GetFilterV1ApplicationService.kt @@ -0,0 +1,61 @@ +/* + * Copyright (C) 2024 usbharu + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package dev.usbharu.hideout.mastodon.application.filter + +import dev.usbharu.hideout.core.application.shared.AbstractApplicationService +import dev.usbharu.hideout.core.application.shared.Transaction +import dev.usbharu.hideout.core.domain.model.filter.FilterContext.* +import dev.usbharu.hideout.core.domain.model.filter.FilterKeywordId +import dev.usbharu.hideout.core.domain.model.filter.FilterMode +import dev.usbharu.hideout.core.domain.model.filter.FilterRepository +import dev.usbharu.hideout.core.domain.model.support.principal.Principal +import dev.usbharu.hideout.mastodon.interfaces.api.generated.model.V1Filter +import org.slf4j.LoggerFactory +import org.springframework.stereotype.Repository + +@Repository +class GetFilterV1ApplicationService(private val filterRepository: FilterRepository, transaction: Transaction) : + AbstractApplicationService( + transaction, logger + ) { + override suspend fun internalExecute(command: GetFilterV1, principal: Principal): V1Filter { + val filter = filterRepository.findByFilterKeywordId(FilterKeywordId(command.filterKeywordId)) + ?: throw Exception("Not Found") + + val filterKeyword = filter.filterKeywords.find { it.id.id == command.filterKeywordId } + return V1Filter( + id = filter.id.id.toString(), + phrase = filterKeyword?.keyword?.keyword, + context = filter.filterContext.map { + when (it) { + HOME -> V1Filter.Context.home + NOTIFICATION -> V1Filter.Context.notifications + PUBLIC -> V1Filter.Context.public + THREAD -> V1Filter.Context.thread + ACCOUNT -> V1Filter.Context.account + } + }, + expiresAt = null, + irreversible = false, + wholeWord = filterKeyword?.mode == FilterMode.WHOLE_WORD + ) + } + + companion object { + private val logger = LoggerFactory.getLogger(GetFilterV1ApplicationService::class.java) + } +} \ No newline at end of file diff --git a/hideout-mastodon/src/main/kotlin/dev/usbharu/hideout/mastodon/application/status/GetStatus.kt b/hideout-mastodon/src/main/kotlin/dev/usbharu/hideout/mastodon/application/status/GetStatus.kt new file mode 100644 index 00000000..37b6882e --- /dev/null +++ b/hideout-mastodon/src/main/kotlin/dev/usbharu/hideout/mastodon/application/status/GetStatus.kt @@ -0,0 +1,21 @@ +/* + * Copyright (C) 2024 usbharu + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package dev.usbharu.hideout.mastodon.application.status + +data class GetStatus( + val id: String, +) diff --git a/hideout-mastodon/src/main/kotlin/dev/usbharu/hideout/mastodon/application/status/GetStatusApplicationService.kt b/hideout-mastodon/src/main/kotlin/dev/usbharu/hideout/mastodon/application/status/GetStatusApplicationService.kt new file mode 100644 index 00000000..63bccc23 --- /dev/null +++ b/hideout-mastodon/src/main/kotlin/dev/usbharu/hideout/mastodon/application/status/GetStatusApplicationService.kt @@ -0,0 +1,42 @@ +/* + * Copyright (C) 2024 usbharu + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package dev.usbharu.hideout.mastodon.application.status + +import dev.usbharu.hideout.core.application.shared.AbstractApplicationService +import dev.usbharu.hideout.core.application.shared.Transaction +import dev.usbharu.hideout.core.domain.model.support.principal.Principal +import dev.usbharu.hideout.mastodon.interfaces.api.generated.model.Status +import dev.usbharu.hideout.mastodon.query.StatusQueryService +import org.slf4j.LoggerFactory +import org.springframework.stereotype.Service + +@Service +class GetStatusApplicationService( + private val statusQueryService: StatusQueryService, + transaction: Transaction, +) : AbstractApplicationService( + transaction, + logger +) { + companion object { + val logger = LoggerFactory.getLogger(GetStatusApplicationService::class.java)!! + } + + override suspend fun internalExecute(command: GetStatus, principal: Principal): Status { + return statusQueryService.findByPostId(command.id.toLong()) ?: throw Exception("Not fount") + } +} \ No newline at end of file diff --git a/hideout-mastodon/src/main/kotlin/dev/usbharu/hideout/mastodon/config/MastodonSecurityConfig.kt b/hideout-mastodon/src/main/kotlin/dev/usbharu/hideout/mastodon/config/MastodonSecurityConfig.kt new file mode 100644 index 00000000..513994b1 --- /dev/null +++ b/hideout-mastodon/src/main/kotlin/dev/usbharu/hideout/mastodon/config/MastodonSecurityConfig.kt @@ -0,0 +1,112 @@ +/* + * Copyright (C) 2024 usbharu + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package dev.usbharu.hideout.mastodon.config + +import dev.usbharu.hideout.mastodon.external.RoleHierarchyAuthorizationManagerFactory +import org.springframework.context.annotation.Bean +import org.springframework.context.annotation.Configuration +import org.springframework.core.annotation.Order +import org.springframework.http.HttpMethod.* +import org.springframework.security.config.annotation.web.builders.HttpSecurity +import org.springframework.security.config.annotation.web.invoke +import org.springframework.security.web.SecurityFilterChain + +@Configuration +class MastodonSecurityConfig { + @Bean + @Order(2) + @Suppress("LongMethod") + fun mastodonApiSecurityFilterChain( + http: HttpSecurity, + rf: RoleHierarchyAuthorizationManagerFactory, + ): SecurityFilterChain { + http { + securityMatcher("/api/v1/**", "/api/v2/**") + authorizeHttpRequests { + authorize(POST, "/api/v1/apps", permitAll) + authorize(GET, "/api/v1/instance/**", permitAll) + authorize(POST, "/api/v1/accounts", authenticated) + + authorize(GET, "/api/v1/accounts/verify_credentials", rf.hasScope("read:accounts")) + authorize(GET, "/api/v1/accounts/relationships", rf.hasScope("read:follows")) + authorize(GET, "/api/v1/accounts/*", permitAll) + authorize(GET, "/api/v1/accounts/*/statuses", permitAll) + authorize(POST, "/api/v1/accounts/*/follow", rf.hasScope("write:follows")) + authorize(POST, "/api/v1/accounts/*/unfollow", rf.hasScope("write:follows")) + authorize(POST, "/api/v1/accounts/*/block", rf.hasScope("write:blocks")) + authorize(POST, "/api/v1/accounts/*/unblock", rf.hasScope("write:blocks")) + authorize(POST, "/api/v1/accounts/*/mute", rf.hasScope("write:mutes")) + authorize(POST, "/api/v1/accounts/*/unmute", rf.hasScope("write:mutes")) + authorize(GET, "/api/v1/mutes", rf.hasScope("read:mutes")) + + authorize(POST, "/api/v1/media", rf.hasScope("write:media")) + authorize(POST, "/api/v1/statuses", rf.hasScope("write:statuses")) + authorize(GET, "/api/v1/statuses/*", permitAll) + authorize(POST, "/api/v1/statuses/*/favourite", rf.hasScope("write:favourites")) + authorize(POST, "/api/v1/statuses/*/unfavourite", rf.hasScope("write:favourites")) + authorize(PUT, "/api/v1/statuses/*/emoji_reactions/*", rf.hasScope("write:favourites")) + + authorize(GET, "/api/v1/timelines/public", permitAll) + authorize(GET, "/api/v1/timelines/home", rf.hasScope("read:statuses")) + + authorize(GET, "/api/v2/filters", rf.hasScope("read:filters")) + authorize(POST, "/api/v2/filters", rf.hasScope("write:filters")) + + authorize(GET, "/api/v2/filters/*", rf.hasScope("read:filters")) + authorize(PUT, "/api/v2/filters/*", rf.hasScope("write:filters")) + authorize(DELETE, "/api/v2/filters/*", rf.hasScope("write:filters")) + + authorize(GET, "/api/v2/filters/*/keywords", rf.hasScope("read:filters")) + authorize(POST, "/api/v2/filters/*/keywords", rf.hasScope("write:filters")) + + authorize(GET, "/api/v2/filters/keywords/*", rf.hasScope("read:filters")) + authorize(PUT, "/api/v2/filters/keywords/*", rf.hasScope("write:filters")) + authorize(DELETE, "/api/v2/filters/keywords/*", rf.hasScope("write:filters")) + + authorize(GET, "/api/v2/filters/*/statuses", rf.hasScope("read:filters")) + authorize(POST, "/api/v2/filters/*/statuses", rf.hasScope("write:filters")) + + authorize(GET, "/api/v2/filters/statuses/*", rf.hasScope("read:filters")) + authorize(DELETE, "/api/v2/filters/statuses/*", rf.hasScope("write:filters")) + + authorize(GET, "/api/v1/filters", rf.hasScope("read:filters")) + authorize(POST, "/api/v1/filters", rf.hasScope("write:filters")) + + authorize(GET, "/api/v1/filters/*", rf.hasScope("read:filters")) + authorize(POST, "/api/v1/filters/*", rf.hasScope("write:filters")) + authorize(DELETE, "/api/v1/filters/*", rf.hasScope("write:filters")) + + authorize(GET, "/api/v1/notifications", rf.hasScope("read:notifications")) + authorize(GET, "/api/v1/notifications/*", rf.hasScope("read:notifications")) + authorize(POST, "/api/v1/notifications/clear", rf.hasScope("write:notifications")) + authorize(POST, "/api/v1/notifications/*/dismiss", rf.hasScope("write:notifications")) + + authorize(anyRequest, authenticated) + } + + oauth2ResourceServer { + jwt { } + } + + csrf { + ignoringRequestMatchers("/api/v1/apps") + } + } + + return http.build() + } +} \ No newline at end of file diff --git a/hideout-mastodon/src/main/kotlin/dev/usbharu/hideout/mastodon/external/RoleHierarchyAuthorizationManagerFactory.kt b/hideout-mastodon/src/main/kotlin/dev/usbharu/hideout/mastodon/external/RoleHierarchyAuthorizationManagerFactory.kt new file mode 100644 index 00000000..66395c20 --- /dev/null +++ b/hideout-mastodon/src/main/kotlin/dev/usbharu/hideout/mastodon/external/RoleHierarchyAuthorizationManagerFactory.kt @@ -0,0 +1,32 @@ +/* + * Copyright (C) 2024 usbharu + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package dev.usbharu.hideout.mastodon.external + +import org.springframework.security.access.hierarchicalroles.RoleHierarchy +import org.springframework.security.authorization.AuthorityAuthorizationManager +import org.springframework.security.authorization.AuthorizationManager +import org.springframework.security.web.access.intercept.RequestAuthorizationContext +import org.springframework.stereotype.Component + +@Component +class RoleHierarchyAuthorizationManagerFactory(private val roleHierarchy: RoleHierarchy) { + fun hasScope(role: String): AuthorizationManager { + val hasAuthority = AuthorityAuthorizationManager.hasAuthority("SCOPE_$role") + hasAuthority.setRoleHierarchy(roleHierarchy) + return hasAuthority + } +} \ No newline at end of file diff --git a/hideout-mastodon/src/main/kotlin/dev/usbharu/hideout/mastodon/infrastructure/exposedquery/ExposedAccountQueryService.kt b/hideout-mastodon/src/main/kotlin/dev/usbharu/hideout/mastodon/infrastructure/exposedquery/ExposedAccountQueryService.kt new file mode 100644 index 00000000..41ca4ab2 --- /dev/null +++ b/hideout-mastodon/src/main/kotlin/dev/usbharu/hideout/mastodon/infrastructure/exposedquery/ExposedAccountQueryService.kt @@ -0,0 +1,73 @@ +/* + * Copyright (C) 2024 usbharu + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package dev.usbharu.hideout.mastodon.infrastructure.exposedquery + +import dev.usbharu.hideout.core.config.ApplicationConfig +import dev.usbharu.hideout.core.infrastructure.exposedrepository.Actors +import dev.usbharu.hideout.mastodon.interfaces.api.generated.model.Account +import dev.usbharu.hideout.mastodon.query.AccountQueryService +import org.jetbrains.exposed.sql.ResultRow +import org.jetbrains.exposed.sql.selectAll +import org.springframework.stereotype.Repository + +@Repository +class AccountQueryServiceImpl(private val applicationConfig: ApplicationConfig) : AccountQueryService { + override suspend fun findById(accountId: Long): Account? { + val query = Actors.selectAll().where { Actors.id eq accountId } + + return query + .singleOrNull() + ?.let { toAccount(it) } + } + + override suspend fun findByIds(accountIds: List): List { + val query = Actors.selectAll().where { Actors.id inList accountIds } + + return query + .map { toAccount(it) } + } + + private fun toAccount( + resultRow: ResultRow, + ): Account { + val userUrl = "${applicationConfig.url}/users/${resultRow[Actors.id]}" + + return Account( + id = resultRow[Actors.id].toString(), + username = resultRow[Actors.name], + acct = "${resultRow[Actors.name]}@${resultRow[Actors.domain]}", + url = resultRow[Actors.url], + displayName = resultRow[Actors.screenName], + note = resultRow[Actors.description], + avatar = userUrl + "/icon.jpg", + avatarStatic = userUrl + "/icon.jpg", + header = userUrl + "/header.jpg", + headerStatic = userUrl + "/header.jpg", + locked = resultRow[Actors.locked], + fields = emptyList(), + emojis = emptyList(), + bot = false, + group = false, + discoverable = true, + createdAt = resultRow[Actors.createdAt].toString(), + lastStatusAt = resultRow[Actors.lastPostAt]?.toString(), + statusesCount = resultRow[Actors.postsCount], + followersCount = resultRow[Actors.followersCount], + followingCount = resultRow[Actors.followingCount], + ) + } +} \ No newline at end of file diff --git a/hideout-mastodon/src/main/kotlin/dev/usbharu/hideout/mastodon/infrastructure/exposedquery/ExposedStatusQueryService.kt b/hideout-mastodon/src/main/kotlin/dev/usbharu/hideout/mastodon/infrastructure/exposedquery/ExposedStatusQueryService.kt new file mode 100644 index 00000000..98a8a4c4 --- /dev/null +++ b/hideout-mastodon/src/main/kotlin/dev/usbharu/hideout/mastodon/infrastructure/exposedquery/ExposedStatusQueryService.kt @@ -0,0 +1,292 @@ +/* + * Copyright (C) 2024 usbharu + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package dev.usbharu.hideout.mastodon.infrastructure.exposedquery + +import dev.usbharu.hideout.core.domain.model.emoji.CustomEmoji +import dev.usbharu.hideout.core.domain.model.media.* +import dev.usbharu.hideout.core.domain.model.post.Visibility +import dev.usbharu.hideout.core.infrastructure.exposedrepository.* +import dev.usbharu.hideout.mastodon.interfaces.api.generated.model.Account +import dev.usbharu.hideout.mastodon.interfaces.api.generated.model.MediaAttachment +import dev.usbharu.hideout.mastodon.interfaces.api.generated.model.Status +import dev.usbharu.hideout.mastodon.interfaces.api.generated.model.Status.Visibility.* +import dev.usbharu.hideout.mastodon.query.StatusQuery +import dev.usbharu.hideout.mastodon.query.StatusQueryService +import org.jetbrains.exposed.sql.ResultRow +import org.jetbrains.exposed.sql.andWhere +import org.jetbrains.exposed.sql.leftJoin +import org.jetbrains.exposed.sql.selectAll +import org.springframework.stereotype.Repository +import java.net.URI +import dev.usbharu.hideout.core.domain.model.media.Media as EntityMedia +import dev.usbharu.hideout.mastodon.interfaces.api.generated.model.CustomEmoji as MastodonEmoji + +@Suppress("IncompleteDestructuring") +@Repository +class StatusQueryServiceImpl : StatusQueryService { + override suspend fun findByPostIds(ids: List): List = findByPostIdsWithMedia(ids) + + override suspend fun findByPostIdsWithMediaIds(statusQueries: List): List { + val postIdSet = mutableSetOf() + postIdSet.addAll(statusQueries.flatMap { listOfNotNull(it.postId, it.replyId, it.repostId) }) + val mediaIdSet = mutableSetOf() + mediaIdSet.addAll(statusQueries.flatMap { it.mediaIds }) + + val emojiIdSet = mutableSetOf() + emojiIdSet.addAll(statusQueries.flatMap { it.emojiIds }) + + val postMap = Posts + .leftJoin(Actors) + .selectAll().where { Posts.id inList postIdSet } + .associate { it[Posts.id] to toStatus(it) } + val mediaMap = Media.selectAll().where { Media.id inList mediaIdSet } + .associate { + it[Media.id] to it.toMedia().toMediaAttachments() + } + + val emojiMap = CustomEmojis.selectAll().where { CustomEmojis.id inList emojiIdSet }.associate { + it[CustomEmojis.id] to it.toCustomEmoji().toMastodonEmoji() + } + return statusQueries.mapNotNull { statusQuery -> + postMap[statusQuery.postId]?.copy( + inReplyToId = statusQuery.replyId?.toString(), + inReplyToAccountId = postMap[statusQuery.replyId]?.account?.id, + reblog = postMap[statusQuery.repostId], + mediaAttachments = statusQuery.mediaIds.mapNotNull { mediaMap[it] }, + emojis = statusQuery.emojiIds.mapNotNull { emojiMap[it] } + ) + } + } + + override suspend fun accountsStatus( + accountId: Long, + onlyMedia: Boolean, + excludeReplies: Boolean, + excludeReblogs: Boolean, + pinned: Boolean, + tagged: String?, + includeFollowers: Boolean, + ): List { + val query = Posts + .leftJoin(PostsMedia) + .leftJoin(Actors) + .leftJoin(Media) + .selectAll().where { Posts.actorId eq accountId } + + if (onlyMedia) { + query.andWhere { PostsMedia.mediaId.isNotNull() } + } + if (excludeReplies) { + query.andWhere { Posts.replyId.isNotNull() } + } + if (excludeReblogs) { + query.andWhere { Posts.repostId.isNotNull() } + } + if (includeFollowers) { + query.andWhere { Posts.visibility inList listOf(public.name, unlisted.name, private.name) } + } else { + query.andWhere { Posts.visibility inList listOf(public.name, unlisted.name) } + } + + val pairs = query + .groupBy { it[Posts.id] } + .map { it.value } + .map { + toStatus(it.first()).copy( + mediaAttachments = it.mapNotNull { resultRow -> + resultRow.toMediaOrNull()?.toMediaAttachments() + } + ) to it.first()[Posts.repostId] + } + + val statuses = resolveReplyAndRepost(pairs) + return statuses + } + + override suspend fun findByPostId(id: Long): Status? { + val map = Posts + .leftJoin(PostsMedia) + .leftJoin(Actors) + .leftJoin(Media,{PostsMedia.mediaId},{Media.id}) + .selectAll() + .where { Posts.id eq id } + .groupBy { it[Posts.id] } + .map { it.value } + .map { + toStatus(it.first()).copy( + mediaAttachments = it.mapNotNull { resultRow -> + resultRow.toMediaOrNull()?.toMediaAttachments() + }, + emojis = it.mapNotNull { resultRow -> resultRow.toCustomEmojiOrNull()?.toMastodonEmoji() } + ) to it.first()[Posts.repostId] + } + return resolveReplyAndRepost(map).singleOrNull() + } + + private fun resolveReplyAndRepost(pairs: List>): List { + val statuses = pairs.map { it.first } + return pairs + .map { + if (it.second != null) { + it.first.copy(reblog = statuses.find { (id) -> id == it.second.toString() }) + } else { + it.first + } + } + .map { + if (it.inReplyToId != null) { + println("statuses trace: $statuses") + println("inReplyToId trace: ${it.inReplyToId}") + it.copy(inReplyToAccountId = statuses.find { (id) -> id == it.inReplyToId }?.account?.id) + } else { + it + } + } + } + + private suspend fun findByPostIdsWithMedia(ids: List): List { + val pairs = Posts + .leftJoin(PostsMedia) + .leftJoin(PostsEmojis) + .leftJoin(CustomEmojis) + .leftJoin(Actors) + .leftJoin(Media) + .selectAll().where { Posts.id inList ids } + .groupBy { it[Posts.id] } + .map { it.value } + .map { + toStatus(it.first()).copy( + mediaAttachments = it.mapNotNull { resultRow -> + resultRow.toMediaOrNull()?.toMediaAttachments() + }, + emojis = it.mapNotNull { resultRow -> resultRow.toCustomEmojiOrNull()?.toMastodonEmoji() } + ) to it.first()[Posts.repostId] + } + return resolveReplyAndRepost(pairs) + } +} + +private fun CustomEmoji.toMastodonEmoji(): MastodonEmoji = MastodonEmoji( + shortcode = this.name, + url = this.url.toString(), + staticUrl = this.url.toString(), + visibleInPicker = true, + category = this.category.orEmpty() +) + +private fun toStatus(it: ResultRow) = Status( + id = it[Posts.id].toString(), + uri = it[Posts.apId], + createdAt = it[Posts.createdAt].toString(), + account = Account( + id = it[Actors.id].toString(), + username = it[Actors.name], + acct = "${it[Actors.name]}@${it[Actors.domain]}", + url = it[Actors.url], + displayName = it[Actors.screenName], + note = it[Actors.description], + avatar = it[Actors.url] + "/icon.jpg", + avatarStatic = it[Actors.url] + "/icon.jpg", + header = it[Actors.url] + "/header.jpg", + headerStatic = it[Actors.url] + "/header.jpg", + locked = it[Actors.locked], + fields = emptyList(), + emojis = emptyList(), + bot = false, + group = false, + discoverable = true, + createdAt = it[Actors.createdAt].toString(), + lastStatusAt = it[Actors.lastPostAt]?.toString(), + statusesCount = it[Actors.postsCount], + followersCount = it[Actors.followersCount], + followingCount = it[Actors.followingCount], + noindex = false, + moved = false, + suspended = false, + limited = false + ), + content = it[Posts.text], + visibility = when (Visibility.valueOf(it[Posts.visibility])) { + Visibility.PUBLIC -> public + Visibility.UNLISTED -> unlisted + Visibility.FOLLOWERS -> private + Visibility.DIRECT -> direct + }, + sensitive = it[Posts.sensitive], + spoilerText = it[Posts.overview].orEmpty(), + mediaAttachments = emptyList(), + mentions = emptyList(), + tags = emptyList(), + emojis = emptyList(), + reblogsCount = 0, + favouritesCount = 0, + repliesCount = 0, + url = it[Posts.apId], + inReplyToId = it[Posts.replyId]?.toString(), + inReplyToAccountId = null, + language = null, + text = it[Posts.text], + editedAt = null +) + +fun ResultRow.toMedia(): EntityMedia { + val fileType = FileType.valueOf(this[Media.type]) + val mimeType = this[Media.mimeType] + return EntityMedia( + id = MediaId(this[Media.id]), + name = MediaName(this[Media.name]), + url = URI.create(this[Media.url]), + remoteUrl = this[Media.remoteUrl]?.let { URI.create(it) }, + thumbnailUrl = this[Media.thumbnailUrl]?.let { URI.create(it) }, + type = fileType, + blurHash = this[Media.blurhash]?.let { MediaBlurHash(it) }, + mimeType = MimeType(mimeType.substringBefore("/"), mimeType.substringAfter("/"), fileType), + description = this[Media.description]?.let { MediaDescription(it) } + ) +} + +fun ResultRow.toMediaOrNull(): EntityMedia? { + val fileType = FileType.valueOf(this.getOrNull(Media.type) ?: return null) + val mimeType = this.getOrNull(Media.mimeType) ?: return null + return EntityMedia( + id = MediaId(this.getOrNull(Media.id) ?: return null), + name = MediaName(this.getOrNull(Media.name) ?: return null), + url = URI.create(this.getOrNull(Media.url) ?: return null), + remoteUrl = this[Media.remoteUrl]?.let { URI.create(it) }, + thumbnailUrl = this[Media.thumbnailUrl]?.let { URI.create(it) }, + type = FileType.valueOf(this[Media.type]), + blurHash = this[Media.blurhash]?.let { MediaBlurHash(it) }, + mimeType = MimeType(mimeType.substringBefore("/"), mimeType.substringAfter("/"), fileType), + description = MediaDescription(this[Media.description] ?: return null) + ) +} + +fun EntityMedia.toMediaAttachments(): MediaAttachment = MediaAttachment( + id = id.toString(), + type = when (type) { + FileType.Image -> MediaAttachment.Type.image + FileType.Video -> MediaAttachment.Type.video + FileType.Audio -> MediaAttachment.Type.audio + FileType.Unknown -> MediaAttachment.Type.unknown + }, + url = url.toString(), + previewUrl = thumbnailUrl?.toString(), + remoteUrl = remoteUrl?.toString(), + description = description?.description, + blurhash = blurHash?.hash, + textUrl = url.toString() +) \ No newline at end of file diff --git a/hideout-mastodon/src/main/kotlin/dev/usbharu/hideout/mastodon/interfaces/api/SpringAccountApi.kt b/hideout-mastodon/src/main/kotlin/dev/usbharu/hideout/mastodon/interfaces/api/SpringAccountApi.kt new file mode 100644 index 00000000..4a3dd110 --- /dev/null +++ b/hideout-mastodon/src/main/kotlin/dev/usbharu/hideout/mastodon/interfaces/api/SpringAccountApi.kt @@ -0,0 +1,228 @@ +/* + * Copyright (C) 2024 usbharu + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package dev.usbharu.hideout.mastodon.interfaces.api + +import dev.usbharu.hideout.core.application.actor.GetUserDetail +import dev.usbharu.hideout.core.application.actor.GetUserDetailApplicationService +import dev.usbharu.hideout.core.application.relationship.acceptfollowrequest.AcceptFollowRequest +import dev.usbharu.hideout.core.application.relationship.acceptfollowrequest.UserAcceptFollowRequestApplicationService +import dev.usbharu.hideout.core.application.relationship.block.Block +import dev.usbharu.hideout.core.application.relationship.block.UserBlockApplicationService +import dev.usbharu.hideout.core.application.relationship.followrequest.FollowRequest +import dev.usbharu.hideout.core.application.relationship.followrequest.UserFollowRequestApplicationService +import dev.usbharu.hideout.core.application.relationship.get.GetRelationship +import dev.usbharu.hideout.core.application.relationship.get.GetRelationshipApplicationService +import dev.usbharu.hideout.core.application.relationship.mute.Mute +import dev.usbharu.hideout.core.application.relationship.mute.UserMuteApplicationService +import dev.usbharu.hideout.core.application.relationship.rejectfollowrequest.RejectFollowRequest +import dev.usbharu.hideout.core.application.relationship.rejectfollowrequest.UserRejectFollowRequestApplicationService +import dev.usbharu.hideout.core.application.relationship.removefromfollowers.RemoveFromFollowers +import dev.usbharu.hideout.core.application.relationship.removefromfollowers.UserRemoveFromFollowersApplicationService +import dev.usbharu.hideout.core.application.relationship.unblock.Unblock +import dev.usbharu.hideout.core.application.relationship.unblock.UserUnblockApplicationService +import dev.usbharu.hideout.core.application.relationship.unfollow.Unfollow +import dev.usbharu.hideout.core.application.relationship.unfollow.UserUnfollowApplicationService +import dev.usbharu.hideout.core.application.relationship.unmute.Unmute +import dev.usbharu.hideout.core.application.relationship.unmute.UserUnmuteApplicationService +import dev.usbharu.hideout.core.infrastructure.springframework.oauth2.SpringSecurityOauth2PrincipalContextHolder +import dev.usbharu.hideout.mastodon.application.accounts.GetAccount +import dev.usbharu.hideout.mastodon.application.accounts.GetAccountApplicationService +import dev.usbharu.hideout.mastodon.interfaces.api.generated.AccountApi +import dev.usbharu.hideout.mastodon.interfaces.api.generated.model.* +import org.springframework.http.ResponseEntity +import org.springframework.stereotype.Controller + +@Controller +class SpringAccountApi( + private val getUserDetailApplicationService: GetUserDetailApplicationService, + private val getAccountApplicationService: GetAccountApplicationService, + private val userFollowRequestApplicationService: UserFollowRequestApplicationService, + private val getRelationshipApplicationService: GetRelationshipApplicationService, + private val userBlockApplicationService: UserBlockApplicationService, + private val userUnblockApplicationService: UserUnblockApplicationService, + private val userMuteApplicationService: UserMuteApplicationService, + private val userUnmuteApplicationService: UserUnmuteApplicationService, + private val userAcceptFollowRequestApplicationService: UserAcceptFollowRequestApplicationService, + private val userRejectFollowRequestApplicationService: UserRejectFollowRequestApplicationService, + private val userRemoveFromFollowersApplicationService: UserRemoveFromFollowersApplicationService, + private val userUnfollowApplicationService: UserUnfollowApplicationService, + private val principalContextHolder: SpringSecurityOauth2PrincipalContextHolder +) : AccountApi { + + + override suspend fun apiV1AccountsIdBlockPost(id: String): ResponseEntity { + userBlockApplicationService.execute(Block(id.toLong()), principalContextHolder.getPrincipal()) + return fetchRelationship(id) + } + + override suspend fun apiV1AccountsIdFollowPost( + id: String, + followRequestBody: FollowRequestBody?, + ): ResponseEntity { + + userFollowRequestApplicationService.execute( + FollowRequest(id.toLong()), principalContextHolder.getPrincipal() + ) + return fetchRelationship(id) + } + + private suspend fun fetchRelationship( + id: String, + ): ResponseEntity { + val relationship = getRelationshipApplicationService.execute( + GetRelationship(id.toLong()), + principalContextHolder.getPrincipal() + ) + return ResponseEntity.ok( + Relationship( + id = relationship.targetId.toString(), + following = relationship.following, + showingReblogs = true, + notifying = false, + followedBy = relationship.followedBy, + blocking = relationship.blocking, + blockedBy = relationship.blockedBy, + muting = relationship.muting, + mutingNotifications = false, + requested = relationship.followRequesting, + domainBlocking = relationship.domainBlocking, + endorsed = false, + note = "" + ) + ) + } + + override suspend fun apiV1AccountsIdGet(id: String): ResponseEntity { + return ResponseEntity.ok( + getAccountApplicationService.execute( + GetAccount(id), principalContextHolder.getPrincipal() + ) + ) + } + + override suspend fun apiV1AccountsIdMutePost(id: String): ResponseEntity { + userMuteApplicationService.execute( + Mute(id.toLong()), principalContextHolder.getPrincipal() + ) + return fetchRelationship(id) + } + + override suspend fun apiV1AccountsIdRemoveFromFollowersPost(id: String): ResponseEntity { + userRemoveFromFollowersApplicationService.execute( + RemoveFromFollowers(id.toLong()), principalContextHolder.getPrincipal() + ) + return fetchRelationship(id) + } + + override suspend fun apiV1AccountsIdUnblockPost(id: String): ResponseEntity { + userUnblockApplicationService.execute( + Unblock(id.toLong()), principalContextHolder.getPrincipal() + ) + return fetchRelationship(id) + } + + override suspend fun apiV1AccountsIdUnfollowPost(id: String): ResponseEntity { + userUnfollowApplicationService.execute( + Unfollow(id.toLong()), principalContextHolder.getPrincipal() + ) + return fetchRelationship(id) + } + + override suspend fun apiV1AccountsIdUnmutePost(id: String): ResponseEntity { + userUnmuteApplicationService.execute( + Unmute(id.toLong()), principalContextHolder.getPrincipal() + ) + return fetchRelationship(id) + } + + override suspend fun apiV1AccountsPost(accountsCreateRequest: AccountsCreateRequest): ResponseEntity { + return super.apiV1AccountsPost(accountsCreateRequest) + } + + override suspend fun apiV1AccountsUpdateCredentialsPatch(updateCredentials: UpdateCredentials?): ResponseEntity { + return super.apiV1AccountsUpdateCredentialsPatch(updateCredentials) + } + + override suspend fun apiV1AccountsVerifyCredentialsGet(): ResponseEntity { + val principal = principalContextHolder.getPrincipal() + val localActor = + getUserDetailApplicationService.execute(GetUserDetail(principal.userDetailId.id), principal) + + return ResponseEntity.ok( + CredentialAccount( + id = localActor.id.toString(), + username = localActor.name, + acct = localActor.name + "@" + localActor.domain, + url = localActor.url, + displayName = localActor.screenName, + note = localActor.description, + avatar = localActor.iconUrl, + avatarStatic = localActor.iconUrl, + header = localActor.iconUrl, + headerStatic = localActor.iconUrl, + locked = localActor.locked, + fields = emptyList(), + emojis = localActor.emojis.map { + CustomEmoji( + shortcode = it.name, + url = it.url.toString(), + staticUrl = it.url.toString(), + true, + category = it.category.orEmpty() + ) + }, + bot = false, + group = false, + discoverable = true, + createdAt = localActor.createdAt.toString(), + lastStatusAt = localActor.lastPostAt?.toString(), + statusesCount = localActor.postsCount, + followersCount = localActor.followersCount, + followingCount = localActor.followingCount, + moved = localActor.moveTo != null, + noindex = true, + suspendex = localActor.suspend, + limited = false, + role = null, + source = AccountSource( + localActor.description, + emptyList(), + AccountSource.Privacy.`public`, + false, + 0 + ) + ) + ) + } + + override suspend fun apiV1FollowRequestsAccountIdAuthorizePost(accountId: String): ResponseEntity { + + userAcceptFollowRequestApplicationService.execute( + AcceptFollowRequest(accountId.toLong()), principalContextHolder.getPrincipal() + ) + return fetchRelationship(accountId) + } + + override suspend fun apiV1FollowRequestsAccountIdRejectPost(accountId: String): ResponseEntity { + + userRejectFollowRequestApplicationService.execute( + RejectFollowRequest(accountId.toLong()), principalContextHolder.getPrincipal() + ) + return fetchRelationship(accountId) + } + +} \ No newline at end of file diff --git a/hideout-mastodon/src/main/kotlin/dev/usbharu/hideout/mastodon/interfaces/api/SpringAppApi.kt b/hideout-mastodon/src/main/kotlin/dev/usbharu/hideout/mastodon/interfaces/api/SpringAppApi.kt new file mode 100644 index 00000000..af17a724 --- /dev/null +++ b/hideout-mastodon/src/main/kotlin/dev/usbharu/hideout/mastodon/interfaces/api/SpringAppApi.kt @@ -0,0 +1,51 @@ +/* + * Copyright (C) 2024 usbharu + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package dev.usbharu.hideout.mastodon.interfaces.api + +import dev.usbharu.hideout.core.application.application.RegisterApplication +import dev.usbharu.hideout.core.application.application.RegisterApplicationApplicationService +import dev.usbharu.hideout.core.domain.model.support.principal.Anonymous +import dev.usbharu.hideout.mastodon.interfaces.api.generated.AppApi +import dev.usbharu.hideout.mastodon.interfaces.api.generated.model.Application +import dev.usbharu.hideout.mastodon.interfaces.api.generated.model.AppsRequest +import org.springframework.http.ResponseEntity +import org.springframework.stereotype.Controller +import java.net.URI + +@Controller +class SpringAppApi(private val registerApplicationApplicationService: RegisterApplicationApplicationService) : AppApi { + override suspend fun apiV1AppsPost(appsRequest: AppsRequest): ResponseEntity { + + val registerApplication = RegisterApplication( + appsRequest.clientName, + setOf(URI.create(appsRequest.redirectUris)), + false, + appsRequest.scopes?.split(" ").orEmpty().toSet().ifEmpty { setOf("read") } + ) + val registeredApplication = registerApplicationApplicationService.execute(registerApplication, Anonymous) + return ResponseEntity.ok( + Application( + registeredApplication.name, + "invalid-vapid-key", + null, + registeredApplication.clientId, + registeredApplication.clientSecret, + appsRequest.redirectUris + ) + ) + } +} \ No newline at end of file diff --git a/hideout-mastodon/src/main/kotlin/dev/usbharu/hideout/mastodon/interfaces/api/SpringFilterApi.kt b/hideout-mastodon/src/main/kotlin/dev/usbharu/hideout/mastodon/interfaces/api/SpringFilterApi.kt new file mode 100644 index 00000000..c5748dab --- /dev/null +++ b/hideout-mastodon/src/main/kotlin/dev/usbharu/hideout/mastodon/interfaces/api/SpringFilterApi.kt @@ -0,0 +1,231 @@ +/* + * Copyright (C) 2024 usbharu + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package dev.usbharu.hideout.mastodon.interfaces.api + +import dev.usbharu.hideout.core.application.filter.* +import dev.usbharu.hideout.core.domain.model.filter.FilterAction +import dev.usbharu.hideout.core.domain.model.filter.FilterContext +import dev.usbharu.hideout.core.domain.model.filter.FilterMode +import dev.usbharu.hideout.core.domain.model.support.principal.PrincipalContextHolder +import dev.usbharu.hideout.mastodon.application.filter.DeleteFilterV1 +import dev.usbharu.hideout.mastodon.application.filter.DeleteFilterV1ApplicationService +import dev.usbharu.hideout.mastodon.application.filter.GetFilterV1 +import dev.usbharu.hideout.mastodon.application.filter.GetFilterV1ApplicationService +import dev.usbharu.hideout.mastodon.interfaces.api.generated.FilterApi +import dev.usbharu.hideout.mastodon.interfaces.api.generated.model.* +import dev.usbharu.hideout.mastodon.interfaces.api.generated.model.Filter +import dev.usbharu.hideout.mastodon.interfaces.api.generated.model.FilterKeyword +import dev.usbharu.hideout.mastodon.interfaces.api.generated.model.FilterPostRequest.Context +import dev.usbharu.hideout.mastodon.interfaces.api.generated.model.V1FilterPostRequest.Context.* +import org.springframework.http.ResponseEntity +import org.springframework.stereotype.Controller + +@Controller +class SpringFilterApi( + private val userRegisterFilterApplicationService: UserRegisterFilterApplicationService, + private val getFilterV1ApplicationService: GetFilterV1ApplicationService, + private val deleteFilterV1ApplicationService: DeleteFilterV1ApplicationService, + private val userDeleteFilterApplicationService: UserDeleteFilterApplicationService, + private val userGetFilterApplicationService: UserGetFilterApplicationService, + private val principalContextHolder: PrincipalContextHolder +) : FilterApi { + + override suspend fun apiV1FiltersIdDelete(id: String): ResponseEntity { + return ResponseEntity.ok( + deleteFilterV1ApplicationService.execute( + DeleteFilterV1(id.toLong()), principalContextHolder.getPrincipal() + ) + ) + } + + override suspend fun apiV1FiltersIdGet(id: String): ResponseEntity { + return ResponseEntity.ok( + getFilterV1ApplicationService.execute( + GetFilterV1(id.toLong()), principalContextHolder.getPrincipal() + ) + ) + } + + override suspend fun apiV1FiltersIdPut( + id: String, + phrase: String?, + context: List?, + irreversible: Boolean?, + wholeWord: Boolean?, + expiresIn: Int?, + ): ResponseEntity { + return super.apiV1FiltersIdPut(id, phrase, context, irreversible, wholeWord, expiresIn) + } + + override suspend fun apiV1FiltersPost(v1FilterPostRequest: V1FilterPostRequest): ResponseEntity { + + val filterMode = if (v1FilterPostRequest.wholeWord == true) { + FilterMode.WHOLE_WORD + } else { + FilterMode.NONE + } + val filterContext = v1FilterPostRequest.context.map { + when (it) { + home -> FilterContext.HOME + notifications -> FilterContext.NOTIFICATION + public -> FilterContext.PUBLIC + thread -> FilterContext.THREAD + account -> FilterContext.ACCOUNT + } + }.toSet() + val filter = userRegisterFilterApplicationService.execute( + RegisterFilter( + v1FilterPostRequest.phrase, filterContext, FilterAction.WARN, + setOf(RegisterFilterKeyword(v1FilterPostRequest.phrase, filterMode)) + ), principalContextHolder.getPrincipal() + ) + return ResponseEntity.ok( + getFilterV1ApplicationService.execute( + GetFilterV1(filter.filterKeywords.first().id), principalContextHolder.getPrincipal() + ) + ) + } + + override suspend fun apiV2FiltersFilterIdKeywordsPost( + filterId: String, + filterKeywordsPostRequest: FilterKeywordsPostRequest, + ): ResponseEntity { + return super.apiV2FiltersFilterIdKeywordsPost(filterId, filterKeywordsPostRequest) + } + + override suspend fun apiV2FiltersFilterIdStatusesPost( + filterId: String, + filterStatusRequest: FilterStatusRequest, + ): ResponseEntity { + return super.apiV2FiltersFilterIdStatusesPost(filterId, filterStatusRequest) + } + + override suspend fun apiV2FiltersIdDelete(id: String): ResponseEntity { + userDeleteFilterApplicationService.execute( + DeleteFilter(id.toLong()), principalContextHolder.getPrincipal() + ) + return ResponseEntity.ok(Unit) + } + + override suspend fun apiV2FiltersIdGet(id: String): ResponseEntity { + val filter = userGetFilterApplicationService.execute( + GetFilter(id.toLong()), principalContextHolder.getPrincipal() + ) + return ResponseEntity.ok( + filter(filter) + ) + } + + private fun filter(filter: dev.usbharu.hideout.core.application.filter.Filter) = Filter( + id = filter.filterId.toString(), + title = filter.name, + context = filter.filterContext.map { + when (it) { + FilterContext.HOME -> Filter.Context.home + FilterContext.NOTIFICATION -> Filter.Context.notifications + FilterContext.PUBLIC -> Filter.Context.public + FilterContext.THREAD -> Filter.Context.thread + FilterContext.ACCOUNT -> Filter.Context.account + } + }, + expiresAt = null, + filterAction = when (filter.filterAction) { + FilterAction.WARN -> Filter.FilterAction.warn + FilterAction.HIDE -> Filter.FilterAction.hide + + }, + keywords = filter.filterKeywords.map { + FilterKeyword( + it.id.toString(), + it.keyword, + it.filterMode == FilterMode.WHOLE_WORD + ) + }, statuses = null + ) + + override suspend fun apiV2FiltersIdPut( + id: String, + title: String?, + context: List?, + filterAction: String?, + expiresIn: Int?, + keywordsAttributes: List?, + ): ResponseEntity { + return super.apiV2FiltersIdPut(id, title, context, filterAction, expiresIn, keywordsAttributes) + } + + override suspend fun apiV2FiltersKeywordsIdDelete(id: String): ResponseEntity { + return super.apiV2FiltersKeywordsIdDelete(id) + } + + override suspend fun apiV2FiltersKeywordsIdGet(id: String): ResponseEntity { + return super.apiV2FiltersKeywordsIdGet(id) + } + + override suspend fun apiV2FiltersKeywordsIdPut( + id: String, + keyword: String?, + wholeWord: Boolean?, + regex: Boolean?, + ): ResponseEntity { + return super.apiV2FiltersKeywordsIdPut(id, keyword, wholeWord, regex) + } + + override suspend fun apiV2FiltersPost(filterPostRequest: FilterPostRequest): ResponseEntity { + + val filter = userRegisterFilterApplicationService.execute( + RegisterFilter( + filterName = filterPostRequest.title, + filterContext = filterPostRequest.context.map { + when (it) { + Context.home -> FilterContext.HOME + Context.notifications -> FilterContext.NOTIFICATION + Context.public -> FilterContext.PUBLIC + Context.thread -> FilterContext.THREAD + Context.account -> FilterContext.ACCOUNT + } + }.toSet(), + filterAction = when (filterPostRequest.filterAction) { + FilterPostRequest.FilterAction.warn -> FilterAction.WARN + FilterPostRequest.FilterAction.hide -> FilterAction.HIDE + null -> FilterAction.WARN + }, + filterKeywords = filterPostRequest.keywordsAttributes.orEmpty().map { + RegisterFilterKeyword( + it.keyword, + if (it.regex == true) { + FilterMode.REGEX + } else if (it.wholeWord == true) { + FilterMode.WHOLE_WORD + } else { + FilterMode.NONE + } + ) + }.toSet() + ), principalContextHolder.getPrincipal() + ) + return ResponseEntity.ok(filter(filter)) + } + + override suspend fun apiV2FiltersStatusesIdDelete(id: String): ResponseEntity { + return ResponseEntity.notFound().build() + } + + override suspend fun apiV2FiltersStatusesIdGet(id: String): ResponseEntity { + return ResponseEntity.notFound().build() + } +} \ No newline at end of file diff --git a/hideout-mastodon/src/main/kotlin/dev/usbharu/hideout/mastodon/interfaces/api/SpringInstanceApi.kt b/hideout-mastodon/src/main/kotlin/dev/usbharu/hideout/mastodon/interfaces/api/SpringInstanceApi.kt new file mode 100644 index 00000000..b4aa3cf8 --- /dev/null +++ b/hideout-mastodon/src/main/kotlin/dev/usbharu/hideout/mastodon/interfaces/api/SpringInstanceApi.kt @@ -0,0 +1,39 @@ +/* + * Copyright (C) 2024 usbharu + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package dev.usbharu.hideout.mastodon.interfaces.api + +import dev.usbharu.hideout.mastodon.interfaces.api.generated.InstanceApi +import dev.usbharu.hideout.mastodon.interfaces.api.generated.model.* +import kotlinx.coroutines.flow.Flow +import org.springframework.http.ResponseEntity +import org.springframework.stereotype.Controller + +@Controller +class SpringInstanceApi : InstanceApi { + + override suspend fun apiV1InstanceExtendedDescriptionGet(): ResponseEntity { + return super.apiV1InstanceExtendedDescriptionGet() + } + + override suspend fun apiV1InstanceGet(): ResponseEntity { + return super.apiV1InstanceGet() + } + + override suspend fun apiV2InstanceGet(): ResponseEntity { + return super.apiV2InstanceGet() + } +} \ No newline at end of file diff --git a/hideout-mastodon/src/main/kotlin/dev/usbharu/hideout/mastodon/interfaces/api/SpringMediaApi.kt b/hideout-mastodon/src/main/kotlin/dev/usbharu/hideout/mastodon/interfaces/api/SpringMediaApi.kt new file mode 100644 index 00000000..c22afbc7 --- /dev/null +++ b/hideout-mastodon/src/main/kotlin/dev/usbharu/hideout/mastodon/interfaces/api/SpringMediaApi.kt @@ -0,0 +1,76 @@ +/* + * Copyright (C) 2024 usbharu + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package dev.usbharu.hideout.mastodon.interfaces.api + +import dev.usbharu.hideout.core.application.media.UploadMedia +import dev.usbharu.hideout.core.application.media.UploadMediaApplicationService +import dev.usbharu.hideout.core.domain.model.media.FileType.* +import dev.usbharu.hideout.core.domain.model.support.principal.PrincipalContextHolder +import dev.usbharu.hideout.mastodon.interfaces.api.generated.MediaApi +import dev.usbharu.hideout.mastodon.interfaces.api.generated.model.MediaAttachment +import org.springframework.http.ResponseEntity +import org.springframework.stereotype.Controller +import org.springframework.web.multipart.MultipartFile +import java.nio.file.Files + +@Controller +class SpringMediaApi( + private val uploadMediaApplicationService: UploadMediaApplicationService, + private val principalContextHolder: PrincipalContextHolder +) : MediaApi { + override suspend fun apiV1MediaPost( + file: MultipartFile, + thumbnail: MultipartFile?, + description: String?, + focus: String?, + ): ResponseEntity { + val tempFile = Files.createTempFile("hideout-tmp-file", ".tmp") + + Files.newOutputStream(tempFile).use { outputStream -> + file.inputStream.use { + it.transferTo(outputStream) + } + } + + val media = uploadMediaApplicationService.execute( + UploadMedia( + tempFile, + file.originalFilename ?: file.name, + null, + description + ), principalContextHolder.getPrincipal() + ) + + return ResponseEntity.ok( + MediaAttachment( + media.id.toString(), + when (media.type) { + Image -> MediaAttachment.Type.image + Video -> MediaAttachment.Type.video + Audio -> MediaAttachment.Type.audio + Unknown -> MediaAttachment.Type.unknown + }, + media.url.toString(), + media.thumbprintURI?.toString(), + media.remoteURL?.toString(), + media.description, + media.blurHash, + media.url.toASCIIString() + ) + ) + } +} \ No newline at end of file diff --git a/hideout-mastodon/src/main/kotlin/dev/usbharu/hideout/mastodon/interfaces/api/SpringNotificationApi.kt b/hideout-mastodon/src/main/kotlin/dev/usbharu/hideout/mastodon/interfaces/api/SpringNotificationApi.kt new file mode 100644 index 00000000..cd196658 --- /dev/null +++ b/hideout-mastodon/src/main/kotlin/dev/usbharu/hideout/mastodon/interfaces/api/SpringNotificationApi.kt @@ -0,0 +1,38 @@ +/* + * Copyright (C) 2024 usbharu + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package dev.usbharu.hideout.mastodon.interfaces.api + +import dev.usbharu.hideout.mastodon.interfaces.api.generated.NotificationsApi +import dev.usbharu.hideout.mastodon.interfaces.api.generated.model.Notification +import kotlinx.coroutines.flow.Flow +import org.springframework.http.ResponseEntity +import org.springframework.stereotype.Controller + +@Controller +class SpringNotificationApi : NotificationsApi { + override suspend fun apiV1NotificationsClearPost(): ResponseEntity { + return super.apiV1NotificationsClearPost() + } + + override suspend fun apiV1NotificationsIdDismissPost(id: String): ResponseEntity { + return super.apiV1NotificationsIdDismissPost(id) + } + + override suspend fun apiV1NotificationsIdGet(id: String): ResponseEntity { + return super.apiV1NotificationsIdGet(id) + } +} \ No newline at end of file diff --git a/hideout-mastodon/src/main/kotlin/dev/usbharu/hideout/mastodon/interfaces/api/SpringStatusApi.kt b/hideout-mastodon/src/main/kotlin/dev/usbharu/hideout/mastodon/interfaces/api/SpringStatusApi.kt new file mode 100644 index 00000000..0fe04e58 --- /dev/null +++ b/hideout-mastodon/src/main/kotlin/dev/usbharu/hideout/mastodon/interfaces/api/SpringStatusApi.kt @@ -0,0 +1,82 @@ +/* + * Copyright (C) 2024 usbharu + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package dev.usbharu.hideout.mastodon.interfaces.api + +import dev.usbharu.hideout.core.application.post.RegisterLocalPost +import dev.usbharu.hideout.core.application.post.RegisterLocalPostApplicationService +import dev.usbharu.hideout.core.domain.model.post.Visibility +import dev.usbharu.hideout.core.domain.model.support.principal.PrincipalContextHolder +import dev.usbharu.hideout.mastodon.application.status.GetStatus +import dev.usbharu.hideout.mastodon.application.status.GetStatusApplicationService +import dev.usbharu.hideout.mastodon.interfaces.api.generated.StatusApi +import dev.usbharu.hideout.mastodon.interfaces.api.generated.model.Status +import dev.usbharu.hideout.mastodon.interfaces.api.generated.model.StatusesRequest +import dev.usbharu.hideout.mastodon.interfaces.api.generated.model.StatusesRequest.Visibility.* +import org.springframework.http.ResponseEntity +import org.springframework.stereotype.Controller + +@Controller +class SpringStatusApi( + private val registerLocalPostApplicationService: RegisterLocalPostApplicationService, + private val getStatusApplicationService: GetStatusApplicationService, + private val principalContextHolder: PrincipalContextHolder +) : StatusApi { + override suspend fun apiV1StatusesIdEmojiReactionsEmojiDelete(id: String, emoji: String): ResponseEntity { + return super.apiV1StatusesIdEmojiReactionsEmojiDelete(id, emoji) + } + + override suspend fun apiV1StatusesIdEmojiReactionsEmojiPut(id: String, emoji: String): ResponseEntity { + return super.apiV1StatusesIdEmojiReactionsEmojiPut(id, emoji) + } + + override suspend fun apiV1StatusesIdGet(id: String): ResponseEntity { + + return ResponseEntity.ok( + getStatusApplicationService.execute( + GetStatus(id), principalContextHolder.getPrincipal() + ) + ) + } + + override suspend fun apiV1StatusesPost(statusesRequest: StatusesRequest): ResponseEntity { + + val execute = registerLocalPostApplicationService.execute( + RegisterLocalPost( + content = statusesRequest.status.orEmpty(), + overview = statusesRequest.spoilerText, + visibility = when (statusesRequest.visibility) { + public -> Visibility.PUBLIC + unlisted -> Visibility.UNLISTED + private -> Visibility.FOLLOWERS + direct -> Visibility.DIRECT + null -> Visibility.PUBLIC + }, + repostId = null, + replyId = statusesRequest.inReplyToId?.toLong(), + sensitive = statusesRequest.sensitive == true, + mediaIds = statusesRequest.mediaIds.orEmpty().map { it.toLong() } + ), principalContextHolder.getPrincipal() + ) + + + val status = + getStatusApplicationService.execute(GetStatus(execute.toString()), principalContextHolder.getPrincipal()) + return ResponseEntity.ok( + status + ) + } +} \ No newline at end of file diff --git a/hideout-mastodon/src/main/kotlin/dev/usbharu/hideout/mastodon/interfaces/api/SpringTimelineApi.kt b/hideout-mastodon/src/main/kotlin/dev/usbharu/hideout/mastodon/interfaces/api/SpringTimelineApi.kt new file mode 100644 index 00000000..2f631d70 --- /dev/null +++ b/hideout-mastodon/src/main/kotlin/dev/usbharu/hideout/mastodon/interfaces/api/SpringTimelineApi.kt @@ -0,0 +1,23 @@ +/* + * Copyright (C) 2024 usbharu + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package dev.usbharu.hideout.mastodon.interfaces.api + +import dev.usbharu.hideout.mastodon.interfaces.api.generated.TimelineApi +import org.springframework.stereotype.Controller + +@Controller +class SpringTimelineApi : TimelineApi \ No newline at end of file diff --git a/hideout-mastodon/src/main/kotlin/dev/usbharu/hideout/mastodon/query/AccountQueryService.kt b/hideout-mastodon/src/main/kotlin/dev/usbharu/hideout/mastodon/query/AccountQueryService.kt new file mode 100644 index 00000000..61de616c --- /dev/null +++ b/hideout-mastodon/src/main/kotlin/dev/usbharu/hideout/mastodon/query/AccountQueryService.kt @@ -0,0 +1,24 @@ +/* + * Copyright (C) 2024 usbharu + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package dev.usbharu.hideout.mastodon.query + +import dev.usbharu.hideout.mastodon.interfaces.api.generated.model.Account + +interface AccountQueryService { + suspend fun findById(accountId: Long): Account? + suspend fun findByIds(accountIds: List): List +} \ No newline at end of file diff --git a/hideout-mastodon/src/main/kotlin/dev/usbharu/hideout/mastodon/query/StatusQueryService.kt b/hideout-mastodon/src/main/kotlin/dev/usbharu/hideout/mastodon/query/StatusQueryService.kt new file mode 100644 index 00000000..dc7cc88e --- /dev/null +++ b/hideout-mastodon/src/main/kotlin/dev/usbharu/hideout/mastodon/query/StatusQueryService.kt @@ -0,0 +1,45 @@ +/* + * Copyright (C) 2024 usbharu + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package dev.usbharu.hideout.mastodon.query + +import dev.usbharu.hideout.mastodon.interfaces.api.generated.model.Status + +interface StatusQueryService { + suspend fun findByPostIds(ids: List): List + suspend fun findByPostIdsWithMediaIds(statusQueries: List): List + + @Suppress("LongParameterList") + suspend fun accountsStatus( + accountId: Long, + onlyMedia: Boolean = false, + excludeReplies: Boolean = false, + excludeReblogs: Boolean = false, + pinned: Boolean = false, + tagged: String?, + includeFollowers: Boolean = false, + ): List + + suspend fun findByPostId(id: Long): Status? +} + +data class StatusQuery( + val postId: Long, + val replyId: Long?, + val repostId: Long?, + val mediaIds: List, + val emojiIds: List, +) \ No newline at end of file diff --git a/hideout-mastodon/src/main/resources/openapi/mastodon.yaml b/hideout-mastodon/src/main/resources/openapi/mastodon.yaml new file mode 100644 index 00000000..bc5195d5 --- /dev/null +++ b/hideout-mastodon/src/main/resources/openapi/mastodon.yaml @@ -0,0 +1,2989 @@ +openapi: 3.0.3 +info: + title: Hideout Mastodon Compatible API + description: Hideout Mastodon Compatible API + version: 1.0.0 +servers: + - url: 'https://test-hideout.usbharu.dev' + +tags: + - name: status + description: status + - name: account + description: account + - name: app + description: app + - name: instance + description: instance + - name: timeline + description: timeline + - name: media + description: media + - name: notification + description: notification + - name: filter + description: filter + +paths: + /api/v2/instance: + get: + tags: + - instance + security: + - { } + responses: + 200: + description: 成功 + content: + application/json: + schema: + $ref: "#/components/schemas/Instance" + + /api/v1/instance/peers: + get: + tags: + - instance + security: + - { } + responses: + 200: + description: 成功 + content: + application/json: + schema: + type: array + items: + type: string + + /api/v1/instance/activity: + get: + tags: + - instance + security: + - { } + responses: + 200: + description: 成功 + content: + application/json: + schema: + type: array + items: + $ref: "#/components/schemas/V1InstanceActivity" + + /api/v1/instance/rules: + get: + tags: + - instance + security: + - { } + responses: + 200: + description: 成功 + content: + application/json: + schema: + type: array + items: + $ref: "#/components/schemas/Rule" + + /api/v1/instance/domain_blocks: + get: + tags: + - instance + security: + - { } + responses: + 200: + description: 成功 + content: + application/json: + schema: + type: array + items: + $ref: "#/components/schemas/DomainBlock" + + /api/v1/instance/extended_description: + get: + tags: + - instance + security: + - { } + responses: + 200: + description: 成功 + content: + application/json: + schema: + $ref: "#/components/schemas/ExtendedDescription" + + /api/v1/instance: + get: + tags: + - instance + security: + - { } + responses: + 200: + description: 成功 + content: + application/json: + schema: + $ref: "#/components/schemas/V1Instance" + + /api/v1/statuses: + post: + tags: + - status + security: + - OAuth2: + - "write:statuses" + requestBody: + description: 投稿する内容 + required: true + content: + application/json: + schema: + $ref: "#/components/schemas/StatusesRequest" + application/x-www-form-urlencoded: + schema: + $ref: "#/components/schemas/StatusesRequest" + responses: + 200: + description: 成功 + content: + application/json: + schema: + $ref: "#/components/schemas/Status" + + /api/v1/statuses/{id}: + get: + tags: + - status + security: + - OAuth2: + - "write:statuses" + parameters: + - in: path + name: id + required: true + schema: + type: string + responses: + 200: + description: 成功 + content: + application/json: + schema: + $ref: "#/components/schemas/Status" + + /api/v1/statuses/{id}/emoji_reactions/{emoji}: + put: + tags: + - status + security: + - OAuth2: + - "write:statuses" + parameters: + - in: path + name: id + required: true + schema: + type: string + - in: path + name: emoji + required: true + schema: + type: string + responses: + 200: + description: 成功 + content: + application/json: + schema: + $ref: "#/components/schemas/Status" + + delete: + tags: + - status + security: + - OAuth2: + - "write:statuses" + parameters: + - in: path + name: id + required: true + schema: + type: string + - in: path + name: emoji + required: true + schema: + type: string + responses: + 200: + description: 成功 + content: + application/json: + schema: + $ref: "#/components/schemas/Status" + + + /api/v1/apps: + post: + tags: + - app + security: + - { } + requestBody: + description: 作成するApp + required: true + content: + application/json: + schema: + $ref: "#/components/schemas/AppsRequest" + application/x-www-form-urlencoded: + schema: + $ref: "#/components/schemas/AppsRequest" + + responses: + 200: + description: 成功 + content: + application/json: + schema: + $ref: "#/components/schemas/Application" + 422: + $ref: "#/components/responses/unprocessableEntity" + + /api/v1/accounts/verify_credentials: + get: + tags: + - account + security: + - OAuth2: + - "read:accounts" + responses: + 200: + description: 成功 + content: + application/json: + schema: + $ref: "#/components/schemas/CredentialAccount" + 401: + $ref: "#/components/responses/unauthorized" + 403: + $ref: "#/components/responses/forbidden" + 422: + $ref: "#/components/responses/unprocessableEntity" + + /api/v1/accounts: + post: + tags: + - account + security: + - OAuth2: + - "write:accounts" + requestBody: + required: true + content: + application/json: + schema: + $ref: "#/components/schemas/AccountsCreateRequest" + application/x-www-form-urlencoded: + schema: + $ref: "#/components/schemas/AccountsCreateRequest" + responses: + 200: + description: 成功 + 401: + $ref: "#/components/responses/unauthorized" + 429: + $ref: "#/components/responses/rateLimited" + + /api/v1/accounts/relationships: + get: + tags: + - account + security: + - OAuth2: + - "read:follows" + parameters: + - in: query + name: id[] + required: false + schema: + type: array + items: + type: string + - in: query + name: with_suspended + required: false + schema: + type: boolean + default: false + responses: + 200: + description: 成功 + content: + application/json: + schema: + type: array + items: + $ref: "#/components/schemas/Relationship" + 401: + $ref: "#/components/responses/unauthorized" + 422: + $ref: "#/components/responses/unprocessableEntity" + + /api/v1/accounts/update_credentials: + patch: + tags: + - account + security: + - OAuth2: + - "write:accounts" + requestBody: + required: false + content: + application/json: + schema: + $ref: "#/components/schemas/UpdateCredentials" + responses: + 200: + description: 成功 + content: + application/json: + schema: + $ref: "#/components/schemas/Account" + 401: + $ref: "#/components/responses/unauthorized" + 422: + $ref: "#/components/responses/unprocessableEntity" + + /api/v1/accounts/{id}: + get: + tags: + - account + security: + - { } + parameters: + - in: path + name: id + required: true + schema: + type: string + responses: + 200: + description: 成功 + content: + application/json: + schema: + $ref: "#/components/schemas/Account" + 401: + $ref: "#/components/responses/unauthorized" + 404: + $ref: "#/components/responses/notfound" + + /api/v1/accounts/{id}/follow: + post: + tags: + - account + security: + - OAuth2: + - "write:follows" + parameters: + - in: path + name: id + required: true + schema: + type: string + requestBody: + required: false + content: + application/json: + schema: + $ref: "#/components/schemas/FollowRequestBody" + application/x-www-form-urlencoded: + schema: + $ref: "#/components/schemas/FollowRequestBody" + responses: + 200: + description: 成功 + content: + application/json: + schema: + $ref: "#/components/schemas/Relationship" + 401: + $ref: "#/components/responses/unauthorized" + 403: + $ref: "#/components/responses/forbidden" + 422: + $ref: "#/components/responses/unprocessableEntity" + + /api/v1/accounts/{id}/block: + post: + tags: + - account + security: + - OAuth2: + - "write:blocks" + parameters: + - in: path + name: id + required: true + schema: + type: string + responses: + 200: + description: 成功 + content: + application/json: + schema: + $ref: "#/components/schemas/Relationship" + 401: + $ref: "#/components/responses/unauthorized" + 403: + $ref: "#/components/responses/forbidden" + 422: + $ref: "#/components/responses/unprocessableEntity" + + /api/v1/accounts/{id}/unfollow: + post: + tags: + - account + security: + - OAuth2: + - "write:follows" + parameters: + - in: path + name: id + required: true + schema: + type: string + responses: + 200: + description: 成功 + content: + application/json: + schema: + $ref: "#/components/schemas/Relationship" + 401: + $ref: "#/components/responses/unauthorized" + 403: + $ref: "#/components/responses/forbidden" + 422: + $ref: "#/components/responses/unprocessableEntity" + + /api/v1/accounts/{id}/unblock: + post: + tags: + - account + security: + - OAuth2: + - "write:blocks" + parameters: + - in: path + name: id + required: true + schema: + type: string + responses: + 200: + description: 成功 + content: + application/json: + schema: + $ref: "#/components/schemas/Relationship" + 401: + $ref: "#/components/responses/unauthorized" + 403: + $ref: "#/components/responses/forbidden" + 422: + $ref: "#/components/responses/unprocessableEntity" + + /api/v1/accounts/{id}/remove_from_followers: + post: + tags: + - account + security: + - OAuth2: + - "write:follows" + parameters: + - in: path + name: id + required: true + schema: + type: string + responses: + 200: + description: 成功 + content: + application/json: + schema: + $ref: "#/components/schemas/Relationship" + 401: + $ref: "#/components/responses/unauthorized" + 403: + $ref: "#/components/responses/forbidden" + 422: + $ref: "#/components/responses/unprocessableEntity" + + /api/v1/accounts/{id}/mute: + post: + tags: + - account + security: + - OAuth2: + - "write:mutes" + parameters: + - in: path + name: id + required: true + schema: + type: string + responses: + 200: + description: 成功 + content: + application/json: + schema: + $ref: "#/components/schemas/Relationship" + 401: + $ref: "#/components/responses/unauthorized" + 403: + $ref: "#/components/responses/forbidden" + 422: + $ref: "#/components/responses/unprocessableEntity" + + /api/v1/accounts/{id}/unmute: + post: + tags: + - account + security: + - OAuth2: + - "write:mutes" + parameters: + - in: path + name: id + required: true + schema: + type: string + responses: + 200: + description: 成功 + content: + application/json: + schema: + $ref: "#/components/schemas/Relationship" + 401: + $ref: "#/components/responses/unauthorized" + 403: + $ref: "#/components/responses/forbidden" + 422: + $ref: "#/components/responses/unprocessableEntity" + + /api/v1/mutes: + get: + tags: + - account + security: + - OAuth2: + - "read:mutes" + parameters: + - in: query + name: max_id + required: false + schema: + type: string + - in: query + name: since_id + required: false + schema: + type: string + - in: query + name: limit + schema: + type: integer + required: false + responses: + 200: + description: 成功 + content: + application/json: + schema: + items: + $ref: "#/components/schemas/Account" + 401: + $ref: "#/components/responses/unauthorized" + 403: + $ref: "#/components/responses/forbidden" + 422: + $ref: "#/components/responses/unprocessableEntity" + + /api/v1/accounts/{id}/statuses: + get: + tags: + - account + security: + - OAuth2: + - "read:statuses" + parameters: + - in: path + name: id + required: true + schema: + type: string + - in: query + name: max_id + required: false + schema: + type: string + - in: query + name: since_id + required: false + schema: + type: string + - in: query + name: min_id + required: false + schema: + type: string + - in: query + name: limit + required: false + schema: + type: integer + nullable: true + default: 20 + - in: query + name: only_media + required: false + schema: + type: boolean + default: false + - in: query + name: exclude_replies + required: false + schema: + type: boolean + default: false + - in: query + name: exclude_reblogs + required: false + schema: + type: boolean + default: false + - in: query + name: pinned + required: false + schema: + type: boolean + default: false + - in: query + required: false + name: tagged + schema: + type: string + + responses: + 200: + description: 成功 + content: + application/json: + schema: + type: array + items: + $ref: "#/components/schemas/Status" + + 401: + $ref: "#/components/responses/unauthorized" + 404: + $ref: "#/components/responses/notfound" + + /api/v1/timelines/public: + get: + tags: + - timeline + parameters: + - in: query + name: local + required: false + schema: + type: boolean + - in: query + name: remote + required: false + schema: + type: boolean + - in: query + name: only_media + required: false + schema: + type: boolean + - in: query + name: max_id + required: false + schema: + type: string + - in: query + name: since_id + required: false + schema: + type: string + - in: query + name: min_id + required: false + schema: + type: string + - in: query + name: limit + required: false + schema: + type: integer + responses: + 200: + description: 成功 + content: + application/json: + schema: + type: array + items: + $ref: "#/components/schemas/Status" + + /api/v1/timelines/home: + get: + tags: + - timeline + security: + - OAuth2: + - "read:statuses" + parameters: + - in: query + name: max_id + required: false + schema: + type: string + - in: query + name: since_id + required: false + schema: + type: string + - in: query + name: min_id + required: false + schema: + type: string + - in: query + name: limit + required: false + schema: + type: integer + responses: + 200: + description: 成功 + content: + application/json: + schema: + type: array + items: + $ref: "#/components/schemas/Status" + 206: + $ref: "#/components/responses/partialContent" + 401: + $ref: "#/components/responses/unauthorized" + + /api/v1/media: + post: + tags: + - media + security: + - OAuth2: + - "write:media" + requestBody: + required: true + content: + multipart/form-data: + schema: + $ref: "#/components/schemas/V1MediaRequest" + encoding: + file: + contentType: image/jpeg, image/png + responses: + 200: + description: 成功 + content: + application/json: + schema: + $ref: "#/components/schemas/MediaAttachment" + 401: + $ref: "#/components/responses/unauthorized" + 422: + $ref: "#/components/responses/unprocessableEntity" + + /api/v1/follow_requests: + get: + tags: + - account + security: + - OAuth2: + - "read:follows" + parameters: + - in: query + name: max_id + schema: + type: string + required: false + - in: query + name: since_id + schema: + type: string + required: false + - in: query + name: limit + schema: + type: integer + required: false + responses: + 200: + description: 成功 + headers: + Link: + schema: + type: string + description: ページネーション + content: + application/json: + schema: + type: array + items: + $ref: "#/components/schemas/Account" + + /api/v1/follow_requests/{account_id}/authorize: + post: + tags: + - account + security: + - OAuth2: + - "write:follows" + parameters: + - in: path + name: account_id + schema: + type: string + required: true + responses: + 200: + description: 成功 + content: + application/json: + schema: + $ref: "#/components/schemas/Relationship" + + /api/v1/follow_requests/{account_id}/reject: + post: + tags: + - account + security: + - OAuth2: + - "write:follows" + parameters: + - in: path + name: account_id + schema: + type: string + required: true + responses: + 200: + description: 成功 + content: + application/json: + schema: + $ref: "#/components/schemas/Relationship" + + /api/v1/notifications: + get: + tags: + - notifications + security: + - OAuth2: + - "read:notifications" + parameters: + - in: query + name: max_id + required: false + schema: + type: string + - in: query + name: since_id + required: false + schema: + type: string + - in: query + name: min_id + required: false + schema: + type: string + - in: query + name: limit + required: false + schema: + type: integer + - in: query + name: types[] + required: false + schema: + type: array + items: + type: string + - in: query + name: exclude_types[] + required: false + schema: + type: array + items: + type: string + - in: query + name: account_id + required: false + schema: + type: array + items: + type: string + responses: + 200: + description: 成功 + content: + application/json: + schema: + type: array + items: + $ref: "#/components/schemas/Notification" + + /api/v1/notifications/{id}: + get: + tags: + - notifications + security: + - OAuth2: + - "read:notifications" + parameters: + - in: path + required: true + name: id + schema: + type: string + responses: + 200: + description: 成功 + content: + application/json: + schema: + $ref: "#/components/schemas/Notification" + + /api/v1/notifications/clear: + post: + tags: + - notifications + security: + - OAuth2: + - "write:notifications" + responses: + 200: + description: 成功 + content: + application/json: + schema: + type: object + + /api/v1/notifications/{id}/dismiss: + post: + tags: + - notifications + security: + - OAuth2: + - "write:notifications" + parameters: + - in: path + name: id + required: true + schema: + type: string + responses: + 200: + description: 成功 + content: + application/json: + schema: + type: object + + /api/v2/filters: + get: + tags: + - filter + security: + - OAuth2: + - "read:filters" + responses: + 200: + description: 成功 + content: + application/json: + schema: + type: array + items: + $ref: "#/components/schemas/Filter" + post: + tags: + - filter + security: + - OAuth2: + - "write:filters" + requestBody: + required: true + content: + application/json: + schema: + $ref: "#/components/schemas/FilterPostRequest" + application/x-www-form-urlencoded: + schema: + $ref: "#/components/schemas/FilterPostRequest" + responses: + 200: + description: 成功 + content: + application/json: + schema: + $ref: "#/components/schemas/Filter" + + /api/v2/filters/{id}: + get: + tags: + - filter + security: + - OAuth2: + - "read:filters" + parameters: + - in: path + name: id + required: true + schema: + type: string + responses: + 200: + description: 成功 + content: + application/json: + schema: + $ref: "#/components/schemas/Filter" + put: + tags: + - filter + security: + - OAuth2: + - "write:filters" + parameters: + - in: path + name: id + required: true + schema: + type: string + requestBody: + required: true + content: + application/x-www-form-urlencoded: + schema: + $ref: "#/components/schemas/FilterPutRequest" + responses: + 200: + description: 成功 + content: + application/json: + schema: + $ref: "#/components/schemas/Filter" + delete: + tags: + - filter + security: + - OAuth2: + - "write:filters" + parameters: + - in: path + name: id + required: true + schema: + type: string + responses: + 200: + description: 成功 + content: + application/json: + schema: + type: object + + /api/v2/filters/{filter_id}/keywords: + get: + tags: + - filter + security: + - OAuth2: + - "read:filters" + parameters: + - in: path + name: filter_id + required: true + schema: + type: string + responses: + 200: + description: 成功 + content: + application/json: + schema: + type: array + items: + $ref: "#/components/schemas/FilterKeyword" + post: + tags: + - filter + security: + - OAuth2: + - "write:filters" + parameters: + - in: path + name: filter_id + required: true + schema: + type: string + requestBody: + required: true + content: + application/json: + schema: + $ref: "#/components/schemas/FilterKeywordsPostRequest" + application/x-www-form-urlencoded: + schema: + $ref: "#/components/schemas/FilterKeywordsPostRequest" + responses: + 200: + description: 成功 + content: + application/json: + schema: + $ref: "#/components/schemas/FilterKeyword" + + /api/v2/filters/keywords/{id}: + get: + tags: + - filter + security: + - OAuth2: + - "read:filters" + parameters: + - in: path + name: id + required: true + schema: + type: string + responses: + 200: + description: 成功 + content: + application/json: + schema: + $ref: "#/components/schemas/FilterKeyword" + + put: + tags: + - filter + security: + - OAuth2: + - "write:filters" + parameters: + - in: path + name: id + required: true + schema: + type: string + requestBody: + required: true + content: + application/x-www-form-urlencoded: + schema: + $ref: "#/components/schemas/FilterKeywordsPutRequest" + responses: + 200: + description: 成功 + content: + application/json: + schema: + $ref: "#/components/schemas/FilterKeyword" + + delete: + tags: + - filter + security: + - OAuth2: + - "write:filters" + parameters: + - in: path + name: id + required: true + schema: + type: string + responses: + 200: + description: 成功 + content: + application/json: + schema: + type: object + /api/v2/filters/{filter_id}/statuses: + get: + tags: + - filter + security: + - OAuth2: + - "read:filters" + parameters: + - in: path + name: filter_id + required: true + schema: + type: string + responses: + 200: + description: 成功 + content: + application/json: + schema: + type: array + items: + $ref: "#/components/schemas/FilterStatus" + post: + tags: + - filter + security: + - OAuth2: + - "write:filters" + parameters: + - in: path + name: filter_id + required: true + schema: + type: string + requestBody: + required: true + content: + application/json: + schema: + $ref: "#/components/schemas/FilterStatusRequest" + application/x-www-form-urlencoded: + schema: + $ref: "#/components/schemas/FilterStatusRequest" + responses: + 200: + description: 成功 + content: + application/json: + schema: + $ref: "#/components/schemas/FilterStatus" + + /api/v2/filters/statuses/{id}: + get: + tags: + - filter + security: + - OAuth2: + - "read:filters" + parameters: + - in: path + name: id + required: true + schema: + type: string + responses: + 200: + description: 成功 + content: + application/json: + schema: + $ref: "#/components/schemas/FilterStatus" + delete: + tags: + - filter + security: + - OAuth2: + - "write:filters" + parameters: + - in: path + name: id + required: true + schema: + type: string + responses: + 200: + description: 成功 + content: + application/json: + schema: + type: object + + + /api/v1/filters: + get: + tags: + - filter + security: + - OAuth2: + - "read:filters" + responses: + 200: + description: 成功 + content: + application/json: + schema: + type: array + items: + $ref: "#/components/schemas/V1Filter" + post: + tags: + - filter + security: + - OAuth2: + - "write:filters" + requestBody: + required: true + content: + application/json: + schema: + $ref: "#/components/schemas/V1FilterPostRequest" + application/x-www-form-urlencoded: + schema: + $ref: "#/components/schemas/V1FilterPostRequest" + responses: + 200: + description: 成功 + content: + application/json: + schema: + $ref: "#/components/schemas/V1Filter" + + /api/v1/filters/{id}: + get: + tags: + - filter + security: + - OAuth2: + - "read:filters" + parameters: + - in: path + name: id + required: true + schema: + type: string + responses: + 200: + description: 成功 + content: + application/json: + schema: + $ref: "#/components/schemas/V1Filter" + put: + tags: + - filter + security: + - OAuth2: + - "write:filters" + parameters: + - in: path + name: id + required: true + schema: + type: string + requestBody: + required: true + content: + application/x-www-form-urlencoded: + schema: + $ref: "#/components/schemas/V1FilterPutRequest" + responses: + 200: + description: 成功 + content: + application/json: + schema: + $ref: "#/components/schemas/V1Filter" + delete: + tags: + - filter + security: + - OAuth2: + - "write:filters" + parameters: + - in: path + name: id + required: true + schema: + type: string + responses: + 200: + description: 成功 + content: + application/json: + schema: + type: object + + +components: + schemas: + V1MediaRequest: + type: object + properties: + file: + type: string + format: binary + thumbnail: + type: string + format: binary + description: + type: string + maxLength: 4000 + focus: + type: string + required: + - file + + AccountsCreateRequest: + type: object + properties: + username: + + nullable: false + type: string + minLength: 1 + maxLength: 300 + pattern: '^[a-zA-Z0-9_-]{1,300}$' + email: + type: string + format: email + password: + type: string + format: password + agreement: + type: boolean + default: false + locale: + type: boolean + reason: + type: string + required: + - username + - password + + Token: + type: object + properties: + access_token: + type: string + token_type: + type: string + scope: + type: string + created_at: + type: integer + format: int64 + + Account: + type: object + properties: + id: + type: string + username: + type: string + minLength: 1 + pattern: '^[a-zA-Z0-9_-]{1,300}$' + acct: + type: string + url: + type: string + display_name: + type: string + note: + type: string + avatar: + type: string + avatar_static: + type: string + header: + type: string + header_static: + type: string + locked: + type: boolean + fields: + type: array + items: + $ref: "#/components/schemas/Field" + emojis: + type: array + items: + $ref: "#/components/schemas/CustomEmoji" + bot: + type: boolean + group: + type: boolean + discoverable: + type: boolean + nullable: true + noindex: + type: boolean + moved: + type: boolean + suspended: + type: boolean + limited: + type: boolean + created_at: + type: string + last_status_at: + type: string + nullable: true + statuses_count: + type: integer + followers_count: + type: integer + following_count: + type: integer + source: + $ref: "#/components/schemas/AccountSource" + + required: + - id + - username + - acct + - url + - display_name + - note + - avatar + - avatar_static + - header + - header_static + - locked + - fields + - emojis + - bot + - group + - discoverable + - created_at + - statuses_count + + CredentialAccount: + type: object + properties: + id: + type: string + username: + type: string + acct: + type: string + url: + type: string + display_name: + type: string + note: + type: string + avatar: + type: string + avatar_static: + type: string + header: + type: string + header_static: + type: string + locked: + type: boolean + fields: + type: array + items: + $ref: "#/components/schemas/Field" + emojis: + type: array + items: + $ref: "#/components/schemas/CustomEmoji" + bot: + type: boolean + group: + type: boolean + discoverable: + type: boolean + nullable: true + noindex: + type: boolean + moved: + type: boolean + suspendex: + type: boolean + limited: + type: boolean + created_at: + type: string + last_status_at: + type: string + nullable: true + statuses_count: + type: integer + followers_count: + type: integer + following_count: + type: integer + source: + $ref: "#/components/schemas/AccountSource" + role: + $ref: "#/components/schemas/Role" + required: + - id + - username + - acct + - url + - display_name + - note + - avatar + - avatar_static + - header + - header_static + - locked + - fields + - emojis + - bot + - group + - discoverable + - created_at + - last_status_at + - statuses_count + - source + + AccountSource: + type: object + properties: + note: + type: string + fields: + type: array + items: + $ref: "#/components/schemas/Field" + privacy: + type: string + enum: + - public + - unlisted + - private + - direct + sensitive: + type: boolean + follow_requests_count: + type: integer + + Role: + type: object + properties: + id: + type: integer + name: + type: string + color: + type: string + permissions: + type: integer + highlighted: + type: boolean + + Field: + type: object + properties: + name: + type: string + value: + type: string + verified_at: + type: string + nullable: true + required: + - name + - value + - verified_at + + CustomEmoji: + type: object + properties: + shortcode: + type: string + url: + type: string + static_url: + type: string + visible_in_picker: + type: boolean + category: + type: string + required: + - shortcode + - url + - static_url + - visible_in_picker + - category + + Status: + type: object + properties: + id: + type: string + uri: + type: string + created_at: + type: string + account: + $ref: "#/components/schemas/Account" + content: + type: string + visibility: + type: string + enum: + - public + - unlisted + - private + - direct + sensitive: + type: boolean + spoiler_text: + type: string + media_attachments: + type: array + items: + $ref: "#/components/schemas/MediaAttachment" + application: + $ref: "#/components/schemas/StatusApplication" + mentions: + type: array + items: + $ref: "#/components/schemas/StatusMention" + tags: + type: array + items: + $ref: "#/components/schemas/StatusTag" + emojis: + type: array + items: + $ref: "#/components/schemas/CustomEmoji" + reblogs_count: + type: integer + favourites_count: + type: integer + replies_count: + type: integer + url: + type: string + nullable: true + in_reply_to_id: + type: string + nullable: true + in_reply_to_account_id: + type: string + nullable: true + reblog: + $ref: "#/components/schemas/Status" + poll: + $ref: "#/components/schemas/Poll" + card: + $ref: "#/components/schemas/PreviewCard" + language: + type: string + nullable: true + default: null + text: + type: string + nullable: true + edited_at: + type: string + nullable: true + favourited: + type: boolean + reblogged: + type: boolean + muted: + type: boolean + bookmarked: + type: boolean + pinned: + type: boolean + filtered: + type: array + items: + $ref: "#/components/schemas/FilterResult" + required: + - id + - uri + - created_at + - account + - content + - visibility + - sensitive + - spoiler_text + - media_attachments + - mentions + - tags + - emojis + - reblogs_count + - favourites_count + - replies_count + - url + - text + + MediaAttachment: + type: object + properties: + id: + type: string + type: + type: string + enum: + - unknown + - image + - gifv + - video + - audio + url: + type: string + preview_url: + type: string + remote_url: + type: string + nullable: true + description: + type: string + blurhash: + type: string + text_url: + type: string + + StatusApplication: + type: object + properties: + name: + type: string + website: + type: string + nullable: true + + StatusMention: + type: object + properties: + id: + type: string + username: + type: string + url: + type: string + acct: + type: string + + StatusTag: + type: object + properties: + name: + type: string + url: + type: string + Poll: + type: object + properties: + id: + type: string + expires_at: + type: string + nullable: true + expired: + type: boolean + multiple: + type: boolean + votes_count: + type: integer + voters_count: + type: integer + nullable: true + options: + type: array + items: + $ref: "#/components/schemas/PollOption" + emojis: + type: array + items: + $ref: "#/components/schemas/CustomEmoji" + voted: + type: boolean + own_votes: + type: array + items: + type: integer + + PollOption: + type: object + properties: + title: + type: string + votes_count: + type: integer + nullable: true + + PreviewCard: + type: object + properties: + url: + type: string + title: + type: string + description: + type: string + type: + type: string + enum: + - link + - photo + - video + - rich + author_name: + type: string + author_url: + type: string + provider_name: + type: string + provider_url: + type: string + html: + type: string + width: + type: integer + height: + type: integer + image: + type: string + nullable: true + embed_url: + type: string + blurhash: + type: string + nullable: true + + FilterResult: + type: object + properties: + filter: + $ref: "#/components/schemas/FilterResult" + keyword_matches: + type: array + items: + type: string + nullable: true + status_matches: + type: string + nullable: true + + Filter: + type: object + properties: + id: + type: string + title: + type: string + context: + type: array + items: + type: string + enum: + - home + - notifications + - public + - thread + - account + expires_at: + type: string + nullable: true + filter_action: + type: string + enum: + - warn + - hide + keywords: + type: array + items: + $ref: "#/components/schemas/FilterKeyword" + statuses: + type: array + items: + $ref: "#/components/schemas/FilterStatus" + + FilterKeyword: + type: object + properties: + id: + type: string + keyword: + type: string + whole_word: + type: boolean + + FilterStatus: + type: object + properties: + id: + type: string + status_id: + type: string + + V1Filter: + type: object + properties: + id: + type: string + phrase: + type: string + context: + type: array + items: + enum: + - home + - notifications + - thread + - public + - account + expires_at: + type: string + irreversible: + type: boolean + whole_word: + type: boolean + + V1FilterPostRequest: + type: object + properties: + phrase: + type: string + maxLength: 1000 + context: + type: array + maxItems: 10 + items: + type: string + enum: + - home + - notifications + - public + - thread + - account + irreversible: + type: boolean + default: false + whole_word: + type: boolean + default: false + expires_in: + type: integer + required: + - phrase + - context + + V1FilterPutRequest: + type: object + properties: + phrase: + type: string + maxLength: 1000 + context: + type: array + maxItems: 10 + items: + type: string + enum: + - home + - notifications + - public + - thread + - account + irreversible: + type: boolean + whole_word: + type: boolean + expires_in: + type: integer + + FilterPostRequest: + type: object + properties: + title: + type: string + maxLength: 255 + context: + type: array + maxItems: 10 + items: + type: string + enum: + - home + - notifications + - public + - thread + - account + filter_action: + type: string + enum: + - warn + - hide + expires_in: + type: integer + keywords_attributes: + maxItems: 1000 + type: array + items: + $ref: "#/components/schemas/FilterPostRequestKeyword" + required: + - title + - context + + FilterPostRequestKeyword: + type: object + properties: + keyword: + type: string + maxLength: 1000 + whole_word: + type: boolean + default: false + regex: + type: boolean + default: false + required: + - keyword + + FilterKeywordsPostRequest: + type: object + properties: + keyword: + type: string + maxLength: 1000 + whole_word: + type: boolean + default: false + regex: + type: boolean + default: false + required: + - keyword + + FilterKeywordsPutRequest: + type: object + properties: + keyword: + type: string + maxLength: 1000 + whole_word: + type: boolean + regex: + type: boolean + + FilterPutRequest: + type: object + properties: + title: + type: string + maxLength: 255 + context: + type: array + maxItems: 10 + items: + type: string + enum: + - homa + - notifications + - public + - thread + - account + filter_action: + type: string + enum: + - warn + - hide + expires_in: + type: integer + keywords_attributes: + maxItems: 1000 + type: array + items: + $ref: "#/components/schemas/FilterPubRequestKeyword" + + FilterPubRequestKeyword: + type: object + properties: + keyword: + type: string + maxLength: 1000 + whole_word: + type: boolean + regex: + type: boolean + id: + type: string + _destroy: + type: boolean + default: false + required: + - id + + FilterStatusRequest: + type: object + properties: + status_id: + type: string + + Instance: + type: object + properties: + domain: + type: string + title: + type: string + version: + type: string + source_url: + type: string + description: + type: string + usage: + $ref: "#/components/schemas/InstanceUsage" + thumbnail: + $ref: "#/components/schemas/InstanceThumbnail" + languages: + type: array + items: + type: string + configuration: + $ref: "#/components/schemas/InstanceConfiguration" + + + InstanceUsage: + type: object + properties: + users: + $ref: "#/components/schemas/InstanceUsageUsers" + + + InstanceUsageUsers: + type: object + properties: + active_month: + type: integer + + InstanceThumbnail: + type: object + properties: + blurhash: + type: string + versions: + $ref: "#/components/schemas/InstanceThumbnailVersions" + + InstanceThumbnailVersions: + type: object + properties: + "@1x": + type: string + "@2x": + type: string + + InstanceConfiguration: + type: object + properties: + urls: + $ref: "#/components/schemas/InstanceConfigurationUrls" + accounts: + $ref: "#/components/schemas/InstanceConfigurationAccounts" + statuses: + $ref: "#/components/schemas/InstanceConfigurationStatuses" + media_attachments: + $ref: "#/components/schemas/InstanceConfigurationMediaAttachments" + polls: + $ref: "#/components/schemas/InstanceConfigurationPolls" + translation: + $ref: "#/components/schemas/InstanceConfigurationTranslation" + registrations: + $ref: "#/components/schemas/InstanceConfigurationRegistrations" + contact: + $ref: "#/components/schemas/InstanceConfigurationContact" + rules: + type: array + items: + $ref: "#/components/schemas/Rule" + + InstanceConfigurationUrls: + type: object + properties: + streaming_api: + type: string + + InstanceConfigurationAccounts: + type: object + properties: + max_featured_tags: + type: integer + + InstanceConfigurationStatuses: + type: object + properties: + max_characters: + type: integer + max_media_attachments: + type: integer + characters_reserved_per_url: + type: integer + + InstanceConfigurationMediaAttachments: + type: object + properties: + supported_mime_types: + type: array + items: + type: string + image_size_limit: + type: integer + image_matrix_limit: + type: integer + video_size_limit: + type: integer + video_frame_rate_limit: + type: integer + video_matrix_limit: + type: integer + + InstanceConfigurationPolls: + type: object + properties: + max_options: + type: integer + max_characters_per_option: + type: integer + min_expiration: + type: integer + max_expiration: + type: integer + + InstanceConfigurationTranslation: + type: object + properties: + enabled: + type: boolean + + InstanceConfigurationRegistrations: + type: object + properties: + enabled: + type: boolean + approval_required: + type: boolean + message: + type: string + nullable: true + + InstanceConfigurationContact: + type: object + properties: + email: + type: string + account: + $ref: "#/components/schemas/Account" + + Rule: + type: object + properties: + id: + type: string + text: + type: string + + V1Instance: + type: object + properties: + uri: + type: string + title: + type: string + short_description: + type: string + description: + type: string + email: + type: string + version: + type: string + urls: + $ref: "#/components/schemas/V1InstanceUrls" + stats: + $ref: "#/components/schemas/V1InstanceStats" + thumbnail: + type: string + nullable: true + languages: + type: array + items: + type: string + registrations: + type: boolean + approval_required: + type: boolean + invites_enabled: + type: boolean + configuration: + $ref: "#/components/schemas/V1InstanceConfiguration" + contact_account: + $ref: "#/components/schemas/Account" + rules: + type: array + items: + $ref: "#/components/schemas/Rule" + required: + - uri + - title + - short_description + - description + - email + - version + - urls + - stats + - thumbnail + - languages + - registrations + - approval_required + - invites_enabled + - configuration + - contact_account + - rules + + V1InstanceUrls: + type: object + properties: + streaming_api: + type: string + required: + - streaming_api + + V1InstanceStats: + type: object + properties: + user_count: + type: integer + status_count: + type: integer + domain_count: + type: integer + required: + - user_count + - status_count + - domain_count + + V1InstanceConfiguration: + type: object + properties: + accounts: + $ref: "#/components/schemas/V1InstanceConfigurationAccounts" + statuses: + $ref: "#/components/schemas/V1InstanceConfigurationStatuses" + media_attachments: + $ref: "#/components/schemas/V1InstanceConfigurationMediaAttachments" + polls: + $ref: "#/components/schemas/V1InstanceConfigurationPolls" + required: + - accounts + - statuses + - media_attachments + - polls + + + V1InstanceConfigurationAccounts: + type: object + properties: + max_featured_tags: + type: integer + + V1InstanceConfigurationStatuses: + type: object + properties: + max_characters: + type: integer + max_media_attachments: + type: integer + characters_reserved_per_url: + type: integer + + V1InstanceConfigurationMediaAttachments: + type: object + properties: + supported_mime_types: + type: array + items: + type: string + image_size_limit: + type: integer + image_matrix_limit: + type: integer + video_size_limit: + type: integer + video_frame_rate_limit: + type: integer + video_matrix_limit: + type: integer + + V1InstanceConfigurationPolls: + type: object + properties: + max_options: + type: integer + max_characters_per_option: + type: integer + min_expiration: + type: integer + max_expiration: + type: integer + + V1InstanceActivity: + type: object + properties: + week: + type: integer + statuses: + type: integer + logins: + type: integer + registrations: + type: integer + + DomainBlock: + type: object + properties: + domain: + type: string + digest: + type: string + severity: + type: string + enum: + - silence + - suspend + comment: + type: string + + ExtendedDescription: + type: object + properties: + updated_at: + type: string + content: + type: string + + StatusesRequest: + type: object + properties: + status: + type: string + nullable: true + maxLength: 3000 + media_ids: + type: array + items: + type: string + maxItems: 4 + poll: + $ref: "#/components/schemas/StatusesRequestPoll" + in_reply_to_id: + type: string + sensitive: + type: boolean + default: false + spoiler_text: + type: string + maxLength: 100 + visibility: + type: string + enum: + - public + - unlisted + - private + - direct + language: + type: string + maxLength: 100 + scheduled_at: + type: string + format: date-time + example: "2019-12-05T12:33:01.000Z" + + StatusesRequestPoll: + type: object + properties: + options: + type: array + maxItems: 10 + items: + type: string + maxLength: 100 + expires_in: + type: integer + multiple: + type: boolean + default: false + hide_totals: + type: boolean + default: false + + Application: + type: object + properties: + name: + type: string + website: + type: string + nullable: true + vapid_key: + type: string + client_id: + type: string + client_secret: + type: string + redirect_uri: + type: string + required: + - name + - vapid_key + + AppsRequest: + type: object + properties: + client_name: + type: string + maxLength: 200 + redirect_uris: + type: string + maxLength: 1000 + scopes: + type: string + maxLength: 1000 + website: + type: string + maxLength: 1000 + required: + - client_name + - redirect_uris + + Relationship: + type: object + properties: + id: + type: string + following: + type: boolean + showing_reblogs: + type: boolean + notifying: + type: boolean + followed_by: + type: boolean + blocking: + type: boolean + blocked_by: + type: boolean + muting: + type: boolean + muting_notifications: + type: boolean + requested: + type: boolean + domain_blocking: + type: boolean + endorsed: + type: boolean + note: + type: string + required: + - id + - following + - showing_reblogs + - notifying + - followed_by + - blocking + - blocked_by + - muting + - muting_notifications + - requested + - domain_blocking + - endorsed + - note + + + FollowRequestBody: + type: object + properties: + reblogs: + type: boolean + default: true + notify: + type: boolean + default: false + languages: + type: array + maxItems: 10 + items: + type: string + maxLength: 10 + + UpdateCredentials: + type: object + properties: + display_name: + type: string + maxLength: 300 + note: + type: string + maxLength: 2000 + avatar: + type: string + format: binary + header: + type: string + format: binary + locked: + type: boolean + bot: + type: boolean + discoverable: + type: boolean + hide_collections: + type: boolean + indexable: + type: boolean + fields_attributes: + type: object + additionalProperties: + $ref: "#/components/schemas/UpdateCredentialsFieldsAttributes" + source: + $ref: "#/components/schemas/UpdateCredentialsSource" + + UpdateCredentialsSource: + type: object + properties: + privacy: + type: string + enum: + - public + - unlisted + - private + sensitive: + type: boolean + language: + type: string + + UpdateCredentialsFieldsAttributes: + type: object + properties: + name: + type: string + value: + type: string + + Report: + type: object + + RelationshipServeranceEvent: + type: object + + Notification: + type: object + properties: + id: + type: string + type: + type: string + enum: + - mention + - status + - reblog + - follow + - follow_request + - favourite + - poll + - update + - admin.sign_up + - admin.report + - severed_relationships + created_at: + type: string + account: + $ref: "#/components/schemas/Account" + status: + $ref: "#/components/schemas/Status" + report: + $ref: "#/components/schemas/Report" + relationship_severance_event: + $ref: "#/components/schemas/RelationshipServeranceEvent" + required: + - id + - type + - created_at + - account + + UnprocessableEntityResponse: + type: object + properties: + error: + type: string + details: + type: array + additionalProperties: + type: array + items: + $ref: "#/components/schemas/UnprocessableEntityResponseDetails" + + UnprocessableEntityResponseDetails: + type: object + properties: + error: + type: string + description: + type: string + nullable: false + required: + - error + - description + + UnauthorizedResponse: + type: object + properties: + error: + type: string + default: "The access token is invalid" + required: + - error + + RateLimitedResponse: + type: object + properties: + error: + type: string + default: "Too many requests" + required: + - error + + ForbiddenResponse: + type: object + properties: + error: + type: string + required: + - error + + NotFoundResponse: + type: object + properties: + error: + type: string + required: + - error + + PartialContentResponse: + type: object + + + responses: + forbidden: + description: forbidden + content: + application/json: + schema: + $ref: "#/components/schemas/ForbiddenResponse" + unprocessableEntity: + description: Unprocessable entity + content: + application/json: + schema: + $ref: "#/components/schemas/UnprocessableEntityResponse" + + unauthorized: + description: Unauthorized + content: + application/json: + schema: + $ref: "#/components/schemas/UnauthorizedResponse" + + rateLimited: + description: Too many requests + content: + application/json: + schema: + $ref: "#/components/schemas/RateLimitedResponse" + + notfound: + description: Not found + content: + application/json: + schema: + $ref: "#/components/schemas/NotFoundResponse" + + partialContent: + description: Partial content + content: + application/json: + schema: + $ref: "#/components/schemas/PartialContentResponse" + + securitySchemes: + OAuth2: + type: oauth2 + description: Mastodon Oauth + flows: + authorizationCode: + authorizationUrl: /oauth/authorize + tokenUrl: /oauth/token + scopes: + read:accounts: "" + read:blocks: "" + read:bookmarks: "" + read:favourites: "" + read:filters: "" + read:follows: "" + read:lists: "" + read:mutes: "" + read:notifications: "" + read:search: "" + read:statuses: "" + write:accounts: "" + write:blocks: "" + write:bookmarks: "" + write:conversations: "" + write:favourites: "" + write:filters: "" + write:follows: "" + write:lists: "" + write:media: "" + write:mutes: "" + write:notifications: "" + write:reports: "" + write:statuses: "" + admin:read:accounts: "" + admin:read:reports: "" + admin:read:domain_allows: "" + admin:read:domain_blocks: "" + admin:read:ip_blocks: "" + admin:read:email_domain_blocks: "" + admin:read:canonical_email_blocks: "" + admin:write:accounts: "" + admin:write:reports: "" + admin:write:domain_allows: "" + admin:write:domain_blocks: "" + admin:write:ip_blocks: "" + admin:write:email_domain_blocks: "" + admin:write:canonical_email_blocks: "" diff --git a/hideout-mastodon/templates/api.mustache b/hideout-mastodon/templates/api.mustache new file mode 100644 index 00000000..46845778 --- /dev/null +++ b/hideout-mastodon/templates/api.mustache @@ -0,0 +1,96 @@ +package {{package}} + +{{#imports}}import {{import}} +{{/imports}} +{{#swagger2AnnotationLibrary}} + import io.swagger.v3.oas.annotations.* + import io.swagger.v3.oas.annotations.enums.* + import io.swagger.v3.oas.annotations.media.* + import io.swagger.v3.oas.annotations.responses.* + import io.swagger.v3.oas.annotations.security.* +{{/swagger2AnnotationLibrary}} +{{#swagger1AnnotationLibrary}} + import io.swagger.annotations.Api + import io.swagger.annotations.ApiOperation + import io.swagger.annotations.ApiParam + import io.swagger.annotations.ApiResponse + import io.swagger.annotations.ApiResponses + import io.swagger.annotations.Authorization + import io.swagger.annotations.AuthorizationScope +{{/swagger1AnnotationLibrary}} +import org.springframework.http.HttpStatus +import org.springframework.http.MediaType +import org.springframework.http.ResponseEntity + +import org.springframework.web.bind.annotation.* +{{#useBeanValidation}} + import org.springframework.validation.annotation.Validated +{{/useBeanValidation}} +import org.springframework.web.context.request.NativeWebRequest +import org.springframework.beans.factory.annotation.Autowired +import dev.usbharu.hideout.generate.JsonOrFormBind + +{{#useBeanValidation}} + import {{javaxPackage}}.validation.Valid + import {{javaxPackage}}.validation.constraints.DecimalMax + import {{javaxPackage}}.validation.constraints.DecimalMin + import {{javaxPackage}}.validation.constraints.Email + import {{javaxPackage}}.validation.constraints.Max + import {{javaxPackage}}.validation.constraints.Min + import {{javaxPackage}}.validation.constraints.NotNull + import {{javaxPackage}}.validation.constraints.Pattern + import {{javaxPackage}}.validation.constraints.Size +{{/useBeanValidation}} + +{{#reactive}} + import kotlinx.coroutines.flow.Flow +{{/reactive}} +import kotlin.collections.List +import kotlin.collections.Map + +@RestController{{#beanQualifiers}}("{{package}}.{{classname}}Controller"){{/beanQualifiers}} +{{#useBeanValidation}} + @Validated +{{/useBeanValidation}} +{{#swagger1AnnotationLibrary}} + @Api(value = "{{{baseName}}}", description = "The {{{baseName}}} API") +{{/swagger1AnnotationLibrary}} +{{=<% %>=}} +@RequestMapping("\${api.base-path:<%contextPath%>}") +<%={{ }}=%> +{{#operations}} + class {{classname}}Controller({{#serviceInterface}}@Autowired(required = true) val service: {{classname}}Service{{/serviceInterface}}) { + {{#operation}} + + {{#swagger2AnnotationLibrary}} + @Operation( + summary = "{{{summary}}}", + operationId = "{{{operationId}}}", + description = """{{{unescapedNotes}}}""", + responses = [{{#responses}} + ApiResponse(responseCode = "{{{code}}}", description = "{{{message}}}"{{#baseType}}, content = [Content({{#isArray}}array = ArraySchema({{/isArray}}schema = Schema(implementation = {{{baseType}}}::class)){{#isArray}}){{/isArray}}]{{/baseType}}){{^-last}},{{/-last}}{{/responses}} ]{{#hasAuthMethods}}, + security = [ {{#authMethods}}SecurityRequirement(name = "{{name}}"{{#isOAuth}}, scopes = [ {{#scopes}}"{{scope}}"{{^-last}}, {{/-last}}{{/scopes}} ]{{/isOAuth}}){{^-last}},{{/-last}}{{/authMethods}} ]{{/hasAuthMethods}} + ){{/swagger2AnnotationLibrary}}{{#swagger1AnnotationLibrary}} + @ApiOperation( + value = "{{{summary}}}", + nickname = "{{{operationId}}}", + notes = "{{{notes}}}"{{#returnBaseType}}, + response = {{{.}}}::class{{/returnBaseType}}{{#returnContainer}}, + responseContainer = "{{{.}}}"{{/returnContainer}}{{#hasAuthMethods}}, + authorizations = [{{#authMethods}}Authorization(value = "{{name}}"{{#isOAuth}}, scopes = [{{#scopes}}AuthorizationScope(scope = "{{scope}}", description = "{{description}}"){{^-last}}, {{/-last}}{{/scopes}}]{{/isOAuth}}){{^-last}}, {{/-last}}{{/authMethods}}]{{/hasAuthMethods}}) + @ApiResponses( + value = [{{#responses}}ApiResponse(code = {{{code}}}, message = "{{{message}}}"{{#baseType}}, response = {{{.}}}::class{{/baseType}}{{#containerType}}, responseContainer = "{{{.}}}"{{/containerType}}){{^-last}},{{/-last}}{{/responses}}]){{/swagger1AnnotationLibrary}} + @RequestMapping( + method = [RequestMethod.{{httpMethod}}], + value = ["{{#lambda.escapeDoubleQuote}}{{path}}{{/lambda.escapeDoubleQuote}}"]{{#singleContentTypes}}{{#hasProduces}}, + produces = "{{{vendorExtensions.x-accepts}}}"{{/hasProduces}}{{#hasConsumes}}, + consumes = "{{{vendorExtensions.x-content-type}}}"{{/hasConsumes}}{{/singleContentTypes}}{{^singleContentTypes}}{{#hasProduces}}, + produces = [{{#produces}}"{{{mediaType}}}"{{^-last}}, {{/-last}}{{/produces}}]{{/hasProduces}}{{#hasConsumes}}, + consumes = [{{#consumes}}"{{{mediaType}}}"{{^-last}}, {{/-last}}{{/consumes}}]{{/hasConsumes}}{{/singleContentTypes}} + ) + {{#reactive}}{{^isArray}}suspend {{/isArray}}{{/reactive}}fun {{operationId}}({{#allParams}}{{>queryParams}}{{>pathParams}}{{>headerParams}}{{>bodyParams}}{{>formParams}}{{^-last}},{{/-last}}{{/allParams}}): ResponseEntity<{{>returnTypes}}> { + return {{>returnValue}} + } + {{/operation}} + } +{{/operations}} diff --git a/hideout-mastodon/templates/apiController.mustache b/hideout-mastodon/templates/apiController.mustache new file mode 100644 index 00000000..8d65aee1 --- /dev/null +++ b/hideout-mastodon/templates/apiController.mustache @@ -0,0 +1,25 @@ +package {{package}} + +import org.springframework.stereotype.Controller +import org.springframework.web.bind.annotation.RequestMapping +import java.util.Optional +import dev.usbharu.hideout.generate.JsonOrFormBind + +{{>generatedAnnotation}} +@Controller{{#beanQualifiers}}("{{package}}.{{classname}}Controller"){{/beanQualifiers}} +{{=<% %>=}} +@RequestMapping("\${openapi.<%title%>.base-path:<%>defaultBasePath%>}") +<%={{ }}=%> +{{#operations}} + class {{classname}}Controller( + @org.springframework.beans.factory.annotation.Autowired(required = false) delegate: {{classname}}Delegate? + ) : {{classname}} { + private val delegate: {{classname}}Delegate + + init { + this.delegate = Optional.ofNullable(delegate).orElse(object : {{classname}}Delegate {}) + } + + override fun getDelegate(): {{classname}}Delegate = delegate + } +{{/operations}} diff --git a/hideout-mastodon/templates/apiDelegate.mustache b/hideout-mastodon/templates/apiDelegate.mustache new file mode 100644 index 00000000..e7d62ddb --- /dev/null +++ b/hideout-mastodon/templates/apiDelegate.mustache @@ -0,0 +1,46 @@ +package {{package}} + +{{#imports}}import {{import}} +{{/imports}} +import org.springframework.http.HttpStatus +import org.springframework.http.MediaType +import org.springframework.http.ResponseEntity +import org.springframework.web.context.request.NativeWebRequest +import org.springframework.core.io.Resource +{{#reactive}} + import kotlinx.coroutines.flow.Flow +{{/reactive}} + +import java.util.Optional +{{#async}} + import java.util.concurrent.CompletableFuture +{{/async}} + +{{#operations}} + /** + * A delegate to be called by the {@link {{classname}}Controller}}. + * Implement this interface with a {@link org.springframework.stereotype.Service} annotated class. + */ + {{>generatedAnnotation}} + interface {{classname}}Delegate { + + fun getRequest(): Optional + = Optional.empty() + {{#operation}} + + /** + * @see {{classname}}#{{operationId}} + */ + {{#reactive}}{{^isArray}}suspend {{/isArray}}{{/reactive}}fun {{operationId}}({{#allParams}}{{paramName}} + : {{^isFile}}{{^reactive}}{{>optionalDataType}}{{/reactive}}{{#reactive}}{{^isArray}}{{>optionalDataType}}{{/isArray}}{{#isArray}}{{#isBodyParam}} + Flow<{{{baseType}}} + >{{/isBodyParam}}{{^isBodyParam}}{{>optionalDataType}}{{/isBodyParam}}{{/isArray}}{{/reactive}}{{/isFile}}{{#isFile}} + Resource?{{/isFile}}{{^-last}}, + {{/-last}}{{/allParams}}): {{#responseWrapper}}{{.}} + <{{/responseWrapper}}ResponseEntity<{{>returnTypes}}>{{#responseWrapper}}>{{/responseWrapper}} { + {{>methodBody}} + } + + {{/operation}} + } +{{/operations}} diff --git a/hideout-mastodon/templates/apiInterface.mustache b/hideout-mastodon/templates/apiInterface.mustache new file mode 100644 index 00000000..99d240c5 --- /dev/null +++ b/hideout-mastodon/templates/apiInterface.mustache @@ -0,0 +1,112 @@ +/** +* NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech) ({{{generatorVersion}}}). +* https://openapi-generator.tech +* Do not edit the class manually. +*/ +package {{package}} + +{{#imports}}import {{import}} +{{/imports}} +{{#swagger2AnnotationLibrary}} + import io.swagger.v3.oas.annotations.* + import io.swagger.v3.oas.annotations.enums.* + import io.swagger.v3.oas.annotations.media.* + import io.swagger.v3.oas.annotations.responses.* + import io.swagger.v3.oas.annotations.security.* +{{/swagger2AnnotationLibrary}} +{{#swagger1AnnotationLibrary}} + import io.swagger.annotations.Api + import io.swagger.annotations.ApiOperation + import io.swagger.annotations.ApiParam + import io.swagger.annotations.ApiResponse + import io.swagger.annotations.ApiResponses + import io.swagger.annotations.Authorization + import io.swagger.annotations.AuthorizationScope +{{/swagger1AnnotationLibrary}} +import org.springframework.http.HttpStatus +import org.springframework.http.MediaType +import org.springframework.http.ResponseEntity + +import org.springframework.web.bind.annotation.* +{{#useBeanValidation}} + import org.springframework.validation.annotation.Validated +{{/useBeanValidation}} +import org.springframework.web.context.request.NativeWebRequest +import org.springframework.beans.factory.annotation.Autowired + +{{#useBeanValidation}} + import {{javaxPackage}}.validation.constraints.DecimalMax + import {{javaxPackage}}.validation.constraints.DecimalMin + import {{javaxPackage}}.validation.constraints.Email + import {{javaxPackage}}.validation.constraints.Max + import {{javaxPackage}}.validation.constraints.Min + import {{javaxPackage}}.validation.constraints.NotNull + import {{javaxPackage}}.validation.constraints.Pattern + import {{javaxPackage}}.validation.constraints.Size + import {{javaxPackage}}.validation.Valid +{{/useBeanValidation}} + +{{#reactive}} + import kotlinx.coroutines.flow.Flow +{{/reactive}} +import kotlin.collections.List +import kotlin.collections.Map +import dev.usbharu.hideout.generate.JsonOrFormBind + +{{#useBeanValidation}} + @Validated +{{/useBeanValidation}} +{{#swagger1AnnotationLibrary}} + @Api(value = "{{{baseName}}}", description = "The {{{baseName}}} API") +{{/swagger1AnnotationLibrary}} +{{^useFeignClient}} + {{=<% %>=}} + @RequestMapping("\${api.base-path:<%contextPath%>}") + <%={{ }}=%> +{{/useFeignClient}} +{{#operations}} + interface {{classname}} { + {{#isDelegate}} + + fun getDelegate(): {{classname}}Delegate = object: {{classname}}Delegate {} + {{/isDelegate}} + {{#operation}} + + {{#swagger2AnnotationLibrary}} + @Operation( + summary = "{{{summary}}}", + operationId = "{{{operationId}}}", + description = """{{{unescapedNotes}}}""", + responses = [{{#responses}} + ApiResponse(responseCode = "{{{code}}}", description = "{{{message}}}"{{#baseType}}, content = [Content({{#isArray}}array = ArraySchema({{/isArray}}schema = Schema(implementation = {{{baseType}}}::class)){{#isArray}}){{/isArray}}]{{/baseType}}){{^-last}},{{/-last}}{{/responses}} + ]{{#hasAuthMethods}}, + security = [ {{#authMethods}}SecurityRequirement(name = "{{name}}"{{#isOAuth}}, scopes = [ {{#scopes}}"{{scope}}"{{^-last}}, {{/-last}}{{/scopes}} ]{{/isOAuth}}){{^-last}},{{/-last}}{{/authMethods}} ]{{/hasAuthMethods}} + ){{/swagger2AnnotationLibrary}}{{#swagger1AnnotationLibrary}} + @ApiOperation( + value = "{{{summary}}}", + nickname = "{{{operationId}}}", + notes = "{{{notes}}}"{{#returnBaseType}}, + response = {{{.}}}::class{{/returnBaseType}}{{#returnContainer}}, + responseContainer = "{{{.}}}"{{/returnContainer}}{{#hasAuthMethods}}, + authorizations = [{{#authMethods}}Authorization(value = "{{name}}"{{#isOAuth}}, scopes = [{{#scopes}}AuthorizationScope(scope = "{{scope}}", description = "{{description}}"){{^-last}}, {{/-last}}{{/scopes}}]{{/isOAuth}}){{^-last}}, {{/-last}}{{/authMethods}}]{{/hasAuthMethods}}) + @ApiResponses( + value = [{{#responses}}ApiResponse(code = {{{code}}}, message = "{{{message}}}"{{#baseType}}, response = {{{.}}}::class{{/baseType}}{{#containerType}}, responseContainer = "{{{.}}}"{{/containerType}}){{^-last}}, {{/-last}}{{/responses}}]){{/swagger1AnnotationLibrary}} + @RequestMapping( + method = [RequestMethod.{{httpMethod}}], + value = ["{{#lambda.escapeDoubleQuote}}{{path}}{{/lambda.escapeDoubleQuote}}"]{{#singleContentTypes}}{{#hasProduces}}, + produces = "{{{vendorExtensions.x-accepts}}}"{{/hasProduces}}{{#hasConsumes}}, + consumes = "{{{vendorExtensions.x-content-type}}}"{{/hasConsumes}}{{/singleContentTypes}}{{^singleContentTypes}}{{#hasProduces}}, + produces = [{{#produces}}"{{{mediaType}}}"{{^-last}}, {{/-last}}{{/produces}}]{{/hasProduces}}{{#hasConsumes}}, + consumes = [{{#consumes}}"{{{mediaType}}}"{{^-last}}, {{/-last}}{{/consumes}}]{{/hasConsumes}}{{/singleContentTypes}} + ) + {{#reactive}}{{^isArray}}suspend {{/isArray}}{{/reactive}}fun {{operationId}}({{#allParams}}{{>queryParams}}{{>pathParams}}{{>headerParams}}{{>bodyParams}}{{>formParams}}{{^-last}},{{/-last}}{{/allParams}}): ResponseEntity<{{>returnTypes}}>{{^skipDefaultInterface}} { + {{^isDelegate}} + return {{>returnValue}} + {{/isDelegate}} + {{#isDelegate}} + return getDelegate().{{operationId}}({{#allParams}}{{paramName}}{{^-last}}, {{/-last}}{{/allParams}}) + {{/isDelegate}} + }{{/skipDefaultInterface}} + {{/operation}} + } +{{/operations}} diff --git a/hideout-mastodon/templates/apiUtil.mustache b/hideout-mastodon/templates/apiUtil.mustache new file mode 100644 index 00000000..101ed40e --- /dev/null +++ b/hideout-mastodon/templates/apiUtil.mustache @@ -0,0 +1,23 @@ +package {{apiPackage}} + +{{^reactive}} + import org.springframework.web.context.request.NativeWebRequest + + import {{javaxPackage}}.servlet.http.HttpServletResponse + import java.io.IOException +{{/reactive}} + +object ApiUtil { +{{^reactive}} + fun setExampleResponse(req: NativeWebRequest, contentType: String, example: String) { + try { + val res = req.getNativeResponse(HttpServletResponse::class.java) + res?.characterEncoding = "UTF-8" + res?.addHeader("Content-Type", contentType) + res?.writer?.print(example) + } catch (e: IOException) { + throw RuntimeException(e) + } + } +{{/reactive}} +} diff --git a/hideout-mastodon/templates/api_test.mustache b/hideout-mastodon/templates/api_test.mustache new file mode 100644 index 00000000..d84af783 --- /dev/null +++ b/hideout-mastodon/templates/api_test.mustache @@ -0,0 +1,38 @@ +package {{package}} + +{{#imports}}import {{import}} +{{/imports}} +import org.junit.jupiter.api.Test +{{#reactive}} + import kotlinx.coroutines.flow.Flow + import kotlinx.coroutines.test.runBlockingTest +{{/reactive}} +import org.springframework.http.ResponseEntity + +class {{classname}}Test { + +{{#serviceInterface}} + private val service: {{classname}}Service = {{classname}}ServiceImpl() +{{/serviceInterface}} +private val api: {{classname}}Controller = {{classname}}Controller({{#serviceInterface}}service{{/serviceInterface}}) +{{#operations}} + {{#operation}} + + /** + * To test {{classname}}Controller.{{operationId}} + * + * @throws ApiException + * if the Api call fails + */ + @Test + fun {{operationId}}Test() {{#reactive}}= runBlockingTest {{/reactive}}{ + {{#allParams}} + val {{{paramName}}}: {{>optionalDataType}} = TODO() + {{/allParams}} + val response: ResponseEntity<{{>returnTypes}}> = api.{{operationId}}({{#allParams}}{{{paramName}}}{{^-last}}, {{/-last}}{{/allParams}}) + + // TODO: test validations + } + {{/operation}} +{{/operations}} +} diff --git a/hideout-mastodon/templates/beanValidation.mustache b/hideout-mastodon/templates/beanValidation.mustache new file mode 100644 index 00000000..ee25df4a --- /dev/null +++ b/hideout-mastodon/templates/beanValidation.mustache @@ -0,0 +1,4 @@ +{{#isContainer}}{{^isPrimitiveType}}{{^isEnum}} + @field:Valid{{/isEnum}}{{/isPrimitiveType}}{{/isContainer}}{{! +}}{{^isContainer}}{{^isPrimitiveType}}{{^isNumber}}{{^isUuid}}{{^isDateTime}} + @field:Valid{{/isDateTime}}{{/isUuid}}{{/isNumber}}{{/isPrimitiveType}}{{/isContainer}} \ No newline at end of file diff --git a/hideout-mastodon/templates/beanValidationModel.mustache b/hideout-mastodon/templates/beanValidationModel.mustache new file mode 100644 index 00000000..0d4b99fa --- /dev/null +++ b/hideout-mastodon/templates/beanValidationModel.mustache @@ -0,0 +1,39 @@ +{{! +format: email +}}{{#isEmail}} + @get:Email{{/isEmail}}{{! +pattern set +}}{{#pattern}} + @get:Pattern(regexp="{{{.}}}"){{/pattern}}{{! +minLength && maxLength set +}}{{#minLength}}{{#maxLength}} + @get:Size(min={{minLength}},max={{maxLength}}){{/maxLength}}{{/minLength}}{{! +minLength set, maxLength not +}}{{#minLength}}{{^maxLength}} + @get:Size(min={{minLength}}){{/maxLength}}{{/minLength}}{{! +minLength not set, maxLength set +}}{{^minLength}}{{#maxLength}} + @get:Size(max={{.}}){{/maxLength}}{{/minLength}}{{! +@Size: minItems && maxItems set +}}{{#minItems}}{{#maxItems}} + @get:Size(min={{minItems}},max={{maxItems}}) {{/maxItems}}{{/minItems}}{{! +@Size: minItems set, maxItems not +}}{{#minItems}}{{^maxItems}} + @get:Size(min={{minItems}}){{/maxItems}}{{/minItems}}{{! +@Size: minItems not set && maxItems set +}}{{^minItems}}{{#maxItems}} + @get:Size(max={{.}}){{/maxItems}}{{/minItems}}{{! +check for integer or long / all others=decimal type with @Decimal* +isInteger set +}}{{#isInteger}}{{#minimum}} + @get:Min({{.}}){{/minimum}}{{#maximum}} + @get:Max({{.}}){{/maximum}}{{/isInteger}}{{! +isLong set +}}{{#isLong}}{{#minimum}} + @get:Min({{.}}L){{/minimum}}{{#maximum}} + @get:Max({{.}}L){{/maximum}}{{/isLong}}{{! +Not Integer, not Long => we have a decimal value! +}}{{^isInteger}}{{^isLong}}{{#minimum}} + @get:DecimalMin("{{.}}"){{/minimum}}{{#maximum}} + @get:DecimalMax("{{.}}"){{/maximum}}{{/isLong}}{{/isInteger}} + diff --git a/hideout-mastodon/templates/beanValidationPath.mustache b/hideout-mastodon/templates/beanValidationPath.mustache new file mode 100644 index 00000000..8eb9029b --- /dev/null +++ b/hideout-mastodon/templates/beanValidationPath.mustache @@ -0,0 +1,22 @@ +{{#isEmail}}@Email {{/isEmail}}{{! +pattern set +}}{{#pattern}}@Pattern(regexp="{{{.}}}") {{/pattern}}{{! +minLength && maxLength set +}}{{#minLength}}{{#maxLength}}@Size(min={{minLength}},max={{maxLength}}) {{/maxLength}}{{/minLength}}{{! +minLength set, maxLength not +}}{{#minLength}}{{^maxLength}}@Size(min={{minLength}}) {{/maxLength}}{{/minLength}}{{! +minLength not set, maxLength set +}}{{^minLength}}{{#maxLength}}@Size(max={{.}}) {{/maxLength}}{{/minLength}}{{! +@Size: minItems && maxItems set +}}{{#minItems}}{{#maxItems}}@Size(min={{minItems}},max={{maxItems}}) {{/maxItems}}{{/minItems}}{{! +@Size: minItems set, maxItems not +}}{{#minItems}}{{^maxItems}}@Size(min={{minItems}}) {{/maxItems}}{{/minItems}}{{! +@Size: minItems not set && maxItems set +}}{{^minItems}}{{#maxItems}}@Size(max={{.}}) {{/maxItems}}{{/minItems}}{{! +check for integer or long / all others=decimal type with @Decimal* +isInteger set +}}{{#isInteger}}{{#minimum}}@Min({{.}}){{/minimum}}{{#maximum}} @Max({{.}}) {{/maximum}}{{/isInteger}}{{! +isLong set +}}{{#isLong}}{{#minimum}}@Min({{.}}L){{/minimum}}{{#maximum}} @Max({{.}}L) {{/maximum}}{{/isLong}}{{! +Not Integer, not Long => we have a decimal value! +}}{{^isInteger}}{{^isLong}}{{#minimum}}@DecimalMin("{{.}}"){{/minimum}}{{#maximum}} @DecimalMax("{{.}}") {{/maximum}}{{/isLong}}{{/isInteger}} \ No newline at end of file diff --git a/hideout-mastodon/templates/beanValidationPathParams.mustache b/hideout-mastodon/templates/beanValidationPathParams.mustache new file mode 100644 index 00000000..3c57e76b --- /dev/null +++ b/hideout-mastodon/templates/beanValidationPathParams.mustache @@ -0,0 +1 @@ +{{! PathParam is always required, no @NotNull necessary }}{{>beanValidationPath}} \ No newline at end of file diff --git a/hideout-mastodon/templates/beanValidationQueryParams.mustache b/hideout-mastodon/templates/beanValidationQueryParams.mustache new file mode 100644 index 00000000..cc53bc96 --- /dev/null +++ b/hideout-mastodon/templates/beanValidationQueryParams.mustache @@ -0,0 +1 @@ +{{#required}}@NotNull {{/required}}{{>beanValidationPath}} \ No newline at end of file diff --git a/hideout-mastodon/templates/bodyParams.mustache b/hideout-mastodon/templates/bodyParams.mustache new file mode 100644 index 00000000..483d28d5 --- /dev/null +++ b/hideout-mastodon/templates/bodyParams.mustache @@ -0,0 +1 @@ +{{#isBodyParam}}{{#swagger2AnnotationLibrary}}@Parameter(description = "{{{description}}}"{{#required}}, required = true{{/required}}{{^isContainer}}{{#allowableValues}}{{#defaultValue}}, schema = Schema(allowableValues = ["{{{allowableValues}}}"], defaultValue = {{^isString}}"{{/isString}}{{#isString}}{{#isEnum}}"{{/isEnum}}{{/isString}}{{{defaultValue}}}{{^isString}}"{{/isString}}{{#isString}}{{#isEnum}}"{{/isEnum}}{{/isString}}){{/defaultValue}}{{/allowableValues}}{{^allowableValues}}{{#defaultValue}}, schema = Schema(defaultValue = {{^isString}}"{{/isString}}{{#isString}}{{#isEnum}}"{{/isEnum}}{{/isString}}{{{defaultValue}}}{{^isString}}"{{/isString}}{{#isString}}{{#isEnum}}"{{/isEnum}}{{/isString}}){{/defaultValue}}{{/allowableValues}}{{#allowableValues}}{{^defaultValue}}, schema = Schema(allowableValues = ["{{{allowableValues}}}"]){{/defaultValue}}{{/allowableValues}}{{/isContainer}}){{/swagger2AnnotationLibrary}}{{#swagger1AnnotationLibrary}}@ApiParam(value = "{{{description}}}"{{#required}}, required = true{{/required}}{{^isContainer}}{{#allowableValues}}, allowableValues = "{{{.}}}"{{/allowableValues}}{{/isContainer}}{{#defaultValue}}, defaultValue = "{{{.}}}"{{/defaultValue}}){{/swagger1AnnotationLibrary}} {{#useBeanValidation}}@Valid{{/useBeanValidation}} @JsonOrFormBind {{{paramName}}}: {{^reactive}}{{>optionalDataType}}{{/reactive}}{{#reactive}}{{^isArray}}{{>optionalDataType}}{{/isArray}}{{#isArray}}Flow<{{{baseType}}}>{{/isArray}}{{/reactive}}{{/isBodyParam}} diff --git a/hideout-mastodon/templates/dataClass.mustache b/hideout-mastodon/templates/dataClass.mustache new file mode 100644 index 00000000..4695e2a9 --- /dev/null +++ b/hideout-mastodon/templates/dataClass.mustache @@ -0,0 +1,34 @@ +/** +* {{{description}}} +{{#vars}} + * @param {{name}} {{{description}}} +{{/vars}} +*/{{#discriminator}} + {{>typeInfoAnnotation}}{{/discriminator}} + +{{#discriminator}}interface {{classname}}{{/discriminator}}{{^discriminator}}{{#hasVars}}data {{/hasVars}}class {{classname}} @ConstructorProperties( {{#vars}}"{{baseName}}",{{/vars}} ) constructor( +{{#requiredVars}} + {{>dataClassReqVar}}{{^-last}}, + {{/-last}}{{/requiredVars}}{{#hasRequired}}{{#hasOptional}}, +{{/hasOptional}}{{/hasRequired}}{{#optionalVars}}{{>dataClassOptVar}}{{^-last}}, +{{/-last}}{{/optionalVars}} +) {{/discriminator}}{{#parent}}: {{{.}}}{{/parent}}{ +{{#discriminator}} + {{#requiredVars}} + {{>interfaceReqVar}} + {{/requiredVars}} + {{#optionalVars}} + {{>interfaceOptVar}} + {{/optionalVars}} +{{/discriminator}} +{{#hasEnums}}{{#vars}}{{#isEnum}} + /** + * {{{description}}} + * Values: {{#allowableValues}}{{#enumVars}}{{&name}}{{^-last}},{{/-last}}{{/enumVars}}{{/allowableValues}} + */ + enum class {{{nameInPascalCase}}}(val value: {{#isContainer}}{{#items}}{{{dataType}}}{{/items}}{{/isContainer}}{{^isContainer}}{{{dataType}}}{{/isContainer}}) { + {{#allowableValues}}{{#values}} + @JsonProperty("{{.}}") `{{.}}`("{{.}}"){{^-last}},{{/-last}}{{/values}}{{/allowableValues}} + } +{{/isEnum}}{{/vars}}{{/hasEnums}} +} diff --git a/hideout-mastodon/templates/dataClassOptVar.mustache b/hideout-mastodon/templates/dataClassOptVar.mustache new file mode 100644 index 00000000..e8a4e681 --- /dev/null +++ b/hideout-mastodon/templates/dataClassOptVar.mustache @@ -0,0 +1,5 @@ +{{#useBeanValidation}}{{>beanValidation}}{{>beanValidationModel}}{{/useBeanValidation}}{{#swagger2AnnotationLibrary}} + @Schema({{#example}}example = "{{#lambdaRemoveLineBreak}}{{#lambdaEscapeDoubleQuote}}{{{.}}}{{/lambdaEscapeDoubleQuote}}{{/lambdaRemoveLineBreak}}", {{/example}}{{#isReadOnly}}readOnly = {{{isReadOnly}}}, {{/isReadOnly}}description = "{{{description}}}"){{/swagger2AnnotationLibrary}}{{#swagger1AnnotationLibrary}} + @ApiModelProperty({{#example}}example = "{{#lambdaRemoveLineBreak}}{{#lambdaEscapeDoubleQuote}}{{{.}}}{{/lambdaEscapeDoubleQuote}}{{/lambdaRemoveLineBreak}}", {{/example}}{{#isReadOnly}}readOnly = {{{isReadOnly}}}, {{/isReadOnly}}value = "{{{description}}}"){{/swagger1AnnotationLibrary}}{{#deprecated}} + @Deprecated(message = ""){{/deprecated}} +@get:JsonProperty("{{{baseName}}}"){{#isInherited}} override{{/isInherited}} {{>modelMutable}} {{#lambda.camelcase}} {{{name}}} {{/lambda.camelcase}}: {{#isEnum}}{{#isArray}}{{baseType}}<{{/isArray}}{{classname}}.{{{nameInPascalCase}}}{{#isArray}}>{{/isArray}}{{/isEnum}}{{^isEnum}}{{{dataType}}}{{/isEnum}}? = {{{defaultValue}}}{{^defaultValue}}null{{/defaultValue}} diff --git a/hideout-mastodon/templates/dataClassReqVar.mustache b/hideout-mastodon/templates/dataClassReqVar.mustache new file mode 100644 index 00000000..15d2118a --- /dev/null +++ b/hideout-mastodon/templates/dataClassReqVar.mustache @@ -0,0 +1,4 @@ +{{#useBeanValidation}}{{>beanValidation}}{{>beanValidationModel}}{{/useBeanValidation}}{{#swagger2AnnotationLibrary}} + @Schema({{#example}}example = "{{#lambdaRemoveLineBreak}}{{#lambdaEscapeDoubleQuote}}{{{.}}}{{/lambdaEscapeDoubleQuote}}{{/lambdaRemoveLineBreak}}", {{/example}}required = true, {{#isReadOnly}}readOnly = {{{isReadOnly}}}, {{/isReadOnly}}description = "{{{description}}}"){{/swagger2AnnotationLibrary}}{{#swagger1AnnotationLibrary}} + @ApiModelProperty({{#example}}example = "{{#lambdaRemoveLineBreak}}{{#lambdaEscapeDoubleQuote}}{{{.}}}{{/lambdaEscapeDoubleQuote}}{{/lambdaRemoveLineBreak}}", {{/example}}required = true, {{#isReadOnly}}readOnly = {{{isReadOnly}}}, {{/isReadOnly}}value = "{{{description}}}"){{/swagger1AnnotationLibrary}}{{^isNullable}}@get:NotNull{{/isNullable}} +@get:JsonProperty("{{{baseName}}}", required = true){{#isInherited}} override{{/isInherited}} {{>modelMutable}} {{{name}}}: {{#isEnum}}{{#isArray}}{{baseType}}<{{/isArray}}{{classname}}.{{{nameInPascalCase}}}{{#isArray}}>{{/isArray}}{{/isEnum}}{{^isEnum}}{{{dataType}}}{{/isEnum}}{{#isNullable}}?{{/isNullable}}{{#defaultValue}} = {{{.}}}{{/defaultValue}} diff --git a/hideout-mastodon/templates/enumClass.mustache b/hideout-mastodon/templates/enumClass.mustache new file mode 100644 index 00000000..57287535 --- /dev/null +++ b/hideout-mastodon/templates/enumClass.mustache @@ -0,0 +1,8 @@ +/** +* {{{description}}} +* Values: {{#allowableValues}}{{#enumVars}}{{&name}}{{^-last}},{{/-last}}{{/enumVars}}{{/allowableValues}} +*/ +enum class {{classname}}(val value: {{dataType}}) { +{{#allowableValues}}{{#enumVars}} + @JsonProperty({{{value}}}) {{&name}}({{{value}}}){{^-last}},{{/-last}}{{/enumVars}}{{/allowableValues}} +} diff --git a/hideout-mastodon/templates/exceptions.mustache b/hideout-mastodon/templates/exceptions.mustache new file mode 100644 index 00000000..5a2a7aec --- /dev/null +++ b/hideout-mastodon/templates/exceptions.mustache @@ -0,0 +1,29 @@ +package {{apiPackage}} + +import org.springframework.http.HttpStatus +import org.springframework.web.bind.annotation.ControllerAdvice +import org.springframework.web.bind.annotation.ExceptionHandler +import {{javaxPackage}}.servlet.http.HttpServletResponse +import {{javaxPackage}}.validation.ConstraintViolationException + +// TODO Extend ApiException for custom exception handling, e.g. the below NotFound exception +sealed class ApiException(msg: String, val code: Int) : Exception(msg) + +class NotFoundException(msg: String, code: Int = HttpStatus.NOT_FOUND.value()) : ApiException(msg, code) + + +@ControllerAdvice +class DefaultExceptionHandler { + +@ExceptionHandler(value = [ApiException::class]) +fun onApiException(ex: ApiException, response: HttpServletResponse): Unit = +response.sendError(ex.code, ex.message) + +@ExceptionHandler(value = [NotImplementedError::class]) +fun onNotImplemented(ex: NotImplementedError, response: HttpServletResponse): Unit = +response.sendError(HttpStatus.NOT_IMPLEMENTED.value()) + +@ExceptionHandler(value = [ConstraintViolationException::class]) +fun onConstraintViolation(ex: ConstraintViolationException, response: HttpServletResponse): Unit = +response.sendError(HttpStatus.BAD_REQUEST.value(), ex.constraintViolations.joinToString(", ") { it.message }) +} diff --git a/hideout-mastodon/templates/formParams.mustache b/hideout-mastodon/templates/formParams.mustache new file mode 100644 index 00000000..ec72b53b --- /dev/null +++ b/hideout-mastodon/templates/formParams.mustache @@ -0,0 +1 @@ +{{#isFormParam}}{{^isFile}}{{#swagger2AnnotationLibrary}}@Parameter(description = "{{{description}}}"{{#required}}, required = true{{/required}}{{#allowableValues}}{{#defaultValue}}, schema = Schema(allowableValues = [{{#values}}"{{{.}}}"{{^-last}}, {{/-last}}{{/values}}]{{^isContainer}}, defaultValue = {{^isString}}"{{/isString}}{{#isString}}{{#isEnum}}"{{/isEnum}}{{/isString}}{{{defaultValue}}}{{^isString}}"{{/isString}}{{#isString}}{{#isEnum}}"{{/isEnum}}{{/isString}}{{/isContainer}}){{/defaultValue}}{{/allowableValues}}{{#allowableValues}}{{^defaultValue}}, schema = Schema(allowableValues = [{{#values}}"{{{.}}}"{{^-last}}, {{/-last}}{{/values}}]){{/defaultValue}}{{/allowableValues}}{{^allowableValues}}{{#defaultValue}}{{^isContainer}}, schema = Schema(defaultValue = {{^isString}}"{{/isString}}{{#isString}}{{#isEnum}}"{{/isEnum}}{{/isString}}{{{defaultValue}}}{{^isString}}"{{/isString}}{{#isString}}{{#isEnum}}"{{/isEnum}}{{/isString}}){{/isContainer}}{{/defaultValue}}{{/allowableValues}}){{/swagger2AnnotationLibrary}}{{#swagger1AnnotationLibrary}}@ApiParam(value = "{{{description}}}"{{#required}}, required = true{{/required}}{{#allowableValues}}, allowableValues = "{{#values}}{{{.}}}{{^-last}}, {{/-last}}{{/values}}"{{/allowableValues}}{{#defaultValue}}, defaultValue = "{{{.}}}"{{/defaultValue}}){{/swagger1AnnotationLibrary}} @RequestParam(value = "{{baseName}}"{{#required}}, required = true{{/required}}{{^required}}, required = false{{/required}}) {{paramName}}: {{>optionalDataType}} {{/isFile}}{{#isFile}}{{#swagger2AnnotationLibrary}}@Parameter(description = "file detail"){{/swagger2AnnotationLibrary}}{{#swagger1AnnotationLibrary}}@ApiParam(value = "file detail"){{/swagger1AnnotationLibrary}} {{#useBeanValidation}}@Valid{{/useBeanValidation}} @RequestPart("file") {{baseName}}: {{>optionalDataType}}{{/isFile}}{{/isFormParam}} \ No newline at end of file diff --git a/hideout-mastodon/templates/generatedAnnotation.mustache b/hideout-mastodon/templates/generatedAnnotation.mustache new file mode 100644 index 00000000..1be8e755 --- /dev/null +++ b/hideout-mastodon/templates/generatedAnnotation.mustache @@ -0,0 +1 @@ +@{{javaxPackage}}.annotation.Generated(value = ["{{generatorClass}}"]{{^hideGenerationTimestamp}}, date = "{{generatedDate}}"{{/hideGenerationTimestamp}}) \ No newline at end of file diff --git a/hideout-mastodon/templates/headerParams.mustache b/hideout-mastodon/templates/headerParams.mustache new file mode 100644 index 00000000..9bc3f600 --- /dev/null +++ b/hideout-mastodon/templates/headerParams.mustache @@ -0,0 +1 @@ +{{#isHeaderParam}}{{#useBeanValidation}}{{>beanValidationPath}}{{/useBeanValidation}}{{#swagger2AnnotationLibrary}}@Parameter(description = "{{{description}}}", `in` = ParameterIn.HEADER{{#required}}, required = true{{/required}}{{#allowableValues}}{{#defaultValue}}, schema = Schema(allowableValues = [{{#values}}"{{{.}}}"{{^-last}}, {{/-last}}{{/values}}]{{^isContainer}}, defaultValue = {{^isString}}"{{/isString}}{{#isString}}{{#isEnum}}"{{/isEnum}}{{/isString}}{{{defaultValue}}}{{^isString}}"{{/isString}}{{#isString}}{{#isEnum}}"{{/isEnum}}{{/isString}}{{/isContainer}}){{/defaultValue}}{{/allowableValues}}{{#allowableValues}}{{^defaultValue}}, schema = Schema(allowableValues = [{{#values}}"{{{.}}}"{{^-last}}, {{/-last}}{{/values}}]){{/defaultValue}}{{/allowableValues}}{{^allowableValues}}{{#defaultValue}}{{^isContainer}}, schema = Schema(defaultValue = {{^isString}}"{{/isString}}{{#isString}}{{#isEnum}}"{{/isEnum}}{{/isString}}{{{defaultValue}}}{{^isString}}"{{/isString}}{{#isString}}{{#isEnum}}"{{/isEnum}}{{/isString}}){{/isContainer}}{{/defaultValue}}{{/allowableValues}}){{/swagger2AnnotationLibrary}}{{#swagger1AnnotationLibrary}}@ApiParam(value = "{{{description}}}"{{#required}}, required = true{{/required}}{{#allowableValues}}, allowableValues = "{{#values}}{{{.}}}{{^-last}}, {{/-last}}{{/values}}"{{/allowableValues}}{{#defaultValue}}, defaultValue = "{{{.}}}"{{/defaultValue}}){{/swagger1AnnotationLibrary}} @RequestHeader(value = "{{baseName}}", required = {{#required}}true{{/required}}{{^required}}false{{/required}}) {{paramName}}: {{>optionalDataType}}{{/isHeaderParam}} \ No newline at end of file diff --git a/hideout-mastodon/templates/homeController.mustache b/hideout-mastodon/templates/homeController.mustache new file mode 100644 index 00000000..b9ab27dd --- /dev/null +++ b/hideout-mastodon/templates/homeController.mustache @@ -0,0 +1,91 @@ +package {{basePackage}} + +import org.springframework.context.annotation.Bean +import org.springframework.stereotype.Controller +import org.springframework.web.bind.annotation.RequestMapping +{{#sourceDocumentationProvider}} + import com.fasterxml.jackson.dataformat.yaml.YAMLMapper + import org.springframework.beans.factory.annotation.Value + import org.springframework.core.io.Resource + import org.springframework.util.StreamUtils + import org.springframework.web.bind.annotation.ResponseBody + import org.springframework.web.bind.annotation.GetMapping +{{/sourceDocumentationProvider}} +{{^sourceDocumentationProvider}} + {{#useSwaggerUI}} + import org.springframework.web.bind.annotation.ResponseBody + import org.springframework.web.bind.annotation.GetMapping + {{/useSwaggerUI}} +{{/sourceDocumentationProvider}} +{{#reactive}} + import org.springframework.web.reactive.function.server.HandlerFunction + import org.springframework.web.reactive.function.server.RequestPredicates.GET + import org.springframework.web.reactive.function.server.RouterFunction + import org.springframework.web.reactive.function.server.RouterFunctions.route + import org.springframework.web.reactive.function.server.ServerResponse + import java.net.URI +{{/reactive}} +{{#sourceDocumentationProvider}} + import java.nio.charset.Charset +{{/sourceDocumentationProvider}} + +/** +* Home redirection to OpenAPI api documentation +*/ +@Controller +class HomeController { +{{#useSwaggerUI}} + {{^springDocDocumentationProvider}} + {{#sourceDocumentationProvider}} + private val apiDocsPath = "/openapi.json" + {{/sourceDocumentationProvider}} + {{#springFoxDocumentationProvider}} + private val apiDocsPath = "/v2/api-docs" + {{/springFoxDocumentationProvider}} + {{/springDocDocumentationProvider}} +{{/useSwaggerUI}} +{{#sourceDocumentationProvider}} + private val yamlMapper = YAMLMapper() + + @Value("classpath:/openapi.yaml") + private lateinit var openapi: Resource + + @Bean + fun openapiContent(): String { + return openapi.inputStream.use { + StreamUtils.copyToString(it, Charset.defaultCharset()) + } + } + + @GetMapping(value = ["/openapi.yaml"], produces = ["application/vnd.oai.openapi"]) + @ResponseBody + fun openapiYaml(): String = openapiContent() + + @GetMapping(value = ["/openapi.json"], produces = ["application/json"]) + @ResponseBody + fun openapiJson(): Any = yamlMapper.readValue(openapiContent(), Any::class.java) +{{/sourceDocumentationProvider}} +{{#useSwaggerUI}} + {{^springDocDocumentationProvider}} + + @GetMapping(value = ["/swagger-config.yaml"], produces = ["text/plain"]) + @ResponseBody + fun swaggerConfig(): String = "url: $apiDocsPath\n" + {{/springDocDocumentationProvider}} + {{#reactive}} + + @Bean + fun index(): RouterFunction + = route( + GET("/"), HandlerFunction + { + ServerResponse.temporaryRedirect(URI.create("swagger-ui.html")).build() + }) + {{/reactive}} + {{^reactive}} + + @RequestMapping("/") + fun index(): String = "redirect:swagger-ui.html" + {{/reactive}} +{{/useSwaggerUI}} + } diff --git a/hideout-mastodon/templates/interfaceOptVar.mustache b/hideout-mastodon/templates/interfaceOptVar.mustache new file mode 100644 index 00000000..158ad759 --- /dev/null +++ b/hideout-mastodon/templates/interfaceOptVar.mustache @@ -0,0 +1,4 @@ +{{#swagger2AnnotationLibrary}} + @get:Schema({{#example}}example = "{{{.}}}", {{/example}}{{#required}}requiredMode = Schema.RequiredMode.REQUIRED, {{/required}}{{#isReadOnly}}readOnly = {{{isReadOnly}}}, {{/isReadOnly}}description = "{{{description}}}"){{/swagger2AnnotationLibrary}}{{#swagger1AnnotationLibrary}} + @get:ApiModelProperty({{#example}}example = "{{{.}}}", {{/example}}{{#required}}required = {{required}}, {{/required}}{{#isReadOnly}}readOnly = {{{isReadOnly}}}, {{/isReadOnly}}value = "{{{description}}}"){{/swagger1AnnotationLibrary}} +{{>modelMutable}} {{{name}}}: {{#isEnum}}{{classname}}.{{nameInCamelCase}}{{/isEnum}}{{^isEnum}}{{{dataType}}}{{/isEnum}}? {{^discriminator}}= {{{defaultValue}}}{{^defaultValue}}null{{/defaultValue}}{{/discriminator}} diff --git a/hideout-mastodon/templates/interfaceReqVar.mustache b/hideout-mastodon/templates/interfaceReqVar.mustache new file mode 100644 index 00000000..eeeda60d --- /dev/null +++ b/hideout-mastodon/templates/interfaceReqVar.mustache @@ -0,0 +1,4 @@ +{{#swagger2AnnotationLibrary}} + @get:Schema({{#example}}example = "{{{.}}}", {{/example}}{{#required}}requiredMode = Schema.RequiredMode.REQUIRED, {{/required}}{{#isReadOnly}}readOnly = {{{isReadOnly}}}, {{/isReadOnly}}description = "{{{description}}}"){{/swagger2AnnotationLibrary}}{{#swagger1AnnotationLibrary}} + @get:ApiModelProperty({{#example}}example = "{{{.}}}", {{/example}}{{#required}}required = {{required}}, {{/required}}{{#isReadOnly}}readOnly = {{{isReadOnly}}}, {{/isReadOnly}}value = "{{{description}}}"){{/swagger1AnnotationLibrary}} +{{>modelMutable}} {{{name}}}: {{#isEnum}}{{classname}}.{{nameInCamelCase}}{{/isEnum}}{{^isEnum}}{{{dataType}}}{{/isEnum}} diff --git a/hideout-mastodon/templates/libraries/spring-boot/README.mustache b/hideout-mastodon/templates/libraries/spring-boot/README.mustache new file mode 100644 index 00000000..c75176be --- /dev/null +++ b/hideout-mastodon/templates/libraries/spring-boot/README.mustache @@ -0,0 +1,21 @@ +# {{title}}{{^title}}Generated Kotlin Spring Boot App{{/title}} + +This Kotlin based [Spring Boot](https://spring.io/projects/spring-boot) application has been generated using the [OpenAPI Generator](https://github.com/OpenAPITools/openapi-generator). + +## Getting Started + +This document assumes you have either maven or gradle available, either via the wrapper or otherwise. This does not come with a gradle / maven wrapper checked in. + +By default a [`pom.xml`](pom.xml) file will be generated. If you specified `gradleBuildFile=true` when generating this project, a `build.gradle.kts` will also be generated. Note this uses [Gradle Kotlin DSL](https://github.com/gradle/kotlin-dsl). + +To build the project using maven, run: +```bash +mvn package && java -jar target/{{artifactId}}-{{artifactVersion}}.jar +``` + +To build the project using gradle, run: +```bash +gradle build && java -jar build/libs/{{artifactId}}-{{artifactVersion}}.jar +``` + +If all builds successfully, the server should run on [http://localhost:8080/](http://localhost:{{serverPort}}/) diff --git a/hideout-mastodon/templates/libraries/spring-boot/application.mustache b/hideout-mastodon/templates/libraries/spring-boot/application.mustache new file mode 100644 index 00000000..9f752949 --- /dev/null +++ b/hideout-mastodon/templates/libraries/spring-boot/application.mustache @@ -0,0 +1,10 @@ +spring: +application: +name: {{title}} + +jackson: +serialization: +WRITE_DATES_AS_TIMESTAMPS: false + +server: +port: {{serverPort}} diff --git a/hideout-mastodon/templates/libraries/spring-boot/buildGradle-sb3-Kts.mustache b/hideout-mastodon/templates/libraries/spring-boot/buildGradle-sb3-Kts.mustache new file mode 100644 index 00000000..adc4d735 --- /dev/null +++ b/hideout-mastodon/templates/libraries/spring-boot/buildGradle-sb3-Kts.mustache @@ -0,0 +1,61 @@ +import org.jetbrains.kotlin.gradle.tasks.KotlinCompile + +group = "{{groupId}}" +version = "{{artifactVersion}}" +java.sourceCompatibility = JavaVersion.VERSION_17 + +repositories { +mavenCentral() +maven { url = uri("https://repo.spring.io/milestone") } +} + +tasks.withType + { + kotlinOptions.jvmTarget = "17" + } + + plugins { + val kotlinVersion = "1.7.10" + id("org.jetbrains.kotlin.jvm") version kotlinVersion + id("org.jetbrains.kotlin.plugin.jpa") version kotlinVersion + id("org.jetbrains.kotlin.plugin.spring") version kotlinVersion + id("org.springframework.boot") version "3.0.2" + id("io.spring.dependency-management") version "1.0.14.RELEASE" + } + + dependencies { + {{#reactive}} val kotlinxCoroutinesVersion = "1.6.1" + {{/reactive}} implementation("org.jetbrains.kotlin:kotlin-stdlib-jdk8") + implementation("org.jetbrains.kotlin:kotlin-reflect"){{^reactive}} + implementation("org.springframework.boot:spring-boot-starter-web"){{/reactive}}{{#reactive}} + implementation("org.springframework.boot:spring-boot-starter-webflux") + implementation("org.jetbrains.kotlinx:kotlinx-coroutines-core:$kotlinxCoroutinesVersion") + implementation("org.jetbrains.kotlinx:kotlinx-coroutines-reactor:$kotlinxCoroutinesVersion"){{/reactive}}{{#springDocDocumentationProvider}}{{#useSwaggerUI}} + implementation("org.springdoc:springdoc-openapi-starter-{{#reactive}} + webflux{{/reactive}}{{^reactive}}webmvc{{/reactive}}-ui:2.0.0-M5"){{/useSwaggerUI}}{{^useSwaggerUI}} + implementation("org.springdoc:springdoc-openapi-{{#reactive}} + webflux{{/reactive}}{{^reactive}}webmvc{{/reactive}} + -core:2.0.0-M5"){{/useSwaggerUI}}{{/springDocDocumentationProvider}}{{#springFoxDocumentationProvider}} + implementation("io.springfox:springfox-swagger2:2.9.2"){{/springFoxDocumentationProvider}}{{#useSwaggerUI}}{{^springDocDocumentationProvider}} + implementation("org.webjars:swagger-ui:4.10.3") + implementation("org.webjars:webjars-locator-core"){{/springDocDocumentationProvider}}{{/useSwaggerUI}}{{^springFoxDocumentationProvider}}{{^springDocDocumentationProvider}}{{#swagger1AnnotationLibrary}} + implementation("io.swagger:swagger-annotations:1.6.6"){{/swagger1AnnotationLibrary}}{{#swagger2AnnotationLibrary}} + implementation("io.swagger.core.v3:swagger-annotations:2.2.0"){{/swagger2AnnotationLibrary}}{{/springDocDocumentationProvider}}{{/springFoxDocumentationProvider}} + + implementation("com.google.code.findbugs:jsr305:3.0.2") + implementation("com.fasterxml.jackson.dataformat:jackson-dataformat-yaml") + implementation("com.fasterxml.jackson.dataformat:jackson-dataformat-xml") + implementation("com.fasterxml.jackson.datatype:jackson-datatype-jsr310") + implementation("com.fasterxml.jackson.module:jackson-module-kotlin") + {{#useBeanValidation}} + implementation("jakarta.validation:jakarta.validation-api"){{/useBeanValidation}} + implementation("jakarta.annotation:jakarta.annotation-api:2.1.0") + + testImplementation("org.jetbrains.kotlin:kotlin-test-junit5") + testImplementation("org.springframework.boot:spring-boot-starter-test") { + exclude(module = "junit") + } + {{#reactive}} + testImplementation`("org.jetbrains.kotlinx:kotlinx-coroutines-test:$kotlinxCoroutinesVersion") + {{/reactive}} + } diff --git a/hideout-mastodon/templates/libraries/spring-boot/buildGradleKts.mustache b/hideout-mastodon/templates/libraries/spring-boot/buildGradleKts.mustache new file mode 100644 index 00000000..3459ef3d --- /dev/null +++ b/hideout-mastodon/templates/libraries/spring-boot/buildGradleKts.mustache @@ -0,0 +1,68 @@ +import org.jetbrains.kotlin.gradle.tasks.KotlinCompile + +buildscript { +repositories { +mavenCentral() +} +dependencies { +classpath("org.springframework.boot:spring-boot-gradle-plugin:2.6.7") +} +} + +group = "{{groupId}}" +version = "{{artifactVersion}}" + +repositories { +mavenCentral() +} + +tasks.withType + { + kotlinOptions.jvmTarget = "1.8" + } + + plugins { + val kotlinVersion = "1.6.21" + id("org.jetbrains.kotlin.jvm") version kotlinVersion + id("org.jetbrains.kotlin.plugin.jpa") version kotlinVersion + id("org.jetbrains.kotlin.plugin.spring") version kotlinVersion + id("org.springframework.boot") version "2.6.7" + id("io.spring.dependency-management") version "1.0.11.RELEASE" + } + + dependencies { + {{#reactive}} val kotlinxCoroutinesVersion = "1.6.1" + {{/reactive}} compile("org.jetbrains.kotlin:kotlin-stdlib-jdk8") + compile("org.jetbrains.kotlin:kotlin-reflect"){{^reactive}} + compile("org.springframework.boot:spring-boot-starter-web"){{/reactive}}{{#reactive}} + compile("org.springframework.boot:spring-boot-starter-webflux") + compile("org.jetbrains.kotlinx:kotlinx-coroutines-core:$kotlinxCoroutinesVersion") + compile("org.jetbrains.kotlinx:kotlinx-coroutines-reactor:$kotlinxCoroutinesVersion"){{/reactive}}{{#springDocDocumentationProvider}}{{#useSwaggerUI}} + compile("org.springdoc:springdoc-openapi-{{#reactive}} + webflux-{{/reactive}}ui:1.6.8"){{/useSwaggerUI}}{{^useSwaggerUI}} + compile("org.springdoc:springdoc-openapi-{{#reactive}} + webflux{{/reactive}}{{^reactive}}webmvc{{/reactive}} + -core:1.6.8"){{/useSwaggerUI}}{{/springDocDocumentationProvider}}{{#springFoxDocumentationProvider}} + compile("io.springfox:springfox-swagger2:2.9.2"){{/springFoxDocumentationProvider}}{{#useSwaggerUI}}{{^springDocDocumentationProvider}} + compile("org.webjars:swagger-ui:4.10.3") + compile("org.webjars:webjars-locator-core"){{/springDocDocumentationProvider}}{{/useSwaggerUI}}{{^springFoxDocumentationProvider}}{{^springDocDocumentationProvider}}{{#swagger1AnnotationLibrary}} + compile("io.swagger:swagger-annotations:1.6.6"){{/swagger1AnnotationLibrary}}{{#swagger2AnnotationLibrary}} + compile("io.swagger.core.v3:swagger-annotations:2.2.0"){{/swagger2AnnotationLibrary}}{{/springDocDocumentationProvider}}{{/springFoxDocumentationProvider}} + + compile("com.google.code.findbugs:jsr305:3.0.2") + compile("com.fasterxml.jackson.dataformat:jackson-dataformat-yaml") + compile("com.fasterxml.jackson.dataformat:jackson-dataformat-xml") + compile("com.fasterxml.jackson.datatype:jackson-datatype-jsr310") + compile("com.fasterxml.jackson.module:jackson-module-kotlin") + {{#useBeanValidation}} + compile("jakarta.validation:jakarta.validation-api"){{/useBeanValidation}} + compile("jakarta.annotation:jakarta.annotation-api:2.1.0") + + testCompile("org.jetbrains.kotlin:kotlin-test-junit5") + testCompile("org.springframework.boot:spring-boot-starter-test") { + exclude(module = "junit") + } + {{#reactive}} + testCompile("org.jetbrains.kotlinx:kotlinx-coroutines-test:$kotlinxCoroutinesVersion") + {{/reactive}} + } diff --git a/hideout-mastodon/templates/libraries/spring-boot/defaultBasePath.mustache b/hideout-mastodon/templates/libraries/spring-boot/defaultBasePath.mustache new file mode 100644 index 00000000..3c7185bd --- /dev/null +++ b/hideout-mastodon/templates/libraries/spring-boot/defaultBasePath.mustache @@ -0,0 +1 @@ +{{contextPath}} \ No newline at end of file diff --git a/hideout-mastodon/templates/libraries/spring-boot/pom-sb3.mustache b/hideout-mastodon/templates/libraries/spring-boot/pom-sb3.mustache new file mode 100644 index 00000000..5fa48b58 --- /dev/null +++ b/hideout-mastodon/templates/libraries/spring-boot/pom-sb3.mustache @@ -0,0 +1,210 @@ + + 4.0.0 + {{groupId}} + {{artifactId}} + jar + {{artifactId}} + {{artifactVersion}} + {{#reactive}} + 1.6.1 + {{/reactive}}{{#springDocDocumentationProvider}}{{#useSwaggerUI}} + 2.0.2 + {{/useSwaggerUI}}{{/springDocDocumentationProvider}}{{#springFoxDocumentationProvider}} + 2.9.2 + {{/springFoxDocumentationProvider}}{{#useSwaggerUI}}{{^springDocDocumentationProvider}} + 4.15.5 + {{/springDocDocumentationProvider}}{{/useSwaggerUI}}{{^springFoxDocumentationProvider}}{{^springDocDocumentationProvider}}{{#swagger1AnnotationLibrary}} + 1.6.6 + {{/swagger1AnnotationLibrary}}{{#swagger2AnnotationLibrary}} + 2.2.7 + {{/swagger2AnnotationLibrary}}{{/springDocDocumentationProvider}}{{/springFoxDocumentationProvider}} + 3.0.2 + 2.1.0 + 1.7.10 + + 1.7.10 + UTF-8 + + + org.springframework.boot + spring-boot-starter-parent + 3.0.2 + + + + repository.spring.milestone + Spring Milestone Repository + https://repo.spring.io/milestone + + + + + spring-milestones + https://repo.spring.io/milestone + + + + ${project.basedir}/src/main/kotlin + ${project.basedir}/src/test/kotlin + {{^interfaceOnly}} + + org.springframework.boot + spring-boot-maven-plugin + + + + repackage + + + + {{/interfaceOnly}} + + kotlin-maven-plugin + org.jetbrains.kotlin + ${kotlin.version} + + + spring + + 17 + + + + compile + compile + + compile + + + + test-compile + test-compile + + test-compile + + + + + + org.jetbrains.kotlin + kotlin-maven-allopen + ${kotlin.version} + + + + + + + + org.jetbrains.kotlin + kotlin-stdlib-jdk8 + ${kotlin.version} + + + org.jetbrains.kotlin + kotlin-reflect + ${kotlin.version} + {{^reactive}} + + org.springframework.boot + spring-boot-starter-web + {{/reactive}}{{#reactive}} + + org.springframework.boot + spring-boot-starter-webflux + + + org.jetbrains.kotlinx + kotlinx-coroutines-core + ${kotlinx-coroutines.version} + + + org.jetbrains.kotlinx + kotlinx-coroutines-reactor + ${kotlinx-coroutines.version} + {{/reactive}} + + {{#springDocDocumentationProvider}} + {{#useSwaggerUI}} + + org.springdoc + springdoc-openapi-starter-{{#reactive}} + webflux{{/reactive}}{{^reactive}}webmvc{{/reactive}}-ui + + ${springdoc-openapi.version} + {{/useSwaggerUI}}{{^useSwaggerUI}} + + org.springdoc + springdoc-openapi-{{#reactive}}webflux{{/reactive}}{{^reactive}}webmvc{{/reactive}}-core + + ${springdoc-openapi.version} + {{/useSwaggerUI}}{{/springDocDocumentationProvider}}{{#springFoxDocumentationProvider}} + + + io.springfox + springfox-swagger2 + ${springfox-swagger2.version} + {{/springFoxDocumentationProvider}}{{#useSwaggerUI}}{{^springDocDocumentationProvider}} + + org.webjars + swagger-ui + ${swagger-ui.version} + + + org.webjars + webjars-locator-core + {{/springDocDocumentationProvider}}{{/useSwaggerUI}}{{^springFoxDocumentationProvider}}{{^springDocDocumentationProvider}}{{#swagger1AnnotationLibrary}} + + io.swagger + swagger-annotations + ${swagger-annotations.version} + {{/swagger1AnnotationLibrary}}{{#swagger2AnnotationLibrary}} + + io.swagger.core.v3 + swagger-annotations + ${swagger-annotations.version} + {{/swagger2AnnotationLibrary}}{{/springDocDocumentationProvider}}{{/springFoxDocumentationProvider}} + + + + com.google.code.findbugs + jsr305 + ${findbugs-jsr305.version} + + + com.fasterxml.jackson.dataformat + jackson-dataformat-yaml + + + com.fasterxml.jackson.dataformat + jackson-dataformat-xml + + + com.fasterxml.jackson.datatype + jackson-datatype-jsr310 + + + com.fasterxml.jackson.module + jackson-module-kotlin + + {{#useBeanValidation}} + + + jakarta.validation + jakarta.validation-api + {{/useBeanValidation}} + + jakarta.annotation + jakarta.annotation-api + ${jakarta-annotation.version} + provided + + + org.jetbrains.kotlin + kotlin-test-junit5 + ${kotlin-test-junit5.version} + test + + + diff --git a/hideout-mastodon/templates/libraries/spring-boot/pom.mustache b/hideout-mastodon/templates/libraries/spring-boot/pom.mustache new file mode 100644 index 00000000..967ff19b --- /dev/null +++ b/hideout-mastodon/templates/libraries/spring-boot/pom.mustache @@ -0,0 +1,195 @@ + + 4.0.0 + {{groupId}} + {{artifactId}} + jar + {{artifactId}} + {{artifactVersion}} + {{#reactive}} + 1.6.1 + {{/reactive}}{{#springDocDocumentationProvider}}{{#useSwaggerUI}} + 1.6.8 + {{/useSwaggerUI}}{{/springDocDocumentationProvider}}{{#springFoxDocumentationProvider}} + 2.9.2 + {{/springFoxDocumentationProvider}}{{#useSwaggerUI}}{{^springDocDocumentationProvider}} + 4.10.3 + {{/springDocDocumentationProvider}}{{/useSwaggerUI}}{{^springFoxDocumentationProvider}}{{^springDocDocumentationProvider}}{{#swagger1AnnotationLibrary}} + 1.6.6 + {{/swagger1AnnotationLibrary}}{{#swagger2AnnotationLibrary}} + 2.2.0 + {{/swagger2AnnotationLibrary}}{{/springDocDocumentationProvider}}{{/springFoxDocumentationProvider}} + 3.0.2 + 2.1.0 + 1.6.21 + + 1.6.21 + UTF-8 + + + org.springframework.boot + spring-boot-starter-parent + 2.6.7 + + + ${project.basedir}/src/main/kotlin + ${project.basedir}/src/test/kotlin + {{^interfaceOnly}} + + org.springframework.boot + spring-boot-maven-plugin + + + + repackage + + + + {{/interfaceOnly}} + + kotlin-maven-plugin + org.jetbrains.kotlin + ${kotlin.version} + + + spring + + 1.8 + + + + compile + compile + + compile + + + + test-compile + test-compile + + test-compile + + + + + + org.jetbrains.kotlin + kotlin-maven-allopen + ${kotlin.version} + + + + + + + + org.jetbrains.kotlin + kotlin-stdlib-jdk8 + ${kotlin.version} + + + org.jetbrains.kotlin + kotlin-reflect + ${kotlin.version} + {{^reactive}} + + org.springframework.boot + spring-boot-starter-web + {{/reactive}}{{#reactive}} + + org.springframework.boot + spring-boot-starter-webflux + + + org.jetbrains.kotlinx + kotlinx-coroutines-core + ${kotlinx-coroutines.version} + + + org.jetbrains.kotlinx + kotlinx-coroutines-reactor + ${kotlinx-coroutines.version} + {{/reactive}} + + {{#springDocDocumentationProvider}} + {{#useSwaggerUI}} + + org.springdoc + springdoc-openapi-{{#reactive}}webflux-{{/reactive}}ui + ${springdoc-openapi.version} + {{/useSwaggerUI}}{{^useSwaggerUI}} + + org.springdoc + springdoc-openapi-{{#reactive}}webflux{{/reactive}}{{^reactive}}webmvc{{/reactive}}-core + + ${springdoc-openapi.version} + {{/useSwaggerUI}}{{/springDocDocumentationProvider}}{{#springFoxDocumentationProvider}} + + + io.springfox + springfox-swagger2 + ${springfox-swagger2.version} + {{/springFoxDocumentationProvider}}{{#useSwaggerUI}}{{^springDocDocumentationProvider}} + + org.webjars + swagger-ui + ${swagger-ui.version} + + + org.webjars + webjars-locator-core + {{/springDocDocumentationProvider}}{{/useSwaggerUI}}{{^springFoxDocumentationProvider}}{{^springDocDocumentationProvider}}{{#swagger1AnnotationLibrary}} + + io.swagger + swagger-annotations + ${swagger-annotations.version} + {{/swagger1AnnotationLibrary}}{{#swagger2AnnotationLibrary}} + + io.swagger.core.v3 + swagger-annotations + ${swagger-annotations.version} + {{/swagger2AnnotationLibrary}}{{/springDocDocumentationProvider}}{{/springFoxDocumentationProvider}} + + + + com.google.code.findbugs + jsr305 + ${findbugs-jsr305.version} + + + com.fasterxml.jackson.dataformat + jackson-dataformat-yaml + + + com.fasterxml.jackson.dataformat + jackson-dataformat-xml + + + com.fasterxml.jackson.datatype + jackson-datatype-jsr310 + + + com.fasterxml.jackson.module + jackson-module-kotlin + + {{#useBeanValidation}} + + + jakarta.validation + jakarta.validation-api + {{/useBeanValidation}} + + jakarta.annotation + jakarta.annotation-api + ${jakarta-annotation.version} + provided + + + org.jetbrains.kotlin + kotlin-test-junit5 + ${kotlin-test-junit5.version} + test + + + diff --git a/hideout-mastodon/templates/libraries/spring-boot/settingsGradle.mustache b/hideout-mastodon/templates/libraries/spring-boot/settingsGradle.mustache new file mode 100644 index 00000000..3176ec97 --- /dev/null +++ b/hideout-mastodon/templates/libraries/spring-boot/settingsGradle.mustache @@ -0,0 +1,15 @@ +pluginManagement { +repositories { +maven { url = uri("https://repo.spring.io/snapshot") } +maven { url = uri("https://repo.spring.io/milestone") } +gradlePluginPortal() +} +resolutionStrategy { +eachPlugin { +if (requested.id.id == "org.springframework.boot") { +useModule("org.springframework.boot:spring-boot-gradle-plugin:${requested.version}") +} +} +} +} +rootProject.name = "{{artifactId}}" diff --git a/hideout-mastodon/templates/libraries/spring-boot/springBootApplication.mustache b/hideout-mastodon/templates/libraries/spring-boot/springBootApplication.mustache new file mode 100644 index 00000000..e86b92e0 --- /dev/null +++ b/hideout-mastodon/templates/libraries/spring-boot/springBootApplication.mustache @@ -0,0 +1,15 @@ +package {{basePackage}} + +import org.springframework.boot.runApplication +import org.springframework.boot.autoconfigure.SpringBootApplication +import org.springframework.context.annotation.ComponentScan + +@SpringBootApplication +@ComponentScan(basePackages = ["{{basePackage}}", "{{apiPackage}}", "{{modelPackage}}"]) +class Application + +fun main(args: Array +) { + runApplication + (*args) + } diff --git a/hideout-mastodon/templates/libraries/spring-boot/swagger-ui.mustache b/hideout-mastodon/templates/libraries/spring-boot/swagger-ui.mustache new file mode 100644 index 00000000..ce2cfd92 --- /dev/null +++ b/hideout-mastodon/templates/libraries/spring-boot/swagger-ui.mustache @@ -0,0 +1,57 @@ + + + + + + Swagger UI + + + + + + + +
+ + + + + + diff --git a/hideout-mastodon/templates/libraries/spring-cloud/README.mustache b/hideout-mastodon/templates/libraries/spring-cloud/README.mustache new file mode 100644 index 00000000..9949253f --- /dev/null +++ b/hideout-mastodon/templates/libraries/spring-cloud/README.mustache @@ -0,0 +1,83 @@ +{{^interfaceOnly}} + # {{artifactId}} + + ## Requirements + + Building the API client library requires [Maven](https://maven.apache.org/) to be installed. + + ## Installation + + To install the API client library to your local Maven repository, simply execute: + + ```shell + mvn install + ``` + + To deploy it to a remote Maven repository instead, configure the settings of the repository and execute: + + ```shell + mvn deploy + ``` + + Refer to the [official documentation](https://maven.apache.org/plugins/maven-deploy-plugin/usage.html) for more information. + + ### Maven users + + Add this dependency to your project's POM: + + ```xml + + {{{groupId}}} + {{{artifactId}}} + {{{artifactVersion}}} + compile + + ``` + + ### Gradle users + + Add this dependency to your project's build file: + + ```groovy + compile "{{{groupId}}}:{{{artifactId}}}:{{{artifactVersion}}}" + ``` + + ### Others + + At first generate the JAR by executing: + + mvn package + + Then manually install the following JARs: + + * target/{{{artifactId}}}-{{{artifactVersion}}}.jar + * target/lib/*.jar +{{/interfaceOnly}} +{{#interfaceOnly}} + # OpenAPI generated API stub + + Spring Framework stub + + + ## Overview + This code was generated by the [OpenAPI Generator](https://openapi-generator.tech) project. + By using the [OpenAPI-Spec](https://openapis.org), you can easily generate an API stub. + This is an example of building API stub interfaces in Java using the Spring framework. + + The stubs generated can be used in your existing Spring-MVC or Spring-Boot application to create controller endpoints + by adding ```@Controller``` classes that implement the interface. Eg: + ```java + @Controller + public class PetController implements PetApi { + // implement all PetApi methods + } + ``` + + You can also use the interface to create [Spring-Cloud Feign clients](http://projects.spring.io/spring-cloud/spring-cloud.html#spring-cloud-feign-inheritance).Eg: + ```java + @FeignClient(name="pet", url="http://petstore.swagger.io/v2") + public interface PetClient extends PetApi { + + } + ``` +{{/interfaceOnly}} diff --git a/hideout-mastodon/templates/libraries/spring-cloud/apiClient.mustache b/hideout-mastodon/templates/libraries/spring-cloud/apiClient.mustache new file mode 100644 index 00000000..877bfbbe --- /dev/null +++ b/hideout-mastodon/templates/libraries/spring-cloud/apiClient.mustache @@ -0,0 +1,11 @@ +package {{package}} + +import org.springframework.cloud.openfeign.FeignClient +import {{configPackage}}.ClientConfiguration + +@FeignClient( +name="\${{openbrace}}{{classVarName}}.name:{{classVarName}}{{closebrace}}", +{{#useFeignClientUrl}}url="\${{openbrace}}{{classVarName}}.url:{{basePath}}{{closebrace}}", {{/useFeignClientUrl}} +configuration = [ClientConfiguration::class] +) +interface {{classname}}Client : {{classname}} diff --git a/hideout-mastodon/templates/libraries/spring-cloud/apiKeyRequestInterceptor.mustache b/hideout-mastodon/templates/libraries/spring-cloud/apiKeyRequestInterceptor.mustache new file mode 100644 index 00000000..e0fa18d2 --- /dev/null +++ b/hideout-mastodon/templates/libraries/spring-cloud/apiKeyRequestInterceptor.mustache @@ -0,0 +1,19 @@ +package {{configPackage}} + +import feign.RequestInterceptor +import feign.RequestTemplate + +class ApiKeyRequestInterceptor( +private val location: String, +private val name: String, +private val value: String, +) : RequestInterceptor { + +override fun apply(requestTemplate: RequestTemplate) { +if (location == "header") { +requestTemplate.header(name, value) +} else if (location == "query") { +requestTemplate.query(name, value) +} +} +} diff --git a/hideout-mastodon/templates/libraries/spring-cloud/buildGradle-sb3-Kts.mustache b/hideout-mastodon/templates/libraries/spring-cloud/buildGradle-sb3-Kts.mustache new file mode 100644 index 00000000..d1cb45c9 --- /dev/null +++ b/hideout-mastodon/templates/libraries/spring-cloud/buildGradle-sb3-Kts.mustache @@ -0,0 +1,72 @@ +import org.jetbrains.kotlin.gradle.tasks.KotlinCompile + +group = "{{groupId}}" +version = "{{artifactVersion}}" +java.sourceCompatibility = JavaVersion.VERSION_17 + +repositories { +mavenCentral() +maven { url = uri("https://repo.spring.io/milestone") } +} + +tasks.withType + { + kotlinOptions.jvmTarget = "17" + } + + plugins { + val kotlinVersion = "1.7.10" + id("org.jetbrains.kotlin.jvm") version kotlinVersion + id("org.jetbrains.kotlin.plugin.jpa") version kotlinVersion + id("org.jetbrains.kotlin.plugin.spring") version kotlinVersion + id("org.springframework.boot") version "3.0.2" + id("io.spring.dependency-management") version "1.0.14.RELEASE" + } + + tasks.getByName("bootJar") { + enabled = false + } + + tasks.getByName("jar") { + enabled = true + } + + dependencyManagement { + imports { + mavenBom("org.springframework.cloud:spring-cloud-dependencies:2021.0.5") + } + } + + dependencies { + {{#reactive}} val kotlinxCoroutinesVersion = "1.6.1" + {{/reactive}} implementation("org.jetbrains.kotlin:kotlin-stdlib-jdk8") + implementation("org.jetbrains.kotlin:kotlin-reflect"){{^reactive}} + implementation("org.springframework.boot:spring-boot-starter-web"){{/reactive}}{{#reactive}} + implementation("org.springframework.boot:spring-boot-starter-webflux") + implementation("org.jetbrains.kotlinx:kotlinx-coroutines-core:$kotlinxCoroutinesVersion") + implementation("org.jetbrains.kotlinx:kotlinx-coroutines-reactor:$kotlinxCoroutinesVersion"){{/reactive}}{{#springDocDocumentationProvider}}{{#useSwaggerUI}} + implementation("org.springdoc:springdoc-openapi-starter-{{#reactive}} + webflux{{/reactive}}{{^reactive}}webmvc{{/reactive}}-ui:2.0.0-M5"){{/useSwaggerUI}}{{^useSwaggerUI}} + implementation("org.springdoc:springdoc-openapi-{{#reactive}} + webflux{{/reactive}}{{^reactive}}webmvc{{/reactive}} + -core:2.0.0-M5"){{/useSwaggerUI}}{{/springDocDocumentationProvider}}{{#springFoxDocumentationProvider}} + implementation("io.springfox:springfox-swagger2:2.9.2"){{/springFoxDocumentationProvider}}{{#useSwaggerUI}}{{^springDocDocumentationProvider}} + implementation("org.webjars:swagger-ui:4.10.3") + implementation("org.webjars:webjars-locator-core"){{/springDocDocumentationProvider}}{{/useSwaggerUI}}{{^springFoxDocumentationProvider}}{{^springDocDocumentationProvider}}{{#swagger1AnnotationLibrary}} + implementation("io.swagger:swagger-annotations:1.6.6"){{/swagger1AnnotationLibrary}}{{#swagger2AnnotationLibrary}} + implementation("io.swagger.core.v3:swagger-annotations:2.2.0"){{/swagger2AnnotationLibrary}}{{/springDocDocumentationProvider}}{{/springFoxDocumentationProvider}} + + implementation("com.google.code.findbugs:jsr305:3.0.2") + implementation("com.fasterxml.jackson.dataformat:jackson-dataformat-yaml") + implementation("com.fasterxml.jackson.dataformat:jackson-dataformat-xml") + implementation("com.fasterxml.jackson.datatype:jackson-datatype-jsr310") + implementation("com.fasterxml.jackson.module:jackson-module-kotlin") + + implementation("org.springframework.cloud:spring-cloud-starter-openfeign"){{#hasAuthMethods}} + implementation("org.springframework.cloud:spring-cloud-starter-oauth2:2.2.5.RELEASE"){{/hasAuthMethods}} + + {{#useBeanValidation}} + implementation("jakarta.validation:jakarta.validation-api"){{/useBeanValidation}} + implementation("jakarta.annotation:jakarta.annotation-api:2.1.0") + + } diff --git a/hideout-mastodon/templates/libraries/spring-cloud/buildGradleKts.mustache b/hideout-mastodon/templates/libraries/spring-cloud/buildGradleKts.mustache new file mode 100644 index 00000000..b944c04c --- /dev/null +++ b/hideout-mastodon/templates/libraries/spring-cloud/buildGradleKts.mustache @@ -0,0 +1,79 @@ +import org.jetbrains.kotlin.gradle.tasks.KotlinCompile + +buildscript { +repositories { +mavenCentral() +} +dependencies { +classpath("org.springframework.boot:spring-boot-gradle-plugin:2.6.7") +} +} + +group = "{{groupId}}" +version = "{{artifactVersion}}" + +repositories { +mavenCentral() +} + +tasks.withType + { + kotlinOptions.jvmTarget = "1.8" + } + + plugins { + val kotlinVersion = "1.6.21" + id("org.jetbrains.kotlin.jvm") version kotlinVersion + id("org.jetbrains.kotlin.plugin.jpa") version kotlinVersion + id("org.jetbrains.kotlin.plugin.spring") version kotlinVersion + id("org.springframework.boot") version "2.6.7" + id("io.spring.dependency-management") version "1.0.11.RELEASE" + } + + tasks.getByName("bootJar") { + enabled = false + } + + tasks.getByName("jar") { + enabled = true + } + + dependencyManagement { + imports { + mavenBom("org.springframework.cloud:spring-cloud-dependencies:2021.0.5") + } + } + + dependencies { + {{#reactive}} val kotlinxCoroutinesVersion = "1.6.1" + {{/reactive}} implementation("org.jetbrains.kotlin:kotlin-stdlib-jdk8") + implementation("org.jetbrains.kotlin:kotlin-reflect"){{^reactive}} + implementation("org.springframework.boot:spring-boot-starter-web"){{/reactive}}{{#reactive}} + implementation("org.springframework.boot:spring-boot-starter-webflux") + implementation("org.jetbrains.kotlinx:kotlinx-coroutines-core:$kotlinxCoroutinesVersion") + implementation("org.jetbrains.kotlinx:kotlinx-coroutines-reactor:$kotlinxCoroutinesVersion"){{/reactive}}{{#springDocDocumentationProvider}}{{#useSwaggerUI}} + implementation("org.springdoc:springdoc-openapi-{{#reactive}} + webflux-{{/reactive}}ui:1.6.8"){{/useSwaggerUI}}{{^useSwaggerUI}} + implementation("org.springdoc:springdoc-openapi-{{#reactive}} + webflux{{/reactive}}{{^reactive}}webmvc{{/reactive}} + -core:1.6.8"){{/useSwaggerUI}}{{/springDocDocumentationProvider}}{{#springFoxDocumentationProvider}} + implementation("io.springfox:springfox-swagger2:2.9.2"){{/springFoxDocumentationProvider}}{{#useSwaggerUI}}{{^springDocDocumentationProvider}} + implementation("org.webjars:swagger-ui:4.10.3") + implementation("org.webjars:webjars-locator-core"){{/springDocDocumentationProvider}}{{/useSwaggerUI}}{{^springFoxDocumentationProvider}}{{^springDocDocumentationProvider}}{{#swagger1AnnotationLibrary}} + implementation("io.swagger:swagger-annotations:1.6.6"){{/swagger1AnnotationLibrary}}{{#swagger2AnnotationLibrary}} + implementation("io.swagger.core.v3:swagger-annotations:2.2.0"){{/swagger2AnnotationLibrary}}{{/springDocDocumentationProvider}}{{/springFoxDocumentationProvider}} + + implementation("com.google.code.findbugs:jsr305:3.0.2") + implementation("com.fasterxml.jackson.dataformat:jackson-dataformat-yaml") + implementation("com.fasterxml.jackson.dataformat:jackson-dataformat-xml") + implementation("com.fasterxml.jackson.datatype:jackson-datatype-jsr310") + implementation("com.fasterxml.jackson.module:jackson-module-kotlin") + + implementation("org.springframework.cloud:spring-cloud-starter-openfeign"){{#hasAuthMethods}} + implementation("org.springframework.cloud:spring-cloud-starter-oauth2:2.2.5.RELEASE"){{/hasAuthMethods}} + + {{#useBeanValidation}} + implementation("jakarta.validation:jakarta.validation-api"){{/useBeanValidation}} + implementation("jakarta.annotation:jakarta.annotation-api:2.1.0") + + } diff --git a/hideout-mastodon/templates/libraries/spring-cloud/clientConfiguration.mustache b/hideout-mastodon/templates/libraries/spring-cloud/clientConfiguration.mustache new file mode 100644 index 00000000..4b9615c4 --- /dev/null +++ b/hideout-mastodon/templates/libraries/spring-cloud/clientConfiguration.mustache @@ -0,0 +1,132 @@ +package {{configPackage}} + +{{#authMethods}} + {{#isBasicBasic}} + import feign.auth.BasicAuthRequestInterceptor + {{/isBasicBasic}} + {{#-first}} + import org.springframework.beans.factory.annotation.Value + import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty + {{/-first}} + {{#isOAuth}} + import org.springframework.boot.context.properties.ConfigurationProperties + {{/isOAuth}} +{{/authMethods}} +import org.springframework.boot.context.properties.EnableConfigurationProperties +{{#authMethods}} + {{#-first}} + import org.springframework.context.annotation.Bean + {{/-first}} +{{/authMethods}} +import org.springframework.context.annotation.Configuration +{{#authMethods}} + {{#isOAuth}} + import org.springframework.cloud.openfeign.security.OAuth2FeignRequestInterceptor + import org.springframework.security.oauth2.client.DefaultOAuth2ClientContext + import org.springframework.security.oauth2.client.OAuth2ClientContext + {{#isApplication}} + import org.springframework.security.oauth2.client.token.grant.client.ClientCredentialsResourceDetails + {{/isApplication}} + {{#isCode}} + import org.springframework.security.oauth2.client.token.grant.code.AuthorizationCodeResourceDetails + {{/isCode}} + {{#isImplicit}} + import org.springframework.security.oauth2.client.token.grant.implicit.ImplicitResourceDetails + {{/isImplicit}} + {{#isPassword}} + import org.springframework.security.oauth2.client.token.grant.password.ResourceOwnerPasswordResourceDetails + {{/isPassword}} + {{/isOAuth}} +{{/authMethods}} + +@Configuration +@EnableConfigurationProperties +class ClientConfiguration { + +{{#authMethods}} + {{#isBasicBasic}} + @Value("\${{openbrace}}{{#lambda.lowercase}}{{{title}}}{{/lambda.lowercase}}.security.{{{name}}}.username:{{closebrace}}") + private lateinit var {{#lambda.camelcase}}{{{name}}}{{/lambda.camelcase}}Username: String + + @Value("\${{openbrace}}{{#lambda.lowercase}}{{{title}}}{{/lambda.lowercase}}.security.{{{name}}}.password:{{closebrace}}") + private lateinit var {{#lambda.camelcase}}{{{name}}}{{/lambda.camelcase}}Password: String + + @Bean + @ConditionalOnProperty("{{#lambda.lowercase}}{{{title}}}{{/lambda.lowercase}}.security.{{{name}}}.username") + fun {{{name}}}RequestInterceptor(): BasicAuthRequestInterceptor { + return BasicAuthRequestInterceptor(this.{{#lambda.camelcase}}{{{name}}}{{/lambda.camelcase}}Username, this.{{#lambda.camelcase}}{{{name}}}{{/lambda.camelcase}}Password) + } + + {{/isBasicBasic}} + {{#isApiKey}} + @Value("\${{openbrace}}{{#lambda.lowercase}}{{{title}}}{{/lambda.lowercase}}.security.{{{name}}}.key:{{closebrace}}") + private lateinit var {{#lambda.camelcase}}{{{name}}}{{/lambda.camelcase}}Key: String + + @Bean + @ConditionalOnProperty("{{#lambda.lowercase}}{{{title}}}{{/lambda.lowercase}}.security.{{{name}}}.key") + fun {{#lambda.camelcase}}{{{name}}}{{/lambda.camelcase}}RequestInterceptor(): ApiKeyRequestInterceptor { + return ApiKeyRequestInterceptor({{#isKeyInHeader}}"header"{{/isKeyInHeader}}{{^isKeyInHeader}}"query"{{/isKeyInHeader}}, "{{{keyParamName}}}", this.{{#lambda.camelcase}}{{{name}}}{{/lambda.camelcase}}Key) + } + + {{/isApiKey}} + {{#isOAuth}} + @Bean + @ConditionalOnProperty("{{#lambda.lowercase}}{{{title}}}{{/lambda.lowercase}}.security.{{{name}}}.client-id") + fun {{#lambda.camelcase}}{{{name}}}{{/lambda.camelcase}}RequestInterceptor(oAuth2ClientContext: OAuth2ClientContext): OAuth2FeignRequestInterceptor { + return OAuth2FeignRequestInterceptor(oAuth2ClientContext, {{#lambda.camelcase}}{{{name}}}{{/lambda.camelcase}}ResourceDetails()) + } + + @Bean + @ConditionalOnProperty("{{#lambda.lowercase}}{{{title}}}{{/lambda.lowercase}}.security.{{{name}}}.client-id") + fun oAuth2ClientContext(): OAuth2ClientContext { + return DefaultOAuth2ClientContext() + } + + {{#isCode}} + @Bean + @ConditionalOnProperty("{{#lambda.lowercase}}{{{title}}}{{/lambda.lowercase}}.security.{{{name}}}.client-id") + @ConfigurationProperties("{{#lambda.lowercase}}{{{title}}}{{/lambda.lowercase}}.security.{{{name}}}") + fun {{#lambda.camelcase}}{{{name}}}{{/lambda.camelcase}}ResourceDetails(): AuthorizationCodeResourceDetails { + val details = AuthorizationCodeResourceDetails() + details.accessTokenUri = "{{{tokenUrl}}}" + details.userAuthorizationUri = "{{{authorizationUrl}}}" + return details + } + + {{/isCode}} + {{#isPassword}} + @Bean + @ConditionalOnProperty("{{#lambda.lowercase}}{{{title}}}{{/lambda.lowercase}}.security.{{{name}}}.client-id") + @ConfigurationProperties("{{#lambda.lowercase}}{{{title}}}{{/lambda.lowercase}}.security.{{{name}}}") + fun {{#lambda.camelcase}}{{{name}}}{{/lambda.camelcase}}ResourceDetails(): ResourceOwnerPasswordResourceDetails { + val details = ResourceOwnerPasswordResourceDetails() + details.accessTokenUri = "{{{tokenUrl}}}" + return details + } + + {{/isPassword}} + {{#isApplication}} + @Bean + @ConditionalOnProperty("{{#lambda.lowercase}}{{{title}}}{{/lambda.lowercase}}.security.{{{name}}}.client-id") + @ConfigurationProperties("{{#lambda.lowercase}}{{{title}}}{{/lambda.lowercase}}.security.{{{name}}}") + fun {{#lambda.camelcase}}{{{name}}}{{/lambda.camelcase}}ResourceDetails(): ClientCredentialsResourceDetails { + val details = ClientCredentialsResourceDetails() + details.accessTokenUri = "{{{tokenUrl}}}" + return details + } + + {{/isApplication}} + {{#isImplicit}} + @Bean + @ConditionalOnProperty("{{#lambda.lowercase}}{{{title}}}{{/lambda.lowercase}}.security.{{{name}}}.client-id") + @ConfigurationProperties("{{#lambda.lowercase}}{{{title}}}{{/lambda.lowercase}}.security.{{{name}}}") + fun {{#lambda.camelcase}}{{{name}}}{{/lambda.camelcase}}ResourceDetails(): ImplicitResourceDetails { + val details = ImplicitResourceDetails() + details.userAuthorizationUri= "{{{authorizationUrl}}}" + return details + } + + {{/isImplicit}} + {{/isOAuth}} +{{/authMethods}} +} diff --git a/hideout-mastodon/templates/libraries/spring-cloud/pom-sb3.mustache b/hideout-mastodon/templates/libraries/spring-cloud/pom-sb3.mustache new file mode 100644 index 00000000..4dfd1558 --- /dev/null +++ b/hideout-mastodon/templates/libraries/spring-cloud/pom-sb3.mustache @@ -0,0 +1,233 @@ + + 4.0.0 + {{groupId}} + {{artifactId}} + jar + {{artifactId}} + {{artifactVersion}} + {{#reactive}} + 1.6.1 + {{/reactive}}{{#springDocDocumentationProvider}}{{#useSwaggerUI}} + 2.0.2 + {{/useSwaggerUI}}{{/springDocDocumentationProvider}}{{#springFoxDocumentationProvider}} + 2.9.2 + {{/springFoxDocumentationProvider}}{{#useSwaggerUI}}{{^springDocDocumentationProvider}} + 4.15.5 + {{/springDocDocumentationProvider}}{{/useSwaggerUI}}{{^springFoxDocumentationProvider}}{{^springDocDocumentationProvider}}{{#swagger1AnnotationLibrary}} + 1.6.6 + {{/swagger1AnnotationLibrary}}{{#swagger2AnnotationLibrary}} + 2.2.7 + {{/swagger2AnnotationLibrary}}{{/springDocDocumentationProvider}}{{/springFoxDocumentationProvider}} + 3.0.2 + 2.1.0 + 1.7.10 + + 1.7.10 + UTF-8 + + + org.springframework.boot + spring-boot-starter-parent + 3.0.2 + + + + + org.springframework.cloud + spring-cloud-starter-parent + 2021.0.5 + pom + import + + + + + + repository.spring.milestone + Spring Milestone Repository + https://repo.spring.io/milestone + + + + + spring-milestones + https://repo.spring.io/milestone + + + + ${project.basedir}/src/main/kotlin + {{^interfaceOnly}} + + org.springframework.boot + spring-boot-maven-plugin + + + + repackage + + + + {{/interfaceOnly}} + + kotlin-maven-plugin + org.jetbrains.kotlin + ${kotlin.version} + + + spring + + 17 + + + + compile + compile + + compile + + + + test-compile + test-compile + + test-compile + + + + + + org.jetbrains.kotlin + kotlin-maven-allopen + ${kotlin.version} + + + + + + + + org.jetbrains.kotlin + kotlin-stdlib-jdk8 + ${kotlin.version} + + + org.jetbrains.kotlin + kotlin-reflect + ${kotlin.version} + {{^reactive}} + + org.springframework.boot + spring-boot-starter-web + {{/reactive}}{{#reactive}} + + org.springframework.boot + spring-boot-starter-webflux + + + org.jetbrains.kotlinx + kotlinx-coroutines-core + ${kotlinx-coroutines.version} + + + org.jetbrains.kotlinx + kotlinx-coroutines-reactor + ${kotlinx-coroutines.version} + {{/reactive}} + + {{#springDocDocumentationProvider}} + {{#useSwaggerUI}} + + org.springdoc + springdoc-openapi-starter-{{#reactive}} + webflux{{/reactive}}{{^reactive}}webmvc{{/reactive}}-ui + + ${springdoc-openapi.version} + {{/useSwaggerUI}}{{^useSwaggerUI}} + + org.springdoc + springdoc-openapi-{{#reactive}}webflux{{/reactive}}{{^reactive}}webmvc{{/reactive}}-core + + ${springdoc-openapi.version} + {{/useSwaggerUI}}{{/springDocDocumentationProvider}}{{#springFoxDocumentationProvider}} + + + io.springfox + springfox-swagger2 + ${springfox-swagger2.version} + {{/springFoxDocumentationProvider}}{{#useSwaggerUI}}{{^springDocDocumentationProvider}} + + org.webjars + swagger-ui + ${swagger-ui.version} + + + org.webjars + webjars-locator-core + {{/springDocDocumentationProvider}}{{/useSwaggerUI}}{{^springFoxDocumentationProvider}}{{^springDocDocumentationProvider}}{{#swagger1AnnotationLibrary}} + + io.swagger + swagger-annotations + ${swagger-annotations.version} + {{/swagger1AnnotationLibrary}}{{#swagger2AnnotationLibrary}} + + io.swagger.core.v3 + swagger-annotations + ${swagger-annotations.version} + {{/swagger2AnnotationLibrary}}{{/springDocDocumentationProvider}}{{/springFoxDocumentationProvider}} + + + + com.google.code.findbugs + jsr305 + ${findbugs-jsr305.version} + + + org.springframework.cloud + spring-cloud-starter-openfeign + + {{#hasAuthMethods}} + + org.springframework.cloud + spring-cloud-starter-oauth2 + {{^parentOverridden}} + 2.2.5.RELEASE + {{/parentOverridden}} + + {{/hasAuthMethods}} + + com.fasterxml.jackson.dataformat + jackson-dataformat-yaml + + + com.fasterxml.jackson.dataformat + jackson-dataformat-xml + + + com.fasterxml.jackson.datatype + jackson-datatype-jsr310 + + + com.fasterxml.jackson.module + jackson-module-kotlin + + {{#useBeanValidation}} + + + jakarta.validation + jakarta.validation-api + {{/useBeanValidation}} + + jakarta.annotation + jakarta.annotation-api + ${jakarta-annotation.version} + provided + + + org.jetbrains.kotlin + kotlin-test-junit5 + ${kotlin-test-junit5.version} + test + + + diff --git a/hideout-mastodon/templates/libraries/spring-cloud/pom.mustache b/hideout-mastodon/templates/libraries/spring-cloud/pom.mustache new file mode 100644 index 00000000..867c9cf2 --- /dev/null +++ b/hideout-mastodon/templates/libraries/spring-cloud/pom.mustache @@ -0,0 +1,218 @@ + + 4.0.0 + {{groupId}} + {{artifactId}} + jar + {{artifactId}} + {{artifactVersion}} + {{#reactive}} + 1.6.1 + {{/reactive}}{{#springDocDocumentationProvider}}{{#useSwaggerUI}} + 1.6.8 + {{/useSwaggerUI}}{{/springDocDocumentationProvider}}{{#springFoxDocumentationProvider}} + 2.9.2 + {{/springFoxDocumentationProvider}}{{#useSwaggerUI}}{{^springDocDocumentationProvider}} + 4.10.3 + {{/springDocDocumentationProvider}}{{/useSwaggerUI}}{{^springFoxDocumentationProvider}}{{^springDocDocumentationProvider}}{{#swagger1AnnotationLibrary}} + 1.6.6 + {{/swagger1AnnotationLibrary}}{{#swagger2AnnotationLibrary}} + 2.2.0 + {{/swagger2AnnotationLibrary}}{{/springDocDocumentationProvider}}{{/springFoxDocumentationProvider}} + 3.0.2 + 2.1.0 + 1.6.21 + + 1.6.21 + UTF-8 + + + org.springframework.boot + spring-boot-starter-parent + 2.6.7 + + + + + org.springframework.cloud + spring-cloud-starter-parent + 2021.0.5 + pom + import + + + + + ${project.basedir}/src/main/kotlin + {{^interfaceOnly}} + + org.springframework.boot + spring-boot-maven-plugin + + + + repackage + + + + {{/interfaceOnly}} + + kotlin-maven-plugin + org.jetbrains.kotlin + ${kotlin.version} + + + spring + + 1.8 + + + + compile + compile + + compile + + + + test-compile + test-compile + + test-compile + + + + + + org.jetbrains.kotlin + kotlin-maven-allopen + ${kotlin.version} + + + + + + + + org.jetbrains.kotlin + kotlin-stdlib-jdk8 + ${kotlin.version} + + + org.jetbrains.kotlin + kotlin-reflect + ${kotlin.version} + {{^reactive}} + + org.springframework.boot + spring-boot-starter-web + {{/reactive}}{{#reactive}} + + org.springframework.boot + spring-boot-starter-webflux + + + org.jetbrains.kotlinx + kotlinx-coroutines-core + ${kotlinx-coroutines.version} + + + org.jetbrains.kotlinx + kotlinx-coroutines-reactor + ${kotlinx-coroutines.version} + {{/reactive}} + + {{#springDocDocumentationProvider}} + {{#useSwaggerUI}} + + org.springdoc + springdoc-openapi-{{#reactive}}webflux-{{/reactive}}ui + ${springdoc-openapi.version} + {{/useSwaggerUI}}{{^useSwaggerUI}} + + org.springdoc + springdoc-openapi-{{#reactive}}webflux{{/reactive}}{{^reactive}}webmvc{{/reactive}}-core + + ${springdoc-openapi.version} + {{/useSwaggerUI}}{{/springDocDocumentationProvider}}{{#springFoxDocumentationProvider}} + + + io.springfox + springfox-swagger2 + ${springfox-swagger2.version} + {{/springFoxDocumentationProvider}}{{#useSwaggerUI}}{{^springDocDocumentationProvider}} + + org.webjars + swagger-ui + ${swagger-ui.version} + + + org.webjars + webjars-locator-core + {{/springDocDocumentationProvider}}{{/useSwaggerUI}}{{^springFoxDocumentationProvider}}{{^springDocDocumentationProvider}}{{#swagger1AnnotationLibrary}} + + io.swagger + swagger-annotations + ${swagger-annotations.version} + {{/swagger1AnnotationLibrary}}{{#swagger2AnnotationLibrary}} + + io.swagger.core.v3 + swagger-annotations + ${swagger-annotations.version} + {{/swagger2AnnotationLibrary}}{{/springDocDocumentationProvider}}{{/springFoxDocumentationProvider}} + + + + com.google.code.findbugs + jsr305 + ${findbugs-jsr305.version} + + + org.springframework.cloud + spring-cloud-starter-openfeign + + {{#hasAuthMethods}} + + org.springframework.cloud + spring-cloud-starter-oauth2 + {{^parentOverridden}} + 2.2.5.RELEASE + {{/parentOverridden}} + + {{/hasAuthMethods}} + + com.fasterxml.jackson.dataformat + jackson-dataformat-yaml + + + com.fasterxml.jackson.dataformat + jackson-dataformat-xml + + + com.fasterxml.jackson.datatype + jackson-datatype-jsr310 + + + com.fasterxml.jackson.module + jackson-module-kotlin + + {{#useBeanValidation}} + + + jakarta.validation + jakarta.validation-api + {{/useBeanValidation}} + + jakarta.annotation + jakarta.annotation-api + ${jakarta-annotation.version} + provided + + + org.jetbrains.kotlin + kotlin-test-junit5 + ${kotlin-test-junit5.version} + test + + + diff --git a/hideout-mastodon/templates/libraries/spring-cloud/settingsGradle.mustache b/hideout-mastodon/templates/libraries/spring-cloud/settingsGradle.mustache new file mode 100644 index 00000000..3176ec97 --- /dev/null +++ b/hideout-mastodon/templates/libraries/spring-cloud/settingsGradle.mustache @@ -0,0 +1,15 @@ +pluginManagement { +repositories { +maven { url = uri("https://repo.spring.io/snapshot") } +maven { url = uri("https://repo.spring.io/milestone") } +gradlePluginPortal() +} +resolutionStrategy { +eachPlugin { +if (requested.id.id == "org.springframework.boot") { +useModule("org.springframework.boot:spring-boot-gradle-plugin:${requested.version}") +} +} +} +} +rootProject.name = "{{artifactId}}" diff --git a/hideout-mastodon/templates/methodBody.mustache b/hideout-mastodon/templates/methodBody.mustache new file mode 100644 index 00000000..f7e69bcd --- /dev/null +++ b/hideout-mastodon/templates/methodBody.mustache @@ -0,0 +1,28 @@ +{{^reactive}} + {{#examples}} + {{#-first}} + {{#async}} + return CompletableFuture.supplyAsync(()-> { + {{/async}}getRequest().ifPresent { request -> + {{#async}} {{/async}} for (mediaType in MediaType.parseMediaTypes(request.getHeader("Accept"))) { + {{/-first}} + {{#async}} {{/async}}{{^async}} {{/async}} if (mediaType.isCompatibleWith(MediaType.valueOf("{{{contentType}}}"))) { + {{#async}} {{/async}}{{^async}} {{/async}} ApiUtil.setExampleResponse(request, "{{{contentType}}}", "{{#lambdaRemoveLineBreak}}{{#lambdaEscapeDoubleQuote}}{{{example}}}{{/lambdaEscapeDoubleQuote}}{{/lambdaRemoveLineBreak}}") + {{#async}} {{/async}}{{^async}} {{/async}} break + {{#async}} {{/async}}{{^async}} {{/async}} } + {{#-last}} + {{#async}} {{/async}}{{^async}} {{/async}} } + {{#async}} {{/async}} } + {{#async}} {{/async}} return ResponseEntity({{#returnSuccessCode}}HttpStatus.valueOf({{{statusCode}}}){{/returnSuccessCode}}{{^returnSuccessCode}}HttpStatus.NOT_IMPLEMENTED{{/returnSuccessCode}}) + {{#async}} + }, Runnable::run) + {{/async}} + {{/-last}} + {{/examples}} + {{^examples}} + return {{#async}}CompletableFuture.completedFuture({{/async}}ResponseEntity({{#returnSuccessCode}}HttpStatus.OK{{/returnSuccessCode}}{{^returnSuccessCode}}HttpStatus.NOT_IMPLEMENTED{{/returnSuccessCode}}) + {{/examples}} +{{/reactive}} +{{#reactive}} + return ResponseEntity({{#returnSuccessCode}}HttpStatus.OK{{/returnSuccessCode}}{{^returnSuccessCode}}HttpStatus.NOT_IMPLEMENTED{{/returnSuccessCode}}) +{{/reactive}} diff --git a/hideout-mastodon/templates/model.mustache b/hideout-mastodon/templates/model.mustache new file mode 100644 index 00000000..d592ec82 --- /dev/null +++ b/hideout-mastodon/templates/model.mustache @@ -0,0 +1,28 @@ +package {{package}} + +import java.util.Objects +{{#imports}}import {{import}} +{{/imports}} +{{#useBeanValidation}} + import {{javaxPackage}}.validation.constraints.DecimalMax + import {{javaxPackage}}.validation.constraints.DecimalMin + import {{javaxPackage}}.validation.constraints.Email + import {{javaxPackage}}.validation.constraints.Max + import {{javaxPackage}}.validation.constraints.Min + import {{javaxPackage}}.validation.constraints.NotNull + import {{javaxPackage}}.validation.constraints.Pattern + import {{javaxPackage}}.validation.constraints.Size + import {{javaxPackage}}.validation.Valid +{{/useBeanValidation}} +{{#swagger2AnnotationLibrary}} + import io.swagger.v3.oas.annotations.media.Schema +{{/swagger2AnnotationLibrary}} +{{#swagger1AnnotationLibrary}} + import io.swagger.annotations.ApiModelProperty +{{/swagger1AnnotationLibrary}} +import java.beans.ConstructorProperties +{{#models}} + {{#model}} + {{#isEnum}}{{>enumClass}}{{/isEnum}}{{^isEnum}}{{>dataClass}}{{/isEnum}} + {{/model}} +{{/models}} diff --git a/hideout-mastodon/templates/modelMutable.mustache b/hideout-mastodon/templates/modelMutable.mustache new file mode 100644 index 00000000..4c7f3900 --- /dev/null +++ b/hideout-mastodon/templates/modelMutable.mustache @@ -0,0 +1 @@ +{{#modelMutable}}var{{/modelMutable}}{{^modelMutable}}val{{/modelMutable}} \ No newline at end of file diff --git a/hideout-mastodon/templates/openapi.mustache b/hideout-mastodon/templates/openapi.mustache new file mode 100644 index 00000000..51ebafb0 --- /dev/null +++ b/hideout-mastodon/templates/openapi.mustache @@ -0,0 +1 @@ +{{{openapi-yaml}}} \ No newline at end of file diff --git a/hideout-mastodon/templates/optionalDataType.mustache b/hideout-mastodon/templates/optionalDataType.mustache new file mode 100644 index 00000000..7c026fa8 --- /dev/null +++ b/hideout-mastodon/templates/optionalDataType.mustache @@ -0,0 +1 @@ +{{#required}}{{{dataType}}}{{/required}}{{^required}}{{#defaultValue}}{{{dataType}}}{{/defaultValue}}{{^defaultValue}}{{{dataType}}}?{{/defaultValue}}{{/required}} \ No newline at end of file diff --git a/hideout-mastodon/templates/pathParams.mustache b/hideout-mastodon/templates/pathParams.mustache new file mode 100644 index 00000000..957ca220 --- /dev/null +++ b/hideout-mastodon/templates/pathParams.mustache @@ -0,0 +1 @@ +{{#isPathParam}}{{#useBeanValidation}}{{>beanValidationPathParams}}{{/useBeanValidation}}{{#swagger2AnnotationLibrary}}@Parameter(description = "{{{description}}}"{{#required}}, required = true{{/required}}{{#allowableValues}}{{#defaultValue}}, schema = Schema(allowableValues = [{{#enumVars}}"{{#lambdaEscapeDoubleQuote}}{{{value}}}{{/lambdaEscapeDoubleQuote}}"{{^-last}}, {{/-last}}{{/enumVars}}]{{^isContainer}}, defaultValue = {{^isString}}"{{/isString}}{{#isString}}{{#isEnum}}"{{/isEnum}}{{/isString}}{{{defaultValue}}}{{^isString}}"{{/isString}}{{#isString}}{{#isEnum}}"{{/isEnum}}{{/isString}}{{/isContainer}}{{/defaultValue}}{{/allowableValues}}{{#allowableValues}}{{^defaultValue}}, schema = Schema(allowableValues = [{{#enumVars}}"{{#lambdaEscapeDoubleQuote}}{{{value}}}{{/lambdaEscapeDoubleQuote}}"{{^-last}}, {{/-last}}{{/enumVars}}]){{/defaultValue}}{{/allowableValues}}{{^allowableValues}}{{#defaultValue}}{{^isContainer}}, schema = Schema(defaultValue = {{^isString}}"{{/isString}}{{#isString}}{{#isEnum}}"{{/isEnum}}{{/isString}}{{{defaultValue}}}{{^isString}}"{{/isString}}{{#isString}}{{#isEnum}}"{{/isEnum}}{{/isString}}{{/isContainer}}{{/defaultValue}}{{/allowableValues}}){{/swagger2AnnotationLibrary}}{{#swagger1AnnotationLibrary}}@ApiParam(value = "{{{description}}}"{{#required}}, required = true{{/required}}{{#allowableValues}}, allowableValues = "{{#enumVars}}{{#lambda.escapeDoubleQuote}}{{{value}}}{{/lambda.escapeDoubleQuote}}{{^-last}}, {{/-last}}{{/enumVars}}"{{/allowableValues}}{{#defaultValue}}, defaultValue = "{{{.}}}"{{/defaultValue}}){{/swagger1AnnotationLibrary}} @PathVariable("{{baseName}}") {{paramName}}: {{>optionalDataType}}{{/isPathParam}} \ No newline at end of file diff --git a/hideout-mastodon/templates/queryParams.mustache b/hideout-mastodon/templates/queryParams.mustache new file mode 100644 index 00000000..3c254c53 --- /dev/null +++ b/hideout-mastodon/templates/queryParams.mustache @@ -0,0 +1 @@ +{{#isQueryParam}}{{#useBeanValidation}}{{>beanValidationQueryParams}}{{/useBeanValidation}}{{#swagger2AnnotationLibrary}}@Parameter(description = "{{{description}}}"{{#required}}, required = true{{/required}}{{#allowableValues}}{{#defaultValue}}, schema = Schema(allowableValues = [{{#values}}"{{{.}}}"{{^-last}}, {{/-last}}{{/values}}]{{^isContainer}}, defaultValue = {{^isString}}"{{/isString}}{{#isString}}{{#isEnum}}"{{/isEnum}}{{/isString}}{{{defaultValue}}}{{^isString}}"{{/isString}}{{#isString}}{{#isEnum}}"{{/isEnum}}{{/isString}}{{/isContainer}}){{/defaultValue}}{{/allowableValues}}{{#allowableValues}}{{^defaultValue}}, schema = Schema(allowableValues = [{{#values}}"{{{.}}}"{{^-last}}, {{/-last}}{{/values}}]){{/defaultValue}}{{/allowableValues}}{{^allowableValues}}{{#defaultValue}}{{^isContainer}}, schema = Schema(defaultValue = {{^isString}}"{{/isString}}{{#isString}}{{#isEnum}}"{{/isEnum}}{{/isString}}{{{defaultValue}}}{{^isString}}"{{/isString}}{{#isString}}{{#isEnum}}"{{/isEnum}}{{/isString}}){{/isContainer}}{{/defaultValue}}{{/allowableValues}}){{/swagger2AnnotationLibrary}}{{#swagger1AnnotationLibrary}}@ApiParam(value = "{{{description}}}"{{#required}}, required = true{{/required}}{{#allowableValues}}, allowableValues = "{{#values}}{{{.}}}{{^-last}}, {{/-last}}{{/values}}"{{/allowableValues}}{{^isContainer}}{{#defaultValue}}, defaultValue = {{^isString}}"{{/isString}}{{#isString}}{{#isEnum}}"{{/isEnum}}{{/isString}}{{{defaultValue}}}{{^isString}}"{{/isString}}{{#isString}}{{#isEnum}}"{{/isEnum}}{{/isString}}{{/defaultValue}}{{/isContainer}}){{/swagger1AnnotationLibrary}}{{#useBeanValidation}} @Valid{{/useBeanValidation}}{{^isModel}} @RequestParam(value = "{{baseName}}"{{#required}}, required = true{{/required}}{{^required}}, required = false{{/required}}{{^isContainer}}{{#defaultValue}}, defaultValue = {{^isString}}"{{/isString}}{{#isString}}{{#isEnum}}"{{/isEnum}}{{/isString}}{{{defaultValue}}}{{^isString}}"{{/isString}}{{#isString}}{{#isEnum}}"{{/isEnum}}{{/isString}}{{/defaultValue}}{{/isContainer}}){{/isModel}}{{#isDate}} @org.springframework.format.annotation.DateTimeFormat(iso = org.springframework.format.annotation.DateTimeFormat.ISO.DATE){{/isDate}}{{#isDateTime}} @org.springframework.format.annotation.DateTimeFormat(iso = org.springframework.format.annotation.DateTimeFormat.ISO.DATE_TIME){{/isDateTime}} {{paramName}}: {{>optionalDataType}}{{/isQueryParam}} \ No newline at end of file diff --git a/hideout-mastodon/templates/returnTypes.mustache b/hideout-mastodon/templates/returnTypes.mustache new file mode 100644 index 00000000..eddd93e3 --- /dev/null +++ b/hideout-mastodon/templates/returnTypes.mustache @@ -0,0 +1,2 @@ +{{#isMap}}Map +{{/isMap}}{{#isArray}}{{#reactive}}Flow{{/reactive}}{{^reactive}}{{{returnContainer}}}{{/reactive}}<{{{returnType}}}>{{/isArray}}{{^returnContainer}}{{{returnType}}}{{/returnContainer}} diff --git a/hideout-mastodon/templates/returnValue.mustache b/hideout-mastodon/templates/returnValue.mustache new file mode 100644 index 00000000..8c7c1aa4 --- /dev/null +++ b/hideout-mastodon/templates/returnValue.mustache @@ -0,0 +1 @@ +{{#serviceInterface}}ResponseEntity(service.{{operationId}}({{#allParams}}{{{paramName}}}{{^-last}}, {{/-last}}{{/allParams}}), {{#responses}}{{#-first}}HttpStatus.valueOf({{code}}){{/-first}}{{/responses}}){{/serviceInterface}}{{^serviceInterface}}ResponseEntity(HttpStatus.NOT_IMPLEMENTED){{/serviceInterface}} \ No newline at end of file diff --git a/hideout-mastodon/templates/service.mustache b/hideout-mastodon/templates/service.mustache new file mode 100644 index 00000000..f212e407 --- /dev/null +++ b/hideout-mastodon/templates/service.mustache @@ -0,0 +1,36 @@ +package {{package}} + +{{#imports}}import {{import}} +{{/imports}} +{{#reactive}} + import kotlinx.coroutines.flow.Flow +{{/reactive}} + +{{#operations}} + interface {{classname}}Service { + {{#operation}} + + /** + * {{httpMethod}} {{{path}}}{{#summary}} : {{.}}{{/summary}} + {{#notes}} + * {{.}} + {{/notes}} + * + {{#allParams}} + * @param {{{paramName}}} {{description}}{{#required}} (required){{/required}}{{^required}} (optional{{#defaultValue}}, default to {{.}}{{/defaultValue}}){{/required}} + {{/allParams}} + * @return {{#responses}}{{message}} (status code {{code}}){{^-last}} + * or {{/-last}}{{/responses}} + {{#isDeprecated}} + * @deprecated + {{/isDeprecated}} + {{#externalDocs}} + * {{description}} + * @see {{summary}} Documentation + {{/externalDocs}} + * @see {{classname}}#{{operationId}} + */ + {{#reactive}}{{^isArray}}suspend {{/isArray}}{{/reactive}}fun {{operationId}}({{#allParams}}{{{paramName}}}: {{^isBodyParam}}{{>optionalDataType}}{{/isBodyParam}}{{#isBodyParam}}{{^reactive}}{{>optionalDataType}}{{/reactive}}{{#reactive}}{{^isArray}}{{>optionalDataType}}{{/isArray}}{{#isArray}}Flow<{{{baseType}}}>{{/isArray}}{{/reactive}}{{/isBodyParam}}{{^-last}}, {{/-last}}{{/allParams}}): {{>returnTypes}} + {{/operation}} + } +{{/operations}} diff --git a/hideout-mastodon/templates/serviceImpl.mustache b/hideout-mastodon/templates/serviceImpl.mustache new file mode 100644 index 00000000..7a707cb3 --- /dev/null +++ b/hideout-mastodon/templates/serviceImpl.mustache @@ -0,0 +1,19 @@ +package {{package}} + +{{#imports}}import {{import}} +{{/imports}} +{{#reactive}} + import kotlinx.coroutines.flow.Flow +{{/reactive}} +import org.springframework.stereotype.Service +@Service +{{#operations}} + class {{classname}}ServiceImpl : {{classname}}Service { + {{#operation}} + + override {{#reactive}}{{^isArray}}suspend {{/isArray}}{{/reactive}}fun {{operationId}}({{#allParams}}{{paramName}}: {{^isBodyParam}}{{>optionalDataType}}{{/isBodyParam}}{{#isBodyParam}}{{^reactive}}{{>optionalDataType}}{{/reactive}}{{#reactive}}{{^isArray}}{{>optionalDataType}}{{/isArray}}{{#isArray}}Flow<{{{baseType}}}>{{/isArray}}{{/reactive}}{{/isBodyParam}}{{^-last}}, {{/-last}}{{/allParams}}): {{>returnTypes}} { + TODO("Implement me") + } + {{/operation}} + } +{{/operations}} diff --git a/hideout-mastodon/templates/springdocDocumentationConfig.mustache b/hideout-mastodon/templates/springdocDocumentationConfig.mustache new file mode 100644 index 00000000..95c4799c --- /dev/null +++ b/hideout-mastodon/templates/springdocDocumentationConfig.mustache @@ -0,0 +1,53 @@ +package {{basePackage}} + +import org.springframework.context.annotation.Bean +import org.springframework.context.annotation.Configuration + +import io.swagger.v3.oas.models.OpenAPI +import io.swagger.v3.oas.models.info.Info +import io.swagger.v3.oas.models.info.Contact +import io.swagger.v3.oas.models.info.License +import io.swagger.v3.oas.models.Components +import io.swagger.v3.oas.models.security.SecurityScheme + +@Configuration +class SpringDocConfiguration { + +@Bean +fun apiInfo(): OpenAPI { +return OpenAPI() +.info( +Info(){{#appName}} + .title("{{appName}}"){{/appName}} +.description("{{{appDescription}}}"){{#termsOfService}} + .termsOfService("{{termsOfService}}"){{/termsOfService}}{{#openAPI}}{{#info}}{{#contact}} + .contact( + Contact(){{#infoName}} + .name("{{infoName}}"){{/infoName}}{{#infoUrl}} + .url("{{infoUrl}}"){{/infoUrl}}{{#infoEmail}} + .email("{{infoEmail}}"){{/infoEmail}} + ){{/contact}}{{#license}} + .license( + License() + {{#licenseInfo}}.name("{{licenseInfo}}") + {{/licenseInfo}}{{#licenseUrl}}.url("{{licenseUrl}}") + {{/licenseUrl}} + ){{/license}}{{/info}}{{/openAPI}} +.version("{{appVersion}}") +){{#hasAuthMethods}} + .components( + Components(){{#authMethods}} + .addSecuritySchemes("{{name}}", SecurityScheme(){{#isBasic}} + .type(SecurityScheme.Type.HTTP) + .scheme("{{scheme}}"){{#bearerFormat}} + .bearerFormat("{{bearerFormat}}"){{/bearerFormat}}{{/isBasic}}{{#isApiKey}} + .type(SecurityScheme.Type.APIKEY){{#isKeyInHeader}} + .`in`(SecurityScheme.In.HEADER){{/isKeyInHeader}}{{#isKeyInQuery}} + .`in`(SecurityScheme.In.QUERY){{/isKeyInQuery}}{{#isKeyInCookie}} + .`in`(SecurityScheme.In.COOKIE){{/isKeyInCookie}} + .name("{{keyParamName}}"){{/isApiKey}}{{#isOAuth}} + .type(SecurityScheme.Type.OAUTH2){{/isOAuth}} + ){{/authMethods}} + ){{/hasAuthMethods}} +} +} diff --git a/hideout-mastodon/templates/springfoxDocumentationConfig.mustache b/hideout-mastodon/templates/springfoxDocumentationConfig.mustache new file mode 100644 index 00000000..ec08792e --- /dev/null +++ b/hideout-mastodon/templates/springfoxDocumentationConfig.mustache @@ -0,0 +1,66 @@ +package {{basePackage}} + +import org.springframework.beans.factory.annotation.Value +import org.springframework.context.annotation.Bean +import org.springframework.context.annotation.Configuration +import org.springframework.web.util.UriComponentsBuilder +import springfox.documentation.builders.ApiInfoBuilder +import springfox.documentation.builders.RequestHandlerSelectors +import springfox.documentation.service.ApiInfo +import springfox.documentation.service.Contact +import springfox.documentation.spi.DocumentationType +import springfox.documentation.spring.web.paths.Paths +import springfox.documentation.spring.web.paths.RelativePathProvider +import springfox.documentation.spring.web.plugins.Docket +import springfox.documentation.swagger2.annotations.EnableSwagger2 +import {{javaxPackage}}.servlet.ServletContext + + +{{>generatedAnnotation}} +@Configuration +@EnableSwagger2 +class SpringFoxConfiguration { + +fun apiInfo(): ApiInfo { +return ApiInfoBuilder() +.title("{{appName}}") +.description("{{{appDescription}}}") +.license("{{licenseInfo}}") +.licenseUrl("{{licenseUrl}}") +.termsOfServiceUrl("{{infoUrl}}") +.version("{{appVersion}}") +.contact(Contact("", "", "{{infoEmail}}")) +.build() +} + +@Bean +{{=<% %>=}} +fun customImplementation(servletContext: ServletContext, @Value("\${openapi.<%title%>.base-path:<%>defaultBasePath%>}") basePath: String): Docket { +<%={{ }}=%> +return Docket(DocumentationType.SWAGGER_2) +.select() +.apis(RequestHandlerSelectors.basePackage("{{apiPackage}}")) +.build() +.pathProvider(BasePathAwareRelativePathProvider(servletContext, basePath)) +.directModelSubstitute(java.time.LocalDate::class.java, java.sql.Date::class.java) +.directModelSubstitute(java.time.OffsetDateTime::class.java, java.util.Date::class.java) +.apiInfo(apiInfo()) +} + +class BasePathAwareRelativePathProvider(servletContext: ServletContext, private val basePath: String) : +RelativePathProvider(servletContext) { + +override fun applicationPath(): String { +return Paths.removeAdjacentForwardSlashes( +UriComponentsBuilder.fromPath(super.applicationPath()).path(basePath).build().toString() +) +} + +override fun getOperationPath(operationPath: String): String { +val uriComponentsBuilder = UriComponentsBuilder.fromPath("/") +return Paths.removeAdjacentForwardSlashes( +uriComponentsBuilder.path(operationPath.replaceFirst("^$basePath", "")).build().toString() +) +} +} +} diff --git a/hideout-mastodon/templates/typeInfoAnnotation.mustache b/hideout-mastodon/templates/typeInfoAnnotation.mustache new file mode 100644 index 00000000..acc68dc7 --- /dev/null +++ b/hideout-mastodon/templates/typeInfoAnnotation.mustache @@ -0,0 +1,8 @@ +{{#jackson}} + + @JsonTypeInfo(use = JsonTypeInfo.Id.NAME, include = JsonTypeInfo.As.PROPERTY, property = "{{{discriminator.propertyBaseName}}}", visible = true) + @JsonSubTypes( + {{#discriminator.mappedModels}} + JsonSubTypes.Type(value = {{modelName}}::class, name = "{{^vendorExtensions.x-discriminator-value}}{{mappingName}}{{/vendorExtensions.x-discriminator-value}}{{#vendorExtensions.x-discriminator-value}}{{{vendorExtensions.x-discriminator-value}}}{{/vendorExtensions.x-discriminator-value}}"){{^-last}},{{/-last}} + {{/discriminator.mappedModels}} + ){{/jackson}} diff --git a/libs.versions.toml b/libs.versions.toml new file mode 100644 index 00000000..51480e2e --- /dev/null +++ b/libs.versions.toml @@ -0,0 +1,114 @@ +[versions] + +kotlin = "2.0.10" +ktor = "2.3.12" +exposed = "0.53.0" +javacv-ffmpeg = "6.1.1-1.5.10" +detekt = "1.23.6" +coroutines = "1.8.1" +swagger = "2.2.22" +serialization = "1.7.1" +kjob = "0.6.0" +tika = "2.9.2" +owl = "0.0.1" +jackson = "2.17.2" + +[libraries] + +exposed-core = { module = "org.jetbrains.exposed:exposed-core", version.ref = "exposed" } +exposed-jdbc = { module = "org.jetbrains.exposed:exposed-jdbc", version.ref = "exposed" } +exposed-spring = { module = "org.jetbrains.exposed:exposed-spring-boot-starter", version.ref = "exposed" } +exposed-java-time = { module = "org.jetbrains.exposed:exposed-java-time", version.ref = "exposed" } + +cotoutines-core = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-core", version.ref = "coroutines" } +cotoutines-reactor = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-reactor", version.ref = "coroutines" } +cotoutines-slf4j = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-slf4j", version.ref = "coroutines" } +coroutines-test = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-test", version.ref = "coroutines" } + +javacv = { module = "org.bytedeco:javacv", version = "1.5.10" } +javacv-ffmpeg = { module = "org.bytedeco:ffmpeg", version.ref = "javacv-ffmpeg" } + +detekt-formatting = { module = "io.gitlab.arturbosch.detekt:detekt-formatting", version.ref = "detekt" } + +ktor-client-core = { module = "io.ktor:ktor-client-core", version.ref = "ktor" } +ktor-client-cio = { module = "io.ktor:ktor-client-cio", version.ref = "ktor" } +ktor-client-content-negotiation = { module = "io.ktor:ktor-client-content-negotiation", version.ref = "ktor" } +ktor-client-mock = { module = "io.ktor:ktor-client-mock", version.ref = "ktor" } +ktor-client-logging-jvm = { module = "io.ktor:ktor-client-logging-jvm", version.ref = "ktor" } +ktor-serialization-jackson = { module = "io.ktor:ktor-serialization-jackson", version.ref = "ktor" } + +spring-boot-oauth2-authorization = { module = "org.springframework.boot:spring-boot-starter-oauth2-authorization-server" } +spring-boot-oauth2-resource = { module = "org.springframework.boot:spring-boot-starter-oauth2-resource-server" } +spring-boot-oauth2-jose = { module = "org.springframework.security:spring-security-oauth2-jose" } + +spring-boot-data-mongodb = { module = "org.springframework.boot:spring-boot-starter-data-mongodb" } +spring-boot-data-mongodb-reactive = { module = "org.springframework.boot:spring-boot-starter-data-mongodb-reactive" } + +jakarta-validation = { module = "jakarta.validation:jakarta.validation-api", version = "3.1.0" } +jakarta-annotation = { module = "jakarta.annotation:jakarta.annotation-api", version = "3.0.0" } +swagger-annotations = { module = "io.swagger.core.v3:swagger-annotations", version.ref = "swagger" } +swagger-models = { module = "io.swagger.core.v3:swagger-models", version.ref = "swagger" } + +serialization-core = { module = "org.jetbrains.kotlinx:kotlinx-serialization-core", version.ref = "serialization" } +serialization-json = { module = "org.jetbrains.kotlinx:kotlinx-serialization-json", version.ref = "serialization" } + +apache-tika-core = { module = "org.apache.tika:tika-core", version.ref = "tika" } +apache-tika-parsers = { module = "org.apache.tika:tika-parsers", version.ref = "tika" } + +kjon-core = { module = "org.drewcarlson:kjob-core", version.ref = "kjob" } +kjon-mongo = { module = "org.drewcarlson:kjob-mongo", version.ref = "kjob" } + +owl-producer-api = { module = "dev.usbharu:owl-producer-api", version.ref = "owl" } +owl-producer-default = { module = "dev.usbharu:owl-producer-default", version.ref = "owl" } +owl-producer-embedded = { module = "dev.usbharu:owl-producer-embedded", version.ref = "owl" } +owl-broker = { module = "dev.usbharu:owl-broker", version.ref = "owl" } +owl-broker-mongodb = { module = "dev.usbharu:owl-broker-mongodb", version.ref = "owl" } + +jackson-databind = { module = "com.fasterxml.jackson.core:jackson-databind", version.ref = "jackson" } +jackson-module-kotlin = { module = "com.fasterxml.jackson.module:jackson-module-kotlin", version.ref = "jackson" } + +blurhash = { module = "io.trbl:blurhash", version = "1.0.0" } + +aws-s3 = { module = "software.amazon.awssdk:s3", version = "2.27.1" } + +jsoup = { module = "org.jsoup:jsoup", version = "1.18.1" } + +owasp-java-html-sanitizer = { module = "com.googlecode.owasp-java-html-sanitizer:owasp-java-html-sanitizer", version = "20240325.1" } + +postgresql = { module = "org.postgresql:postgresql", version = "42.7.3" } + +imageio-webp = { module = "com.twelvemonkeys.imageio:imageio-webp", version = "3.11.0" } + +thumbnailator = { module = "net.coobird:thumbnailator", version = "0.4.20" } + +flyway-core = { module = "org.flywaydb:flyway-core" } +flyway-postgresql = { module = "org.flywaydb:flyway-database-postgresql", version = "10.17.0" } + +h2db = { module = "com.h2database:h2", version = "2.3.230" } + +kotlin-junit = { module = "org.jetbrains.kotlin:kotlin-test-junit", version.ref = "kotlin" } + +[bundles] + +exposed = ["exposed-core", "exposed-java-time", "exposed-jdbc", "exposed-spring"] +coroutines = ["cotoutines-core", "cotoutines-reactor", "cotoutines-slf4j"] +ktor-client = ["ktor-client-cio", "ktor-client-content-negotiation", "ktor-client-core", "ktor-client-logging-jvm", "ktor-serialization-jackson"] +spring-boot-oauth2 = ["spring-boot-oauth2-authorization", "spring-boot-oauth2-jose", "spring-boot-oauth2-resource"] +spring-boot-data-mongodb = ["spring-boot-data-mongodb", "spring-boot-data-mongodb-reactive"] +openapi = ["jakarta-annotation", "jakarta-validation", "swagger-annotations", "swagger-models"] +serialization = ["serialization-core", "serialization-json"] +apache-tika = ["apache-tika-core", "apache-tika-parsers"] +kjob = ["kjon-core", "kjon-mongo"] +owl-producer = ["owl-producer-api", "owl-producer-default", "owl-producer-embedded"] +owl-broker = ["owl-broker", "owl-broker-mongodb"] +jackson = ["jackson-databind", "jackson-module-kotlin"] + +[plugins] + +kotlin-jvm = { id = "org.jetbrains.kotlin.jvm", version.ref = "kotlin" } +spring-boot = { id = "org.springframework.boot", version = "3.3.2" } +detekt = { id = "io.gitlab.arturbosch.detekt", version.ref = "detekt" } +kotlin-spring = { id = "org.jetbrains.kotlin.plugin.spring", version.ref = "kotlin" } +kover = { id = "org.jetbrains.kotlinx.kover", version = "0.8.3" } +openapi-generator = { id = "org.openapi.generator", version = "7.7.0" } +license-report = { id = "com.github.jk1.dependency-license-report", version = "2.8" } \ No newline at end of file diff --git a/license-list.xml b/license-list.xml new file mode 100644 index 00000000..985f9bd9 --- /dev/null +++ b/license-list.xml @@ -0,0 +1,126 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
ProjectVersionLicense
com.fasterxml.jackson:jackson-bom2.15.3Apache License, Version 2.0
io.ktor:ktor-client-cio2.3.8Apache License, Version 2.0
io.ktor:ktor-client-content-negotiation2.3.8Apache License, Version 2.0
io.ktor:ktor-client-core2.3.8Apache License, Version 2.0
io.ktor:ktor-events2.3.8Apache License, Version 2.0
io.ktor:ktor-http2.3.8Apache License, Version 2.0
io.ktor:ktor-http-cio2.3.8Apache License, Version 2.0
io.ktor:ktor-io2.3.8Apache License, Version 2.0
io.ktor:ktor-network2.3.8Apache License, Version 2.0
io.ktor:ktor-network-tls2.3.8Apache License, Version 2.0
io.ktor:ktor-serialization2.3.8Apache License, Version 2.0
io.ktor:ktor-serialization-jackson2.3.8Apache License, Version 2.0
io.ktor:ktor-utils2.3.8Apache License, Version 2.0
io.ktor:ktor-websocket-serialization2.3.8Apache License, Version 2.0
io.ktor:ktor-websockets2.3.8Apache License, Version 2.0
org.apache.tika:tika-parsers2.9.1Apache License, Version 2.0
org.jetbrains.kotlin:kotlin-stdlib-common1.9.22Apache License, Version 2.0
org.jetbrains.kotlinx:kotlinx-coroutines-bom1.7.3Apache License, Version 2.0
org.jetbrains.kotlinx:kotlinx-coroutines-core1.7.3Apache License, Version 2.0
org.jetbrains.kotlinx:kotlinx-serialization-bom1.6.2Apache License, Version 2.0
org.jetbrains.kotlinx:kotlinx-serialization-core1.6.2Apache License, Version 2.0
org.jetbrains.kotlinx:kotlinx-serialization-json1.6.2Apache License, Version 2.0
+
+
+
\ No newline at end of file diff --git a/owl/.gitignore b/owl/.gitignore new file mode 100644 index 00000000..bf3e1b20 --- /dev/null +++ b/owl/.gitignore @@ -0,0 +1,43 @@ +.gradle +build/ +!gradle/wrapper/gradle-wrapper.jar +!**/src/main/**/build/ +!**/src/test/**/build/ + +### IntelliJ IDEA ### +.idea/modules.xml +.idea/jarRepositories.xml +.idea/compiler.xml +.idea/libraries/ +*.iws +*.iml +*.ipr +out/ +!**/src/main/**/out/ +!**/src/test/**/out/ + +### Eclipse ### +.apt_generated +.classpath +.factorypath +.project +.settings +.springBeans +.sts4-cache +bin/ +!**/src/main/**/bin/ +!**/src/test/**/bin/ + +### NetBeans ### +/nbproject/private/ +/nbbuild/ +/dist/ +/nbdist/ +/.nb-gradle/ + +### VS Code ### +.vscode/ + +### Mac OS ### +.DS_Store +/.idea/ diff --git a/owl/.gitkeep b/owl/.gitkeep new file mode 100644 index 00000000..e69de29b diff --git a/owl/build.gradle.kts b/owl/build.gradle.kts new file mode 100644 index 00000000..1bc1d780 --- /dev/null +++ b/owl/build.gradle.kts @@ -0,0 +1,72 @@ +plugins { + alias(libs.plugins.kotlin.jvm) + id("maven-publish") +} + + +allprojects { + group = "dev.usbharu" + version = "0.0.1" + + + repositories { + mavenCentral() + } +} + +tasks { + create("publishMavenPublicationToMavenLocal") { + subprojects.forEach { dependsOn("${it.path}:publishMavenPublicationToMavenLocal") } + } + create("publishMavenPublicationToGiteaRepository") { + subprojects.forEach { dependsOn("${it.path}:publishMavenPublicationToGiteaRepository") } + } +} + +subprojects { + apply { + plugin("org.jetbrains.kotlin.jvm") + plugin("maven-publish") + } + kotlin { + jvmToolchain(21) + } + + dependencies { + implementation("org.slf4j:slf4j-api:2.0.15") + testImplementation("org.junit.jupiter:junit-jupiter:5.10.3") + + + } + + tasks.test { + useJUnitPlatform() + } + + publishing { + repositories { + maven { + name = "Gitea" + url = uri("https://git.usbharu.dev/api/packages/usbharu/maven") + + credentials(HttpHeaderCredentials::class) { + name = "Authorization" + value = "token " + (project.findProperty("gpr.gitea") as String? ?: System.getenv("GITEA")) + } + + authentication { + create("header") + } + } + } + + publications { + register("maven") { + groupId = "dev.usbharu" + artifactId = project.name + version = project.version.toString() + from(components["kotlin"]) + } + } + } +} \ No newline at end of file diff --git a/owl/gradle.properties b/owl/gradle.properties new file mode 100644 index 00000000..43cbcbb4 --- /dev/null +++ b/owl/gradle.properties @@ -0,0 +1,4 @@ +kotlin.code.style=official +org.gradle.daemon=true +org.gradle.parallel=true +org.gradle.configureondemand=true \ No newline at end of file diff --git a/owl/gradle/wrapper/gradle-wrapper.jar b/owl/gradle/wrapper/gradle-wrapper.jar new file mode 100644 index 00000000..2c352119 Binary files /dev/null and b/owl/gradle/wrapper/gradle-wrapper.jar differ diff --git a/owl/gradle/wrapper/gradle-wrapper.properties b/owl/gradle/wrapper/gradle-wrapper.properties new file mode 100644 index 00000000..09523c0e --- /dev/null +++ b/owl/gradle/wrapper/gradle-wrapper.properties @@ -0,0 +1,7 @@ +distributionBase=GRADLE_USER_HOME +distributionPath=wrapper/dists +distributionUrl=https\://services.gradle.org/distributions/gradle-8.9-bin.zip +networkTimeout=10000 +validateDistributionUrl=true +zipStoreBase=GRADLE_USER_HOME +zipStorePath=wrapper/dists diff --git a/owl/gradlew b/owl/gradlew new file mode 100755 index 00000000..f5feea6d --- /dev/null +++ b/owl/gradlew @@ -0,0 +1,252 @@ +#!/bin/sh + +# +# Copyright © 2015-2021 the original authors. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# https://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# +# SPDX-License-Identifier: Apache-2.0 +# + +############################################################################## +# +# Gradle start up script for POSIX generated by Gradle. +# +# Important for running: +# +# (1) You need a POSIX-compliant shell to run this script. If your /bin/sh is +# noncompliant, but you have some other compliant shell such as ksh or +# bash, then to run this script, type that shell name before the whole +# command line, like: +# +# ksh Gradle +# +# Busybox and similar reduced shells will NOT work, because this script +# requires all of these POSIX shell features: +# * functions; +# * expansions «$var», «${var}», «${var:-default}», «${var+SET}», +# «${var#prefix}», «${var%suffix}», and «$( cmd )»; +# * compound commands having a testable exit status, especially «case»; +# * various built-in commands including «command», «set», and «ulimit». +# +# Important for patching: +# +# (2) This script targets any POSIX shell, so it avoids extensions provided +# by Bash, Ksh, etc; in particular arrays are avoided. +# +# The "traditional" practice of packing multiple parameters into a +# space-separated string is a well documented source of bugs and security +# problems, so this is (mostly) avoided, by progressively accumulating +# options in "$@", and eventually passing that to Java. +# +# Where the inherited environment variables (DEFAULT_JVM_OPTS, JAVA_OPTS, +# and GRADLE_OPTS) rely on word-splitting, this is performed explicitly; +# see the in-line comments for details. +# +# There are tweaks for specific operating systems such as AIX, CygWin, +# Darwin, MinGW, and NonStop. +# +# (3) This script is generated from the Groovy template +# https://github.com/gradle/gradle/blob/HEAD/platforms/jvm/plugins-application/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt +# within the Gradle project. +# +# You can find Gradle at https://github.com/gradle/gradle/. +# +############################################################################## + +# Attempt to set APP_HOME + +# Resolve links: $0 may be a link +app_path=$0 + +# Need this for daisy-chained symlinks. +while + APP_HOME=${app_path%"${app_path##*/}"} # leaves a trailing /; empty if no leading path + [ -h "$app_path" ] +do + ls=$( ls -ld "$app_path" ) + link=${ls#*' -> '} + case $link in #( + /*) app_path=$link ;; #( + *) app_path=$APP_HOME$link ;; + esac +done + +# This is normally unused +# shellcheck disable=SC2034 +APP_BASE_NAME=${0##*/} +# Discard cd standard output in case $CDPATH is set (https://github.com/gradle/gradle/issues/25036) +APP_HOME=$( cd -P "${APP_HOME:-./}" > /dev/null && printf '%s +' "$PWD" ) || exit + +# Use the maximum available, or set MAX_FD != -1 to use that value. +MAX_FD=maximum + +warn () { + echo "$*" +} >&2 + +die () { + echo + echo "$*" + echo + exit 1 +} >&2 + +# OS specific support (must be 'true' or 'false'). +cygwin=false +msys=false +darwin=false +nonstop=false +case "$( uname )" in #( + CYGWIN* ) cygwin=true ;; #( + Darwin* ) darwin=true ;; #( + MSYS* | MINGW* ) msys=true ;; #( + NONSTOP* ) nonstop=true ;; +esac + +CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar + + +# Determine the Java command to use to start the JVM. +if [ -n "$JAVA_HOME" ] ; then + if [ -x "$JAVA_HOME/jre/sh/java" ] ; then + # IBM's JDK on AIX uses strange locations for the executables + JAVACMD=$JAVA_HOME/jre/sh/java + else + JAVACMD=$JAVA_HOME/bin/java + fi + if [ ! -x "$JAVACMD" ] ; then + die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." + fi +else + JAVACMD=java + if ! command -v java >/dev/null 2>&1 + then + die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." + fi +fi + +# Increase the maximum file descriptors if we can. +if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then + case $MAX_FD in #( + max*) + # In POSIX sh, ulimit -H is undefined. That's why the result is checked to see if it worked. + # shellcheck disable=SC2039,SC3045 + MAX_FD=$( ulimit -H -n ) || + warn "Could not query maximum file descriptor limit" + esac + case $MAX_FD in #( + '' | soft) :;; #( + *) + # In POSIX sh, ulimit -n is undefined. That's why the result is checked to see if it worked. + # shellcheck disable=SC2039,SC3045 + ulimit -n "$MAX_FD" || + warn "Could not set maximum file descriptor limit to $MAX_FD" + esac +fi + +# Collect all arguments for the java command, stacking in reverse order: +# * args from the command line +# * the main class name +# * -classpath +# * -D...appname settings +# * --module-path (only if needed) +# * DEFAULT_JVM_OPTS, JAVA_OPTS, and GRADLE_OPTS environment variables. + +# For Cygwin or MSYS, switch paths to Windows format before running java +if "$cygwin" || "$msys" ; then + APP_HOME=$( cygpath --path --mixed "$APP_HOME" ) + CLASSPATH=$( cygpath --path --mixed "$CLASSPATH" ) + + JAVACMD=$( cygpath --unix "$JAVACMD" ) + + # Now convert the arguments - kludge to limit ourselves to /bin/sh + for arg do + if + case $arg in #( + -*) false ;; # don't mess with options #( + /?*) t=${arg#/} t=/${t%%/*} # looks like a POSIX filepath + [ -e "$t" ] ;; #( + *) false ;; + esac + then + arg=$( cygpath --path --ignore --mixed "$arg" ) + fi + # Roll the args list around exactly as many times as the number of + # args, so each arg winds up back in the position where it started, but + # possibly modified. + # + # NB: a `for` loop captures its iteration list before it begins, so + # changing the positional parameters here affects neither the number of + # iterations, nor the values presented in `arg`. + shift # remove old arg + set -- "$@" "$arg" # push replacement arg + done +fi + + +# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"' + +# Collect all arguments for the java command: +# * DEFAULT_JVM_OPTS, JAVA_OPTS, JAVA_OPTS, and optsEnvironmentVar are not allowed to contain shell fragments, +# and any embedded shellness will be escaped. +# * For example: A user cannot expect ${Hostname} to be expanded, as it is an environment variable and will be +# treated as '${Hostname}' itself on the command line. + +set -- \ + "-Dorg.gradle.appname=$APP_BASE_NAME" \ + -classpath "$CLASSPATH" \ + org.gradle.wrapper.GradleWrapperMain \ + "$@" + +# Stop when "xargs" is not available. +if ! command -v xargs >/dev/null 2>&1 +then + die "xargs is not available" +fi + +# Use "xargs" to parse quoted args. +# +# With -n1 it outputs one arg per line, with the quotes and backslashes removed. +# +# In Bash we could simply go: +# +# readarray ARGS < <( xargs -n1 <<<"$var" ) && +# set -- "${ARGS[@]}" "$@" +# +# but POSIX shell has neither arrays nor command substitution, so instead we +# post-process each arg (as a line of input to sed) to backslash-escape any +# character that might be a shell metacharacter, then use eval to reverse +# that process (while maintaining the separation between arguments), and wrap +# the whole thing up as a single "set" statement. +# +# This will of course break if any of these variables contains a newline or +# an unmatched quote. +# + +eval "set -- $( + printf '%s\n' "$DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS" | + xargs -n1 | + sed ' s~[^-[:alnum:]+,./:=@_]~\\&~g; ' | + tr '\n' ' ' + )" '"$@"' + +exec "$JAVACMD" "$@" diff --git a/owl/gradlew.bat b/owl/gradlew.bat new file mode 100644 index 00000000..9b42019c --- /dev/null +++ b/owl/gradlew.bat @@ -0,0 +1,94 @@ +@rem +@rem Copyright 2015 the original author or authors. +@rem +@rem Licensed under the Apache License, Version 2.0 (the "License"); +@rem you may not use this file except in compliance with the License. +@rem You may obtain a copy of the License at +@rem +@rem https://www.apache.org/licenses/LICENSE-2.0 +@rem +@rem Unless required by applicable law or agreed to in writing, software +@rem distributed under the License is distributed on an "AS IS" BASIS, +@rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +@rem See the License for the specific language governing permissions and +@rem limitations under the License. +@rem +@rem SPDX-License-Identifier: Apache-2.0 +@rem + +@if "%DEBUG%"=="" @echo off +@rem ########################################################################## +@rem +@rem Gradle startup script for Windows +@rem +@rem ########################################################################## + +@rem Set local scope for the variables with windows NT shell +if "%OS%"=="Windows_NT" setlocal + +set DIRNAME=%~dp0 +if "%DIRNAME%"=="" set DIRNAME=. +@rem This is normally unused +set APP_BASE_NAME=%~n0 +set APP_HOME=%DIRNAME% + +@rem Resolve any "." and ".." in APP_HOME to make it shorter. +for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi + +@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m" + +@rem Find java.exe +if defined JAVA_HOME goto findJavaFromJavaHome + +set JAVA_EXE=java.exe +%JAVA_EXE% -version >NUL 2>&1 +if %ERRORLEVEL% equ 0 goto execute + +echo. 1>&2 +echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 1>&2 +echo. 1>&2 +echo Please set the JAVA_HOME variable in your environment to match the 1>&2 +echo location of your Java installation. 1>&2 + +goto fail + +:findJavaFromJavaHome +set JAVA_HOME=%JAVA_HOME:"=% +set JAVA_EXE=%JAVA_HOME%/bin/java.exe + +if exist "%JAVA_EXE%" goto execute + +echo. 1>&2 +echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 1>&2 +echo. 1>&2 +echo Please set the JAVA_HOME variable in your environment to match the 1>&2 +echo location of your Java installation. 1>&2 + +goto fail + +:execute +@rem Setup the command line + +set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar + + +@rem Execute Gradle +"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %* + +:end +@rem End local scope for the variables with windows NT shell +if %ERRORLEVEL% equ 0 goto mainEnd + +:fail +rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of +rem the _cmd.exe /c_ return code! +set EXIT_CODE=%ERRORLEVEL% +if %EXIT_CODE% equ 0 set EXIT_CODE=1 +if not ""=="%GRADLE_EXIT_CONSOLE%" exit %EXIT_CODE% +exit /b %EXIT_CODE% + +:mainEnd +if "%OS%"=="Windows_NT" endlocal + +:omega diff --git a/owl/owl-broker/build.gradle.kts b/owl/owl-broker/build.gradle.kts new file mode 100644 index 00000000..6161359b --- /dev/null +++ b/owl/owl-broker/build.gradle.kts @@ -0,0 +1,56 @@ +plugins { + alias(libs.plugins.kotlin.jvm) + id("com.google.protobuf") version "0.9.4" +} + + +group = "dev.usbharu" +version = "0.0.1" + +repositories { + mavenCentral() +} + +dependencies { + implementation("io.grpc:grpc-kotlin-stub:1.4.1") + implementation("io.grpc:grpc-protobuf:1.66.0") + implementation("com.google.protobuf:protobuf-kotlin:4.27.3") + implementation("io.grpc:grpc-netty:1.66.0") + implementation("org.jetbrains.kotlinx:kotlinx-coroutines-core:1.8.1") + implementation(project(":owl-common")) + implementation("org.apache.logging.log4j:log4j-slf4j2-impl:2.23.1") + implementation(platform("io.insert-koin:koin-bom:3.5.6")) + implementation("io.insert-koin:koin-core") +} + +tasks.test { + useJUnitPlatform() +} +kotlin { + jvmToolchain(21) +} + +protobuf { + protoc { + artifact = "com.google.protobuf:protoc:4.27.3" + } + plugins { + create("grpc") { + artifact = "io.grpc:protoc-gen-grpc-java:1.66.0" + } + create("grpckt") { + artifact = "io.grpc:protoc-gen-grpc-kotlin:1.4.1:jdk8@jar" + } + } + generateProtoTasks { + all().forEach { + it.plugins { + create("grpc") + create("grpckt") + } + it.builtins { + create("kotlin") + } + } + } +} \ No newline at end of file diff --git a/owl/owl-broker/owl-broker-mongodb/build.gradle.kts b/owl/owl-broker/owl-broker-mongodb/build.gradle.kts new file mode 100644 index 00000000..172af478 --- /dev/null +++ b/owl/owl-broker/owl-broker-mongodb/build.gradle.kts @@ -0,0 +1,31 @@ +plugins { + application + alias(libs.plugins.kotlin.jvm) +} + +group = "dev.usbharu" +version = "0.0.1" + +repositories { + mavenCentral() +} + +dependencies { + implementation("org.mongodb:mongodb-driver-kotlin-coroutine:5.1.3") + implementation(project(":owl-broker")) + implementation(project(":owl-common")) + implementation(platform("io.insert-koin:koin-bom:3.5.6")) + implementation(platform("io.insert-koin:koin-annotations-bom:1.3.1")) + implementation("io.insert-koin:koin-core") +} + +tasks.test { + useJUnitPlatform() +} +kotlin { + jvmToolchain(21) +} + +application { + mainClass = "dev.usbharu.owl.broker.MainKt" +} \ No newline at end of file diff --git a/owl/owl-broker/owl-broker-mongodb/src/main/kotlin/dev/usbharu/owl/broker/mongodb/MongoModuleContext.kt b/owl/owl-broker/owl-broker-mongodb/src/main/kotlin/dev/usbharu/owl/broker/mongodb/MongoModuleContext.kt new file mode 100644 index 00000000..5c1af4fb --- /dev/null +++ b/owl/owl-broker/owl-broker-mongodb/src/main/kotlin/dev/usbharu/owl/broker/mongodb/MongoModuleContext.kt @@ -0,0 +1,62 @@ +/* + * Copyright (C) 2024 usbharu + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package dev.usbharu.owl.broker.mongodb + +import com.mongodb.ConnectionString +import com.mongodb.MongoClientSettings +import com.mongodb.kotlin.client.coroutine.MongoClient +import dev.usbharu.owl.broker.ModuleContext +import dev.usbharu.owl.broker.domain.model.consumer.ConsumerRepository +import dev.usbharu.owl.broker.domain.model.producer.ProducerRepository +import dev.usbharu.owl.broker.domain.model.queuedtask.QueuedTaskRepository +import dev.usbharu.owl.broker.domain.model.task.TaskRepository +import dev.usbharu.owl.broker.domain.model.taskdefinition.TaskDefinitionRepository +import dev.usbharu.owl.broker.domain.model.taskresult.TaskResultRepository +import org.bson.UuidRepresentation +import org.koin.core.module.Module +import org.koin.dsl.module + +class MongoModuleContext : ModuleContext { + override fun module(): Module { + + return module { + single { + val clientSettings = + MongoClientSettings.builder() + .applyConnectionString( + ConnectionString( + System.getProperty( + "owl.broker.mongo.url", + "mongodb://localhost:27017" + ) + ) + ) + .uuidRepresentation(UuidRepresentation.STANDARD).build() + + + MongoClient.create(clientSettings) + .getDatabase(System.getProperty("owl.broker.mongo.database", "mongo-test")) + } + single { MongodbConsumerRepository(get()) } + single { MongodbProducerRepository(get()) } + single { MongodbQueuedTaskRepository(get(), get()) } + single { MongodbTaskDefinitionRepository(get()) } + single { MongodbTaskRepository(get(), get()) } + single { MongodbTaskResultRepository(get(), get()) } + } + } +} \ No newline at end of file diff --git a/owl/owl-broker/owl-broker-mongodb/src/main/kotlin/dev/usbharu/owl/broker/mongodb/MongodbConsumerRepository.kt b/owl/owl-broker/owl-broker-mongodb/src/main/kotlin/dev/usbharu/owl/broker/mongodb/MongodbConsumerRepository.kt new file mode 100644 index 00000000..4310b956 --- /dev/null +++ b/owl/owl-broker/owl-broker-mongodb/src/main/kotlin/dev/usbharu/owl/broker/mongodb/MongodbConsumerRepository.kt @@ -0,0 +1,69 @@ +/* + * Copyright (C) 2024 usbharu + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package dev.usbharu.owl.broker.mongodb + +import com.mongodb.client.model.Filters +import com.mongodb.client.model.ReplaceOptions +import com.mongodb.kotlin.client.coroutine.MongoDatabase +import dev.usbharu.owl.broker.domain.model.consumer.Consumer +import dev.usbharu.owl.broker.domain.model.consumer.ConsumerRepository +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.flow.singleOrNull +import kotlinx.coroutines.withContext +import org.bson.BsonType +import org.bson.codecs.pojo.annotations.BsonId +import org.bson.codecs.pojo.annotations.BsonRepresentation +import java.util.* + +class MongodbConsumerRepository(database: MongoDatabase) : ConsumerRepository { + + private val collection = database.getCollection("consumers") + override suspend fun save(consumer: Consumer): Consumer = withContext(Dispatchers.IO) { + collection.replaceOne(Filters.eq("_id", consumer.id.toString()), ConsumerMongodb.of(consumer), ReplaceOptions().upsert(true)) + return@withContext consumer + } + + override suspend fun findById(id: UUID): Consumer? = withContext(Dispatchers.IO) { + return@withContext collection.find(Filters.eq("_id", id.toString())).singleOrNull()?.toConsumer() + } +} + +data class ConsumerMongodb( + @BsonId + @BsonRepresentation(BsonType.STRING) + val id: String, + val name: String, + val hostname: String, + val tasks: List +){ + + fun toConsumer():Consumer{ + return Consumer( + UUID.fromString(id), name, hostname, tasks + ) + } + companion object{ + fun of(consumer: Consumer):ConsumerMongodb{ + return ConsumerMongodb( + consumer.id.toString(), + consumer.name, + consumer.hostname, + consumer.tasks + ) + } + } +} \ No newline at end of file diff --git a/owl/owl-broker/owl-broker-mongodb/src/main/kotlin/dev/usbharu/owl/broker/mongodb/MongodbProducerRepository.kt b/owl/owl-broker/owl-broker-mongodb/src/main/kotlin/dev/usbharu/owl/broker/mongodb/MongodbProducerRepository.kt new file mode 100644 index 00000000..76d9a755 --- /dev/null +++ b/owl/owl-broker/owl-broker-mongodb/src/main/kotlin/dev/usbharu/owl/broker/mongodb/MongodbProducerRepository.kt @@ -0,0 +1,71 @@ +/* + * Copyright (C) 2024 usbharu + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package dev.usbharu.owl.broker.mongodb + +import com.mongodb.client.model.Filters +import com.mongodb.client.model.ReplaceOptions +import com.mongodb.kotlin.client.coroutine.MongoDatabase +import dev.usbharu.owl.broker.domain.model.producer.Producer +import dev.usbharu.owl.broker.domain.model.producer.ProducerRepository +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.withContext +import java.time.Instant +import java.util.* + +class MongodbProducerRepository(database: MongoDatabase) : ProducerRepository { + + private val collection = database.getCollection("producers") + + override suspend fun save(producer: Producer): Producer = withContext(Dispatchers.IO) { + collection.replaceOne( + Filters.eq("_id", producer.id.toString()), + ProducerMongodb.of(producer), + ReplaceOptions().upsert(true) + ) + return@withContext producer + } +} + +data class ProducerMongodb( + val id: String, + val name: String, + val hostname: String, + val registeredTask: List, + val createdAt: Instant +) { + fun toProducer(): Producer { + return Producer( + id = UUID.fromString(id), + name = name, + hostname = hostname, + registeredTask = registeredTask, + createdAt = createdAt + ) + } + + companion object { + fun of(producer: Producer): ProducerMongodb { + return ProducerMongodb( + id = producer.id.toString(), + name = producer.name, + hostname = producer.hostname, + registeredTask = producer.registeredTask, + createdAt = producer.createdAt + ) + } + } +} \ No newline at end of file diff --git a/owl/owl-broker/owl-broker-mongodb/src/main/kotlin/dev/usbharu/owl/broker/mongodb/MongodbQueuedTaskRepository.kt b/owl/owl-broker/owl-broker-mongodb/src/main/kotlin/dev/usbharu/owl/broker/mongodb/MongodbQueuedTaskRepository.kt new file mode 100644 index 00000000..833baca9 --- /dev/null +++ b/owl/owl-broker/owl-broker-mongodb/src/main/kotlin/dev/usbharu/owl/broker/mongodb/MongodbQueuedTaskRepository.kt @@ -0,0 +1,186 @@ +/* + * Copyright (C) 2024 usbharu + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package dev.usbharu.owl.broker.mongodb + +import com.mongodb.client.model.Filters.* +import com.mongodb.client.model.FindOneAndUpdateOptions +import com.mongodb.client.model.ReplaceOptions +import com.mongodb.client.model.ReturnDocument +import com.mongodb.client.model.Sorts +import com.mongodb.client.model.Updates.set +import com.mongodb.kotlin.client.coroutine.MongoDatabase +import dev.usbharu.owl.broker.domain.model.queuedtask.QueuedTask +import dev.usbharu.owl.broker.domain.model.queuedtask.QueuedTaskRepository +import dev.usbharu.owl.broker.domain.model.task.Task +import dev.usbharu.owl.common.property.PropertySerializeUtils +import dev.usbharu.owl.common.property.PropertySerializerFactory +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.flowOn +import kotlinx.coroutines.flow.map +import kotlinx.coroutines.withContext +import org.bson.BsonType +import org.bson.codecs.pojo.annotations.BsonId +import org.bson.codecs.pojo.annotations.BsonRepresentation +import java.time.Instant +import java.util.* + +class MongodbQueuedTaskRepository( + private val propertySerializerFactory: PropertySerializerFactory, + database: MongoDatabase +) : QueuedTaskRepository { + + private val collection = database.getCollection("queued_task") + override suspend fun save(queuedTask: QueuedTask): QueuedTask { + withContext(Dispatchers.IO) { + collection.replaceOne( + eq("_id", queuedTask.task.id.toString()), QueuedTaskMongodb.of(propertySerializerFactory, queuedTask), + ReplaceOptions().upsert(true) + ) + } + return queuedTask + } + + override suspend fun findByTaskIdAndAssignedConsumerIsNullAndUpdate(id: UUID, update: QueuedTask): QueuedTask { + return withContext(Dispatchers.IO) { + + val findOneAndUpdate = collection.findOneAndUpdate( + and( + eq("_id", id.toString()), + eq(QueuedTaskMongodb::isActive.name, true) + ), + listOf( + set(QueuedTaskMongodb::assignedConsumer.name, update.assignedConsumer?.toString()), + set(QueuedTaskMongodb::assignedAt.name, update.assignedAt), + set(QueuedTaskMongodb::queuedAt.name, update.queuedAt), + set(QueuedTaskMongodb::isActive.name, update.isActive) + ), + FindOneAndUpdateOptions().upsert(false).returnDocument(ReturnDocument.AFTER) + ) + if (findOneAndUpdate == null) { + TODO() + } + findOneAndUpdate.toQueuedTask(propertySerializerFactory) + } + } + + override fun findByTaskNameInAndIsActiveIsTrueAndOrderByPriority( + tasks: List, + limit: Int + ): Flow { + return collection.find( + and( + `in`("task.name", tasks), + eq(QueuedTaskMongodb::isActive.name, true) + ) + ).sort(Sorts.descending("priority")).map { it.toQueuedTask(propertySerializerFactory) }.flowOn(Dispatchers.IO) + } + + override fun findByQueuedAtBeforeAndIsActiveIsTrue(instant: Instant): Flow { + return collection.find( + and( + lte(QueuedTaskMongodb::queuedAt.name, instant), + eq(QueuedTaskMongodb::isActive.name, true) + ) + ) + .map { it.toQueuedTask(propertySerializerFactory) }.flowOn(Dispatchers.IO) + } +} + +data class QueuedTaskMongodb( + @BsonId + @BsonRepresentation(BsonType.STRING) + val id: String, + val task: TaskMongodb, + val attempt: Int, + val queuedAt: Instant, + val priority:Int, + val isActive: Boolean, + val timeoutAt: Instant?, + val assignedConsumer: String?, + val assignedAt: Instant? +) { + + fun toQueuedTask(propertySerializerFactory: PropertySerializerFactory): QueuedTask { + return QueuedTask( + attempt = attempt, + queuedAt = queuedAt, + task = task.toTask(propertySerializerFactory), + priority = priority, + isActive = isActive, + timeoutAt = timeoutAt, + assignedConsumer = assignedConsumer?.let { UUID.fromString(it) }, + assignedAt = assignedAt + ) + } + + data class TaskMongodb( + val name: String, + val id: String, + val publishProducerId: String, + val publishedAt: Instant, + val nextRetry: Instant, + val completedAt: Instant?, + val attempt: Int, + val properties: Map + ) { + + fun toTask(propertySerializerFactory: PropertySerializerFactory): Task { + return Task( + name = name, + id = UUID.fromString(id), + publishProducerId = UUID.fromString(publishProducerId), + publishedAt = publishedAt, + nextRetry = nextRetry, + completedAt = completedAt, + attempt = attempt, + properties = PropertySerializeUtils.deserialize(propertySerializerFactory, properties) + ) + } + + companion object { + fun of(propertySerializerFactory: PropertySerializerFactory, task: Task): TaskMongodb { + return TaskMongodb( + task.name, + task.id.toString(), + task.publishProducerId.toString(), + task.publishedAt, + task.nextRetry, + task.completedAt, + task.attempt, + PropertySerializeUtils.serialize(propertySerializerFactory, task.properties) + ) + } + } + } + + companion object { + fun of(propertySerializerFactory: PropertySerializerFactory, queuedTask: QueuedTask): QueuedTaskMongodb { + return QueuedTaskMongodb( + queuedTask.task.id.toString(), + TaskMongodb.of(propertySerializerFactory, queuedTask.task), + queuedTask.attempt, + queuedTask.queuedAt, + queuedTask.priority, + queuedTask.isActive, + queuedTask.timeoutAt, + queuedTask.assignedConsumer?.toString(), + queuedTask.assignedAt + ) + } + } +} \ No newline at end of file diff --git a/owl/owl-broker/owl-broker-mongodb/src/main/kotlin/dev/usbharu/owl/broker/mongodb/MongodbTaskDefinitionRepository.kt b/owl/owl-broker/owl-broker-mongodb/src/main/kotlin/dev/usbharu/owl/broker/mongodb/MongodbTaskDefinitionRepository.kt new file mode 100644 index 00000000..f3b384a1 --- /dev/null +++ b/owl/owl-broker/owl-broker-mongodb/src/main/kotlin/dev/usbharu/owl/broker/mongodb/MongodbTaskDefinitionRepository.kt @@ -0,0 +1,85 @@ +/* + * Copyright (C) 2024 usbharu + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package dev.usbharu.owl.broker.mongodb + +import com.mongodb.client.model.Filters +import com.mongodb.client.model.ReplaceOptions +import com.mongodb.kotlin.client.coroutine.MongoDatabase +import dev.usbharu.owl.broker.domain.model.taskdefinition.TaskDefinition +import dev.usbharu.owl.broker.domain.model.taskdefinition.TaskDefinitionRepository +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.flow.singleOrNull +import kotlinx.coroutines.withContext +import org.bson.BsonType +import org.bson.codecs.pojo.annotations.BsonId +import org.bson.codecs.pojo.annotations.BsonRepresentation + +class MongodbTaskDefinitionRepository(database: MongoDatabase) : TaskDefinitionRepository { + + private val collection = database.getCollection("task_definition") + override suspend fun save(taskDefinition: TaskDefinition): TaskDefinition = withContext(Dispatchers.IO) { + collection.replaceOne( + Filters.eq("_id", taskDefinition.name), + TaskDefinitionMongodb.of(taskDefinition), + ReplaceOptions().upsert(true) + ) + return@withContext taskDefinition + } + + override suspend fun deleteByName(name: String): Unit = withContext(Dispatchers.IO) { + collection.deleteOne(Filters.eq("_id",name)) + } + + override suspend fun findByName(name: String): TaskDefinition? = withContext(Dispatchers.IO) { + return@withContext collection.find(Filters.eq("_id", name)).singleOrNull()?.toTaskDefinition() + } +} + +data class TaskDefinitionMongodb( + @BsonId + @BsonRepresentation(BsonType.STRING) + val name: String, + val priority: Int, + val maxRetry: Int, + val timeoutMilli: Long, + val propertyDefinitionHash: Long, + val retryPolicy: String +) { + fun toTaskDefinition(): TaskDefinition { + return TaskDefinition( + name = name, + priority = priority, + maxRetry = maxRetry, + timeoutMilli = timeoutMilli, + propertyDefinitionHash = propertyDefinitionHash, + retryPolicy = retryPolicy + ) + } + + companion object { + fun of(taskDefinition: TaskDefinition): TaskDefinitionMongodb { + return TaskDefinitionMongodb( + name = taskDefinition.name, + priority = taskDefinition.priority, + maxRetry = taskDefinition.maxRetry, + timeoutMilli = taskDefinition.timeoutMilli, + propertyDefinitionHash = taskDefinition.propertyDefinitionHash, + retryPolicy = taskDefinition.retryPolicy + ) + } + } +} \ No newline at end of file diff --git a/owl/owl-broker/owl-broker-mongodb/src/main/kotlin/dev/usbharu/owl/broker/mongodb/MongodbTaskRepository.kt b/owl/owl-broker/owl-broker-mongodb/src/main/kotlin/dev/usbharu/owl/broker/mongodb/MongodbTaskRepository.kt new file mode 100644 index 00000000..2745b0cd --- /dev/null +++ b/owl/owl-broker/owl-broker-mongodb/src/main/kotlin/dev/usbharu/owl/broker/mongodb/MongodbTaskRepository.kt @@ -0,0 +1,130 @@ +/* + * Copyright (C) 2024 usbharu + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package dev.usbharu.owl.broker.mongodb + +import com.mongodb.client.model.Filters +import com.mongodb.client.model.ReplaceOneModel +import com.mongodb.client.model.ReplaceOptions +import com.mongodb.kotlin.client.coroutine.MongoDatabase +import dev.usbharu.owl.broker.domain.model.task.Task +import dev.usbharu.owl.broker.domain.model.task.TaskRepository +import dev.usbharu.owl.common.property.PropertySerializeUtils +import dev.usbharu.owl.common.property.PropertySerializerFactory +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.flowOn +import kotlinx.coroutines.flow.map +import kotlinx.coroutines.flow.singleOrNull +import kotlinx.coroutines.withContext +import org.bson.BsonType +import org.bson.codecs.pojo.annotations.BsonId +import org.bson.codecs.pojo.annotations.BsonRepresentation +import java.time.Instant +import java.util.* + + +class MongodbTaskRepository(database: MongoDatabase, private val propertySerializerFactory: PropertySerializerFactory) : + TaskRepository { + + private val collection = database.getCollection("tasks") + override suspend fun save(task: Task): Task = withContext(Dispatchers.IO) { + collection.replaceOne( + Filters.eq("_id", task.id.toString()), TaskMongodb.of(propertySerializerFactory, task), + ReplaceOptions().upsert(true) + ) + return@withContext task + } + + override suspend fun saveAll(tasks: List): Unit = withContext(Dispatchers.IO) { + collection.bulkWrite(tasks.map { + ReplaceOneModel( + Filters.eq(it.id.toString()), + TaskMongodb.of(propertySerializerFactory, it), + ReplaceOptions().upsert(true) + ) + }) + } + + override fun findByNextRetryBeforeAndCompletedAtIsNull(timestamp: Instant): Flow { + return collection.find( + Filters.and( + Filters.lte(TaskMongodb::nextRetry.name, timestamp), + Filters.eq(TaskMongodb::completedAt.name, null) + ) + ) + .map { it.toTask(propertySerializerFactory) }.flowOn(Dispatchers.IO) + } + + override suspend fun findById(uuid: UUID): Task? = withContext(Dispatchers.IO) { + collection.find(Filters.eq(uuid.toString())).singleOrNull()?.toTask(propertySerializerFactory) + } + + override suspend fun findByIdAndUpdate(id: UUID, task: Task) { + collection.replaceOne( + Filters.eq("_id", task.id.toString()), TaskMongodb.of(propertySerializerFactory, task), + ReplaceOptions().upsert(false) + ) + } + + override suspend fun findByPublishProducerIdAndCompletedAtIsNotNull(publishProducerId: UUID): Flow { + return collection + .find(Filters.eq(TaskMongodb::publishProducerId.name, publishProducerId.toString())) + .map { it.toTask(propertySerializerFactory) } + } +} + +data class TaskMongodb( + val name: String, + @BsonId + @BsonRepresentation(BsonType.STRING) + val id: String, + val publishProducerId: String, + val publishedAt: Instant, + val nextRetry: Instant, + val completedAt: Instant?, + val attempt: Int, + val properties: Map +) { + + fun toTask(propertySerializerFactory: PropertySerializerFactory): Task { + return Task( + name = name, + id = UUID.fromString(id), + publishProducerId = UUID.fromString(publishProducerId), + publishedAt = publishedAt, + nextRetry = nextRetry, + completedAt = completedAt, + attempt = attempt, + properties = PropertySerializeUtils.deserialize(propertySerializerFactory, properties) + ) + } + + companion object { + fun of(propertySerializerFactory: PropertySerializerFactory, task: Task): TaskMongodb { + return TaskMongodb( + task.name, + task.id.toString(), + task.publishProducerId.toString(), + task.publishedAt, + task.nextRetry, + task.completedAt, + task.attempt, + PropertySerializeUtils.serialize(propertySerializerFactory, task.properties) + ) + } + } +} \ No newline at end of file diff --git a/owl/owl-broker/owl-broker-mongodb/src/main/kotlin/dev/usbharu/owl/broker/mongodb/MongodbTaskResultRepository.kt b/owl/owl-broker/owl-broker-mongodb/src/main/kotlin/dev/usbharu/owl/broker/mongodb/MongodbTaskResultRepository.kt new file mode 100644 index 00000000..2336a45c --- /dev/null +++ b/owl/owl-broker/owl-broker-mongodb/src/main/kotlin/dev/usbharu/owl/broker/mongodb/MongodbTaskResultRepository.kt @@ -0,0 +1,91 @@ +/* + * Copyright (C) 2024 usbharu + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package dev.usbharu.owl.broker.mongodb + +import com.mongodb.client.model.Filters +import com.mongodb.client.model.ReplaceOptions +import com.mongodb.kotlin.client.coroutine.MongoDatabase +import dev.usbharu.owl.broker.domain.model.taskresult.TaskResult +import dev.usbharu.owl.broker.domain.model.taskresult.TaskResultRepository +import dev.usbharu.owl.common.property.PropertySerializeUtils +import dev.usbharu.owl.common.property.PropertySerializerFactory +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.flowOn +import kotlinx.coroutines.flow.map +import kotlinx.coroutines.withContext +import org.bson.BsonType +import org.bson.codecs.pojo.annotations.BsonId +import org.bson.codecs.pojo.annotations.BsonRepresentation +import java.util.* + +class MongodbTaskResultRepository( + database: MongoDatabase, + private val propertySerializerFactory: PropertySerializerFactory +) : TaskResultRepository { + + private val collection = database.getCollection("task_results") + override suspend fun save(taskResult: TaskResult): TaskResult = withContext(Dispatchers.IO) { + collection.replaceOne( + Filters.eq(taskResult.id.toString()), TaskResultMongodb.of(propertySerializerFactory, taskResult), + ReplaceOptions().upsert(true) + ) + return@withContext taskResult + } + + override fun findByTaskId(id: UUID): Flow { + return collection.find(Filters.eq(id.toString())).map { it.toTaskResult(propertySerializerFactory) }.flowOn(Dispatchers.IO) + } +} + +data class TaskResultMongodb( + @BsonId + @BsonRepresentation(BsonType.STRING) + val id: String, + val taskId: String, + val success: Boolean, + val attempt: Int, + val result: Map, + val message: String +) { + + fun toTaskResult(propertySerializerFactory: PropertySerializerFactory): TaskResult { + return TaskResult( + UUID.fromString(id), + UUID.fromString(taskId), + success, + attempt, + PropertySerializeUtils.deserialize(propertySerializerFactory, result), + message + ) + } + + companion object { + fun of(propertySerializerFactory: PropertySerializerFactory, taskResult: TaskResult): TaskResultMongodb { + return TaskResultMongodb( + taskResult.id.toString(), + taskResult.taskId.toString(), + taskResult.success, + taskResult.attempt, + PropertySerializeUtils.serialize(propertySerializerFactory, taskResult.result), + taskResult.message + ) + + } + + } +} \ No newline at end of file diff --git a/owl/owl-broker/owl-broker-mongodb/src/main/resources/META-INF/services/dev.usbharu.owl.broker.ModuleContext b/owl/owl-broker/owl-broker-mongodb/src/main/resources/META-INF/services/dev.usbharu.owl.broker.ModuleContext new file mode 100644 index 00000000..0a1dface --- /dev/null +++ b/owl/owl-broker/owl-broker-mongodb/src/main/resources/META-INF/services/dev.usbharu.owl.broker.ModuleContext @@ -0,0 +1 @@ +dev.usbharu.owl.broker.mongodb.MongoModuleContext \ No newline at end of file diff --git a/owl/owl-broker/owl-broker-mongodb/src/test/kotlin/dev/usbharu/owl/broker/mongodb/MongodbConsumerRepositoryTest.kt b/owl/owl-broker/owl-broker-mongodb/src/test/kotlin/dev/usbharu/owl/broker/mongodb/MongodbConsumerRepositoryTest.kt new file mode 100644 index 00000000..b71c5ea4 --- /dev/null +++ b/owl/owl-broker/owl-broker-mongodb/src/test/kotlin/dev/usbharu/owl/broker/mongodb/MongodbConsumerRepositoryTest.kt @@ -0,0 +1,48 @@ +package dev.usbharu.owl.broker.mongodb + +import com.mongodb.ConnectionString +import com.mongodb.MongoClientSettings +import com.mongodb.kotlin.client.coroutine.MongoClient +import dev.usbharu.owl.broker.domain.model.consumer.Consumer +import kotlinx.coroutines.runBlocking +import org.bson.UuidRepresentation +import org.junit.jupiter.api.Assertions.assertEquals +import org.junit.jupiter.api.Test +import java.util.* + + +class MongodbConsumerRepositoryTest { + @Test + fun name() { + + val clientSettings = + MongoClientSettings.builder().applyConnectionString(ConnectionString("mongodb://localhost:27017")) + .uuidRepresentation(UuidRepresentation.STANDARD).build() + + + val database = MongoClient.create(clientSettings).getDatabase("mongo-test") + + val mongodbConsumerRepository = MongodbConsumerRepository(database) + + val consumer = Consumer( + UUID.randomUUID(), + name = "test", + hostname = "aaa", + tasks = listOf("a", "b", "c") + ) + runBlocking { + mongodbConsumerRepository.save(consumer) + + val findById = mongodbConsumerRepository.findById(UUID.randomUUID()) + assertEquals(null, findById) + + val findById1 = mongodbConsumerRepository.findById(consumer.id) + assertEquals(consumer, findById1) + + mongodbConsumerRepository.save(consumer.copy(name = "test2")) + + val findById2 = mongodbConsumerRepository.findById(consumer.id) + assertEquals(consumer.copy(name = "test2"), findById2) + } + } +} \ No newline at end of file diff --git a/owl/owl-broker/src/main/kotlin/dev/usbharu/owl/broker/Main.kt b/owl/owl-broker/src/main/kotlin/dev/usbharu/owl/broker/Main.kt new file mode 100644 index 00000000..d5f25364 --- /dev/null +++ b/owl/owl-broker/src/main/kotlin/dev/usbharu/owl/broker/Main.kt @@ -0,0 +1,111 @@ +/* + * Copyright (C) 2024 usbharu + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package dev.usbharu.owl.broker + +import dev.usbharu.owl.broker.interfaces.grpc.* +import dev.usbharu.owl.broker.service.* +import dev.usbharu.owl.broker.service.ProducerService +import dev.usbharu.owl.broker.service.TaskPublishService +import dev.usbharu.owl.common.property.PropertySerializerFactory +import dev.usbharu.owl.common.retry.DefaultRetryPolicyFactory +import dev.usbharu.owl.common.retry.ExponentialRetryPolicy +import dev.usbharu.owl.common.retry.RetryPolicyFactory +import kotlinx.coroutines.runBlocking +import org.koin.core.context.startKoin +import org.koin.dsl.module +import org.slf4j.LoggerFactory +import java.util.* + +val logger = LoggerFactory.getLogger("MAIN") + +val mainModule = module { + single { + AssignQueuedTaskDeciderImpl(get(), get()) + } + single { TaskScannerImpl(get()) } + single { TaskPublishServiceImpl(get(), get(), get()) } + single { + TaskManagementServiceImpl( + taskScanner = get(), + queueStore = get(), + taskDefinitionRepository = get(), + assignQueuedTaskDecider = get(), + retryPolicyFactory = get(), + taskRepository = get(), + queueScanner = get(), + taskResultRepository = get() + ) + } + single { RegisterTaskServiceImpl(get()) } + single { QueueStoreImpl(get()) } + single { QueueScannerImpl(get()) } + single { QueuedTaskAssignerImpl(get(), get()) } + single { ProducerServiceImpl(get()) } + single { DefaultPropertySerializerFactory() } + single { ConsumerServiceImpl(get()) } + single { + OwlBrokerApplication( + assignmentTaskService = get(), + definitionTaskService = get(), + producerService = get(), + subscribeTaskService = get(), + taskPublishService = get(), + taskManagementService = get(), + taskResultSubscribeService = get(), + taskResultService = get() + ) + } + single { AssignmentTaskService(queuedTaskAssigner = get(), propertySerializerFactory = get()) } + single { DefinitionTaskService(registerTaskService = get()) } + single { dev.usbharu.owl.broker.interfaces.grpc.ProducerService(producerService = get()) } + single { SubscribeTaskService(consumerService = get()) } + single { + dev.usbharu.owl.broker.interfaces.grpc.TaskPublishService( + taskPublishService = get(), + propertySerializerFactory = get() + ) + } + single { TaskResultService(taskManagementService = get(), propertySerializerFactory = get()) } + single { TaskResultSubscribeService(taskManagementService = get(), propertySerializerFactory = get()) } +} + +fun main() { + val moduleContexts = ServiceLoader.load(ModuleContext::class.java) + + val moduleContext = moduleContexts.first() + + logger.info("Use module name: {}", moduleContext) + + + val koin = startKoin { + printLogger() + + val module = module { + single { + DefaultRetryPolicyFactory(mapOf("" to ExponentialRetryPolicy())) + } + + } + modules(mainModule, module, moduleContext.module()) + } + + val application = koin.koin.get() + + runBlocking { + application.start(50051).join() + } +} \ No newline at end of file diff --git a/owl/owl-broker/src/main/kotlin/dev/usbharu/owl/broker/ModuleContext.kt b/owl/owl-broker/src/main/kotlin/dev/usbharu/owl/broker/ModuleContext.kt new file mode 100644 index 00000000..fbb32f7c --- /dev/null +++ b/owl/owl-broker/src/main/kotlin/dev/usbharu/owl/broker/ModuleContext.kt @@ -0,0 +1,29 @@ +/* + * Copyright (C) 2024 usbharu + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package dev.usbharu.owl.broker + +import org.koin.core.module.Module + +interface ModuleContext { + fun module():Module +} + +data object EmptyModuleContext : ModuleContext { + override fun module(): Module { + return org.koin.dsl.module { } + } +} \ No newline at end of file diff --git a/owl/owl-broker/src/main/kotlin/dev/usbharu/owl/broker/OwlBrokerApplication.kt b/owl/owl-broker/src/main/kotlin/dev/usbharu/owl/broker/OwlBrokerApplication.kt new file mode 100644 index 00000000..a67661ca --- /dev/null +++ b/owl/owl-broker/src/main/kotlin/dev/usbharu/owl/broker/OwlBrokerApplication.kt @@ -0,0 +1,68 @@ +/* + * Copyright (C) 2024 usbharu + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package dev.usbharu.owl.broker + +import dev.usbharu.owl.broker.interfaces.grpc.* +import dev.usbharu.owl.broker.service.TaskManagementService +import io.grpc.Server +import io.grpc.ServerBuilder +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.GlobalScope +import kotlinx.coroutines.Job +import kotlinx.coroutines.launch + +class OwlBrokerApplication( + private val assignmentTaskService: AssignmentTaskService, + private val definitionTaskService: DefinitionTaskService, + private val producerService: ProducerService, + private val subscribeTaskService: SubscribeTaskService, + private val taskPublishService: TaskPublishService, + private val taskManagementService: TaskManagementService, + private val taskResultSubscribeService: TaskResultSubscribeService, + private val taskResultService: TaskResultService, +) { + + private lateinit var server: Server + + fun start(port: Int,coroutineScope: CoroutineScope = GlobalScope):Job { + server = ServerBuilder.forPort(port) + .addService(assignmentTaskService) + .addService(definitionTaskService) + .addService(producerService) + .addService(subscribeTaskService) + .addService(taskPublishService) + .addService(taskResultSubscribeService) + .addService(taskResultService) + .build() + + server.start() + Runtime.getRuntime().addShutdownHook( + Thread { + server.shutdown() + } + ) + + return coroutineScope.launch { + taskManagementService.startManagement(this) + } + } + + fun stop() { + server.shutdown() + } + +} \ No newline at end of file diff --git a/owl/owl-broker/src/main/kotlin/dev/usbharu/owl/broker/domain/exception/InvalidRepositoryException.kt b/owl/owl-broker/src/main/kotlin/dev/usbharu/owl/broker/domain/exception/InvalidRepositoryException.kt new file mode 100644 index 00000000..9019c314 --- /dev/null +++ b/owl/owl-broker/src/main/kotlin/dev/usbharu/owl/broker/domain/exception/InvalidRepositoryException.kt @@ -0,0 +1,30 @@ +/* + * Copyright (C) 2024 usbharu + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package dev.usbharu.owl.broker.domain.exception + +class InvalidRepositoryException : 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 + ) +} \ No newline at end of file diff --git a/owl/owl-broker/src/main/kotlin/dev/usbharu/owl/broker/domain/exception/repository/FailedSaveException.kt b/owl/owl-broker/src/main/kotlin/dev/usbharu/owl/broker/domain/exception/repository/FailedSaveException.kt new file mode 100644 index 00000000..4cb68305 --- /dev/null +++ b/owl/owl-broker/src/main/kotlin/dev/usbharu/owl/broker/domain/exception/repository/FailedSaveException.kt @@ -0,0 +1,30 @@ +/* + * Copyright (C) 2024 usbharu + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package dev.usbharu.owl.broker.domain.exception.repository + +class FailedSaveException : 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 + ) +} \ No newline at end of file diff --git a/owl/owl-broker/src/main/kotlin/dev/usbharu/owl/broker/domain/exception/repository/RecordNotFoundException.kt b/owl/owl-broker/src/main/kotlin/dev/usbharu/owl/broker/domain/exception/repository/RecordNotFoundException.kt new file mode 100644 index 00000000..62921c46 --- /dev/null +++ b/owl/owl-broker/src/main/kotlin/dev/usbharu/owl/broker/domain/exception/repository/RecordNotFoundException.kt @@ -0,0 +1,30 @@ +/* + * Copyright (C) 2024 usbharu + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package dev.usbharu.owl.broker.domain.exception.repository + +open class RecordNotFoundException : 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 + ) +} \ No newline at end of file diff --git a/owl/owl-broker/src/main/kotlin/dev/usbharu/owl/broker/domain/exception/service/IncompatibleTaskException.kt b/owl/owl-broker/src/main/kotlin/dev/usbharu/owl/broker/domain/exception/service/IncompatibleTaskException.kt new file mode 100644 index 00000000..9f940bc2 --- /dev/null +++ b/owl/owl-broker/src/main/kotlin/dev/usbharu/owl/broker/domain/exception/service/IncompatibleTaskException.kt @@ -0,0 +1,30 @@ +/* + * Copyright (C) 2024 usbharu + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package dev.usbharu.owl.broker.domain.exception.service + +class IncompatibleTaskException : 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 + ) +} \ No newline at end of file diff --git a/owl/owl-broker/src/main/kotlin/dev/usbharu/owl/broker/domain/exception/service/QueueCannotDequeueException.kt b/owl/owl-broker/src/main/kotlin/dev/usbharu/owl/broker/domain/exception/service/QueueCannotDequeueException.kt new file mode 100644 index 00000000..49b47dbf --- /dev/null +++ b/owl/owl-broker/src/main/kotlin/dev/usbharu/owl/broker/domain/exception/service/QueueCannotDequeueException.kt @@ -0,0 +1,30 @@ +/* + * Copyright (C) 2024 usbharu + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package dev.usbharu.owl.broker.domain.exception.service + +class QueueCannotDequeueException : 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 + ) +} \ No newline at end of file diff --git a/owl/owl-broker/src/main/kotlin/dev/usbharu/owl/broker/domain/exception/service/TaskNotRegisterException.kt b/owl/owl-broker/src/main/kotlin/dev/usbharu/owl/broker/domain/exception/service/TaskNotRegisterException.kt new file mode 100644 index 00000000..ca894165 --- /dev/null +++ b/owl/owl-broker/src/main/kotlin/dev/usbharu/owl/broker/domain/exception/service/TaskNotRegisterException.kt @@ -0,0 +1,30 @@ +/* + * Copyright (C) 2024 usbharu + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package dev.usbharu.owl.broker.domain.exception.service + +class TaskNotRegisterException : 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 + ) +} \ No newline at end of file diff --git a/owl/owl-broker/src/main/kotlin/dev/usbharu/owl/broker/domain/model/consumer/Consumer.kt b/owl/owl-broker/src/main/kotlin/dev/usbharu/owl/broker/domain/model/consumer/Consumer.kt new file mode 100644 index 00000000..27fe78b2 --- /dev/null +++ b/owl/owl-broker/src/main/kotlin/dev/usbharu/owl/broker/domain/model/consumer/Consumer.kt @@ -0,0 +1,26 @@ +/* + * Copyright (C) 2024 usbharu + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package dev.usbharu.owl.broker.domain.model.consumer + +import java.util.* + +data class Consumer( + val id: UUID, + val name: String, + val hostname: String, + val tasks: List +) diff --git a/owl/owl-broker/src/main/kotlin/dev/usbharu/owl/broker/domain/model/consumer/ConsumerRepository.kt b/owl/owl-broker/src/main/kotlin/dev/usbharu/owl/broker/domain/model/consumer/ConsumerRepository.kt new file mode 100644 index 00000000..34ebb0af --- /dev/null +++ b/owl/owl-broker/src/main/kotlin/dev/usbharu/owl/broker/domain/model/consumer/ConsumerRepository.kt @@ -0,0 +1,25 @@ +/* + * Copyright (C) 2024 usbharu + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package dev.usbharu.owl.broker.domain.model.consumer + +import java.util.* + +interface ConsumerRepository { + suspend fun save(consumer: Consumer):Consumer + + suspend fun findById(id:UUID):Consumer? +} \ No newline at end of file diff --git a/owl/owl-broker/src/main/kotlin/dev/usbharu/owl/broker/domain/model/producer/Producer.kt b/owl/owl-broker/src/main/kotlin/dev/usbharu/owl/broker/domain/model/producer/Producer.kt new file mode 100644 index 00000000..eebbafd4 --- /dev/null +++ b/owl/owl-broker/src/main/kotlin/dev/usbharu/owl/broker/domain/model/producer/Producer.kt @@ -0,0 +1,28 @@ +/* + * Copyright (C) 2024 usbharu + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package dev.usbharu.owl.broker.domain.model.producer + +import java.time.Instant +import java.util.* + +data class Producer( + val id:UUID, + val name:String, + val hostname:String, + val registeredTask:List, + val createdAt: Instant +) diff --git a/owl/owl-broker/src/main/kotlin/dev/usbharu/owl/broker/domain/model/producer/ProducerRepository.kt b/owl/owl-broker/src/main/kotlin/dev/usbharu/owl/broker/domain/model/producer/ProducerRepository.kt new file mode 100644 index 00000000..932cef10 --- /dev/null +++ b/owl/owl-broker/src/main/kotlin/dev/usbharu/owl/broker/domain/model/producer/ProducerRepository.kt @@ -0,0 +1,21 @@ +/* + * Copyright (C) 2024 usbharu + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package dev.usbharu.owl.broker.domain.model.producer + +interface ProducerRepository { + suspend fun save(producer: Producer):Producer +} \ No newline at end of file diff --git a/owl/owl-broker/src/main/kotlin/dev/usbharu/owl/broker/domain/model/queuedtask/QueuedTask.kt b/owl/owl-broker/src/main/kotlin/dev/usbharu/owl/broker/domain/model/queuedtask/QueuedTask.kt new file mode 100644 index 00000000..b9dab655 --- /dev/null +++ b/owl/owl-broker/src/main/kotlin/dev/usbharu/owl/broker/domain/model/queuedtask/QueuedTask.kt @@ -0,0 +1,36 @@ +/* + * Copyright (C) 2024 usbharu + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package dev.usbharu.owl.broker.domain.model.queuedtask + +import dev.usbharu.owl.broker.domain.model.task.Task +import java.time.Instant +import java.util.* + +/** + * @param attempt キューされた時点での試行回数より1多い + * @param isActive trueならアサイン可能 falseならアサイン済みかタイムアウト等で無効 + */ +data class QueuedTask( + val attempt: Int, + val queuedAt: Instant, + val task: Task, + val priority: Int, + val isActive: Boolean, + val timeoutAt: Instant?, + val assignedConsumer: UUID?, + val assignedAt: Instant? +) diff --git a/owl/owl-broker/src/main/kotlin/dev/usbharu/owl/broker/domain/model/queuedtask/QueuedTaskRepository.kt b/owl/owl-broker/src/main/kotlin/dev/usbharu/owl/broker/domain/model/queuedtask/QueuedTaskRepository.kt new file mode 100644 index 00000000..9f3c12a7 --- /dev/null +++ b/owl/owl-broker/src/main/kotlin/dev/usbharu/owl/broker/domain/model/queuedtask/QueuedTaskRepository.kt @@ -0,0 +1,34 @@ +/* + * Copyright (C) 2024 usbharu + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package dev.usbharu.owl.broker.domain.model.queuedtask + +import kotlinx.coroutines.flow.Flow +import java.time.Instant +import java.util.* + +interface QueuedTaskRepository { + suspend fun save(queuedTask: QueuedTask):QueuedTask + + /** + * トランザクションの代わり + */ + suspend fun findByTaskIdAndAssignedConsumerIsNullAndUpdate(id:UUID,update:QueuedTask):QueuedTask + + fun findByTaskNameInAndIsActiveIsTrueAndOrderByPriority(tasks: List, limit: Int): Flow + + fun findByQueuedAtBeforeAndIsActiveIsTrue(instant: Instant): Flow +} \ No newline at end of file diff --git a/owl/owl-broker/src/main/kotlin/dev/usbharu/owl/broker/domain/model/task/Task.kt b/owl/owl-broker/src/main/kotlin/dev/usbharu/owl/broker/domain/model/task/Task.kt new file mode 100644 index 00000000..8bf3a162 --- /dev/null +++ b/owl/owl-broker/src/main/kotlin/dev/usbharu/owl/broker/domain/model/task/Task.kt @@ -0,0 +1,35 @@ +/* + * Copyright (C) 2024 usbharu + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package dev.usbharu.owl.broker.domain.model.task + +import dev.usbharu.owl.common.property.PropertyValue +import java.time.Instant +import java.util.* + +/** + * @param attempt 失敗を含めて試行した回数 + */ +data class Task( + val name:String, + val id: UUID, + val publishProducerId:UUID, + val publishedAt: Instant, + val nextRetry:Instant, + val completedAt: Instant? = null, + val attempt: Int, + val properties: Map> +) diff --git a/owl/owl-broker/src/main/kotlin/dev/usbharu/owl/broker/domain/model/task/TaskRepository.kt b/owl/owl-broker/src/main/kotlin/dev/usbharu/owl/broker/domain/model/task/TaskRepository.kt new file mode 100644 index 00000000..009dea2d --- /dev/null +++ b/owl/owl-broker/src/main/kotlin/dev/usbharu/owl/broker/domain/model/task/TaskRepository.kt @@ -0,0 +1,35 @@ +/* + * Copyright (C) 2024 usbharu + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package dev.usbharu.owl.broker.domain.model.task + +import kotlinx.coroutines.flow.Flow +import java.time.Instant +import java.util.* + +interface TaskRepository { + suspend fun save(task: Task):Task + + suspend fun saveAll(tasks:List) + + fun findByNextRetryBeforeAndCompletedAtIsNull(timestamp:Instant): Flow + + suspend fun findById(uuid: UUID): Task? + + suspend fun findByIdAndUpdate(id:UUID,task: Task) + + suspend fun findByPublishProducerIdAndCompletedAtIsNotNull(publishProducerId:UUID):Flow +} \ No newline at end of file diff --git a/owl/owl-broker/src/main/kotlin/dev/usbharu/owl/broker/domain/model/taskdefinition/TaskDefinition.kt b/owl/owl-broker/src/main/kotlin/dev/usbharu/owl/broker/domain/model/taskdefinition/TaskDefinition.kt new file mode 100644 index 00000000..8fcb8f1e --- /dev/null +++ b/owl/owl-broker/src/main/kotlin/dev/usbharu/owl/broker/domain/model/taskdefinition/TaskDefinition.kt @@ -0,0 +1,26 @@ +/* + * Copyright (C) 2024 usbharu + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package dev.usbharu.owl.broker.domain.model.taskdefinition + +data class TaskDefinition( + val name: String, + val priority: Int, + val maxRetry: Int, + val timeoutMilli: Long, + val propertyDefinitionHash: Long, + val retryPolicy:String +) diff --git a/owl/owl-broker/src/main/kotlin/dev/usbharu/owl/broker/domain/model/taskdefinition/TaskDefinitionRepository.kt b/owl/owl-broker/src/main/kotlin/dev/usbharu/owl/broker/domain/model/taskdefinition/TaskDefinitionRepository.kt new file mode 100644 index 00000000..2a1c3c6a --- /dev/null +++ b/owl/owl-broker/src/main/kotlin/dev/usbharu/owl/broker/domain/model/taskdefinition/TaskDefinitionRepository.kt @@ -0,0 +1,24 @@ +/* + * Copyright (C) 2024 usbharu + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package dev.usbharu.owl.broker.domain.model.taskdefinition + +interface TaskDefinitionRepository { + suspend fun save(taskDefinition: TaskDefinition): TaskDefinition + suspend fun deleteByName(name:String) + + suspend fun findByName(name:String):TaskDefinition? +} \ No newline at end of file diff --git a/owl/owl-broker/src/main/kotlin/dev/usbharu/owl/broker/domain/model/taskresult/TaskResult.kt b/owl/owl-broker/src/main/kotlin/dev/usbharu/owl/broker/domain/model/taskresult/TaskResult.kt new file mode 100644 index 00000000..b024d04c --- /dev/null +++ b/owl/owl-broker/src/main/kotlin/dev/usbharu/owl/broker/domain/model/taskresult/TaskResult.kt @@ -0,0 +1,29 @@ +/* + * Copyright (C) 2024 usbharu + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package dev.usbharu.owl.broker.domain.model.taskresult + +import dev.usbharu.owl.common.property.PropertyValue +import java.util.* + +data class TaskResult( + val id: UUID, + val taskId:UUID, + val success: Boolean, + val attempt: Int, + val result: Map>, + val message: String +) diff --git a/owl/owl-broker/src/main/kotlin/dev/usbharu/owl/broker/domain/model/taskresult/TaskResultRepository.kt b/owl/owl-broker/src/main/kotlin/dev/usbharu/owl/broker/domain/model/taskresult/TaskResultRepository.kt new file mode 100644 index 00000000..4118cfed --- /dev/null +++ b/owl/owl-broker/src/main/kotlin/dev/usbharu/owl/broker/domain/model/taskresult/TaskResultRepository.kt @@ -0,0 +1,25 @@ +/* + * Copyright (C) 2024 usbharu + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package dev.usbharu.owl.broker.domain.model.taskresult + +import kotlinx.coroutines.flow.Flow +import java.util.* + +interface TaskResultRepository { + suspend fun save(taskResult: TaskResult):TaskResult + fun findByTaskId(id:UUID): Flow +} \ No newline at end of file diff --git a/owl/owl-broker/src/main/kotlin/dev/usbharu/owl/broker/external/GrpcExtension.kt b/owl/owl-broker/src/main/kotlin/dev/usbharu/owl/broker/external/GrpcExtension.kt new file mode 100644 index 00000000..4271b55c --- /dev/null +++ b/owl/owl-broker/src/main/kotlin/dev/usbharu/owl/broker/external/GrpcExtension.kt @@ -0,0 +1,35 @@ +/* + * Copyright (C) 2024 usbharu + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package dev.usbharu.owl.broker.external + +import com.google.protobuf.Timestamp +import dev.usbharu.owl.Uuid +import java.time.Instant +import java.util.* + +fun Uuid.UUID.toUUID(): UUID = UUID(mostSignificantUuidBits, leastSignificantUuidBits) + +fun UUID.toUUID(): Uuid.UUID = Uuid + .UUID + .newBuilder() + .setMostSignificantUuidBits(mostSignificantBits) + .setLeastSignificantUuidBits(leastSignificantBits) + .build() + +fun Timestamp.toInstant(): Instant = Instant.ofEpochSecond(seconds, nanos.toLong()) + +fun Instant.toTimestamp():Timestamp = Timestamp.newBuilder().setSeconds(this.epochSecond).setNanos(this.nano).build() \ No newline at end of file diff --git a/owl/owl-broker/src/main/kotlin/dev/usbharu/owl/broker/interfaces/grpc/AssignmentTaskService.kt b/owl/owl-broker/src/main/kotlin/dev/usbharu/owl/broker/interfaces/grpc/AssignmentTaskService.kt new file mode 100644 index 00000000..f92faf17 --- /dev/null +++ b/owl/owl-broker/src/main/kotlin/dev/usbharu/owl/broker/interfaces/grpc/AssignmentTaskService.kt @@ -0,0 +1,75 @@ +/* + * Copyright (C) 2024 usbharu + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package dev.usbharu.owl.broker.interfaces.grpc + + +import dev.usbharu.owl.AssignmentTaskServiceGrpcKt +import dev.usbharu.owl.Task +import dev.usbharu.owl.broker.external.toTimestamp +import dev.usbharu.owl.broker.external.toUUID +import dev.usbharu.owl.broker.service.QueuedTaskAssigner +import dev.usbharu.owl.common.property.PropertySerializeUtils +import dev.usbharu.owl.common.property.PropertySerializerFactory +import io.grpc.Status +import io.grpc.StatusException +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.flatMapMerge +import kotlinx.coroutines.flow.map +import org.slf4j.LoggerFactory +import kotlin.coroutines.CoroutineContext +import kotlin.coroutines.EmptyCoroutineContext + + +class AssignmentTaskService( + coroutineContext: CoroutineContext = EmptyCoroutineContext, + private val queuedTaskAssigner: QueuedTaskAssigner, + private val propertySerializerFactory: PropertySerializerFactory +) : + AssignmentTaskServiceGrpcKt.AssignmentTaskServiceCoroutineImplBase(coroutineContext) { + + override fun ready(requests: Flow): Flow { + + return try { + requests + .flatMapMerge { + queuedTaskAssigner.ready(it.consumerId.toUUID(), it.numberOfConcurrent) + } + .map { + Task.TaskRequest + .newBuilder() + .setName(it.task.name) + .setId(it.task.id.toUUID()) + .setAttempt(it.attempt) + .setQueuedAt(it.queuedAt.toTimestamp()) + .putAllProperties( + PropertySerializeUtils.serialize( + propertySerializerFactory, + it.task.properties + ) + ) + .build() + } + } catch (e: Exception) { + logger.warn("Error while reading requests", e) + throw StatusException(Status.INTERNAL.withDescription("Error while reading requests").withCause(e)) + } + } + + companion object { + private val logger = LoggerFactory.getLogger(AssignmentTaskService::class.java) + } +} \ No newline at end of file diff --git a/owl/owl-broker/src/main/kotlin/dev/usbharu/owl/broker/interfaces/grpc/DefinitionTaskService.kt b/owl/owl-broker/src/main/kotlin/dev/usbharu/owl/broker/interfaces/grpc/DefinitionTaskService.kt new file mode 100644 index 00000000..d00d584a --- /dev/null +++ b/owl/owl-broker/src/main/kotlin/dev/usbharu/owl/broker/interfaces/grpc/DefinitionTaskService.kt @@ -0,0 +1,53 @@ +/* + * Copyright (C) 2024 usbharu + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package dev.usbharu.owl.broker.interfaces.grpc + +import com.google.protobuf.Empty +import dev.usbharu.owl.DefinitionTask +import dev.usbharu.owl.DefinitionTask.TaskDefined +import dev.usbharu.owl.DefinitionTaskServiceGrpcKt.DefinitionTaskServiceCoroutineImplBase +import dev.usbharu.owl.broker.domain.model.taskdefinition.TaskDefinition +import dev.usbharu.owl.broker.service.RegisterTaskService +import kotlin.coroutines.CoroutineContext +import kotlin.coroutines.EmptyCoroutineContext + +class DefinitionTaskService(coroutineContext: CoroutineContext = EmptyCoroutineContext,private val registerTaskService: RegisterTaskService) : + DefinitionTaskServiceCoroutineImplBase(coroutineContext) { + override suspend fun register(request: DefinitionTask.TaskDefinition): TaskDefined { + registerTaskService.registerTask( + TaskDefinition( + request.name, + request.priority, + request.maxRetry, + request.timeoutMilli, + request.propertyDefinitionHash, + request.retryPolicy + ) + ) + return TaskDefined + .newBuilder() + .setTaskId( + request.name + ) + .build() + } + + override suspend fun unregister(request: DefinitionTask.TaskUnregister): Empty { + registerTaskService.unregisterTask(request.name) + return Empty.getDefaultInstance() + } +} \ No newline at end of file diff --git a/owl/owl-broker/src/main/kotlin/dev/usbharu/owl/broker/interfaces/grpc/ProducerService.kt b/owl/owl-broker/src/main/kotlin/dev/usbharu/owl/broker/interfaces/grpc/ProducerService.kt new file mode 100644 index 00000000..47bbb62e --- /dev/null +++ b/owl/owl-broker/src/main/kotlin/dev/usbharu/owl/broker/interfaces/grpc/ProducerService.kt @@ -0,0 +1,41 @@ +/* + * Copyright (C) 2024 usbharu + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package dev.usbharu.owl.broker.interfaces.grpc + +import dev.usbharu.owl.ProducerOuterClass +import dev.usbharu.owl.ProducerServiceGrpcKt.ProducerServiceCoroutineImplBase +import dev.usbharu.owl.broker.external.toUUID +import dev.usbharu.owl.broker.service.ProducerService +import dev.usbharu.owl.broker.service.RegisterProducerRequest +import kotlin.coroutines.CoroutineContext +import kotlin.coroutines.EmptyCoroutineContext + + +class ProducerService( + coroutineContext: CoroutineContext = EmptyCoroutineContext, + private val producerService: ProducerService +) : + ProducerServiceCoroutineImplBase(coroutineContext) { + override suspend fun registerProducer(request: ProducerOuterClass.Producer): ProducerOuterClass.RegisterProducerResponse { + val registerProducer = producerService.registerProducer( + RegisterProducerRequest( + request.name, request.hostname + ) + ) + return ProducerOuterClass.RegisterProducerResponse.newBuilder().setId(registerProducer.toUUID()).build() + } +} \ No newline at end of file diff --git a/owl/owl-broker/src/main/kotlin/dev/usbharu/owl/broker/interfaces/grpc/SubscribeTaskService.kt b/owl/owl-broker/src/main/kotlin/dev/usbharu/owl/broker/interfaces/grpc/SubscribeTaskService.kt new file mode 100644 index 00000000..62539d55 --- /dev/null +++ b/owl/owl-broker/src/main/kotlin/dev/usbharu/owl/broker/interfaces/grpc/SubscribeTaskService.kt @@ -0,0 +1,37 @@ +/* + * Copyright (C) 2024 usbharu + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package dev.usbharu.owl.broker.interfaces.grpc + +import dev.usbharu.owl.Consumer +import dev.usbharu.owl.SubscribeTaskServiceGrpcKt.SubscribeTaskServiceCoroutineImplBase +import dev.usbharu.owl.broker.external.toUUID +import dev.usbharu.owl.broker.service.ConsumerService +import dev.usbharu.owl.broker.service.RegisterConsumerRequest +import kotlin.coroutines.CoroutineContext +import kotlin.coroutines.EmptyCoroutineContext + +class SubscribeTaskService( + coroutineContext: CoroutineContext = EmptyCoroutineContext, + private val consumerService: ConsumerService +) : + SubscribeTaskServiceCoroutineImplBase(coroutineContext) { + override suspend fun subscribeTask(request: Consumer.SubscribeTaskRequest): Consumer.SubscribeTaskResponse { + val id = + consumerService.registerConsumer(RegisterConsumerRequest(request.name, request.hostname, request.tasksList)) + return Consumer.SubscribeTaskResponse.newBuilder().setId(id.toUUID()).build() + } +} \ No newline at end of file diff --git a/owl/owl-broker/src/main/kotlin/dev/usbharu/owl/broker/interfaces/grpc/TaskPublishService.kt b/owl/owl-broker/src/main/kotlin/dev/usbharu/owl/broker/interfaces/grpc/TaskPublishService.kt new file mode 100644 index 00000000..10b2154f --- /dev/null +++ b/owl/owl-broker/src/main/kotlin/dev/usbharu/owl/broker/interfaces/grpc/TaskPublishService.kt @@ -0,0 +1,82 @@ +/* + * Copyright (C) 2024 usbharu + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package dev.usbharu.owl.broker.interfaces.grpc + +import dev.usbharu.owl.PublishTaskOuterClass +import dev.usbharu.owl.PublishTaskOuterClass.PublishedTask +import dev.usbharu.owl.PublishTaskOuterClass.PublishedTasks +import dev.usbharu.owl.TaskPublishServiceGrpcKt.TaskPublishServiceCoroutineImplBase +import dev.usbharu.owl.broker.external.toUUID +import dev.usbharu.owl.broker.service.PublishTask +import dev.usbharu.owl.broker.service.TaskPublishService +import dev.usbharu.owl.common.property.PropertySerializeUtils +import dev.usbharu.owl.common.property.PropertySerializerFactory +import io.grpc.Status +import io.grpc.StatusException +import org.slf4j.LoggerFactory +import kotlin.coroutines.CoroutineContext +import kotlin.coroutines.EmptyCoroutineContext + +class TaskPublishService( + coroutineContext: CoroutineContext = EmptyCoroutineContext, + private val taskPublishService: TaskPublishService, + private val propertySerializerFactory: PropertySerializerFactory +) : + TaskPublishServiceCoroutineImplBase(coroutineContext) { + + override suspend fun publishTask(request: PublishTaskOuterClass.PublishTask): PublishedTask { + + logger.warn("aaaaaaaaaaa") + + + + return try { + + val publishedTask = taskPublishService.publishTask( + PublishTask( + request.name, + request.producerId.toUUID(), + PropertySerializeUtils.deserialize(propertySerializerFactory, request.propertiesMap) + ) + ) + PublishedTask.newBuilder().setName(publishedTask.name).setId(publishedTask.id.toUUID()).build() + } catch (e: Throwable) { + logger.warn("exception ", e) + throw StatusException(Status.INTERNAL) + } + } + + override suspend fun publishTasks(request: PublishTaskOuterClass.PublishTasks): PublishedTasks { + + val tasks = request.propertiesArrayList.map { + PublishTask( + request.name, + request.producerId.toUUID(), + PropertySerializeUtils.deserialize(propertySerializerFactory, it.propertiesMap) + ) + } + + val publishTasks = taskPublishService.publishTasks(tasks) + + return PublishedTasks.newBuilder().setName(request.name).addAllId(publishTasks.map { it.id.toUUID() }).build() + } + + companion object { + private val logger = + LoggerFactory.getLogger(dev.usbharu.owl.broker.interfaces.grpc.TaskPublishService::class.java) + } +} \ No newline at end of file diff --git a/owl/owl-broker/src/main/kotlin/dev/usbharu/owl/broker/interfaces/grpc/TaskResultService.kt b/owl/owl-broker/src/main/kotlin/dev/usbharu/owl/broker/interfaces/grpc/TaskResultService.kt new file mode 100644 index 00000000..90d10f6b --- /dev/null +++ b/owl/owl-broker/src/main/kotlin/dev/usbharu/owl/broker/interfaces/grpc/TaskResultService.kt @@ -0,0 +1,70 @@ +/* + * Copyright (C) 2024 usbharu + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package dev.usbharu.owl.broker.interfaces.grpc + +import com.google.protobuf.Empty +import dev.usbharu.owl.TaskResultOuterClass +import dev.usbharu.owl.TaskResultServiceGrpcKt +import dev.usbharu.owl.broker.domain.model.taskresult.TaskResult +import dev.usbharu.owl.broker.external.toUUID +import dev.usbharu.owl.broker.service.TaskManagementService +import dev.usbharu.owl.common.property.PropertySerializeUtils +import dev.usbharu.owl.common.property.PropertySerializerFactory +import io.grpc.Status +import io.grpc.StatusException +import kotlinx.coroutines.CancellationException +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.collect +import kotlinx.coroutines.flow.onEach +import org.slf4j.LoggerFactory +import java.util.* +import kotlin.coroutines.CoroutineContext +import kotlin.coroutines.EmptyCoroutineContext + +class TaskResultService( + coroutineContext: CoroutineContext = EmptyCoroutineContext, + private val taskManagementService: TaskManagementService, + private val propertySerializerFactory: PropertySerializerFactory +) : + TaskResultServiceGrpcKt.TaskResultServiceCoroutineImplBase(coroutineContext) { + override suspend fun tasKResult(requests: Flow): Empty { + try { + requests.onEach { + taskManagementService.queueProcessed( + TaskResult( + id = UUID.randomUUID(), + taskId = it.id.toUUID(), + success = it.success, + attempt = it.attempt, + result = PropertySerializeUtils.deserialize(propertySerializerFactory, it.resultMap), + message = it.message + ) + ) + }.collect() + } catch (e: CancellationException) { + throw e + } catch (e: Exception) { + logger.warn("Error while executing task results", e) + throw StatusException(Status.INTERNAL.withDescription("Error while executing task results").withCause(e)) + } + return Empty.getDefaultInstance() + } + + companion object { + private val logger = LoggerFactory.getLogger(TaskResultService::class.java) + } +} \ No newline at end of file diff --git a/owl/owl-broker/src/main/kotlin/dev/usbharu/owl/broker/interfaces/grpc/TaskResultSubscribeService.kt b/owl/owl-broker/src/main/kotlin/dev/usbharu/owl/broker/interfaces/grpc/TaskResultSubscribeService.kt new file mode 100644 index 00000000..e0635d9a --- /dev/null +++ b/owl/owl-broker/src/main/kotlin/dev/usbharu/owl/broker/interfaces/grpc/TaskResultSubscribeService.kt @@ -0,0 +1,56 @@ +/* + * Copyright (C) 2024 usbharu + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package dev.usbharu.owl.broker.interfaces.grpc + +import dev.usbharu.owl.* +import dev.usbharu.owl.broker.external.toUUID +import dev.usbharu.owl.broker.service.TaskManagementService +import dev.usbharu.owl.common.property.PropertySerializeUtils +import dev.usbharu.owl.common.property.PropertySerializerFactory +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.map +import kotlin.coroutines.CoroutineContext +import kotlin.coroutines.EmptyCoroutineContext + +class TaskResultSubscribeService( + private val taskManagementService: TaskManagementService, + private val propertySerializerFactory: PropertySerializerFactory, + coroutineContext: CoroutineContext = EmptyCoroutineContext +) : + TaskResultSubscribeServiceGrpcKt.TaskResultSubscribeServiceCoroutineImplBase(coroutineContext) { + override fun subscribe(request: Uuid.UUID): Flow { + return taskManagementService + .subscribeResult(request.toUUID()) + .map { + taskResults { + id = it.id.toUUID() + name = it.name + attempt = it.attempt + success = it.success + results.addAll(it.results.map { + taskResult { + id = it.taskId.toUUID() + success = it.success + attempt = it.attempt + result.putAll(PropertySerializeUtils.serialize(propertySerializerFactory, it.result)) + message = it.message + } + }) + } + } + } +} \ No newline at end of file diff --git a/owl/owl-broker/src/main/kotlin/dev/usbharu/owl/broker/service/AssignQueuedTaskDecider.kt b/owl/owl-broker/src/main/kotlin/dev/usbharu/owl/broker/service/AssignQueuedTaskDecider.kt new file mode 100644 index 00000000..44dfa36a --- /dev/null +++ b/owl/owl-broker/src/main/kotlin/dev/usbharu/owl/broker/service/AssignQueuedTaskDecider.kt @@ -0,0 +1,48 @@ +/* + * Copyright (C) 2024 usbharu + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package dev.usbharu.owl.broker.service + +import dev.usbharu.owl.broker.domain.exception.repository.RecordNotFoundException +import dev.usbharu.owl.broker.domain.model.consumer.ConsumerRepository +import dev.usbharu.owl.broker.domain.model.queuedtask.QueuedTask +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.emitAll +import kotlinx.coroutines.flow.flow +import kotlinx.coroutines.flow.take +import java.util.* +interface AssignQueuedTaskDecider { + fun findAssignableQueue(consumerId: UUID, numberOfConcurrent: Int): Flow +} +class AssignQueuedTaskDeciderImpl( + private val consumerRepository: ConsumerRepository, + private val queueStore: QueueStore +) : AssignQueuedTaskDecider { + override fun findAssignableQueue(consumerId: UUID, numberOfConcurrent: Int): Flow { + return flow { + val consumer = consumerRepository.findById(consumerId) + ?: throw RecordNotFoundException("Consumer not found. id: $consumerId") + emitAll( + queueStore.findByTaskNameInAndIsActiveIsTrueAndOrderByPriority( + consumer.tasks, + numberOfConcurrent + ).take(numberOfConcurrent) + ) + } + + } + +} \ No newline at end of file diff --git a/owl/owl-broker/src/main/kotlin/dev/usbharu/owl/broker/service/ConsumerService.kt b/owl/owl-broker/src/main/kotlin/dev/usbharu/owl/broker/service/ConsumerService.kt new file mode 100644 index 00000000..84b21095 --- /dev/null +++ b/owl/owl-broker/src/main/kotlin/dev/usbharu/owl/broker/service/ConsumerService.kt @@ -0,0 +1,60 @@ +/* + * Copyright (C) 2024 usbharu + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package dev.usbharu.owl.broker.service + +import dev.usbharu.owl.broker.domain.model.consumer.Consumer +import dev.usbharu.owl.broker.domain.model.consumer.ConsumerRepository +import org.slf4j.LoggerFactory +import java.util.* + +interface ConsumerService { + suspend fun registerConsumer(registerConsumerRequest: RegisterConsumerRequest): UUID +} + +class ConsumerServiceImpl(private val consumerRepository: ConsumerRepository) : ConsumerService { + override suspend fun registerConsumer(registerConsumerRequest: RegisterConsumerRequest): UUID { + val id = UUID.randomUUID() + + consumerRepository.save( + Consumer( + id, + registerConsumerRequest.name, + registerConsumerRequest.hostname, + registerConsumerRequest.tasks + ) + ) + + logger.info( + "Register a new Consumer. name: {} hostname: {} tasks: {}", + registerConsumerRequest.name, + registerConsumerRequest.hostname, + registerConsumerRequest.tasks.size + ) + + return id + } + + companion object { + private val logger = LoggerFactory.getLogger(ConsumerServiceImpl::class.java) + } +} + +data class RegisterConsumerRequest( + val name: String, + val hostname: String, + val tasks: List +) \ No newline at end of file diff --git a/owl/owl-broker/src/main/kotlin/dev/usbharu/owl/broker/service/DefaultPropertySerializerFactory.kt b/owl/owl-broker/src/main/kotlin/dev/usbharu/owl/broker/service/DefaultPropertySerializerFactory.kt new file mode 100644 index 00000000..0eed7b40 --- /dev/null +++ b/owl/owl-broker/src/main/kotlin/dev/usbharu/owl/broker/service/DefaultPropertySerializerFactory.kt @@ -0,0 +1,31 @@ +/* + * Copyright (C) 2024 usbharu + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package dev.usbharu.owl.broker.service + +import dev.usbharu.owl.common.property.* + +class DefaultPropertySerializerFactory : + CustomPropertySerializerFactory( + setOf( + IntegerPropertySerializer(), + StringPropertyValueSerializer(), + DoublePropertySerializer(), + BooleanPropertySerializer(), + LongPropertySerializer(), + FloatPropertySerializer(), + ) + ) \ No newline at end of file diff --git a/owl/owl-broker/src/main/kotlin/dev/usbharu/owl/broker/service/ProducerService.kt b/owl/owl-broker/src/main/kotlin/dev/usbharu/owl/broker/service/ProducerService.kt new file mode 100644 index 00000000..d781ba45 --- /dev/null +++ b/owl/owl-broker/src/main/kotlin/dev/usbharu/owl/broker/service/ProducerService.kt @@ -0,0 +1,57 @@ +/* + * Copyright (C) 2024 usbharu + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package dev.usbharu.owl.broker.service + +import dev.usbharu.owl.broker.domain.model.producer.Producer +import dev.usbharu.owl.broker.domain.model.producer.ProducerRepository +import org.slf4j.LoggerFactory +import java.time.Instant +import java.util.* + +interface ProducerService { + suspend fun registerProducer(producer: RegisterProducerRequest): UUID +} + + +class ProducerServiceImpl(private val producerRepository: ProducerRepository) : ProducerService { + override suspend fun registerProducer(producer: RegisterProducerRequest): UUID { + + val id = UUID.randomUUID() + + val saveProducer = Producer( + id = id, + name = producer.name, + hostname = producer.hostname, + registeredTask = emptyList(), + createdAt = Instant.now() + ) + + producerRepository.save(saveProducer) + + logger.info("Register a new Producer. name: {} hostname: {}", saveProducer.name, saveProducer.hostname) + return id + } + + companion object { + private val logger = LoggerFactory.getLogger(ProducerServiceImpl::class.java) + } +} + +data class RegisterProducerRequest( + val name: String, + val hostname: String +) \ No newline at end of file diff --git a/owl/owl-broker/src/main/kotlin/dev/usbharu/owl/broker/service/QueueScanner.kt b/owl/owl-broker/src/main/kotlin/dev/usbharu/owl/broker/service/QueueScanner.kt new file mode 100644 index 00000000..6368c93e --- /dev/null +++ b/owl/owl-broker/src/main/kotlin/dev/usbharu/owl/broker/service/QueueScanner.kt @@ -0,0 +1,47 @@ +/* + * Copyright (C) 2024 usbharu + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package dev.usbharu.owl.broker.service + +import dev.usbharu.owl.broker.domain.model.queuedtask.QueuedTask +import kotlinx.coroutines.currentCoroutineContext +import kotlinx.coroutines.delay +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.emitAll +import kotlinx.coroutines.flow.flow +import kotlinx.coroutines.isActive +import java.time.Instant + +interface QueueScanner { + fun startScan(): Flow +} + + +class QueueScannerImpl(private val queueStore: QueueStore) : QueueScanner { + override fun startScan(): Flow { + return flow { + while (currentCoroutineContext().isActive) { + emitAll(scanQueue()) + delay(1000) + } + } + } + + private fun scanQueue(): Flow { + return queueStore.findByQueuedAtBeforeAndIsActiveIsTrue(Instant.now().minusSeconds(10)) + } + +} \ No newline at end of file diff --git a/owl/owl-broker/src/main/kotlin/dev/usbharu/owl/broker/service/QueueStore.kt b/owl/owl-broker/src/main/kotlin/dev/usbharu/owl/broker/service/QueueStore.kt new file mode 100644 index 00000000..990e2b76 --- /dev/null +++ b/owl/owl-broker/src/main/kotlin/dev/usbharu/owl/broker/service/QueueStore.kt @@ -0,0 +1,64 @@ +/* + * Copyright (C) 2024 usbharu + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package dev.usbharu.owl.broker.service + +import dev.usbharu.owl.broker.domain.model.queuedtask.QueuedTask +import dev.usbharu.owl.broker.domain.model.queuedtask.QueuedTaskRepository +import kotlinx.coroutines.flow.Flow +import java.time.Instant + +interface QueueStore { + suspend fun enqueue(queuedTask: QueuedTask) + suspend fun enqueueAll(queuedTaskList: List) + + suspend fun dequeue(queuedTask: QueuedTask) + suspend fun dequeueAll(queuedTaskList: List) + fun findByTaskNameInAndIsActiveIsTrueAndOrderByPriority(tasks: List, limit: Int): Flow + + fun findByQueuedAtBeforeAndIsActiveIsTrue(instant: Instant): Flow +} + + +class QueueStoreImpl(private val queuedTaskRepository: QueuedTaskRepository) : QueueStore { + override suspend fun enqueue(queuedTask: QueuedTask) { + queuedTaskRepository.save(queuedTask) + } + + override suspend fun enqueueAll(queuedTaskList: List) { + queuedTaskList.forEach { enqueue(it) } + } + + override suspend fun dequeue(queuedTask: QueuedTask) { + queuedTaskRepository.findByTaskIdAndAssignedConsumerIsNullAndUpdate(queuedTask.task.id, queuedTask) + } + + override suspend fun dequeueAll(queuedTaskList: List) { + return queuedTaskList.forEach { dequeue(it) } + } + + override fun findByTaskNameInAndIsActiveIsTrueAndOrderByPriority( + tasks: List, + limit: Int + ): Flow { + return queuedTaskRepository.findByTaskNameInAndIsActiveIsTrueAndOrderByPriority(tasks, limit) + } + + override fun findByQueuedAtBeforeAndIsActiveIsTrue(instant: Instant): Flow { + return queuedTaskRepository.findByQueuedAtBeforeAndIsActiveIsTrue(instant) + } + +} \ No newline at end of file diff --git a/owl/owl-broker/src/main/kotlin/dev/usbharu/owl/broker/service/QueuedTaskAssigner.kt b/owl/owl-broker/src/main/kotlin/dev/usbharu/owl/broker/service/QueuedTaskAssigner.kt new file mode 100644 index 00000000..33c95413 --- /dev/null +++ b/owl/owl-broker/src/main/kotlin/dev/usbharu/owl/broker/service/QueuedTaskAssigner.kt @@ -0,0 +1,81 @@ +/* + * Copyright (C) 2024 usbharu + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package dev.usbharu.owl.broker.service + +import dev.usbharu.owl.broker.domain.exception.service.QueueCannotDequeueException +import dev.usbharu.owl.broker.domain.model.queuedtask.QueuedTask +import kotlinx.coroutines.flow.* +import org.slf4j.LoggerFactory +import java.time.Instant +import java.util.* + +interface QueuedTaskAssigner { + fun ready(consumerId: UUID, numberOfConcurrent: Int): Flow +} + + +class QueuedTaskAssignerImpl( + private val taskManagementService: TaskManagementService, + private val queueStore: QueueStore +) : QueuedTaskAssigner { + override fun ready(consumerId: UUID, numberOfConcurrent: Int): Flow { + return flow { + taskManagementService.findAssignableTask(consumerId, numberOfConcurrent) + .onEach { + val assignTask = assignTask(it, consumerId) + + if (assignTask != null) { + emit(assignTask) + } + } + .catch { logger.warn("Failed to assign task {}", consumerId, it) } + .collect() + } + } + + private suspend fun assignTask(queuedTask: QueuedTask, consumerId: UUID): QueuedTask? { + return try { + + val assignedTaskQueue = + queuedTask.copy(assignedConsumer = consumerId, assignedAt = Instant.now(), isActive = false) + logger.trace( + "Try assign task: {} id: {} consumer: {}", + queuedTask.task.name, + queuedTask.task.id, + consumerId + ) + + queueStore.dequeue(assignedTaskQueue) + + logger.debug( + "Assign Task. name: {} id: {} attempt: {} consumer: {}", + assignedTaskQueue.task.name, + assignedTaskQueue.task.id, + assignedTaskQueue.attempt, + assignedTaskQueue.assignedConsumer + ) + assignedTaskQueue + } catch (e: QueueCannotDequeueException) { + logger.debug("Failed dequeue queue", e) + return null + } + } + + companion object { + private val logger = LoggerFactory.getLogger(QueuedTaskAssignerImpl::class.java) + } +} \ No newline at end of file diff --git a/owl/owl-broker/src/main/kotlin/dev/usbharu/owl/broker/service/RegisterTaskService.kt b/owl/owl-broker/src/main/kotlin/dev/usbharu/owl/broker/service/RegisterTaskService.kt new file mode 100644 index 00000000..58945d42 --- /dev/null +++ b/owl/owl-broker/src/main/kotlin/dev/usbharu/owl/broker/service/RegisterTaskService.kt @@ -0,0 +1,56 @@ +/* + * Copyright (C) 2024 usbharu + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package dev.usbharu.owl.broker.service + +import dev.usbharu.owl.broker.domain.exception.service.IncompatibleTaskException +import dev.usbharu.owl.broker.domain.model.taskdefinition.TaskDefinition +import dev.usbharu.owl.broker.domain.model.taskdefinition.TaskDefinitionRepository +import org.slf4j.LoggerFactory + +interface RegisterTaskService { + suspend fun registerTask(taskDefinition: TaskDefinition) + + suspend fun unregisterTask(name:String) +} + + +class RegisterTaskServiceImpl(private val taskDefinitionRepository: TaskDefinitionRepository) : RegisterTaskService { + override suspend fun registerTask(taskDefinition: TaskDefinition) { + val definedTask = taskDefinitionRepository.findByName(taskDefinition.name) + if (definedTask != null) { + logger.debug("Task already defined. name: ${taskDefinition.name}") + if (taskDefinition.propertyDefinitionHash != definedTask.propertyDefinitionHash) { + throw IncompatibleTaskException("Task ${taskDefinition.name} has already been defined, and the parameters are incompatible.") + } + return + } + taskDefinitionRepository.save(taskDefinition) + + logger.info("Register a new task. name: {}",taskDefinition.name) + } + + // todo すでにpublish済みのタスクをどうするか決めさせる + override suspend fun unregisterTask(name: String) { + taskDefinitionRepository.deleteByName(name) + + logger.info("Unregister a task. name: {}",name) + } + + companion object{ + private val logger = LoggerFactory.getLogger(RegisterTaskServiceImpl::class.java) + } +} \ No newline at end of file diff --git a/owl/owl-broker/src/main/kotlin/dev/usbharu/owl/broker/service/TaskManagementService.kt b/owl/owl-broker/src/main/kotlin/dev/usbharu/owl/broker/service/TaskManagementService.kt new file mode 100644 index 00000000..29133704 --- /dev/null +++ b/owl/owl-broker/src/main/kotlin/dev/usbharu/owl/broker/service/TaskManagementService.kt @@ -0,0 +1,182 @@ +/* + * Copyright (C) 2024 usbharu + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package dev.usbharu.owl.broker.service + +import dev.usbharu.owl.broker.domain.exception.repository.RecordNotFoundException +import dev.usbharu.owl.broker.domain.exception.service.TaskNotRegisterException +import dev.usbharu.owl.broker.domain.model.queuedtask.QueuedTask +import dev.usbharu.owl.broker.domain.model.task.Task +import dev.usbharu.owl.broker.domain.model.task.TaskRepository +import dev.usbharu.owl.broker.domain.model.taskdefinition.TaskDefinitionRepository +import dev.usbharu.owl.broker.domain.model.taskresult.TaskResult +import dev.usbharu.owl.broker.domain.model.taskresult.TaskResultRepository +import dev.usbharu.owl.common.retry.RetryPolicyFactory +import kotlinx.coroutines.* +import kotlinx.coroutines.flow.* +import org.slf4j.LoggerFactory +import java.time.Instant +import java.util.* + + +interface TaskManagementService { + + suspend fun startManagement(coroutineScope: CoroutineScope) + fun findAssignableTask(consumerId: UUID, numberOfConcurrent: Int): Flow + + suspend fun queueProcessed(taskResult: TaskResult) + + fun subscribeResult(producerId: UUID): Flow +} + +class TaskManagementServiceImpl( + private val taskScanner: TaskScanner, + private val queueStore: QueueStore, + private val taskDefinitionRepository: TaskDefinitionRepository, + private val assignQueuedTaskDecider: AssignQueuedTaskDecider, + private val retryPolicyFactory: RetryPolicyFactory, + private val taskRepository: TaskRepository, + private val queueScanner: QueueScanner, + private val taskResultRepository: TaskResultRepository +) : TaskManagementService { + + private var taskFlow: Flow = flowOf() + private var queueFlow: Flow = flowOf() + override suspend fun startManagement(coroutineScope: CoroutineScope) { + taskFlow = taskScanner.startScan() + queueFlow = queueScanner.startScan() + + coroutineScope { + listOf( + launch { + taskFlow.onEach { + enqueueTask(it) + }.collect() + }, + launch { + queueFlow.onEach { + timeoutQueue(it) + }.collect() + } + ).joinAll() + } + } + + + override fun findAssignableTask(consumerId: UUID, numberOfConcurrent: Int): Flow { + return assignQueuedTaskDecider.findAssignableQueue(consumerId, numberOfConcurrent) + } + + private suspend fun enqueueTask(task: Task): QueuedTask { + + val definedTask = taskDefinitionRepository.findByName(task.name) + ?: throw TaskNotRegisterException("Task ${task.name} not definition.") + + val queuedTask = QueuedTask( + attempt = task.attempt + 1, + queuedAt = Instant.now(), + task = task, + priority = definedTask.priority, + isActive = true, + timeoutAt = null, + assignedConsumer = null, + assignedAt = null + ) + + val copy = task.copy( + nextRetry = retryPolicyFactory.factory(definedTask.retryPolicy) + .nextRetry(Instant.now(), queuedTask.attempt) + ) + + taskRepository.save(copy) + + queueStore.enqueue(queuedTask) + logger.debug("Enqueue Task. name: {} id: {} attempt: {}", task.name, task.id, queuedTask.attempt) + return queuedTask + } + + private suspend fun timeoutQueue(queuedTask: QueuedTask) { + val timeoutQueue = queuedTask.copy(isActive = false, timeoutAt = Instant.now()) + + queueStore.dequeue(timeoutQueue) + + + val task = taskRepository.findById(timeoutQueue.task.id) + ?: throw RecordNotFoundException("Task not found. id: ${timeoutQueue.task.id}") + val copy = task.copy(attempt = timeoutQueue.attempt) + + logger.warn( + "Queue timed out. name: {} id: {} attempt: {}", + timeoutQueue.task.name, + timeoutQueue.task.id, + timeoutQueue.attempt + ) + taskRepository.save(copy) + } + + override suspend fun queueProcessed(taskResult: TaskResult) { + val task = taskRepository.findById(taskResult.taskId) + ?: throw RecordNotFoundException("Task not found. id: ${taskResult.taskId}") + + val taskDefinition = taskDefinitionRepository.findByName(task.name) + ?: throw TaskNotRegisterException("Task ${task.name} not definition.") + + val completedAt = if (taskResult.success) { + Instant.now() + } else if (taskResult.attempt >= taskDefinition.maxRetry) { + Instant.now() + } else { + null + } + + taskResultRepository.save(taskResult) + + taskRepository.findByIdAndUpdate( + taskResult.taskId, + task.copy(completedAt = completedAt, attempt = taskResult.attempt) + ) + + } + + override fun subscribeResult(producerId: UUID): Flow { + return flow { + + while (currentCoroutineContext().isActive) { + taskRepository + .findByPublishProducerIdAndCompletedAtIsNotNull(producerId) + .onEach { + val results = taskResultRepository.findByTaskId(it.id).toList() + emit( + TaskResults( + it.name, + it.id, + results.any { it.success }, + it.attempt, + results + ) + ) + } + delay(500) + } + + } + + } + + companion object { + private val logger = LoggerFactory.getLogger(TaskManagementServiceImpl::class.java) + } +} \ No newline at end of file diff --git a/owl/owl-broker/src/main/kotlin/dev/usbharu/owl/broker/service/TaskPublishService.kt b/owl/owl-broker/src/main/kotlin/dev/usbharu/owl/broker/service/TaskPublishService.kt new file mode 100644 index 00000000..a6604d10 --- /dev/null +++ b/owl/owl-broker/src/main/kotlin/dev/usbharu/owl/broker/service/TaskPublishService.kt @@ -0,0 +1,114 @@ +/* + * Copyright (C) 2024 usbharu + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package dev.usbharu.owl.broker.service + +import dev.usbharu.owl.broker.domain.exception.service.TaskNotRegisterException +import dev.usbharu.owl.broker.domain.model.task.Task +import dev.usbharu.owl.broker.domain.model.task.TaskRepository +import dev.usbharu.owl.broker.domain.model.taskdefinition.TaskDefinitionRepository +import dev.usbharu.owl.common.property.PropertyValue +import dev.usbharu.owl.common.retry.RetryPolicyFactory +import org.slf4j.LoggerFactory +import java.time.Instant +import java.util.* + +interface TaskPublishService { + suspend fun publishTask(publishTask: PublishTask): PublishedTask + suspend fun publishTasks(list: List): List +} + +data class PublishTask( + val name: String, + val producerId: UUID, + val properties: Map> +) + +data class PublishedTask( + val name: String, + val id: UUID +) + +class TaskPublishServiceImpl( + private val taskRepository: TaskRepository, + private val taskDefinitionRepository: TaskDefinitionRepository, + private val retryPolicyFactory: RetryPolicyFactory +) : TaskPublishService { + override suspend fun publishTask(publishTask: PublishTask): PublishedTask { + val id = UUID.randomUUID() + + val definition = taskDefinitionRepository.findByName(publishTask.name) + ?: throw TaskNotRegisterException("Task ${publishTask.name} not definition.") + + val published = Instant.now() + val nextRetry = retryPolicyFactory.factory(definition.retryPolicy).nextRetry(published, 0) + + val task = Task( + name = publishTask.name, + id = id, + publishProducerId = publishTask.producerId, + publishedAt = published, + completedAt = null, + attempt = 0, + properties = publishTask.properties, + nextRetry = nextRetry + ) + + taskRepository.save(task) + + logger.debug("Published task #{} name: {}", task.id, task.name) + + return PublishedTask( + name = publishTask.name, + id = id + ) + } + + override suspend fun publishTasks(list: List): List { + + val first = list.first() + + val definition = taskDefinitionRepository.findByName(first.name) + ?: throw TaskNotRegisterException("Task ${first.name} not definition.") + + val published = Instant.now() + + val nextRetry = retryPolicyFactory.factory(definition.retryPolicy).nextRetry(published, 0) + + val tasks = list.map { + Task( + it.name, + UUID.randomUUID(), + first.producerId, + published, + nextRetry, + null, + 0, + it.properties + ) + } + + taskRepository.saveAll(tasks) + + logger.debug("Published {} tasks. name: {}", tasks.size, first.name) + + return tasks.map { PublishedTask(it.name, it.id) } + } + + companion object { + private val logger = LoggerFactory.getLogger(TaskPublishServiceImpl::class.java) + } +} \ No newline at end of file diff --git a/owl/owl-broker/src/main/kotlin/dev/usbharu/owl/broker/service/TaskResults.kt b/owl/owl-broker/src/main/kotlin/dev/usbharu/owl/broker/service/TaskResults.kt new file mode 100644 index 00000000..a61dbb0c --- /dev/null +++ b/owl/owl-broker/src/main/kotlin/dev/usbharu/owl/broker/service/TaskResults.kt @@ -0,0 +1,28 @@ +/* + * Copyright (C) 2024 usbharu + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package dev.usbharu.owl.broker.service + +import dev.usbharu.owl.broker.domain.model.taskresult.TaskResult +import java.util.* + +data class TaskResults( + val name:String, + val id:UUID, + val success:Boolean, + val attempt:Int, + val results: List +) diff --git a/owl/owl-broker/src/main/kotlin/dev/usbharu/owl/broker/service/TaskScanner.kt b/owl/owl-broker/src/main/kotlin/dev/usbharu/owl/broker/service/TaskScanner.kt new file mode 100644 index 00000000..5d469533 --- /dev/null +++ b/owl/owl-broker/src/main/kotlin/dev/usbharu/owl/broker/service/TaskScanner.kt @@ -0,0 +1,52 @@ +/* + * Copyright (C) 2024 usbharu + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package dev.usbharu.owl.broker.service + +import dev.usbharu.owl.broker.domain.model.task.Task +import dev.usbharu.owl.broker.domain.model.task.TaskRepository +import kotlinx.coroutines.currentCoroutineContext +import kotlinx.coroutines.delay +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.emitAll +import kotlinx.coroutines.flow.flow +import kotlinx.coroutines.isActive +import org.slf4j.LoggerFactory +import java.time.Instant + +interface TaskScanner { + + fun startScan(): Flow +} + +class TaskScannerImpl(private val taskRepository: TaskRepository) : + TaskScanner { + + override fun startScan(): Flow = flow { + while (currentCoroutineContext().isActive) { + emitAll(scanTask()) + delay(500) + } + } + + private fun scanTask(): Flow { + return taskRepository.findByNextRetryBeforeAndCompletedAtIsNull(Instant.now()) + } + + companion object { + private val logger = LoggerFactory.getLogger(TaskScannerImpl::class.java) + } +} \ No newline at end of file diff --git a/owl/owl-broker/src/main/proto/consumer.proto b/owl/owl-broker/src/main/proto/consumer.proto new file mode 100644 index 00000000..252d4a27 --- /dev/null +++ b/owl/owl-broker/src/main/proto/consumer.proto @@ -0,0 +1,18 @@ +syntax = "proto3"; +import "uuid.proto"; + +option java_package = "dev.usbharu.owl"; + +message SubscribeTaskRequest { + string name = 1; + string hostname = 2; + repeated string tasks = 3;; +} + +message SubscribeTaskResponse { + UUID id = 1; +} + +service SubscribeTaskService { + rpc SubscribeTask (SubscribeTaskRequest) returns (SubscribeTaskResponse); +} \ No newline at end of file diff --git a/owl/owl-broker/src/main/proto/definition_task.proto b/owl/owl-broker/src/main/proto/definition_task.proto new file mode 100644 index 00000000..6cab5257 --- /dev/null +++ b/owl/owl-broker/src/main/proto/definition_task.proto @@ -0,0 +1,31 @@ +syntax = "proto3"; + +option java_package = "dev.usbharu.owl"; + +import "google/protobuf/empty.proto"; +import "uuid.proto"; + + +message TaskDefinition { + string name = 1; + int32 priority = 2; + int32 max_retry = 3; + int64 timeout_milli = 4; + int64 property_definition_hash = 5; + UUID producer_id = 6; + string retryPolicy = 7; +} + +message TaskDefined { + string task_id = 1; +} + +message TaskUnregister { + string name = 1; + UUID producer_id = 2; +} + +service DefinitionTaskService { + rpc register(TaskDefinition) returns (TaskDefined); + rpc unregister(TaskUnregister) returns (google.protobuf.Empty); +} \ No newline at end of file diff --git a/owl/owl-broker/src/main/proto/producer.proto b/owl/owl-broker/src/main/proto/producer.proto new file mode 100644 index 00000000..b4bbcd7a --- /dev/null +++ b/owl/owl-broker/src/main/proto/producer.proto @@ -0,0 +1,18 @@ +syntax = "proto3"; + +import "uuid.proto"; + +option java_package = "dev.usbharu.owl"; + +message Producer { + string name = 1; + string hostname = 2; +} + +message RegisterProducerResponse { + UUID id = 1; +} + +service ProducerService { + rpc registerProducer (Producer) returns (RegisterProducerResponse); +} \ No newline at end of file diff --git a/owl/owl-broker/src/main/proto/property.proto b/owl/owl-broker/src/main/proto/property.proto new file mode 100644 index 00000000..138e7e22 --- /dev/null +++ b/owl/owl-broker/src/main/proto/property.proto @@ -0,0 +1,13 @@ +syntax = "proto3"; + +import "google/protobuf/empty.proto"; + +option java_package = "dev.usbharu.owl"; + +message Property{ + oneof value { + google.protobuf.Empty empty = 1; + string string = 2; + int32 integer = 3; + } +} \ No newline at end of file diff --git a/owl/owl-broker/src/main/proto/publish_task.proto b/owl/owl-broker/src/main/proto/publish_task.proto new file mode 100644 index 00000000..620e6396 --- /dev/null +++ b/owl/owl-broker/src/main/proto/publish_task.proto @@ -0,0 +1,41 @@ +syntax = "proto3"; + +import "google/protobuf/timestamp.proto"; + +import "uuid.proto"; + +option java_package = "dev.usbharu.owl"; + + +message PublishTask { + string name = 1; + google.protobuf.Timestamp publishedAt = 2; + map properties = 3; + UUID producer_id = 4; +} + +message Properties { + map properties = 1; +} + +message PublishTasks { + string name = 1; + google.protobuf.Timestamp publishedAt = 2; + repeated Properties propertiesArray = 3; + UUID producer_id = 4; +} + +message PublishedTask { + string name = 1; + UUID id = 2; +} + +message PublishedTasks { + string name = 1; + repeated UUID id = 2; +} + +service TaskPublishService { + rpc publishTask (PublishTask) returns (PublishedTask); + rpc publishTasks(PublishTasks) returns (PublishedTasks); +} diff --git a/owl/owl-broker/src/main/proto/task.proto b/owl/owl-broker/src/main/proto/task.proto new file mode 100644 index 00000000..48566fdb --- /dev/null +++ b/owl/owl-broker/src/main/proto/task.proto @@ -0,0 +1,23 @@ +syntax = "proto3"; +import "uuid.proto"; +import "google/protobuf/timestamp.proto"; +import "property.proto"; + +option java_package = "dev.usbharu.owl"; + +message ReadyRequest { + int32 number_of_concurrent = 1; + UUID consumer_id = 2; +} + +message TaskRequest { + string name = 1; + UUID id = 2; + int32 attempt = 4; + google.protobuf.Timestamp queuedAt = 5; + map properties = 6; +} + +service AssignmentTaskService { + rpc ready (stream ReadyRequest) returns (stream TaskRequest); +} \ No newline at end of file diff --git a/owl/owl-broker/src/main/proto/task_result.proto b/owl/owl-broker/src/main/proto/task_result.proto new file mode 100644 index 00000000..642f7425 --- /dev/null +++ b/owl/owl-broker/src/main/proto/task_result.proto @@ -0,0 +1,18 @@ +syntax = "proto3"; +import "uuid.proto"; +import "google/protobuf/empty.proto"; +import "property.proto"; + +option java_package = "dev.usbharu.owl"; + +message TaskResult { + UUID id = 1; + bool success = 2; + int32 attempt = 3; + map result = 4; + string message = 5; +} + +service TaskResultService{ + rpc tasKResult(stream TaskResult) returns (google.protobuf.Empty); +} \ No newline at end of file diff --git a/owl/owl-broker/src/main/proto/task_result_producer.proto b/owl/owl-broker/src/main/proto/task_result_producer.proto new file mode 100644 index 00000000..6102a020 --- /dev/null +++ b/owl/owl-broker/src/main/proto/task_result_producer.proto @@ -0,0 +1,17 @@ +syntax = "proto3"; +import "uuid.proto"; +import "task_result.proto"; + +option java_package = "dev.usbharu.owl"; + +message TaskResults { + string name = 1; + UUID id = 2; + bool success = 3; + int32 attempt = 4; + repeated TaskResult results = 5; +} + +service TaskResultSubscribeService { + rpc subscribe(UUID) returns (stream TaskResults); +} \ No newline at end of file diff --git a/owl/owl-broker/src/main/proto/uuid.proto b/owl/owl-broker/src/main/proto/uuid.proto new file mode 100644 index 00000000..26d61001 --- /dev/null +++ b/owl/owl-broker/src/main/proto/uuid.proto @@ -0,0 +1,8 @@ +syntax = "proto3"; + +option java_package = "dev.usbharu.owl"; + +message UUID { + uint64 most_significant_uuid_bits = 1; + uint64 least_significant_uuid_bits = 2; +} \ No newline at end of file diff --git a/owl/owl-broker/src/main/resources/log4j2.xml b/owl/owl-broker/src/main/resources/log4j2.xml new file mode 100644 index 00000000..31bfafec --- /dev/null +++ b/owl/owl-broker/src/main/resources/log4j2.xml @@ -0,0 +1,40 @@ + + + + + + + %d{yyyy/MM/dd HH:mm:ss.SSS} [%t] %-6p %c{10} | %m%n + + + + + ${format1} + + + + + + + + + + + + + + \ No newline at end of file diff --git a/owl/owl-common/build.gradle.kts b/owl/owl-common/build.gradle.kts new file mode 100644 index 00000000..b2237941 --- /dev/null +++ b/owl/owl-common/build.gradle.kts @@ -0,0 +1,21 @@ +plugins { + alias(libs.plugins.kotlin.jvm) +} + +group = "dev.usbharu" +version = "1.0-SNAPSHOT" + +repositories { + mavenCentral() +} + +dependencies { + testImplementation("org.jetbrains.kotlin:kotlin-test") +} + +tasks.test { + useJUnitPlatform() +} +kotlin { + jvmToolchain(21) +} \ No newline at end of file diff --git a/owl/owl-common/owl-common-serialize-jackson/build.gradle.kts b/owl/owl-common/owl-common-serialize-jackson/build.gradle.kts new file mode 100644 index 00000000..fc51369e --- /dev/null +++ b/owl/owl-common/owl-common-serialize-jackson/build.gradle.kts @@ -0,0 +1,23 @@ +plugins { + alias(libs.plugins.kotlin.jvm) +} + +group = "dev.usbharu" +version = "0.0.1" + +repositories { + mavenCentral() +} + +dependencies { + implementation(project(":owl-common")) + testImplementation(kotlin("test")) + implementation(libs.bundles.jackson) +} + +tasks.test { + useJUnitPlatform() +} +kotlin { + jvmToolchain(21) +} \ No newline at end of file diff --git a/owl/owl-common/owl-common-serialize-jackson/src/main/kotlin/dev/usbharu/Main.kt b/owl/owl-common/owl-common-serialize-jackson/src/main/kotlin/dev/usbharu/Main.kt new file mode 100644 index 00000000..64eb428e --- /dev/null +++ b/owl/owl-common/owl-common-serialize-jackson/src/main/kotlin/dev/usbharu/Main.kt @@ -0,0 +1,21 @@ +/* + * Copyright (C) 2024 usbharu + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package dev.usbharu + +fun main() { + println("Hello World!") +} \ No newline at end of file diff --git a/owl/owl-common/owl-common-serialize-jackson/src/main/kotlin/dev/usbharu/owl/common/property/ObjectPropertyValue.kt b/owl/owl-common/owl-common-serialize-jackson/src/main/kotlin/dev/usbharu/owl/common/property/ObjectPropertyValue.kt new file mode 100644 index 00000000..6583d4f1 --- /dev/null +++ b/owl/owl-common/owl-common-serialize-jackson/src/main/kotlin/dev/usbharu/owl/common/property/ObjectPropertyValue.kt @@ -0,0 +1,50 @@ +/* + * Copyright (C) 2024 usbharu + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package dev.usbharu.owl.common.property + +import com.fasterxml.jackson.databind.ObjectMapper + +class ObjectPropertyValue(override val value: Any) : PropertyValue() { + override val type: PropertyType + get() = PropertyType.string +} + +class ObjectPropertySerializer(private val objectMapper: ObjectMapper) : PropertySerializer { + override fun isSupported(propertyValue: PropertyValue<*>): Boolean { + return propertyValue is ObjectPropertyValue + } + + override fun isSupported(string: String): Boolean { + return string.startsWith("jackson:") + } + + override fun serialize(propertyValue: PropertyValue<*>): String { + return "jackson:" + propertyValue.value!!::class.qualifiedName + ":" + objectMapper.writeValueAsString( + propertyValue.value + ) + } + + override fun deserialize(string: String): PropertyValue { + return ObjectPropertyValue( + objectMapper.readValue( + string.substringAfter("jackson:").substringAfter(":"), + Class.forName(string.substringAfter("jackson:").substringBefore(":")) + ) + ) + + } +} \ No newline at end of file diff --git a/owl/owl-common/src/main/kotlin/dev/usbharu/owl/common/ReflectionUtils.kt b/owl/owl-common/src/main/kotlin/dev/usbharu/owl/common/ReflectionUtils.kt new file mode 100644 index 00000000..f0a6ff90 --- /dev/null +++ b/owl/owl-common/src/main/kotlin/dev/usbharu/owl/common/ReflectionUtils.kt @@ -0,0 +1,26 @@ +/* + * Copyright (C) 2024 usbharu + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package dev.usbharu.owl.common + +import java.lang.reflect.Field + +val Class<*>.allFields: List + get() = if (superclass != null) { + superclass.allFields + declaredFields + } else { + declaredFields.toList() + }.map { it.trySetAccessible();it } \ No newline at end of file diff --git a/owl/owl-common/src/main/kotlin/dev/usbharu/owl/common/property/BooleanPropertyValue.kt b/owl/owl-common/src/main/kotlin/dev/usbharu/owl/common/property/BooleanPropertyValue.kt new file mode 100644 index 00000000..b595f31c --- /dev/null +++ b/owl/owl-common/src/main/kotlin/dev/usbharu/owl/common/property/BooleanPropertyValue.kt @@ -0,0 +1,33 @@ +package dev.usbharu.owl.common.property + +/** + * Boolean型のプロパティ + * + * @property value プロパティ + */ +class BooleanPropertyValue(override val value: Boolean) : PropertyValue() { + override val type: PropertyType + get() = PropertyType.binary +} + +/** + * [BooleanPropertyValue]のシリアライザー + * + */ +class BooleanPropertySerializer : PropertySerializer { + override fun isSupported(propertyValue: PropertyValue<*>): Boolean { + return propertyValue.value is Boolean + } + + override fun isSupported(string: String): Boolean { + return string.startsWith("bool:") + } + + override fun serialize(propertyValue: PropertyValue<*>): String { + return "bool:" + propertyValue.value.toString() + } + + override fun deserialize(string: String): PropertyValue { + return BooleanPropertyValue(string.replace("bool:", "").toBoolean()) + } +} \ No newline at end of file diff --git a/owl/owl-common/src/main/kotlin/dev/usbharu/owl/common/property/CustomPropertySerializerFactory.kt b/owl/owl-common/src/main/kotlin/dev/usbharu/owl/common/property/CustomPropertySerializerFactory.kt new file mode 100644 index 00000000..00d7a3f3 --- /dev/null +++ b/owl/owl-common/src/main/kotlin/dev/usbharu/owl/common/property/CustomPropertySerializerFactory.kt @@ -0,0 +1,34 @@ +/* + * Copyright (C) 2024 usbharu + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package dev.usbharu.owl.common.property + +/** + * [Set]でカスタマイズできる[PropertySerializerFactory] + * + * @property propertySerializers [PropertySerializer]の[Set] + */ +open class CustomPropertySerializerFactory(private val propertySerializers: Set>) : + PropertySerializerFactory { + override fun factory(propertyValue: PropertyValue): PropertySerializer { + return propertySerializers.firstOrNull { it.isSupported(propertyValue) } as PropertySerializer? + ?: throw IllegalArgumentException("PropertySerializer not found: $propertyValue") + } + + override fun factory(string: String): PropertySerializer<*> { + return propertySerializers.first { it.isSupported(string) } + } +} \ No newline at end of file diff --git a/owl/owl-common/src/main/kotlin/dev/usbharu/owl/common/property/DoublePropertyValue.kt b/owl/owl-common/src/main/kotlin/dev/usbharu/owl/common/property/DoublePropertyValue.kt new file mode 100644 index 00000000..c201cbaa --- /dev/null +++ b/owl/owl-common/src/main/kotlin/dev/usbharu/owl/common/property/DoublePropertyValue.kt @@ -0,0 +1,33 @@ +package dev.usbharu.owl.common.property + +/** + * Double型のプロパティ + * + * @property value プロパティ + */ +class DoublePropertyValue(override val value: Double) : PropertyValue() { + override val type: PropertyType + get() = PropertyType.number +} + +/** + * [DoublePropertyValue]のシリアライザー + * + */ +class DoublePropertySerializer : PropertySerializer { + override fun isSupported(propertyValue: PropertyValue<*>): Boolean { + return propertyValue.value is Double + } + + override fun isSupported(string: String): Boolean { + return string.startsWith("double:") + } + + override fun serialize(propertyValue: PropertyValue<*>): String { + return "double:" + propertyValue.value.toString() + } + + override fun deserialize(string: String): PropertyValue { + return DoublePropertyValue(string.replace("double:", "").toDouble()) + } +} \ No newline at end of file diff --git a/owl/owl-common/src/main/kotlin/dev/usbharu/owl/common/property/FloatPropertyValue.kt b/owl/owl-common/src/main/kotlin/dev/usbharu/owl/common/property/FloatPropertyValue.kt new file mode 100644 index 00000000..0f18c832 --- /dev/null +++ b/owl/owl-common/src/main/kotlin/dev/usbharu/owl/common/property/FloatPropertyValue.kt @@ -0,0 +1,44 @@ +/* + * Copyright (C) 2024 usbharu + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package dev.usbharu.owl.common.property + +class FloatPropertyValue(override val value: Float) : PropertyValue() { + override val type: PropertyType + get() = PropertyType.number +} + +/** + * [FloatPropertyValue]のシリアライザー + * + */ +class FloatPropertySerializer : PropertySerializer { + override fun isSupported(propertyValue: PropertyValue<*>): Boolean { + return propertyValue.value is Float + } + + override fun isSupported(string: String): Boolean { + return string.startsWith("float:") + } + + override fun serialize(propertyValue: PropertyValue<*>): String { + return "float:" + propertyValue.value.toString() + } + + override fun deserialize(string: String): PropertyValue { + return FloatPropertyValue(string.replace("float:", "").toFloat()) + } +} \ No newline at end of file diff --git a/owl/owl-common/src/main/kotlin/dev/usbharu/owl/common/property/IntegerPropertyValue.kt b/owl/owl-common/src/main/kotlin/dev/usbharu/owl/common/property/IntegerPropertyValue.kt new file mode 100644 index 00000000..9b49c39c --- /dev/null +++ b/owl/owl-common/src/main/kotlin/dev/usbharu/owl/common/property/IntegerPropertyValue.kt @@ -0,0 +1,49 @@ +/* + * Copyright (C) 2024 usbharu + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package dev.usbharu.owl.common.property + +/** + * Integer型のプロパティ + * + * @property value プロパティ + */ +class IntegerPropertyValue(override val value: Int) : PropertyValue() { + override val type: PropertyType + get() = PropertyType.number +} + +/** + * [IntegerPropertyValue]のシリアライザー + * + */ +class IntegerPropertySerializer : PropertySerializer { + override fun isSupported(propertyValue: PropertyValue<*>): Boolean { + return propertyValue.value is Int + } + + override fun isSupported(string: String): Boolean { + return string.startsWith("int32:") + } + + override fun serialize(propertyValue: PropertyValue<*>): String { + return "int32:" + propertyValue.value.toString() + } + + override fun deserialize(string: String): PropertyValue { + return IntegerPropertyValue(string.replace("int32:", "").toInt()) + } +} \ No newline at end of file diff --git a/owl/owl-common/src/main/kotlin/dev/usbharu/owl/common/property/LongPropertyValue.kt b/owl/owl-common/src/main/kotlin/dev/usbharu/owl/common/property/LongPropertyValue.kt new file mode 100644 index 00000000..660b0b69 --- /dev/null +++ b/owl/owl-common/src/main/kotlin/dev/usbharu/owl/common/property/LongPropertyValue.kt @@ -0,0 +1,45 @@ +/* + * Copyright (C) 2024 usbharu + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package dev.usbharu.owl.common.property + +class LongPropertyValue(override val value: Long) : PropertyValue() { + + override val type: PropertyType + get() = PropertyType.number +} + +/** + * [LongPropertyValue]のシリアライザー + * + */ +class LongPropertySerializer : PropertySerializer { + override fun isSupported(propertyValue: PropertyValue<*>): Boolean { + return propertyValue.value is Long + } + + override fun isSupported(string: String): Boolean { + return string.startsWith("int64:") + } + + override fun serialize(propertyValue: PropertyValue<*>): String { + return "int64:" + propertyValue.value.toString() + } + + override fun deserialize(string: String): PropertyValue { + return LongPropertyValue(string.replace("int64:", "").toLong()) + } +} \ No newline at end of file diff --git a/owl/owl-common/src/main/kotlin/dev/usbharu/owl/common/property/PropertySerializeException.kt b/owl/owl-common/src/main/kotlin/dev/usbharu/owl/common/property/PropertySerializeException.kt new file mode 100644 index 00000000..4acc597d --- /dev/null +++ b/owl/owl-common/src/main/kotlin/dev/usbharu/owl/common/property/PropertySerializeException.kt @@ -0,0 +1,30 @@ +/* + * Copyright (C) 2024 usbharu + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package dev.usbharu.owl.common.property + +class PropertySerializeException : 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 + ) +} \ No newline at end of file diff --git a/owl/owl-common/src/main/kotlin/dev/usbharu/owl/common/property/PropertySerializeUtils.kt b/owl/owl-common/src/main/kotlin/dev/usbharu/owl/common/property/PropertySerializeUtils.kt new file mode 100644 index 00000000..817f3d24 --- /dev/null +++ b/owl/owl-common/src/main/kotlin/dev/usbharu/owl/common/property/PropertySerializeUtils.kt @@ -0,0 +1,62 @@ +/* + * Copyright (C) 2024 usbharu + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package dev.usbharu.owl.common.property + +/** + * [PropertySerializer]のユーティリティークラス + */ +object PropertySerializeUtils { + /** + * Stringと[PropertyValue]の[Map]から[PropertyValue]をシリアライズし、StringとStringの[Map]として返します + * + * @param serializerFactory シリアライズに使用する[PropertySerializerFactory] + * @param properties シリアライズする[Map] + * @return Stringとシリアライズ済みの[PropertyValue]の[Map] + */ + fun serialize( + serializerFactory: PropertySerializerFactory, + properties: Map>, + ): Map { + return properties.map { + try { + it.key to serializerFactory.factory(it.value).serialize(it.value) + } catch (e: Exception) { + throw PropertySerializeException("Failed to serialize property in ${serializerFactory.javaClass}", e) + } + }.toMap() + } + + /** + * Stringとシリアライズ済みの[PropertyValue]の[Map]からシリアライズ済みの[PropertyValue]をデシリアライズし、Stringと[PropertyValue]の[Map]として返します + * + * @param serializerFactory デシリアライズに使用する[PropertySerializerFactory] + * @param properties デシリアライズする[Map] + * @return Stringと[PropertyValue]の[Map] + */ + fun deserialize( + serializerFactory: PropertySerializerFactory, + properties: Map, + ): Map> { + return properties.map { + try { + it.key to serializerFactory.factory(it.value).deserialize(it.value) + } catch (e: Exception) { + throw PropertySerializeException("Failed to deserialize property in ${serializerFactory.javaClass}", e) + } + }.toMap() + } +} \ No newline at end of file diff --git a/owl/owl-common/src/main/kotlin/dev/usbharu/owl/common/property/PropertySerializer.kt b/owl/owl-common/src/main/kotlin/dev/usbharu/owl/common/property/PropertySerializer.kt new file mode 100644 index 00000000..950bd9f4 --- /dev/null +++ b/owl/owl-common/src/main/kotlin/dev/usbharu/owl/common/property/PropertySerializer.kt @@ -0,0 +1,56 @@ +/* + * Copyright (C) 2024 usbharu + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package dev.usbharu.owl.common.property + +/** + * [PropertyValue]をシリアライズ・デシリアライズします + * + * @param T [PropertyValue]の型 + */ +interface PropertySerializer { + /** + * [PropertyValue]をサポートしているかを確認します + * + * @param propertyValue 確認する[PropertyValue] + * @return サポートしている場合true + */ + fun isSupported(propertyValue: PropertyValue<*>): Boolean + + /** + * シリアライズ済みの[PropertyValue]から[PropertyValue]をサポートしているかを確認します + * + * @param string 確認するシリアライズ済みの[PropertyValue] + * @return サポートしている場合true + */ + fun isSupported(string: String): Boolean + + /** + * [PropertyValue]をシリアライズします + * + * @param propertyValue シリアライズする[PropertyValue] + * @return シリアライズ済みの[PropertyValue] + */ + fun serialize(propertyValue: PropertyValue<*>): String + + /** + * デシリアライズします + * + * @param string シリアライズ済みの[PropertyValue] + * @return デシリアライズされた[PropertyValue] + */ + fun deserialize(string: String): PropertyValue +} \ No newline at end of file diff --git a/owl/owl-common/src/main/kotlin/dev/usbharu/owl/common/property/PropertySerializerFactory.kt b/owl/owl-common/src/main/kotlin/dev/usbharu/owl/common/property/PropertySerializerFactory.kt new file mode 100644 index 00000000..9983d6cf --- /dev/null +++ b/owl/owl-common/src/main/kotlin/dev/usbharu/owl/common/property/PropertySerializerFactory.kt @@ -0,0 +1,40 @@ +/* + * Copyright (C) 2024 usbharu + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package dev.usbharu.owl.common.property + +/** + * [PropertyValue]のシリアライザーのファクトリ + * + */ +interface PropertySerializerFactory { + /** + * [PropertyValue]からシリアライザーを作成します + * + * @param T [PropertyValue]の型 + * @param propertyValue シリアライザーを作成する[PropertyValue] + * @return 作成されたシリアライザー + */ + fun factory(propertyValue: PropertyValue): PropertySerializer + + /** + * シリアライズ済みの[PropertyValue]からシリアライザーを作成します + * + * @param string シリアライズ済みの[PropertyValue] + * @return 作成されたシリアライザー + */ + fun factory(string: String): PropertySerializer<*> +} \ No newline at end of file diff --git a/owl/owl-common/src/main/kotlin/dev/usbharu/owl/common/property/PropertyType.kt b/owl/owl-common/src/main/kotlin/dev/usbharu/owl/common/property/PropertyType.kt new file mode 100644 index 00000000..4259c62f --- /dev/null +++ b/owl/owl-common/src/main/kotlin/dev/usbharu/owl/common/property/PropertyType.kt @@ -0,0 +1,41 @@ +/* + * Copyright (C) 2024 usbharu + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package dev.usbharu.owl.common.property + +/** + * プロパティの型 + * + */ +enum class PropertyType { + /** + * 数字 + * + */ + number, + + /** + * 文字列 + * + */ + string, + + /** + * バイナリ + * + */ + binary +} \ No newline at end of file diff --git a/owl/owl-common/src/main/kotlin/dev/usbharu/owl/common/property/PropertyValue.kt b/owl/owl-common/src/main/kotlin/dev/usbharu/owl/common/property/PropertyValue.kt new file mode 100644 index 00000000..d04ca229 --- /dev/null +++ b/owl/owl-common/src/main/kotlin/dev/usbharu/owl/common/property/PropertyValue.kt @@ -0,0 +1,39 @@ +/* + * Copyright (C) 2024 usbharu + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package dev.usbharu.owl.common.property + +/** + * プロパティで使用される値 + * + * @param T プロパティの型 + */ +abstract class PropertyValue { + /** + * プロパティ + */ + abstract val value: T + + /** + * プロパティの型 + */ + abstract val type: PropertyType + override fun toString(): String { + return "PropertyValue(value=$value, type=$type)" + } + + +} \ No newline at end of file diff --git a/owl/owl-common/src/main/kotlin/dev/usbharu/owl/common/property/StringPropertyValue.kt b/owl/owl-common/src/main/kotlin/dev/usbharu/owl/common/property/StringPropertyValue.kt new file mode 100644 index 00000000..5b4cbaa1 --- /dev/null +++ b/owl/owl-common/src/main/kotlin/dev/usbharu/owl/common/property/StringPropertyValue.kt @@ -0,0 +1,33 @@ +package dev.usbharu.owl.common.property + +/** + * String型のプロパティ + * + * @property value プロパティ + */ +class StringPropertyValue(override val value: String) : PropertyValue() { + override val type: PropertyType + get() = PropertyType.string +} + +/** + * [StringPropertyValue]のシリアライザー + * + */ +class StringPropertyValueSerializer : PropertySerializer { + override fun isSupported(propertyValue: PropertyValue<*>): Boolean { + return propertyValue.value is String + } + + override fun isSupported(string: String): Boolean { + return string.startsWith("str:") + } + + override fun serialize(propertyValue: PropertyValue<*>): String { + return "str:" + propertyValue.value + } + + override fun deserialize(string: String): PropertyValue { + return StringPropertyValue(string.replace("str:", "")) + } +} \ No newline at end of file diff --git a/owl/owl-common/src/main/kotlin/dev/usbharu/owl/common/retry/ExponentialRetryPolicy.kt b/owl/owl-common/src/main/kotlin/dev/usbharu/owl/common/retry/ExponentialRetryPolicy.kt new file mode 100644 index 00000000..b34cacf5 --- /dev/null +++ b/owl/owl-common/src/main/kotlin/dev/usbharu/owl/common/retry/ExponentialRetryPolicy.kt @@ -0,0 +1,17 @@ +package dev.usbharu.owl.common.retry + +import java.time.Instant +import kotlin.math.pow +import kotlin.math.roundToLong + +/** + * 指数関数的に待機時間が増えるリトライポリシー + * `firstRetrySeconds x attempt ^ 2 - firstRetrySeconds` + * + * @property firstRetrySeconds + */ +class ExponentialRetryPolicy(private val firstRetrySeconds: Int = 30) : RetryPolicy { + override fun nextRetry(now: Instant, attempt: Int): Instant = + now.plusSeconds(firstRetrySeconds.times((2.0).pow(attempt).roundToLong()) - firstRetrySeconds) + +} \ No newline at end of file diff --git a/owl/owl-common/src/main/kotlin/dev/usbharu/owl/common/retry/RetryPolicy.kt b/owl/owl-common/src/main/kotlin/dev/usbharu/owl/common/retry/RetryPolicy.kt new file mode 100644 index 00000000..da6e4ec0 --- /dev/null +++ b/owl/owl-common/src/main/kotlin/dev/usbharu/owl/common/retry/RetryPolicy.kt @@ -0,0 +1,36 @@ +/* + * Copyright (C) 2024 usbharu + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package dev.usbharu.owl.common.retry + +import java.time.Instant + +/** + * リトライポリシー + * + */ +interface RetryPolicy { + /** + * 次のリトライ時刻を返します。 + * + * [attempt]を負の値にしてはいけません + * + * @param now 現在の時刻 + * @param attempt 試行回数 + * @return 次のリトライ時刻 + */ + fun nextRetry(now: Instant, attempt: Int): Instant +} \ No newline at end of file diff --git a/owl/owl-common/src/main/kotlin/dev/usbharu/owl/common/retry/RetryPolicyFactory.kt b/owl/owl-common/src/main/kotlin/dev/usbharu/owl/common/retry/RetryPolicyFactory.kt new file mode 100644 index 00000000..6948e7bf --- /dev/null +++ b/owl/owl-common/src/main/kotlin/dev/usbharu/owl/common/retry/RetryPolicyFactory.kt @@ -0,0 +1,39 @@ +/* + * Copyright (C) 2024 usbharu + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package dev.usbharu.owl.common.retry + + +import org.slf4j.LoggerFactory + +interface RetryPolicyFactory { + fun factory(name: String): RetryPolicy +} + +class DefaultRetryPolicyFactory(private val map: Map) : RetryPolicyFactory { + override fun factory(name: String): RetryPolicy { + return map[name] ?: throwException(name) + } + + private fun throwException(name: String): Nothing { + logger.warn("RetryPolicy not found. name: {}", name) + throw RetryPolicyNotFoundException("RetryPolicy not found. name: $name") + } + + companion object { + private val logger = LoggerFactory.getLogger(RetryPolicyFactory::class.java) + } +} \ No newline at end of file diff --git a/owl/owl-common/src/main/kotlin/dev/usbharu/owl/common/retry/RetryPolicyNotFoundException.kt b/owl/owl-common/src/main/kotlin/dev/usbharu/owl/common/retry/RetryPolicyNotFoundException.kt new file mode 100644 index 00000000..dba37f35 --- /dev/null +++ b/owl/owl-common/src/main/kotlin/dev/usbharu/owl/common/retry/RetryPolicyNotFoundException.kt @@ -0,0 +1,30 @@ +/* + * Copyright (C) 2024 usbharu + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package dev.usbharu.owl.common.retry + +class RetryPolicyNotFoundException : 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 + ) +} \ No newline at end of file diff --git a/owl/owl-common/src/main/kotlin/dev/usbharu/owl/common/task/PropertyDefinition.kt b/owl/owl-common/src/main/kotlin/dev/usbharu/owl/common/task/PropertyDefinition.kt new file mode 100644 index 00000000..cbc96b0b --- /dev/null +++ b/owl/owl-common/src/main/kotlin/dev/usbharu/owl/common/task/PropertyDefinition.kt @@ -0,0 +1,41 @@ +/* + * Copyright (C) 2024 usbharu + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package dev.usbharu.owl.common.task + +import dev.usbharu.owl.common.property.PropertyType + +/** + * プロパティ定義 + * + * @property map プロパティ名とプロパティタイプの[Map] + */ +class PropertyDefinition(val map: Map) : Map by map { + /** + * プロパティ定義のハッシュを求めます + * + * ハッシュ値はプロパティ名とプロパティタイプ名を結合したものを結合し、各文字のUTF-16コードと31を掛け続けたものです。 + * + * @return + */ + fun hash(): Long { + var hash = 1L + map.map { it.key + it.value.name }.joinToString("").map { hash *= it.code * 31 } + return hash + } + + +} diff --git a/owl/owl-common/src/main/kotlin/dev/usbharu/owl/common/task/PublishedTask.kt b/owl/owl-common/src/main/kotlin/dev/usbharu/owl/common/task/PublishedTask.kt new file mode 100644 index 00000000..a0f944e5 --- /dev/null +++ b/owl/owl-common/src/main/kotlin/dev/usbharu/owl/common/task/PublishedTask.kt @@ -0,0 +1,34 @@ +/* + * Copyright (C) 2024 usbharu + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package dev.usbharu.owl.common.task + +import java.time.Instant +import java.util.* + +/** + * 公開済みのタスク + * + * @param T タスク + * @property task タスク + * @property id タスクのID + * @property published 公開された時刻 + */ +data class PublishedTask( + val task: T, + val id: UUID, + val published: Instant +) \ No newline at end of file diff --git a/owl/owl-common/src/main/kotlin/dev/usbharu/owl/common/task/Task.kt b/owl/owl-common/src/main/kotlin/dev/usbharu/owl/common/task/Task.kt new file mode 100644 index 00000000..f29d5d2d --- /dev/null +++ b/owl/owl-common/src/main/kotlin/dev/usbharu/owl/common/task/Task.kt @@ -0,0 +1,23 @@ +/* + * Copyright (C) 2024 usbharu + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package dev.usbharu.owl.common.task + +/** + * タスク + * + */ +open class Task \ No newline at end of file diff --git a/owl/owl-common/src/main/kotlin/dev/usbharu/owl/common/task/TaskDefinition.kt b/owl/owl-common/src/main/kotlin/dev/usbharu/owl/common/task/TaskDefinition.kt new file mode 100644 index 00000000..5cced498 --- /dev/null +++ b/owl/owl-common/src/main/kotlin/dev/usbharu/owl/common/task/TaskDefinition.kt @@ -0,0 +1,130 @@ +/* + * Copyright (C) 2024 usbharu + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package dev.usbharu.owl.common.task + +import dev.usbharu.owl.common.allFields +import dev.usbharu.owl.common.property.* + +/** + * タスク定義 + * + * @param T タスク + */ +interface TaskDefinition { + /** + * タスク名 + */ + val name: String + get() = type.simpleName + + /** + * 優先度 + */ + val priority: Int + get() = 0 + + /** + * 最大リトライ数 + */ + val maxRetry: Int + get() = 5 + + /** + * リトライポリシー名 + * + * ポリシーの解決は各Brokerに依存しています + */ + val retryPolicy: String + get() = "" + + /** + * タスク実行時のタイムアウト(ミリ秒) + */ + val timeoutMilli: Long + get() = 1000 + + /** + * プロパティ定義 + */ + val propertyDefinition: PropertyDefinition + get() { + val mapValues = type.allFields.associate { it.name to it.type }.mapValues { + when { + it.value === Int::class.java -> PropertyType.number + it.value === String::class.java -> PropertyType.string + it.value === Long::class.java -> PropertyType.number + it.value === Double::class.java -> PropertyType.number + it.value === Float::class.java -> PropertyType.number + else -> PropertyType.binary + } + } + return PropertyDefinition(mapValues) + } + + /** + * [Task]の[Class] + */ + val type: Class + + /** + * タスクをシリアライズします. + * プロパティのシリアライズと混同しないようにしてください。 + * @param task シリアライズするタスク + * @return シリアライズされたタスク + */ + fun serialize(task: T): Map> { + return type.allFields.associateBy { it.name }.mapValues { + when { + it.value.type === Int::class.java -> IntegerPropertyValue(it.value.getInt(task)) + it.value.type === String::class.java -> StringPropertyValue(it.value.get(task) as String) + it.value.type === Long::class.java -> LongPropertyValue(it.value.getLong(task)) + it.value.type === Double::class.java -> DoublePropertyValue(it.value.getDouble(task)) + it.value.type === Float::class.java -> FloatPropertyValue(it.value.getFloat(task)) + it.value.type === Boolean::class.java -> BooleanPropertyValue(it.value.getBoolean(task)) + else -> throw IllegalArgumentException("Unsupported type ${it.value} in ${task.javaClass.name}") + } + } + } + + /** + * タスクをデシリアライズします。 + * プロパティのデシリアライズと混同しないようにしてください + * @param value デシリアライズするタスク + * @return デシリアライズされたタスク + */ + fun deserialize(value: Map>): T { + + val task = try { + type.getDeclaredConstructor().newInstance() + } catch (e: Exception) { + throw IllegalArgumentException("Unable to deserialize value $value for type ${type.name}", e) + } + + type.allFields.associateBy { it.name }.mapValues { + when { + it.value.type === Int::class.java -> it.value.setInt(task, value.getValue(it.key).value as Int) + it.value.type === Double::class.java -> it.value.setDouble(task, value.getValue(it.key).value as Double) + it.value.type === Float::class.java -> it.value.setFloat(task, value.getValue(it.key).value as Float) + it.value.type === String::class.java -> it.value.set(task, value.getValue(it.key).value as String) + it.value.type === Long::class.java -> it.value.setLong(task, value.getValue(it.key).value as Long) + else -> throw IllegalArgumentException("Unsupported type ${it.value} in ${task.javaClass.name}") + } + } + + return task + } +} \ No newline at end of file diff --git a/owl/owl-common/src/test/kotlin/dev/usbharu/owl/common/retry/ExponentialRetryPolicyTest.kt b/owl/owl-common/src/test/kotlin/dev/usbharu/owl/common/retry/ExponentialRetryPolicyTest.kt new file mode 100644 index 00000000..c1a1dc2e --- /dev/null +++ b/owl/owl-common/src/test/kotlin/dev/usbharu/owl/common/retry/ExponentialRetryPolicyTest.kt @@ -0,0 +1,36 @@ +/* + * Copyright (C) 2024 usbharu + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package dev.usbharu.owl.common.retry + +import org.junit.jupiter.api.Assertions.assertEquals +import org.junit.jupiter.api.Test +import java.time.Instant + +class ExponentialRetryPolicyTest { + @Test + fun exponential0() { + val nextRetry = ExponentialRetryPolicy().nextRetry(Instant.ofEpochSecond(300), 0) + + assertEquals(Instant.ofEpochSecond(300), nextRetry) + } + + @Test + fun exponential1() { + val nextRetry = ExponentialRetryPolicy().nextRetry(Instant.ofEpochSecond(300), 1) + assertEquals(Instant.ofEpochSecond(330), nextRetry) + } +} \ No newline at end of file diff --git a/owl/owl-consumer/build.gradle.kts b/owl/owl-consumer/build.gradle.kts new file mode 100644 index 00000000..1a1fc521 --- /dev/null +++ b/owl/owl-consumer/build.gradle.kts @@ -0,0 +1,54 @@ +plugins { + alias(libs.plugins.kotlin.jvm) + id("com.google.protobuf") version "0.9.4" +} + +group = "dev.usbharu" +version = "0.0.1" + +repositories { + mavenCentral() +} + +dependencies { + testImplementation("org.jetbrains.kotlin:kotlin-test") + implementation("io.grpc:grpc-kotlin-stub:1.4.1") + implementation("io.grpc:grpc-protobuf:1.66.0") + implementation("com.google.protobuf:protobuf-kotlin:4.27.3") + implementation("io.grpc:grpc-netty:1.66.0") + implementation("org.jetbrains.kotlinx:kotlinx-coroutines-core:1.8.1") + implementation(project(":owl-common")) + protobuf(files(project(":owl-broker").dependencyProject.projectDir.toString() + "/src/main/proto")) +} + +tasks.test { + useJUnitPlatform() +} +kotlin { + jvmToolchain(21) +} + +protobuf { + protoc { + artifact = "com.google.protobuf:protoc:4.27.3" + } + plugins { + create("grpc") { + artifact = "io.grpc:protoc-gen-grpc-java:1.66.0" + } + create("grpckt") { + artifact = "io.grpc:protoc-gen-grpc-kotlin:1.4.1:jdk8@jar" + } + } + generateProtoTasks { + all().forEach { + it.plugins { + create("grpc") + create("grpckt") + } + it.builtins { + create("kotlin") + } + } + } +} \ No newline at end of file diff --git a/owl/owl-consumer/src/main/kotlin/dev/usbharu/owl/consumer/AbstractTaskRunner.kt b/owl/owl-consumer/src/main/kotlin/dev/usbharu/owl/consumer/AbstractTaskRunner.kt new file mode 100644 index 00000000..e1a5125e --- /dev/null +++ b/owl/owl-consumer/src/main/kotlin/dev/usbharu/owl/consumer/AbstractTaskRunner.kt @@ -0,0 +1,33 @@ +/* + * Copyright (C) 2024 usbharu + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package dev.usbharu.owl.consumer + +import dev.usbharu.owl.common.task.Task +import dev.usbharu.owl.common.task.TaskDefinition + +abstract class AbstractTaskRunner>(private val taskDefinition: D) : TaskRunner { + override val name: String + get() = taskDefinition.name + + override suspend fun run(taskRequest: TaskRequest): TaskResult { + val deserialize = taskDefinition.deserialize(taskRequest.properties) + return typedRun(deserialize, taskRequest) + } + + abstract suspend fun typedRun(typedParam: T, taskRequest: TaskRequest): TaskResult + +} \ No newline at end of file diff --git a/owl/owl-consumer/src/main/kotlin/dev/usbharu/owl/consumer/Consumer.kt b/owl/owl-consumer/src/main/kotlin/dev/usbharu/owl/consumer/Consumer.kt new file mode 100644 index 00000000..56ac00c7 --- /dev/null +++ b/owl/owl-consumer/src/main/kotlin/dev/usbharu/owl/consumer/Consumer.kt @@ -0,0 +1,209 @@ +/* + * Copyright (C) 2024 usbharu + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package dev.usbharu.owl.consumer + +import dev.usbharu.owl.* +import dev.usbharu.owl.Uuid.UUID +import dev.usbharu.owl.common.property.PropertySerializeUtils +import dev.usbharu.owl.common.property.PropertySerializerFactory +import kotlinx.coroutines.* +import kotlinx.coroutines.flow.* +import org.slf4j.LoggerFactory +import java.time.Instant +import kotlin.math.max + +/** + * Consumer + * + * @property subscribeTaskStub + * @property assignmentTaskStub + * @property taskResultStub + * @property runnerMap + * @property propertySerializerFactory + * @constructor + * TODO + * + * @param consumerConfig + */ +class Consumer( + private val subscribeTaskStub: SubscribeTaskServiceGrpcKt.SubscribeTaskServiceCoroutineStub, + private val assignmentTaskStub: AssignmentTaskServiceGrpcKt.AssignmentTaskServiceCoroutineStub, + private val taskResultStub: TaskResultServiceGrpcKt.TaskResultServiceCoroutineStub, + taskRunnerLoader: TaskRunnerLoader, + private val propertySerializerFactory: PropertySerializerFactory, + consumerConfig: ConsumerConfig, +) { + + private lateinit var consumerId: UUID + + private lateinit var coroutineScope: CoroutineScope + + private val concurrent = MutableStateFlow(consumerConfig.concurrent) + private val processing = MutableStateFlow(0) + + private val runnerMap = taskRunnerLoader.load() + + /** + * Consumerを初期化します + * + * @param name Consumer名 + * @param hostname Consumerのホスト名 + */ + suspend fun init(name: String, hostname: String) { + logger.info("Initialize Consumer name: {} hostname: {}", name, hostname) + logger.debug("Registered Tasks: {}", runnerMap.keys) + consumerId = subscribeTaskStub.subscribeTask(subscribeTaskRequest { + this.name = name + this.hostname = hostname + this.tasks.addAll(runnerMap.keys) + }).id + logger.info("Success initialize consumer. ConsumerID: {}", consumerId) + } + + /** + * タスクの受付を開始します + * + */ + suspend fun start() { + coroutineScope = CoroutineScope(Dispatchers.Default) + coroutineScope { + while (isActive) { + try { + taskResultStub + .tasKResult(flow { + assignmentTaskStub + .ready(flow { + requestTask() + }).onEach { + logger.info("Start Task name: {} id: {}", it.name, it.id) + processing.update { it + 1 } + + try { + val taskResult = runnerMap.getValue(it.name).run( + TaskRequest( + it.name, + java.util.UUID( + it.id.mostSignificantUuidBits, + it.id.leastSignificantUuidBits + ), + it.attempt, + Instant.ofEpochSecond(it.queuedAt.seconds, it.queuedAt.nanos.toLong()), + PropertySerializeUtils.deserialize( + propertySerializerFactory, + it.propertiesMap + ) + ) + ) + + emit(taskResult { + this.success = taskResult.success + this.attempt = it.attempt + this.id = it.id + this.result.putAll( + PropertySerializeUtils.serialize( + propertySerializerFactory, taskResult.result + ) + ) + this.message = taskResult.message + }) + logger.info( + "Success execute task. name: {} success: {}", + it.name, + taskResult.success + ) + logger.debug("TRACE RESULT {}", taskResult) + } catch (e: CancellationException) { + logger.warn("Cancelled execute task.", e) + emit(taskResult { + this.success = false + this.attempt = it.attempt + this.id = it.id + this.message = e.localizedMessage + }) + throw e + } catch (e: Exception) { + logger.warn("Failed execute task. name: {} id: {}", it.name, it.id, e) + emit(taskResult { + this.success = false + this.attempt = it.attempt + this.id = it.id + this.message = e.localizedMessage + }) + } finally { + logger.debug(" Task name: {} id: {}", it.name, it.id) + processing.update { it - 1 } + concurrent.update { + if (it < 64) { + it + 1 + } else { + 64 + } + } + } + }.flowOn(Dispatchers.Default).collect() + }) + } catch (e: CancellationException) { + throw e + } catch (e: Exception) { + logger.warn("Consumer error", e) + } + + delay(1000) + } + } + } + + private suspend fun FlowCollector.requestTask() { + while (coroutineScope.isActive) { + val andSet = concurrent.getAndUpdate { 0 } + + + if (andSet != 0) { + logger.debug("Request {} tasks.", andSet) + try { + emit(readyRequest { + this.consumerId = this@Consumer.consumerId + this.numberOfConcurrent = andSet + }) + } catch (e: CancellationException) { + throw e + } catch (e: Exception) { + logger.warn("Failed request task.", e) + } + continue + } + delay(100) + + concurrent.update { + ((64 - it) - processing.value).coerceIn(0, 64 - max(0, processing.value)) + } + } + } + + /** + * タスクの受付を停止します + * + */ + fun stop() { + logger.info("Stop Consumer. consumerID: {}", consumerId) + coroutineScope.cancel() + } + + companion object { + private val logger = LoggerFactory.getLogger(Consumer::class.java) + } +} \ No newline at end of file diff --git a/owl/owl-consumer/src/main/kotlin/dev/usbharu/owl/consumer/ConsumerConfig.kt b/owl/owl-consumer/src/main/kotlin/dev/usbharu/owl/consumer/ConsumerConfig.kt new file mode 100644 index 00000000..a4609e51 --- /dev/null +++ b/owl/owl-consumer/src/main/kotlin/dev/usbharu/owl/consumer/ConsumerConfig.kt @@ -0,0 +1,26 @@ +/* + * Copyright (C) 2024 usbharu + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package dev.usbharu.owl.consumer + +/** + * Consumerの構成 + * + * @property concurrent Consumerのワーカーの同時実行数 + */ +data class ConsumerConfig( + val concurrent: Int +) diff --git a/owl/owl-consumer/src/main/kotlin/dev/usbharu/owl/consumer/Main.kt b/owl/owl-consumer/src/main/kotlin/dev/usbharu/owl/consumer/Main.kt new file mode 100644 index 00000000..31fba291 --- /dev/null +++ b/owl/owl-consumer/src/main/kotlin/dev/usbharu/owl/consumer/Main.kt @@ -0,0 +1,29 @@ +/* + * Copyright (C) 2024 usbharu + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package dev.usbharu.owl.consumer + +import kotlinx.coroutines.runBlocking + +fun main() { + val standaloneConsumer = StandaloneConsumer() + + runBlocking { + standaloneConsumer.init() + standaloneConsumer.start() + } + +} \ No newline at end of file diff --git a/owl/owl-consumer/src/main/kotlin/dev/usbharu/owl/consumer/ServiceLoaderTaskRunnerLoader.kt b/owl/owl-consumer/src/main/kotlin/dev/usbharu/owl/consumer/ServiceLoaderTaskRunnerLoader.kt new file mode 100644 index 00000000..a34fc37b --- /dev/null +++ b/owl/owl-consumer/src/main/kotlin/dev/usbharu/owl/consumer/ServiceLoaderTaskRunnerLoader.kt @@ -0,0 +1,29 @@ +/* + * Copyright (C) 2024 usbharu + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package dev.usbharu.owl.consumer + +import java.util.* + +class ServiceLoaderTaskRunnerLoader : TaskRunnerLoader { + private val taskRunnerMap = ServiceLoader + .load(TaskRunner::class.java) + .associateBy { it.name } + + override fun load(): Map { + return taskRunnerMap + } +} \ No newline at end of file diff --git a/owl/owl-consumer/src/main/kotlin/dev/usbharu/owl/consumer/StandaloneConsumer.kt b/owl/owl-consumer/src/main/kotlin/dev/usbharu/owl/consumer/StandaloneConsumer.kt new file mode 100644 index 00000000..34f75a05 --- /dev/null +++ b/owl/owl-consumer/src/main/kotlin/dev/usbharu/owl/consumer/StandaloneConsumer.kt @@ -0,0 +1,106 @@ +/* + * Copyright (C) 2024 usbharu + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package dev.usbharu.owl.consumer + +import dev.usbharu.owl.AssignmentTaskServiceGrpcKt +import dev.usbharu.owl.SubscribeTaskServiceGrpcKt +import dev.usbharu.owl.TaskResultServiceGrpcKt +import dev.usbharu.owl.common.property.CustomPropertySerializerFactory +import dev.usbharu.owl.common.property.PropertySerializerFactory +import io.grpc.ManagedChannelBuilder +import java.nio.file.Path + +/** + * 単独で起動できるConsumer + * + * @property config Consumerの起動構成 + * @property propertySerializerFactory [dev.usbharu.owl.common.property.PropertyValue]のシリアライザーのファクトリ + */ +class StandaloneConsumer( + private val config: StandaloneConsumerConfig, + private val propertySerializerFactory: PropertySerializerFactory, + taskRunnerLoader: TaskRunnerLoader, +) { + constructor( + path: Path, + propertySerializerFactory: PropertySerializerFactory = CustomPropertySerializerFactory( + emptySet() + ), + taskRunnerLoader: TaskRunnerLoader = ServiceLoaderTaskRunnerLoader(), + ) : this(StandaloneConsumerConfigLoader.load(path), propertySerializerFactory, taskRunnerLoader) + + constructor( + string: String, + propertySerializerFactory: PropertySerializerFactory = CustomPropertySerializerFactory(emptySet()), + taskRunnerLoader: TaskRunnerLoader = ServiceLoaderTaskRunnerLoader(), + ) : this(Path.of(string), propertySerializerFactory, taskRunnerLoader) + + constructor( + propertySerializerFactory: PropertySerializerFactory = CustomPropertySerializerFactory(emptySet()), + taskRunnerLoader: TaskRunnerLoader = ServiceLoaderTaskRunnerLoader(), + ) : this( + Path.of(StandaloneConsumer::class.java.getClassLoader().getResource("consumer.properties").toURI()), + propertySerializerFactory, + taskRunnerLoader + ) + + private val channel = ManagedChannelBuilder.forAddress(config.address, config.port) + .usePlaintext() + .build() + + private val subscribeStub = SubscribeTaskServiceGrpcKt.SubscribeTaskServiceCoroutineStub(channel) + private val assignmentTaskStub = AssignmentTaskServiceGrpcKt.AssignmentTaskServiceCoroutineStub(channel) + private val taskResultStub = TaskResultServiceGrpcKt.TaskResultServiceCoroutineStub(channel) + + private val consumer = Consumer( + subscribeTaskStub = subscribeStub, + assignmentTaskStub = assignmentTaskStub, + taskResultStub = taskResultStub, + taskRunnerLoader = taskRunnerLoader, + propertySerializerFactory = propertySerializerFactory, + consumerConfig = ConsumerConfig(config.concurrency), + ) + + /** + * Consumerを初期化します + * + */ + suspend fun init() { + consumer.init(config.name, config.hostname) + } + + /** + * Consumerのワーカーを起動し、タスクの受付を開始します。 + * + * シャットダウンフックに[stop]が登録されます。 + */ + suspend fun start() { + consumer.start() + Runtime.getRuntime().addShutdownHook(Thread { + consumer.stop() + }) + } + + /** + * Consumerを停止します + * + */ + fun stop() { + consumer.stop() + } + +} \ No newline at end of file diff --git a/owl/owl-consumer/src/main/kotlin/dev/usbharu/owl/consumer/StandaloneConsumerConfig.kt b/owl/owl-consumer/src/main/kotlin/dev/usbharu/owl/consumer/StandaloneConsumerConfig.kt new file mode 100644 index 00000000..ff31dde8 --- /dev/null +++ b/owl/owl-consumer/src/main/kotlin/dev/usbharu/owl/consumer/StandaloneConsumerConfig.kt @@ -0,0 +1,34 @@ +/* + * Copyright (C) 2024 usbharu + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package dev.usbharu.owl.consumer + +/** + * 単独で起動できるConsumerの構成 + * + * @property address brokerのアドレス + * @property port brokerのポート + * @property name Consumerの名前 + * @property hostname Consumerのホスト名 + * @property concurrency ConsumerのWorkerの最大同時実行数 + */ +data class StandaloneConsumerConfig( + val address: String, + val port: Int, + val name: String, + val hostname: String, + val concurrency: Int, +) diff --git a/owl/owl-consumer/src/main/kotlin/dev/usbharu/owl/consumer/StandaloneConsumerConfigLoader.kt b/owl/owl-consumer/src/main/kotlin/dev/usbharu/owl/consumer/StandaloneConsumerConfigLoader.kt new file mode 100644 index 00000000..d11bda43 --- /dev/null +++ b/owl/owl-consumer/src/main/kotlin/dev/usbharu/owl/consumer/StandaloneConsumerConfigLoader.kt @@ -0,0 +1,46 @@ +/* + * Copyright (C) 2024 usbharu + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package dev.usbharu.owl.consumer + +import java.nio.file.Files +import java.nio.file.Path +import java.util.* + +/** + * 単独で起動できるConsumerの構成のローダー + */ +object StandaloneConsumerConfigLoader { + /** + * [Path]から構成を読み込みます + * + * @param path 読み込むパス + * @return 読み込まれた構成 + */ + fun load(path: Path): StandaloneConsumerConfig { + val properties = Properties() + + properties.load(Files.newInputStream(path)) + + val address = properties.getProperty("address") + val port = properties.getProperty("port").toInt() + val name = properties.getProperty("name") + val hostname = properties.getProperty("hostname") + val concurrency = properties.getProperty("concurrency").toInt() + + return StandaloneConsumerConfig(address, port, name, hostname, concurrency) + } +} \ No newline at end of file diff --git a/owl/owl-consumer/src/main/kotlin/dev/usbharu/owl/consumer/TaskRequest.kt b/owl/owl-consumer/src/main/kotlin/dev/usbharu/owl/consumer/TaskRequest.kt new file mode 100644 index 00000000..006856f2 --- /dev/null +++ b/owl/owl-consumer/src/main/kotlin/dev/usbharu/owl/consumer/TaskRequest.kt @@ -0,0 +1,38 @@ +/* + * Copyright (C) 2024 usbharu + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package dev.usbharu.owl.consumer + +import dev.usbharu.owl.common.property.PropertyValue +import java.time.Instant +import java.util.* + +/** + * タスクをConsumerに要求します + * + * @property name タスク名 + * @property id タスクID + * @property attempt 試行回数 + * @property queuedAt タスクがキューに入れられた時間 + * @property properties タスクに渡されたパラメータ + */ +data class TaskRequest( + val name:String, + val id:UUID, + val attempt:Int, + val queuedAt: Instant, + val properties:Map> +) diff --git a/owl/owl-consumer/src/main/kotlin/dev/usbharu/owl/consumer/TaskResult.kt b/owl/owl-consumer/src/main/kotlin/dev/usbharu/owl/consumer/TaskResult.kt new file mode 100644 index 00000000..07d4bd9f --- /dev/null +++ b/owl/owl-consumer/src/main/kotlin/dev/usbharu/owl/consumer/TaskResult.kt @@ -0,0 +1,38 @@ +/* + * Copyright (C) 2024 usbharu + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package dev.usbharu.owl.consumer + +import dev.usbharu.owl.common.property.PropertyValue + +/** + * タスクの実行結果 + * + * @property success 成功したらtrue + * @property result タスクの実行結果のMap + * @property message その他メッセージ + */ +data class TaskResult( + val success: Boolean, + val result: Map>, + val message: String, +) { + companion object { + fun ok(result: Map> = emptyMap()): TaskResult { + return TaskResult(true, result, "") + } + } +} \ No newline at end of file diff --git a/owl/owl-consumer/src/main/kotlin/dev/usbharu/owl/consumer/TaskRunner.kt b/owl/owl-consumer/src/main/kotlin/dev/usbharu/owl/consumer/TaskRunner.kt new file mode 100644 index 00000000..613b0166 --- /dev/null +++ b/owl/owl-consumer/src/main/kotlin/dev/usbharu/owl/consumer/TaskRunner.kt @@ -0,0 +1,36 @@ +/* + * Copyright (C) 2024 usbharu + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package dev.usbharu.owl.consumer + +/** + * タスクを実行するランナー + * + */ +interface TaskRunner { + /** + * 実行するタスク名 + */ + val name: String + + /** + * タスクを実行する + * + * @param taskRequest 実行するタスク + * @return タスク実行結果 + */ + suspend fun run(taskRequest: TaskRequest): TaskResult +} \ No newline at end of file diff --git a/owl/owl-consumer/src/main/kotlin/dev/usbharu/owl/consumer/TaskRunnerLoader.kt b/owl/owl-consumer/src/main/kotlin/dev/usbharu/owl/consumer/TaskRunnerLoader.kt new file mode 100644 index 00000000..2581e5de --- /dev/null +++ b/owl/owl-consumer/src/main/kotlin/dev/usbharu/owl/consumer/TaskRunnerLoader.kt @@ -0,0 +1,21 @@ +/* + * Copyright (C) 2024 usbharu + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package dev.usbharu.owl.consumer + +interface TaskRunnerLoader { + fun load(): Map +} \ No newline at end of file diff --git a/owl/owl-consumer/src/main/resources/consumer.properties b/owl/owl-consumer/src/main/resources/consumer.properties new file mode 100644 index 00000000..05da7435 --- /dev/null +++ b/owl/owl-consumer/src/main/resources/consumer.properties @@ -0,0 +1,20 @@ +# +# Copyright (C) 2024 usbharu +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# +address=localhost +port=50051 +name=owl +hostname=localhost +concurrency=10 \ No newline at end of file diff --git a/owl/owl-producer/owl-producer-api/build.gradle.kts b/owl/owl-producer/owl-producer-api/build.gradle.kts new file mode 100644 index 00000000..38154017 --- /dev/null +++ b/owl/owl-producer/owl-producer-api/build.gradle.kts @@ -0,0 +1,21 @@ +plugins { + alias(libs.plugins.kotlin.jvm) +} + +group = "dev.usbharu" +version = "0.0.1" + +repositories { + mavenCentral() +} + +dependencies { + api(project(":owl-common")) +} + +tasks.test { + useJUnitPlatform() +} +kotlin { + jvmToolchain(21) +} \ No newline at end of file diff --git a/owl/owl-producer/owl-producer-api/src/main/kotlin/dev/usbharu/owl/producer/api/OwlProducer.kt b/owl/owl-producer/owl-producer-api/src/main/kotlin/dev/usbharu/owl/producer/api/OwlProducer.kt new file mode 100644 index 00000000..7ece4505 --- /dev/null +++ b/owl/owl-producer/owl-producer-api/src/main/kotlin/dev/usbharu/owl/producer/api/OwlProducer.kt @@ -0,0 +1,53 @@ +/* + * Copyright (C) 2024 usbharu + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package dev.usbharu.owl.producer.api + +import dev.usbharu.owl.common.task.PublishedTask +import dev.usbharu.owl.common.task.Task +import dev.usbharu.owl.common.task.TaskDefinition + +/** + * タスクを発生させるクライアント + * + */ +interface OwlProducer { + + /** + * Producerを開始します + * + */ + suspend fun start() + + /** + * タスク定義を登録します + * + * @param T 登録するタスク + * @param taskDefinition 登録するタスクの定義 + */ + suspend fun registerTask(taskDefinition: TaskDefinition) + + /** + * タスクを公開します。タスクは定義済みである必要があります。 + * + * @param T 公開するタスク + * @param task タスクの詳細 + * @return 公開されたタスク + */ + suspend fun publishTask(task: T): PublishedTask + + suspend fun stop() +} diff --git a/owl/owl-producer/owl-producer-api/src/main/kotlin/dev/usbharu/owl/producer/api/OwlProducerBuilder.kt b/owl/owl-producer/owl-producer-api/src/main/kotlin/dev/usbharu/owl/producer/api/OwlProducerBuilder.kt new file mode 100644 index 00000000..0678266e --- /dev/null +++ b/owl/owl-producer/owl-producer-api/src/main/kotlin/dev/usbharu/owl/producer/api/OwlProducerBuilder.kt @@ -0,0 +1,46 @@ +/* + * Copyright (C) 2024 usbharu + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package dev.usbharu.owl.producer.api + +/** + * [OwlProducer]を作成するビルダー + * + * @param P 作成する[OwlProducer] + * @param T [OwlProducer]の構成 + */ +interface OwlProducerBuilder

{ + /** + * 現在の構成を返します + * + * @return 現在の構成 + */ + fun config(): T + + /** + * 構成を適用します + * + * @param owlProducerConfig 適用する構成 + */ + fun apply(owlProducerConfig: T) + + /** + * 適用されている構成を使用して[OwlProducer]のインスタンスを作成します。 + * + * @return 作成された[OwlProducer] + */ + fun build(): P +} \ No newline at end of file diff --git a/owl/owl-producer/owl-producer-api/src/main/kotlin/dev/usbharu/owl/producer/api/OwlProducerBuilderConfig.kt b/owl/owl-producer/owl-producer-api/src/main/kotlin/dev/usbharu/owl/producer/api/OwlProducerBuilderConfig.kt new file mode 100644 index 00000000..efedddf0 --- /dev/null +++ b/owl/owl-producer/owl-producer-api/src/main/kotlin/dev/usbharu/owl/producer/api/OwlProducerBuilderConfig.kt @@ -0,0 +1,34 @@ +/* + * Copyright (C) 2024 usbharu + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package dev.usbharu.owl.producer.api + +/** + * [OwlProducerBuilder]と[OwlProducerConfig]を使用して[OwlProducer]のインスタンスを作成します。 + * + * @param P 作成する[OwlProducer] + * @param T 作成に使用する[OwlProducerBuilder] + * @param C 構成 + * @param owlProducerBuilder 作成に使用する[OwlProducerBuilder] + * @param configBlock 構成 + */ +fun

, C : OwlProducerConfig> OWL( + owlProducerBuilder: T, + configBlock: C.() -> Unit = {}, +): P { + owlProducerBuilder.apply(owlProducerBuilder.config().apply { configBlock() }) + return owlProducerBuilder.build() +} \ No newline at end of file diff --git a/owl/owl-producer/owl-producer-api/src/main/kotlin/dev/usbharu/owl/producer/api/OwlProducerConfig.kt b/owl/owl-producer/owl-producer-api/src/main/kotlin/dev/usbharu/owl/producer/api/OwlProducerConfig.kt new file mode 100644 index 00000000..9334b0fd --- /dev/null +++ b/owl/owl-producer/owl-producer-api/src/main/kotlin/dev/usbharu/owl/producer/api/OwlProducerConfig.kt @@ -0,0 +1,23 @@ +/* + * Copyright (C) 2024 usbharu + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package dev.usbharu.owl.producer.api + +/** + * [OwlProducer]の構成 + * + */ +interface OwlProducerConfig \ No newline at end of file diff --git a/owl/owl-producer/owl-producer-default/build.gradle.kts b/owl/owl-producer/owl-producer-default/build.gradle.kts new file mode 100644 index 00000000..cbde1aa6 --- /dev/null +++ b/owl/owl-producer/owl-producer-default/build.gradle.kts @@ -0,0 +1,55 @@ +plugins { + alias(libs.plugins.kotlin.jvm) + id("com.google.protobuf") version "0.9.4" +} + +group = "dev.usbharu" +version = "0.0.1" + +repositories { + mavenCentral() +} + +dependencies { + testImplementation("org.jetbrains.kotlin:kotlin-test") + api(project(":owl-producer:owl-producer-api")) + implementation("io.grpc:grpc-kotlin-stub:1.4.1") + implementation("io.grpc:grpc-protobuf:1.66.0") + implementation("com.google.protobuf:protobuf-kotlin:4.27.3") + implementation("io.grpc:grpc-netty:1.66.0") + implementation("org.jetbrains.kotlinx:kotlinx-coroutines-core:1.8.1") + implementation(project(":owl-common")) + protobuf(files(project(":owl-broker").dependencyProject.projectDir.toString() + "/src/main/proto")) +} + +tasks.test { + useJUnitPlatform() +} +kotlin { + jvmToolchain(21) +} + +protobuf { + protoc { + artifact = "com.google.protobuf:protoc:4.27.3" + } + plugins { + create("grpc") { + artifact = "io.grpc:protoc-gen-grpc-java:1.66.0" + } + create("grpckt") { + artifact = "io.grpc:protoc-gen-grpc-kotlin:1.4.1:jdk8@jar" + } + } + generateProtoTasks { + all().forEach { + it.plugins { + create("grpc") + create("grpckt") + } + it.builtins { + create("kotlin") + } + } + } +} \ No newline at end of file diff --git a/owl/owl-producer/owl-producer-default/src/main/kotlin/dev/usbharu/owl/producer/defaultimpl/DefaultOwlProducer.kt b/owl/owl-producer/owl-producer-default/src/main/kotlin/dev/usbharu/owl/producer/defaultimpl/DefaultOwlProducer.kt new file mode 100644 index 00000000..e4a1dfd4 --- /dev/null +++ b/owl/owl-producer/owl-producer-default/src/main/kotlin/dev/usbharu/owl/producer/defaultimpl/DefaultOwlProducer.kt @@ -0,0 +1,94 @@ +/* + * Copyright (C) 2024 usbharu + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package dev.usbharu.owl.producer.defaultimpl + +import com.google.protobuf.timestamp +import dev.usbharu.owl.* +import dev.usbharu.owl.Uuid.UUID +import dev.usbharu.owl.common.property.PropertySerializeUtils +import dev.usbharu.owl.common.task.PublishedTask +import dev.usbharu.owl.common.task.Task +import dev.usbharu.owl.common.task.TaskDefinition +import dev.usbharu.owl.producer.api.OwlProducer +import java.time.Instant + +class DefaultOwlProducer(private val defaultOwlProducerConfig: DefaultOwlProducerConfig) : OwlProducer { + + lateinit var producerId: UUID + lateinit var producerServiceCoroutineStub: ProducerServiceGrpcKt.ProducerServiceCoroutineStub + lateinit var defineTaskServiceCoroutineStub: DefinitionTaskServiceGrpcKt.DefinitionTaskServiceCoroutineStub + lateinit var taskPublishServiceCoroutineStub: TaskPublishServiceGrpcKt.TaskPublishServiceCoroutineStub + val map = mutableMapOf, TaskDefinition<*>>() + override suspend fun start() { + producerServiceCoroutineStub = + ProducerServiceGrpcKt.ProducerServiceCoroutineStub(defaultOwlProducerConfig.channel) + producerId = producerServiceCoroutineStub.registerProducer(producer { + this.name = defaultOwlProducerConfig.name + this.hostname = defaultOwlProducerConfig.hostname + }).id + + defineTaskServiceCoroutineStub = + DefinitionTaskServiceGrpcKt.DefinitionTaskServiceCoroutineStub(defaultOwlProducerConfig.channel) + + taskPublishServiceCoroutineStub = + TaskPublishServiceGrpcKt.TaskPublishServiceCoroutineStub(defaultOwlProducerConfig.channel) + } + + + override suspend fun registerTask(taskDefinition: TaskDefinition) { + defineTaskServiceCoroutineStub.register(taskDefinition { + this.producerId = this@DefaultOwlProducer.producerId + this.name = taskDefinition.name + this.maxRetry = taskDefinition.maxRetry + this.priority = taskDefinition.priority + this.retryPolicy = taskDefinition.retryPolicy + this.timeoutMilli = taskDefinition.timeoutMilli + this.propertyDefinitionHash = taskDefinition.propertyDefinition.hash() + }) + } + + override suspend fun publishTask(task: T): PublishedTask { + val taskDefinition = map.getValue(task::class.java) as TaskDefinition + val properties = PropertySerializeUtils.serialize( + defaultOwlProducerConfig.propertySerializerFactory, + taskDefinition.serialize(task) + ) + val now = Instant.now() + val publishTask = taskPublishServiceCoroutineStub.publishTask( + dev.usbharu.owl.publishTask { + this.producerId = this@DefaultOwlProducer.producerId + + this.publishedAt = timestamp { + this.seconds = now.epochSecond + this.nanos = now.nano + } + this.name = taskDefinition.name + this.properties.putAll(properties) + } + ) + + return PublishedTask( + task, + java.util.UUID(publishTask.id.mostSignificantUuidBits, publishTask.id.leastSignificantUuidBits), + now + ) + } + + override suspend fun stop() { + defaultOwlProducerConfig.channel.shutdownNow() + } +} \ No newline at end of file diff --git a/owl/owl-producer/owl-producer-default/src/main/kotlin/dev/usbharu/owl/producer/defaultimpl/DefaultOwlProducerBuilder.kt b/owl/owl-producer/owl-producer-default/src/main/kotlin/dev/usbharu/owl/producer/defaultimpl/DefaultOwlProducerBuilder.kt new file mode 100644 index 00000000..4e45e9f4 --- /dev/null +++ b/owl/owl-producer/owl-producer-default/src/main/kotlin/dev/usbharu/owl/producer/defaultimpl/DefaultOwlProducerBuilder.kt @@ -0,0 +1,47 @@ +/* + * Copyright (C) 2024 usbharu + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package dev.usbharu.owl.producer.defaultimpl + +import dev.usbharu.owl.producer.api.OwlProducerBuilder +import io.grpc.ManagedChannelBuilder + +class DefaultOwlProducerBuilder : OwlProducerBuilder { + + var config: DefaultOwlProducerConfig = config() + + override fun config(): DefaultOwlProducerConfig { + val defaultOwlProducerConfig = DefaultOwlProducerConfig() + + with(defaultOwlProducerConfig) { + channel = ManagedChannelBuilder.forAddress("localhost", 50051).usePlaintext().build() + } + + return defaultOwlProducerConfig + } + + override fun build(): DefaultOwlProducer { + return DefaultOwlProducer( + config + ) + } + + override fun apply(owlProducerConfig: DefaultOwlProducerConfig) { + this.config = owlProducerConfig + } +} + +val DEFAULT by lazy { DefaultOwlProducerBuilder() } \ No newline at end of file diff --git a/owl/owl-producer/owl-producer-default/src/main/kotlin/dev/usbharu/owl/producer/defaultimpl/DefaultOwlProducerConfig.kt b/owl/owl-producer/owl-producer-default/src/main/kotlin/dev/usbharu/owl/producer/defaultimpl/DefaultOwlProducerConfig.kt new file mode 100644 index 00000000..1f955677 --- /dev/null +++ b/owl/owl-producer/owl-producer-default/src/main/kotlin/dev/usbharu/owl/producer/defaultimpl/DefaultOwlProducerConfig.kt @@ -0,0 +1,48 @@ +/* + * Copyright (C) 2024 usbharu + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package dev.usbharu.owl.producer.defaultimpl + +import dev.usbharu.owl.common.property.PropertySerializerFactory +import dev.usbharu.owl.producer.api.OwlProducerConfig +import io.grpc.Channel +import io.grpc.ManagedChannel + +/** + * デフォルトの[dev.usbharu.owl.producer.api.OwlProducer]の構成 + * + */ +class DefaultOwlProducerConfig : OwlProducerConfig { + /** + * gRPCで使用する[Channel] + */ + lateinit var channel: ManagedChannel + + /** + * プロデューサー名 + */ + lateinit var name: String + + /** + * プロデューサーのホスト名 + */ + lateinit var hostname: String + + /** + * [dev.usbharu.owl.common.property.PropertyValue]のシリアライズに使用するファクトリ + */ + lateinit var propertySerializerFactory: PropertySerializerFactory +} \ No newline at end of file diff --git a/owl/owl-producer/owl-producer-embedded/build.gradle.kts b/owl/owl-producer/owl-producer-embedded/build.gradle.kts new file mode 100644 index 00000000..945078e0 --- /dev/null +++ b/owl/owl-producer/owl-producer-embedded/build.gradle.kts @@ -0,0 +1,26 @@ +plugins { + alias(libs.plugins.kotlin.jvm) +} + +group = "dev.usbharu" +version = "0.0.1" + +repositories { + mavenCentral() +} + +dependencies { + testImplementation(kotlin("test")) + implementation(project(":owl-producer:owl-producer-api")) + implementation(project(":owl-broker")) + implementation(platform("io.insert-koin:koin-bom:3.5.6")) + implementation("io.insert-koin:koin-core") + implementation("org.jetbrains.kotlinx:kotlinx-coroutines-core:1.8.1") +} + +tasks.test { + useJUnitPlatform() +} +kotlin { + jvmToolchain(21) +} \ No newline at end of file diff --git a/owl/owl-producer/owl-producer-embedded/src/main/kotlin/dev/usbharu/owl/producer/embedded/EmbeddedGrpcOwlProducer.kt b/owl/owl-producer/owl-producer-embedded/src/main/kotlin/dev/usbharu/owl/producer/embedded/EmbeddedGrpcOwlProducer.kt new file mode 100644 index 00000000..cbf29443 --- /dev/null +++ b/owl/owl-producer/owl-producer-embedded/src/main/kotlin/dev/usbharu/owl/producer/embedded/EmbeddedGrpcOwlProducer.kt @@ -0,0 +1,62 @@ +/* + * Copyright (C) 2024 usbharu + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package dev.usbharu.owl.producer.embedded + +import dev.usbharu.owl.broker.OwlBrokerApplication +import dev.usbharu.owl.broker.mainModule +import dev.usbharu.owl.common.retry.RetryPolicyFactory +import dev.usbharu.owl.common.task.PublishedTask +import dev.usbharu.owl.common.task.Task +import dev.usbharu.owl.common.task.TaskDefinition +import dev.usbharu.owl.producer.api.OwlProducer +import org.koin.core.Koin +import org.koin.core.context.GlobalContext.startKoin +import org.koin.dsl.module + +class EmbeddedGrpcOwlProducer( + private val config: EmbeddedGrpcOwlProducerConfig, +) : OwlProducer { + + private lateinit var application: Koin + + override suspend fun start() { + application = startKoin { + printLogger() + + val module = module { + single { + config.retryPolicyFactory + } + } + modules(mainModule, module, config.moduleContext.module()) + }.koin + + application.get().start(config.port.toInt()) + } + + override suspend fun registerTask(taskDefinition: TaskDefinition) { + config.owlProducer.registerTask(taskDefinition) + } + + override suspend fun publishTask(task: T): PublishedTask { + return config.owlProducer.publishTask(task) + } + + override suspend fun stop() { + config.owlProducer.stop() + } +} \ No newline at end of file diff --git a/owl/owl-producer/owl-producer-embedded/src/main/kotlin/dev/usbharu/owl/producer/embedded/EmbeddedGrpcOwlProducerBuilder.kt b/owl/owl-producer/owl-producer-embedded/src/main/kotlin/dev/usbharu/owl/producer/embedded/EmbeddedGrpcOwlProducerBuilder.kt new file mode 100644 index 00000000..be8b04c2 --- /dev/null +++ b/owl/owl-producer/owl-producer-embedded/src/main/kotlin/dev/usbharu/owl/producer/embedded/EmbeddedGrpcOwlProducerBuilder.kt @@ -0,0 +1,39 @@ +/* + * Copyright (C) 2024 usbharu + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package dev.usbharu.owl.producer.embedded + +import dev.usbharu.owl.producer.api.OwlProducerBuilder + +class EmbeddedGrpcOwlProducerBuilder : OwlProducerBuilder { + private var config = config() + + override fun config(): EmbeddedGrpcOwlProducerConfig { + return EmbeddedGrpcOwlProducerConfig() + } + + override fun build(): EmbeddedGrpcOwlProducer { + return EmbeddedGrpcOwlProducer( + config + ) + } + + override fun apply(owlProducerConfig: EmbeddedGrpcOwlProducerConfig) { + this.config = owlProducerConfig + } +} + +val EMBEDDED_GRPC by lazy { EmbeddedGrpcOwlProducerBuilder() } \ No newline at end of file diff --git a/owl/owl-producer/owl-producer-embedded/src/main/kotlin/dev/usbharu/owl/producer/embedded/EmbeddedGrpcOwlProducerConfig.kt b/owl/owl-producer/owl-producer-embedded/src/main/kotlin/dev/usbharu/owl/producer/embedded/EmbeddedGrpcOwlProducerConfig.kt new file mode 100644 index 00000000..1efe8bbe --- /dev/null +++ b/owl/owl-producer/owl-producer-embedded/src/main/kotlin/dev/usbharu/owl/producer/embedded/EmbeddedGrpcOwlProducerConfig.kt @@ -0,0 +1,29 @@ +/* + * Copyright (C) 2024 usbharu + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package dev.usbharu.owl.producer.embedded + +import dev.usbharu.owl.broker.ModuleContext +import dev.usbharu.owl.common.retry.RetryPolicyFactory +import dev.usbharu.owl.producer.api.OwlProducer +import dev.usbharu.owl.producer.api.OwlProducerConfig + +class EmbeddedGrpcOwlProducerConfig : OwlProducerConfig { + lateinit var moduleContext: ModuleContext + lateinit var retryPolicyFactory: RetryPolicyFactory + lateinit var port: String + lateinit var owlProducer: OwlProducer +} \ No newline at end of file diff --git a/owl/owl-producer/owl-producer-embedded/src/main/kotlin/dev/usbharu/owl/producer/embedded/EmbeddedOwlProducer.kt b/owl/owl-producer/owl-producer-embedded/src/main/kotlin/dev/usbharu/owl/producer/embedded/EmbeddedOwlProducer.kt new file mode 100644 index 00000000..c9a30c9c --- /dev/null +++ b/owl/owl-producer/owl-producer-embedded/src/main/kotlin/dev/usbharu/owl/producer/embedded/EmbeddedOwlProducer.kt @@ -0,0 +1,120 @@ +/* + * Copyright (C) 2024 usbharu + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package dev.usbharu.owl.producer.embedded + +import dev.usbharu.owl.broker.OwlBrokerApplication +import dev.usbharu.owl.broker.domain.exception.InvalidRepositoryException +import dev.usbharu.owl.broker.domain.model.producer.ProducerRepository +import dev.usbharu.owl.broker.mainModule +import dev.usbharu.owl.broker.service.* +import dev.usbharu.owl.common.property.PropertySerializerFactory +import dev.usbharu.owl.common.retry.RetryPolicyFactory +import dev.usbharu.owl.common.task.PublishedTask +import dev.usbharu.owl.common.task.Task +import dev.usbharu.owl.common.task.TaskDefinition +import dev.usbharu.owl.producer.api.OwlProducer +import org.koin.core.Koin +import org.koin.core.context.GlobalContext +import org.koin.core.context.GlobalContext.startKoin +import org.koin.dsl.module +import java.time.Instant +import java.util.* +import dev.usbharu.owl.broker.domain.model.taskdefinition.TaskDefinition as BrokerTaskDefinition + +class EmbeddedOwlProducer( + private val embeddedOwlProducerConfig: EmbeddedOwlProducerConfig, +) : OwlProducer { + + private lateinit var producerId: UUID + + private lateinit var application: Koin + + private lateinit var brokerApplication: OwlBrokerApplication + + private val taskMap: MutableMap, TaskDefinition<*>> = mutableMapOf() + + override suspend fun start() { + GlobalContext.stopKoin() + application = startKoin { + printLogger() + + val module = module { + single { + embeddedOwlProducerConfig.retryPolicyFactory + } + single { + embeddedOwlProducerConfig.propertySerializerFactory + } + } + modules(mainModule, module, embeddedOwlProducerConfig.moduleContext.module()) + }.koin + + application.getOrNull() + ?: throw InvalidRepositoryException("Repository not found. Install owl-broker-mongodb, etc. on the classpath") + + val producerService = application.get() + + producerId = producerService.registerProducer( + RegisterProducerRequest( + embeddedOwlProducerConfig.name, + embeddedOwlProducerConfig.name + ) + ) + + brokerApplication = application.get() + brokerApplication.start(embeddedOwlProducerConfig.port.toInt()) + } + + override suspend fun registerTask(taskDefinition: TaskDefinition) { + application.get() + .registerTask( + BrokerTaskDefinition( + name = taskDefinition.name, + priority = taskDefinition.priority, + maxRetry = taskDefinition.maxRetry, + timeoutMilli = taskDefinition.timeoutMilli, + propertyDefinitionHash = taskDefinition.propertyDefinition.hash(), + retryPolicy = taskDefinition.retryPolicy + ) + ) + + taskMap[taskDefinition.type] = taskDefinition + } + + override suspend fun publishTask(task: T): PublishedTask { + + val taskDefinition = taskMap.getValue(task::class.java) as TaskDefinition + + val publishTask = application.get().publishTask( + PublishTask( + taskDefinition.name, + producerId, + taskDefinition.serialize(task) + ) + ) + + return PublishedTask( + task, + publishTask.id, + Instant.now() + ) + } + + override suspend fun stop() { + brokerApplication.stop() + } +} \ No newline at end of file diff --git a/owl/owl-producer/owl-producer-embedded/src/main/kotlin/dev/usbharu/owl/producer/embedded/EmbeddedOwlProducerBuilder.kt b/owl/owl-producer/owl-producer-embedded/src/main/kotlin/dev/usbharu/owl/producer/embedded/EmbeddedOwlProducerBuilder.kt new file mode 100644 index 00000000..e92b6fc9 --- /dev/null +++ b/owl/owl-producer/owl-producer-embedded/src/main/kotlin/dev/usbharu/owl/producer/embedded/EmbeddedOwlProducerBuilder.kt @@ -0,0 +1,52 @@ +/* + * Copyright (C) 2024 usbharu + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package dev.usbharu.owl.producer.embedded + +import dev.usbharu.owl.broker.EmptyModuleContext +import dev.usbharu.owl.common.retry.DefaultRetryPolicyFactory +import dev.usbharu.owl.common.retry.ExponentialRetryPolicy +import dev.usbharu.owl.producer.api.OwlProducerBuilder + +class EmbeddedOwlProducerBuilder : OwlProducerBuilder { + var config: EmbeddedOwlProducerConfig = config() + + override fun config(): EmbeddedOwlProducerConfig { + val embeddedOwlProducerConfig = EmbeddedOwlProducerConfig() + + with(embeddedOwlProducerConfig) { + moduleContext = EmptyModuleContext + retryPolicyFactory = DefaultRetryPolicyFactory(mapOf("" to ExponentialRetryPolicy())) + name = "embedded-owl-producer" + port = "50051" + } + + return embeddedOwlProducerConfig + } + + override fun build(): EmbeddedOwlProducer { + return EmbeddedOwlProducer( + config + ) + } + + override fun apply(owlProducerConfig: EmbeddedOwlProducerConfig) { + this.config = owlProducerConfig + } + +} + +val EMBEDDED by lazy { EmbeddedOwlProducerBuilder() } \ No newline at end of file diff --git a/owl/owl-producer/owl-producer-embedded/src/main/kotlin/dev/usbharu/owl/producer/embedded/EmbeddedOwlProducerConfig.kt b/owl/owl-producer/owl-producer-embedded/src/main/kotlin/dev/usbharu/owl/producer/embedded/EmbeddedOwlProducerConfig.kt new file mode 100644 index 00000000..61e60220 --- /dev/null +++ b/owl/owl-producer/owl-producer-embedded/src/main/kotlin/dev/usbharu/owl/producer/embedded/EmbeddedOwlProducerConfig.kt @@ -0,0 +1,30 @@ +/* + * Copyright (C) 2024 usbharu + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package dev.usbharu.owl.producer.embedded + +import dev.usbharu.owl.broker.ModuleContext +import dev.usbharu.owl.common.property.CustomPropertySerializerFactory +import dev.usbharu.owl.common.retry.RetryPolicyFactory +import dev.usbharu.owl.producer.api.OwlProducerConfig + +class EmbeddedOwlProducerConfig : OwlProducerConfig { + lateinit var moduleContext: ModuleContext + lateinit var retryPolicyFactory: RetryPolicyFactory + lateinit var propertySerializerFactory: CustomPropertySerializerFactory + lateinit var name: String + lateinit var port: String +} diff --git a/owl/settings.gradle.kts b/owl/settings.gradle.kts new file mode 100644 index 00000000..b78e970f --- /dev/null +++ b/owl/settings.gradle.kts @@ -0,0 +1,27 @@ +plugins { + id("org.gradle.toolchains.foojay-resolver-convention") version "0.8.0" +} +dependencyResolutionManagement { + repositories { + mavenCentral() + } + + versionCatalogs { + create("libs") { + from(files("../libs.versions.toml")) + } + } +} +rootProject.name = "owl" +include("owl-common") +include("owl-producer:owl-producer-api") +findProject(":owl-producer:owl-producer-api")?.name = "owl-producer-api" +include("owl-broker") +include("owl-broker:owl-broker-mongodb") +findProject(":owl-broker:owl-broker-mongodb")?.name = "owl-broker-mongodb" +include("owl-producer:owl-producer-default") +findProject(":owl-producer:owl-producer-default")?.name = "owl-producer-default" +include("owl-consumer") +include("owl-producer:owl-producer-embedded") +include("owl-common:owl-common-serialize-jackson") +findProject(":owl-common:owl-common-serialize-jackson")?.name = "owl-common-serialize-jackson" diff --git a/renovate.json b/renovate.json new file mode 100644 index 00000000..a1d57573 --- /dev/null +++ b/renovate.json @@ -0,0 +1,20 @@ +{ + "$schema": "https://docs.renovatebot.com/renovate-schema.json", + "extends": [ + "config:recommended" + ], + "autoApprove": true, + "platformAutomerge": true, + "prHourlyLimit": 0, + "labels": [ + "dependencies", + "renovate" + ], + "prConcurrentLimit": 5, + "packageRules": [ + { + "matchUpdateTypes": ["patch","minor"], + "automerge": true + } + ] +} diff --git a/settings.gradle.kts b/settings.gradle.kts index 76167ae3..8c6faaff 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -1 +1,35 @@ -rootProject.name = "hideout" \ No newline at end of file +/* + * Copyright (C) 2024 usbharu + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +plugins { + id("org.gradle.toolchains.foojay-resolver-convention") version "0.8.0" +} +rootProject.name = "hideout" + +includeBuild("hideout-core") +includeBuild("hideout-mastodon") + +dependencyResolutionManagement { + repositories { + mavenCentral() + } + + versionCatalogs { + create("libs") { + from(files("libs.versions.toml")) + } + } +} \ No newline at end of file diff --git a/src/main/kotlin/dev/usbharu/hideout/Application.kt b/src/main/kotlin/dev/usbharu/hideout/Application.kt deleted file mode 100644 index 320ec162..00000000 --- a/src/main/kotlin/dev/usbharu/hideout/Application.kt +++ /dev/null @@ -1,117 +0,0 @@ -package dev.usbharu.hideout - -import com.fasterxml.jackson.annotation.JsonInclude -import com.fasterxml.jackson.databind.DeserializationFeature -import com.fasterxml.jackson.module.kotlin.jacksonObjectMapper -import dev.usbharu.hideout.config.Config -import dev.usbharu.hideout.config.ConfigData -import dev.usbharu.hideout.domain.model.job.ReceiveFollowJob -import dev.usbharu.hideout.plugins.* -import dev.usbharu.hideout.repository.IUserAuthRepository -import dev.usbharu.hideout.repository.IUserRepository -import dev.usbharu.hideout.repository.UserAuthRepository -import dev.usbharu.hideout.repository.UserRepository -import dev.usbharu.hideout.routing.register -import dev.usbharu.hideout.service.IUserAuthService -import dev.usbharu.hideout.service.activitypub.* -import dev.usbharu.hideout.service.impl.UserAuthService -import dev.usbharu.hideout.service.impl.UserService -import dev.usbharu.hideout.service.job.JobQueueParentService -import dev.usbharu.hideout.service.job.KJobJobQueueParentService -import dev.usbharu.hideout.service.signature.HttpSignatureVerifyService -import dev.usbharu.hideout.service.signature.HttpSignatureVerifyServiceImpl -import dev.usbharu.kjob.exposed.ExposedKJob -import io.ktor.client.* -import io.ktor.client.engine.cio.* -import io.ktor.client.plugins.logging.* -import io.ktor.server.application.* -import kjob.core.Job -import kjob.core.KJob -import kjob.core.dsl.JobContextWithProps -import kjob.core.dsl.JobRegisterContext -import kjob.core.dsl.KJobFunctions -import kjob.core.kjob -import org.jetbrains.exposed.sql.Database -import org.koin.ktor.ext.inject - -fun main(args: Array): Unit = io.ktor.server.netty.EngineMain.main(args) - -val Application.property: Application.(propertyName: String) -> String - get() = { - environment.config.property(it).getString() - } - -@Suppress("unused") // application.conf references the main function. This annotation prevents the IDE from marking it as unused. -fun Application.parent() { - - Config.configData = ConfigData( - url = property("hideout.url"), - objectMapper = jacksonObjectMapper().enable(DeserializationFeature.ACCEPT_SINGLE_VALUE_AS_ARRAY) - .setSerializationInclusion(JsonInclude.Include.NON_EMPTY) - .configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false) - ) - - val module = org.koin.dsl.module { - single { - Database.connect( - url = property("hideout.database.url"), - driver = property("hideout.database.driver"), - user = property("hideout.database.username"), - password = property("hideout.database.password") - ) - } - - single { UserRepository(get()) } - single { UserAuthRepository(get()) } - single { UserAuthService(get(), get()) } - single { HttpSignatureVerifyServiceImpl(get()) } - single { - val kJobJobQueueService = KJobJobQueueParentService(get()) - kJobJobQueueService.init(listOf()) - kJobJobQueueService - } - single { - HttpClient(CIO).config { - install(Logging) { - logger = Logger.DEFAULT - level = LogLevel.ALL - } - install(httpSignaturePlugin){ - keyMap = KtorKeyMap(get()) - } - } - } - single { ActivityPubFollowServiceImpl(get(), get(), get(),get()) } - single { ActivityPubServiceImpl(get()) } - single { UserService(get()) } - single { ActivityPubUserServiceImpl(get(), get(), get()) } - } - - - configureKoin(module) - configureHTTP() - configureSockets() - configureMonitoring() - configureSerialization() - register(inject().value) - configureRouting( - inject().value, - inject().value, - inject().value, - inject().value - ) -} -@Suppress("unused") -fun Application.worker() { - val kJob = kjob(ExposedKJob) { - connectionDatabase = inject().value - }.start() - - val activityPubService = inject().value - - kJob.register(ReceiveFollowJob){ - execute { - activityPubService.processActivity(this,it) - } - } -} diff --git a/src/main/kotlin/dev/usbharu/hideout/ap/Accept.kt b/src/main/kotlin/dev/usbharu/hideout/ap/Accept.kt deleted file mode 100644 index 35b822c8..00000000 --- a/src/main/kotlin/dev/usbharu/hideout/ap/Accept.kt +++ /dev/null @@ -1,38 +0,0 @@ -package dev.usbharu.hideout.ap - -open class Accept : Object { - public var `object`:Object? = null - public var actor:String? = null - protected constructor() : super() - constructor( - type: List = emptyList(), - name: String, - `object`: Object?, - actor: String? - ) : super(add(type,"Accept"), name) { - this.`object` = `object` - this.actor = actor - } - - override fun equals(other: Any?): Boolean { - if (this === other) return true - if (other !is Accept) return false - if (!super.equals(other)) return false - - if (`object` != other.`object`) return false - return actor == other.actor - } - - override fun hashCode(): Int { - var result = super.hashCode() - result = 31 * result + (`object`?.hashCode() ?: 0) - result = 31 * result + (actor?.hashCode() ?: 0) - return result - } - - override fun toString(): String { - return "Accept(`object`=$`object`, actor=$actor) ${super.toString()}" - } - - -} diff --git a/src/main/kotlin/dev/usbharu/hideout/ap/Follow.kt b/src/main/kotlin/dev/usbharu/hideout/ap/Follow.kt deleted file mode 100644 index b2ab16c6..00000000 --- a/src/main/kotlin/dev/usbharu/hideout/ap/Follow.kt +++ /dev/null @@ -1,18 +0,0 @@ -package dev.usbharu.hideout.ap - -open class Follow : Object{ - public var `object`:String? = null - public var actor:String? = null - protected constructor() : super() - constructor( - type: List = emptyList(), - name: String, - `object`: String?, - actor: String? - ) : super(add(type,"Follow"), name) { - this.`object` = `object` - this.actor = actor - } - - -} diff --git a/src/main/kotlin/dev/usbharu/hideout/ap/Image.kt b/src/main/kotlin/dev/usbharu/hideout/ap/Image.kt deleted file mode 100644 index 29639e20..00000000 --- a/src/main/kotlin/dev/usbharu/hideout/ap/Image.kt +++ /dev/null @@ -1,33 +0,0 @@ -package dev.usbharu.hideout.ap - -open class Image : Object { - private var mediaType: String? = null - private var url: String? = null - - protected constructor() : super() - constructor(type: List = emptyList(), name: String, mediaType: String?, url: String?) : super( - add(type,"Image"), - name - ) { - this.mediaType = mediaType - this.url = url - } - - override fun equals(other: Any?): Boolean { - if (this === other) return true - if (other !is Image) return false - if (!super.equals(other)) return false - - if (mediaType != other.mediaType) return false - return url == other.url - } - - override fun hashCode(): Int { - var result = super.hashCode() - result = 31 * result + (mediaType?.hashCode() ?: 0) - result = 31 * result + (url?.hashCode() ?: 0) - return result - } - - -} diff --git a/src/main/kotlin/dev/usbharu/hideout/ap/JsonLd.kt b/src/main/kotlin/dev/usbharu/hideout/ap/JsonLd.kt deleted file mode 100644 index 285319bd..00000000 --- a/src/main/kotlin/dev/usbharu/hideout/ap/JsonLd.kt +++ /dev/null @@ -1,74 +0,0 @@ -package dev.usbharu.hideout.ap - -import com.fasterxml.jackson.annotation.JsonAutoDetect -import com.fasterxml.jackson.annotation.JsonCreator -import com.fasterxml.jackson.annotation.JsonProperty -import com.fasterxml.jackson.core.JsonGenerator -import com.fasterxml.jackson.core.TreeNode -import com.fasterxml.jackson.databind.JsonDeserializer -import com.fasterxml.jackson.databind.JsonNode -import com.fasterxml.jackson.databind.JsonSerializer -import com.fasterxml.jackson.databind.SerializerProvider -import com.fasterxml.jackson.databind.annotation.JsonDeserialize -import com.fasterxml.jackson.databind.annotation.JsonSerialize - -@JsonAutoDetect(fieldVisibility = JsonAutoDetect.Visibility.ANY) -open class JsonLd { - @JsonProperty("@context") - @JsonDeserialize(contentUsing = ContextDeserializer::class) - @JsonSerialize(using = ContextSerializer::class) - var context:List = emptyList() - - @JsonCreator - constructor(context:List){ - this.context = context - } - - protected constructor() - - override fun equals(other: Any?): Boolean { - if (this === other) return true - if (other !is JsonLd) return false - - return context == other.context - } - - override fun hashCode(): Int { - return context.hashCode() - } - - override fun toString(): String { - return "JsonLd(context=$context)" - } - - -} - -public class ContextDeserializer : JsonDeserializer() { - override fun deserialize(p0: com.fasterxml.jackson.core.JsonParser?, p1: com.fasterxml.jackson.databind.DeserializationContext?): String { - val readTree : JsonNode = p0?.codec?.readTree(p0) ?: return "" - if (readTree.isObject) { - return "" - } - return readTree.asText() - } -} - -public class ContextSerializer : JsonSerializer>() { - override fun serialize(value: List?, gen: JsonGenerator?, serializers: SerializerProvider?) { - if (value.isNullOrEmpty()) { - gen?.writeNull() - return - } - if (value?.size == 1) { - gen?.writeString(value[0]) - } else { - gen?.writeStartArray() - value?.forEach { - gen?.writeString(it) - } - gen?.writeEndArray() - } - } - -} diff --git a/src/main/kotlin/dev/usbharu/hideout/ap/Key.kt b/src/main/kotlin/dev/usbharu/hideout/ap/Key.kt deleted file mode 100644 index b3dccdfd..00000000 --- a/src/main/kotlin/dev/usbharu/hideout/ap/Key.kt +++ /dev/null @@ -1,39 +0,0 @@ -package dev.usbharu.hideout.ap - -open class Key : Object{ - var id:String? = null - var owner:String? = null - var publicKeyPem:String? = null - protected constructor() : super() - constructor( - type: List, - name: String, - id: String?, - owner: String?, - publicKeyPem: String? - ) : super(add(type,"Key"), name) { - this.id = id - this.owner = owner - this.publicKeyPem = publicKeyPem - } - - override fun equals(other: Any?): Boolean { - if (this === other) return true - if (other !is Key) return false - if (!super.equals(other)) return false - - if (id != other.id) return false - if (owner != other.owner) return false - return publicKeyPem == other.publicKeyPem - } - - override fun hashCode(): Int { - var result = super.hashCode() - result = 31 * result + (id?.hashCode() ?: 0) - result = 31 * result + (owner?.hashCode() ?: 0) - result = 31 * result + (publicKeyPem?.hashCode() ?: 0) - return result - } - - -} diff --git a/src/main/kotlin/dev/usbharu/hideout/ap/Object.kt b/src/main/kotlin/dev/usbharu/hideout/ap/Object.kt deleted file mode 100644 index 27362b30..00000000 --- a/src/main/kotlin/dev/usbharu/hideout/ap/Object.kt +++ /dev/null @@ -1,63 +0,0 @@ -package dev.usbharu.hideout.ap - -import com.fasterxml.jackson.core.JsonGenerator -import com.fasterxml.jackson.databind.JsonSerializer -import com.fasterxml.jackson.databind.SerializerProvider -import com.fasterxml.jackson.databind.annotation.JsonSerialize - -open class Object : JsonLd { - @JsonSerialize(using = TypeSerializer::class) - private var type: List = emptyList() - var name: String? = null - - protected constructor() - constructor(type: List, name: String) : super() { - this.type = type - this.name = name - } - - companion object { - @JvmStatic - protected fun add(list:List,type:String):List { - val toMutableList = list.toMutableList() - toMutableList.add(type) - return toMutableList.distinct() - } - } - - override fun equals(other: Any?): Boolean { - if (this === other) return true - if (other !is Object) return false - - if (type != other.type) return false - return name == other.name - } - - override fun hashCode(): Int { - var result = type.hashCode() - result = 31 * result + (name?.hashCode() ?: 0) - return result - } - - override fun toString(): String { - return "Object(type=$type, name=$name) ${super.toString()}" - } - - -} - -public class TypeSerializer : JsonSerializer>() { - override fun serialize(value: List?, gen: JsonGenerator?, serializers: SerializerProvider?) { - println(value) - if (value?.size == 1) { - gen?.writeString(value[0]) - } else { - gen?.writeStartArray() - value?.forEach { - gen?.writeString(it) - } - gen?.writeEndArray() - } - } - -} diff --git a/src/main/kotlin/dev/usbharu/hideout/ap/Person.kt b/src/main/kotlin/dev/usbharu/hideout/ap/Person.kt deleted file mode 100644 index 148892b0..00000000 --- a/src/main/kotlin/dev/usbharu/hideout/ap/Person.kt +++ /dev/null @@ -1,62 +0,0 @@ -package dev.usbharu.hideout.ap - -open class Person : Object { - private var id:String? = null - var preferredUsername:String? = null - var summary:String? = null - var inbox:String? = null - var outbox:String? = null - private var url:String? = null - private var icon:Image? = null - var publicKey:Key? = null - protected constructor() : super() - constructor( - type: List = emptyList(), - name: String, - id: String?, - preferredUsername: String?, - summary: String?, - inbox: String?, - outbox: String?, - url: String?, - icon: Image?, - publicKey: Key? - ) : super(add(type,"Person"), name) { - this.id = id - this.preferredUsername = preferredUsername - this.summary = summary - this.inbox = inbox - this.outbox = outbox - this.url = url - this.icon = icon - this.publicKey = publicKey - } - - override fun equals(other: Any?): Boolean { - if (this === other) return true - if (other !is Person) 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 - if (outbox != other.outbox) return false - if (url != other.url) return false - if (icon != other.icon) return false - return publicKey == other.publicKey - } - - override fun hashCode(): Int { - var result = id?.hashCode() ?: 0 - 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 + (icon?.hashCode() ?: 0) - result = 31 * result + (publicKey?.hashCode() ?: 0) - return result - } - - -} diff --git a/src/main/kotlin/dev/usbharu/hideout/config/Config.kt b/src/main/kotlin/dev/usbharu/hideout/config/Config.kt deleted file mode 100644 index 02358c41..00000000 --- a/src/main/kotlin/dev/usbharu/hideout/config/Config.kt +++ /dev/null @@ -1,14 +0,0 @@ -package dev.usbharu.hideout.config - -import com.fasterxml.jackson.databind.ObjectMapper -import com.fasterxml.jackson.module.kotlin.jacksonObjectMapper - -object Config { - var configData: ConfigData = ConfigData() -} - -data class ConfigData( - val url: String = "", - val domain: String = url.substringAfter("://").substringBeforeLast(":"), - val objectMapper: ObjectMapper = jacksonObjectMapper() -) diff --git a/src/main/kotlin/dev/usbharu/hideout/domain/model/ActivityPubStringResponse.kt b/src/main/kotlin/dev/usbharu/hideout/domain/model/ActivityPubStringResponse.kt deleted file mode 100644 index 2e4b8b4d..00000000 --- a/src/main/kotlin/dev/usbharu/hideout/domain/model/ActivityPubStringResponse.kt +++ /dev/null @@ -1,24 +0,0 @@ -package dev.usbharu.hideout.domain.model - -import dev.usbharu.hideout.ap.JsonLd -import dev.usbharu.hideout.util.HttpUtil.Activity -import io.ktor.http.* - -sealed class ActivityPubResponse( - val httpStatusCode: HttpStatusCode, - val contentType: ContentType = ContentType.Application.Activity -) - -class ActivityPubStringResponse( - httpStatusCode: HttpStatusCode = HttpStatusCode.OK, - val message: String, - contentType: ContentType = ContentType.Application.Activity -) : - ActivityPubResponse(httpStatusCode, contentType) - -class ActivityPubObjectResponse( - httpStatusCode: HttpStatusCode = HttpStatusCode.OK, - val message: JsonLd, - contentType: ContentType = ContentType.Application.Activity -) : - ActivityPubResponse(httpStatusCode, contentType) diff --git a/src/main/kotlin/dev/usbharu/hideout/domain/model/User.kt b/src/main/kotlin/dev/usbharu/hideout/domain/model/User.kt deleted file mode 100644 index 28e57f99..00000000 --- a/src/main/kotlin/dev/usbharu/hideout/domain/model/User.kt +++ /dev/null @@ -1,49 +0,0 @@ -package dev.usbharu.hideout.domain.model - -import org.jetbrains.exposed.dao.id.LongIdTable - -data class User( - val name: String, - val domain: String, - val screenName: String, - val description: String, - val inbox: String, - val outbox: String, - val url: String -) - -data class UserEntity( - val id: Long, - val name: String, - val domain: String, - val screenName: String, - val description: String, - val inbox: String, - val outbox: String, - val url: String -) { - constructor(id: Long, user: User) : this( - id, - user.name, - user.domain, - user.screenName, - user.description, - user.inbox, - user.outbox, - user.url - ) -} - -object Users : LongIdTable("users") { - val name = varchar("name", length = 64) - val domain = varchar("domain", length = 255) - val screenName = varchar("screen_name", length = 64) - val description = varchar("description", length = 600) - val inbox = varchar("inbox", length = 255).uniqueIndex() - val outbox = varchar("outbox", length = 255).uniqueIndex() - val url = varchar("url", length = 255).uniqueIndex() - - init { - uniqueIndex(name, domain) - } -} diff --git a/src/main/kotlin/dev/usbharu/hideout/domain/model/UserAuthentication.kt b/src/main/kotlin/dev/usbharu/hideout/domain/model/UserAuthentication.kt deleted file mode 100644 index d1cd3d82..00000000 --- a/src/main/kotlin/dev/usbharu/hideout/domain/model/UserAuthentication.kt +++ /dev/null @@ -1,34 +0,0 @@ -package dev.usbharu.hideout.domain.model - -import org.jetbrains.exposed.dao.id.LongIdTable -import org.jetbrains.exposed.sql.ReferenceOption - -data class UserAuthentication( - val userId: Long, - val hash: String?, - val publicKey: String, - val privateKey: String? -) - -data class UserAuthenticationEntity( - val id: Long, - val userId: Long, - val hash: String?, - val publicKey: String, - val privateKey: String? -) { - constructor(id: Long, userAuthentication: UserAuthentication) : this( - id, - userAuthentication.userId, - userAuthentication.hash, - userAuthentication.publicKey, - userAuthentication.privateKey - ) -} - -object UsersAuthentication : LongIdTable("users_auth") { - val userId = long("user_id").references(Users.id, onUpdate = ReferenceOption.CASCADE) - val hash = varchar("hash", length = 64).nullable() - val publicKey = varchar("public_key", length = 1000_000) - val privateKey = varchar("private_key", length = 1000_000).nullable() -} diff --git a/src/main/kotlin/dev/usbharu/hideout/domain/model/UsersFollowers.kt b/src/main/kotlin/dev/usbharu/hideout/domain/model/UsersFollowers.kt deleted file mode 100644 index 1f5de8ce..00000000 --- a/src/main/kotlin/dev/usbharu/hideout/domain/model/UsersFollowers.kt +++ /dev/null @@ -1,11 +0,0 @@ -package dev.usbharu.hideout.domain.model - -import org.jetbrains.exposed.dao.id.LongIdTable - -object UsersFollowers : LongIdTable("users_followers") { - val userId = long("user_id").references(Users.id).index() - val followerId = long("follower_id").references(Users.id) - init { - uniqueIndex(userId, followerId) - } -} diff --git a/src/main/kotlin/dev/usbharu/hideout/domain/model/job/HideoutJob.kt b/src/main/kotlin/dev/usbharu/hideout/domain/model/job/HideoutJob.kt deleted file mode 100644 index c499807c..00000000 --- a/src/main/kotlin/dev/usbharu/hideout/domain/model/job/HideoutJob.kt +++ /dev/null @@ -1,11 +0,0 @@ -package dev.usbharu.hideout.domain.model.job - -import kjob.core.Job - -sealed class HideoutJob(name: String = "") : Job(name) - -object ReceiveFollowJob : HideoutJob("ReceiveFollowJob"){ - val actor = string("actor") - val follow = string("follow") - val targetActor = string("targetActor") -} diff --git a/src/main/kotlin/dev/usbharu/hideout/domain/model/wellknown/WebFinger.kt b/src/main/kotlin/dev/usbharu/hideout/domain/model/wellknown/WebFinger.kt deleted file mode 100644 index b8542f23..00000000 --- a/src/main/kotlin/dev/usbharu/hideout/domain/model/wellknown/WebFinger.kt +++ /dev/null @@ -1,5 +0,0 @@ -package dev.usbharu.hideout.domain.model.wellknown - -data class WebFinger(val subject:String,val links:List){ - data class Link(val rel:String,val type:String,val href:String) -} diff --git a/src/main/kotlin/dev/usbharu/hideout/exception/HttpSignatureVerifyException.kt b/src/main/kotlin/dev/usbharu/hideout/exception/HttpSignatureVerifyException.kt deleted file mode 100644 index d123025f..00000000 --- a/src/main/kotlin/dev/usbharu/hideout/exception/HttpSignatureVerifyException.kt +++ /dev/null @@ -1,8 +0,0 @@ -package dev.usbharu.hideout.exception - -class HttpSignatureVerifyException : IllegalArgumentException { - constructor() : super() - constructor(s: String?) : super(s) - constructor(message: String?, cause: Throwable?) : super(message, cause) - constructor(cause: Throwable?) : super(cause) -} diff --git a/src/main/kotlin/dev/usbharu/hideout/exception/IllegalParameterException.kt b/src/main/kotlin/dev/usbharu/hideout/exception/IllegalParameterException.kt deleted file mode 100644 index dd94d127..00000000 --- a/src/main/kotlin/dev/usbharu/hideout/exception/IllegalParameterException.kt +++ /dev/null @@ -1,8 +0,0 @@ -package dev.usbharu.hideout.exception - -class IllegalParameterException : IllegalArgumentException { - constructor() : super() - constructor(s: String?) : super(s) - constructor(message: String?, cause: Throwable?) : super(message, cause) - constructor(cause: Throwable?) : super(cause) -} diff --git a/src/main/kotlin/dev/usbharu/hideout/exception/JsonParseException.kt b/src/main/kotlin/dev/usbharu/hideout/exception/JsonParseException.kt deleted file mode 100644 index d5749f75..00000000 --- a/src/main/kotlin/dev/usbharu/hideout/exception/JsonParseException.kt +++ /dev/null @@ -1,8 +0,0 @@ -package dev.usbharu.hideout.exception - -class JsonParseException : IllegalArgumentException { - constructor() : super() - constructor(s: String?) : super(s) - constructor(message: String?, cause: Throwable?) : super(message, cause) - constructor(cause: Throwable?) : super(cause) -} diff --git a/src/main/kotlin/dev/usbharu/hideout/exception/ParameterNotExistException.kt b/src/main/kotlin/dev/usbharu/hideout/exception/ParameterNotExistException.kt deleted file mode 100644 index d3ca6693..00000000 --- a/src/main/kotlin/dev/usbharu/hideout/exception/ParameterNotExistException.kt +++ /dev/null @@ -1,8 +0,0 @@ -package dev.usbharu.hideout.exception - -class ParameterNotExistException : IllegalArgumentException { - constructor() : super() - constructor(s: String?) : super(s) - constructor(message: String?, cause: Throwable?) : super(message, cause) - constructor(cause: Throwable?) : super(cause) -} diff --git a/src/main/kotlin/dev/usbharu/hideout/exception/UserNotFoundException.kt b/src/main/kotlin/dev/usbharu/hideout/exception/UserNotFoundException.kt deleted file mode 100644 index 4634e141..00000000 --- a/src/main/kotlin/dev/usbharu/hideout/exception/UserNotFoundException.kt +++ /dev/null @@ -1,14 +0,0 @@ -package dev.usbharu.hideout.exception - -class UserNotFoundException : Exception { - 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) -} diff --git a/src/main/kotlin/dev/usbharu/hideout/exception/ap/IllegalActivityPubObjectException.kt b/src/main/kotlin/dev/usbharu/hideout/exception/ap/IllegalActivityPubObjectException.kt deleted file mode 100644 index 12965d62..00000000 --- a/src/main/kotlin/dev/usbharu/hideout/exception/ap/IllegalActivityPubObjectException.kt +++ /dev/null @@ -1,8 +0,0 @@ -package dev.usbharu.hideout.exception.ap - -class IllegalActivityPubObjectException : IllegalArgumentException { - constructor() : super() - constructor(s: String?) : super(s) - constructor(message: String?, cause: Throwable?) : super(message, cause) - constructor(cause: Throwable?) : super(cause) -} diff --git a/src/main/kotlin/dev/usbharu/hideout/plugins/ActivityPub.kt b/src/main/kotlin/dev/usbharu/hideout/plugins/ActivityPub.kt deleted file mode 100644 index 2c8d1d52..00000000 --- a/src/main/kotlin/dev/usbharu/hideout/plugins/ActivityPub.kt +++ /dev/null @@ -1,177 +0,0 @@ -package dev.usbharu.hideout.plugins - -import dev.usbharu.hideout.ap.JsonLd -import dev.usbharu.hideout.config.Config -import dev.usbharu.hideout.service.IUserAuthService -import dev.usbharu.hideout.service.impl.UserAuthService -import dev.usbharu.hideout.util.HttpUtil.Activity -import io.ktor.client.* -import io.ktor.client.plugins.api.* -import io.ktor.client.request.* -import io.ktor.client.statement.* -import io.ktor.http.* -import io.ktor.server.application.* -import io.ktor.server.response.* -import kotlinx.coroutines.runBlocking -import tech.barbero.http.message.signing.HttpMessage -import tech.barbero.http.message.signing.HttpMessageSigner -import tech.barbero.http.message.signing.HttpRequest -import tech.barbero.http.message.signing.KeyMap -import java.net.URI -import java.security.KeyFactory -import java.security.PrivateKey -import java.security.PublicKey -import java.security.spec.PKCS8EncodedKeySpec -import java.security.spec.X509EncodedKeySpec -import java.text.SimpleDateFormat -import java.util.* -import javax.crypto.SecretKey - -suspend fun ApplicationCall.respondAp(message: T, status: HttpStatusCode = HttpStatusCode.OK) { - message.context += "https://www.w3.org/ns/activitystreams" - val activityJson = Config.configData.objectMapper.writeValueAsString(message) - respondText(activityJson, ContentType.Application.Activity, status) -} - -suspend fun HttpClient.postAp(urlString: String, username: String, jsonLd: JsonLd): HttpResponse { - jsonLd.context += "https://www.w3.org/ns/activitystreams" - return this.post(urlString) { - header("Accept", ContentType.Application.Activity) - header("Content-Type", ContentType.Application.Activity) - header("Signature", "keyId=\"$username\",algorithm=\"rsa-sha256\",headers=\"(request-target) digest date\"") - val text = Config.configData.objectMapper.writeValueAsString(jsonLd) - setBody(text) - } -} - -class HttpSignaturePluginConfig { - lateinit var keyMap: KeyMap -} - -val httpSignaturePlugin = createClientPlugin("HttpSign", ::HttpSignaturePluginConfig) { - val keyMap = pluginConfig.keyMap - val format = SimpleDateFormat("EEE, dd MMM yyyy HH:mm:ss zzz", Locale.US) - format.timeZone = TimeZone.getTimeZone("GMT") - onRequest { request, body -> - - - request.header("Date", format.format(Date())) - println(request.bodyType) - println(request.bodyType?.type) - if (request.bodyType?.type == String::class) { - println(body as String) - println("Digest !!") -// UserAuthService.sha256.reset() - val digest = - Base64.getEncoder().encodeToString(UserAuthService.sha256.digest(body.toByteArray(Charsets.UTF_8))) - request.headers.append("Digest", "sha-256="+digest) - } - - if (request.headers.contains("Signature")) { - val all = request.headers.getAll("Signature")!! - val parameters = mutableListOf() - for (s in all) { - s.split(",").forEach { parameters.add(it) } - } - - val keyId = parameters.find { it.startsWith("keyId") }?.split("=")?.get(1)?.replace("\"", "") - val algorithm = - parameters.find { it.startsWith("algorithm") }?.split("=")?.get(1)?.replace("\"", "") - val headers = parameters.find { it.startsWith("headers") }?.split("=")?.get(1)?.replace("\"", "") - ?.split(" ")?.toMutableList().orEmpty() - - val algorithmType = when (algorithm) { - "rsa-sha256" -> { - HttpMessageSigner.Algorithm.RSA_SHA256 - } - - else -> { - TODO() - } - } - - headers.map { - when (it) { - "(request-target)" -> { - HttpMessageSigner.REQUEST_TARGET - } - - "digest" -> { - "Digest" - } - - "date" -> { - "Date" - } - - else -> { - it - } - } - } - - val builder = HttpMessageSigner.builder().algorithm(algorithmType).keyId(keyId).keyMap(keyMap) - var tmp = builder - headers.forEach { - tmp = tmp.addHeaderToSign(it) - } - val signer = tmp.build() - - request.headers.remove("Signature") - - signer!!.sign(object : HttpMessage, HttpRequest { - override fun headerValues(name: String?): MutableList { - return name?.let { request.headers.getAll(it) }?.toMutableList() ?: mutableListOf() - } - - override fun addHeader(name: String?, value: String?) { - val split = value?.split("=").orEmpty() - name?.let { request.header(it, split.get(0)+"=\""+split.get(1).trim('"')+"\"") } - } - - override fun method(): String { - return request.method.value - } - - override fun uri(): URI { - return request.url.build().toURI() - } - - - }) - } - - } -} - -class KtorKeyMap(private val userAuthRepository: IUserAuthService) : KeyMap { - override fun getPublicKey(keyId: String?): PublicKey = runBlocking { - val username = (keyId ?: throw IllegalArgumentException("keyId is null")).substringBeforeLast("#pubkey") - .substringAfterLast("/") - val publicBytes = Base64.getDecoder().decode( - userAuthRepository.findByUsername( - username - ).publicKey?.replace("-----BEGIN PUBLIC KEY-----", "-----END PUBLIC KEY-----")?.replace("", "") - ?.replace("\n", "") - ) - val x509EncodedKeySpec = X509EncodedKeySpec(publicBytes) - return@runBlocking KeyFactory.getInstance("RSA").generatePublic(x509EncodedKeySpec) - } - - override fun getPrivateKey(keyId: String?): PrivateKey = runBlocking { - val username = (keyId ?: throw IllegalArgumentException("keyId is null")).substringBeforeLast("#pubkey") - .substringAfterLast("/") - val publicBytes = Base64.getDecoder().decode( - userAuthRepository.findByUsername( - username - ).privateKey?.replace("-----BEGIN PRIVATE KEY-----", "")?.replace("-----END PRIVATE KEY-----", "") - ?.replace("\n", "") - ) - val x509EncodedKeySpec = PKCS8EncodedKeySpec(publicBytes) - return@runBlocking KeyFactory.getInstance("RSA").generatePrivate(x509EncodedKeySpec) - } - - override fun getSecretKey(keyId: String?): SecretKey { - TODO("Not yet implemented") - } -} diff --git a/src/main/kotlin/dev/usbharu/hideout/plugins/HTTP.kt b/src/main/kotlin/dev/usbharu/hideout/plugins/HTTP.kt deleted file mode 100644 index c89f9289..00000000 --- a/src/main/kotlin/dev/usbharu/hideout/plugins/HTTP.kt +++ /dev/null @@ -1,24 +0,0 @@ -package dev.usbharu.hideout.plugins - -import io.ktor.http.* -import io.ktor.server.plugins.cors.routing.* -import io.ktor.server.plugins.defaultheaders.* -import io.ktor.server.plugins.forwardedheaders.* -import io.ktor.server.application.* - -fun Application.configureHTTP() { - install(CORS) { - allowMethod(HttpMethod.Options) - allowMethod(HttpMethod.Put) - allowMethod(HttpMethod.Delete) - allowMethod(HttpMethod.Patch) - allowHeader(HttpHeaders.Authorization) - allowHeader("MyCustomHeader") - anyHost() // @TODO: Don't do this in production if possible. Try to limit it. - } - install(DefaultHeaders) { - header("X-Engine", "Ktor") // will send this header with each response - } - install(ForwardedHeaders) // WARNING: for security, do not include this if not behind a reverse proxy - install(XForwardedHeaders) // WARNING: for security, do not include this if not behind a reverse proxy -} diff --git a/src/main/kotlin/dev/usbharu/hideout/plugins/Koin.kt b/src/main/kotlin/dev/usbharu/hideout/plugins/Koin.kt deleted file mode 100644 index f880852b..00000000 --- a/src/main/kotlin/dev/usbharu/hideout/plugins/Koin.kt +++ /dev/null @@ -1,13 +0,0 @@ -package dev.usbharu.hideout.plugins - -import io.ktor.server.application.* -import org.koin.core.module.Module -import org.koin.ktor.plugin.Koin -import org.koin.logger.slf4jLogger - -fun Application.configureKoin(module: Module) { - install(Koin) { - slf4jLogger() - modules(module) - } -} diff --git a/src/main/kotlin/dev/usbharu/hideout/plugins/Monitoring.kt b/src/main/kotlin/dev/usbharu/hideout/plugins/Monitoring.kt deleted file mode 100644 index 3ce065b7..00000000 --- a/src/main/kotlin/dev/usbharu/hideout/plugins/Monitoring.kt +++ /dev/null @@ -1,12 +0,0 @@ -package dev.usbharu.hideout.plugins - -import io.ktor.server.plugins.callloging.* -import org.slf4j.event.* -import io.ktor.server.request.* -import io.ktor.server.application.* - -fun Application.configureMonitoring() { - install(CallLogging) { - level = Level.INFO - } -} diff --git a/src/main/kotlin/dev/usbharu/hideout/plugins/Routing.kt b/src/main/kotlin/dev/usbharu/hideout/plugins/Routing.kt deleted file mode 100644 index 6b44c0fd..00000000 --- a/src/main/kotlin/dev/usbharu/hideout/plugins/Routing.kt +++ /dev/null @@ -1,28 +0,0 @@ -package dev.usbharu.hideout.plugins - -import dev.usbharu.hideout.routing.activitypub.inbox -import dev.usbharu.hideout.routing.activitypub.outbox -import dev.usbharu.hideout.routing.activitypub.usersAP -import dev.usbharu.hideout.routing.wellknown.webfinger -import dev.usbharu.hideout.service.activitypub.ActivityPubService -import dev.usbharu.hideout.service.activitypub.ActivityPubUserService -import dev.usbharu.hideout.service.impl.UserService -import dev.usbharu.hideout.service.signature.HttpSignatureVerifyService -import io.ktor.server.application.* -import io.ktor.server.plugins.autohead.* -import io.ktor.server.routing.* - -fun Application.configureRouting( - httpSignatureVerifyService: HttpSignatureVerifyService, - activityPubService: ActivityPubService, - userService:UserService, - activityPubUserService: ActivityPubUserService -) { - install(AutoHeadResponse) - routing { - inbox(httpSignatureVerifyService, activityPubService) - outbox() - usersAP(activityPubUserService) - webfinger(userService) - } -} diff --git a/src/main/kotlin/dev/usbharu/hideout/plugins/Security.kt b/src/main/kotlin/dev/usbharu/hideout/plugins/Security.kt deleted file mode 100644 index 94485569..00000000 --- a/src/main/kotlin/dev/usbharu/hideout/plugins/Security.kt +++ /dev/null @@ -1,27 +0,0 @@ -package dev.usbharu.hideout.plugins - -import dev.usbharu.hideout.service.IUserAuthService -import io.ktor.server.application.* -import io.ktor.server.auth.* -import io.ktor.server.sessions.* -import kotlin.collections.set - -data class UserSession(val username: String) : Principal - -const val tokenAuth = "token-auth" - -fun Application.configureSecurity(userAuthService: IUserAuthService) { - install(Authentication) { - bearer(tokenAuth) { - authenticate { bearerTokenCredential -> - UserIdPrincipal(bearerTokenCredential.token) - } - skipWhen { true } - } - } -// install(Sessions) { -// cookie("MY_SESSION") { -// cookie.extensions["SameSite"] = "lax" -// } -// } -} diff --git a/src/main/kotlin/dev/usbharu/hideout/plugins/Serialization.kt b/src/main/kotlin/dev/usbharu/hideout/plugins/Serialization.kt deleted file mode 100644 index c8b71a7d..00000000 --- a/src/main/kotlin/dev/usbharu/hideout/plugins/Serialization.kt +++ /dev/null @@ -1,24 +0,0 @@ -package dev.usbharu.hideout.plugins - -import com.fasterxml.jackson.annotation.JsonInclude -import com.fasterxml.jackson.annotation.JsonSetter -import com.fasterxml.jackson.annotation.Nulls -import com.fasterxml.jackson.databind.DeserializationFeature -import dev.usbharu.hideout.util.HttpUtil.Activity -import io.ktor.http.* -import io.ktor.serialization.jackson.* -import io.ktor.server.application.* -import io.ktor.server.plugins.contentnegotiation.* -import io.ktor.server.response.* -import io.ktor.server.routing.* - -fun Application.configureSerialization() { - install(ContentNegotiation) { - jackson { - enable(DeserializationFeature.ACCEPT_SINGLE_VALUE_AS_ARRAY) - setSerializationInclusion(JsonInclude.Include.NON_EMPTY) - configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false) - configOverride(List::class.java).setSetterInfo(JsonSetter.Value.forContentNulls(Nulls.AS_EMPTY)) - } - } -} diff --git a/src/main/kotlin/dev/usbharu/hideout/plugins/Sockets.kt b/src/main/kotlin/dev/usbharu/hideout/plugins/Sockets.kt deleted file mode 100644 index 0c6f217f..00000000 --- a/src/main/kotlin/dev/usbharu/hideout/plugins/Sockets.kt +++ /dev/null @@ -1,29 +0,0 @@ -package dev.usbharu.hideout.plugins - -import io.ktor.server.websocket.* -import io.ktor.websocket.* -import java.time.Duration -import io.ktor.server.application.* -import io.ktor.server.routing.* - -fun Application.configureSockets() { - install(WebSockets) { - pingPeriod = Duration.ofSeconds(15) - timeout = Duration.ofSeconds(15) - maxFrameSize = Long.MAX_VALUE - masking = false - } - routing { - webSocket("/ws") { // websocketSession - for (frame in incoming) { - if (frame is Frame.Text) { - val text = frame.readText() - outgoing.send(Frame.Text("YOU SAID: $text")) - if (text.equals("bye", ignoreCase = true)) { - close(CloseReason(CloseReason.Codes.NORMAL, "Client said BYE")) - } - } - } - } - } -} diff --git a/src/main/kotlin/dev/usbharu/hideout/plugins/StatusPages.kt b/src/main/kotlin/dev/usbharu/hideout/plugins/StatusPages.kt deleted file mode 100644 index cd078e28..00000000 --- a/src/main/kotlin/dev/usbharu/hideout/plugins/StatusPages.kt +++ /dev/null @@ -1,17 +0,0 @@ -package dev.usbharu.hideout.plugins - -import io.ktor.http.* -import io.ktor.server.application.* -import io.ktor.server.plugins.statuspages.* -import io.ktor.server.response.* - -fun Application.configureStatusPages() { - install(StatusPages) { - exception { call, cause -> - call.respondText(text = "500: $cause", status = HttpStatusCode.InternalServerError) - } - exception { call, cause -> - call.respondText(text = "400: $cause", status = HttpStatusCode.BadRequest) - } - } -} diff --git a/src/main/kotlin/dev/usbharu/hideout/repository/IUserAuthRepository.kt b/src/main/kotlin/dev/usbharu/hideout/repository/IUserAuthRepository.kt deleted file mode 100644 index 2f6f46ba..00000000 --- a/src/main/kotlin/dev/usbharu/hideout/repository/IUserAuthRepository.kt +++ /dev/null @@ -1,15 +0,0 @@ -package dev.usbharu.hideout.repository - -import dev.usbharu.hideout.domain.model.UserAuthentication -import dev.usbharu.hideout.domain.model.UserAuthenticationEntity - -interface IUserAuthRepository { - suspend fun create(userAuthentication: UserAuthentication):UserAuthenticationEntity - - suspend fun findById(id:Long):UserAuthenticationEntity? - - suspend fun update(userAuthenticationEntity: UserAuthenticationEntity) - - suspend fun delete(id:Long) - suspend fun findByUserId(id: Long): UserAuthenticationEntity? -} diff --git a/src/main/kotlin/dev/usbharu/hideout/repository/IUserKeyRepository.kt b/src/main/kotlin/dev/usbharu/hideout/repository/IUserKeyRepository.kt deleted file mode 100644 index d72bac45..00000000 --- a/src/main/kotlin/dev/usbharu/hideout/repository/IUserKeyRepository.kt +++ /dev/null @@ -1,3 +0,0 @@ -package dev.usbharu.hideout.repository - -interface IUserKeyRepository diff --git a/src/main/kotlin/dev/usbharu/hideout/repository/IUserRepository.kt b/src/main/kotlin/dev/usbharu/hideout/repository/IUserRepository.kt deleted file mode 100644 index dd89dbd9..00000000 --- a/src/main/kotlin/dev/usbharu/hideout/repository/IUserRepository.kt +++ /dev/null @@ -1,32 +0,0 @@ -package dev.usbharu.hideout.repository - -import dev.usbharu.hideout.domain.model.User -import dev.usbharu.hideout.domain.model.UserEntity - -interface IUserRepository { - suspend fun create(user: User): UserEntity - - suspend fun findById(id: Long): UserEntity? - - suspend fun findByIds(ids: List): List - - suspend fun findByName(name: String): UserEntity? - - suspend fun findByNameAndDomains(names: List>): List - - suspend fun findByUrl(url:String):UserEntity? - - suspend fun findByUrls(urls: List): List - - suspend fun update(userEntity: UserEntity) - - suspend fun delete(id: Long) - - suspend fun findAll(): List - - suspend fun findAllByLimitAndByOffset(limit: Int, offset: Long = 0): List - - suspend fun createFollower(id: Long, follower: Long) - suspend fun deleteFollower(id: Long, follower: Long) - suspend fun findFollowersById(id: Long): List -} diff --git a/src/main/kotlin/dev/usbharu/hideout/repository/UserAuthRepository.kt b/src/main/kotlin/dev/usbharu/hideout/repository/UserAuthRepository.kt deleted file mode 100644 index 0bb5543c..00000000 --- a/src/main/kotlin/dev/usbharu/hideout/repository/UserAuthRepository.kt +++ /dev/null @@ -1,63 +0,0 @@ -package dev.usbharu.hideout.repository - -import dev.usbharu.hideout.domain.model.UserAuthentication -import dev.usbharu.hideout.domain.model.UserAuthenticationEntity -import dev.usbharu.hideout.domain.model.UsersAuthentication -import kotlinx.coroutines.Dispatchers -import org.jetbrains.exposed.sql.* -import org.jetbrains.exposed.sql.transactions.experimental.newSuspendedTransaction -import org.jetbrains.exposed.sql.transactions.transaction - -class UserAuthRepository(private val database: Database) : IUserAuthRepository { - - init { - transaction(database) { - SchemaUtils.create(UsersAuthentication) - SchemaUtils.createMissingTablesAndColumns(UsersAuthentication) - } - } - - private fun ResultRow.toUserAuth():UserAuthenticationEntity{ - return UserAuthenticationEntity( - id = this[UsersAuthentication.id].value, - userId = this[UsersAuthentication.userId], - hash = this[UsersAuthentication.hash], - publicKey = this[UsersAuthentication.publicKey], - privateKey = this[UsersAuthentication.privateKey] - ) - } - - - suspend fun query(block: suspend () -> T): T = - newSuspendedTransaction(Dispatchers.IO) {block()} - override suspend fun create(userAuthentication: UserAuthentication): UserAuthenticationEntity { - return query { - UserAuthenticationEntity( - UsersAuthentication.insert { - it[userId] = userAuthentication.userId - it[hash] = userAuthentication.hash - it[publicKey] = userAuthentication.publicKey - it[privateKey] = userAuthentication.privateKey - }[UsersAuthentication.id].value,userAuthentication - ) - } - } - - override suspend fun findById(id: Long): UserAuthenticationEntity? { - TODO("Not yet implemented") - } - - override suspend fun findByUserId(id:Long):UserAuthenticationEntity? { - return query { - UsersAuthentication.select { UsersAuthentication.userId eq id }.map { it.toUserAuth() }.singleOrNull() - } - } - - override suspend fun update(userAuthenticationEntity: UserAuthenticationEntity) { - TODO("Not yet implemented") - } - - override suspend fun delete(id: Long) { - TODO("Not yet implemented") - } -} diff --git a/src/main/kotlin/dev/usbharu/hideout/repository/UserKeyRepository.kt b/src/main/kotlin/dev/usbharu/hideout/repository/UserKeyRepository.kt deleted file mode 100644 index b8a8de36..00000000 --- a/src/main/kotlin/dev/usbharu/hideout/repository/UserKeyRepository.kt +++ /dev/null @@ -1,4 +0,0 @@ -package dev.usbharu.hideout.repository - -class UserKeyRepository { -} diff --git a/src/main/kotlin/dev/usbharu/hideout/repository/UserRepository.kt b/src/main/kotlin/dev/usbharu/hideout/repository/UserRepository.kt deleted file mode 100644 index f51240f0..00000000 --- a/src/main/kotlin/dev/usbharu/hideout/repository/UserRepository.kt +++ /dev/null @@ -1,195 +0,0 @@ -package dev.usbharu.hideout.repository - -import dev.usbharu.hideout.domain.model.User -import dev.usbharu.hideout.domain.model.UserEntity -import dev.usbharu.hideout.domain.model.Users -import dev.usbharu.hideout.domain.model.UsersFollowers -import kotlinx.coroutines.Dispatchers -import org.jetbrains.exposed.sql.* -import org.jetbrains.exposed.sql.SqlExpressionBuilder.eq -import org.jetbrains.exposed.sql.transactions.experimental.newSuspendedTransaction -import org.jetbrains.exposed.sql.transactions.transaction - -class UserRepository(private val database: Database) : IUserRepository { - init { - transaction(database) { - SchemaUtils.create(Users) - SchemaUtils.create(UsersFollowers) - SchemaUtils.createMissingTablesAndColumns(Users) - SchemaUtils.createMissingTablesAndColumns(UsersFollowers) - } - } - - private fun ResultRow.toUser(): User { - return User( - this[Users.name], - this[Users.domain], - this[Users.screenName], - this[Users.description], - this[Users.inbox], - this[Users.outbox], - this[Users.url] - ) - } - - private fun ResultRow.toUserEntity(): UserEntity { - return UserEntity( - this[Users.id].value, - this[Users.name], - this[Users.domain], - this[Users.screenName], - this[Users.description], - this[Users.inbox], - this[Users.outbox], - this[Users.url], - ) - } - - suspend fun query(block: suspend () -> T): T = - newSuspendedTransaction(Dispatchers.IO) { block() } - - override suspend fun create(user: User): UserEntity { - return query { - UserEntity(Users.insert { - it[name] = user.name - it[domain] = user.domain - it[screenName] = user.screenName - it[description] = user.description - it[inbox] = user.inbox - it[outbox] = user.outbox - it[url] = user.url - }[Users.id].value, user) - } - } - - override suspend fun createFollower(id: Long, follower: Long) { - return query { - UsersFollowers.insert { - it[userId] = id - it[followerId] = follower - } - } - } - - override suspend fun findById(id: Long): UserEntity? { - return query { - Users.select { Users.id eq id }.map { - it.toUserEntity() - }.singleOrNull() - } - } - - override suspend fun findByIds(ids: List): List { - return query { - Users.select { Users.id inList ids }.map { - it.toUserEntity() - } - } - } - - override suspend fun findByName(name: String): UserEntity? { - return query { - Users.select { Users.name eq name }.map { - it.toUserEntity() - }.singleOrNull() - } - } - - override suspend fun findByNameAndDomains(names: List>): List { - return query { - val selectAll = Users.selectAll() - names.forEach { (name, domain) -> - selectAll.orWhere { Users.name eq name and (Users.domain eq domain) } - } - selectAll.map { it.toUserEntity() } - } - } - - override suspend fun findByUrl(url: String): UserEntity? { - return query { - Users.select { Users.url eq url }.singleOrNull()?.toUserEntity() - } - } - - override suspend fun findByUrls(urls: List): List { - TODO("Not yet implemented") - } - - override suspend fun findFollowersById(id: Long): List { - return query { - val followers = Users.alias("FOLLOWERS") - Users.innerJoin( - otherTable = UsersFollowers, - onColumn = { Users.id }, - otherColumn = { UsersFollowers.userId }) - - .innerJoin( - otherTable = followers, - onColumn = { UsersFollowers.followerId }, - otherColumn = { followers[Users.id] }) - - .slice( - followers.get(Users.id), - followers.get(Users.name), - followers.get(Users.domain), - followers.get(Users.screenName), - followers.get(Users.description), - followers.get(Users.inbox), - followers.get(Users.outbox), - followers.get(Users.url) - ) - .select { Users.id eq id } - .map { - UserEntity( - id = it[followers[Users.id]].value, - name = it[followers[Users.name]], - domain = it[followers[Users.domain]], - screenName = it[followers[Users.screenName]], - description = it[followers[Users.description]], - inbox = it[followers[Users.inbox]], - outbox = it[followers[Users.outbox]], - url = it[followers[Users.url]], - ) - } - } - } - - - override suspend fun update(userEntity: UserEntity) { - return query { - Users.update({ Users.id eq userEntity.id }) { - it[name] = userEntity.name - it[domain] = userEntity.domain - it[screenName] = userEntity.screenName - it[description] = userEntity.description - it[inbox] = userEntity.inbox - it[outbox] = userEntity.outbox - it[url] = userEntity.url - } - } - } - - override suspend fun delete(id: Long) { - query { - Users.deleteWhere { Users.id.eq(id) } - } - } - - override suspend fun deleteFollower(id: Long, follower: Long) { - query { - UsersFollowers.deleteWhere { (userId eq id).and(followerId eq follower) } - } - } - - override suspend fun findAll(): List { - return query { - Users.selectAll().map { it.toUser() } - } - } - - override suspend fun findAllByLimitAndByOffset(limit: Int, offset: Long): List { - return query { - Users.selectAll().limit(limit, offset).map { it.toUserEntity() } - } - } -} diff --git a/src/main/kotlin/dev/usbharu/hideout/routing/LoginRouting.kt b/src/main/kotlin/dev/usbharu/hideout/routing/LoginRouting.kt deleted file mode 100644 index f3e44b7d..00000000 --- a/src/main/kotlin/dev/usbharu/hideout/routing/LoginRouting.kt +++ /dev/null @@ -1,45 +0,0 @@ -package dev.usbharu.hideout.routing - -import dev.usbharu.hideout.plugins.UserSession -import dev.usbharu.hideout.plugins.tokenAuth -import io.ktor.http.* -import io.ktor.server.application.* -import io.ktor.server.auth.* -import io.ktor.server.response.* -import io.ktor.server.routing.* -import io.ktor.server.sessions.* - -fun Application.login(){ - routing { - authenticate(tokenAuth) { - post("/login") { - println("aaaaaaaaaaaaaaaaaaaaa") - val principal = call.principal() -// call.sessions.set(UserSession(principal!!.name)) - call.respondRedirect("/users/${principal!!.name}") - } - } - - get("/login"){ - call.respondText(contentType = ContentType.Text.Html) { - - //language=HTML - """ - - - - - -

login

-
- - - -
- - - """.trimIndent() - } - } - } -} diff --git a/src/main/kotlin/dev/usbharu/hideout/routing/RegisterRouting.kt b/src/main/kotlin/dev/usbharu/hideout/routing/RegisterRouting.kt deleted file mode 100644 index a6ba6d4c..00000000 --- a/src/main/kotlin/dev/usbharu/hideout/routing/RegisterRouting.kt +++ /dev/null @@ -1,52 +0,0 @@ -package dev.usbharu.hideout.routing - -import dev.usbharu.hideout.plugins.UserSession -import dev.usbharu.hideout.service.IUserAuthService -import io.ktor.http.* -import io.ktor.server.application.* -import io.ktor.server.auth.* -import io.ktor.server.request.* -import io.ktor.server.response.* -import io.ktor.server.routing.* -import io.ktor.server.sessions.* - -fun Application.register(userAuthService: IUserAuthService) { - - routing { - get("/register") { - val principal = call.principal() - if (principal != null) { - call.respondRedirect("/users/${principal.name}") - } - call.respondText(ContentType.Text.Html) { - //language=HTML - """ - - - - -
- - - -
- - - """.trimIndent() - } - } - post("/register") { - val parameters = call.receiveParameters() - val password = parameters["password"] ?: return@post call.respondRedirect("/register") - val username = parameters["username"] ?: return@post call.respondRedirect("/register") - if (userAuthService.usernameAlreadyUse(username)) { - return@post call.respondRedirect("/register") - } - - val hash = userAuthService.hash(password) - userAuthService.registerAccount(username,hash) -// call.respondRedirect("/login") - call.respondRedirect("/users/$username") - } - } -} diff --git a/src/main/kotlin/dev/usbharu/hideout/routing/UserRouting.kt b/src/main/kotlin/dev/usbharu/hideout/routing/UserRouting.kt deleted file mode 100644 index d43b070d..00000000 --- a/src/main/kotlin/dev/usbharu/hideout/routing/UserRouting.kt +++ /dev/null @@ -1,86 +0,0 @@ -package dev.usbharu.hideout.routing - -import dev.usbharu.hideout.domain.model.User -import dev.usbharu.hideout.plugins.UserSession -import dev.usbharu.hideout.plugins.respondAp -import dev.usbharu.hideout.plugins.tokenAuth -import dev.usbharu.hideout.service.impl.ActivityPubUserService -import dev.usbharu.hideout.service.impl.UserService -import dev.usbharu.hideout.util.HttpUtil -import io.ktor.http.* -import io.ktor.server.application.* -import io.ktor.server.auth.* -import io.ktor.server.request.* -import io.ktor.server.response.* -import io.ktor.server.routing.* - -@Suppress("unused") -fun Application.user(userService: UserService, activityPubUserService: ActivityPubUserService) { - routing { - route("/users") { - authenticate(tokenAuth, optional = true) { - - get { - val limit = call.request.queryParameters["limit"]?.toInt() - val offset = call.request.queryParameters["offset"]?.toLong() - val result = userService.findAll(limit, offset) - call.respond(result) - } - post { - val user = call.receive() - userService.create(user) - call.response.header( - HttpHeaders.Location, - call.request.path() + "/${user.name}" - ) - call.respond(HttpStatusCode.Created) - } - get("/{name}") { - val contentType = ContentType.parse(call.request.accept() ?: "*/*") - call.application.environment.log.debug("Accept Content-Type : ${contentType.contentType}/${contentType.contentSubtype} ${contentType.parameters}") - val typeOfActivityPub = HttpUtil.isContentTypeOfActivityPub( - contentType.contentType, - contentType.contentSubtype, - contentType.parameter("profile").orEmpty() - ) - val name = call.parameters["name"] - if (typeOfActivityPub) { - println("Required Activity !!") - val userModel = activityPubUserService.generateUserModel(name!!) - return@get call.respondAp(userModel) - } - name?.let { it1 -> userService.findByName(it1).id } - ?.let { it2 -> println(userService.findFollowersById(it2)) } - val principal = call.principal() - if (principal != null && name != null) { -// iUserService.findByName(name) - if (principal.name == name) { - call.respondText { - principal.name - } - //todo - } - } - call.respondText { - "hello $name !!" - } - } - get("/{name}/icon.png"){ - call.respondBytes(String.javaClass.classLoader.getResourceAsStream("icon.png").readAllBytes(),ContentType.Image.PNG) - } - } - - authenticate(tokenAuth) { - get("/admin") { - println("cccccccccccc " + call.principal()) - println("cccccccccccc " + call.principal()) - - return@get call.respondText { - "you alredy in admin !! hello " + - call.principal()?.name.toString() - } - } - } - } - } -} diff --git a/src/main/kotlin/dev/usbharu/hideout/routing/WellKnownRouting.kt b/src/main/kotlin/dev/usbharu/hideout/routing/WellKnownRouting.kt deleted file mode 100644 index f1e6a7a6..00000000 --- a/src/main/kotlin/dev/usbharu/hideout/routing/WellKnownRouting.kt +++ /dev/null @@ -1,85 +0,0 @@ -package dev.usbharu.hideout.routing - -import dev.usbharu.hideout.config.Config -import dev.usbharu.hideout.service.impl.UserService -import dev.usbharu.hideout.util.HttpUtil.Activity -import io.ktor.http.* -import io.ktor.server.application.* -import io.ktor.server.response.* -import io.ktor.server.routing.* -import kotlinx.serialization.Serializable -import org.intellij.lang.annotations.Language - -fun Application.wellKnown(userService: UserService) { - routing { - route("/.well-known") { - get("/host-meta") { - //language=XML - val xml = """ - - """.trimIndent() - return@get call.respondText( - contentType = ContentType("application", "xrd+xml"), - status = HttpStatusCode.OK, - text = xml - ) - } - - get("/host-meta.json") { - @Language("JSON") val json = """ - { - "links": [ - { - "rel": "lrdd", - "type": "application/jrd+json", - "template": "${Config.configData.url}/.well-known/webfinger?resource={uri}" - } - ] - } - """.trimIndent() - return@get call.respondText( - contentType = ContentType("application", "xrd+json"), - status = HttpStatusCode.OK, - text = json - ) - } - - get("/webfinger") { - val uri = call.request.queryParameters["resource"] ?: return@get call.respondText( - "resource was not found", - status = HttpStatusCode.BadRequest - ) - val decodeURLPart = uri.decodeURLPart() - if (!decodeURLPart.startsWith("acct:")) { - return@get call.respondText( - "$uri was not found.", - status = HttpStatusCode.BadRequest - ) - } - val accountName = - uri.substringBeforeLast("@").substringAfter("acct:").trimStart('@') - val userEntity = userService.findByName(accountName) - - return@get call.respond( - WebFingerResource( - subject = decodeURLPart, - listOf( - WebFingerResource.Link( - rel = "self", - type = ContentType.Application.Activity.toString(), - href = "${Config.configData.url}/users/${userEntity.name}" - ) - ) - ) - ) - } - - } - } -} - -@Serializable -data class WebFingerResource(val subject: String, val links: List) { - @Serializable - data class Link(val rel: String, val type: String, val href: String) -} diff --git a/src/main/kotlin/dev/usbharu/hideout/routing/activitypub/InboxRouting.kt b/src/main/kotlin/dev/usbharu/hideout/routing/activitypub/InboxRouting.kt deleted file mode 100644 index 9ab1d098..00000000 --- a/src/main/kotlin/dev/usbharu/hideout/routing/activitypub/InboxRouting.kt +++ /dev/null @@ -1,72 +0,0 @@ -package dev.usbharu.hideout.routing.activitypub - -import dev.usbharu.hideout.config.Config -import dev.usbharu.hideout.domain.model.ActivityPubObjectResponse -import dev.usbharu.hideout.domain.model.ActivityPubStringResponse -import dev.usbharu.hideout.exception.HttpSignatureVerifyException -import dev.usbharu.hideout.service.signature.HttpSignatureVerifyService -import io.ktor.http.* -import io.ktor.server.application.* -import io.ktor.server.request.* -import io.ktor.server.response.* -import io.ktor.server.routing.* - -fun Routing.inbox( - httpSignatureVerifyService: HttpSignatureVerifyService, - activityPubService: dev.usbharu.hideout.service.activitypub.ActivityPubService -){ - - route("/inbox") { - get { - call.respond(HttpStatusCode.MethodNotAllowed) - } - post { - if (httpSignatureVerifyService.verify(call.request.headers).not()) { - throw HttpSignatureVerifyException() - } - val json = call.receiveText() - call.application.log.trace("Received: $json") - val activityTypes = activityPubService.parseActivity(json) - call.application.log.debug("ActivityTypes: ${activityTypes.name}") - val response = activityPubService.processActivity(json, activityTypes) - when (response) { - is ActivityPubObjectResponse -> call.respond( - response.httpStatusCode, - Config.configData.objectMapper.writeValueAsString(response.message.apply { - context = - listOf("https://www.w3.org/ns/activitystreams") - }) - ) - is ActivityPubStringResponse -> call.respond(response.httpStatusCode, response.message) - null -> call.respond(HttpStatusCode.NotImplemented) - } - } - } - route("/users/{name}/inbox"){ - get { - call.respond(HttpStatusCode.MethodNotAllowed) - } - post { - if (httpSignatureVerifyService.verify(call.request.headers).not()) { - throw HttpSignatureVerifyException() - } - val json = call.receiveText() - call.application.log.trace("Received: $json") - val activityTypes = activityPubService.parseActivity(json) - call.application.log.debug("ActivityTypes: ${activityTypes.name}") - val response = activityPubService.processActivity(json, activityTypes) - when (response) { - is ActivityPubObjectResponse -> call.respond( - response.httpStatusCode, - Config.configData.objectMapper.writeValueAsString(response.message.apply { - context = - listOf("https://www.w3.org/ns/activitystreams") - }) - ) - is ActivityPubStringResponse -> call.respond(response.httpStatusCode, response.message) - null -> call.respond(HttpStatusCode.NotImplemented) - } - } - } - -} diff --git a/src/main/kotlin/dev/usbharu/hideout/routing/activitypub/OutboxRouting.kt b/src/main/kotlin/dev/usbharu/hideout/routing/activitypub/OutboxRouting.kt deleted file mode 100644 index 7bfced91..00000000 --- a/src/main/kotlin/dev/usbharu/hideout/routing/activitypub/OutboxRouting.kt +++ /dev/null @@ -1,27 +0,0 @@ -package dev.usbharu.hideout.routing.activitypub - -import io.ktor.http.* -import io.ktor.server.application.* -import io.ktor.server.response.* -import io.ktor.server.routing.* - -fun Routing.outbox() { - - route("/outbox") { - get { - call.respond(HttpStatusCode.NotImplemented) - } - post { - call.respond(HttpStatusCode.NotImplemented) - } - } - route("/users/{name}/outbox"){ - get { - call.respond(HttpStatusCode.NotImplemented) - } - post { - call.respond(HttpStatusCode.NotImplemented) - } - } - -} diff --git a/src/main/kotlin/dev/usbharu/hideout/routing/activitypub/UserRouting.kt b/src/main/kotlin/dev/usbharu/hideout/routing/activitypub/UserRouting.kt deleted file mode 100644 index 36b4d80d..00000000 --- a/src/main/kotlin/dev/usbharu/hideout/routing/activitypub/UserRouting.kt +++ /dev/null @@ -1,39 +0,0 @@ -package dev.usbharu.hideout.routing.activitypub - -import dev.usbharu.hideout.exception.ParameterNotExistException -import dev.usbharu.hideout.plugins.respondAp -import dev.usbharu.hideout.service.activitypub.ActivityPubUserService -import dev.usbharu.hideout.util.HttpUtil.Activity -import dev.usbharu.hideout.util.HttpUtil.JsonLd -import io.ktor.http.* -import io.ktor.server.application.* -import io.ktor.server.request.* -import io.ktor.server.routing.* - -fun Routing.usersAP(activityPubUserService: ActivityPubUserService) { - route("/users/{name}") { - createChild(ContentTypeRouteSelector(ContentType.Application.Activity, ContentType.Application.JsonLd)).handle { - val name = - call.parameters["name"] ?: throw ParameterNotExistException("Parameter(name='name') does not exist.") - val person = activityPubUserService.getPersonByName(name) - return@handle call.respondAp( - person, - HttpStatusCode.OK - ) - } - } -} - -class ContentTypeRouteSelector(private vararg val contentType: ContentType) : RouteSelector() { - override fun evaluate(context: RoutingResolveContext, segmentIndex: Int): RouteSelectorEvaluation { - - val requestContentType = - ContentType.parse(context.call.request.accept() ?: return RouteSelectorEvaluation.FailedParameter) - return if (contentType.any { contentType -> contentType.match(requestContentType) }) { - RouteSelectorEvaluation.Constant - } else { - RouteSelectorEvaluation.FailedParameter - } - } - -} diff --git a/src/main/kotlin/dev/usbharu/hideout/routing/wellknown/WebfingerRouting.kt b/src/main/kotlin/dev/usbharu/hideout/routing/wellknown/WebfingerRouting.kt deleted file mode 100644 index f862f745..00000000 --- a/src/main/kotlin/dev/usbharu/hideout/routing/wellknown/WebfingerRouting.kt +++ /dev/null @@ -1,44 +0,0 @@ -package dev.usbharu.hideout.routing.wellknown - -import dev.usbharu.hideout.config.Config -import dev.usbharu.hideout.domain.model.wellknown.WebFinger -import dev.usbharu.hideout.exception.IllegalParameterException -import dev.usbharu.hideout.exception.ParameterNotExistException -import dev.usbharu.hideout.service.impl.UserService -import dev.usbharu.hideout.util.HttpUtil.Activity -import io.ktor.http.* -import io.ktor.server.application.* -import io.ktor.server.response.* -import io.ktor.server.routing.* - -fun Routing.webfinger(userService:UserService){ - route("/.well-known/webfinger"){ - get { - val acct = call.request.queryParameters["resource"]?.decodeURLPart() - ?: throw ParameterNotExistException("Parameter(name='resource') does not exist.") - - if (acct.startsWith("acct:").not()) { - throw IllegalParameterException("Parameter(name='resource') is not start with 'acct:'.") - } - - val accountName = acct.substringBeforeLast("@") - .substringAfter("acct:") - .trimStart('@') - - val userEntity = userService.findByName(accountName) - - val webFinger = WebFinger( - subject = acct, - links = listOf( - WebFinger.Link( - rel = "self", - type = ContentType.Application.Activity.toString(), - href = "${Config.configData.url}/users/${userEntity.name}" - ) - ) - ) - - return@get call.respond(webFinger) - } - } -} diff --git a/src/main/kotlin/dev/usbharu/hideout/service/IUserAuthService.kt b/src/main/kotlin/dev/usbharu/hideout/service/IUserAuthService.kt deleted file mode 100644 index d2096d37..00000000 --- a/src/main/kotlin/dev/usbharu/hideout/service/IUserAuthService.kt +++ /dev/null @@ -1,18 +0,0 @@ -package dev.usbharu.hideout.service - -import dev.usbharu.hideout.domain.model.UserAuthentication -import dev.usbharu.hideout.domain.model.UserAuthenticationEntity - -interface IUserAuthService { - fun hash(password: String): String - - suspend fun usernameAlreadyUse(username: String): Boolean - suspend fun registerAccount(username: String, hash: String) - - suspend fun verifyAccount(username: String, password: String): Boolean - - suspend fun findByUserId(userId: Long): UserAuthenticationEntity - - suspend fun findByUsername(username: String): UserAuthenticationEntity - suspend fun createAccount(userEntity: UserAuthentication): UserAuthenticationEntity -} diff --git a/src/main/kotlin/dev/usbharu/hideout/service/IWebFingerService.kt b/src/main/kotlin/dev/usbharu/hideout/service/IWebFingerService.kt deleted file mode 100644 index 2d01e147..00000000 --- a/src/main/kotlin/dev/usbharu/hideout/service/IWebFingerService.kt +++ /dev/null @@ -1,18 +0,0 @@ -package dev.usbharu.hideout.service - -import dev.usbharu.hideout.ap.Person -import dev.usbharu.hideout.domain.model.UserEntity -import dev.usbharu.hideout.webfinger.WebFinger - -interface IWebFingerService { - suspend fun fetch(acct:String): WebFinger? - - suspend fun sync(webFinger: WebFinger):UserEntity - - suspend fun fetchAndSync(acct: String):UserEntity{ - val webFinger = fetch(acct)?: throw IllegalArgumentException() - return sync(webFinger) - } - - suspend fun fetchUserModel(actor: String): Person? -} diff --git a/src/main/kotlin/dev/usbharu/hideout/service/activitypub/ActivityPubFollowService.kt b/src/main/kotlin/dev/usbharu/hideout/service/activitypub/ActivityPubFollowService.kt deleted file mode 100644 index 4fed9bb6..00000000 --- a/src/main/kotlin/dev/usbharu/hideout/service/activitypub/ActivityPubFollowService.kt +++ /dev/null @@ -1,11 +0,0 @@ -package dev.usbharu.hideout.service.activitypub - -import dev.usbharu.hideout.ap.Follow -import dev.usbharu.hideout.domain.model.ActivityPubResponse -import dev.usbharu.hideout.domain.model.job.ReceiveFollowJob -import kjob.core.job.JobProps - -interface ActivityPubFollowService { - suspend fun receiveFollow(follow:Follow):ActivityPubResponse - suspend fun receiveFollowJob(props: JobProps) -} diff --git a/src/main/kotlin/dev/usbharu/hideout/service/activitypub/ActivityPubFollowServiceImpl.kt b/src/main/kotlin/dev/usbharu/hideout/service/activitypub/ActivityPubFollowServiceImpl.kt deleted file mode 100644 index 0a606f4a..00000000 --- a/src/main/kotlin/dev/usbharu/hideout/service/activitypub/ActivityPubFollowServiceImpl.kt +++ /dev/null @@ -1,52 +0,0 @@ -package dev.usbharu.hideout.service.activitypub - -import com.fasterxml.jackson.module.kotlin.readValue -import dev.usbharu.hideout.ap.Accept -import dev.usbharu.hideout.ap.Follow -import dev.usbharu.hideout.config.Config -import dev.usbharu.hideout.domain.model.ActivityPubResponse -import dev.usbharu.hideout.domain.model.ActivityPubStringResponse -import dev.usbharu.hideout.domain.model.job.ReceiveFollowJob -import dev.usbharu.hideout.plugins.postAp -import dev.usbharu.hideout.service.impl.UserService -import dev.usbharu.hideout.service.job.JobQueueParentService -import io.ktor.client.* -import io.ktor.http.* -import kjob.core.job.JobProps - -class ActivityPubFollowServiceImpl( - private val jobQueueParentService: JobQueueParentService, - private val activityPubUserService: ActivityPubUserService, - private val userService: UserService, - private val httpClient: HttpClient -) : ActivityPubFollowService { - override suspend fun receiveFollow(follow: Follow): ActivityPubResponse { - // TODO: Verify HTTP Signature - jobQueueParentService.schedule(ReceiveFollowJob) { - props[it.actor] = follow.actor - props[it.follow] = Config.configData.objectMapper.writeValueAsString(follow) - props[it.targetActor] = follow.`object` - } - return ActivityPubStringResponse(HttpStatusCode.OK, "{}", ContentType.Application.Json) - } - - override suspend fun receiveFollowJob(props: JobProps) { - val actor = props[ReceiveFollowJob.actor] - val person = activityPubUserService.fetchPerson(actor) - val follow = Config.configData.objectMapper.readValue(props[ReceiveFollowJob.follow]) - val targetActor = props[ReceiveFollowJob.targetActor] - httpClient.postAp( - urlString = person.inbox ?: throw IllegalArgumentException("inbox is not found"), - username = "$targetActor#pubkey", - jsonLd = Accept( - name = "Follow", - `object` = follow, - actor = targetActor - ) - ) - val users = - userService.findByUrls(listOf(targetActor, follow.actor ?: throw IllegalArgumentException("actor is null"))) - - userService.addFollowers(users.first { it.url == targetActor }.id, users.first { it.url == follow.actor }.id) - } -} diff --git a/src/main/kotlin/dev/usbharu/hideout/service/activitypub/ActivityPubService.kt b/src/main/kotlin/dev/usbharu/hideout/service/activitypub/ActivityPubService.kt deleted file mode 100644 index 939d3d3b..00000000 --- a/src/main/kotlin/dev/usbharu/hideout/service/activitypub/ActivityPubService.kt +++ /dev/null @@ -1,45 +0,0 @@ -package dev.usbharu.hideout.service.activitypub - -import dev.usbharu.hideout.domain.model.ActivityPubResponse -import dev.usbharu.hideout.domain.model.job.HideoutJob -import kjob.core.dsl.JobContextWithProps - -interface ActivityPubService { - fun parseActivity(json: String): ActivityType - - suspend fun processActivity(json: String, type: ActivityType): ActivityPubResponse? - - suspend fun processActivity(job: JobContextWithProps,hideoutJob: HideoutJob) -} - -enum class ActivityType { - Accept, - Add, - Announce, - Arrive, - Block, - Create, - Delete, - Dislike, - Flag, - Follow, - Ignore, - Invite, - Join, - Leave, - Like, - Listen, - Move, - Offer, - Question, - Reject, - Read, - Remove, - TentativeReject, - TentativeAccept, - Travel, - Undo, - Update, - View, - Other -} diff --git a/src/main/kotlin/dev/usbharu/hideout/service/activitypub/ActivityPubServiceImpl.kt b/src/main/kotlin/dev/usbharu/hideout/service/activitypub/ActivityPubServiceImpl.kt deleted file mode 100644 index 06224a67..00000000 --- a/src/main/kotlin/dev/usbharu/hideout/service/activitypub/ActivityPubServiceImpl.kt +++ /dev/null @@ -1,82 +0,0 @@ -package dev.usbharu.hideout.service.activitypub - -import com.fasterxml.jackson.databind.JsonNode -import dev.usbharu.hideout.ap.Follow -import dev.usbharu.hideout.config.Config -import dev.usbharu.hideout.domain.model.ActivityPubResponse -import dev.usbharu.hideout.domain.model.job.HideoutJob -import dev.usbharu.hideout.domain.model.job.ReceiveFollowJob -import dev.usbharu.hideout.exception.JsonParseException -import kjob.core.Job -import kjob.core.dsl.JobContextWithProps -import kjob.core.job.JobProps -import org.slf4j.LoggerFactory -import kotlin.reflect.full.createInstance -import kotlin.reflect.full.primaryConstructor - -class ActivityPubServiceImpl(private val activityPubFollowService: ActivityPubFollowService) : ActivityPubService { - - val logger = LoggerFactory.getLogger(this::class.java) - override fun parseActivity(json: String): ActivityType { - val readTree = Config.configData.objectMapper.readTree(json) - logger.debug("readTree: {}", readTree) - if (readTree.isObject.not()) { - throw JsonParseException("Json is not object.") - } - val type = readTree["type"] - if (type.isArray) { - return type.mapNotNull { jsonNode: JsonNode -> - ActivityType.values().firstOrNull { it.name.equals(jsonNode.asText(), true) } - }.first() - } - return ActivityType.values().first { it.name.equals(type.asText(), true) } - } - - override suspend fun processActivity(json: String, type: ActivityType): ActivityPubResponse? { - return when (type) { - ActivityType.Accept -> TODO() - ActivityType.Add -> TODO() - ActivityType.Announce -> TODO() - ActivityType.Arrive -> TODO() - ActivityType.Block -> TODO() - ActivityType.Create -> TODO() - ActivityType.Delete -> TODO() - ActivityType.Dislike -> TODO() - ActivityType.Flag -> TODO() - ActivityType.Follow -> activityPubFollowService.receiveFollow( - Config.configData.objectMapper.readValue( - json, - Follow::class.java - ) - ) - - ActivityType.Ignore -> TODO() - ActivityType.Invite -> TODO() - ActivityType.Join -> TODO() - ActivityType.Leave -> TODO() - ActivityType.Like -> TODO() - ActivityType.Listen -> TODO() - ActivityType.Move -> TODO() - ActivityType.Offer -> TODO() - ActivityType.Question -> TODO() - ActivityType.Reject -> TODO() - ActivityType.Read -> TODO() - ActivityType.Remove -> TODO() - ActivityType.TentativeReject -> TODO() - ActivityType.TentativeAccept -> TODO() - ActivityType.Travel -> TODO() - ActivityType.Undo -> TODO() - ActivityType.Update -> TODO() - ActivityType.View -> TODO() - ActivityType.Other -> TODO() - } - } - - override suspend fun processActivity(job: JobContextWithProps, hideoutJob: HideoutJob) { - when (hideoutJob) { - ReceiveFollowJob -> activityPubFollowService.receiveFollowJob(job.props as JobProps) - } - } - - -} diff --git a/src/main/kotlin/dev/usbharu/hideout/service/activitypub/ActivityPubUserService.kt b/src/main/kotlin/dev/usbharu/hideout/service/activitypub/ActivityPubUserService.kt deleted file mode 100644 index 0b67e383..00000000 --- a/src/main/kotlin/dev/usbharu/hideout/service/activitypub/ActivityPubUserService.kt +++ /dev/null @@ -1,9 +0,0 @@ -package dev.usbharu.hideout.service.activitypub - -import dev.usbharu.hideout.ap.Person - -interface ActivityPubUserService { - suspend fun getPersonByName(name:String):Person - - suspend fun fetchPerson(url:String):Person -} diff --git a/src/main/kotlin/dev/usbharu/hideout/service/activitypub/ActivityPubUserServiceImpl.kt b/src/main/kotlin/dev/usbharu/hideout/service/activitypub/ActivityPubUserServiceImpl.kt deleted file mode 100644 index 2c5c5002..00000000 --- a/src/main/kotlin/dev/usbharu/hideout/service/activitypub/ActivityPubUserServiceImpl.kt +++ /dev/null @@ -1,113 +0,0 @@ -package dev.usbharu.hideout.service.activitypub - -import com.fasterxml.jackson.module.kotlin.readValue -import dev.usbharu.hideout.ap.Image -import dev.usbharu.hideout.ap.Key -import dev.usbharu.hideout.ap.Person -import dev.usbharu.hideout.config.Config -import dev.usbharu.hideout.domain.model.User -import dev.usbharu.hideout.domain.model.UserAuthentication -import dev.usbharu.hideout.exception.UserNotFoundException -import dev.usbharu.hideout.exception.ap.IllegalActivityPubObjectException -import dev.usbharu.hideout.service.IUserAuthService -import dev.usbharu.hideout.service.impl.UserService -import dev.usbharu.hideout.util.HttpUtil.Activity -import io.ktor.client.* -import io.ktor.client.request.* -import io.ktor.client.statement.* -import io.ktor.http.* - -class ActivityPubUserServiceImpl( - private val userService: UserService, - private val userAuthService: IUserAuthService, - private val httpClient: HttpClient -) : - ActivityPubUserService { - override suspend fun getPersonByName(name: String): Person { - // TODO: JOINで書き直し - val userEntity = userService.findByName(name) - val userAuthEntity = userAuthService.findByUserId(userEntity.id) - val userUrl = "${Config.configData.url}/users/$name" - return Person( - type = emptyList(), - name = userEntity.name, - id = userUrl, - preferredUsername = name, - summary = userEntity.description, - inbox = "$userUrl/inbox", - outbox = "$userUrl/outbox", - 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 = "$userUrl#pubkey", - owner = userUrl, - publicKeyPem = userAuthEntity.publicKey - ) - ) - } - - override suspend fun fetchPerson(url: String): Person { - return try { - val userEntity = userService.findByUrl(url) - val userAuthEntity = userAuthService.findByUsername(userEntity.name) - 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 = "$url#pubkey", - owner = url, - publicKeyPem = userAuthEntity.publicKey - ) - ) - - } catch (e: UserNotFoundException) { - val httpResponse = httpClient.get(url) { - accept(ContentType.Application.Activity) - } - val person = Config.configData.objectMapper.readValue(httpResponse.bodyAsText()) - val userEntity = userService.create( - User( - name = person.preferredUsername - ?: throw IllegalActivityPubObjectException("preferredUsername is null"), - domain = url.substringAfter(":").substringBeforeLast("/"), - screenName = person.name ?: throw IllegalActivityPubObjectException("name is null"), - description = person.summary ?: throw IllegalActivityPubObjectException("summary is null"), - inbox = person.inbox ?: throw IllegalActivityPubObjectException("inbox is null"), - outbox = person.outbox ?: throw IllegalActivityPubObjectException("outbox is null"), - url = url - ) - ) - userAuthService.createAccount( - UserAuthentication( - userEntity.id, - null, - person.publicKey?.publicKeyPem ?: throw IllegalActivityPubObjectException("publicKey is null"), - null - ) - ) - person - } - - } -} diff --git a/src/main/kotlin/dev/usbharu/hideout/service/impl/ActivityPubService.kt b/src/main/kotlin/dev/usbharu/hideout/service/impl/ActivityPubService.kt deleted file mode 100644 index e693bd57..00000000 --- a/src/main/kotlin/dev/usbharu/hideout/service/impl/ActivityPubService.kt +++ /dev/null @@ -1,20 +0,0 @@ -package dev.usbharu.hideout.service.impl - -import dev.usbharu.hideout.config.Config - -class ActivityPubService() { - - enum class ActivityType{ - Follow, - Undo - } - - fun switchApType(json:String): ActivityType { - val typeAsText = Config.configData.objectMapper.readTree(json).get("type").asText() - return when(typeAsText){ - "Follow" -> ActivityType.Follow - "Undo" -> ActivityType.Undo - else -> ActivityType.Undo - } - } -} diff --git a/src/main/kotlin/dev/usbharu/hideout/service/impl/ActivityPubUserService.kt b/src/main/kotlin/dev/usbharu/hideout/service/impl/ActivityPubUserService.kt deleted file mode 100644 index fd26123c..00000000 --- a/src/main/kotlin/dev/usbharu/hideout/service/impl/ActivityPubUserService.kt +++ /dev/null @@ -1,57 +0,0 @@ -package dev.usbharu.hideout.service.impl - -import dev.usbharu.hideout.ap.* -import dev.usbharu.hideout.config.Config -import dev.usbharu.hideout.plugins.postAp -import dev.usbharu.hideout.service.IUserAuthService -import dev.usbharu.hideout.service.IWebFingerService -import io.ktor.client.* - -class ActivityPubUserService( - private val httpClient: HttpClient, - private val userService: UserService, - private val userAuthService: IUserAuthService, - private val webFingerService: IWebFingerService -) { - suspend fun generateUserModel(name: String): Person { - val userEntity = userService.findByName(name) - val userAuthEntity = userAuthService.findByUserId(userEntity.id) - val userUrl = "${Config.configData.url}/users/$name" - return Person( - type = emptyList(), - name = userEntity.name, - id = userUrl, - preferredUsername = name, - summary = userEntity.description, - inbox = "$userUrl/inbox", - outbox = "$userUrl/outbox", - 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 = "$userUrl#pubkey", - owner = userUrl, - publicKeyPem = userAuthEntity.publicKey - ) - ) - } - - suspend fun receiveFollow(follow: Follow) { - val actor = follow.actor ?: throw IllegalArgumentException("actor is null") - val person = webFingerService.fetchUserModel(actor) ?: throw IllegalArgumentException("actor is not found") - val inboxUrl = person.inbox ?: throw IllegalArgumentException("inbox is not found") - httpClient.postAp( - inboxUrl, "${follow.`object`!!}#pubkey", Accept( - name = "Follow", - `object` = follow, - actor = follow.`object`.orEmpty() - ) - ) - } -} diff --git a/src/main/kotlin/dev/usbharu/hideout/service/impl/HttpSignService.kt b/src/main/kotlin/dev/usbharu/hideout/service/impl/HttpSignService.kt deleted file mode 100644 index 07cf8941..00000000 --- a/src/main/kotlin/dev/usbharu/hideout/service/impl/HttpSignService.kt +++ /dev/null @@ -1,9 +0,0 @@ -package dev.usbharu.hideout.service.impl - -import java.security.PrivateKey - -class HttpSignService { - suspend fun sign(){ - - } -} diff --git a/src/main/kotlin/dev/usbharu/hideout/service/impl/UserAuthService.kt b/src/main/kotlin/dev/usbharu/hideout/service/impl/UserAuthService.kt deleted file mode 100644 index 5542d1ee..00000000 --- a/src/main/kotlin/dev/usbharu/hideout/service/impl/UserAuthService.kt +++ /dev/null @@ -1,106 +0,0 @@ -package dev.usbharu.hideout.service.impl - -import dev.usbharu.hideout.config.Config -import dev.usbharu.hideout.domain.model.User -import dev.usbharu.hideout.domain.model.UserAuthentication -import dev.usbharu.hideout.domain.model.UserAuthenticationEntity -import dev.usbharu.hideout.domain.model.UserEntity -import dev.usbharu.hideout.exception.UserNotFoundException -import dev.usbharu.hideout.repository.IUserAuthRepository -import dev.usbharu.hideout.repository.IUserRepository -import dev.usbharu.hideout.service.IUserAuthService -import io.ktor.util.* -import java.security.* -import java.security.interfaces.RSAPrivateKey -import java.security.interfaces.RSAPublicKey -import java.util.* - -class UserAuthService( - val userRepository: IUserRepository, - val userAuthRepository: IUserAuthRepository -) : IUserAuthService { - - - override fun hash(password: String): String { - val digest = sha256.digest(password.toByteArray(Charsets.UTF_8)) - return hex(digest) - } - - override suspend fun usernameAlreadyUse(username: String): Boolean { - userRepository.findByName(username) ?: return false - return true - } - - override suspend fun registerAccount(username: String, hash: String) { - val url = "${Config.configData.url}/users/$username" - val registerUser = User( - name = username, - domain = Config.configData.domain, - screenName = username, - description = "", - inbox = "$url/inbox", - outbox = "$url/outbox", - url = url - ) - val createdUser = userRepository.create(registerUser) - - val keyPair = generateKeyPair() - val privateKey = keyPair.private as RSAPrivateKey - val publicKey = keyPair.public as RSAPublicKey - - - val userAuthentication = UserAuthentication( - createdUser.id, - hash, - publicKey.toPem(), - privateKey.toPem() - ) - - userAuthRepository.create(userAuthentication) - } - - override suspend fun verifyAccount(username: String, password: String): Boolean { - val userEntity = userRepository.findByName(username) - ?: throw UserNotFoundException("$username was not found") - val userAuthEntity = userAuthRepository.findByUserId(userEntity.id) - ?: throw UserNotFoundException("$username auth data was not found") - return userAuthEntity.hash == hash(password) - } - - override suspend fun findByUserId(userId: Long): UserAuthenticationEntity { - return userAuthRepository.findByUserId(userId) ?: throw UserNotFoundException("$userId was not found") - } - - override suspend fun findByUsername(username: String): UserAuthenticationEntity { - val userEntity = userRepository.findByName(username) ?: throw UserNotFoundException("$username was not found") - return userAuthRepository.findByUserId(userEntity.id) - ?: throw UserNotFoundException("$username auth data was not found") - } - - override suspend fun createAccount(userEntity: UserAuthentication): UserAuthenticationEntity { - return userAuthRepository.create(userEntity) - } - - private fun generateKeyPair(): KeyPair { - val keyPairGenerator = KeyPairGenerator.getInstance("RSA") - keyPairGenerator.initialize(1024) - return keyPairGenerator.generateKeyPair() - } - - - companion object { - val sha256: MessageDigest = MessageDigest.getInstance("SHA-256") - } -} - -public fun PublicKey.toPem(): String { - return "-----BEGIN PUBLIC KEY-----\n" + - Base64.getEncoder().encodeToString(encoded).chunked(64).joinToString("\n") + - "\n-----END PUBLIC KEY-----\n" -} - -public fun PrivateKey.toPem(): String { - return "-----BEGIN PRIVATE KEY-----" + - Base64.getEncoder().encodeToString(encoded).chunked(64).joinToString("\n") + - "\n-----END PRIVATE KEY-----\n" -} diff --git a/src/main/kotlin/dev/usbharu/hideout/service/impl/UserService.kt b/src/main/kotlin/dev/usbharu/hideout/service/impl/UserService.kt deleted file mode 100644 index a64ccd04..00000000 --- a/src/main/kotlin/dev/usbharu/hideout/service/impl/UserService.kt +++ /dev/null @@ -1,57 +0,0 @@ -package dev.usbharu.hideout.service.impl - -import dev.usbharu.hideout.domain.model.User -import dev.usbharu.hideout.domain.model.UserEntity -import dev.usbharu.hideout.exception.UserNotFoundException -import dev.usbharu.hideout.repository.IUserRepository -import java.lang.Integer.min - -class UserService(private val userRepository: IUserRepository) { - - private val maxLimit = 100 - suspend fun findAll(limit: Int? = maxLimit, offset: Long? = 0): List { - - return userRepository.findAllByLimitAndByOffset( - min(limit ?: maxLimit, maxLimit), - offset ?: 0 - ) - } - - suspend fun findById(id: Long): UserEntity { - return userRepository.findById(id) ?: throw UserNotFoundException("$id was not found.") - } - - suspend fun findByIds(ids: List): List { - return userRepository.findByIds(ids) - } - - suspend fun findByName(name: String): UserEntity { - return userRepository.findByName(name) - ?: throw UserNotFoundException("$name was not found.") - } - - suspend fun findByNameAndDomains(names: List>): List { - return userRepository.findByNameAndDomains(names) - } - - suspend fun findByUrl(url: String): UserEntity { - return userRepository.findByUrl(url) ?: throw UserNotFoundException("$url was not found.") - } - - suspend fun findByUrls(urls: List): List { - return userRepository.findByUrls(urls) - } - - suspend fun create(user: User): UserEntity { - return userRepository.create(user) - } - - suspend fun findFollowersById(id: Long): List { - return userRepository.findFollowersById(id) - } - - suspend fun addFollowers(id: Long, follower: Long) { - return userRepository.createFollower(id, follower) - } - -} diff --git a/src/main/kotlin/dev/usbharu/hideout/service/impl/WebFingerService.kt b/src/main/kotlin/dev/usbharu/hideout/service/impl/WebFingerService.kt deleted file mode 100644 index fe928bf6..00000000 --- a/src/main/kotlin/dev/usbharu/hideout/service/impl/WebFingerService.kt +++ /dev/null @@ -1,77 +0,0 @@ -package dev.usbharu.hideout.service.impl - -import dev.usbharu.hideout.ap.Person -import dev.usbharu.hideout.domain.model.User -import dev.usbharu.hideout.domain.model.UserEntity -import dev.usbharu.hideout.service.IWebFingerService -import dev.usbharu.hideout.util.HttpUtil -import dev.usbharu.hideout.webfinger.WebFinger -import io.ktor.client.* -import io.ktor.client.call.* -import io.ktor.client.plugins.* -import io.ktor.client.request.* -import io.ktor.http.* - -class WebFingerService( - private val httpClient: HttpClient, - private val userService: UserService -) : IWebFingerService { - override suspend fun fetch(acct: String): WebFinger? { - - val fullName = acct.substringAfter("acct:") - val domain = fullName.substringAfterLast("@") - - return try { - httpClient.get("https://$domain/.well-known/webfinger?resource=acct:$fullName") - .body() - } catch (e: ResponseException) { - if (e.response.status == HttpStatusCode.NotFound) { - return null - } - throw e - } - } - - override suspend fun fetchUserModel(url: String): Person? { - return try { - httpClient.get(url) { - header("Accept", "application/activity+json") - }.body() - } catch (e: ResponseException) { - if (e.response.status == HttpStatusCode.NotFound) { - e.printStackTrace() - return null - } - throw e - } - } - - override suspend fun sync(webFinger: WebFinger): UserEntity { - - val link = webFinger.links.find { - it.rel == "self" && HttpUtil.isContentTypeOfActivityPub( - ContentType.parse( - it.type.orEmpty() - ) - ) - }?.href ?: throw Exception() - - val fullName = webFinger.subject.substringAfter("acct:") - val domain = fullName.substringAfterLast("@") - val userName = fullName.substringBeforeLast("@") - - val userModel = fetchUserModel(link) ?: throw Exception() - - val user = User( - userModel.preferredUsername ?: throw IllegalStateException(), - domain, - userName, - userModel.summary.orEmpty(), - "", - "", - "" - ) - TODO() - return userService.create(user) - } -} diff --git a/src/main/kotlin/dev/usbharu/hideout/service/job/JobQueueParentService.kt b/src/main/kotlin/dev/usbharu/hideout/service/job/JobQueueParentService.kt deleted file mode 100644 index 4029514b..00000000 --- a/src/main/kotlin/dev/usbharu/hideout/service/job/JobQueueParentService.kt +++ /dev/null @@ -1,10 +0,0 @@ -package dev.usbharu.hideout.service.job - -import kjob.core.Job -import kjob.core.dsl.ScheduleContext - -interface JobQueueParentService { - - fun init(jobDefines:List) - suspend fun schedule(job: J, block: ScheduleContext.(J) -> Unit = {}) -} diff --git a/src/main/kotlin/dev/usbharu/hideout/service/job/JobQueueWorkerService.kt b/src/main/kotlin/dev/usbharu/hideout/service/job/JobQueueWorkerService.kt deleted file mode 100644 index 0d15caae..00000000 --- a/src/main/kotlin/dev/usbharu/hideout/service/job/JobQueueWorkerService.kt +++ /dev/null @@ -1,10 +0,0 @@ -package dev.usbharu.hideout.service.job - -import kjob.core.Job -import kjob.core.dsl.JobContextWithProps -import kjob.core.dsl.JobRegisterContext -import kjob.core.dsl.KJobFunctions - -interface JobQueueWorkerService { - fun init(defines: List>.(Job) -> KJobFunctions>>>) -} diff --git a/src/main/kotlin/dev/usbharu/hideout/service/job/KJobJobQueueParentService.kt b/src/main/kotlin/dev/usbharu/hideout/service/job/KJobJobQueueParentService.kt deleted file mode 100644 index 1f4178b5..00000000 --- a/src/main/kotlin/dev/usbharu/hideout/service/job/KJobJobQueueParentService.kt +++ /dev/null @@ -1,24 +0,0 @@ -package dev.usbharu.hideout.service.job - -import dev.usbharu.kjob.exposed.ExposedKJob -import kjob.core.Job -import kjob.core.KJob -import kjob.core.dsl.ScheduleContext -import kjob.core.kjob -import org.jetbrains.exposed.sql.Database - -class KJobJobQueueParentService(private val database: Database) : JobQueueParentService { - - val kjob: KJob = kjob(ExposedKJob) { - connectionDatabase = database - isWorker = false - }.start() - - override fun init(jobDefines: List) { - - } - - override suspend fun schedule(job: J,block:ScheduleContext.(J)->Unit) { - kjob.schedule(job,block) - } -} diff --git a/src/main/kotlin/dev/usbharu/hideout/service/job/KJobJobQueueWorkerService.kt b/src/main/kotlin/dev/usbharu/hideout/service/job/KJobJobQueueWorkerService.kt deleted file mode 100644 index cab3dcc8..00000000 --- a/src/main/kotlin/dev/usbharu/hideout/service/job/KJobJobQueueWorkerService.kt +++ /dev/null @@ -1,28 +0,0 @@ -package dev.usbharu.hideout.service.job - -import dev.usbharu.kjob.exposed.ExposedKJob -import kjob.core.Job -import kjob.core.dsl.JobContextWithProps -import kjob.core.dsl.JobRegisterContext -import kjob.core.dsl.KJobFunctions -import kjob.core.kjob -import org.jetbrains.exposed.sql.Database - -class KJobJobQueueWorkerService(private val database: Database) : JobQueueWorkerService { - - val kjob by lazy { - kjob(ExposedKJob) { - connectionDatabase = database - nonBlockingMaxJobs = 10 - blockingMaxJobs = 10 - jobExecutionPeriodInSeconds = 10 - }.start() - } - - override fun init(defines: List>.(Job) -> KJobFunctions>>>) { - defines.forEach { job -> - kjob.register(job.first, job.second) - } - } - -} diff --git a/src/main/kotlin/dev/usbharu/hideout/service/signature/HttpSignatureVerifyService.kt b/src/main/kotlin/dev/usbharu/hideout/service/signature/HttpSignatureVerifyService.kt deleted file mode 100644 index 34706206..00000000 --- a/src/main/kotlin/dev/usbharu/hideout/service/signature/HttpSignatureVerifyService.kt +++ /dev/null @@ -1,7 +0,0 @@ -package dev.usbharu.hideout.service.signature - -import io.ktor.http.* - -interface HttpSignatureVerifyService { - fun verify(headers:Headers):Boolean -} diff --git a/src/main/kotlin/dev/usbharu/hideout/service/signature/HttpSignatureVerifyServiceImpl.kt b/src/main/kotlin/dev/usbharu/hideout/service/signature/HttpSignatureVerifyServiceImpl.kt deleted file mode 100644 index 74525981..00000000 --- a/src/main/kotlin/dev/usbharu/hideout/service/signature/HttpSignatureVerifyServiceImpl.kt +++ /dev/null @@ -1,24 +0,0 @@ -package dev.usbharu.hideout.service.signature - -import dev.usbharu.hideout.plugins.KtorKeyMap -import dev.usbharu.hideout.service.IUserAuthService -import io.ktor.http.* -import tech.barbero.http.message.signing.HttpMessage -import tech.barbero.http.message.signing.SignatureHeaderVerifier - -class HttpSignatureVerifyServiceImpl(private val userAuthService: IUserAuthService) : HttpSignatureVerifyService { - override fun verify(headers: Headers): Boolean { - val build = SignatureHeaderVerifier.builder().keyMap(KtorKeyMap(userAuthService)).build() - return true; - build.verify(object : HttpMessage { - override fun headerValues(name: String?): MutableList { - return name?.let { headers.getAll(it) }?.toMutableList() ?: mutableListOf() - } - - override fun addHeader(name: String?, value: String?) { - TODO() - } - - }) - } -} diff --git a/src/main/kotlin/dev/usbharu/hideout/util/HttpUtil.kt b/src/main/kotlin/dev/usbharu/hideout/util/HttpUtil.kt deleted file mode 100644 index 979b8302..00000000 --- a/src/main/kotlin/dev/usbharu/hideout/util/HttpUtil.kt +++ /dev/null @@ -1,35 +0,0 @@ -package dev.usbharu.hideout.util - -import io.ktor.http.* - -object HttpUtil { - fun isContentTypeOfActivityPub( - contentType: String, - subType: String, - parameter: String - ): Boolean { - println("$contentType/$subType $parameter") - if (contentType != "application") { - return false - } - if (subType == "activity+json") { - return true - } - return subType == "ld+json" - } - - fun isContentTypeOfActivityPub(contentType: ContentType): Boolean { - return isContentTypeOfActivityPub( - contentType.contentType, - contentType.contentSubtype, - contentType.parameter("profile").orEmpty() - ) - } - - val ContentType.Application.Activity: ContentType - get() = ContentType("application", "activity+json") - - val ContentType.Application.JsonLd: ContentType - get() = ContentType("application", "ld+json", listOf(HeaderValueParam("profile", "https://www.w3.org/ns/activitystreams"))) -// fun -} diff --git a/src/main/kotlin/dev/usbharu/hideout/webfinger/WebFinger.kt b/src/main/kotlin/dev/usbharu/hideout/webfinger/WebFinger.kt deleted file mode 100644 index 22fa25e5..00000000 --- a/src/main/kotlin/dev/usbharu/hideout/webfinger/WebFinger.kt +++ /dev/null @@ -1,5 +0,0 @@ -package dev.usbharu.hideout.webfinger - -class WebFinger(val subject: String, val aliases: List, val links: List) { - class Link(val rel: String, val type: String?, val href: String?, val template: String) -} diff --git a/src/main/kotlin/dev/usbharu/kjob/exposed/ExposedJobRepository.kt b/src/main/kotlin/dev/usbharu/kjob/exposed/ExposedJobRepository.kt deleted file mode 100644 index 6533358a..00000000 --- a/src/main/kotlin/dev/usbharu/kjob/exposed/ExposedJobRepository.kt +++ /dev/null @@ -1,290 +0,0 @@ -package dev.usbharu.kjob.exposed - -import kjob.core.job.JobProgress -import kjob.core.job.JobSettings -import kjob.core.job.JobStatus -import kjob.core.job.ScheduledJob -import kjob.core.repository.JobRepository -import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.flow.Flow -import kotlinx.coroutines.flow.asFlow -import kotlinx.serialization.encodeToString -import kotlinx.serialization.json.* -import org.jetbrains.exposed.dao.id.LongIdTable -import org.jetbrains.exposed.sql.* -import org.jetbrains.exposed.sql.SqlExpressionBuilder.eq -import org.jetbrains.exposed.sql.SqlExpressionBuilder.inList -import org.jetbrains.exposed.sql.SqlExpressionBuilder.isNull -import org.jetbrains.exposed.sql.SqlExpressionBuilder.plus -import org.jetbrains.exposed.sql.transactions.experimental.newSuspendedTransaction -import org.jetbrains.exposed.sql.transactions.transaction -import java.time.Clock -import java.time.Instant -import java.util.* - -class ExposedJobRepository( - private val database: Database, - private val tableName: String, - private val clock: Clock, - private val json: Json -) : - JobRepository { - - class Jobs(tableName: String) : LongIdTable(tableName) { - val status = text("status") - val runAt = long("runAt").nullable() - val statusMessage = text("statusMessage").nullable() - val retries = integer("retries") - val kjobId = char("kjobId", 36).nullable() - val createdAt = long("createdAt") - val updatedAt = long("updatedAt") - val jobId = text("jobId") - val name = text("name") - val properties = text("properties").nullable() - val step = integer("step") - val max = integer("max").nullable() - val startedAt = long("startedAt").nullable() - val completedAt = long("completedAt").nullable() - } - - val jobs: Jobs = Jobs(tableName) - - fun createTable() { - transaction(database) { - SchemaUtils.create(jobs) - } - } - - suspend fun query(block: suspend () -> T): T = newSuspendedTransaction(Dispatchers.IO) { block() } - - - override suspend fun completeProgress(id: String): Boolean { - val now = Instant.now(clock).toEpochMilli() - return query { - jobs.update({ jobs.id eq id.toLong() }) { - it[jobs.completedAt] = now - it[jobs.updatedAt] = now - } == 1 - } - } - - override suspend fun exist(jobId: String): Boolean { - return query { - jobs.select(jobs.jobId eq jobId).empty().not() - } - } - - override suspend fun findNext(names: Set, status: Set, limit: Int): Flow { - return query { - jobs.select( - jobs.status.inList(list = status.map { it.name }) - .and(if (names.isEmpty()) Op.TRUE else jobs.name.inList(names)) - ).limit(limit) - .map { it.toScheduledJob() }.asFlow() - } - } - - override suspend fun get(id: String): ScheduledJob? { - val single = query { jobs.select(jobs.id eq id.toLong()).singleOrNull() } ?: return null - return single.toScheduledJob() - - } - - override suspend fun reset(id: String, oldKjobId: UUID?): Boolean { - return query { - jobs.update({ jobs.id eq id.toLong() and if (oldKjobId == null) jobs.kjobId.isNull() else jobs.kjobId eq oldKjobId.toString() }) { - it[jobs.status] = JobStatus.CREATED.name - it[jobs.statusMessage] = null - it[jobs.kjobId] = null - it[jobs.step] = 0 - it[jobs.max] = null - it[jobs.startedAt] = null - it[jobs.completedAt] = null - it[jobs.updatedAt] = Instant.now(clock).toEpochMilli() - } == 1 - } - } - - override suspend fun save(jobSettings: JobSettings, runAt: Instant?): ScheduledJob { - val now = Instant.now(clock) - val scheduledJob = - ScheduledJob("", JobStatus.CREATED, runAt, null, 0, null, now, now, jobSettings, JobProgress(0)) - val id = query { - jobs.insert { - it[jobs.status] = scheduledJob.status.name - it[jobs.createdAt] = scheduledJob.createdAt.toEpochMilli() - it[jobs.updatedAt] = scheduledJob.updatedAt.toEpochMilli() - it[jobs.jobId] = scheduledJob.settings.id - it[jobs.name] = scheduledJob.settings.name - it[jobs.properties] = scheduledJob.settings.properties.stringify() - it[jobs.runAt] = scheduledJob.runAt?.toEpochMilli() - it[jobs.statusMessage] = null - it[jobs.retries] = 0 - it[jobs.kjobId] = null - it[jobs.step] = 0 - it[jobs.max] = null - it[jobs.startedAt] = null - it[jobs.completedAt] = null - }[jobs.id].value - } - return scheduledJob.copy(id = id.toString()) - } - - override suspend fun setProgressMax(id: String, max: Long): Boolean { - val now = Instant.now(clock).toEpochMilli() - return query { - jobs.update({ jobs.id eq id.toLong() }) { - it[jobs.max] = max.toInt() - it[jobs.updatedAt] = now - } == 1 - } - } - - override suspend fun startProgress(id: String): Boolean { - val now = Instant.now(clock).toEpochMilli() - return query { - jobs.update({ jobs.id eq id.toLong() }) { - it[jobs.startedAt] = now - it[jobs.updatedAt] = now - } == 1 - } - } - - override suspend fun stepProgress(id: String, step: Long): Boolean { - val now = Instant.now(clock).toEpochMilli() - return query { - jobs.update({ jobs.id eq id.toLong() }) { - it[jobs.step] = jobs.step + step.toInt() - it[jobs.updatedAt] = now - } == 1 - } - } - - override suspend fun update( - id: String, - oldKjobId: UUID?, - kjobId: UUID?, - status: JobStatus, - statusMessage: String?, - retries: Int - ): Boolean { - return query { - jobs.update({ (jobs.id eq id.toLong()) and if (oldKjobId == null) jobs.kjobId.isNull() else jobs.kjobId eq oldKjobId.toString() }) { - it[jobs.status] = status.name - it[jobs.retries] = retries - it[jobs.updatedAt] = Instant.now(clock).toEpochMilli() - it[jobs.id] = id.toLong() - it[jobs.statusMessage] = statusMessage - it[jobs.kjobId] = kjobId.toString() - } == 1 - } - } - - private fun String?.parseJsonMap(): Map { - this ?: return emptyMap() - return json.parseToJsonElement(this).jsonObject.mapValues { (_, el) -> - if (el is JsonObject) { - val t = el["t"]?.jsonPrimitive?.content ?: error("Cannot get jsonPrimitive") - val value = el["v"]?.jsonArray ?: error("Cannot get jsonArray") - when (t) { - "s" -> value.map { it.jsonPrimitive.content } - "d" -> value.map { it.jsonPrimitive.double } - "l" -> value.map { it.jsonPrimitive.long } - "i" -> value.map { it.jsonPrimitive.int } - "b" -> value.map { it.jsonPrimitive.boolean } - else -> error("Unknown type prefix '$t'") - }.toList() - } else { - val content = el.jsonPrimitive.content - val t = content.substringBefore(':') - val value = content.substringAfter(':') - when (t) { - "s" -> value - "d" -> value.toDouble() - "l" -> value.toLong() - "i" -> value.toInt() - "b" -> value.toBoolean() - else -> error("Unknown type prefix '$t'") - } - } - } - } - - private fun Map.stringify(): String? { - if (isEmpty()) { - return null - } - - fun listSerialize(value: List<*>): JsonElement { - return if (value.isEmpty()) { - buildJsonObject { - put("t", "s") - putJsonArray("v") {} - } - } else { - val (t, values) = when (val item = value.first()) { - is Double -> "d" to (value as List).map(::JsonPrimitive) - is Long -> "l" to (value as List).map(::JsonPrimitive) - is Int -> "i" to (value as List).map(::JsonPrimitive) - is String -> "s" to (value as List).map(::JsonPrimitive) - is Boolean -> "b" to (value as List).map(::JsonPrimitive) - else -> error("Cannot serialize unsupported list property value: $value") - } - buildJsonObject { - put("t", t) - put("v", JsonArray(values)) - } - } - } - - fun createJsonPrimitive(string: String, value: Any) = JsonPrimitive("$string:$value") - - val jsonObject = JsonObject( - mapValues { (_, value) -> - when (value) { - is List<*> -> listSerialize(value) - is Double -> createJsonPrimitive("d", value) - is Long -> createJsonPrimitive("l", value) - is Int -> createJsonPrimitive("i", value) - is String -> createJsonPrimitive("s", value) - is Boolean -> createJsonPrimitive("b", value) - else -> error("Cannot serialize unsupported property value: $value") - } - } - ) - return json.encodeToString(jsonObject) - } - - private fun ResultRow.toScheduledJob(): ScheduledJob { - val single = this - jobs.run { - return ScheduledJob( - id = single[this.id].value.toString(), - status = JobStatus.valueOf(single[status]), - runAt = single[runAt]?.let { Instant.ofEpochMilli(it) }, - statusMessage = single[statusMessage], - retries = single[retries], - kjobId = single[kjobId]?.let { - try { - UUID.fromString(it) - } catch (e: IllegalArgumentException) { - null - } - }, - createdAt = Instant.ofEpochMilli(single[createdAt]), - updatedAt = Instant.ofEpochMilli(single[updatedAt]), - settings = JobSettings( - id = single[jobId], - name = single[name], - properties = single[properties].parseJsonMap() - ), - progress = JobProgress( - step = single[step].toLong(), - max = single[max]?.toLong(), - startedAt = single[startedAt]?.let { Instant.ofEpochMilli(it) }, - completedAt = single[completedAt]?.let { Instant.ofEpochMilli(it) } - ) - ) - } - } -} diff --git a/src/main/kotlin/dev/usbharu/kjob/exposed/ExposedKJob.kt b/src/main/kotlin/dev/usbharu/kjob/exposed/ExposedKJob.kt deleted file mode 100644 index 76d00008..00000000 --- a/src/main/kotlin/dev/usbharu/kjob/exposed/ExposedKJob.kt +++ /dev/null @@ -1,50 +0,0 @@ -package dev.usbharu.kjob.exposed - -import kjob.core.BaseKJob -import kjob.core.KJob -import kjob.core.KJobFactory -import kotlinx.coroutines.runBlocking -import org.jetbrains.exposed.sql.Database -import java.time.Clock - -class ExposedKJob(config: Configuration) : BaseKJob(config) { - - companion object : KJobFactory { - override fun create(configure: Configuration.() -> Unit): KJob { - return ExposedKJob(Configuration().apply(configure)) - } - } - - class Configuration : BaseKJob.Configuration() { - var connectionString: String? = null - var driverClassName: String? = null - var connectionDatabase: Database? = null - - var jobTableName = "kjobJobs" - - var lockTableName = "kjobLocks" - - var expireLockInMinutes = 5L - } - - private val database: Database = config.connectionDatabase ?: Database.connect( - requireNotNull(config.connectionString), - requireNotNull(config.driverClassName) - ) - - override val jobRepository: ExposedJobRepository - get() = ExposedJobRepository(database, config.jobTableName, Clock.systemUTC(), config.json) - override val lockRepository: ExposedLockRepository - get() = ExposedLockRepository(database, config, clock) - - override fun start(): KJob { - jobRepository.createTable() - lockRepository.createTable() - return super.start() - } - - override fun shutdown() = runBlocking { - super.shutdown() - lockRepository.clearExpired() - } -} diff --git a/src/main/kotlin/dev/usbharu/kjob/exposed/ExposedLockRepository.kt b/src/main/kotlin/dev/usbharu/kjob/exposed/ExposedLockRepository.kt deleted file mode 100644 index 76d6fa44..00000000 --- a/src/main/kotlin/dev/usbharu/kjob/exposed/ExposedLockRepository.kt +++ /dev/null @@ -1,74 +0,0 @@ -package dev.usbharu.kjob.exposed - -import kjob.core.job.Lock -import kjob.core.repository.LockRepository -import kotlinx.coroutines.Dispatchers -import org.jetbrains.exposed.dao.id.UUIDTable -import org.jetbrains.exposed.sql.* -import org.jetbrains.exposed.sql.SqlExpressionBuilder.eq -import org.jetbrains.exposed.sql.SqlExpressionBuilder.greater -import org.jetbrains.exposed.sql.transactions.experimental.newSuspendedTransaction -import org.jetbrains.exposed.sql.transactions.transaction -import java.time.Clock -import java.time.Instant -import java.util.* -import kotlin.time.Duration.Companion.minutes - -class ExposedLockRepository( - private val database: Database, - private val config: ExposedKJob.Configuration, - private val clock: Clock -) : LockRepository { - - class Locks(tableName: String) : UUIDTable(tableName) { - val updatedAt = long("updatedAt") - val expiresAt = long("expiresAt") - } - - val locks: Locks = Locks(config.lockTableName) - - fun createTable() { - transaction(database) { - SchemaUtils.create(locks) - } - } - - suspend fun query(block: suspend () -> T): T = newSuspendedTransaction(Dispatchers.IO) { block() } - - override suspend fun exists(id: UUID): Boolean { - val now = Instant.now(clock) - return query { - locks.select(locks.id eq id and locks.expiresAt.greater(now.toEpochMilli())).empty().not() - } - } - - override suspend fun ping(id: UUID): Lock { - val now = Instant.now(clock) - val expiresAt = now.plusSeconds(config.expireLockInMinutes.minutes.inWholeSeconds) - val lock = Lock(id, now) - query { - if (locks.select(locks.id eq id).limit(1) - .map { Lock(it[locks.id].value, Instant.ofEpochMilli(it[locks.expiresAt])) }.isEmpty() - ) { - locks.insert { - it[locks.id] = id - it[locks.updatedAt] = now.toEpochMilli() - it[locks.expiresAt] = expiresAt.toEpochMilli() - } - } else { - locks.update({ locks.id eq id }) { - it[locks.updatedAt] = now.toEpochMilli() - it[locks.expiresAt] = expiresAt.toEpochMilli() - } - } - } - return lock - } - - suspend fun clearExpired() { - val now = Instant.now(clock).toEpochMilli() - query { - locks.deleteWhere { locks.expiresAt greater now } - } - } -} diff --git a/src/main/resources/application.conf b/src/main/resources/application.conf deleted file mode 100644 index beb8fa3b..00000000 --- a/src/main/resources/application.conf +++ /dev/null @@ -1,22 +0,0 @@ -ktor { - development = true - deployment { - port = 8080 - port = ${?PORT} - watch = [classes, resources] - } - application { - modules = [dev.usbharu.hideout.ApplicationKt.parent,dev.usbharu.hideout.ApplicationKt.worker] - } -} - -hideout { - url = "http://localhost:8080" - - database { - url = "jdbc:h2:./test;MODE=POSTGRESQL" - driver = "org.h2.Driver" - username = "" - password = "" - } -} diff --git a/src/main/resources/logback.xml b/src/main/resources/logback.xml deleted file mode 100644 index 60418308..00000000 --- a/src/main/resources/logback.xml +++ /dev/null @@ -1,12 +0,0 @@ - - - - %d{YYYY-MM-dd HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n - - - - - - - - diff --git a/src/test/kotlin/dev/usbharu/hideout/Empty.kt b/src/test/kotlin/dev/usbharu/hideout/Empty.kt deleted file mode 100644 index 0203d877..00000000 --- a/src/test/kotlin/dev/usbharu/hideout/Empty.kt +++ /dev/null @@ -1,7 +0,0 @@ -package dev.usbharu.hideout - -import io.ktor.server.application.* - -fun Application.empty(){ - -} diff --git a/src/test/kotlin/dev/usbharu/hideout/ap/ContextDeserializerTest.kt b/src/test/kotlin/dev/usbharu/hideout/ap/ContextDeserializerTest.kt deleted file mode 100644 index a9b933c5..00000000 --- a/src/test/kotlin/dev/usbharu/hideout/ap/ContextDeserializerTest.kt +++ /dev/null @@ -1,51 +0,0 @@ -package dev.usbharu.hideout.ap - -import com.fasterxml.jackson.databind.DeserializationFeature -import com.fasterxml.jackson.module.kotlin.jacksonObjectMapper -import com.fasterxml.jackson.module.kotlin.readValue -import org.junit.jupiter.api.Assertions.* - -class ContextDeserializerTest { - - @org.junit.jupiter.api.Test - fun deserialize() { - //language=JSON - val s = """ - { - "@context": [ - "https://www.w3.org/ns/activitystreams", - "https://w3id.org/security/v1", - { - "manuallyApprovesFollowers": "as:manuallyApprovesFollowers", - "sensitive": "as:sensitive", - "Hashtag": "as:Hashtag", - "quoteUrl": "as:quoteUrl", - "toot": "http://joinmastodon.org/ns#", - "Emoji": "toot:Emoji", - "featured": "toot:featured", - "discoverable": "toot:discoverable", - "schema": "http://schema.org#", - "PropertyValue": "schema:PropertyValue", - "value": "schema:value", - "misskey": "https://misskey-hub.net/ns#", - "_misskey_content": "misskey:_misskey_content", - "_misskey_quote": "misskey:_misskey_quote", - "_misskey_reaction": "misskey:_misskey_reaction", - "_misskey_votes": "misskey:_misskey_votes", - "_misskey_talk": "misskey:_misskey_talk", - "isCat": "misskey:isCat", - "vcard": "http://www.w3.org/2006/vcard/ns#" - } - ], - "id": "https://test-misskey-v12.usbharu.dev/follows/9bg1zu54y7/9cydqvpjcn", - "type": "Follow", - "actor": "https://test-misskey-v12.usbharu.dev/users/9bg1zu54y7", - "object": "https://test-hideout.usbharu.dev/users/test3" -} - -""" - val readValue = jacksonObjectMapper().enable(DeserializationFeature.ACCEPT_SINGLE_VALUE_AS_ARRAY).configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES,false).readValue(s) - println(readValue) - println(readValue.actor) - } -} diff --git a/src/test/kotlin/dev/usbharu/hideout/ap/ContextSerializerTest.kt b/src/test/kotlin/dev/usbharu/hideout/ap/ContextSerializerTest.kt deleted file mode 100644 index 5558cb68..00000000 --- a/src/test/kotlin/dev/usbharu/hideout/ap/ContextSerializerTest.kt +++ /dev/null @@ -1,23 +0,0 @@ -package dev.usbharu.hideout.ap - -import com.fasterxml.jackson.module.kotlin.jacksonObjectMapper -import org.junit.jupiter.api.Assertions.* -import org.junit.jupiter.api.Test - -class ContextSerializerTest{ - - @Test - fun serialize() { - val accept = Accept( - name = "aaa", - actor = "bbb", - `object` = Follow( - name = "ccc", - `object` = "ddd", - actor = "aaa" - ) - ) - val writeValueAsString = jacksonObjectMapper().writeValueAsString(accept) - println(writeValueAsString) - } -} diff --git a/src/test/kotlin/dev/usbharu/hideout/plugins/ActivityPubKtTest.kt b/src/test/kotlin/dev/usbharu/hideout/plugins/ActivityPubKtTest.kt deleted file mode 100644 index 19246b90..00000000 --- a/src/test/kotlin/dev/usbharu/hideout/plugins/ActivityPubKtTest.kt +++ /dev/null @@ -1,126 +0,0 @@ -package dev.usbharu.hideout.plugins - -import dev.usbharu.hideout.ap.JsonLd -import dev.usbharu.hideout.domain.model.User -import dev.usbharu.hideout.domain.model.UserAuthentication -import dev.usbharu.hideout.domain.model.UserAuthenticationEntity -import dev.usbharu.hideout.domain.model.UserEntity -import dev.usbharu.hideout.repository.IUserAuthRepository -import dev.usbharu.hideout.repository.IUserRepository -import dev.usbharu.hideout.service.impl.UserAuthService -import dev.usbharu.hideout.service.impl.toPem -import io.ktor.client.* -import io.ktor.client.engine.mock.* -import io.ktor.client.plugins.logging.* -import kotlinx.coroutines.runBlocking -import org.junit.jupiter.api.Test -import java.security.KeyPairGenerator -import java.security.interfaces.RSAPrivateKey -import java.security.interfaces.RSAPublicKey - -class ActivityPubKtTest { - @Test - fun HttpSignTest(): Unit = runBlocking { - - val ktorKeyMap = KtorKeyMap(UserAuthService(object : IUserRepository { - override suspend fun create(user: User): UserEntity { - TODO("Not yet implemented") - } - - override suspend fun findById(id: Long): UserEntity? { - TODO("Not yet implemented") - } - - override suspend fun findByIds(ids: List): List { - TODO("Not yet implemented") - } - - override suspend fun findByName(name: String): UserEntity? { - return UserEntity(1, "test", "localhost", "test", "","","","") - } - - override suspend fun findByNameAndDomains(names: List>): List { - TODO("Not yet implemented") - } - - override suspend fun findByUrl(url: String): UserEntity? { - TODO("Not yet implemented") - } - - override suspend fun findByUrls(urls: List): List { - TODO("Not yet implemented") - } - - override suspend fun update(userEntity: UserEntity) { - TODO("Not yet implemented") - } - - override suspend fun delete(id: Long) { - TODO("Not yet implemented") - } - - override suspend fun findAll(): List { - TODO("Not yet implemented") - } - - override suspend fun findAllByLimitAndByOffset(limit: Int, offset: Long): List { - TODO("Not yet implemented") - } - - override suspend fun createFollower(id: Long, follower: Long) { - TODO("Not yet implemented") - } - - override suspend fun deleteFollower(id: Long, follower: Long) { - TODO("Not yet implemented") - } - - override suspend fun findFollowersById(id: Long): List { - TODO("Not yet implemented") - } - - }, object : IUserAuthRepository { - override suspend fun create(userAuthentication: UserAuthentication): UserAuthenticationEntity { - TODO("Not yet implemented") - } - - override suspend fun findById(id: Long): UserAuthenticationEntity? { - TODO("Not yet implemented") - } - - override suspend fun update(userAuthenticationEntity: UserAuthenticationEntity) { - TODO("Not yet implemented") - } - - override suspend fun delete(id: Long) { - TODO("Not yet implemented") - } - - override suspend fun findByUserId(id: Long): UserAuthenticationEntity? { - val keyPairGenerator = KeyPairGenerator.getInstance("RSA") - keyPairGenerator.initialize(1024) - val generateKeyPair = keyPairGenerator.generateKeyPair() - return UserAuthenticationEntity( - 1, 1, "test", (generateKeyPair.public as RSAPublicKey).toPem(), - (generateKeyPair.private as RSAPrivateKey).toPem() - ) - } - })) - - val httpClient = HttpClient(MockEngine { httpRequestData -> - respondOk() - }) { - install(httpSignaturePlugin) { - keyMap = ktorKeyMap - } - install(Logging) { - logger = Logger.DEFAULT - level = LogLevel.ALL - } - } - - httpClient.postAp("https://localhost", "test", JsonLd(emptyList())) - - - } -} diff --git a/src/test/kotlin/dev/usbharu/hideout/plugins/KtorKeyMapTest.kt b/src/test/kotlin/dev/usbharu/hideout/plugins/KtorKeyMapTest.kt deleted file mode 100644 index 02cfb9b4..00000000 --- a/src/test/kotlin/dev/usbharu/hideout/plugins/KtorKeyMapTest.kt +++ /dev/null @@ -1,107 +0,0 @@ -package dev.usbharu.hideout.plugins - -import dev.usbharu.hideout.domain.model.User -import dev.usbharu.hideout.domain.model.UserAuthentication -import dev.usbharu.hideout.domain.model.UserAuthenticationEntity -import dev.usbharu.hideout.domain.model.UserEntity -import dev.usbharu.hideout.repository.IUserAuthRepository -import dev.usbharu.hideout.repository.IUserRepository -import dev.usbharu.hideout.service.impl.UserAuthService -import dev.usbharu.hideout.service.impl.toPem -import org.junit.jupiter.api.Test -import java.security.KeyPairGenerator -import java.security.interfaces.RSAPrivateKey -import java.security.interfaces.RSAPublicKey - -class KtorKeyMapTest { - - @Test - fun getPrivateKey() { - val ktorKeyMap = KtorKeyMap(UserAuthService(object : IUserRepository { - override suspend fun create(user: User): UserEntity { - TODO("Not yet implemented") - } - - override suspend fun findById(id: Long): UserEntity? { - TODO("Not yet implemented") - } - - override suspend fun findByIds(ids: List): List { - TODO("Not yet implemented") - } - - override suspend fun findByName(name: String): UserEntity? { - return UserEntity(1, "test", "localhost", "test", "","","","") - } - - override suspend fun findByNameAndDomains(names: List>): List { - TODO("Not yet implemented") - } - - override suspend fun findByUrl(url: String): UserEntity? { - TODO("Not yet implemented") - } - - override suspend fun findByUrls(urls: List): List { - TODO("Not yet implemented") - } - - override suspend fun update(userEntity: UserEntity) { - TODO("Not yet implemented") - } - - override suspend fun delete(id: Long) { - TODO("Not yet implemented") - } - - override suspend fun findAll(): List { - TODO("Not yet implemented") - } - - override suspend fun findAllByLimitAndByOffset(limit: Int, offset: Long): List { - TODO("Not yet implemented") - } - - override suspend fun createFollower(id: Long, follower: Long) { - TODO("Not yet implemented") - } - - override suspend fun deleteFollower(id: Long, follower: Long) { - TODO("Not yet implemented") - } - - override suspend fun findFollowersById(id: Long): List { - TODO("Not yet implemented") - } - - }, object : IUserAuthRepository { - override suspend fun create(userAuthentication: UserAuthentication): UserAuthenticationEntity { - TODO("Not yet implemented") - } - - override suspend fun findById(id: Long): UserAuthenticationEntity? { - TODO("Not yet implemented") - } - - override suspend fun update(userAuthenticationEntity: UserAuthenticationEntity) { - TODO("Not yet implemented") - } - - override suspend fun delete(id: Long) { - TODO("Not yet implemented") - } - - override suspend fun findByUserId(id: Long): UserAuthenticationEntity? { - val keyPairGenerator = KeyPairGenerator.getInstance("RSA") - keyPairGenerator.initialize(1024) - val generateKeyPair = keyPairGenerator.generateKeyPair() - return UserAuthenticationEntity( - 1, 1, "test", (generateKeyPair.public as RSAPublicKey).toPem(), - (generateKeyPair.private as RSAPrivateKey).toPem() - ) - } - })) - - ktorKeyMap.getPrivateKey("test") - } -} diff --git a/src/test/kotlin/dev/usbharu/hideout/repository/UserRepositoryTest.kt b/src/test/kotlin/dev/usbharu/hideout/repository/UserRepositoryTest.kt deleted file mode 100644 index 740bc90e..00000000 --- a/src/test/kotlin/dev/usbharu/hideout/repository/UserRepositoryTest.kt +++ /dev/null @@ -1,120 +0,0 @@ -@file:OptIn(ExperimentalCoroutinesApi::class) - -package dev.usbharu.hideout.repository - -import dev.usbharu.hideout.domain.model.User -import dev.usbharu.hideout.domain.model.Users -import dev.usbharu.hideout.domain.model.UsersFollowers -import kotlinx.coroutines.ExperimentalCoroutinesApi -import kotlinx.coroutines.test.runTest -import org.jetbrains.exposed.sql.Database -import org.jetbrains.exposed.sql.SchemaUtils -import org.jetbrains.exposed.sql.select -import org.jetbrains.exposed.sql.transactions.transaction -import org.junit.jupiter.api.AfterEach -import org.junit.jupiter.api.Assertions.assertIterableEquals -import org.junit.jupiter.api.BeforeEach -import org.junit.jupiter.api.Test - - -class UserRepositoryTest { - - lateinit var db: Database - - @BeforeEach - fun beforeEach() { - db = Database.connect("jdbc:h2:mem:test;DB_CLOSE_DELAY=-1", driver = "org.h2.Driver") - transaction(db) { - SchemaUtils.create(Users) - SchemaUtils.create(UsersFollowers) - } - } - - @AfterEach - fun tearDown() { - transaction(db) { - - SchemaUtils.drop(UsersFollowers) - SchemaUtils.drop(Users) - } - } - - @Test - fun `findFollowersById フォロワー一覧を取得`() = runTest { - val userRepository = UserRepository(db) - val user = userRepository.create( - User( - "test", - "example.com", - "testUser", - "This user is test user.", - "https://example.com/inbox", - "https://example.com/outbox", - "https://example.com" - ) - ) - val follower = userRepository.create( - User( - "follower", - "follower.example.com", - "followerUser", - "This user is follower user.", - "https://follower.example.com/inbox", - "https://follower.example.com/outbox", - "https://follower.example.com" - ) - ) - val follower2 = userRepository.create( - User( - "follower2", - "follower2.example.com", - "followerUser2", - "This user is follower user 2.", - "https://follower2.example.com/inbox", - "https://follower2.example.com/outbox", - "https://follower2.example.com" - ) - ) - userRepository.createFollower(user.id, follower.id) - userRepository.createFollower(user.id, follower2.id) - userRepository.findFollowersById(user.id).let { - assertIterableEquals(listOf(follower, follower2), it) - } - - } - - @Test - fun `createFollower フォロワー追加`() = runTest { - val userRepository = UserRepository(db) - val user = userRepository.create( - User( - "test", - "example.com", - "testUser", - "This user is test user.", - "https://example.com/inbox", - "https://example.com/outbox", - "https://example.com" - ) - ) - val follower = userRepository.create( - User( - "follower", - "follower.example.com", - "followerUser", - "This user is follower user.", - "https://follower.example.com/inbox", - "https://follower.example.com/outbox", - "https://follower.example.com" - ) - ) - userRepository.createFollower(user.id, follower.id) - transaction { - - val followerIds = - UsersFollowers.select { UsersFollowers.userId eq user.id }.map { it[UsersFollowers.followerId] } - assertIterableEquals(listOf(follower.id), followerIds) - } - - } -} diff --git a/src/test/kotlin/dev/usbharu/hideout/routing/activitypub/InboxRoutingKtTest.kt b/src/test/kotlin/dev/usbharu/hideout/routing/activitypub/InboxRoutingKtTest.kt deleted file mode 100644 index c4de8bfb..00000000 --- a/src/test/kotlin/dev/usbharu/hideout/routing/activitypub/InboxRoutingKtTest.kt +++ /dev/null @@ -1,96 +0,0 @@ -package dev.usbharu.hideout.routing.activitypub - -import dev.usbharu.hideout.exception.JsonParseException -import dev.usbharu.hideout.plugins.configureRouting -import dev.usbharu.hideout.plugins.configureSerialization -import dev.usbharu.hideout.plugins.configureStatusPages -import dev.usbharu.hideout.service.activitypub.ActivityPubService -import dev.usbharu.hideout.service.activitypub.ActivityPubUserService -import dev.usbharu.hideout.service.impl.UserService -import dev.usbharu.hideout.service.signature.HttpSignatureVerifyService -import io.ktor.client.request.* -import io.ktor.http.* -import io.ktor.server.config.* -import io.ktor.server.testing.* -import org.junit.jupiter.api.Assertions -import org.junit.jupiter.api.Test -import org.mockito.kotlin.any -import org.mockito.kotlin.doReturn -import org.mockito.kotlin.doThrow -import org.mockito.kotlin.mock - -class InboxRoutingKtTest { - @Test - fun `sharedInboxにGETしたら405が帰ってくる`() = testApplication { - environment { - config = ApplicationConfig("empty.conf") - } - application { - configureSerialization() - configureRouting(mock(), mock(), mock(), mock()) - } - client.get("/inbox").let { - Assertions.assertEquals(HttpStatusCode.MethodNotAllowed, it.status) - } - } - - @Test - fun `sharedInboxに空のリクエストボディでPOSTしたら400が帰ってくる`() = testApplication { - environment { - config = ApplicationConfig("empty.conf") - } - val httpSignatureVerifyService = mock{ - on { verify(any()) } doReturn true - } - val activityPubService = mock{ - on { parseActivity(any()) } doThrow JsonParseException() - } - val userService = mock() - val activityPubUserService = mock() - application { - configureStatusPages() - configureSerialization() - configureRouting(httpSignatureVerifyService, activityPubService, userService, activityPubUserService) - } - client.post("/inbox").let { - Assertions.assertEquals(HttpStatusCode.BadRequest, it.status) - } - } - - @Test - fun `ユーザのinboxにGETしたら405が帰ってくる`() = testApplication { - environment { - config = ApplicationConfig("empty.conf") - } - application { - configureSerialization() - configureRouting(mock(), mock(), mock(), mock()) - } - client.get("/users/test/inbox").let { - Assertions.assertEquals(HttpStatusCode.MethodNotAllowed, it.status) - } - } - - @Test - fun `ユーザーのinboxに空のリクエストボディでPOSTしたら400が帰ってくる`() = testApplication { - environment { - config = ApplicationConfig("empty.conf") - } - val httpSignatureVerifyService = mock{ - on { verify(any()) } doReturn true - } - val activityPubService = mock{ - on { parseActivity(any()) } doThrow JsonParseException() - } - val userService = mock() - val activityPubUserService = mock() - application { - configureStatusPages() - configureSerialization() - configureRouting(httpSignatureVerifyService, activityPubService, userService, activityPubUserService) - } - client.post("/users/test/inbox").let { - Assertions.assertEquals(HttpStatusCode.BadRequest, it.status) - } - } -} diff --git a/src/test/kotlin/dev/usbharu/hideout/routing/activitypub/UsersAPTest.kt b/src/test/kotlin/dev/usbharu/hideout/routing/activitypub/UsersAPTest.kt deleted file mode 100644 index e09f504b..00000000 --- a/src/test/kotlin/dev/usbharu/hideout/routing/activitypub/UsersAPTest.kt +++ /dev/null @@ -1,91 +0,0 @@ -package dev.usbharu.hideout.routing.activitypub - -import com.fasterxml.jackson.annotation.JsonInclude -import com.fasterxml.jackson.annotation.JsonSetter -import com.fasterxml.jackson.annotation.Nulls -import com.fasterxml.jackson.databind.DeserializationFeature -import com.fasterxml.jackson.module.kotlin.jacksonObjectMapper -import com.fasterxml.jackson.module.kotlin.readValue -import dev.usbharu.hideout.ap.Image -import dev.usbharu.hideout.ap.Key -import dev.usbharu.hideout.ap.Person -import dev.usbharu.hideout.plugins.configureRouting -import dev.usbharu.hideout.plugins.configureSerialization -import dev.usbharu.hideout.service.activitypub.ActivityPubService -import dev.usbharu.hideout.service.activitypub.ActivityPubUserService -import dev.usbharu.hideout.service.impl.UserService -import dev.usbharu.hideout.service.signature.HttpSignatureVerifyService -import dev.usbharu.hideout.util.HttpUtil.Activity -import io.ktor.client.request.* -import io.ktor.client.statement.* -import io.ktor.http.* -import io.ktor.server.config.* -import io.ktor.server.testing.* -import org.junit.jupiter.api.Test -import org.mockito.ArgumentMatchers.anyString -import org.mockito.kotlin.doReturn -import org.mockito.kotlin.mock -import kotlin.test.assertEquals - - -class UsersAPTest { - - @Test() - fun `ユーザのURLにAcceptヘッダーをActivityにしてアクセスしたときPersonが返ってくる`() = testApplication { - environment { - config = ApplicationConfig("empty.conf") - } - val person = Person( - type = emptyList(), - name = "test", - id = "http://example.com/users/test", - preferredUsername = "test", - summary = "test user", - inbox = "http://example.com/users/test/inbox", - outbox = "http://example.com/users/test/outbox", - url = "http://example.com/users/test", - icon = Image( - type = emptyList(), - name = "http://example.com/users/test/icon.png", - mediaType = "image/png", - url = "http://example.com/users/test/icon.png" - ), - publicKey = Key( - type = emptyList(), - name = "Public Key", - id = "http://example.com/users/test#pubkey", - owner = "https://example.com/users/test", - publicKeyPem = "-----BEGIN PUBLIC KEY-----\n\n-----END PUBLIC KEY-----" - ) - ) - person.context = listOf("https://www.w3.org/ns/activitystreams") - - val httpSignatureVerifyService = mock {} - val activityPubService = mock {} - val userService = mock {} - - val activityPubUserService = mock { - onBlocking { getPersonByName(anyString()) } doReturn person - } - - application { - configureSerialization() - configureRouting(httpSignatureVerifyService, activityPubService, userService, activityPubUserService) - } - client.get("/users/test") { - accept(ContentType.Application.Activity) - }.let { - val objectMapper = jacksonObjectMapper().enable(DeserializationFeature.ACCEPT_SINGLE_VALUE_AS_ARRAY) - .setSerializationInclusion(JsonInclude.Include.NON_EMPTY) - .configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false) - objectMapper.configOverride(List::class.java).setSetterInfo( - JsonSetter.Value.forValueNulls( - Nulls.AS_EMPTY - ) - ) - val actual = it.bodyAsText() - val readValue = objectMapper.readValue(actual) - assertEquals(person, readValue) - } - } -} diff --git a/src/test/kotlin/dev/usbharu/hideout/service/activitypub/ActivityPubFollowServiceImplTest.kt b/src/test/kotlin/dev/usbharu/hideout/service/activitypub/ActivityPubFollowServiceImplTest.kt deleted file mode 100644 index d447ce3e..00000000 --- a/src/test/kotlin/dev/usbharu/hideout/service/activitypub/ActivityPubFollowServiceImplTest.kt +++ /dev/null @@ -1,153 +0,0 @@ -@file:OptIn(ExperimentalCoroutinesApi::class) -@file:Suppress("INVISIBLE_MEMBER", "INVISIBLE_REFERENCE") - -package dev.usbharu.hideout.service.activitypub - -import com.fasterxml.jackson.module.kotlin.readValue -import dev.usbharu.hideout.ap.* -import dev.usbharu.hideout.config.Config -import dev.usbharu.hideout.config.ConfigData -import dev.usbharu.hideout.domain.model.UserEntity -import dev.usbharu.hideout.domain.model.job.ReceiveFollowJob -import dev.usbharu.hideout.service.impl.UserService -import dev.usbharu.hideout.service.job.JobQueueParentService -import io.ktor.client.* -import io.ktor.client.engine.mock.* -import kjob.core.dsl.ScheduleContext -import kjob.core.job.JobProps -import kotlinx.coroutines.ExperimentalCoroutinesApi -import kotlinx.coroutines.test.runTest -import kotlinx.serialization.json.Json -import org.junit.jupiter.api.Assertions.assertEquals -import org.junit.jupiter.api.Test -import org.mockito.ArgumentMatchers.anyString -import org.mockito.kotlin.* -import utils.JsonObjectMapper - -class ActivityPubFollowServiceImplTest { - @Test - fun `receiveFollow フォロー受付処理`() = runTest { - val jobQueueParentService = mock { - onBlocking { schedule(eq(ReceiveFollowJob), any()) } doReturn Unit - } - val activityPubFollowService = ActivityPubFollowServiceImpl(jobQueueParentService, mock(), mock(), mock()) - activityPubFollowService.receiveFollow( - Follow( - emptyList(), - "Follow", - "https://example.com", - "https://follower.example.com" - ) - ) - verify(jobQueueParentService, times(1)).schedule(eq(ReceiveFollowJob), any()) - argumentCaptor.(ReceiveFollowJob) -> Unit> { - verify(jobQueueParentService, times(1)).schedule(eq(ReceiveFollowJob), capture()) - val scheduleContext = ScheduleContext(Json) - firstValue.invoke(scheduleContext, ReceiveFollowJob) - val actor = scheduleContext.props.props[ReceiveFollowJob.actor.name] - val targetActor = scheduleContext.props.props[ReceiveFollowJob.targetActor.name] - val follow = scheduleContext.props.props[ReceiveFollowJob.follow.name] - assertEquals("https://follower.example.com", actor) - assertEquals("https://example.com", targetActor) - assertEquals( - """{"type":"Follow","name":"Follow","object":"https://example.com","actor":"https://follower.example.com","@context":null}""", - follow - ) - } - } - - @Test - fun `receiveFollowJob フォロー受付処理のJob`() = runTest { - Config.configData = ConfigData(objectMapper = JsonObjectMapper.objectMapper) - val person = Person( - type = emptyList(), - name = "follower", - id = "https://follower.example.com", - preferredUsername = "followerUser", - summary = "This user is follower user.", - inbox = "https://follower.example.com/inbox", - outbox = "https://follower.example.com/outbox", - url = "https://follower.example.com", - icon = Image( - type = emptyList(), - name = "https://follower.example.com/image", - mediaType = "image/png", - url = "https://follower.example.com/image" - ), - publicKey = Key( - type = emptyList(), - name = "Public Key", - id = "https://follower.example.com#main-key", - owner = "https://follower.example.com", - publicKeyPem = "BEGIN PUBLIC KEY...END PUBLIC KEY", - ) - - ) - val activityPubUserService = mock { - onBlocking { fetchPerson(anyString()) } doReturn person - } - val userService = mock { - onBlocking { findByUrls(any()) } doReturn listOf( - UserEntity( - id = 1L, - name = "test", - domain = "example.com", - screenName = "testUser", - description = "This user is test user.", - inbox = "https://example.com/inbox", - outbox = "https://example.com/outbox", - url = "https://example.com" - ), - UserEntity( - id = 2L, - name = "follower", - domain = "follower.example.com", - screenName = "followerUser", - description = "This user is test follower user.", - inbox = "https://follower.example.com/inbox", - outbox = "https://follower.example.com/outbox", - url = "https://follower.example.com" - ) - ) - onBlocking { addFollowers(any(), any()) } doReturn Unit - } - val activityPubFollowService = - ActivityPubFollowServiceImpl( - mock(), - activityPubUserService, - userService, - HttpClient(MockEngine { httpRequestData -> - assertEquals(person.inbox, httpRequestData.url.toString()) - val accept = Accept( - type = emptyList(), - name = "Follow", - `object` = Follow( - type = emptyList(), - name = "Follow", - `object` = "https://example.com", - actor = "https://follower.example.com" - ), - actor = "https://example.com" - ) - accept.context += "https://www.w3.org/ns/activitystreams" - assertEquals( - accept, - Config.configData.objectMapper.readValue( - httpRequestData.body.toByteArray().decodeToString() - ) - ) - respondOk() - }) - ) - activityPubFollowService.receiveFollowJob( - JobProps( - data = mapOf( - ReceiveFollowJob.actor.name to "https://follower.example.com", - ReceiveFollowJob.targetActor.name to "https://example.com", - ReceiveFollowJob.follow.name to """{"type":"Follow","name":"Follow","object":"https://example.com","actor":"https://follower.example.com","@context":null}""" - ), - json = Json - ) - ) - } -} diff --git a/src/test/kotlin/dev/usbharu/hideout/service/job/KJobJobQueueWorkerServiceTest.kt b/src/test/kotlin/dev/usbharu/hideout/service/job/KJobJobQueueWorkerServiceTest.kt deleted file mode 100644 index 0244399a..00000000 --- a/src/test/kotlin/dev/usbharu/hideout/service/job/KJobJobQueueWorkerServiceTest.kt +++ /dev/null @@ -1,16 +0,0 @@ -package dev.usbharu.hideout.service.job - -import kjob.core.Job -import org.jetbrains.exposed.sql.Database -import org.junit.jupiter.api.Test - -class KJobJobQueueWorkerServiceTest { - - object TestJob : Job("test-job") - - @Test - fun init() { - val kJobJobWorkerService = KJobJobQueueWorkerService(Database.connect("jdbc:h2:mem:")) - kJobJobWorkerService.init(listOf(TestJob to { it -> execute { it as TestJob;println(it.propNames) } })) - } -} diff --git a/src/test/kotlin/utils/DBResetInterceptor.kt b/src/test/kotlin/utils/DBResetInterceptor.kt deleted file mode 100644 index 32fc88a9..00000000 --- a/src/test/kotlin/utils/DBResetInterceptor.kt +++ /dev/null @@ -1,28 +0,0 @@ -package utils - -import org.jetbrains.exposed.sql.Transaction -import org.jetbrains.exposed.sql.transactions.TransactionManager -import org.junit.jupiter.api.extension.* - -class DBResetInterceptor : BeforeAllCallback,AfterAllCallback,BeforeEachCallback,AfterEachCallback { - private lateinit var transactionAll: Transaction - private lateinit var transactionEach: Transaction - - override fun beforeAll(context: ExtensionContext?) { - transactionAll = TransactionManager.manager.newTransaction() - } - - override fun afterAll(context: ExtensionContext?) { - transactionAll.rollback() - transactionAll.close() - } - - override fun beforeEach(context: ExtensionContext?) { - transactionEach = TransactionManager.manager.newTransaction(outerTransaction = transactionAll) - } - - override fun afterEach(context: ExtensionContext?) { - transactionEach.rollback() - transactionEach.close() - } -} diff --git a/src/test/kotlin/utils/DatabaseTestBase.kt b/src/test/kotlin/utils/DatabaseTestBase.kt deleted file mode 100644 index 10653631..00000000 --- a/src/test/kotlin/utils/DatabaseTestBase.kt +++ /dev/null @@ -1,18 +0,0 @@ -package utils - -import org.jetbrains.exposed.sql.Database -import org.jetbrains.exposed.sql.DatabaseConfig -import org.junit.jupiter.api.extension.ExtendWith - - -@ExtendWith(DBResetInterceptor::class) -abstract class DatabaseTestBase { - companion object { - init { - Database.connect( - "jdbc:h2:mem:test;DB_CLOSE_DELAY=-1", - driver = "org.h2.Driver", - databaseConfig = DatabaseConfig { useNestedTransactions = true }) - } - } -} diff --git a/src/test/kotlin/utils/JsonObjectMapper.kt b/src/test/kotlin/utils/JsonObjectMapper.kt deleted file mode 100644 index 4a595630..00000000 --- a/src/test/kotlin/utils/JsonObjectMapper.kt +++ /dev/null @@ -1,22 +0,0 @@ -package utils - -import com.fasterxml.jackson.annotation.JsonInclude -import com.fasterxml.jackson.annotation.JsonSetter -import com.fasterxml.jackson.annotation.Nulls -import com.fasterxml.jackson.databind.DeserializationFeature -import com.fasterxml.jackson.module.kotlin.jacksonObjectMapper - -object JsonObjectMapper { - val objectMapper: com.fasterxml.jackson.databind.ObjectMapper = - jacksonObjectMapper().enable(DeserializationFeature.ACCEPT_SINGLE_VALUE_AS_ARRAY) - .setSerializationInclusion(JsonInclude.Include.NON_EMPTY) - .configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false) - - init { - objectMapper.configOverride(List::class.java).setSetterInfo( - JsonSetter.Value.forValueNulls( - Nulls.AS_EMPTY - ) - ) - } -}