mirror of
https://github.com/ReVanced/revanced-api.git
synced 2026-01-11 22:06:19 +00:00
Compare commits
41 Commits
v1.1.0-dev
...
v1.4.0-dev
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
65ee2700e7 | ||
|
|
56a00ddb85 | ||
|
|
50b81fd337 | ||
|
|
c51db8da72 | ||
|
|
06098415f1 | ||
|
|
9ed724e161 | ||
|
|
a6d7da1205 | ||
|
|
87174eadd6 | ||
|
|
fde2857915 | ||
|
|
fae8cb6b23 | ||
|
|
a754159800 | ||
|
|
080e2e582c | ||
|
|
8ff1bbd41f | ||
|
|
6442757927 | ||
|
|
710416ff36 | ||
|
|
1181be12e2 | ||
|
|
53c36002e9 | ||
|
|
8b17d8894d | ||
|
|
1e3e46ff4f | ||
|
|
977d252497 | ||
|
|
96bcd7719a | ||
|
|
2d85ce17f6 | ||
|
|
c26e129bda | ||
|
|
84ea5e4a41 | ||
|
|
491533d3f4 | ||
|
|
ef5f0b5ddd | ||
|
|
c0dc763f99 | ||
|
|
11327af879 | ||
|
|
9f0eb5bfe9 | ||
|
|
d2575d5191 | ||
|
|
32bedb7fad | ||
|
|
d605efd54a | ||
|
|
74e53891a1 | ||
|
|
27b18c62f5 | ||
|
|
df116bd221 | ||
|
|
2d7b4e7b7f | ||
|
|
4e8e83db1a | ||
|
|
e113daa089 | ||
|
|
170edd3157 | ||
|
|
d18e09cba3 | ||
|
|
bc919b85f5 |
4
.github/workflows/release.yml
vendored
4
.github/workflows/release.yml
vendored
@@ -12,7 +12,7 @@ jobs:
|
||||
name: Release
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
contents: read
|
||||
contents: write
|
||||
packages: write
|
||||
steps:
|
||||
- name: Checkout
|
||||
@@ -60,7 +60,7 @@ jobs:
|
||||
DOCKER_REGISTRY_USER: ${{ github.actor }}
|
||||
DOCKER_REGISTRY_PASSWORD: ${{ secrets.GITHUB_TOKEN }}
|
||||
GITHUB_ACTOR: ${{ github.actor }}
|
||||
GITHUB_TOKEN: ${{ secrets.REPOSITORY_PUSH_ACCESS }}
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
run: npm exec semantic-release
|
||||
|
||||
- name: Set Portainer stack webhook URL based on branch
|
||||
|
||||
@@ -21,10 +21,10 @@
|
||||
"@semantic-release/git",
|
||||
{
|
||||
"assets": [
|
||||
"README.md",
|
||||
"CHANGELOG.md",
|
||||
"gradle.properties"
|
||||
]
|
||||
],
|
||||
"message": "chore: Release v${nextRelease.version} [skip ci]\n\n${nextRelease.notes}"
|
||||
}
|
||||
],
|
||||
[
|
||||
|
||||
140
CHANGELOG.md
140
CHANGELOG.md
@@ -1,3 +1,143 @@
|
||||
# [1.4.0-dev.1](https://github.com/ReVanced/revanced-api/compare/v1.3.0...v1.4.0-dev.1) (2024-11-01)
|
||||
|
||||
|
||||
### Features
|
||||
|
||||
* Improve announcements API ([#192](https://github.com/ReVanced/revanced-api/issues/192)) ([56a00dd](https://github.com/ReVanced/revanced-api/commit/56a00ddb85f302d441f0b222a9902ea2c1c18897))
|
||||
|
||||
# [1.3.0](https://github.com/ReVanced/revanced-api/compare/v1.2.0...v1.3.0) (2024-10-07)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* Add missing OK response to routes ([1181be1](https://github.com/ReVanced/revanced-api/commit/1181be12e2223b245019f64570bc8f7bef4e7dc2))
|
||||
* Allow more necessary HTTP methods for CORS ([080e2e5](https://github.com/ReVanced/revanced-api/commit/080e2e582cb8ea97421c402a4cb82414e11fb1cf))
|
||||
* Configure CORS properly to allow authorization and content-type header ([6442757](https://github.com/ReVanced/revanced-api/commit/6442757927c0307c01b2793858d25df7e3fca122))
|
||||
* Expire token relative to current date time instead of just time ([c26e129](https://github.com/ReVanced/revanced-api/commit/c26e129bda09345761f291917f026c13e89a2572))
|
||||
* Expose www-authenticate header to JS ([9ed724e](https://github.com/ReVanced/revanced-api/commit/9ed724e161f9029967f67e4c2066f2fdf7be0a27))
|
||||
* Respond with JSON when returning token ([1e3e46f](https://github.com/ReVanced/revanced-api/commit/1e3e46ff4f7c12569b88fcd1bc252aeb5a611b63))
|
||||
* Specify a validation function to fix authentication ([53c3600](https://github.com/ReVanced/revanced-api/commit/53c36002e9af89aa5fed71f831470b42d5d777c9))
|
||||
|
||||
|
||||
### Features
|
||||
|
||||
* Add missing parameter and response documentation ([491533d](https://github.com/ReVanced/revanced-api/commit/491533d3f44ccd716eee80123d0875a05eb9435b))
|
||||
* Customize logging level through environment variable ([8b17d88](https://github.com/ReVanced/revanced-api/commit/8b17d8894db8db4a168c30be50af91c04e173e14))
|
||||
* Improve response info description wording ([977d252](https://github.com/ReVanced/revanced-api/commit/977d25249738b24cb6a3530543349efe1d71a9ba))
|
||||
* Only allow requests from HTTPs ([a6d7da1](https://github.com/ReVanced/revanced-api/commit/a6d7da1205ef7bc23eba0b1fca2480a4327def19))
|
||||
|
||||
# [1.3.0-dev.6](https://github.com/ReVanced/revanced-api/compare/v1.3.0-dev.5...v1.3.0-dev.6) (2024-10-02)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* Expose www-authenticate header to JS ([9ed724e](https://github.com/ReVanced/revanced-api/commit/9ed724e161f9029967f67e4c2066f2fdf7be0a27))
|
||||
|
||||
|
||||
### Features
|
||||
|
||||
* Only allow requests from HTTPs ([a6d7da1](https://github.com/ReVanced/revanced-api/commit/a6d7da1205ef7bc23eba0b1fca2480a4327def19))
|
||||
|
||||
# [1.3.0-dev.5](https://github.com/ReVanced/revanced-api/compare/v1.3.0-dev.4...v1.3.0-dev.5) (2024-09-30)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* Allow more necessary HTTP methods for CORS ([080e2e5](https://github.com/ReVanced/revanced-api/commit/080e2e582cb8ea97421c402a4cb82414e11fb1cf))
|
||||
|
||||
# [1.3.0-dev.4](https://github.com/ReVanced/revanced-api/compare/v1.3.0-dev.3...v1.3.0-dev.4) (2024-09-29)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* Configure CORS properly to allow authorization and content-type header ([6442757](https://github.com/ReVanced/revanced-api/commit/6442757927c0307c01b2793858d25df7e3fca122))
|
||||
|
||||
# [1.3.0-dev.3](https://github.com/ReVanced/revanced-api/compare/v1.3.0-dev.2...v1.3.0-dev.3) (2024-09-29)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* Add missing OK response to routes ([1181be1](https://github.com/ReVanced/revanced-api/commit/1181be12e2223b245019f64570bc8f7bef4e7dc2))
|
||||
* Respond with JSON when returning token ([1e3e46f](https://github.com/ReVanced/revanced-api/commit/1e3e46ff4f7c12569b88fcd1bc252aeb5a611b63))
|
||||
* Specify a validation function to fix authentication ([53c3600](https://github.com/ReVanced/revanced-api/commit/53c36002e9af89aa5fed71f831470b42d5d777c9))
|
||||
|
||||
|
||||
### Features
|
||||
|
||||
* Customize logging level through environment variable ([8b17d88](https://github.com/ReVanced/revanced-api/commit/8b17d8894db8db4a168c30be50af91c04e173e14))
|
||||
* Improve response info description wording ([977d252](https://github.com/ReVanced/revanced-api/commit/977d25249738b24cb6a3530543349efe1d71a9ba))
|
||||
|
||||
# [1.3.0-dev.2](https://github.com/ReVanced/revanced-api/compare/v1.3.0-dev.1...v1.3.0-dev.2) (2024-09-27)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* Expire token relative to current date time instead of just time ([c26e129](https://github.com/ReVanced/revanced-api/commit/c26e129bda09345761f291917f026c13e89a2572))
|
||||
|
||||
# [1.3.0-dev.1](https://github.com/ReVanced/revanced-api/compare/v1.2.0...v1.3.0-dev.1) (2024-09-11)
|
||||
|
||||
|
||||
### Features
|
||||
|
||||
* Add missing parameter and response documentation ([491533d](https://github.com/ReVanced/revanced-api/commit/491533d3f44ccd716eee80123d0875a05eb9435b))
|
||||
|
||||
# [1.2.0](https://github.com/ReVanced/revanced-api/compare/v1.1.0...v1.2.0) (2024-09-06)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* Add back deprecated routes for backwards compatibility ([9f0eb5b](https://github.com/ReVanced/revanced-api/commit/9f0eb5bfe9d0436e76462b9c094f8b1158e04a44))
|
||||
* Make sure, expected paths in configuration exist ([32bedb7](https://github.com/ReVanced/revanced-api/commit/32bedb7fad3eef8116625964e5e1f0a2543ea2a4))
|
||||
* Return correct GPG keys url ([#187](https://github.com/ReVanced/revanced-api/issues/187)) ([74e5389](https://github.com/ReVanced/revanced-api/commit/74e53891a17bd3f76f358477e4228550e6f70149))
|
||||
|
||||
|
||||
### Features
|
||||
|
||||
* Move /latest routes to parent ([4e8e83d](https://github.com/ReVanced/revanced-api/commit/4e8e83db1a20c76a81967af4e7e3a8634649790a))
|
||||
* Respond to all ping request methods ([df116bd](https://github.com/ReVanced/revanced-api/commit/df116bd22134c8222c72b28e9387bc9871d3473e))
|
||||
|
||||
# [1.2.0-dev.4](https://github.com/ReVanced/revanced-api/compare/v1.2.0-dev.3...v1.2.0-dev.4) (2024-09-06)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* Add back deprecated routes for backwards compatibility ([9f0eb5b](https://github.com/ReVanced/revanced-api/commit/9f0eb5bfe9d0436e76462b9c094f8b1158e04a44))
|
||||
* Make sure, expected paths in configuration exist ([32bedb7](https://github.com/ReVanced/revanced-api/commit/32bedb7fad3eef8116625964e5e1f0a2543ea2a4))
|
||||
|
||||
# [1.2.0-dev.3](https://github.com/ReVanced/revanced-api/compare/v1.2.0-dev.2...v1.2.0-dev.3) (2024-09-04)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* Return correct GPG keys url ([#187](https://github.com/ReVanced/revanced-api/issues/187)) ([74e5389](https://github.com/ReVanced/revanced-api/commit/74e53891a17bd3f76f358477e4228550e6f70149))
|
||||
|
||||
# [1.2.0-dev.2](https://github.com/ReVanced/revanced-api/compare/v1.2.0-dev.1...v1.2.0-dev.2) (2024-08-24)
|
||||
|
||||
|
||||
### Features
|
||||
|
||||
* Respond to all ping request methods ([df116bd](https://github.com/ReVanced/revanced-api/commit/df116bd22134c8222c72b28e9387bc9871d3473e))
|
||||
|
||||
# [1.2.0-dev.1](https://github.com/ReVanced/revanced-api/compare/v1.1.0...v1.2.0-dev.1) (2024-08-16)
|
||||
|
||||
|
||||
### Features
|
||||
|
||||
* Move /latest routes to parent ([4e8e83d](https://github.com/ReVanced/revanced-api/commit/4e8e83db1a20c76a81967af4e7e3a8634649790a))
|
||||
|
||||
# [1.1.0](https://github.com/ReVanced/revanced-api/compare/v1.0.0...v1.1.0) (2024-07-15)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* Don't encode public keys & instead send them raw ([435beae](https://github.com/ReVanced/revanced-api/commit/435beae3831fc8ce161aec676ff20f253b1caf66))
|
||||
|
||||
|
||||
### Features
|
||||
|
||||
* Add static file paths to remove env specific files in resources ([39d0b78](https://github.com/ReVanced/revanced-api/commit/39d0b78c7919f684439b6f052ab3f064159c2a70))
|
||||
* Convert static about file to documented route & add key parameter to about route ([dfe6df3](https://github.com/ReVanced/revanced-api/commit/dfe6df3ef6006d06681673bcfaf87c44c40ad446))
|
||||
|
||||
# [1.1.0-dev.1](https://github.com/ReVanced/revanced-api/compare/v1.0.0...v1.1.0-dev.1) (2024-07-15)
|
||||
|
||||
|
||||
|
||||
24
README.md
24
README.md
@@ -75,7 +75,7 @@ with updates and ReVanced Patches.
|
||||
|
||||
Some of the features ReVanced API include:
|
||||
|
||||
- 📢 **Announcements**: Post and get announcements grouped by channels
|
||||
- 📢 **Announcements**: Post and get announcements
|
||||
- ℹ️ **About**: Get more information such as a description, ways to donate to,
|
||||
and links of the hoster of ReVanced API
|
||||
- 🧩 **Patches**: Get the latest updates of ReVanced Patches, directly from ReVanced API
|
||||
@@ -96,20 +96,30 @@ so before you can pull the image, you need to [authenticate to the Container reg
|
||||
|
||||
1. Create an `.env` file using [.env.example](.env.example) as a template
|
||||
2. Create a `configuration.toml` file using [configuration.example.toml](configuration.example.toml) as a template
|
||||
3. Create a `docker-compose.yml` file using [docker-compose.example.yml](docker-compose.example.yml) as a template
|
||||
4. Run `docker-compose up -d` to start the server
|
||||
3. Create an `about.json` file using [about.example.json](about.example.json) as a template
|
||||
4. Create a `docker-compose.yml` file using [docker-compose.example.yml](docker-compose.example.yml) as a template
|
||||
5. Run `docker-compose up -d` to start the server
|
||||
|
||||
### 💻 Docker CLI
|
||||
|
||||
1. Create an `.env` file using [.env.example](.env.example) as a template
|
||||
2. Create a `configuration.toml` file using [configuration.example.toml](configuration.example.toml) as a template
|
||||
3. Start the container using the following command:
|
||||
3. Create an `about.json` file using [about.example.json](about.example.json) as a template
|
||||
4. Start the container using the following command:
|
||||
```shell
|
||||
docker run -d --name revanced-api \
|
||||
# Mount the .env file
|
||||
-v $(pwd)/.env:/app/.env \
|
||||
# Mount the configuration.toml file
|
||||
-v $(pwd)/configuration.toml:/app/configuration.toml \
|
||||
# Mount the patches public key
|
||||
-v $(pwd)/patches-public-key.asc:/app/patches-public-key.asc \
|
||||
# Mount the integrations public key
|
||||
-v $(pwd)/integrations-public-key.asc:/app/integrations-public-key.asc \
|
||||
# Mount the static folder
|
||||
-v $(pwd)/static:/app/static \
|
||||
# Mount the about.json file
|
||||
-v $(pwd)/about.json:/app/about.json \
|
||||
# Mount the persistence folder
|
||||
-v $(pwd)/persistence:/app/persistence \
|
||||
# Expose the port 8888
|
||||
@@ -132,7 +142,8 @@ A Java Runtime Environment (JRE) must be installed.
|
||||
2. In the same folder, create an `.env` file using [.env.example](.env.example) as a template
|
||||
3. In the same folder, create a `configuration.toml` file
|
||||
using [configuration.example.toml](configuration.example.toml) as a template
|
||||
4. Run `java -jar revanced-api.jar start` to start the server
|
||||
4. In the same folder, create an `about.json` file using [about.example.json](about.example.json) as a template
|
||||
5. Run `java -jar revanced-api.jar start` to start the server
|
||||
|
||||
### 🛠️ From source
|
||||
|
||||
@@ -141,7 +152,8 @@ A Java Development Kit (JDK) and Git must be installed.
|
||||
1. Run `git clone git@github.com:ReVanced/revanced-api.git` to clone the repository
|
||||
2. Copy [.env.example](.env.example) to `.env` and fill in the required values
|
||||
3. Copy [configuration.example.toml](configuration.example.toml) to `configuration.toml` and fill in the required values
|
||||
4. Run `gradlew run --args=start` to start the server
|
||||
4. Copy [about.example.json](about.example.json) to `about.json` and fill in the required values
|
||||
5. Run `gradlew run --args=start` to start the server
|
||||
|
||||
## 📚 Everything else
|
||||
|
||||
|
||||
@@ -48,6 +48,12 @@ kotlin {
|
||||
}
|
||||
}
|
||||
|
||||
tasks {
|
||||
test {
|
||||
useJUnitPlatform()
|
||||
}
|
||||
}
|
||||
|
||||
repositories {
|
||||
mavenCentral()
|
||||
google()
|
||||
@@ -98,6 +104,8 @@ dependencies {
|
||||
implementation(libs.caffeine)
|
||||
implementation(libs.bouncy.castle.provider)
|
||||
implementation(libs.bouncy.castle.pgp)
|
||||
|
||||
testImplementation(kotlin("test"))
|
||||
}
|
||||
|
||||
// The maven-publish plugin is necessary to make signing work.
|
||||
|
||||
@@ -13,5 +13,5 @@ services:
|
||||
environment:
|
||||
- COMMAND=start
|
||||
ports:
|
||||
- 8888:8888
|
||||
- "8888:8888"
|
||||
restart: unless-stopped
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
org.gradle.parallel = true
|
||||
org.gradle.caching = true
|
||||
kotlin.code.style = official
|
||||
version = 1.1.0-dev.1
|
||||
version = 1.4.0-dev.1
|
||||
|
||||
@@ -10,8 +10,8 @@ ktor = "2.3.7"
|
||||
ktoml = "0.5.2"
|
||||
picocli = "4.7.6"
|
||||
datetime = "0.6.0"
|
||||
revanced-patcher = "19.3.1"
|
||||
revanced-library = "2.3.0"
|
||||
revanced-patcher = "20.0.0"
|
||||
revanced-library = "3.0.1-dev.1"
|
||||
caffeine = "3.1.8"
|
||||
bouncy-castle = "1.78.1"
|
||||
|
||||
|
||||
BIN
gradle/wrapper/gradle-wrapper.jar
vendored
BIN
gradle/wrapper/gradle-wrapper.jar
vendored
Binary file not shown.
4
gradle/wrapper/gradle-wrapper.properties
vendored
4
gradle/wrapper/gradle-wrapper.properties
vendored
@@ -1,7 +1,7 @@
|
||||
distributionBase=GRADLE_USER_HOME
|
||||
distributionPath=wrapper/dists
|
||||
distributionSha256Sum=a4b4158601f8636cdeeab09bd76afb640030bb5b144aafe261a5e8af027dc612
|
||||
distributionUrl=https\://services.gradle.org/distributions/gradle-8.8-bin.zip
|
||||
distributionSha256Sum=d725d707bfabd4dfdc958c624003b3c80accc03f7037b5122c4b1d0ef15cecab
|
||||
distributionUrl=https\://services.gradle.org/distributions/gradle-8.9-bin.zip
|
||||
networkTimeout=10000
|
||||
validateDistributionUrl=true
|
||||
zipStoreBase=GRADLE_USER_HOME
|
||||
|
||||
5
gradlew
vendored
5
gradlew
vendored
@@ -15,6 +15,8 @@
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
#
|
||||
# SPDX-License-Identifier: Apache-2.0
|
||||
#
|
||||
|
||||
##############################################################################
|
||||
#
|
||||
@@ -84,7 +86,8 @@ done
|
||||
# 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 "${APP_HOME:-./}" > /dev/null && pwd -P ) || exit
|
||||
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
|
||||
|
||||
2
gradlew.bat
vendored
2
gradlew.bat
vendored
@@ -13,6 +13,8 @@
|
||||
@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 ##########################################################################
|
||||
|
||||
3648
package-lock.json
generated
3648
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@@ -4,7 +4,7 @@
|
||||
"@saithodev/semantic-release-backmerge": "^4.0.1",
|
||||
"@semantic-release/changelog": "^6.0.3",
|
||||
"@semantic-release/git": "^10.0.1",
|
||||
"gradle-semantic-release-plugin": "^1.9.2",
|
||||
"semantic-release": "^24.0.0"
|
||||
"gradle-semantic-release-plugin": "^1.10.1",
|
||||
"semantic-release": "^24.1.2"
|
||||
}
|
||||
}
|
||||
@@ -1,6 +1,7 @@
|
||||
package app.revanced.api.command
|
||||
|
||||
import app.revanced.api.configuration.*
|
||||
import io.github.cdimascio.dotenv.Dotenv
|
||||
import io.ktor.server.engine.*
|
||||
import io.ktor.server.jetty.*
|
||||
import picocli.CommandLine
|
||||
@@ -33,6 +34,8 @@ internal object StartAPICommand : Runnable {
|
||||
private var configFile = File("configuration.toml")
|
||||
|
||||
override fun run() {
|
||||
Dotenv.configure().systemProperties().load()
|
||||
|
||||
embeddedServer(Jetty, port, host) {
|
||||
configureDependencies(configFile)
|
||||
configureHTTP()
|
||||
|
||||
@@ -7,12 +7,11 @@ import app.revanced.api.configuration.repository.GitHubBackendRepository
|
||||
import app.revanced.api.configuration.services.*
|
||||
import app.revanced.api.configuration.services.AnnouncementService
|
||||
import app.revanced.api.configuration.services.ApiService
|
||||
import app.revanced.api.configuration.services.AuthService
|
||||
import app.revanced.api.configuration.services.AuthenticationService
|
||||
import app.revanced.api.configuration.services.OldApiService
|
||||
import app.revanced.api.configuration.services.PatchesService
|
||||
import com.akuleshov7.ktoml.Toml
|
||||
import com.akuleshov7.ktoml.source.decodeFromStream
|
||||
import io.github.cdimascio.dotenv.Dotenv
|
||||
import io.ktor.client.*
|
||||
import io.ktor.client.engine.okhttp.*
|
||||
import io.ktor.client.plugins.*
|
||||
@@ -38,11 +37,7 @@ import java.io.File
|
||||
fun Application.configureDependencies(
|
||||
configFile: File,
|
||||
) {
|
||||
val globalModule = module {
|
||||
single {
|
||||
Dotenv.configure().load()
|
||||
}
|
||||
|
||||
val miscModule = module {
|
||||
factory { params ->
|
||||
val defaultRequestUri: String = params.get<String>()
|
||||
val configBlock = params.getOrNull<(HttpClientConfig<OkHttpConfig>.() -> Unit)>() ?: {}
|
||||
@@ -72,7 +67,7 @@ fun Application.configureDependencies(
|
||||
)
|
||||
}
|
||||
|
||||
get<Dotenv>()["BACKEND_API_TOKEN"]?.let {
|
||||
System.getProperty("BACKEND_API_TOKEN")?.let {
|
||||
install(Auth) {
|
||||
bearer {
|
||||
loadTokens {
|
||||
@@ -98,12 +93,10 @@ fun Application.configureDependencies(
|
||||
}
|
||||
|
||||
single {
|
||||
val dotenv = get<Dotenv>()
|
||||
|
||||
TransactionManager.defaultDatabase = Database.connect(
|
||||
url = dotenv["DB_URL"],
|
||||
user = dotenv["DB_USER"],
|
||||
password = dotenv["DB_PASSWORD"],
|
||||
url = System.getProperty("DB_URL"),
|
||||
user = System.getProperty("DB_USER"),
|
||||
password = System.getProperty("DB_PASSWORD"),
|
||||
)
|
||||
|
||||
AnnouncementRepository()
|
||||
@@ -112,15 +105,13 @@ fun Application.configureDependencies(
|
||||
|
||||
val serviceModule = module {
|
||||
single {
|
||||
val dotenv = get<Dotenv>()
|
||||
val jwtSecret = System.getProperty("JWT_SECRET")
|
||||
val issuer = System.getProperty("JWT_ISSUER")
|
||||
val validityInMin = System.getProperty("JWT_VALIDITY_IN_MIN").toLong()
|
||||
|
||||
val jwtSecret = dotenv["JWT_SECRET"]
|
||||
val issuer = dotenv["JWT_ISSUER"]
|
||||
val validityInMin = dotenv["JWT_VALIDITY_IN_MIN"].toInt()
|
||||
val authSHA256DigestString = System.getProperty("AUTH_SHA256_DIGEST")
|
||||
|
||||
val authSHA256DigestString = dotenv["AUTH_SHA256_DIGEST"]
|
||||
|
||||
AuthService(issuer, validityInMin, jwtSecret, authSHA256DigestString)
|
||||
AuthenticationService(issuer, validityInMin, jwtSecret, authSHA256DigestString)
|
||||
}
|
||||
single {
|
||||
val configuration = get<ConfigurationRepository>()
|
||||
@@ -140,7 +131,7 @@ fun Application.configureDependencies(
|
||||
|
||||
install(Koin) {
|
||||
modules(
|
||||
globalModule,
|
||||
miscModule,
|
||||
repositoryModule,
|
||||
serviceModule,
|
||||
)
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
package app.revanced.api.configuration
|
||||
|
||||
import io.bkbn.kompendium.core.metadata.MethodInfo
|
||||
import io.bkbn.kompendium.core.plugin.NotarizedRoute
|
||||
import io.ktor.http.*
|
||||
import io.ktor.http.content.*
|
||||
@@ -40,3 +41,11 @@ internal fun Route.staticFiles(
|
||||
extensions("json")
|
||||
},
|
||||
) = staticFiles(remotePath, dir.toFile(), null, block)
|
||||
|
||||
internal fun MethodInfo.Builder<*>.canRespondUnauthorized() {
|
||||
canRespond {
|
||||
responseCode(HttpStatusCode.Unauthorized)
|
||||
description("Unauthorized")
|
||||
responseType<Unit>()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
package app.revanced.api.configuration
|
||||
|
||||
import app.revanced.api.configuration.repository.ConfigurationRepository
|
||||
import io.ktor.http.*
|
||||
import io.ktor.server.application.*
|
||||
import io.ktor.server.plugins.*
|
||||
import io.ktor.server.plugins.cors.routing.*
|
||||
@@ -13,11 +14,16 @@ fun Application.configureHTTP() {
|
||||
val configurationRepository = get<ConfigurationRepository>()
|
||||
|
||||
install(CORS) {
|
||||
HttpMethod.DefaultMethods.minus(HttpMethod.Options).forEach(::allowMethod)
|
||||
|
||||
allowHeader(HttpHeaders.ContentType)
|
||||
allowHeader(HttpHeaders.Authorization)
|
||||
exposeHeader(HttpHeaders.WWWAuthenticate)
|
||||
|
||||
allowCredentials = true
|
||||
|
||||
configurationRepository.corsAllowedHosts.forEach { host ->
|
||||
allowHost(
|
||||
host = host,
|
||||
schemes = listOf("http", "https")
|
||||
)
|
||||
allowHost(host = host, schemes = listOf("https"))
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,9 +1,17 @@
|
||||
package app.revanced.api.configuration
|
||||
|
||||
import app.revanced.api.configuration.services.AuthService
|
||||
import app.revanced.api.configuration.services.AuthenticationService
|
||||
import io.ktor.server.application.*
|
||||
import io.ktor.server.auth.*
|
||||
import org.koin.ktor.ext.get
|
||||
|
||||
fun Application.configureSecurity() {
|
||||
get<AuthService>().configureSecurity(this)
|
||||
val authenticationService = get<AuthenticationService>()
|
||||
|
||||
install(Authentication) {
|
||||
with(authenticationService) {
|
||||
jwt()
|
||||
digest()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
package app.revanced.api.configuration.repository
|
||||
|
||||
import app.revanced.api.configuration.schema.APIAnnouncement
|
||||
import app.revanced.api.configuration.schema.APIResponseAnnouncement
|
||||
import app.revanced.api.configuration.schema.APIResponseAnnouncementId
|
||||
import app.revanced.api.configuration.schema.ApiAnnouncement
|
||||
import app.revanced.api.configuration.schema.ApiAnnouncementTag
|
||||
import app.revanced.api.configuration.schema.ApiResponseAnnouncement
|
||||
import app.revanced.api.configuration.schema.ApiResponseAnnouncementId
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.awaitAll
|
||||
import kotlinx.coroutines.runBlocking
|
||||
import kotlinx.datetime.*
|
||||
import org.jetbrains.exposed.dao.IntEntity
|
||||
@@ -15,126 +15,175 @@ import org.jetbrains.exposed.sql.*
|
||||
import org.jetbrains.exposed.sql.kotlin.datetime.CurrentDateTime
|
||||
import org.jetbrains.exposed.sql.kotlin.datetime.datetime
|
||||
import org.jetbrains.exposed.sql.transactions.experimental.newSuspendedTransaction
|
||||
import org.jetbrains.exposed.sql.transactions.experimental.suspendedTransactionAsync
|
||||
import java.time.LocalDateTime
|
||||
|
||||
internal class AnnouncementRepository {
|
||||
// This is better than doing a maxByOrNull { it.id }.
|
||||
// This is better than doing a maxByOrNull { it.id } on every request.
|
||||
private var latestAnnouncement: Announcement? = null
|
||||
private val latestAnnouncementByChannel = mutableMapOf<String, Announcement>()
|
||||
|
||||
private fun updateLatestAnnouncement(new: Announcement) {
|
||||
if (latestAnnouncement?.id?.value == new.id.value) {
|
||||
latestAnnouncement = new
|
||||
latestAnnouncementByChannel[new.channel ?: return] = new
|
||||
}
|
||||
}
|
||||
private val latestAnnouncementByTag = mutableMapOf<Int, Announcement>()
|
||||
|
||||
init {
|
||||
runBlocking {
|
||||
transaction {
|
||||
SchemaUtils.create(Announcements, Attachments)
|
||||
SchemaUtils.create(
|
||||
Announcements,
|
||||
Attachments,
|
||||
Tags,
|
||||
AnnouncementTags,
|
||||
)
|
||||
|
||||
// Initialize the latest announcement.
|
||||
latestAnnouncement = Announcement.all().onEach {
|
||||
latestAnnouncementByChannel[it.channel ?: return@onEach] = it
|
||||
}.maxByOrNull { it.id } ?: return@transaction
|
||||
initializeLatestAnnouncements()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
suspend fun all() = transaction {
|
||||
Announcement.all().map { it.toApi() }
|
||||
private fun initializeLatestAnnouncements() {
|
||||
latestAnnouncement = Announcement.all().orderBy(Announcements.id to SortOrder.DESC).firstOrNull()
|
||||
|
||||
Tag.all().map { it.id.value }.forEach(::updateLatestAnnouncementForTag)
|
||||
}
|
||||
|
||||
suspend fun all(channel: String) = transaction {
|
||||
Announcement.find { Announcements.channel eq channel }.map { it.toApi() }
|
||||
private fun updateLatestAnnouncement(new: Announcement) {
|
||||
if (latestAnnouncement == null || latestAnnouncement!!.id.value <= new.id.value) {
|
||||
latestAnnouncement = new
|
||||
new.tags.forEach { tag -> latestAnnouncementByTag[tag.id.value] = new }
|
||||
}
|
||||
}
|
||||
|
||||
private fun updateLatestAnnouncementForTag(tag: Int) {
|
||||
val latestAnnouncementForTag = AnnouncementTags.select(AnnouncementTags.announcement)
|
||||
.where { AnnouncementTags.tag eq tag }
|
||||
.map { it[AnnouncementTags.announcement] }
|
||||
.mapNotNull { Announcement.findById(it) }
|
||||
.maxByOrNull { it.id }
|
||||
|
||||
latestAnnouncementForTag?.let { latestAnnouncementByTag[tag] = it }
|
||||
}
|
||||
|
||||
suspend fun latest() = transaction {
|
||||
latestAnnouncement.toApiResponseAnnouncement()
|
||||
}
|
||||
|
||||
suspend fun latest(tags: Set<Int>) = transaction {
|
||||
tags.mapNotNull { tag -> latestAnnouncementByTag[tag] }.toApiAnnouncement()
|
||||
}
|
||||
|
||||
fun latestId() = latestAnnouncement?.id?.value.toApiResponseAnnouncementId()
|
||||
|
||||
fun latestId(tags: Set<Int>) =
|
||||
tags.map { tag -> latestAnnouncementByTag[tag]?.id?.value }.toApiResponseAnnouncementId()
|
||||
|
||||
suspend fun paged(cursor: Int, count: Int, tags: Set<Int>?, archived: Boolean) = transaction {
|
||||
Announcement.find {
|
||||
fun idLessEq() = Announcements.id lessEq cursor
|
||||
fun archivedAtIsNull() = Announcements.archivedAt.isNull()
|
||||
fun archivedAtGreaterNow() = Announcements.archivedAt greater LocalDateTime.now().toKotlinLocalDateTime()
|
||||
|
||||
if (tags == null) {
|
||||
if (archived) {
|
||||
idLessEq()
|
||||
} else {
|
||||
idLessEq() and (archivedAtIsNull() or archivedAtGreaterNow())
|
||||
}
|
||||
} else {
|
||||
fun archivedAtGreaterOrNullOrTrue() = if (archived) {
|
||||
Op.TRUE
|
||||
} else {
|
||||
archivedAtIsNull() or archivedAtGreaterNow()
|
||||
}
|
||||
|
||||
fun hasTags() = tags.mapNotNull { Tag.findById(it)?.id }.let { tags ->
|
||||
Announcements.id inSubQuery Announcements.leftJoin(AnnouncementTags)
|
||||
.select(AnnouncementTags.announcement)
|
||||
.where { AnnouncementTags.tag inList tags }
|
||||
.withDistinct()
|
||||
}
|
||||
|
||||
idLessEq() and archivedAtGreaterOrNullOrTrue() and hasTags()
|
||||
}
|
||||
}.orderBy(Announcements.id to SortOrder.DESC).limit(count).toApiAnnouncement()
|
||||
}
|
||||
|
||||
suspend fun get(id: Int) = transaction {
|
||||
Announcement.findById(id).toApiResponseAnnouncement()
|
||||
}
|
||||
|
||||
suspend fun new(new: ApiAnnouncement) = transaction {
|
||||
Announcement.new {
|
||||
author = new.author
|
||||
title = new.title
|
||||
content = new.content
|
||||
archivedAt = new.archivedAt
|
||||
level = new.level
|
||||
tags = SizedCollection(
|
||||
new.tags.map { tag -> Tag.find { Tags.name eq tag }.firstOrNull() ?: Tag.new { name = tag } },
|
||||
)
|
||||
}.apply {
|
||||
new.attachments.map { attachmentUrl ->
|
||||
Attachment.new {
|
||||
url = attachmentUrl
|
||||
announcement = this@apply
|
||||
}
|
||||
}
|
||||
}.let(::updateLatestAnnouncement)
|
||||
}
|
||||
|
||||
suspend fun update(id: Int, new: ApiAnnouncement) = transaction {
|
||||
Announcement.findByIdAndUpdate(id) {
|
||||
it.author = new.author
|
||||
it.title = new.title
|
||||
it.content = new.content
|
||||
it.archivedAt = new.archivedAt
|
||||
it.level = new.level
|
||||
|
||||
// Get the old tags, create new tags if they don't exist,
|
||||
// and delete tags that are not in the new tags, after updating the announcement.
|
||||
val oldTags = it.tags.toList()
|
||||
val updatedTags = new.tags.map { name ->
|
||||
Tag.find { Tags.name eq name }.firstOrNull() ?: Tag.new { this.name = name }
|
||||
}
|
||||
it.tags = SizedCollection(updatedTags)
|
||||
oldTags.forEach { tag ->
|
||||
if (tag in updatedTags || !tag.announcements.empty()) return@forEach
|
||||
tag.delete()
|
||||
}
|
||||
|
||||
// Delete old attachments and create new attachments.
|
||||
it.attachments.forEach { attachment -> attachment.delete() }
|
||||
new.attachments.map { attachment ->
|
||||
Attachment.new {
|
||||
url = attachment
|
||||
announcement = it
|
||||
}
|
||||
}
|
||||
}?.let(::updateLatestAnnouncement) ?: Unit
|
||||
}
|
||||
|
||||
suspend fun delete(id: Int) = transaction {
|
||||
val announcement = Announcement.findById(id) ?: return@transaction
|
||||
|
||||
// Delete the tag if no other announcements are referencing it.
|
||||
// One count means that the announcement is the only one referencing the tag.
|
||||
announcement.tags.filter { tag -> tag.announcements.count() == 1L }.forEach { tag ->
|
||||
latestAnnouncementByTag -= tag.id.value
|
||||
tag.delete()
|
||||
}
|
||||
|
||||
announcement.delete()
|
||||
|
||||
// In case the latest announcement was deleted, query the new latest announcement again.
|
||||
// If the deleted announcement is the latest announcement, set the new latest announcement.
|
||||
if (latestAnnouncement?.id?.value == id) {
|
||||
latestAnnouncement = Announcement.all().maxByOrNull { it.id }
|
||||
latestAnnouncement = Announcement.all().orderBy(Announcements.id to SortOrder.DESC).firstOrNull()
|
||||
}
|
||||
|
||||
// If no latest announcement was found, remove it from the channel map.
|
||||
if (latestAnnouncement == null) {
|
||||
latestAnnouncementByChannel.remove(announcement.channel)
|
||||
} else {
|
||||
latestAnnouncementByChannel[latestAnnouncement!!.channel ?: return@transaction] = latestAnnouncement!!
|
||||
}
|
||||
// The new announcement may be the latest for a specific tag. Set the new latest announcement for that tag.
|
||||
latestAnnouncementByTag.keys.forEach { tag ->
|
||||
updateLatestAnnouncementForTag(tag)
|
||||
}
|
||||
}
|
||||
|
||||
fun latest() = latestAnnouncement?.toApi()
|
||||
|
||||
fun latest(channel: String) = latestAnnouncementByChannel[channel]?.toApi()
|
||||
|
||||
fun latestId() = latest()?.id?.toApi()
|
||||
|
||||
fun latestId(channel: String) = latest(channel)?.id?.toApi()
|
||||
|
||||
suspend fun archive(
|
||||
id: Int,
|
||||
archivedAt: LocalDateTime?,
|
||||
) = transaction {
|
||||
Announcement.findByIdAndUpdate(id) {
|
||||
it.archivedAt = archivedAt ?: java.time.LocalDateTime.now().toKotlinLocalDateTime()
|
||||
}?.also(::updateLatestAnnouncement)
|
||||
}
|
||||
|
||||
suspend fun unarchive(id: Int) = transaction {
|
||||
Announcement.findByIdAndUpdate(id) {
|
||||
it.archivedAt = null
|
||||
}?.also(::updateLatestAnnouncement)
|
||||
}
|
||||
|
||||
suspend fun new(new: APIAnnouncement) = transaction {
|
||||
Announcement.new {
|
||||
author = new.author
|
||||
title = new.title
|
||||
content = new.content
|
||||
channel = new.channel
|
||||
archivedAt = new.archivedAt
|
||||
level = new.level
|
||||
}.also { newAnnouncement ->
|
||||
new.attachmentUrls.map { newUrl ->
|
||||
suspendedTransactionAsync {
|
||||
Attachment.new {
|
||||
url = newUrl
|
||||
announcement = newAnnouncement
|
||||
}
|
||||
}
|
||||
}.awaitAll()
|
||||
}.also(::updateLatestAnnouncement)
|
||||
}
|
||||
|
||||
suspend fun update(id: Int, new: APIAnnouncement) = transaction {
|
||||
Announcement.findByIdAndUpdate(id) {
|
||||
it.author = new.author
|
||||
it.title = new.title
|
||||
it.content = new.content
|
||||
it.channel = new.channel
|
||||
it.archivedAt = new.archivedAt
|
||||
it.level = new.level
|
||||
}?.also { newAnnouncement ->
|
||||
newAnnouncement.attachments.map {
|
||||
suspendedTransactionAsync {
|
||||
it.delete()
|
||||
}
|
||||
}.awaitAll()
|
||||
|
||||
new.attachmentUrls.map { newUrl ->
|
||||
suspendedTransactionAsync {
|
||||
Attachment.new {
|
||||
url = newUrl
|
||||
announcement = newAnnouncement
|
||||
}
|
||||
}
|
||||
}.awaitAll()
|
||||
}?.also(::updateLatestAnnouncement)
|
||||
suspend fun tags() = transaction {
|
||||
Tag.all().toList().toApiTag()
|
||||
}
|
||||
|
||||
private suspend fun <T> transaction(statement: suspend Transaction.() -> T) =
|
||||
@@ -144,7 +193,6 @@ internal class AnnouncementRepository {
|
||||
val author = varchar("author", 32).nullable()
|
||||
val title = varchar("title", 64)
|
||||
val content = text("content").nullable()
|
||||
val channel = varchar("channel", 16).nullable()
|
||||
val createdAt = datetime("createdAt").defaultExpression(CurrentDateTime)
|
||||
val archivedAt = datetime("archivedAt").nullable()
|
||||
val level = integer("level")
|
||||
@@ -155,6 +203,19 @@ internal class AnnouncementRepository {
|
||||
val announcement = reference("announcement", Announcements, onDelete = ReferenceOption.CASCADE)
|
||||
}
|
||||
|
||||
private object Tags : IntIdTable() {
|
||||
val name = varchar("name", 16).uniqueIndex()
|
||||
}
|
||||
|
||||
private object AnnouncementTags : Table() {
|
||||
val tag = reference("tag", Tags, onDelete = ReferenceOption.CASCADE)
|
||||
val announcement = reference("announcement", Announcements, onDelete = ReferenceOption.CASCADE)
|
||||
|
||||
init {
|
||||
uniqueIndex(tag, announcement)
|
||||
}
|
||||
}
|
||||
|
||||
class Announcement(id: EntityID<Int>) : IntEntity(id) {
|
||||
companion object : IntEntityClass<Announcement>(Announcements)
|
||||
|
||||
@@ -162,7 +223,7 @@ internal class AnnouncementRepository {
|
||||
var title by Announcements.title
|
||||
var content by Announcements.content
|
||||
val attachments by Attachment referrersOn Attachments.announcement
|
||||
var channel by Announcements.channel
|
||||
var tags by Tag via AnnouncementTags
|
||||
var createdAt by Announcements.createdAt
|
||||
var archivedAt by Announcements.archivedAt
|
||||
var level by Announcements.level
|
||||
@@ -175,17 +236,32 @@ internal class AnnouncementRepository {
|
||||
var announcement by Announcement referencedOn Attachments.announcement
|
||||
}
|
||||
|
||||
private fun Announcement.toApi() = APIResponseAnnouncement(
|
||||
id.value,
|
||||
author,
|
||||
title,
|
||||
content,
|
||||
attachments.map { it.url },
|
||||
channel,
|
||||
createdAt,
|
||||
archivedAt,
|
||||
level,
|
||||
)
|
||||
class Tag(id: EntityID<Int>) : IntEntity(id) {
|
||||
companion object : IntEntityClass<Tag>(Tags)
|
||||
|
||||
private fun Int.toApi() = APIResponseAnnouncementId(this)
|
||||
var name by Tags.name
|
||||
var announcements by Announcement via AnnouncementTags
|
||||
}
|
||||
|
||||
private fun Announcement?.toApiResponseAnnouncement() = this?.let {
|
||||
ApiResponseAnnouncement(
|
||||
id.value,
|
||||
author,
|
||||
title,
|
||||
content,
|
||||
attachments.map { it.url },
|
||||
tags.map { it.id.value },
|
||||
createdAt,
|
||||
archivedAt,
|
||||
level,
|
||||
)
|
||||
}
|
||||
|
||||
private fun Iterable<Announcement>.toApiAnnouncement() = map { it.toApiResponseAnnouncement()!! }
|
||||
|
||||
private fun Iterable<Tag>.toApiTag() = map { ApiAnnouncementTag(it.id.value, it.name) }
|
||||
|
||||
private fun Int?.toApiResponseAnnouncementId() = this?.let { ApiResponseAnnouncementId(this) }
|
||||
|
||||
private fun Iterable<Int?>.toApiResponseAnnouncementId() = map { it.toApiResponseAnnouncementId() }
|
||||
}
|
||||
|
||||
@@ -17,6 +17,7 @@ import kotlinx.serialization.json.JsonNamingStrategy
|
||||
import kotlinx.serialization.json.decodeFromStream
|
||||
import java.io.File
|
||||
import java.nio.file.Path
|
||||
import kotlin.io.path.createDirectories
|
||||
|
||||
/**
|
||||
* The repository storing the configuration for the API.
|
||||
@@ -60,6 +61,11 @@ internal class ConfigurationRepository(
|
||||
@SerialName("about-json-file-path")
|
||||
val about: APIAbout,
|
||||
) {
|
||||
init {
|
||||
staticFilesPath.createDirectories()
|
||||
versionedStaticFilesPath.createDirectories()
|
||||
}
|
||||
|
||||
/**
|
||||
* Am asset configuration whose asset is signed.
|
||||
*
|
||||
|
||||
@@ -98,7 +98,7 @@ class GitHubBackendRepository(client: HttpClient) : BackendRepository(client) {
|
||||
gpgKeys =
|
||||
BackendMember.GpgKeys(
|
||||
ids = gpgKeys.map { it.keyId },
|
||||
url = "https://api.github.com/users/${user.login}.gpg",
|
||||
url = "https://github.com/${user.login}.gpg",
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
@@ -1,24 +1,20 @@
|
||||
package app.revanced.api.configuration.routes
|
||||
|
||||
import app.revanced.api.configuration.canRespondUnauthorized
|
||||
import app.revanced.api.configuration.installCache
|
||||
import app.revanced.api.configuration.installNotarizedRoute
|
||||
import app.revanced.api.configuration.respondOrNotFound
|
||||
import app.revanced.api.configuration.schema.APIAnnouncement
|
||||
import app.revanced.api.configuration.schema.APIAnnouncementArchivedAt
|
||||
import app.revanced.api.configuration.schema.APIResponseAnnouncement
|
||||
import app.revanced.api.configuration.schema.APIResponseAnnouncementId
|
||||
import app.revanced.api.configuration.schema.ApiAnnouncement
|
||||
import app.revanced.api.configuration.schema.ApiResponseAnnouncement
|
||||
import app.revanced.api.configuration.schema.ApiResponseAnnouncementId
|
||||
import app.revanced.api.configuration.services.AnnouncementService
|
||||
import io.bkbn.kompendium.core.metadata.DeleteInfo
|
||||
import io.bkbn.kompendium.core.metadata.GetInfo
|
||||
import io.bkbn.kompendium.core.metadata.PatchInfo
|
||||
import io.bkbn.kompendium.core.metadata.PostInfo
|
||||
import io.bkbn.kompendium.core.metadata.*
|
||||
import io.bkbn.kompendium.json.schema.definition.TypeDefinition
|
||||
import io.bkbn.kompendium.oas.payload.Parameter
|
||||
import io.ktor.http.*
|
||||
import io.ktor.server.application.*
|
||||
import io.ktor.server.auth.*
|
||||
import io.ktor.server.plugins.ratelimit.*
|
||||
import io.ktor.server.request.*
|
||||
import io.ktor.server.response.*
|
||||
import io.ktor.server.routing.*
|
||||
import io.ktor.server.util.*
|
||||
@@ -34,139 +30,185 @@ internal fun Route.announcementsRoute() = route("announcements") {
|
||||
|
||||
rateLimit(RateLimitName("strong")) {
|
||||
get {
|
||||
call.respond(announcementService.all())
|
||||
val cursor = call.parameters["cursor"]?.toInt() ?: Int.MAX_VALUE
|
||||
val count = call.parameters["count"]?.toInt() ?: 16
|
||||
val tags = call.parameters.getAll("tag")
|
||||
val archived = call.parameters["archived"]?.toBoolean() ?: true
|
||||
|
||||
call.respond(announcementService.paged(cursor, count, tags?.map { it.toInt() }?.toSet(), archived))
|
||||
}
|
||||
}
|
||||
|
||||
rateLimit(RateLimitName("strong")) {
|
||||
route("{channel}/latest") {
|
||||
installLatestChannelAnnouncementRouteDocumentation()
|
||||
|
||||
get {
|
||||
val channel: String by call.parameters
|
||||
|
||||
call.respondOrNotFound(announcementService.latest(channel))
|
||||
}
|
||||
|
||||
route("id") {
|
||||
installLatestChannelAnnouncementIdRouteDocumentation()
|
||||
|
||||
get {
|
||||
val channel: String by call.parameters
|
||||
|
||||
call.respondOrNotFound(announcementService.latestId(channel))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
rateLimit(RateLimitName("strong")) {
|
||||
route("{channel}") {
|
||||
installChannelAnnouncementsRouteDocumentation()
|
||||
|
||||
get {
|
||||
val channel: String by call.parameters
|
||||
|
||||
call.respond(announcementService.all(channel))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
rateLimit(RateLimitName("strong")) {
|
||||
route("latest") {
|
||||
installLatestAnnouncementRouteDocumentation()
|
||||
|
||||
get {
|
||||
call.respondOrNotFound(announcementService.latest())
|
||||
}
|
||||
|
||||
route("id") {
|
||||
installLatestAnnouncementIdRouteDocumentation()
|
||||
|
||||
get {
|
||||
call.respondOrNotFound(announcementService.latestId())
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
rateLimit(RateLimitName("strong")) {
|
||||
rateLimit(RateLimitName("weak")) {
|
||||
authenticate("jwt") {
|
||||
installAnnouncementRouteDocumentation()
|
||||
|
||||
post<APIAnnouncement> { announcement ->
|
||||
post<ApiAnnouncement> { announcement ->
|
||||
announcementService.new(announcement)
|
||||
|
||||
call.respond(HttpStatusCode.OK)
|
||||
}
|
||||
}
|
||||
|
||||
route("latest") {
|
||||
installAnnouncementsLatestRouteDocumentation()
|
||||
|
||||
get {
|
||||
val tags = call.parameters.getAll("tag")
|
||||
|
||||
if (tags?.isNotEmpty() == true) {
|
||||
call.respond(announcementService.latest(tags.map { it.toInt() }.toSet()))
|
||||
} else {
|
||||
call.respondOrNotFound(announcementService.latest())
|
||||
}
|
||||
}
|
||||
|
||||
route("{id}") {
|
||||
installAnnouncementIdRouteDocumentation()
|
||||
route("id") {
|
||||
installAnnouncementsLatestIdRouteDocumentation()
|
||||
|
||||
patch<APIAnnouncement> { announcement ->
|
||||
get {
|
||||
val tags = call.parameters.getAll("tag")
|
||||
|
||||
if (tags?.isNotEmpty() == true) {
|
||||
call.respond(announcementService.latestId(tags.map { it.toInt() }.toSet()))
|
||||
} else {
|
||||
call.respondOrNotFound(announcementService.latestId())
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
route("{id}") {
|
||||
installAnnouncementsIdRouteDocumentation()
|
||||
|
||||
get {
|
||||
val id: Int by call.parameters
|
||||
|
||||
call.respondOrNotFound(announcementService.get(id))
|
||||
}
|
||||
|
||||
authenticate("jwt") {
|
||||
patch<ApiAnnouncement> { announcement ->
|
||||
val id: Int by call.parameters
|
||||
|
||||
announcementService.update(id, announcement)
|
||||
|
||||
call.respond(HttpStatusCode.OK)
|
||||
}
|
||||
|
||||
delete {
|
||||
val id: Int by call.parameters
|
||||
|
||||
announcementService.delete(id)
|
||||
|
||||
call.respond(HttpStatusCode.OK)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
route("archive") {
|
||||
installAnnouncementArchiveRouteDocumentation()
|
||||
route("tags") {
|
||||
installAnnouncementsTagsRouteDocumentation()
|
||||
|
||||
post {
|
||||
val id: Int by call.parameters
|
||||
val archivedAt = call.receiveNullable<APIAnnouncementArchivedAt>()?.archivedAt
|
||||
|
||||
announcementService.archive(id, archivedAt)
|
||||
}
|
||||
}
|
||||
|
||||
route("unarchive") {
|
||||
installAnnouncementUnarchiveRouteDocumentation()
|
||||
|
||||
post {
|
||||
val id: Int by call.parameters
|
||||
|
||||
announcementService.unarchive(id)
|
||||
}
|
||||
}
|
||||
get {
|
||||
call.respond(announcementService.tags())
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun Route.installAnnouncementRouteDocumentation() = installNotarizedRoute {
|
||||
private val authHeaderParameter = Parameter(
|
||||
name = "Authorization",
|
||||
`in` = Parameter.Location.header,
|
||||
schema = TypeDefinition.STRING,
|
||||
required = true,
|
||||
examples = mapOf("Bearer authentication" to Parameter.Example("Bearer abc123")),
|
||||
)
|
||||
|
||||
private fun Route.installAnnouncementsRouteDocumentation() = installNotarizedRoute {
|
||||
tags = setOf("Announcements")
|
||||
|
||||
get = GetInfo.builder {
|
||||
description("Get a page of announcements")
|
||||
summary("Get announcements")
|
||||
parameters(
|
||||
Parameter(
|
||||
name = "cursor",
|
||||
`in` = Parameter.Location.query,
|
||||
schema = TypeDefinition.INT,
|
||||
description = "The offset of the announcements. Default is Int.MAX_VALUE (Newest first)",
|
||||
required = false,
|
||||
),
|
||||
Parameter(
|
||||
name = "count",
|
||||
`in` = Parameter.Location.query,
|
||||
schema = TypeDefinition.INT,
|
||||
description = "The count of the announcements. Default is 16",
|
||||
required = false,
|
||||
),
|
||||
Parameter(
|
||||
name = "tag",
|
||||
`in` = Parameter.Location.query,
|
||||
schema = TypeDefinition.INT,
|
||||
description = "The tag IDs to filter the announcements by. Default is all tags",
|
||||
required = false,
|
||||
),
|
||||
Parameter(
|
||||
name = "archived",
|
||||
`in` = Parameter.Location.query,
|
||||
schema = TypeDefinition.BOOLEAN,
|
||||
description = "Whether to include archived announcements. Default is true",
|
||||
required = false,
|
||||
),
|
||||
)
|
||||
response {
|
||||
responseCode(HttpStatusCode.OK)
|
||||
mediaTypes("application/json")
|
||||
description("The announcements")
|
||||
responseType<Set<ApiResponseAnnouncement>>()
|
||||
}
|
||||
}
|
||||
|
||||
post = PostInfo.builder {
|
||||
description("Create a new announcement")
|
||||
summary("Create announcement")
|
||||
parameters(authHeaderParameter)
|
||||
request {
|
||||
requestType<APIAnnouncement>()
|
||||
requestType<ApiAnnouncement>()
|
||||
description("The new announcement")
|
||||
}
|
||||
response {
|
||||
description("When the announcement was created")
|
||||
description("The announcement is created")
|
||||
responseCode(HttpStatusCode.OK)
|
||||
responseType<Unit>()
|
||||
}
|
||||
canRespondUnauthorized()
|
||||
}
|
||||
}
|
||||
|
||||
private fun Route.installLatestAnnouncementRouteDocumentation() = installNotarizedRoute {
|
||||
private fun Route.installAnnouncementsLatestRouteDocumentation() = installNotarizedRoute {
|
||||
tags = setOf("Announcements")
|
||||
|
||||
get = GetInfo.builder {
|
||||
description("Get the latest announcement")
|
||||
summary("Get latest announcement")
|
||||
parameters(
|
||||
Parameter(
|
||||
name = "tag",
|
||||
`in` = Parameter.Location.query,
|
||||
schema = TypeDefinition.INT,
|
||||
description = "The tag IDs to filter the latest announcements by",
|
||||
required = false,
|
||||
),
|
||||
)
|
||||
response {
|
||||
responseCode(HttpStatusCode.OK)
|
||||
mediaTypes("application/json")
|
||||
description("The latest announcement")
|
||||
responseType<APIResponseAnnouncement>()
|
||||
responseType<ApiResponseAnnouncement>()
|
||||
}
|
||||
canRespond {
|
||||
responseCode(HttpStatusCode.OK)
|
||||
mediaTypes("application/json")
|
||||
description("The latest announcements")
|
||||
responseType<Set<ApiResponseAnnouncement>>()
|
||||
}
|
||||
canRespond {
|
||||
responseCode(HttpStatusCode.NotFound)
|
||||
@@ -176,17 +218,32 @@ private fun Route.installLatestAnnouncementRouteDocumentation() = installNotariz
|
||||
}
|
||||
}
|
||||
|
||||
private fun Route.installLatestAnnouncementIdRouteDocumentation() = installNotarizedRoute {
|
||||
private fun Route.installAnnouncementsLatestIdRouteDocumentation() = installNotarizedRoute {
|
||||
tags = setOf("Announcements")
|
||||
|
||||
get = GetInfo.builder {
|
||||
description("Get the id of the latest announcement")
|
||||
summary("Get id of latest announcement")
|
||||
description("Get the ID of the latest announcement")
|
||||
summary("Get ID of latest announcement")
|
||||
parameters(
|
||||
Parameter(
|
||||
name = "tag",
|
||||
`in` = Parameter.Location.query,
|
||||
schema = TypeDefinition.INT,
|
||||
description = "The tag IDs to filter the latest announcements by",
|
||||
required = false,
|
||||
),
|
||||
)
|
||||
response {
|
||||
responseCode(HttpStatusCode.OK)
|
||||
mediaTypes("application/json")
|
||||
description("The id of the latest announcement")
|
||||
responseType<APIResponseAnnouncementId>()
|
||||
description("The ID of the latest announcement")
|
||||
responseType<ApiResponseAnnouncementId>()
|
||||
}
|
||||
canRespond {
|
||||
responseCode(HttpStatusCode.OK)
|
||||
mediaTypes("application/json")
|
||||
description("The IDs of the latest announcements")
|
||||
responseType<Set<ApiResponseAnnouncement>>()
|
||||
}
|
||||
canRespond {
|
||||
responseCode(HttpStatusCode.NotFound)
|
||||
@@ -196,195 +253,73 @@ private fun Route.installLatestAnnouncementIdRouteDocumentation() = installNotar
|
||||
}
|
||||
}
|
||||
|
||||
private fun Route.installChannelAnnouncementsRouteDocumentation() = installNotarizedRoute {
|
||||
private fun Route.installAnnouncementsIdRouteDocumentation() = installNotarizedRoute {
|
||||
tags = setOf("Announcements")
|
||||
|
||||
parameters = listOf(
|
||||
Parameter(
|
||||
name = "channel",
|
||||
name = "id",
|
||||
`in` = Parameter.Location.path,
|
||||
schema = TypeDefinition.STRING,
|
||||
description = "The channel to get the announcements from",
|
||||
schema = TypeDefinition.INT,
|
||||
description = "The ID of the announcement to update",
|
||||
required = true,
|
||||
),
|
||||
authHeaderParameter,
|
||||
)
|
||||
|
||||
get = GetInfo.builder {
|
||||
description("Get the announcements from a channel")
|
||||
summary("Get announcements from channel")
|
||||
description("Get an announcement")
|
||||
summary("Get announcement")
|
||||
response {
|
||||
description("The announcement")
|
||||
responseCode(HttpStatusCode.OK)
|
||||
mediaTypes("application/json")
|
||||
description("The announcements in the channel")
|
||||
responseType<Set<APIResponseAnnouncement>>()
|
||||
responseType<ApiResponseAnnouncement>()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun Route.installAnnouncementArchiveRouteDocumentation() = installNotarizedRoute {
|
||||
tags = setOf("Announcements")
|
||||
|
||||
parameters = listOf(
|
||||
Parameter(
|
||||
name = "id",
|
||||
`in` = Parameter.Location.path,
|
||||
schema = TypeDefinition.INT,
|
||||
description = "The id of the announcement to archive",
|
||||
required = true,
|
||||
),
|
||||
Parameter(
|
||||
name = "archivedAt",
|
||||
`in` = Parameter.Location.query,
|
||||
schema = TypeDefinition.STRING,
|
||||
description = "The date and time the announcement to be archived",
|
||||
required = false,
|
||||
),
|
||||
)
|
||||
|
||||
post = PostInfo.builder {
|
||||
description("Archive an announcement")
|
||||
summary("Archive announcement")
|
||||
response {
|
||||
description("When the announcement was archived")
|
||||
responseCode(HttpStatusCode.OK)
|
||||
canRespond {
|
||||
responseCode(HttpStatusCode.NotFound)
|
||||
description("The announcement does not exist")
|
||||
responseType<Unit>()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun Route.installAnnouncementUnarchiveRouteDocumentation() = installNotarizedRoute {
|
||||
tags = setOf("Announcements")
|
||||
|
||||
parameters = listOf(
|
||||
Parameter(
|
||||
name = "id",
|
||||
`in` = Parameter.Location.path,
|
||||
schema = TypeDefinition.INT,
|
||||
description = "The id of the announcement to unarchive",
|
||||
required = true,
|
||||
),
|
||||
)
|
||||
|
||||
post = PostInfo.builder {
|
||||
description("Unarchive an announcement")
|
||||
summary("Unarchive announcement")
|
||||
response {
|
||||
description("When announcement was unarchived")
|
||||
responseCode(HttpStatusCode.OK)
|
||||
responseType<Unit>()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun Route.installAnnouncementIdRouteDocumentation() = installNotarizedRoute {
|
||||
tags = setOf("Announcements")
|
||||
|
||||
parameters = listOf(
|
||||
Parameter(
|
||||
name = "id",
|
||||
`in` = Parameter.Location.path,
|
||||
schema = TypeDefinition.INT,
|
||||
description = "The id of the announcement to update",
|
||||
required = true,
|
||||
),
|
||||
)
|
||||
|
||||
patch = PatchInfo.builder {
|
||||
description("Update an announcement")
|
||||
summary("Update announcement")
|
||||
request {
|
||||
requestType<APIAnnouncement>()
|
||||
requestType<ApiAnnouncement>()
|
||||
description("The new announcement")
|
||||
}
|
||||
response {
|
||||
description("When announcement was updated")
|
||||
description("The announcement is updated")
|
||||
responseCode(HttpStatusCode.OK)
|
||||
responseType<Unit>()
|
||||
}
|
||||
canRespondUnauthorized()
|
||||
}
|
||||
|
||||
delete = DeleteInfo.builder {
|
||||
description("Delete an announcement")
|
||||
summary("Delete announcement")
|
||||
response {
|
||||
description("When the announcement was deleted")
|
||||
description("The announcement is deleted")
|
||||
responseCode(HttpStatusCode.OK)
|
||||
responseType<Unit>()
|
||||
}
|
||||
canRespondUnauthorized()
|
||||
}
|
||||
}
|
||||
|
||||
private fun Route.installAnnouncementsRouteDocumentation() = installNotarizedRoute {
|
||||
private fun Route.installAnnouncementsTagsRouteDocumentation() = installNotarizedRoute {
|
||||
tags = setOf("Announcements")
|
||||
|
||||
get = GetInfo.builder {
|
||||
description("Get the announcements")
|
||||
summary("Get announcements")
|
||||
description("Get all announcement tags")
|
||||
summary("Get announcement tags")
|
||||
response {
|
||||
responseCode(HttpStatusCode.OK)
|
||||
mediaTypes("application/json")
|
||||
description("The announcements")
|
||||
responseType<Set<APIResponseAnnouncement>>()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun Route.installLatestChannelAnnouncementRouteDocumentation() = installNotarizedRoute {
|
||||
tags = setOf("Announcements")
|
||||
|
||||
parameters = listOf(
|
||||
Parameter(
|
||||
name = "channel",
|
||||
`in` = Parameter.Location.path,
|
||||
schema = TypeDefinition.STRING,
|
||||
description = "The channel to get the latest announcement from",
|
||||
required = true,
|
||||
),
|
||||
)
|
||||
|
||||
get = GetInfo.builder {
|
||||
description("Get the latest announcement from a channel")
|
||||
summary("Get latest channel announcement")
|
||||
response {
|
||||
responseCode(HttpStatusCode.OK)
|
||||
mediaTypes("application/json")
|
||||
description("The latest announcement in the channel")
|
||||
responseType<APIResponseAnnouncement>()
|
||||
}
|
||||
canRespond {
|
||||
responseCode(HttpStatusCode.NotFound)
|
||||
description("The channel does not exist")
|
||||
responseType<Unit>()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun Route.installLatestChannelAnnouncementIdRouteDocumentation() = installNotarizedRoute {
|
||||
tags = setOf("Announcements")
|
||||
|
||||
parameters = listOf(
|
||||
Parameter(
|
||||
name = "channel",
|
||||
`in` = Parameter.Location.path,
|
||||
schema = TypeDefinition.STRING,
|
||||
description = "The channel to get the latest announcement id from",
|
||||
required = true,
|
||||
),
|
||||
)
|
||||
|
||||
get = GetInfo.builder {
|
||||
description("Get the id of the latest announcement from a channel")
|
||||
summary("Get id of latest announcement from channel")
|
||||
response {
|
||||
responseCode(HttpStatusCode.OK)
|
||||
mediaTypes("application/json")
|
||||
description("The id of the latest announcement from the channel")
|
||||
responseType<APIResponseAnnouncementId>()
|
||||
}
|
||||
canRespond {
|
||||
responseCode(HttpStatusCode.NotFound)
|
||||
description("The channel does not exist")
|
||||
responseType<Unit>()
|
||||
description("The announcement tags")
|
||||
responseType<Set<String>>()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,14 +4,14 @@ import app.revanced.api.configuration.*
|
||||
import app.revanced.api.configuration.installCache
|
||||
import app.revanced.api.configuration.installNoCache
|
||||
import app.revanced.api.configuration.installNotarizedRoute
|
||||
import app.revanced.api.configuration.repository.ConfigurationRepository
|
||||
import app.revanced.api.configuration.respondOrNotFound
|
||||
import app.revanced.api.configuration.schema.APIAbout
|
||||
import app.revanced.api.configuration.schema.APIContributable
|
||||
import app.revanced.api.configuration.schema.APIMember
|
||||
import app.revanced.api.configuration.schema.APIRateLimit
|
||||
import app.revanced.api.configuration.schema.*
|
||||
import app.revanced.api.configuration.services.ApiService
|
||||
import app.revanced.api.configuration.services.AuthService
|
||||
import app.revanced.api.configuration.services.AuthenticationService
|
||||
import io.bkbn.kompendium.core.metadata.*
|
||||
import io.bkbn.kompendium.json.schema.definition.TypeDefinition
|
||||
import io.bkbn.kompendium.oas.payload.Parameter
|
||||
import io.ktor.http.*
|
||||
import io.ktor.server.application.*
|
||||
import io.ktor.server.auth.*
|
||||
@@ -23,7 +23,7 @@ import org.koin.ktor.ext.get as koinGet
|
||||
|
||||
internal fun Route.apiRoute() {
|
||||
val apiService = koinGet<ApiService>()
|
||||
val authService = koinGet<AuthService>()
|
||||
val authenticationService = koinGet<AuthenticationService>()
|
||||
|
||||
rateLimit(RateLimitName("strong")) {
|
||||
authenticate("auth-digest") {
|
||||
@@ -31,7 +31,7 @@ internal fun Route.apiRoute() {
|
||||
installTokenRouteDocumentation()
|
||||
|
||||
get {
|
||||
call.respond(authService.newToken())
|
||||
call.respond(authenticationService.newToken())
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -72,7 +72,7 @@ internal fun Route.apiRoute() {
|
||||
|
||||
installPingRouteDocumentation()
|
||||
|
||||
head {
|
||||
handle {
|
||||
call.respond(HttpStatusCode.NoContent)
|
||||
}
|
||||
}
|
||||
@@ -115,7 +115,7 @@ private fun Route.installRateLimitRouteDocumentation() = installNotarizedRoute {
|
||||
description("The rate limit of the backend")
|
||||
mediaTypes("application/json")
|
||||
responseCode(HttpStatusCode.OK)
|
||||
responseType<APIRateLimit>()
|
||||
responseType<ApiRateLimit>()
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -144,7 +144,7 @@ private fun Route.installTeamRouteDocumentation() = installNotarizedRoute {
|
||||
description("The list of team members")
|
||||
mediaTypes("application/json")
|
||||
responseCode(HttpStatusCode.OK)
|
||||
responseType<Set<APIMember>>()
|
||||
responseType<Set<ApiMember>>()
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -165,16 +165,38 @@ private fun Route.installContributorsRouteDocumentation() = installNotarizedRout
|
||||
}
|
||||
|
||||
private fun Route.installTokenRouteDocumentation() = installNotarizedRoute {
|
||||
val configuration = koinGet<ConfigurationRepository>()
|
||||
|
||||
tags = setOf("API")
|
||||
|
||||
get = GetInfo.builder {
|
||||
description("Get a new authorization token")
|
||||
summary("Get authorization token")
|
||||
parameters(
|
||||
Parameter(
|
||||
name = "Authorization",
|
||||
`in` = Parameter.Location.header,
|
||||
schema = TypeDefinition.STRING,
|
||||
required = true,
|
||||
examples = mapOf(
|
||||
"Digest access authentication" to Parameter.Example(
|
||||
value = "Digest " +
|
||||
"username=\"ReVanced\", " +
|
||||
"realm=\"ReVanced\", " +
|
||||
"nonce=\"abc123\", " +
|
||||
"uri=\"/v${configuration.apiVersion}/token\", " +
|
||||
"algorithm=SHA-256, " +
|
||||
"response=\"yxz456\"",
|
||||
),
|
||||
), // Provide an example for the header
|
||||
),
|
||||
)
|
||||
response {
|
||||
description("The authorization token")
|
||||
mediaTypes("application/json")
|
||||
responseCode(HttpStatusCode.OK)
|
||||
responseType<String>()
|
||||
responseType<ApiToken>()
|
||||
}
|
||||
canRespondUnauthorized()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
package app.revanced.api.configuration.routes
|
||||
|
||||
import app.revanced.api.configuration.installNotarizedRoute
|
||||
import app.revanced.api.configuration.schema.APIManagerAsset
|
||||
import app.revanced.api.configuration.schema.APIRelease
|
||||
import app.revanced.api.configuration.schema.APIReleaseVersion
|
||||
import app.revanced.api.configuration.schema.ApiManagerAsset
|
||||
import app.revanced.api.configuration.schema.ApiRelease
|
||||
import app.revanced.api.configuration.schema.ApiReleaseVersion
|
||||
import app.revanced.api.configuration.services.ManagerService
|
||||
import io.bkbn.kompendium.core.metadata.GetInfo
|
||||
import io.ktor.http.*
|
||||
@@ -14,53 +14,62 @@ import io.ktor.server.routing.*
|
||||
import org.koin.ktor.ext.get as koinGet
|
||||
|
||||
internal fun Route.managerRoute() = route("manager") {
|
||||
configure()
|
||||
|
||||
// TODO: Remove this deprecated route eventually.
|
||||
route("latest") {
|
||||
configure(deprecated = true)
|
||||
}
|
||||
}
|
||||
|
||||
private fun Route.configure(deprecated: Boolean = false) {
|
||||
val managerService = koinGet<ManagerService>()
|
||||
|
||||
route("latest") {
|
||||
installLatestManagerRouteDocumentation()
|
||||
installManagerRouteDocumentation(deprecated)
|
||||
|
||||
rateLimit(RateLimitName("weak")) {
|
||||
get {
|
||||
call.respond(managerService.latestRelease())
|
||||
}
|
||||
|
||||
route("version") {
|
||||
installManagerVersionRouteDocumentation(deprecated)
|
||||
|
||||
rateLimit(RateLimitName("weak")) {
|
||||
get {
|
||||
call.respond(managerService.latestRelease())
|
||||
}
|
||||
|
||||
route("version") {
|
||||
installLatestManagerVersionRouteDocumentation()
|
||||
|
||||
get {
|
||||
call.respond(managerService.latestVersion())
|
||||
}
|
||||
call.respond(managerService.latestVersion())
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun Route.installLatestManagerRouteDocumentation() = installNotarizedRoute {
|
||||
private fun Route.installManagerRouteDocumentation(deprecated: Boolean) = installNotarizedRoute {
|
||||
tags = setOf("Manager")
|
||||
|
||||
get = GetInfo.builder {
|
||||
description("Get the latest manager release")
|
||||
summary("Get latest manager release")
|
||||
if (deprecated) isDeprecated()
|
||||
description("Get the current manager release")
|
||||
summary("Get current manager release")
|
||||
response {
|
||||
description("The latest manager release")
|
||||
mediaTypes("application/json")
|
||||
responseCode(HttpStatusCode.OK)
|
||||
responseType<APIRelease<APIManagerAsset>>()
|
||||
responseType<ApiRelease<ApiManagerAsset>>()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun Route.installLatestManagerVersionRouteDocumentation() = installNotarizedRoute {
|
||||
private fun Route.installManagerVersionRouteDocumentation(deprecated: Boolean) = installNotarizedRoute {
|
||||
tags = setOf("Manager")
|
||||
|
||||
get = GetInfo.builder {
|
||||
description("Get the latest manager release version")
|
||||
summary("Get latest manager release version")
|
||||
if (deprecated) isDeprecated()
|
||||
description("Get the current manager release version")
|
||||
summary("Get current manager release version")
|
||||
response {
|
||||
description("The latest manager release version")
|
||||
description("The current manager release version")
|
||||
mediaTypes("application/json")
|
||||
responseCode(HttpStatusCode.OK)
|
||||
responseType<APIReleaseVersion>()
|
||||
responseType<ApiReleaseVersion>()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,10 +2,10 @@ package app.revanced.api.configuration.routes
|
||||
|
||||
import app.revanced.api.configuration.installCache
|
||||
import app.revanced.api.configuration.installNotarizedRoute
|
||||
import app.revanced.api.configuration.schema.APIAssetPublicKeys
|
||||
import app.revanced.api.configuration.schema.APIPatchesAsset
|
||||
import app.revanced.api.configuration.schema.APIRelease
|
||||
import app.revanced.api.configuration.schema.APIReleaseVersion
|
||||
import app.revanced.api.configuration.schema.ApiAssetPublicKeys
|
||||
import app.revanced.api.configuration.schema.ApiPatchesAsset
|
||||
import app.revanced.api.configuration.schema.ApiRelease
|
||||
import app.revanced.api.configuration.schema.ApiReleaseVersion
|
||||
import app.revanced.api.configuration.services.PatchesService
|
||||
import io.bkbn.kompendium.core.metadata.GetInfo
|
||||
import io.ktor.http.*
|
||||
@@ -17,32 +17,39 @@ import kotlin.time.Duration.Companion.days
|
||||
import org.koin.ktor.ext.get as koinGet
|
||||
|
||||
internal fun Route.patchesRoute() = route("patches") {
|
||||
configure()
|
||||
|
||||
// TODO: Remove this deprecated route eventually.
|
||||
route("latest") {
|
||||
configure(deprecated = true)
|
||||
}
|
||||
}
|
||||
|
||||
private fun Route.configure(deprecated: Boolean = false) {
|
||||
val patchesService = koinGet<PatchesService>()
|
||||
|
||||
route("latest") {
|
||||
installLatestPatchesRouteDocumentation()
|
||||
installPatchesRouteDocumentation(deprecated)
|
||||
|
||||
rateLimit(RateLimitName("weak")) {
|
||||
get {
|
||||
call.respond(patchesService.latestRelease())
|
||||
}
|
||||
|
||||
route("version") {
|
||||
installLatestPatchesVersionRouteDocumentation()
|
||||
|
||||
get {
|
||||
call.respond(patchesService.latestVersion())
|
||||
}
|
||||
}
|
||||
rateLimit(RateLimitName("weak")) {
|
||||
get {
|
||||
call.respond(patchesService.latestRelease())
|
||||
}
|
||||
|
||||
rateLimit(RateLimitName("strong")) {
|
||||
route("list") {
|
||||
installLatestPatchesListRouteDocumentation()
|
||||
route("version") {
|
||||
installPatchesVersionRouteDocumentation(deprecated)
|
||||
|
||||
get {
|
||||
call.respondBytes(ContentType.Application.Json) { patchesService.list() }
|
||||
}
|
||||
get {
|
||||
call.respond(patchesService.latestVersion())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
rateLimit(RateLimitName("strong")) {
|
||||
route("list") {
|
||||
installPatchesListRouteDocumentation(deprecated)
|
||||
|
||||
get {
|
||||
call.respondBytes(ContentType.Application.Json) { patchesService.list() }
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -51,7 +58,7 @@ internal fun Route.patchesRoute() = route("patches") {
|
||||
route("keys") {
|
||||
installCache(356.days)
|
||||
|
||||
installPatchesPublicKeyRouteDocumentation()
|
||||
installPatchesPublicKeyRouteDocumentation(deprecated)
|
||||
|
||||
get {
|
||||
call.respond(patchesService.publicKeys())
|
||||
@@ -60,42 +67,45 @@ internal fun Route.patchesRoute() = route("patches") {
|
||||
}
|
||||
}
|
||||
|
||||
private fun Route.installLatestPatchesRouteDocumentation() = installNotarizedRoute {
|
||||
private fun Route.installPatchesRouteDocumentation(deprecated: Boolean) = installNotarizedRoute {
|
||||
tags = setOf("Patches")
|
||||
|
||||
get = GetInfo.builder {
|
||||
description("Get the latest patches release")
|
||||
summary("Get latest patches release")
|
||||
if (deprecated) isDeprecated()
|
||||
description("Get the current patches release")
|
||||
summary("Get current patches release")
|
||||
response {
|
||||
description("The latest patches release")
|
||||
description("The current patches release")
|
||||
mediaTypes("application/json")
|
||||
responseCode(HttpStatusCode.OK)
|
||||
responseType<APIRelease<APIPatchesAsset>>()
|
||||
responseType<ApiRelease<ApiPatchesAsset>>()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun Route.installLatestPatchesVersionRouteDocumentation() = installNotarizedRoute {
|
||||
private fun Route.installPatchesVersionRouteDocumentation(deprecated: Boolean) = installNotarizedRoute {
|
||||
tags = setOf("Patches")
|
||||
|
||||
get = GetInfo.builder {
|
||||
description("Get the latest patches release version")
|
||||
summary("Get latest patches release version")
|
||||
if (deprecated) isDeprecated()
|
||||
description("Get the current patches release version")
|
||||
summary("Get current patches release version")
|
||||
response {
|
||||
description("The latest patches release version")
|
||||
description("The current patches release version")
|
||||
mediaTypes("application/json")
|
||||
responseCode(HttpStatusCode.OK)
|
||||
responseType<APIReleaseVersion>()
|
||||
responseType<ApiReleaseVersion>()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun Route.installLatestPatchesListRouteDocumentation() = installNotarizedRoute {
|
||||
private fun Route.installPatchesListRouteDocumentation(deprecated: Boolean) = installNotarizedRoute {
|
||||
tags = setOf("Patches")
|
||||
|
||||
get = GetInfo.builder {
|
||||
description("Get the list of patches from the latest patches release")
|
||||
summary("Get list of patches from latest patches release")
|
||||
if (deprecated) isDeprecated()
|
||||
description("Get the list of patches from the current patches release")
|
||||
summary("Get list of patches from current patches release")
|
||||
response {
|
||||
description("The list of patches")
|
||||
mediaTypes("application/json")
|
||||
@@ -105,17 +115,18 @@ private fun Route.installLatestPatchesListRouteDocumentation() = installNotarize
|
||||
}
|
||||
}
|
||||
|
||||
private fun Route.installPatchesPublicKeyRouteDocumentation() = installNotarizedRoute {
|
||||
private fun Route.installPatchesPublicKeyRouteDocumentation(deprecated: Boolean) = installNotarizedRoute {
|
||||
tags = setOf("Patches")
|
||||
|
||||
get = GetInfo.builder {
|
||||
if (deprecated) isDeprecated()
|
||||
description("Get the public keys for verifying patches and integrations assets")
|
||||
summary("Get patches and integrations public keys")
|
||||
response {
|
||||
description("The public keys")
|
||||
mediaTypes("application/json")
|
||||
responseCode(HttpStatusCode.OK)
|
||||
responseType<APIAssetPublicKeys>()
|
||||
responseType<ApiAssetPublicKeys>()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,44 +3,44 @@ package app.revanced.api.configuration.schema
|
||||
import kotlinx.datetime.LocalDateTime
|
||||
import kotlinx.serialization.Serializable
|
||||
|
||||
interface APIUser {
|
||||
interface ApiUser {
|
||||
val name: String
|
||||
val avatarUrl: String
|
||||
val url: String
|
||||
}
|
||||
|
||||
@Serializable
|
||||
class APIMember(
|
||||
class ApiMember(
|
||||
override val name: String,
|
||||
override val avatarUrl: String,
|
||||
override val url: String,
|
||||
val bio: String?,
|
||||
val gpgKey: APIGpgKey?,
|
||||
) : APIUser
|
||||
val gpgKey: ApiGpgKey?,
|
||||
) : ApiUser
|
||||
|
||||
@Serializable
|
||||
class APIGpgKey(
|
||||
class ApiGpgKey(
|
||||
val id: String,
|
||||
val url: String,
|
||||
)
|
||||
|
||||
@Serializable
|
||||
class APIContributor(
|
||||
class ApiContributor(
|
||||
override val name: String,
|
||||
override val avatarUrl: String,
|
||||
override val url: String,
|
||||
val contributions: Int,
|
||||
) : APIUser
|
||||
) : ApiUser
|
||||
|
||||
@Serializable
|
||||
class APIContributable(
|
||||
val name: String,
|
||||
// Using a list instead of a set because set semantics are unnecessary here.
|
||||
val contributors: List<APIContributor>,
|
||||
val contributors: List<ApiContributor>,
|
||||
)
|
||||
|
||||
@Serializable
|
||||
class APIRelease<T>(
|
||||
class ApiRelease<T>(
|
||||
val version: String,
|
||||
val createdAt: LocalDateTime,
|
||||
val description: String,
|
||||
@@ -49,74 +49,82 @@ class APIRelease<T>(
|
||||
)
|
||||
|
||||
@Serializable
|
||||
class APIManagerAsset(
|
||||
class ApiManagerAsset(
|
||||
val downloadUrl: String,
|
||||
)
|
||||
|
||||
@Serializable
|
||||
class APIPatchesAsset(
|
||||
class ApiPatchesAsset(
|
||||
val downloadUrl: String,
|
||||
val signatureDownloadUrl: String,
|
||||
// TODO: Remove this eventually when integrations are merged into patches.
|
||||
val name: APIAssetName,
|
||||
val name: ApiAssetName,
|
||||
)
|
||||
|
||||
@Serializable
|
||||
enum class APIAssetName {
|
||||
enum class ApiAssetName {
|
||||
PATCHES,
|
||||
INTEGRATION,
|
||||
}
|
||||
|
||||
@Serializable
|
||||
class APIReleaseVersion(
|
||||
class ApiReleaseVersion(
|
||||
val version: String,
|
||||
)
|
||||
|
||||
@Serializable
|
||||
class APIAnnouncement(
|
||||
class ApiAnnouncement(
|
||||
val author: String? = null,
|
||||
val title: String,
|
||||
val content: String? = null,
|
||||
// Using a list instead of a set because set semantics are unnecessary here.
|
||||
val attachmentUrls: List<String> = emptyList(),
|
||||
val channel: String? = null,
|
||||
val attachments: List<String> = emptyList(),
|
||||
// Using a list instead of a set because set semantics are unnecessary here.
|
||||
val tags: List<String> = emptyList(),
|
||||
val archivedAt: LocalDateTime? = null,
|
||||
val level: Int = 0,
|
||||
)
|
||||
|
||||
@Serializable
|
||||
class APIResponseAnnouncement(
|
||||
class ApiResponseAnnouncement(
|
||||
val id: Int,
|
||||
val author: String? = null,
|
||||
val title: String,
|
||||
val content: String? = null,
|
||||
// Using a list instead of a set because set semantics are unnecessary here.
|
||||
val attachmentUrls: List<String> = emptyList(),
|
||||
val channel: String? = null,
|
||||
val attachments: List<String> = emptyList(),
|
||||
// Using a list instead of a set because set semantics are unnecessary here.
|
||||
val tags: List<Int> = emptyList(),
|
||||
val createdAt: LocalDateTime,
|
||||
val archivedAt: LocalDateTime? = null,
|
||||
val level: Int = 0,
|
||||
)
|
||||
|
||||
@Serializable
|
||||
class APIResponseAnnouncementId(
|
||||
class ApiResponseAnnouncementId(
|
||||
val id: Int,
|
||||
)
|
||||
|
||||
@Serializable
|
||||
class APIAnnouncementArchivedAt(
|
||||
class ApiAnnouncementArchivedAt(
|
||||
val archivedAt: LocalDateTime,
|
||||
)
|
||||
|
||||
@Serializable
|
||||
class APIRateLimit(
|
||||
class ApiAnnouncementTag(
|
||||
val id: Int,
|
||||
val name: String,
|
||||
)
|
||||
|
||||
@Serializable
|
||||
class ApiRateLimit(
|
||||
val limit: Int,
|
||||
val remaining: Int,
|
||||
val reset: LocalDateTime,
|
||||
)
|
||||
|
||||
@Serializable
|
||||
class APIAssetPublicKeys(
|
||||
class ApiAssetPublicKeys(
|
||||
val patchesPublicKey: String,
|
||||
val integrationsPublicKey: String,
|
||||
)
|
||||
@@ -172,3 +180,6 @@ class APIAbout(
|
||||
val links: List<Link>?,
|
||||
)
|
||||
}
|
||||
|
||||
@Serializable
|
||||
class ApiToken(val token: String)
|
||||
|
||||
@@ -1,35 +1,29 @@
|
||||
package app.revanced.api.configuration.services
|
||||
|
||||
import app.revanced.api.configuration.repository.AnnouncementRepository
|
||||
import app.revanced.api.configuration.schema.APIAnnouncement
|
||||
import app.revanced.api.configuration.schema.APIResponseAnnouncementId
|
||||
import kotlinx.datetime.LocalDateTime
|
||||
import app.revanced.api.configuration.schema.ApiAnnouncement
|
||||
|
||||
internal class AnnouncementService(
|
||||
private val announcementRepository: AnnouncementRepository,
|
||||
) {
|
||||
fun latestId(channel: String): APIResponseAnnouncementId? = announcementRepository.latestId(channel)
|
||||
fun latestId(): APIResponseAnnouncementId? = announcementRepository.latestId()
|
||||
suspend fun latest(tags: Set<Int>) = announcementRepository.latest(tags)
|
||||
|
||||
fun latest(channel: String) = announcementRepository.latest(channel)
|
||||
fun latest() = announcementRepository.latest()
|
||||
suspend fun latest() = announcementRepository.latest()
|
||||
|
||||
suspend fun all(channel: String) = announcementRepository.all(channel)
|
||||
suspend fun all() = announcementRepository.all()
|
||||
fun latestId(tags: Set<Int>) = announcementRepository.latestId(tags)
|
||||
|
||||
suspend fun new(new: APIAnnouncement) {
|
||||
announcementRepository.new(new)
|
||||
}
|
||||
suspend fun archive(id: Int, archivedAt: LocalDateTime?) {
|
||||
announcementRepository.archive(id, archivedAt)
|
||||
}
|
||||
suspend fun unarchive(id: Int) {
|
||||
announcementRepository.unarchive(id)
|
||||
}
|
||||
suspend fun update(id: Int, new: APIAnnouncement) {
|
||||
announcementRepository.update(id, new)
|
||||
}
|
||||
suspend fun delete(id: Int) {
|
||||
announcementRepository.delete(id)
|
||||
}
|
||||
fun latestId() = announcementRepository.latestId()
|
||||
|
||||
suspend fun paged(cursor: Int, limit: Int, tags: Set<Int>?, archived: Boolean) =
|
||||
announcementRepository.paged(cursor, limit, tags, archived)
|
||||
|
||||
suspend fun get(id: Int) = announcementRepository.get(id)
|
||||
|
||||
suspend fun update(id: Int, new: ApiAnnouncement) = announcementRepository.update(id, new)
|
||||
|
||||
suspend fun delete(id: Int) = announcementRepository.delete(id)
|
||||
|
||||
suspend fun new(new: ApiAnnouncement) = announcementRepository.new(new)
|
||||
|
||||
suspend fun tags() = announcementRepository.tags()
|
||||
}
|
||||
|
||||
@@ -21,7 +21,7 @@ internal class ApiService(
|
||||
APIContributable(
|
||||
it,
|
||||
backendRepository.contributors(configurationRepository.organization, it).map {
|
||||
APIContributor(it.name, it.avatarUrl, it.url, it.contributions)
|
||||
ApiContributor(it.name, it.avatarUrl, it.url, it.contributions)
|
||||
},
|
||||
)
|
||||
}
|
||||
@@ -29,13 +29,13 @@ internal class ApiService(
|
||||
}.awaitAll()
|
||||
|
||||
suspend fun team() = backendRepository.members(configurationRepository.organization).map { member ->
|
||||
APIMember(
|
||||
ApiMember(
|
||||
member.name,
|
||||
member.avatarUrl,
|
||||
member.url,
|
||||
member.bio,
|
||||
if (member.gpgKeys.ids.isNotEmpty()) {
|
||||
APIGpgKey(
|
||||
ApiGpgKey(
|
||||
// Must choose one of the GPG keys, because it does not make sense to have multiple GPG keys for the API.
|
||||
member.gpgKeys.ids.first(),
|
||||
member.gpgKeys.url,
|
||||
@@ -47,6 +47,6 @@ internal class ApiService(
|
||||
}
|
||||
|
||||
suspend fun rateLimit() = backendRepository.rateLimit()?.let {
|
||||
APIRateLimit(it.limit, it.remaining, it.reset)
|
||||
ApiRateLimit(it.limit, it.remaining, it.reset)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,49 +0,0 @@
|
||||
package app.revanced.api.configuration.services
|
||||
|
||||
import com.auth0.jwt.JWT
|
||||
import com.auth0.jwt.algorithms.Algorithm
|
||||
import io.ktor.server.application.*
|
||||
import io.ktor.server.auth.*
|
||||
import io.ktor.server.auth.jwt.*
|
||||
import java.util.*
|
||||
import kotlin.text.HexFormat
|
||||
import kotlin.time.Duration.Companion.minutes
|
||||
|
||||
internal class AuthService private constructor(
|
||||
private val issuer: String,
|
||||
private val validityInMin: Int,
|
||||
private val jwtSecret: String,
|
||||
private val authSHA256Digest: ByteArray,
|
||||
) {
|
||||
@OptIn(ExperimentalStdlibApi::class)
|
||||
constructor(issuer: String, validityInMin: Int, jwtSecret: String, authSHA256DigestString: String) : this(
|
||||
issuer,
|
||||
validityInMin,
|
||||
jwtSecret,
|
||||
authSHA256DigestString.hexToByteArray(HexFormat.Default),
|
||||
)
|
||||
|
||||
val configureSecurity: Application.() -> Unit = {
|
||||
install(Authentication) {
|
||||
jwt("jwt") {
|
||||
realm = "ReVanced"
|
||||
|
||||
verifier(JWT.require(Algorithm.HMAC256(jwtSecret)).withIssuer(issuer).build())
|
||||
}
|
||||
|
||||
digest("auth-digest") {
|
||||
realm = "ReVanced"
|
||||
algorithmName = "SHA-256"
|
||||
|
||||
digestProvider { _, _ ->
|
||||
authSHA256Digest
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun newToken(): String = JWT.create()
|
||||
.withIssuer(issuer)
|
||||
.withExpiresAt(Date(System.currentTimeMillis() + validityInMin.minutes.inWholeMilliseconds))
|
||||
.sign(Algorithm.HMAC256(jwtSecret))
|
||||
}
|
||||
@@ -0,0 +1,52 @@
|
||||
package app.revanced.api.configuration.services
|
||||
|
||||
import app.revanced.api.configuration.schema.ApiToken
|
||||
import com.auth0.jwt.JWT
|
||||
import com.auth0.jwt.algorithms.Algorithm
|
||||
import io.ktor.server.auth.*
|
||||
import io.ktor.server.auth.jwt.*
|
||||
import java.time.Instant
|
||||
import java.time.temporal.ChronoUnit
|
||||
import kotlin.text.HexFormat
|
||||
|
||||
internal class AuthenticationService private constructor(
|
||||
private val issuer: String,
|
||||
private val validityInMin: Long,
|
||||
private val jwtSecret: String,
|
||||
private val authSHA256Digest: ByteArray,
|
||||
) {
|
||||
@OptIn(ExperimentalStdlibApi::class)
|
||||
constructor(issuer: String, validityInMin: Long, jwtSecret: String, authSHA256DigestString: String) : this(
|
||||
issuer,
|
||||
validityInMin,
|
||||
jwtSecret,
|
||||
authSHA256DigestString.hexToByteArray(HexFormat.Default),
|
||||
)
|
||||
|
||||
fun AuthenticationConfig.jwt() {
|
||||
jwt("jwt") {
|
||||
realm = "ReVanced"
|
||||
verifier(JWT.require(Algorithm.HMAC256(jwtSecret)).withIssuer(issuer).build())
|
||||
// This is required and not optional. Authentication will fail if this is not present.
|
||||
validate { JWTPrincipal(it.payload) }
|
||||
}
|
||||
}
|
||||
|
||||
fun AuthenticationConfig.digest() {
|
||||
digest("auth-digest") {
|
||||
realm = "ReVanced"
|
||||
algorithmName = "SHA-256"
|
||||
|
||||
digestProvider { _, _ ->
|
||||
authSHA256Digest
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun newToken() = ApiToken(
|
||||
JWT.create()
|
||||
.withIssuer(issuer)
|
||||
.withExpiresAt(Instant.now().plus(validityInMin, ChronoUnit.MINUTES))
|
||||
.sign(Algorithm.HMAC256(jwtSecret)),
|
||||
)
|
||||
}
|
||||
@@ -9,17 +9,17 @@ internal class ManagerService(
|
||||
private val backendRepository: BackendRepository,
|
||||
private val configurationRepository: ConfigurationRepository,
|
||||
) {
|
||||
suspend fun latestRelease(): APIRelease<APIManagerAsset> {
|
||||
suspend fun latestRelease(): ApiRelease<ApiManagerAsset> {
|
||||
val managerRelease = backendRepository.release(
|
||||
configurationRepository.organization,
|
||||
configurationRepository.manager.repository,
|
||||
)
|
||||
|
||||
val managerAsset = APIManagerAsset(
|
||||
val managerAsset = ApiManagerAsset(
|
||||
managerRelease.assets.first(configurationRepository.manager.assetRegex).downloadUrl,
|
||||
)
|
||||
|
||||
return APIRelease(
|
||||
return ApiRelease(
|
||||
managerRelease.tag,
|
||||
managerRelease.createdAt,
|
||||
managerRelease.releaseNote,
|
||||
@@ -27,12 +27,12 @@ internal class ManagerService(
|
||||
)
|
||||
}
|
||||
|
||||
suspend fun latestVersion(): APIReleaseVersion {
|
||||
suspend fun latestVersion(): ApiReleaseVersion {
|
||||
val managerRelease = backendRepository.release(
|
||||
configurationRepository.organization,
|
||||
configurationRepository.manager.repository,
|
||||
)
|
||||
|
||||
return APIReleaseVersion(managerRelease.tag)
|
||||
return ApiReleaseVersion(managerRelease.tag)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,8 +4,8 @@ import app.revanced.api.configuration.repository.BackendRepository
|
||||
import app.revanced.api.configuration.repository.BackendRepository.BackendOrganization.BackendRepository.BackendRelease.Companion.first
|
||||
import app.revanced.api.configuration.repository.ConfigurationRepository
|
||||
import app.revanced.api.configuration.schema.*
|
||||
import app.revanced.library.PatchUtils
|
||||
import app.revanced.patcher.PatchBundleLoader
|
||||
import app.revanced.library.serializeTo
|
||||
import app.revanced.patcher.patch.loadPatchesFromJar
|
||||
import com.github.benmanes.caffeine.cache.Caffeine
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.withContext
|
||||
@@ -17,7 +17,7 @@ internal class PatchesService(
|
||||
private val backendRepository: BackendRepository,
|
||||
private val configurationRepository: ConfigurationRepository,
|
||||
) {
|
||||
suspend fun latestRelease(): APIRelease<APIPatchesAsset> {
|
||||
suspend fun latestRelease(): ApiRelease<ApiPatchesAsset> {
|
||||
val patchesRelease = backendRepository.release(
|
||||
configurationRepository.organization,
|
||||
configurationRepository.patches.repository,
|
||||
@@ -30,8 +30,8 @@ internal class PatchesService(
|
||||
|
||||
fun ConfigurationRepository.SignedAssetConfiguration.asset(
|
||||
release: BackendRepository.BackendOrganization.BackendRepository.BackendRelease,
|
||||
assetName: APIAssetName,
|
||||
) = APIPatchesAsset(
|
||||
assetName: ApiAssetName,
|
||||
) = ApiPatchesAsset(
|
||||
release.assets.first(assetRegex).downloadUrl,
|
||||
release.assets.first(signatureAssetRegex).downloadUrl,
|
||||
assetName,
|
||||
@@ -39,14 +39,14 @@ internal class PatchesService(
|
||||
|
||||
val patchesAsset = configurationRepository.patches.asset(
|
||||
patchesRelease,
|
||||
APIAssetName.PATCHES,
|
||||
ApiAssetName.PATCHES,
|
||||
)
|
||||
val integrationsAsset = configurationRepository.integrations.asset(
|
||||
integrationsRelease,
|
||||
APIAssetName.INTEGRATION,
|
||||
ApiAssetName.INTEGRATION,
|
||||
)
|
||||
|
||||
return APIRelease(
|
||||
return ApiRelease(
|
||||
patchesRelease.tag,
|
||||
patchesRelease.createdAt,
|
||||
patchesRelease.releaseNote,
|
||||
@@ -54,13 +54,13 @@ internal class PatchesService(
|
||||
)
|
||||
}
|
||||
|
||||
suspend fun latestVersion(): APIReleaseVersion {
|
||||
suspend fun latestVersion(): ApiReleaseVersion {
|
||||
val patchesRelease = backendRepository.release(
|
||||
configurationRepository.organization,
|
||||
configurationRepository.patches.repository,
|
||||
)
|
||||
|
||||
return APIReleaseVersion(patchesRelease.tag)
|
||||
return ApiReleaseVersion(patchesRelease.tag)
|
||||
}
|
||||
|
||||
private val patchesListCache = Caffeine
|
||||
@@ -94,7 +94,7 @@ internal class PatchesService(
|
||||
configurationRepository.patches.publicKeyId,
|
||||
)
|
||||
) {
|
||||
PatchBundleLoader.Jar(patchesFile)
|
||||
loadPatchesFromJar(setOf(patchesFile))
|
||||
} else {
|
||||
// Use an empty set of patches if the signature is invalid.
|
||||
emptySet()
|
||||
@@ -103,7 +103,7 @@ internal class PatchesService(
|
||||
patchesFile.delete()
|
||||
|
||||
ByteArrayOutputStream().use { stream ->
|
||||
PatchUtils.Json.serialize(patches, outputStream = stream)
|
||||
patches.serializeTo(outputStream = stream)
|
||||
|
||||
stream.toByteArray()
|
||||
}
|
||||
@@ -111,12 +111,12 @@ internal class PatchesService(
|
||||
}
|
||||
}
|
||||
|
||||
fun publicKeys(): APIAssetPublicKeys {
|
||||
fun publicKeys(): ApiAssetPublicKeys {
|
||||
fun readPublicKey(
|
||||
getSignedAssetConfiguration: ConfigurationRepository.() -> ConfigurationRepository.SignedAssetConfiguration,
|
||||
) = configurationRepository.getSignedAssetConfiguration().publicKeyFile.readText()
|
||||
|
||||
return APIAssetPublicKeys(
|
||||
return ApiAssetPublicKeys(
|
||||
readPublicKey { patches },
|
||||
readPublicKey { integrations },
|
||||
)
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
<pattern>%d{YYYY-MM-dd HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n</pattern>
|
||||
</encoder>
|
||||
</appender>
|
||||
<root level="info">
|
||||
<root level="\${LOG_LEVEL:-INFO}">
|
||||
<appender-ref ref="STDOUT"/>
|
||||
</root>
|
||||
</configuration>
|
||||
|
||||
@@ -0,0 +1,194 @@
|
||||
package app.revanced.api.configuration.services
|
||||
|
||||
import app.revanced.api.configuration.repository.AnnouncementRepository
|
||||
import app.revanced.api.configuration.schema.ApiAnnouncement
|
||||
import kotlinx.coroutines.runBlocking
|
||||
import kotlinx.datetime.toKotlinLocalDateTime
|
||||
import org.jetbrains.exposed.sql.Database
|
||||
import org.jetbrains.exposed.sql.transactions.TransactionManager
|
||||
import org.junit.jupiter.api.*
|
||||
import org.junit.jupiter.api.Assertions.assertNull
|
||||
import java.time.LocalDateTime
|
||||
import kotlin.test.assertEquals
|
||||
import kotlin.test.assertNotNull
|
||||
|
||||
private object AnnouncementServiceTest {
|
||||
private lateinit var announcementService: AnnouncementService
|
||||
|
||||
@JvmStatic
|
||||
@BeforeAll
|
||||
fun setUp() {
|
||||
TransactionManager.defaultDatabase =
|
||||
Database.connect("jdbc:h2:mem:test;DB_CLOSE_DELAY=-1;DATABASE_TO_UPPER=false")
|
||||
|
||||
announcementService = AnnouncementService(AnnouncementRepository())
|
||||
}
|
||||
|
||||
@BeforeEach
|
||||
fun clear() {
|
||||
runBlocking {
|
||||
while (true) {
|
||||
val latestId = announcementService.latestId() ?: break
|
||||
announcementService.delete(latestId.id)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `can do basic crud`(): Unit = runBlocking {
|
||||
announcementService.new(ApiAnnouncement(title = "title"))
|
||||
|
||||
val latestId = announcementService.latestId()!!.id
|
||||
|
||||
announcementService.update(latestId, ApiAnnouncement(title = "new title"))
|
||||
assert(announcementService.get(latestId)?.title == "new title")
|
||||
|
||||
announcementService.delete(latestId)
|
||||
assertNull(announcementService.get(latestId))
|
||||
assertNull(announcementService.latestId())
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `archiving works properly`() = runBlocking {
|
||||
announcementService.new(ApiAnnouncement(title = "title"))
|
||||
|
||||
val latest = announcementService.latest()!!
|
||||
assertNull(announcementService.get(latest.id)?.archivedAt)
|
||||
|
||||
val updated = ApiAnnouncement(
|
||||
title = latest.title,
|
||||
archivedAt = LocalDateTime.now().toKotlinLocalDateTime(),
|
||||
)
|
||||
|
||||
announcementService.update(latest.id, updated)
|
||||
assertNotNull(announcementService.get(latest.id)?.archivedAt)
|
||||
|
||||
return@runBlocking
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `latest works properly`() = runBlocking {
|
||||
announcementService.new(ApiAnnouncement(title = "title"))
|
||||
announcementService.new(ApiAnnouncement(title = "title2"))
|
||||
|
||||
var latest = announcementService.latest()
|
||||
assert(latest?.title == "title2")
|
||||
|
||||
announcementService.delete(latest!!.id)
|
||||
|
||||
latest = announcementService.latest()
|
||||
assert(latest?.title == "title")
|
||||
|
||||
announcementService.delete(latest!!.id)
|
||||
assertNull(announcementService.latest())
|
||||
|
||||
announcementService.new(ApiAnnouncement(title = "1", tags = listOf("tag1", "tag2")))
|
||||
announcementService.new(ApiAnnouncement(title = "2", tags = listOf("tag1", "tag3")))
|
||||
announcementService.new(ApiAnnouncement(title = "3", tags = listOf("tag1", "tag4")))
|
||||
|
||||
val tag2 = announcementService.tags().find { it.name == "tag2" }!!.id
|
||||
assert(announcementService.latest(setOf(tag2)).first().title == "1")
|
||||
|
||||
val tag3 = announcementService.tags().find { it.name == "tag3" }!!.id
|
||||
assert(announcementService.latest(setOf(tag3)).last().title == "2")
|
||||
|
||||
val tag1and3 =
|
||||
announcementService.tags().filter { it.name == "tag1" || it.name == "tag3" }.map { it.id }.toSet()
|
||||
val announcement2and3 = announcementService.latest(tag1and3)
|
||||
assert(announcement2and3.size == 2)
|
||||
assert(announcement2and3.any { it.title == "2" })
|
||||
assert(announcement2and3.any { it.title == "3" })
|
||||
|
||||
announcementService.delete(announcementService.latestId()!!.id)
|
||||
assert(announcementService.latest(tag1and3).first().title == "2")
|
||||
|
||||
announcementService.delete(announcementService.latestId()!!.id)
|
||||
assert(announcementService.latest(tag1and3).first().title == "1")
|
||||
|
||||
announcementService.delete(announcementService.latestId()!!.id)
|
||||
assert(announcementService.latest(tag1and3).isEmpty())
|
||||
assert(announcementService.tags().isEmpty())
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `tags work properly`() = runBlocking {
|
||||
announcementService.new(ApiAnnouncement(title = "title", tags = listOf("tag1", "tag2")))
|
||||
announcementService.new(ApiAnnouncement(title = "title2", tags = listOf("tag1", "tag3")))
|
||||
|
||||
val tags = announcementService.tags()
|
||||
assertEquals(3, tags.size)
|
||||
assert(tags.any { it.name == "tag1" })
|
||||
assert(tags.any { it.name == "tag2" })
|
||||
assert(tags.any { it.name == "tag3" })
|
||||
|
||||
announcementService.delete(announcementService.latestId()!!.id)
|
||||
assertEquals(2, announcementService.tags().size)
|
||||
|
||||
announcementService.update(
|
||||
announcementService.latestId()!!.id,
|
||||
ApiAnnouncement(title = "title", tags = listOf("tag1", "tag3")),
|
||||
)
|
||||
|
||||
assertEquals(2, announcementService.tags().size)
|
||||
assert(announcementService.tags().any { it.name == "tag3" })
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `attachments work properly`() = runBlocking {
|
||||
announcementService.new(ApiAnnouncement(title = "title", attachments = listOf("attachment1", "attachment2")))
|
||||
|
||||
val latestAnnouncement = announcementService.latest()!!
|
||||
val latestId = latestAnnouncement.id
|
||||
|
||||
val attachments = latestAnnouncement.attachments
|
||||
assertEquals(2, attachments.size)
|
||||
assert(attachments.any { it == "attachment1" })
|
||||
assert(attachments.any { it == "attachment2" })
|
||||
|
||||
announcementService.update(
|
||||
latestId,
|
||||
ApiAnnouncement(title = "title", attachments = listOf("attachment1", "attachment3")),
|
||||
)
|
||||
assert(announcementService.get(latestId)!!.attachments.any { it == "attachment3" })
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `paging works correctly`() = runBlocking {
|
||||
repeat(10) {
|
||||
announcementService.new(ApiAnnouncement(title = "title$it"))
|
||||
}
|
||||
|
||||
val announcements = announcementService.paged(Int.MAX_VALUE, 5, null, true)
|
||||
assertEquals(5, announcements.size, "Returns correct number of announcements")
|
||||
assertEquals("title9", announcements.first().title, "Starts from the latest announcement")
|
||||
|
||||
val announcements2 = announcementService.paged(5, 5, null, true)
|
||||
assertEquals(5, announcements2.size, "Returns correct number of announcements when starting from the cursor")
|
||||
assertEquals("title4", announcements2.first().title, "Starts from the cursor")
|
||||
|
||||
(0..4).forEach { id ->
|
||||
announcementService.update(
|
||||
id,
|
||||
ApiAnnouncement(
|
||||
title = "title$id",
|
||||
tags = (0..id).map { "tag$it" },
|
||||
archivedAt = if (id % 2 == 0) {
|
||||
// Only two announcements will be archived.
|
||||
LocalDateTime.now().plusDays(2).minusDays(id.toLong()).toKotlinLocalDateTime()
|
||||
} else {
|
||||
null
|
||||
},
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
val tags = announcementService.tags()
|
||||
assertEquals(5, tags.size, "Returns correct number of newly created tags")
|
||||
|
||||
val announcements3 = announcementService.paged(5, 5, setOf(tags[1].id), true)
|
||||
assertEquals(4, announcements3.size, "Filters announcements by tag")
|
||||
|
||||
val announcements4 = announcementService.paged(Int.MAX_VALUE, 10, null, false)
|
||||
assertEquals(8, announcements4.size, "Filters out archived announcements")
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user