Compare commits

..

No commits in common. "main" and "v1.0.0-dev.2" have entirely different histories.

48 changed files with 2168 additions and 4241 deletions

View File

@ -1,5 +1,7 @@
# Optional token for API calls to the backend
BACKEND_API_TOKEN=
# A URL to the old API to proxy for migration purposes
OLD_API_URL=
# Database connection details
DB_URL=jdbc:h2:./persistence/revanced-api
@ -13,6 +15,3 @@ AUTH_SHA256_DIGEST=
JWT_SECRET=
JWT_ISSUER=
JWT_VALIDITY_IN_MIN=
# Logging level for the application
LOG_LEVEL=INFO

View File

@ -12,7 +12,7 @@ jobs:
name: Release
runs-on: ubuntu-latest
permissions:
contents: write
contents: read
packages: write
steps:
- name: Checkout
@ -45,7 +45,7 @@ jobs:
with:
gpg_private_key: ${{ secrets.GPG_PRIVATE_KEY }}
passphrase: ${{ secrets.GPG_PASSPHRASE }}
fingerprint: ${{ vars.GPG_FINGERPRINT }}
fingerprint: ${{ env.GPG_FINGERPRINT }}
- name: Setup QEMU
uses: docker/setup-qemu-action@v3
@ -60,7 +60,7 @@ jobs:
DOCKER_REGISTRY_USER: ${{ github.actor }}
DOCKER_REGISTRY_PASSWORD: ${{ secrets.GITHUB_TOKEN }}
GITHUB_ACTOR: ${{ github.actor }}
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
GITHUB_TOKEN: ${{ secrets.REPOSITORY_PUSH_ACCESS }}
run: npm exec semantic-release
- name: Set Portainer stack webhook URL based on branch

3
.gitignore vendored
View File

@ -41,6 +41,5 @@ persistence/
configuration.toml
docker-compose.yml
patches-public-key.asc
integrations-public-key.asc
node_modules/
static/
about.json

View File

@ -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}"
]
}
],
[

View File

@ -1,416 +1,3 @@
# [1.6.0](https://github.com/ReVanced/revanced-api/compare/v1.5.0...v1.6.0) (2025-02-04)
### Features
* Add status page link to about ([8a957cd](https://github.com/ReVanced/revanced-api/commit/8a957cd797e7e42f43670baaed60ac0d3543342f))
* Add support for prereleases ([c25bc8b](https://github.com/ReVanced/revanced-api/commit/c25bc8b4ba2bd4bf1708f19dc8bc228a7f54d548))
* Allow setting `Announcement.createdAt` when creating an announcement ([7f6e29d](https://github.com/ReVanced/revanced-api/commit/7f6e29de5205f63ac4aaea490c844b58e14000c8))
* Make some announcements schema fields nullable ([db22874](https://github.com/ReVanced/revanced-api/commit/db22874f063bae0c9e7f0c99a20cdf1b16addd89))
# [1.6.0-dev.3](https://github.com/ReVanced/revanced-api/compare/v1.6.0-dev.2...v1.6.0-dev.3) (2024-12-25)
### Features
* Add status page link to about ([8a957cd](https://github.com/ReVanced/revanced-api/commit/8a957cd797e7e42f43670baaed60ac0d3543342f))
* Add support for prereleases ([c25bc8b](https://github.com/ReVanced/revanced-api/commit/c25bc8b4ba2bd4bf1708f19dc8bc228a7f54d548))
# [1.6.0-dev.2](https://github.com/ReVanced/revanced-api/compare/v1.6.0-dev.1...v1.6.0-dev.2) (2024-12-20)
### Features
* Make some announcements schema fields nullable ([db22874](https://github.com/ReVanced/revanced-api/commit/db22874f063bae0c9e7f0c99a20cdf1b16addd89))
# [1.6.0-dev.1](https://github.com/ReVanced/revanced-api/compare/v1.5.0...v1.6.0-dev.1) (2024-11-23)
### Features
* Allow setting `Announcement.createdAt` when creating an announcement ([7f6e29d](https://github.com/ReVanced/revanced-api/commit/7f6e29de5205f63ac4aaea490c844b58e14000c8))
# [1.5.0](https://github.com/ReVanced/revanced-api/compare/v1.4.0...v1.5.0) (2024-11-06)
### Features
* Allow updating `createdAt` field for announcements ([58ba4cb](https://github.com/ReVanced/revanced-api/commit/58ba4cb11c789507826cd70ac548943a94da4223))
* Move spec url to versioned path ([e871b23](https://github.com/ReVanced/revanced-api/commit/e871b23210798723c34bce93c7567d8fbcf4e060))
* Simplify log pattern ([d5d9e04](https://github.com/ReVanced/revanced-api/commit/d5d9e04325fa93540be0438e7b51243e2aeeab3d))
# [1.5.0-dev.2](https://github.com/ReVanced/revanced-api/compare/v1.5.0-dev.1...v1.5.0-dev.2) (2024-11-06)
### Features
* Allow updating `createdAt` field for announcements ([58ba4cb](https://github.com/ReVanced/revanced-api/commit/58ba4cb11c789507826cd70ac548943a94da4223))
* Simplify log pattern ([d5d9e04](https://github.com/ReVanced/revanced-api/commit/d5d9e04325fa93540be0438e7b51243e2aeeab3d))
# [1.5.0-dev.1](https://github.com/ReVanced/revanced-api/compare/v1.4.0...v1.5.0-dev.1) (2024-11-06)
### Features
* Move spec url to versioned path ([e871b23](https://github.com/ReVanced/revanced-api/commit/e871b23210798723c34bce93c7567d8fbcf4e060))
* Simplify log pattern ([d5d9e04](https://github.com/ReVanced/revanced-api/commit/d5d9e04325fa93540be0438e7b51243e2aeeab3d))
# [1.5.0-dev.1](https://github.com/ReVanced/revanced-api/compare/v1.4.0...v1.5.0-dev.1) (2024-11-06)
### Features
* Move spec url to versioned path ([e871b23](https://github.com/ReVanced/revanced-api/commit/e871b23210798723c34bce93c7567d8fbcf4e060))
# [1.4.0](https://github.com/ReVanced/revanced-api/compare/v1.3.0...v1.4.0) (2024-11-06)
### Bug Fixes
* Add missing logging level environment variable to .env.example ([3b62120](https://github.com/ReVanced/revanced-api/commit/3b6212065a5cfb95c303b6d0551747ba1eb317f6))
* Use new patches file extension ([d42a3a3](https://github.com/ReVanced/revanced-api/commit/d42a3a393396a0f4e9085cda46e0af2c12b63cb1))
### Features
* Add URL and use friendly name for `APIContributable` ([a5498ab](https://github.com/ReVanced/revanced-api/commit/a5498aba2b99db89c28a65738cc58cc4c852c327))
* Allow versioning by arbitrary path string ([814d3c9](https://github.com/ReVanced/revanced-api/commit/814d3c946e31068e12e3886aa8beb3238ef126ae))
* Improve announcements API ([#192](https://github.com/ReVanced/revanced-api/issues/192)) ([56a00dd](https://github.com/ReVanced/revanced-api/commit/56a00ddb85f302d441f0b222a9902ea2c1c18897))
* Make backend configurable ([f91f3a6](https://github.com/ReVanced/revanced-api/commit/f91f3a65c5e07b5b58ccbff1d4b0a5ba9b15fc50))
* Remove "archived" query parameter ([8ad614e](https://github.com/ReVanced/revanced-api/commit/8ad614ef4fdaf45af87a3316ef4db7e7236fd64a))
* Remove deprecated routes and old API ([eca40a6](https://github.com/ReVanced/revanced-api/commit/eca40a69799240f7803aa8851eb3ee961937e4d6))
* Remove ReVanced Integrations ([f1c1092](https://github.com/ReVanced/revanced-api/commit/f1c10928ae3be1c6b1d675819755b3046fad70d8))
* Use tag name directly instead of ID ([fc40427](https://github.com/ReVanced/revanced-api/commit/fc40427fbaafb523045eb6f5285d90949b206b8b))
# [1.4.0-dev.6](https://github.com/ReVanced/revanced-api/compare/v1.4.0-dev.5...v1.4.0-dev.6) (2024-11-06)
### Features
* Allow versioning by arbitrary path string ([814d3c9](https://github.com/ReVanced/revanced-api/commit/814d3c946e31068e12e3886aa8beb3238ef126ae))
* Remove deprecated routes and old API ([eca40a6](https://github.com/ReVanced/revanced-api/commit/eca40a69799240f7803aa8851eb3ee961937e4d6))
# [1.4.0-dev.5](https://github.com/ReVanced/revanced-api/compare/v1.4.0-dev.4...v1.4.0-dev.5) (2024-11-05)
# [1.4.0-dev.4](https://github.com/ReVanced/revanced-api/compare/v1.4.0-dev.3...v1.4.0-dev.4) (2024-11-01)
### Features
* Remove "archived" query parameter ([8ad614e](https://github.com/ReVanced/revanced-api/commit/8ad614ef4fdaf45af87a3316ef4db7e7236fd64a))
* Use tag name directly instead of ID ([fc40427](https://github.com/ReVanced/revanced-api/commit/fc40427fbaafb523045eb6f5285d90949b206b8b))
# [1.4.0-dev.3](https://github.com/ReVanced/revanced-api/compare/v1.4.0-dev.2...v1.4.0-dev.3) (2024-11-01)
### Bug Fixes
* Use new patches file extension ([d42a3a3](https://github.com/ReVanced/revanced-api/commit/d42a3a393396a0f4e9085cda46e0af2c12b63cb1))
# [1.4.0-dev.2](https://github.com/ReVanced/revanced-api/compare/v1.4.0-dev.1...v1.4.0-dev.2) (2024-11-01)
### Bug Fixes
* Add missing logging level environment variable to .env.example ([3b62120](https://github.com/ReVanced/revanced-api/commit/3b6212065a5cfb95c303b6d0551747ba1eb317f6))
### Features
* Add URL and use friendly name for `APIContributable` ([a5498ab](https://github.com/ReVanced/revanced-api/commit/a5498aba2b99db89c28a65738cc58cc4c852c327))
* Make backend configurable ([f91f3a6](https://github.com/ReVanced/revanced-api/commit/f91f3a65c5e07b5b58ccbff1d4b0a5ba9b15fc50))
* Remove ReVanced Integrations ([f1c1092](https://github.com/ReVanced/revanced-api/commit/f1c10928ae3be1c6b1d675819755b3046fad70d8))
# [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)
### 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.0.0 (2024-07-13)
### Bug Fixes
* Add missing auth realm ([a6008a2](https://github.com/ReVanced/revanced-api/commit/a6008a2fb6d01fb577dcf357a1d698b6c1068b31))
* Add missing OpenAPI docs ([d4ac471](https://github.com/ReVanced/revanced-api/commit/d4ac47194e51b3e708516150d1690a72830ab809))
* add required headers ([#8](https://github.com/ReVanced/revanced-api/issues/8)) ([f4c10dc](https://github.com/ReVanced/revanced-api/commit/f4c10dc064c4ac0fb6b8e612d680e4f6bda2b0c7))
* Add uri to rate limiter request key ([e8c2488](https://github.com/ReVanced/revanced-api/commit/e8c2488bc61793345c4b8171e520fb0127b34643))
* **ci:** add git dep ([d61ddcc](https://github.com/ReVanced/revanced-api/commit/d61ddcc8ac76d78b7a79c09bbfba812f652aa4cf))
* **ci:** fix git deps ([5564c2d](https://github.com/ReVanced/revanced-api/commit/5564c2da0946d67abab0f95e43fe6e6b108f5ec7))
* Configure CORS correctly ([2ed4cf3](https://github.com/ReVanced/revanced-api/commit/2ed4cf3b40caeb6181d068d411344e6732000f22))
* Correct env var comment ([9d7b049](https://github.com/ReVanced/revanced-api/commit/9d7b0493498bbf594928fc61181beadf6f59643b))
* Correct persistence directory name ([6238e33](https://github.com/ReVanced/revanced-api/commit/6238e33c42216885d5e9e8075bfc46eaf2a990ed))
* **deps:** fix requirements file ([5f0ab26](https://github.com/ReVanced/revanced-api/commit/5f0ab26cedef27cc95eec58ea4dce00904b53289))
* **deps:** missing pydantic ([089f29e](https://github.com/ReVanced/revanced-api/commit/089f29e95fc6b87f90885eb31b8e3460857224a8))
* **deps:** update poetry.lock ([b2c2fa7](https://github.com/ReVanced/revanced-api/commit/b2c2fa7136f6305703ba11d0bd5a87c0d563eaf3))
* Don't configure server ([280dbc3](https://github.com/ReVanced/revanced-api/commit/280dbc30f607483adad0bde6ab1b016d5da047ba))
* Encode defaults to fix OpenAPI spec ([e9d1c8f](https://github.com/ReVanced/revanced-api/commit/e9d1c8fae0bc46e761056197658c4bb045784104))
* Finish DB Model to API model transformation inside transaction ([89a577e](https://github.com/ReVanced/revanced-api/commit/89a577e91abbfcd2865e770088661eac4aeb4dd7))
* fix codeql issues ([b5568db](https://github.com/ReVanced/revanced-api/commit/b5568db79fa0620d06d6945b42a66744b6340bc5))
* Fix OpenAPI docs casing of a word ([541783d](https://github.com/ReVanced/revanced-api/commit/541783d9599c257f184d1b244e1b857b7c200227))
* Fix spelling mistake ([17ecf58](https://github.com/ReVanced/revanced-api/commit/17ecf58e550d13dd93ab69e1cf522366aeb3da3f))
* Increase pool size to mitigate overflow ([#113](https://github.com/ReVanced/revanced-api/issues/113)) ([5aed3d6](https://github.com/ReVanced/revanced-api/commit/5aed3d6ce6efcaf04b3cfa8f344623413518c9b1))
* Move old API endpoint configuration from env to configuration file ([7e99e49](https://github.com/ReVanced/revanced-api/commit/7e99e49af202c4ec0a0d7e61dd0182dd2097e867))
* Move robots.txt to root ([2ade550](https://github.com/ReVanced/revanced-api/commit/2ade550d58c0e4b53fa7417bef0064f4f476aed8))
* Only list public members ([97a5d11](https://github.com/ReVanced/revanced-api/commit/97a5d119ec415f9c25fbc50cb240603047defc73))
* Remove punctuation ([f9cae1e](https://github.com/ReVanced/revanced-api/commit/f9cae1ea56c93aded25159f6b0814bf84d192192))
* remove revanced-api from tools map ([4800ee9](https://github.com/ReVanced/revanced-api/commit/4800ee96a82c0a9fc1905c6960cd560b55304944))
* Serialize response correctly ([1dccfd2](https://github.com/ReVanced/revanced-api/commit/1dccfd2deff3c5de6a6cf2156cac8516b40bdd22))
* Set body for all eligible request methods ([c6cacef](https://github.com/ReVanced/revanced-api/commit/c6cacef907a5039ed029e1e26204aaba8e698aaa))
* unversioned endpoints placement under v0 should be at root ([8b29f49](https://github.com/ReVanced/revanced-api/commit/8b29f49805d4a651bdd26aa4958a3639760a2f4b))
* Use correct persistance folder path ([500a589](https://github.com/ReVanced/revanced-api/commit/500a5896e34f6a9600dcec2834b3d340161bb90a))
* Use correct proxy path ([ef92768](https://github.com/ReVanced/revanced-api/commit/ef927688a377c16fe37b578ea870207871c30056))
* Use correct resource path ([4dffd32](https://github.com/ReVanced/revanced-api/commit/4dffd32c99a5a3deafe21bc9e9960795ff93ff1d))
* Use multidict 6.0.5, add setup tools as dev dependency ([af19446](https://github.com/ReVanced/revanced-api/commit/af19446a67445e96624dfdb9f1fa6b4de77545c8))
### Features
* Add `/list` route ([6c930ff](https://github.com/ReVanced/revanced-api/commit/6c930fff9a99f9ee23edd3d661694d2074f53b23))
* Add `preferred` field to socials ([#100](https://github.com/ReVanced/revanced-api/issues/100)) ([24c8f60](https://github.com/ReVanced/revanced-api/commit/24c8f60a707ebec9465d9fdcd234b1c949a11a60))
* Add announcements API ([42f7318](https://github.com/ReVanced/revanced-api/commit/42f731854d0b91070bd7b075054d67d3d9e1d1b4))
* Add announcements endpoints ([#91](https://github.com/ReVanced/revanced-api/issues/91)) ([8583e2a](https://github.com/ReVanced/revanced-api/commit/8583e2a2bbdd62c4c10eb2af607f0bf6b686331c))
* Add backend rate limit route ([b967170](https://github.com/ReVanced/revanced-api/commit/b9671703be9f0842c00c38caa0953a177dc74491))
* Add bio field for team members ([c40d50c](https://github.com/ReVanced/revanced-api/commit/c40d50c01368cb6b9ab06857694ec51d27aba2cb))
* Add CLI ([a988ffb](https://github.com/ReVanced/revanced-api/commit/a988ffbd2303a79ee18be7263ef6cd45c7bc4daf))
* Add configuration to specify public key id ([ad7d4b2](https://github.com/ReVanced/revanced-api/commit/ad7d4b226f71fd965421dc2f2a51825c8e8b3036))
* add download_count to releases ([#118](https://github.com/ReVanced/revanced-api/issues/118)) ([665b913](https://github.com/ReVanced/revanced-api/commit/665b913c04edd8e55e7ba89839c5c39f8dfa42ac))
* add friendly crypto names ([#53](https://github.com/ReVanced/revanced-api/issues/53)) ([7b70780](https://github.com/ReVanced/revanced-api/commit/7b707807cc307c1a64abbb09983fb70d5b095698))
* Add GPG key to team members ([71f58cf](https://github.com/ReVanced/revanced-api/commit/71f58cf352e06b8504e00582e0c68aec55c4688b))
* Add local ReVanced API server ([cd5d57f](https://github.com/ReVanced/revanced-api/commit/cd5d57f8f87125df361e23715eda6e755203d727))
* Add manager route ([f814fe5](https://github.com/ReVanced/revanced-api/commit/f814fe5825eb8b864f2b0cba53ff721eba577eb4))
* add member gpg keys ([80cdb3b](https://github.com/ReVanced/revanced-api/commit/80cdb3be6ad2264977bf84269b09546a744576f5))
* Add OpenAPI docs and cache to routes ([6ea63be](https://github.com/ReVanced/revanced-api/commit/6ea63be490e7786c6486ee78c1fa38f302e8b81c))
* add preferred field to donations ([#56](https://github.com/ReVanced/revanced-api/issues/56)) ([6b705b1](https://github.com/ReVanced/revanced-api/commit/6b705b10545d5e6d2c9424275fbbd996bc45089f))
* Add proxy for old API ([39f54bb](https://github.com/ReVanced/revanced-api/commit/39f54bbb32512a6df255bbec6821f7f7acf91340))
* Add rate limiting to routes ([80403f7](https://github.com/ReVanced/revanced-api/commit/80403f7130cd48e68e802ee3111760256e49c77d))
* added branding ([#104](https://github.com/ReVanced/revanced-api/issues/104)) ([edcad62](https://github.com/ReVanced/revanced-api/commit/edcad620f29ee7e522bbc3a29db607f9823d9ce9))
* API Fixes and Adjustments ([#23](https://github.com/ReVanced/revanced-api/issues/23)) ([b18097e](https://github.com/ReVanced/revanced-api/commit/b18097e030c82e97f3880fd0788b562645f6e002))
* API rewrite ([#2](https://github.com/ReVanced/revanced-api/issues/2)) ([45ef337](https://github.com/ReVanced/revanced-api/commit/45ef33741c7a8fbd0144fda04370fe361a1d7c0c))
* better versioning engine ([8d36663](https://github.com/ReVanced/revanced-api/commit/8d36663610164d8198c18e5080184c1357f29254))
* Change default port to avoid using existing port ([9825865](https://github.com/ReVanced/revanced-api/commit/9825865bbc7505fc3e808d21a46a151151796591))
* Disallow all web crawlers ([#111](https://github.com/ReVanced/revanced-api/issues/111)) ([b69acfa](https://github.com/ReVanced/revanced-api/commit/b69acfa8d7f5385735f933a761239be6afd07384))
* Do not ignore, if `.env` file is missing ([24c6f4e](https://github.com/ReVanced/revanced-api/commit/24c6f4e4354b4e6da0e4a4e7f0ee0a7a5e3c90ed))
* favicon ([a8126d7](https://github.com/ReVanced/revanced-api/commit/a8126d785f8f828a746f7f1ce2fe20d1a605e8f8))
* hostname filtering ([5482d9c](https://github.com/ReVanced/revanced-api/commit/5482d9c44245bae75007935db6ec02fb553bfa2d))
* Implement more routes and add configuration ([9999b24](https://github.com/ReVanced/revanced-api/commit/9999b242ad05dcea9e6b021e3ed4eeead4567805))
* Improve routing paths ([df999c0](https://github.com/ReVanced/revanced-api/commit/df999c00c4c1f8645cc67ae19b732f0af9d71823))
* info endpoint ([#71](https://github.com/ReVanced/revanced-api/issues/71)) ([9bbd056](https://github.com/ReVanced/revanced-api/commit/9bbd056c1bc6a30059a8ed0a47823c228531e4c0))
* Initialize project ([8ae50b5](https://github.com/ReVanced/revanced-api/commit/8ae50b543eb78f2173c66acc2e9b676a7026f80a))
* List more repository contributors ([19ebc82](https://github.com/ReVanced/revanced-api/commit/19ebc827bfb54a597dd06f9d99bdc820ee9977ee))
* Load system properties ([e373d26](https://github.com/ReVanced/revanced-api/commit/e373d269982428357bd04b659d9d17110cf0d5d2))
* manager-related endpoints ([3a128c4](https://github.com/ReVanced/revanced-api/commit/3a128c4661b161dc9961837056a3b64b84824162))
* Move config file to CLI argument ([6a9f0ca](https://github.com/ReVanced/revanced-api/commit/6a9f0cadac9e09bcf834ef708fab180038ebd4bd))
* **observability:** sentry ([99e645c](https://github.com/ReVanced/revanced-api/commit/99e645c5f5604b5b1c8cbab0bed22177fc01c695))
* project init ([856fc66](https://github.com/ReVanced/revanced-api/commit/856fc667170d21ae5aba996bcc356ca44412e10d))
* remove appinfo capabilities ([10f5225](https://github.com/ReVanced/revanced-api/commit/10f5225f514a03b2532ec430b5caace6c6049b92))
* remove codecov tests ([9c69fa3](https://github.com/ReVanced/revanced-api/commit/9c69fa3b924b2ecda5c52256c383d8619ec61ece))
* Remove Swagger and OpenAPI ([af0b086](https://github.com/ReVanced/revanced-api/commit/af0b0865f4c2f22975a836b72ff0b902ddac1ce9))
* Setup CI/CD ([c736a75](https://github.com/ReVanced/revanced-api/commit/c736a75d92d97124e1aca392f37049449a786c84))
* Setup cors and cache ([205bcde](https://github.com/ReVanced/revanced-api/commit/205bcde77aad90e0eb49fc25961399f1e37698bb))
* Show default CLI option values ([db0bfc3](https://github.com/ReVanced/revanced-api/commit/db0bfc3be5b8d86466fa7f1a01a72247b4e878aa))
* Use auth digest instead of basic auth ([89e2acf](https://github.com/ReVanced/revanced-api/commit/89e2acfebb5e14f71d9ce3d962c5531070b7600e))
* Use Jetty instead of Netty ([c23cd5c](https://github.com/ReVanced/revanced-api/commit/c23cd5cdad01fee52aecdb36b93a619886edfa4d))
* use objects for /socials and /donations ([#51](https://github.com/ReVanced/revanced-api/issues/51)) ([d4eac5c](https://github.com/ReVanced/revanced-api/commit/d4eac5c757106c4762cb3e95f9e4b3509132b39f))
* use objects for contact field ([5922830](https://github.com/ReVanced/revanced-api/commit/5922830e0bf1c0504d3205d2fa864c76d0f09c02))
### Performance Improvements
* Cache latest announcements for constant access time ([1ca9952](https://github.com/ReVanced/revanced-api/commit/1ca9952de81feeae1333872a7741119d6d8e7814))
* Cache patches list instead of just the patches file ([7a1957d](https://github.com/ReVanced/revanced-api/commit/7a1957d013cfd0851dc0191b715e8f36c530d9d2))
* Make async db transactions and use List instead of Set ([a7d1892](https://github.com/ReVanced/revanced-api/commit/a7d1892343094ddb2762ca346759771e6a274081))
# [1.0.0-dev.7](https://github.com/ReVanced/revanced-api/compare/v1.0.0-dev.6...v1.0.0-dev.7) (2024-07-13)
### Bug Fixes
* Fix OpenAPI docs casing of a word ([541783d](https://github.com/ReVanced/revanced-api/commit/541783d9599c257f184d1b244e1b857b7c200227))
# [1.0.0-dev.6](https://github.com/ReVanced/revanced-api/compare/v1.0.0-dev.5...v1.0.0-dev.6) (2024-07-13)
### Features
* Add bio field for team members ([c40d50c](https://github.com/ReVanced/revanced-api/commit/c40d50c01368cb6b9ab06857694ec51d27aba2cb))
# [1.0.0-dev.5](https://github.com/ReVanced/revanced-api/compare/v1.0.0-dev.4...v1.0.0-dev.5) (2024-07-12)
### Bug Fixes
* Move robots.txt to root ([2ade550](https://github.com/ReVanced/revanced-api/commit/2ade550d58c0e4b53fa7417bef0064f4f476aed8))
* Only list public members ([97a5d11](https://github.com/ReVanced/revanced-api/commit/97a5d119ec415f9c25fbc50cb240603047defc73))
### Features
* Add configuration to specify public key id ([ad7d4b2](https://github.com/ReVanced/revanced-api/commit/ad7d4b226f71fd965421dc2f2a51825c8e8b3036))
* Add manager route ([f814fe5](https://github.com/ReVanced/revanced-api/commit/f814fe5825eb8b864f2b0cba53ff721eba577eb4))
# [1.0.0-dev.4](https://github.com/ReVanced/revanced-api/compare/v1.0.0-dev.3...v1.0.0-dev.4) (2024-07-11)
### Features
* List more repository contributors ([19ebc82](https://github.com/ReVanced/revanced-api/commit/19ebc827bfb54a597dd06f9d99bdc820ee9977ee))
# [1.0.0-dev.3](https://github.com/ReVanced/revanced-api/compare/v1.0.0-dev.2...v1.0.0-dev.3) (2024-07-11)
### Bug Fixes
* Move old API endpoint configuration from env to configuration file ([7e99e49](https://github.com/ReVanced/revanced-api/commit/7e99e49af202c4ec0a0d7e61dd0182dd2097e867))
# [1.0.0-dev.2](https://github.com/ReVanced/revanced-api/compare/v1.0.0-dev.1...v1.0.0-dev.2) (2024-07-11)

View File

@ -68,19 +68,19 @@ API server for ReVanced.
## ❓ About
ReVanced API is a server that is used as the backend for ReVanced.
ReVanced API acts as the data source for [ReVanced Website](https://github.com/ReVanced/revanced-website) and
powers [ReVanced Manager](https://github.com/ReVanced/revanced-manager)
ReVanced API acts as the data source for [ReVanced Website](https://github.com/ReVanced/revanced-website) and powers [ReVanced Manager](https://github.com/ReVanced/revanced-manager)
with updates and ReVanced Patches.
## 💪 Features
Some of the features ReVanced API include:
- 📢 **Announcements**: Post and get announcements
- 📢 **Announcements**: Post and get announcements grouped by channels
- **About**: Get more information such as a description, ways to donate to,
and links of the hoster of ReVanced API
and links of the hoster of ReVanced API
- 🧩 **Patches**: Get the latest updates of ReVanced Patches, directly from ReVanced API
- 👥 **Contributors**: List all contributors involved in the project
- 🔄 **Backwards compatibility**: Proxy an old API for migration purposes and backwards compatibility
## 🚀 How to get started
@ -90,35 +90,26 @@ ReVanced API can be deployed as a Docker container or used standalone.
To deploy ReVanced API as a Docker container, you can use Docker Compose or Docker CLI.
The Docker image is published on GitHub Container registry,
so before you can pull the image, you need
to [authenticate to the Container registry](https://docs.github.com/en/packages/working-with-a-github-packages-registry/working-with-the-container-registry#authenticating-to-the-container-registry).
so before you can pull the image, you need to [authenticate to the Container registry](https://docs.github.com/en/packages/working-with-a-github-packages-registry/working-with-the-container-registry#authenticating-to-the-container-registry).
### 🗄️ Docker Compose
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 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
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
### 💻 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. Create an `about.json` file using [about.example.json](about.example.json) as a template
4. Start the container using the following command:
3. 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 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
@ -140,9 +131,8 @@ A Java Runtime Environment (JRE) must be installed.
1. [Download](https://github.com/ReVanced/revanced-api/releases/latest) ReVanced API to a folder
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. 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
using [configuration.example.toml](configuration.example.toml) as a template
4. Run `java -jar revanced-api.jar start` to start the server
### 🛠️ From source
@ -151,15 +141,13 @@ 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. 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
4. Run `gradlew run --args=start` to start the server
## 📚 Everything else
### 📙 Contributing
Thank you for considering contributing to ReVanced API. You can find the contribution
guidelines [here](CONTRIBUTING.md).
Thank you for considering contributing to ReVanced API. You can find the contribution guidelines [here](CONTRIBUTING.md).
### 🛠️ Building

View File

@ -48,12 +48,6 @@ kotlin {
}
}
tasks {
test {
useJUnitPlatform()
}
}
repositories {
mavenCentral()
google()
@ -104,8 +98,6 @@ 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.

View File

@ -1,29 +1,17 @@
api-version = "v1"
organization = "revanced"
patches = { repository = "revanced-patches", asset-regex = "jar$", signature-asset-regex = "asc$", public-key-file = "patches-public-key.asc" }
integrations = { repository = "revanced-integrations", asset-regex = "apk$", signature-asset-regex = "asc$", public-key-file = "integrations-public-key.asc" }
contributors-repositories = [
"revanced-patcher",
"revanced-patches",
"revanced-integrations",
"revanced-website",
"revanced-cli",
"revanced-manager",
]
api-version = 1
cors-allowed-hosts = [
"revanced.app",
"*.revanced.app"
]
endpoint = "https://api.revanced.app"
static-files-path = "static/root"
versioned-static-files-path = "static/versioned"
backend-service-name = "GitHub"
about-json-file-path = "about.json"
organization = "revanced"
[patches]
repository = "revanced-patches"
asset-regex = "rvp$"
signature-asset-regex = "asc$"
public-key-file = "static/root/keys.asc"
public-key-id = 3897925568445097277
[manager]
repository = "revanced-manager"
asset-regex = "apk$"
[contributors-repositories]
revanced-patcher = "ReVanced Patcher"
revanced-patches = "ReVanced Patches"
revanced-website = "ReVanced Website"
revanced-cli = "ReVanced CLI"
revanced-manager = "ReVanced Manager"

14
configuration.toml Normal file
View File

@ -0,0 +1,14 @@
organization = "revanced"
patches = { repository = "revanced-patches", asset-regex = "jar$", signature-asset-regex = "asc$", public-key-file = "key.asc" }
integrations = { repository = "revanced-integrations", asset-regex = "apk$", signature-asset-regex = "asc$", public-key-file = "key.asc" }
contributors-repositories = [
"revanced-patcher",
"revanced-patches",
"revanced-integrations",
"revanced-website",
"revanced-cli",
"revanced-manager",
]
api-version = 1
cors = { host = "*.127.0.0.1:8888", sub-domains = [] }
endpoint = "http://127.0.0.1:8888/"

View File

@ -7,10 +7,9 @@ services:
- /data/revanced-api/.env:/app/.env
- /data/revanced-api/configuration.toml:/app/configuration.toml
- /data/revanced-api/patches-public-key.asc:/app/patches-public-key.asc
- /data/revanced-api/static:/app/static
- /data/revanced-api/about.json:/app/about.json
- /data/revanced-api/integrations-public-key.asc:/app/integrations-public-key.asc
environment:
- COMMAND=start
ports:
- "8888:8888"
- 8888:8888
restart: unless-stopped

View File

@ -1,4 +1,4 @@
org.gradle.parallel = true
org.gradle.caching = true
kotlin.code.style = official
version = 1.6.0
version = 1.0.0-dev.2

View File

@ -1,6 +1,6 @@
[versions]
kompendium-core = "3.14.4"
kotlin = "2.0.20"
kotlin = "2.0.0"
logback = "1.5.6"
exposed = "0.52.0"
h2 = "2.2.224"
@ -10,8 +10,8 @@ ktor = "2.3.7"
ktoml = "0.5.2"
picocli = "4.7.6"
datetime = "0.6.0"
revanced-patcher = "21.0.0"
revanced-library = "3.0.2"
revanced-patcher = "19.3.1"
revanced-library = "2.3.0"
caffeine = "3.1.8"
bouncy-castle = "1.78.1"

Binary file not shown.

View File

@ -1,7 +1,7 @@
distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists
distributionSha256Sum=d725d707bfabd4dfdc958c624003b3c80accc03f7037b5122c4b1d0ef15cecab
distributionUrl=https\://services.gradle.org/distributions/gradle-8.9-bin.zip
distributionSha256Sum=a4b4158601f8636cdeeab09bd76afb640030bb5b144aafe261a5e8af027dc612
distributionUrl=https\://services.gradle.org/distributions/gradle-8.8-bin.zip
networkTimeout=10000
validateDistributionUrl=true
zipStoreBase=GRADLE_USER_HOME

5
gradlew vendored
View File

@ -15,8 +15,6 @@
# See the License for the specific language governing permissions and
# limitations under the License.
#
# SPDX-License-Identifier: Apache-2.0
#
##############################################################################
#
@ -86,8 +84,7 @@ 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 -P "${APP_HOME:-./}" > /dev/null && printf '%s
' "$PWD" ) || exit
APP_HOME=$( cd "${APP_HOME:-./}" > /dev/null && pwd -P ) || exit
# Use the maximum available, or set MAX_FD != -1 to use that value.
MAX_FD=maximum

2
gradlew.bat vendored
View File

@ -13,8 +13,6 @@
@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 ##########################################################################

3604
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@ -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.10.1",
"semantic-release": "^24.1.2"
"gradle-semantic-release-plugin": "^1.9.2",
"semantic-release": "^24.0.0"
}
}

View File

@ -1,7 +1,6 @@
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
@ -34,8 +33,6 @@ internal object StartAPICommand : Runnable {
private var configFile = File("configuration.toml")
override fun run() {
Dotenv.configure().systemProperties().load()
embeddedServer(Jetty, port, host) {
configureDependencies(configFile)
configureHTTP()

View File

@ -1,170 +0,0 @@
package app.revanced.api.configuration
import kotlinx.datetime.Clock
import kotlinx.datetime.LocalDateTime
import kotlinx.datetime.TimeZone
import kotlinx.datetime.toLocalDateTime
import kotlinx.serialization.Serializable
interface ApiUser {
val name: String
val avatarUrl: String
val url: String
}
@Serializable
class ApiMember(
override val name: String,
override val avatarUrl: String,
override val url: String,
val bio: String?,
val gpgKey: ApiGpgKey?,
) : ApiUser
@Serializable
class ApiGpgKey(
val id: String,
val url: String,
)
@Serializable
class ApiContributor(
override val name: String,
override val avatarUrl: String,
override val url: String,
val contributions: Int,
) : ApiUser
@Serializable
class APIContributable(
val name: String,
val url: String,
// Using a list instead of a set because set semantics are unnecessary here.
val contributors: List<ApiContributor>,
)
@Serializable
class ApiRelease(
val version: String,
val createdAt: LocalDateTime,
val description: String,
val downloadUrl: String,
val signatureDownloadUrl: String? = null,
)
@Serializable
class ApiReleaseVersion(
val version: String,
)
@Serializable
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 attachments: List<String>? = null,
// Using a list instead of a set because set semantics are unnecessary here.
val tags: List<String>? = null,
val createdAt: LocalDateTime = Clock.System.now().toLocalDateTime(TimeZone.currentSystemDefault()),
val archivedAt: LocalDateTime? = null,
val level: Int = 0,
)
@Serializable
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 attachments: List<String>? = null,
// Using a list instead of a set because set semantics are unnecessary here.
val tags: List<String>? = null,
val createdAt: LocalDateTime,
val archivedAt: LocalDateTime? = null,
val level: Int = 0,
)
@Serializable
class ApiResponseAnnouncementId(
val id: Int,
)
@Serializable
class ApiAnnouncementArchivedAt(
val archivedAt: LocalDateTime,
)
@Serializable
class ApiAnnouncementTag(
val name: String,
)
@Serializable
class ApiRateLimit(
val limit: Int,
val remaining: Int,
val reset: LocalDateTime,
)
@Serializable
class ApiAssetPublicKey(
val patchesPublicKey: String,
)
@Serializable
class APIAbout(
val name: String,
val about: String,
val keys: String,
val branding: Branding?,
val contact: Contact?,
// Using a list instead of a set because set semantics are unnecessary here.
val socials: List<Social>?,
val donations: Donations?,
val status: String,
) {
@Serializable
class Branding(
val logo: String,
)
@Serializable
class Contact(
val email: String,
)
@Serializable
class Social(
val name: String,
val url: String,
val preferred: Boolean? = false,
)
@Serializable
class Wallet(
val network: String,
val currencyCode: String,
val address: String,
val preferred: Boolean? = false,
)
@Serializable
class Link(
val name: String,
val url: String,
val preferred: Boolean? = false,
)
@Serializable
class Donations(
// Using a list instead of a set because set semantics are unnecessary here.
val wallets: List<Wallet>?,
// Using a list instead of a set because set semantics are unnecessary here.
val links: List<Link>?,
)
}
@Serializable
class ApiToken(val token: String)

View File

@ -5,61 +5,140 @@ import app.revanced.api.configuration.repository.BackendRepository
import app.revanced.api.configuration.repository.ConfigurationRepository
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.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.*
import io.ktor.client.plugins.auth.*
import io.ktor.client.plugins.auth.providers.*
import io.ktor.client.plugins.cache.*
import io.ktor.client.plugins.contentnegotiation.*
import io.ktor.client.plugins.resources.*
import io.ktor.serialization.kotlinx.json.*
import io.ktor.server.application.*
import kotlinx.serialization.ExperimentalSerializationApi
import kotlinx.serialization.json.Json
import kotlinx.serialization.json.JsonNamingStrategy
import org.jetbrains.exposed.sql.Database
import org.jetbrains.exposed.sql.transactions.TransactionManager
import org.koin.core.module.dsl.singleOf
import org.koin.core.parameter.parameterArrayOf
import org.koin.dsl.module
import org.koin.ktor.plugin.Koin
import java.io.File
@OptIn(ExperimentalSerializationApi::class)
fun Application.configureDependencies(
configFile: File,
) {
val repositoryModule = module {
single<ConfigurationRepository> { Toml.decodeFromStream(configFile.inputStream()) }
val globalModule = module {
single {
Database.connect(
url = System.getProperty("DB_URL"),
user = System.getProperty("DB_USER"),
password = System.getProperty("DB_PASSWORD"),
Dotenv.configure().load()
}
factory { params ->
val defaultRequestUri: String = params.get<String>()
val configBlock = params.getOrNull<(HttpClientConfig<OkHttpConfig>.() -> Unit)>() ?: {}
HttpClient(OkHttp) {
defaultRequest { url(defaultRequestUri) }
configBlock()
}
}
}
val repositoryModule = module {
single<BackendRepository> {
GitHubBackendRepository(
get {
val defaultRequestUri = "https://api.github.com"
val configBlock: HttpClientConfig<OkHttpConfig>.() -> Unit = {
install(HttpCache)
install(Resources)
install(ContentNegotiation) {
json(
Json {
ignoreUnknownKeys = true
namingStrategy = JsonNamingStrategy.SnakeCase
},
)
}
get<Dotenv>()["BACKEND_API_TOKEN"]?.let {
install(Auth) {
bearer {
loadTokens {
BearerTokens(
accessToken = it,
refreshToken = "", // Required dummy value
)
}
sendWithoutRequest { true }
}
}
}
}
parameterArrayOf(defaultRequestUri, configBlock)
},
)
}
singleOf(::AnnouncementRepository)
singleOf(::GitHubBackendRepository)
single<BackendRepository> {
val backendServices = mapOf(
GitHubBackendRepository.SERVICE_NAME to { get<GitHubBackendRepository>() },
// Implement more backend services here.
single<ConfigurationRepository> {
Toml.decodeFromStream(configFile.inputStream())
}
single {
val dotenv = get<Dotenv>()
TransactionManager.defaultDatabase = Database.connect(
url = dotenv["DB_URL"],
user = dotenv["DB_USER"],
password = dotenv["DB_PASSWORD"],
)
val configuration = get<ConfigurationRepository>()
val backendFactory = backendServices[configuration.backendServiceName]!!
backendFactory()
AnnouncementRepository()
}
}
val serviceModule = module {
single {
val jwtSecret = System.getProperty("JWT_SECRET")
val issuer = System.getProperty("JWT_ISSUER")
val validityInMin = System.getProperty("JWT_VALIDITY_IN_MIN").toLong()
val dotenv = get<Dotenv>()
val authSHA256DigestString = System.getProperty("AUTH_SHA256_DIGEST")
val jwtSecret = dotenv["JWT_SECRET"]
val issuer = dotenv["JWT_ISSUER"]
val validityInMin = dotenv["JWT_VALIDITY_IN_MIN"].toInt()
AuthenticationService(issuer, validityInMin, jwtSecret, authSHA256DigestString)
val authSHA256DigestString = dotenv["AUTH_SHA256_DIGEST"]
AuthService(issuer, validityInMin, jwtSecret, authSHA256DigestString)
}
single {
OldApiService(
get {
val defaultRequestUri = get<Dotenv>()["OLD_API_URL"]
parameterArrayOf(defaultRequestUri)
},
)
}
singleOf(::AnnouncementService)
singleOf(::SignatureService)
singleOf(::PatchesService)
singleOf(::ManagerService)
singleOf(::ApiService)
}
install(Koin) {
modules(
globalModule,
repositoryModule,
serviceModule,
)

View File

@ -1,16 +1,11 @@
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.*
import io.ktor.server.application.*
import io.ktor.server.http.content.*
import io.ktor.server.plugins.cachingheaders.*
import io.ktor.server.response.*
import io.ktor.server.routing.*
import java.io.File
import java.nio.file.Path
import kotlin.time.Duration
internal suspend fun ApplicationCall.respondOrNotFound(value: Any?) = respond(value ?: HttpStatusCode.NotFound)
@ -30,22 +25,3 @@ internal fun ApplicationCallPipeline.installCache(cacheControl: CacheControl) =
internal fun ApplicationCallPipeline.installNotarizedRoute(configure: NotarizedRoute.Config.() -> Unit = {}) =
install(NotarizedRoute(), configure)
internal fun Route.staticFiles(
remotePath: String,
dir: Path,
block: StaticContentConfig<File>.() -> Unit = {
contentType {
ContentType.Application.Json
}
extensions("json")
},
) = staticFiles(remotePath, dir.toFile(), null, block)
internal fun MethodInfo.Builder<*>.canRespondUnauthorized() {
canRespond {
responseCode(HttpStatusCode.Unauthorized)
description("Unauthorized")
responseType<Unit>()
}
}

View File

@ -1,7 +1,6 @@
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.*
@ -11,19 +10,14 @@ import org.koin.ktor.ext.get
import kotlin.time.Duration.Companion.minutes
fun Application.configureHTTP() {
val configuration = get<ConfigurationRepository>()
val configurationRepository = get<ConfigurationRepository>()
install(CORS) {
HttpMethod.DefaultMethods.minus(HttpMethod.Options).forEach(::allowMethod)
allowHeader(HttpHeaders.ContentType)
allowHeader(HttpHeaders.Authorization)
exposeHeader(HttpHeaders.WWWAuthenticate)
allowCredentials = true
configuration.corsAllowedHosts.forEach { host ->
allowHost(host = host, schemes = listOf("https"))
configurationRepository.corsAllowedHosts.forEach { host ->
allowHost(
host = host,
schemes = listOf("http", "https")
)
}
}

View File

@ -2,7 +2,6 @@ package app.revanced.api.configuration
import app.revanced.api.command.applicationVersion
import app.revanced.api.configuration.repository.ConfigurationRepository
import io.bkbn.kompendium.core.attribute.KompendiumAttributes
import io.bkbn.kompendium.core.plugin.NotarizedApplication
import io.bkbn.kompendium.json.schema.KotlinXSchemaConfigurator
import io.bkbn.kompendium.oas.OpenApiSpec
@ -13,22 +12,13 @@ import io.bkbn.kompendium.oas.info.License
import io.bkbn.kompendium.oas.security.BearerAuth
import io.bkbn.kompendium.oas.server.Server
import io.ktor.server.application.*
import io.ktor.server.response.*
import io.ktor.server.routing.*
import org.koin.ktor.ext.get
import java.net.URI
import org.koin.ktor.ext.get as koinGet
internal fun Application.configureOpenAPI() {
val configuration = koinGet<ConfigurationRepository>()
val configurationRepository = get<ConfigurationRepository>()
install(NotarizedApplication()) {
openApiJson = {
route("/${configuration.apiVersion}/openapi.json") {
get {
call.respond(application.attributes[KompendiumAttributes.openApiSpec])
}
}
}
spec = OpenApiSpec(
info = Info(
title = "ReVanced API",
@ -51,7 +41,7 @@ internal fun Application.configureOpenAPI() {
),
).apply {
servers += Server(
url = URI(configuration.endpoint),
url = URI(configurationRepository.endpoint),
description = "ReVanced API server",
)
}

View File

@ -1,13 +1,12 @@
package app.revanced.api.configuration
import app.revanced.api.configuration.repository.ConfigurationRepository
import app.revanced.api.configuration.routes.*
import app.revanced.api.configuration.routes.announcementsRoute
import app.revanced.api.configuration.routes.apiRoute
import app.revanced.api.configuration.routes.oldApiRoute
import app.revanced.api.configuration.routes.patchesRoute
import app.revanced.api.configuration.routes.rootRoute
import io.bkbn.kompendium.core.routes.redoc
import io.bkbn.kompendium.core.routes.swagger
import io.ktor.http.*
import io.ktor.server.application.*
import io.ktor.server.routing.*
import kotlin.time.Duration.Companion.minutes
@ -18,41 +17,15 @@ internal fun Application.configureRouting() = routing {
installCache(5.minutes)
route("/${configuration.apiVersion}") {
announcementsRoute()
route("/v${configuration.apiVersion}") {
patchesRoute()
managerRoute()
apiRoute()
announcementsRoute()
rootRoute()
}
staticFiles("/", configuration.staticFilesPath) {
contentType {
when (it.extension) {
"json" -> ContentType.Application.Json
"asc" -> ContentType.Text.Plain
"ico" -> ContentType.Image.XIcon
"svg" -> ContentType.Image.SVG
"jpg", "jpeg" -> ContentType.Image.JPEG
"png" -> ContentType.Image.PNG
"gif" -> ContentType.Image.GIF
"mp4" -> ContentType.Video.MP4
"ogg" -> ContentType.Video.OGG
"mp3" -> ContentType.Audio.MPEG
"css" -> ContentType.Text.CSS
"js" -> ContentType.Application.JavaScript
"html" -> ContentType.Text.Html
"xml" -> ContentType.Application.Xml
"pdf" -> ContentType.Application.Pdf
"zip" -> ContentType.Application.Zip
"gz" -> ContentType.Application.GZip
else -> ContentType.Application.OctetStream
}
}
swagger(pageTitle = "ReVanced API", path = "/")
redoc(pageTitle = "ReVanced API", path = "/redoc")
extensions("json", "asc")
}
val specUrl = "/${configuration.apiVersion}/openapi.json"
swagger(pageTitle = "ReVanced API", path = "/", specUrl = specUrl)
redoc(pageTitle = "ReVanced API", path = "/redoc", specUrl = specUrl)
// TODO: Remove, once migration period from v2 API is over (In 1-2 years).
oldApiRoute()
}

View File

@ -1,17 +1,9 @@
package app.revanced.api.configuration
import app.revanced.api.configuration.services.AuthenticationService
import app.revanced.api.configuration.services.AuthService
import io.ktor.server.application.*
import io.ktor.server.auth.*
import org.koin.ktor.ext.get
fun Application.configureSecurity() {
val authenticationService = get<AuthenticationService>()
install(Authentication) {
with(authenticationService) {
jwt()
digest()
}
}
get<AuthService>().configureSecurity(this)
}

View File

@ -1,11 +1,12 @@
package app.revanced.api.configuration.repository
import app.revanced.api.configuration.ApiAnnouncement
import app.revanced.api.configuration.ApiAnnouncementTag
import app.revanced.api.configuration.ApiResponseAnnouncement
import app.revanced.api.configuration.ApiResponseAnnouncementId
import app.revanced.api.configuration.schema.APIAnnouncement
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
import org.jetbrains.exposed.dao.IntEntityClass
import org.jetbrains.exposed.dao.id.EntityID
@ -14,178 +15,136 @@ 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
internal class AnnouncementRepository(private val database: Database) {
// This is better than doing a maxByOrNull { it.id } on every request.
internal class AnnouncementRepository {
// This is better than doing a maxByOrNull { it.id }.
private var latestAnnouncement: Announcement? = null
private val latestAnnouncementByTag = mutableMapOf<String, Announcement>()
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
}
}
init {
runBlocking {
transaction {
SchemaUtils.create(
Announcements,
Attachments,
Tags,
AnnouncementTags,
)
SchemaUtils.create(Announcements, Attachments)
initializeLatestAnnouncements()
// Initialize the latest announcement.
latestAnnouncement = Announcement.all().onEach {
latestAnnouncementByChannel[it.channel ?: return@onEach] = it
}.maxByOrNull { it.id } ?: return@transaction
}
}
}
private fun initializeLatestAnnouncements() {
latestAnnouncement = Announcement.all().orderBy(Announcements.id to SortOrder.DESC).firstOrNull()
Tag.all().map { it.name }.forEach(::updateLatestAnnouncementForTag)
suspend fun all() = transaction {
Announcement.all().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.name] = new }
}
}
private fun updateLatestAnnouncementForTag(tag: String) {
val latestAnnouncementForTag = Tags.innerJoin(AnnouncementTags)
.select(AnnouncementTags.announcement)
.where { Tags.name eq tag }
.orderBy(AnnouncementTags.announcement to SortOrder.DESC)
.limit(1)
.firstNotNullOfOrNull { Announcement.findById(it[AnnouncementTags.announcement]) }
latestAnnouncementForTag?.let { latestAnnouncementByTag[tag] = it }
}
suspend fun latest() = transaction {
latestAnnouncement.toApiResponseAnnouncement()
}
suspend fun latest(tags: Set<String>) = transaction {
tags.mapNotNull { tag -> latestAnnouncementByTag[tag] }.toApiAnnouncement()
}
fun latestId() = latestAnnouncement?.id?.value.toApiResponseAnnouncementId()
fun latestId(tags: Set<String>) = tags.map { tag -> latestAnnouncementByTag[tag]?.id?.value }.toApiResponseAnnouncementId()
suspend fun paged(cursor: Int, count: Int, tags: Set<String>?) = transaction {
Announcement.find {
fun idLessEq() = Announcements.id lessEq cursor
if (tags == null) {
idLessEq()
} else {
fun hasTags() = Announcements.id inSubQuery (
AnnouncementTags.innerJoin(Tags)
.select(AnnouncementTags.announcement)
.withDistinct()
.where { Tags.name inList tags }
)
idLessEq() 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
createdAt = new.createdAt
archivedAt = new.archivedAt
level = new.level
if (new.tags != null) {
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.createdAt = new.createdAt
it.archivedAt = new.archivedAt
it.level = new.level
if (new.tags != null) {
// 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.
if (new.attachments != null) {
it.attachments.forEach { attachment -> attachment.delete() }
new.attachments.map { attachment ->
Attachment.new {
url = attachment
announcement = it
}
}
}
}?.let(::updateLatestAnnouncement) ?: Unit
suspend fun all(channel: String) = transaction {
Announcement.find { Announcements.channel eq channel }.map { it.toApi() }
}
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.name
tag.delete()
}
announcement.delete()
// If the deleted announcement is the latest announcement, set the new latest announcement.
// In case the latest announcement was deleted, query the new latest announcement again.
if (latestAnnouncement?.id?.value == id) {
latestAnnouncement = Announcement.all().orderBy(Announcements.id to SortOrder.DESC).firstOrNull()
}
latestAnnouncement = Announcement.all().maxByOrNull { it.id }
// 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)
// 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!!
}
}
}
suspend fun tags() = transaction {
Tag.all().toList().toApiTag()
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)
}
private suspend fun <T> transaction(statement: suspend Transaction.() -> T) = newSuspendedTransaction(Dispatchers.IO, database, statement = statement)
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)
}
private suspend fun <T> transaction(statement: suspend Transaction.() -> T) =
newSuspendedTransaction(Dispatchers.IO, statement = statement)
private object Announcements : IntIdTable() {
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")
@ -196,19 +155,6 @@ internal class AnnouncementRepository(private val database: Database) {
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)
@ -216,7 +162,7 @@ internal class AnnouncementRepository(private val database: Database) {
var title by Announcements.title
var content by Announcements.content
val attachments by Attachment referrersOn Attachments.announcement
var tags by Tag via AnnouncementTags
var channel by Announcements.channel
var createdAt by Announcements.createdAt
var archivedAt by Announcements.archivedAt
var level by Announcements.level
@ -229,32 +175,17 @@ internal class AnnouncementRepository(private val database: Database) {
var announcement by Announcement referencedOn Attachments.announcement
}
class Tag(id: EntityID<Int>) : IntEntity(id) {
companion object : IntEntityClass<Tag>(Tags)
private fun Announcement.toApi() = APIResponseAnnouncement(
id.value,
author,
title,
content,
attachments.map { it.url },
channel,
createdAt,
archivedAt,
level,
)
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.name },
createdAt,
archivedAt,
level,
)
}
private fun Iterable<Announcement>.toApiAnnouncement() = map { it.toApiResponseAnnouncement()!! }
private fun Iterable<Tag>.toApiTag() = map { ApiAnnouncementTag(it.name) }
private fun Int?.toApiResponseAnnouncementId() = this?.let { ApiResponseAnnouncementId(this) }
private fun Iterable<Int?>.toApiResponseAnnouncementId() = map { it.toApiResponseAnnouncementId() }
private fun Int.toApi() = APIResponseAnnouncementId(this)
}

View File

@ -1,59 +1,16 @@
package app.revanced.api.configuration.repository
import io.ktor.client.*
import io.ktor.client.engine.okhttp.*
import io.ktor.client.plugins.*
import io.ktor.client.plugins.auth.*
import io.ktor.client.plugins.auth.providers.*
import io.ktor.client.plugins.cache.*
import io.ktor.client.plugins.contentnegotiation.*
import io.ktor.client.plugins.resources.*
import io.ktor.serialization.kotlinx.json.*
import kotlinx.datetime.LocalDateTime
import kotlinx.serialization.json.Json
import kotlinx.serialization.json.JsonNamingStrategy
/**
* The backend of the API used to get data.
*
* @param defaultRequestUri The URI to use for requests.
* @param website The site of the backend users can visit.
* @param client The HTTP client to use for requests.
*/
abstract class BackendRepository internal constructor(
defaultRequestUri: String,
internal val website: String,
protected val client: HttpClient,
) {
protected val client: HttpClient = HttpClient(OkHttp) {
defaultRequest { url(defaultRequestUri) }
install(HttpCache)
install(Resources)
install(ContentNegotiation) {
json(
Json {
ignoreUnknownKeys = true
@Suppress("OPT_IN_USAGE")
namingStrategy = JsonNamingStrategy.SnakeCase
},
)
}
System.getProperty("BACKEND_API_TOKEN")?.let {
install(Auth) {
bearer {
loadTokens {
BearerTokens(
accessToken = it,
refreshToken = "", // Required dummy value
)
}
sendWithoutRequest { true }
}
}
}
}
/**
* A user.
*
@ -135,14 +92,12 @@ abstract class BackendRepository internal constructor(
* @property tag The tag of the release.
* @property assets The assets of the release.
* @property createdAt The date and time the release was created.
* @property prerelease Whether the release is a prerelease.
* @property releaseNote The release note of the release.
*/
class BackendRelease(
val tag: String,
val releaseNote: String,
val createdAt: LocalDateTime,
val prerelease: Boolean,
// Using a list instead of a set because set semantics are unnecessary here.
val assets: List<BackendAsset>,
) {
@ -182,13 +137,13 @@ abstract class BackendRepository internal constructor(
*
* @param owner The owner of the repository.
* @param repository The name of the repository.
* @param prerelease Whether to get a prerelease.
* @param tag The tag of the release. If null, the latest release is returned.
* @return The release.
*/
abstract suspend fun release(
owner: String,
repository: String,
prerelease: Boolean,
tag: String? = null,
): BackendOrganization.BackendRepository.BackendRelease
/**
@ -198,10 +153,7 @@ abstract class BackendRepository internal constructor(
* @param repository The name of the repository.
* @return The contributors.
*/
abstract suspend fun contributors(
owner: String,
repository: String,
): List<BackendOrganization.BackendRepository.BackendContributor>
abstract suspend fun contributors(owner: String, repository: String): List<BackendOrganization.BackendRepository.BackendContributor>
/**
* Get the members of an organization.

View File

@ -1,9 +1,6 @@
package app.revanced.api.configuration.repository
import app.revanced.api.configuration.APIAbout
import app.revanced.api.configuration.services.ManagerService
import app.revanced.api.configuration.services.PatchesService
import kotlinx.serialization.ExperimentalSerializationApi
import kotlinx.serialization.KSerializer
import kotlinx.serialization.SerialName
import kotlinx.serialization.Serializable
@ -12,62 +9,36 @@ import kotlinx.serialization.descriptors.PrimitiveSerialDescriptor
import kotlinx.serialization.descriptors.SerialDescriptor
import kotlinx.serialization.encoding.Decoder
import kotlinx.serialization.encoding.Encoder
import kotlinx.serialization.json.Json
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.
*
* @property organization The API backends organization name where the repositories are.
* @property organization The API backends organization name where the repositories for the patches and integrations are.
* @property patches The source of the patches.
* @property manager The source of the manager.
* @property contributorsRepositoryNames The friendly name of repos mapped to the repository names to get contributors from.
* @property backendServiceName The name of the backend service to use for the repositories, contributors, etc.
* @property integrations The source of the integrations.
* @property contributorsRepositoryNames The names of the repositories to get contributors from.
* @property apiVersion The version to use for the API.
* @property corsAllowedHosts The hosts allowed to make requests to the API.
* @property endpoint The endpoint of the API.
* @property staticFilesPath The path to the static files to be served under the root path.
* @property versionedStaticFilesPath The path to the static files to be served under a versioned path.
* @property about The path to the json file deserialized to [APIAbout]
* (because com.akuleshov7.ktoml.Toml does not support nested tables).
*/
@Serializable
internal class ConfigurationRepository(
val organization: String,
val patches: SignedAssetConfiguration,
val manager: AssetConfiguration,
val patches: AssetConfiguration,
val integrations: AssetConfiguration,
@SerialName("contributors-repositories")
val contributorsRepositoryNames: Map<String, String>,
@SerialName("backend-service-name")
val backendServiceName: String,
val contributorsRepositoryNames: Set<String>,
@SerialName("api-version")
val apiVersion: String = "v1",
val apiVersion: Int = 1,
@SerialName("cors-allowed-hosts")
val corsAllowedHosts: Set<String>,
val endpoint: String,
@Serializable(with = PathSerializer::class)
@SerialName("static-files-path")
val staticFilesPath: Path,
@Serializable(with = PathSerializer::class)
@SerialName("versioned-static-files-path")
val versionedStaticFilesPath: Path,
@Serializable(with = AboutSerializer::class)
@SerialName("about-json-file-path")
val about: APIAbout,
) {
init {
staticFilesPath.createDirectories()
versionedStaticFilesPath.createDirectories()
}
/**
* Am asset configuration whose asset is signed.
* An asset configuration.
*
* [PatchesService] for example uses [BackendRepository] to get assets from its releases.
* [PatchesService] uses [BackendRepository] to get assets from its releases.
* A release contains multiple assets.
*
* This configuration is used in [ConfigurationRepository]
@ -77,10 +48,9 @@ internal class ConfigurationRepository(
* @property assetRegex The regex matching the asset name.
* @property signatureAssetRegex The regex matching the signature asset name to verify the asset.
* @property publicKeyFile The public key file to verify the signature of the asset.
* @property publicKeyId The ID of the public key to verify the signature of the asset.
*/
@Serializable
internal class SignedAssetConfiguration(
internal class AssetConfiguration(
val repository: String,
@Serializable(with = RegexSerializer::class)
@SerialName("asset-regex")
@ -91,28 +61,6 @@ internal class ConfigurationRepository(
@Serializable(with = FileSerializer::class)
@SerialName("public-key-file")
val publicKeyFile: File,
@SerialName("public-key-id")
val publicKeyId: Long,
)
/**
* Am asset configuration.
*
* [ManagerService] for example uses [BackendRepository] to get assets from its releases.
* A release contains multiple assets.
*
* This configuration is used in [ConfigurationRepository]
* to determine which release assets from repositories to get and to verify them.
*
* @property repository The repository in which releases are made to get an asset.
* @property assetRegex The regex matching the asset name.
*/
@Serializable
internal class AssetConfiguration(
val repository: String,
@Serializable(with = RegexSerializer::class)
@SerialName("asset-regex")
val assetRegex: Regex,
)
}
@ -131,23 +79,3 @@ private object FileSerializer : KSerializer<File> {
override fun deserialize(decoder: Decoder) = File(decoder.decodeString())
}
private object PathSerializer : KSerializer<Path> {
override val descriptor: SerialDescriptor = PrimitiveSerialDescriptor("Path", PrimitiveKind.STRING)
override fun serialize(encoder: Encoder, value: Path) = encoder.encodeString(value.toString())
override fun deserialize(decoder: Decoder): Path = Path.of(decoder.decodeString())
}
private object AboutSerializer : KSerializer<APIAbout> {
override val descriptor: SerialDescriptor = PrimitiveSerialDescriptor("APIAbout", PrimitiveKind.STRING)
override fun serialize(encoder: Encoder, value: APIAbout) = error("Serializing APIAbout is not supported")
@OptIn(ExperimentalSerializationApi::class)
val json = Json { namingStrategy = JsonNamingStrategy.SnakeCase }
override fun deserialize(decoder: Decoder): APIAbout =
json.decodeFromStream(File(decoder.decodeString()).inputStream())
}

View File

@ -8,26 +8,24 @@ import app.revanced.api.configuration.repository.GitHubOrganization.GitHubReposi
import app.revanced.api.configuration.repository.GitHubOrganization.GitHubRepository.GitHubRelease
import app.revanced.api.configuration.repository.Organization.Repository.Contributors
import app.revanced.api.configuration.repository.Organization.Repository.Releases
import io.ktor.client.*
import io.ktor.client.call.*
import io.ktor.client.plugins.resources.*
import io.ktor.resources.*
import kotlinx.coroutines.async
import kotlinx.coroutines.awaitAll
import kotlinx.coroutines.coroutineScope
import kotlinx.coroutines.*
import kotlinx.datetime.Instant
import kotlinx.datetime.TimeZone
import kotlinx.datetime.toLocalDateTime
import kotlinx.serialization.SerialName
import kotlinx.serialization.Serializable
class GitHubBackendRepository : BackendRepository("https://api.github.com", "https://github.com") {
class GitHubBackendRepository(client: HttpClient) : BackendRepository(client) {
override suspend fun release(
owner: String,
repository: String,
prerelease: Boolean,
tag: String?,
): BackendRelease {
val release: GitHubRelease = if (prerelease) {
client.get(Releases(owner, repository)).body<List<GitHubRelease>>().first { it.prerelease }
val release: GitHubRelease = if (tag != null) {
client.get(Releases.Tag(owner, repository, tag)).body()
} else {
client.get(Releases.Latest(owner, repository)).body()
}
@ -36,7 +34,6 @@ class GitHubBackendRepository : BackendRepository("https://api.github.com", "htt
tag = release.tagName,
releaseNote = release.body,
createdAt = release.createdAt.toLocalDateTime(TimeZone.UTC),
prerelease = release.prerelease,
assets = release.assets.map {
BackendAsset(
name = it.name,
@ -69,11 +66,10 @@ class GitHubBackendRepository : BackendRepository("https://api.github.com", "htt
override suspend fun members(organization: String): List<BackendMember> {
// Get the list of members of the organization.
val publicMembers: List<GitHubOrganization.GitHubMember> =
client.get(Organization.PublicMembers(organization)).body()
val members: List<GitHubOrganization.GitHubMember> = client.get(Organization.Members(organization)).body()
return coroutineScope {
publicMembers.map { member ->
members.map { member ->
async {
awaitAll(
async {
@ -101,7 +97,7 @@ class GitHubBackendRepository : BackendRepository("https://api.github.com", "htt
gpgKeys =
BackendMember.GpgKeys(
ids = gpgKeys.map { it.keyId },
url = "https://github.com/${user.login}.gpg",
url = "https://api.github.com/users/${user.login}.gpg",
),
)
}
@ -116,10 +112,6 @@ class GitHubBackendRepository : BackendRepository("https://api.github.com", "htt
reset = Instant.fromEpochSeconds(rateLimit.rate.reset).toLocalDateTime(TimeZone.UTC),
)
}
companion object {
const val SERVICE_NAME = "GitHub"
}
}
interface IGitHubUser {
@ -164,7 +156,6 @@ class GitHubOrganization {
// Using a list instead of a set because set semantics are unnecessary here.
val assets: List<GitHubAsset>,
val createdAt: Instant,
val prerelease: Boolean,
val body: String,
) {
@Serializable
@ -195,15 +186,18 @@ class User(val login: String) {
}
class Organization {
@Resource("/orgs/{org}/public_members")
class PublicMembers(val org: String)
@Resource("/orgs/{org}/members")
class Members(val org: String)
class Repository {
@Resource("/repos/{owner}/{repo}/contributors")
class Contributors(val owner: String, val repo: String, @SerialName("per_page") val perPage: Int = 100)
class Contributors(val owner: String, val repo: String)
@Resource("/repos/{owner}/{repo}/releases")
class Releases(val owner: String, val repo: String) {
@Resource("/repos/{owner}/{repo}/releases/tags/{tag}")
class Tag(val owner: String, val repo: String, val tag: String)
@Resource("/repos/{owner}/{repo}/releases/latest")
class Latest(val owner: String, val repo: String)
}

View File

@ -1,12 +1,12 @@
package app.revanced.api.configuration.routes
import app.revanced.api.configuration.ApiAnnouncement
import app.revanced.api.configuration.ApiResponseAnnouncement
import app.revanced.api.configuration.ApiResponseAnnouncementId
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.services.AnnouncementService
import io.bkbn.kompendium.core.metadata.DeleteInfo
import io.bkbn.kompendium.core.metadata.GetInfo
@ -18,6 +18,7 @@ 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.*
@ -33,177 +34,139 @@ internal fun Route.announcementsRoute() = route("announcements") {
rateLimit(RateLimitName("strong")) {
get {
val cursor = call.parameters["cursor"]?.toInt() ?: Int.MAX_VALUE
val count = call.parameters["count"]?.toInt() ?: 16
val tags = call.parameters.getAll("tag")
call.respond(announcementService.paged(cursor, count, tags?.toSet()))
call.respond(announcementService.all())
}
}
rateLimit(RateLimitName("weak")) {
authenticate("jwt") {
post<ApiAnnouncement> { announcement ->
announcementService.new(announcement)
call.respond(HttpStatusCode.OK)
}
}
route("latest") {
installAnnouncementsLatestRouteDocumentation()
rateLimit(RateLimitName("strong")) {
route("{channel}/latest") {
installLatestChannelAnnouncementRouteDocumentation()
get {
val tags = call.parameters.getAll("tag")
val channel: String by call.parameters
if (tags?.isNotEmpty() == true) {
call.respond(announcementService.latest(tags.toSet()))
} else {
call.respondOrNotFound(announcementService.latest())
}
call.respondOrNotFound(announcementService.latest(channel))
}
route("id") {
installAnnouncementsLatestIdRouteDocumentation()
installLatestChannelAnnouncementIdRouteDocumentation()
get {
val tags = call.parameters.getAll("tag")
val channel: String by call.parameters
if (tags?.isNotEmpty() == true) {
call.respond(announcementService.latestId(tags.toSet()))
} else {
call.respondOrNotFound(announcementService.latestId())
}
call.respondOrNotFound(announcementService.latestId(channel))
}
}
}
}
route("{id}") {
installAnnouncementsIdRouteDocumentation()
rateLimit(RateLimitName("strong")) {
route("{channel}") {
installChannelAnnouncementsRouteDocumentation()
get {
val id: Int by call.parameters
val channel: String by call.parameters
call.respondOrNotFound(announcementService.get(id))
call.respond(announcementService.all(channel))
}
}
}
rateLimit(RateLimitName("strong")) {
route("latest") {
installLatestAnnouncementRouteDocumentation()
get {
call.respondOrNotFound(announcementService.latest())
}
authenticate("jwt") {
patch<ApiAnnouncement> { announcement ->
route("id") {
installLatestAnnouncementIdRouteDocumentation()
get {
call.respondOrNotFound(announcementService.latestId())
}
}
}
}
rateLimit(RateLimitName("strong")) {
authenticate("jwt") {
installAnnouncementRouteDocumentation()
post<APIAnnouncement> { announcement ->
announcementService.new(announcement)
}
route("{id}") {
installAnnouncementIdRouteDocumentation()
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("tags") {
installAnnouncementsTagsRouteDocumentation()
route("archive") {
installAnnouncementArchiveRouteDocumentation()
get {
call.respond(announcementService.tags())
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)
}
}
}
}
}
}
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 {
private fun Route.installAnnouncementRouteDocumentation() = 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.STRING,
description = "The tags to filter the announcements by. Default is all tags",
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("The announcement is created")
description("When the announcement was created")
responseCode(HttpStatusCode.OK)
responseType<Unit>()
}
canRespondUnauthorized()
}
}
private fun Route.installAnnouncementsLatestRouteDocumentation() = installNotarizedRoute {
private fun Route.installLatestAnnouncementRouteDocumentation() = 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.STRING,
description = "The tags to filter the latest announcements by",
required = false,
),
)
response {
responseCode(HttpStatusCode.OK)
mediaTypes("application/json")
description("The latest announcement")
responseType<ApiResponseAnnouncement>()
}
canRespond {
responseCode(HttpStatusCode.OK)
mediaTypes("application/json")
description("The latest announcements")
responseType<Set<ApiResponseAnnouncement>>()
responseType<APIResponseAnnouncement>()
}
canRespond {
responseCode(HttpStatusCode.NotFound)
@ -213,32 +176,17 @@ private fun Route.installAnnouncementsLatestRouteDocumentation() = installNotari
}
}
private fun Route.installAnnouncementsLatestIdRouteDocumentation() = installNotarizedRoute {
private fun Route.installLatestAnnouncementIdRouteDocumentation() = installNotarizedRoute {
tags = setOf("Announcements")
get = GetInfo.builder {
description("Get the ID of the latest announcement")
summary("Get ID of latest announcement")
parameters(
Parameter(
name = "tag",
`in` = Parameter.Location.query,
schema = TypeDefinition.STRING,
description = "The tags to filter the latest announcements by",
required = false,
),
)
description("Get the id of the latest announcement")
summary("Get id of latest announcement")
response {
responseCode(HttpStatusCode.OK)
mediaTypes("application/json")
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>>()
description("The id of the latest announcement")
responseType<APIResponseAnnouncementId>()
}
canRespond {
responseCode(HttpStatusCode.NotFound)
@ -248,7 +196,32 @@ private fun Route.installAnnouncementsLatestIdRouteDocumentation() = installNota
}
}
private fun Route.installAnnouncementsIdRouteDocumentation() = installNotarizedRoute {
private fun Route.installChannelAnnouncementsRouteDocumentation() = installNotarizedRoute {
tags = setOf("Announcements")
parameters = listOf(
Parameter(
name = "channel",
`in` = Parameter.Location.path,
schema = TypeDefinition.STRING,
description = "The channel to get the announcements from",
required = true,
),
)
get = GetInfo.builder {
description("Get the announcements from a channel")
summary("Get announcements from channel")
response {
responseCode(HttpStatusCode.OK)
mediaTypes("application/json")
description("The announcements in the channel")
responseType<Set<APIResponseAnnouncement>>()
}
}
}
private fun Route.installAnnouncementArchiveRouteDocumentation() = installNotarizedRoute {
tags = setOf("Announcements")
parameters = listOf(
@ -256,65 +229,162 @@ private fun Route.installAnnouncementsIdRouteDocumentation() = installNotarizedR
name = "id",
`in` = Parameter.Location.path,
schema = TypeDefinition.INT,
description = "The ID of the announcement to update",
description = "The id of the announcement to archive",
required = true,
),
authHeaderParameter,
Parameter(
name = "archivedAt",
`in` = Parameter.Location.query,
schema = TypeDefinition.STRING,
description = "The date and time the announcement to be archived",
required = false,
),
)
get = GetInfo.builder {
description("Get an announcement")
summary("Get announcement")
post = PostInfo.builder {
description("Archive an announcement")
summary("Archive announcement")
response {
description("The announcement")
description("When the announcement was archived")
responseCode(HttpStatusCode.OK)
responseType<ApiResponseAnnouncement>()
}
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("The announcement is updated")
description("When announcement was updated")
responseCode(HttpStatusCode.OK)
responseType<Unit>()
}
canRespondUnauthorized()
}
delete = DeleteInfo.builder {
description("Delete an announcement")
summary("Delete announcement")
response {
description("The announcement is deleted")
description("When the announcement was deleted")
responseCode(HttpStatusCode.OK)
responseType<Unit>()
}
canRespondUnauthorized()
}
}
private fun Route.installAnnouncementsTagsRouteDocumentation() = installNotarizedRoute {
private fun Route.installAnnouncementsRouteDocumentation() = installNotarizedRoute {
tags = setOf("Announcements")
get = GetInfo.builder {
description("Get all announcement tags")
summary("Get announcement tags")
description("Get the announcements")
summary("Get announcements")
response {
responseCode(HttpStatusCode.OK)
mediaTypes("application/json")
description("The announcement tags")
responseType<Set<String>>()
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>()
}
}
}

View File

@ -1,28 +1,28 @@
package app.revanced.api.configuration.routes
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.APIContributable
import app.revanced.api.configuration.schema.APIMember
import app.revanced.api.configuration.schema.APIRateLimit
import app.revanced.api.configuration.services.ApiService
import app.revanced.api.configuration.services.AuthenticationService
import app.revanced.api.configuration.services.AuthService
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.http.content.*
import io.ktor.server.plugins.ratelimit.*
import io.ktor.server.response.*
import io.ktor.server.routing.*
import kotlin.time.Duration.Companion.days
import org.koin.ktor.ext.get as koinGet
internal fun Route.apiRoute() {
internal fun Route.rootRoute() {
val apiService = koinGet<ApiService>()
val authenticationService = koinGet<AuthenticationService>()
val authService = koinGet<AuthService>()
rateLimit(RateLimitName("strong")) {
authenticate("auth-digest") {
@ -30,7 +30,7 @@ internal fun Route.apiRoute() {
installTokenRouteDocumentation()
get {
call.respond(authenticationService.newToken())
call.respond(authService.newToken())
}
}
}
@ -56,22 +56,12 @@ internal fun Route.apiRoute() {
}
}
route("about") {
installCache(1.days)
installAboutRouteDocumentation()
get {
call.respond(apiService.about)
}
}
route("ping") {
installNoCache()
installPingRouteDocumentation()
handle {
head {
call.respond(HttpStatusCode.NoContent)
}
}
@ -85,26 +75,14 @@ internal fun Route.apiRoute() {
}
}
staticFiles("/", apiService.versionedStaticFilesPath)
}
}
private fun Route.installAboutRouteDocumentation() = installNotarizedRoute {
tags = setOf("API")
get = GetInfo.builder {
description("Get information about the API")
summary("Get about")
response {
description("Information about the API")
mediaTypes("application/json")
responseCode(HttpStatusCode.OK)
responseType<APIAbout>()
staticResources("/", "/app/revanced/api/static") {
contentType { ContentType.Application.Json }
extensions("json")
}
}
}
private fun Route.installRateLimitRouteDocumentation() = installNotarizedRoute {
fun Route.installRateLimitRouteDocumentation() = installNotarizedRoute {
tags = setOf("API")
get = GetInfo.builder {
@ -114,12 +92,12 @@ private fun Route.installRateLimitRouteDocumentation() = installNotarizedRoute {
description("The rate limit of the backend")
mediaTypes("application/json")
responseCode(HttpStatusCode.OK)
responseType<ApiRateLimit>()
responseType<APIRateLimit>()
}
}
}
private fun Route.installPingRouteDocumentation() = installNotarizedRoute {
fun Route.installPingRouteDocumentation() = installNotarizedRoute {
tags = setOf("API")
head = HeadInfo.builder {
@ -133,7 +111,7 @@ private fun Route.installPingRouteDocumentation() = installNotarizedRoute {
}
}
private fun Route.installTeamRouteDocumentation() = installNotarizedRoute {
fun Route.installTeamRouteDocumentation() = installNotarizedRoute {
tags = setOf("API")
get = GetInfo.builder {
@ -143,12 +121,12 @@ private fun Route.installTeamRouteDocumentation() = installNotarizedRoute {
description("The list of team members")
mediaTypes("application/json")
responseCode(HttpStatusCode.OK)
responseType<Set<ApiMember>>()
responseType<Set<APIMember>>()
}
}
}
private fun Route.installContributorsRouteDocumentation() = installNotarizedRoute {
fun Route.installContributorsRouteDocumentation() = installNotarizedRoute {
tags = setOf("API")
get = GetInfo.builder {
@ -163,39 +141,17 @@ private fun Route.installContributorsRouteDocumentation() = installNotarizedRout
}
}
private fun Route.installTokenRouteDocumentation() = installNotarizedRoute {
val configuration = koinGet<ConfigurationRepository>()
fun Route.installTokenRouteDocumentation() = installNotarizedRoute {
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=\"/${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<ApiToken>()
responseType<String>()
}
canRespondUnauthorized()
}
}

View File

@ -1,79 +0,0 @@
package app.revanced.api.configuration.routes
import app.revanced.api.configuration.ApiRelease
import app.revanced.api.configuration.ApiReleaseVersion
import app.revanced.api.configuration.installNotarizedRoute
import app.revanced.api.configuration.services.ManagerService
import io.bkbn.kompendium.core.metadata.GetInfo
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.plugins.ratelimit.*
import io.ktor.server.response.*
import io.ktor.server.routing.*
import org.koin.ktor.ext.get as koinGet
internal fun Route.managerRoute() = route("manager") {
val managerService = koinGet<ManagerService>()
installManagerRouteDocumentation()
rateLimit(RateLimitName("weak")) {
get {
val prerelease = call.parameters["prerelease"]?.toBoolean() ?: false
call.respond(managerService.latestRelease(prerelease))
}
route("version") {
installManagerVersionRouteDocumentation()
get {
val prerelease = call.parameters["prerelease"]?.toBoolean() ?: false
call.respond(managerService.latestVersion(prerelease))
}
}
}
}
private val prereleaseParameter = Parameter(
name = "prerelease",
`in` = Parameter.Location.query,
schema = TypeDefinition.STRING,
description = "Whether to get the current manager prerelease",
required = false,
)
private fun Route.installManagerRouteDocumentation() = installNotarizedRoute {
tags = setOf("Manager")
get = GetInfo.builder {
description("Get the current manager release")
summary("Get current manager release")
parameters(prereleaseParameter)
response {
description("The latest manager release")
mediaTypes("application/json")
responseCode(HttpStatusCode.OK)
responseType<ApiRelease>()
}
}
}
private fun Route.installManagerVersionRouteDocumentation() = installNotarizedRoute {
tags = setOf("Manager")
get = GetInfo.builder {
description("Get the current manager release version")
summary("Get current manager release version")
parameters(prereleaseParameter)
response {
description("The current manager release version")
mediaTypes("application/json")
responseCode(HttpStatusCode.OK)
responseType<ApiReleaseVersion>()
}
}
}

View File

@ -0,0 +1,19 @@
package app.revanced.api.configuration.routes
import app.revanced.api.configuration.services.OldApiService
import io.ktor.server.application.*
import io.ktor.server.plugins.ratelimit.*
import io.ktor.server.routing.*
import org.koin.ktor.ext.get
internal fun Route.oldApiRoute() {
val oldApiService = get<OldApiService>()
rateLimit(RateLimitName("weak")) {
route(Regex("/(v2|tools|contributors).*")) {
handle {
oldApiService.proxy(call)
}
}
}
}

View File

@ -1,14 +1,12 @@
package app.revanced.api.configuration.routes
import app.revanced.api.configuration.ApiAssetPublicKey
import app.revanced.api.configuration.ApiRelease
import app.revanced.api.configuration.ApiReleaseVersion
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.APIRelease
import app.revanced.api.configuration.schema.APIReleaseVersion
import app.revanced.api.configuration.services.PatchesService
import io.bkbn.kompendium.core.metadata.GetInfo
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.plugins.ratelimit.*
@ -20,34 +18,30 @@ import org.koin.ktor.ext.get as koinGet
internal fun Route.patchesRoute() = route("patches") {
val patchesService = koinGet<PatchesService>()
installPatchesRouteDocumentation()
rateLimit(RateLimitName("weak")) {
get {
val prerelease = call.parameters["prerelease"]?.toBoolean() ?: false
call.respond(patchesService.latestRelease(prerelease))
}
route("version") {
installPatchesVersionRouteDocumentation()
route("latest") {
installLatestPatchesRouteDocumentation()
rateLimit(RateLimitName("weak")) {
get {
val prerelease = call.parameters["prerelease"]?.toBoolean() ?: false
call.respond(patchesService.latestRelease())
}
call.respond(patchesService.latestVersion(prerelease))
route("version") {
installLatestPatchesVersionRouteDocumentation()
get {
call.respond(patchesService.latestVersion())
}
}
}
}
rateLimit(RateLimitName("strong")) {
route("list") {
installPatchesListRouteDocumentation()
rateLimit(RateLimitName("strong")) {
route("list") {
installLatestPatchesListRouteDocumentation()
get {
val prerelease = call.parameters["prerelease"]?.toBoolean() ?: false
call.respondBytes(ContentType.Application.Json) { patchesService.list(prerelease) }
get {
call.respondBytes(ContentType.Application.Json) { patchesService.list() }
}
}
}
}
@ -59,59 +53,48 @@ internal fun Route.patchesRoute() = route("patches") {
installPatchesPublicKeyRouteDocumentation()
get {
call.respond(patchesService.publicKey())
call.respond(patchesService.publicKeys())
}
}
}
}
private val prereleaseParameter = Parameter(
name = "prerelease",
`in` = Parameter.Location.query,
schema = TypeDefinition.STRING,
description = "Whether to get the current patches prerelease",
required = false,
)
private fun Route.installPatchesRouteDocumentation() = installNotarizedRoute {
fun Route.installLatestPatchesRouteDocumentation() = installNotarizedRoute {
tags = setOf("Patches")
get = GetInfo.builder {
description("Get the current patches release")
summary("Get current patches release")
parameters(prereleaseParameter)
description("Get the latest patches release")
summary("Get latest patches release")
response {
description("The current patches release")
description("The latest patches release")
mediaTypes("application/json")
responseCode(HttpStatusCode.OK)
responseType<ApiRelease>()
responseType<APIRelease>()
}
}
}
private fun Route.installPatchesVersionRouteDocumentation() = installNotarizedRoute {
fun Route.installLatestPatchesVersionRouteDocumentation() = installNotarizedRoute {
tags = setOf("Patches")
get = GetInfo.builder {
description("Get the current patches release version")
summary("Get current patches release version")
parameters(prereleaseParameter)
description("Get the latest patches release version")
summary("Get latest patches release version")
response {
description("The current patches release version")
description("The latest patches release version")
mediaTypes("application/json")
responseCode(HttpStatusCode.OK)
responseType<ApiReleaseVersion>()
responseType<APIReleaseVersion>()
}
}
}
private fun Route.installPatchesListRouteDocumentation() = installNotarizedRoute {
fun Route.installLatestPatchesListRouteDocumentation() = installNotarizedRoute {
tags = setOf("Patches")
get = GetInfo.builder {
description("Get the list of patches from the current patches release")
summary("Get list of patches from current patches release")
parameters(prereleaseParameter)
description("Get the list of patches from the latest patches release")
summary("Get list of patches from latest patches release")
response {
description("The list of patches")
mediaTypes("application/json")
@ -121,17 +104,17 @@ private fun Route.installPatchesListRouteDocumentation() = installNotarizedRoute
}
}
private fun Route.installPatchesPublicKeyRouteDocumentation() = installNotarizedRoute {
fun Route.installPatchesPublicKeyRouteDocumentation() = installNotarizedRoute {
tags = setOf("Patches")
get = GetInfo.builder {
description("Get the public keys for verifying patches assets")
summary("Get patches public keys")
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<ApiAssetPublicKey>()
responseType<APIAssetPublicKeys>()
}
}
}

View File

@ -0,0 +1,116 @@
package app.revanced.api.configuration.schema
import kotlinx.datetime.LocalDateTime
import kotlinx.serialization.Serializable
@Serializable
class APIRelease(
val version: String,
val createdAt: LocalDateTime,
val description: String,
// Using a list instead of a set because set semantics are unnecessary here.
val assets: List<APIAsset>,
)
interface APIUser {
val name: String
val avatarUrl: String
val url: String
}
@Serializable
class APIMember(
override val name: String,
override val avatarUrl: String,
override val url: String,
val gpgKey: APIGpgKey?,
) : APIUser
@Serializable
class APIGpgKey(
val id: String,
val url: String,
)
@Serializable
class APIContributor(
override val name: String,
override val avatarUrl: String,
override val url: String,
val contributions: Int,
) : APIUser
@Serializable
class APIContributable(
val name: String,
// Using a list instead of a set because set semantics are unnecessary here.
val contributors: List<APIContributor>,
)
@Serializable
class APIAsset(
val downloadUrl: String,
val signatureDownloadUrl: String,
// TODO: Remove this eventually when integrations are merged into patches.
val name: APIAssetName,
)
@Serializable
enum class APIAssetName {
PATCHES,
INTEGRATION,
}
@Serializable
class APIReleaseVersion(
val version: String,
)
@Serializable
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 archivedAt: LocalDateTime? = null,
val level: Int = 0,
)
@Serializable
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 createdAt: LocalDateTime,
val archivedAt: LocalDateTime? = null,
val level: Int = 0,
)
@Serializable
class APIResponseAnnouncementId(
val id: Int,
)
@Serializable
class APIAnnouncementArchivedAt(
val archivedAt: LocalDateTime,
)
@Serializable
class APIRateLimit(
val limit: Int,
val remaining: Int,
val reset: LocalDateTime,
)
@Serializable
class APIAssetPublicKeys(
val patchesPublicKey: String,
val integrationsPublicKey: String,
)

View File

@ -1,29 +1,35 @@
package app.revanced.api.configuration.services
import app.revanced.api.configuration.ApiAnnouncement
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
internal class AnnouncementService(
private val announcementRepository: AnnouncementRepository,
) {
suspend fun latest(tags: Set<String>) = announcementRepository.latest(tags)
fun latestId(channel: String): APIResponseAnnouncementId? = announcementRepository.latestId(channel)
fun latestId(): APIResponseAnnouncementId? = announcementRepository.latestId()
suspend fun latest() = announcementRepository.latest()
fun latest(channel: String) = announcementRepository.latest(channel)
fun latest() = announcementRepository.latest()
fun latestId(tags: Set<String>) = announcementRepository.latestId(tags)
suspend fun all(channel: String) = announcementRepository.all(channel)
suspend fun all() = announcementRepository.all()
fun latestId() = announcementRepository.latestId()
suspend fun paged(cursor: Int, limit: Int, tags: Set<String>?) =
announcementRepository.paged(cursor, limit, tags)
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()
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)
}
}

View File

@ -1,9 +1,8 @@
package app.revanced.api.configuration.services
import app.revanced.api.configuration.*
import app.revanced.api.configuration.repository.BackendRepository
import app.revanced.api.configuration.repository.ConfigurationRepository
import io.ktor.http.*
import app.revanced.api.configuration.schema.*
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.async
import kotlinx.coroutines.awaitAll
@ -13,20 +12,13 @@ internal class ApiService(
private val backendRepository: BackendRepository,
private val configurationRepository: ConfigurationRepository,
) {
val versionedStaticFilesPath = configurationRepository.versionedStaticFilesPath
val about = configurationRepository.about
suspend fun contributors() = withContext(Dispatchers.IO) {
configurationRepository.contributorsRepositoryNames.map { (repository, name) ->
configurationRepository.contributorsRepositoryNames.map {
async {
APIContributable(
name,
URLBuilder().apply {
takeFrom(backendRepository.website)
path(configurationRepository.organization, repository)
}.buildString(),
backendRepository.contributors(configurationRepository.organization, repository).map {
ApiContributor(it.name, it.avatarUrl, it.url, it.contributions)
it,
backendRepository.contributors(configurationRepository.organization, it).map {
APIContributor(it.name, it.avatarUrl, it.url, it.contributions)
},
)
}
@ -34,13 +26,12 @@ 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,
@ -48,10 +39,11 @@ internal class ApiService(
} else {
null
},
)
}
suspend fun rateLimit() = backendRepository.rateLimit()?.let {
ApiRateLimit(it.limit, it.remaining, it.reset)
APIRateLimit(it.limit, it.remaining, it.reset)
}
}

View File

@ -0,0 +1,49 @@
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))
}

View File

@ -1,52 +0,0 @@
package app.revanced.api.configuration.services
import app.revanced.api.configuration.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)),
)
}

View File

@ -1,37 +0,0 @@
package app.revanced.api.configuration.services
import app.revanced.api.configuration.ApiRelease
import app.revanced.api.configuration.ApiReleaseVersion
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
internal class ManagerService(
private val backendRepository: BackendRepository,
private val configurationRepository: ConfigurationRepository,
) {
suspend fun latestRelease(prerelease: Boolean): ApiRelease {
val managerRelease = backendRepository.release(
configurationRepository.organization,
configurationRepository.manager.repository,
prerelease,
)
return ApiRelease(
managerRelease.tag,
managerRelease.createdAt,
managerRelease.releaseNote,
managerRelease.assets.first(configurationRepository.manager.assetRegex).downloadUrl,
)
}
suspend fun latestVersion(prerelease: Boolean): ApiReleaseVersion {
val managerRelease = backendRepository.release(
configurationRepository.organization,
configurationRepository.manager.repository,
prerelease,
)
return ApiReleaseVersion(managerRelease.tag)
}
}

View File

@ -0,0 +1,69 @@
package app.revanced.api.configuration.services
import io.ktor.client.*
import io.ktor.client.request.*
import io.ktor.client.statement.*
import io.ktor.http.*
import io.ktor.http.content.*
import io.ktor.server.application.*
import io.ktor.server.request.*
import io.ktor.server.response.*
import io.ktor.util.*
import io.ktor.utils.io.*
internal class OldApiService(private val client: HttpClient) {
@OptIn(InternalAPI::class)
suspend fun proxy(call: ApplicationCall) {
val channel = call.request.receiveChannel()
val size = channel.availableForRead
val byteArray = ByteArray(size)
channel.readFully(byteArray)
val response: HttpResponse = client.request(call.request.uri) {
method = call.request.httpMethod
headers {
appendAll(
call.request.headers.filter { key, _ ->
!(
key.equals(HttpHeaders.ContentType, ignoreCase = true) ||
key.equals(HttpHeaders.ContentLength, ignoreCase = true) ||
key.equals(HttpHeaders.Host, ignoreCase = true)
)
},
)
}
when (call.request.httpMethod) {
HttpMethod.Post,
HttpMethod.Put,
HttpMethod.Patch,
HttpMethod.Delete,
-> body = ByteArrayContent(byteArray, call.request.contentType())
}
}
val headers = response.headers
call.respond(object : OutgoingContent.WriteChannelContent() {
override val contentLength: Long? = headers[HttpHeaders.ContentLength]?.toLong()
override val contentType = headers[HttpHeaders.ContentType]?.let { ContentType.parse(it) }
override val headers: Headers = Headers.build {
appendAll(
headers.filter { key, _ ->
!key.equals(
HttpHeaders.ContentType,
ignoreCase = true,
) &&
!key.equals(HttpHeaders.ContentLength, ignoreCase = true)
},
)
}
override val status = response.status
override suspend fun writeTo(channel: ByteWriteChannel) {
response.content.copyAndClose(channel)
}
})
}
}

View File

@ -1,14 +1,13 @@
package app.revanced.api.configuration.services
import app.revanced.api.configuration.ApiAssetPublicKey
import app.revanced.api.configuration.ApiRelease
import app.revanced.api.configuration.ApiReleaseVersion
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.library.serializeTo
import app.revanced.patcher.patch.loadPatchesFromJar
import app.revanced.api.configuration.schema.*
import app.revanced.library.PatchUtils
import app.revanced.patcher.PatchBundleLoader
import com.github.benmanes.caffeine.cache.Caffeine
import io.ktor.util.*
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext
import java.io.ByteArrayOutputStream
@ -19,30 +18,50 @@ internal class PatchesService(
private val backendRepository: BackendRepository,
private val configurationRepository: ConfigurationRepository,
) {
suspend fun latestRelease(prerelease: Boolean): ApiRelease {
suspend fun latestRelease(): APIRelease {
val patchesRelease = backendRepository.release(
configurationRepository.organization,
configurationRepository.patches.repository,
prerelease,
)
return ApiRelease(
val integrationsRelease = backendRepository.release(
configurationRepository.organization,
configurationRepository.integrations.repository,
)
fun ConfigurationRepository.AssetConfiguration.asset(
release: BackendRepository.BackendOrganization.BackendRepository.BackendRelease,
assetName: APIAssetName,
) = APIAsset(
release.assets.first(assetRegex).downloadUrl,
release.assets.first(signatureAssetRegex).downloadUrl,
assetName,
)
val patchesAsset = configurationRepository.patches.asset(
patchesRelease,
APIAssetName.PATCHES,
)
val integrationsAsset = configurationRepository.integrations.asset(
integrationsRelease,
APIAssetName.INTEGRATION,
)
return APIRelease(
patchesRelease.tag,
patchesRelease.createdAt,
patchesRelease.releaseNote,
patchesRelease.assets.first(configurationRepository.patches.assetRegex).downloadUrl,
patchesRelease.assets.first(configurationRepository.patches.signatureAssetRegex).downloadUrl,
listOf(patchesAsset, integrationsAsset),
)
}
suspend fun latestVersion(prerelease: Boolean): ApiReleaseVersion {
suspend fun latestVersion(): APIReleaseVersion {
val patchesRelease = backendRepository.release(
configurationRepository.organization,
configurationRepository.patches.repository,
prerelease,
)
return ApiReleaseVersion(patchesRelease.tag)
return APIReleaseVersion(patchesRelease.tag)
}
private val patchesListCache = Caffeine
@ -50,11 +69,10 @@ internal class PatchesService(
.maximumSize(1)
.build<String, ByteArray>()
suspend fun list(prerelease: Boolean): ByteArray {
suspend fun list(): ByteArray {
val patchesRelease = backendRepository.release(
configurationRepository.organization,
configurationRepository.patches.repository,
prerelease,
)
return withContext(Dispatchers.IO) {
@ -74,10 +92,9 @@ internal class PatchesService(
patchesFile,
signatureDownloadUrl,
configurationRepository.patches.publicKeyFile,
configurationRepository.patches.publicKeyId,
)
) {
loadPatchesFromJar(setOf(patchesFile))
PatchBundleLoader.Jar(patchesFile)
} else {
// Use an empty set of patches if the signature is invalid.
emptySet()
@ -86,7 +103,7 @@ internal class PatchesService(
patchesFile.delete()
ByteArrayOutputStream().use { stream ->
patches.serializeTo(outputStream = stream)
PatchUtils.Json.serialize(patches, outputStream = stream)
stream.toByteArray()
}
@ -94,5 +111,13 @@ internal class PatchesService(
}
}
fun publicKey() = ApiAssetPublicKey(configurationRepository.patches.publicKeyFile.readText())
fun publicKeys(): APIAssetPublicKeys {
fun publicKeyBase64(getAssetConfiguration: ConfigurationRepository.() -> ConfigurationRepository.AssetConfiguration) =
configurationRepository.getAssetConfiguration().publicKeyFile.readBytes().encodeBase64()
return APIAssetPublicKeys(
publicKeyBase64 { patches },
publicKeyBase64 { integrations },
)
}
}

View File

@ -1,6 +1,7 @@
package app.revanced.api.configuration.services
import com.github.benmanes.caffeine.cache.Caffeine
import org.bouncycastle.jce.provider.BouncyCastleProvider
import org.bouncycastle.openpgp.*
import org.bouncycastle.openpgp.operator.bc.BcKeyFingerprintCalculator
import org.bouncycastle.openpgp.operator.bc.BcPGPContentVerifierBuilderProvider
@ -8,18 +9,18 @@ import java.io.File
import java.io.InputStream
import java.net.URL
import java.security.MessageDigest
import java.security.Security
internal class SignatureService {
private val signatureCache = Caffeine
.newBuilder()
.maximumSize(2) // 2 because currently only the latest release and prerelease patches are needed.
.maximumSize(2) // Assuming this is enough for patches and integrations.
.build<ByteArray, Boolean>() // Hash -> Verified.
fun verify(
file: File,
signatureDownloadUrl: String,
publicKeyFile: File,
publicKeyId: Long,
): Boolean {
val fileBytes = file.readBytes()
@ -27,8 +28,7 @@ internal class SignatureService {
verify(
fileBytes = fileBytes,
signatureInputStream = URL(signatureDownloadUrl).openStream(),
publicKeyFileInputStream = publicKeyFile.inputStream(),
publicKeyId = publicKeyId,
publicKeyInputStream = publicKeyFile.inputStream(),
)
}
}
@ -36,32 +36,37 @@ internal class SignatureService {
private fun verify(
fileBytes: ByteArray,
signatureInputStream: InputStream,
publicKeyFileInputStream: InputStream,
publicKeyId: Long,
publicKeyInputStream: InputStream,
) = getSignature(signatureInputStream).apply {
init(BcPGPContentVerifierBuilderProvider(), getPublicKey(publicKeyFileInputStream, publicKeyId))
init(BcPGPContentVerifierBuilderProvider(), getPublicKey(publicKeyInputStream))
update(fileBytes)
}.verify()
private fun getPublicKey(
publicKeyFileInputStream: InputStream,
publicKeyId: Long,
): PGPPublicKey {
val decoderStream = PGPUtil.getDecoderStream(publicKeyFileInputStream)
val pgpPublicKeyRingCollection = PGPPublicKeyRingCollection(decoderStream, BcKeyFingerprintCalculator())
val publicKeyRing = pgpPublicKeyRingCollection.getPublicKeyRing(publicKeyId)
?: throw IllegalArgumentException("Can't find public key ring with ID $publicKeyId.")
private fun getPublicKey(publicKeyInputStream: InputStream): PGPPublicKey {
val decoderStream = PGPUtil.getDecoderStream(publicKeyInputStream)
return publicKeyRing.getPublicKey(publicKeyId)
?: throw IllegalArgumentException("Can't find public key with ID $publicKeyId.")
PGPPublicKeyRingCollection(decoderStream, BcKeyFingerprintCalculator()).forEach { keyRing ->
keyRing.publicKeys.forEach { publicKey ->
if (publicKey.isEncryptionKey) {
return publicKey
}
}
}
throw IllegalArgumentException("Can't find encryption key in key ring.")
}
private fun getSignature(inputStream: InputStream): PGPSignature {
val decoderStream = PGPUtil.getDecoderStream(inputStream)
val pgpSignatureList = PGPObjectFactory(decoderStream, BcKeyFingerprintCalculator()).first {
it is PGPSignatureList
} as PGPSignatureList
val pgpObjectFactory = PGPObjectFactory(decoderStream, BcKeyFingerprintCalculator())
val signatureList = pgpObjectFactory.nextObject() as PGPSignatureList
return pgpSignatureList.first()
return signatureList.first()
}
private companion object {
init {
Security.addProvider(BouncyCastleProvider())
}
}
}

View File

@ -1,11 +1,9 @@
{
"name": "ReVanced",
"about": "ReVanced was born out of Vanced's discontinuation and it is our goal to continue the legacy of what Vanced left behind. Thanks to ReVanced Patcher, it's possible to create long-lasting patches for nearly any Android app. ReVanced's patching system is designed to allow patches to work on new versions of the apps automatically with bare minimum maintenance.",
"keys": "https://api.revanced.app/keys",
"branding": {
"logo": "https://raw.githubusercontent.com/ReVanced/revanced-branding/main/assets/revanced-logo/revanced-logo.svg"
},
"status": "https://status.revanced.app",
"contact": {
"email": "contact@revanced.app"
},

View File

@ -0,0 +1,2 @@
User-agent: *
Disallow: /

View File

@ -1,10 +1,10 @@
<configuration>
<appender name="STDOUT" class="ch.qos.logback.core.ConsoleAppender">
<encoder>
<pattern>%d{YYYY-MM-dd HH:mm:ss.SSS} %-5level %msg%n</pattern>
<pattern>%d{YYYY-MM-dd HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n</pattern>
</encoder>
</appender>
<root level="\${LOG_LEVEL:-INFO}">
<root level="info">
<appender-ref ref="STDOUT"/>
</root>
</configuration>

View File

@ -1,186 +0,0 @@
package app.revanced.api.configuration.services
import app.revanced.api.configuration.ApiAnnouncement
import app.revanced.api.configuration.repository.AnnouncementRepository
import kotlinx.coroutines.runBlocking
import kotlinx.datetime.toKotlinLocalDateTime
import org.jetbrains.exposed.sql.Database
import org.junit.jupiter.api.Assertions.assertNull
import org.junit.jupiter.api.BeforeAll
import org.junit.jupiter.api.BeforeEach
import org.junit.jupiter.api.Test
import java.time.LocalDateTime
import kotlin.test.assertEquals
import kotlin.test.assertNotNull
private object AnnouncementServiceTest {
private lateinit var announcementService: AnnouncementService
@JvmStatic
@BeforeAll
fun setUp() {
val database = Database.connect("jdbc:h2:mem:test;DB_CLOSE_DELAY=-1;DATABASE_TO_UPPER=false")
announcementService = AnnouncementService(AnnouncementRepository(database))
}
@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")))
assert(announcementService.latest(setOf("tag2")).first().title == "1")
assert(announcementService.latest(setOf("tag3")).last().title == "2")
val announcement2and3 = announcementService.latest(setOf("tag1", "tag3"))
assert(announcement2and3.size == 2)
assert(announcement2and3.any { it.title == "2" })
assert(announcement2and3.any { it.title == "3" })
announcementService.delete(announcementService.latestId()!!.id)
assert(announcementService.latest(setOf("tag1", "tag3")).first().title == "2")
announcementService.delete(announcementService.latestId()!!.id)
assert(announcementService.latest(setOf("tag1", "tag3")).first().title == "1")
announcementService.delete(announcementService.latestId()!!.id)
assert(announcementService.latest(setOf("tag1", "tag3")).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)
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)
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].name))
assertEquals(4, announcements3.size, "Filters announcements by tag")
}
}