Compare commits

..

83 Commits

Author SHA1 Message Date
semantic-release-bot
01d86ebba8 chore: Release v1.6.0 [skip ci]
# [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](8a957cd797))
* Add support for prereleases ([c25bc8b](c25bc8b4ba))
* Allow setting `Announcement.createdAt` when creating an announcement ([7f6e29d](7f6e29de52))
* Make some announcements schema fields nullable ([db22874](db22874f06))
2025-02-04 00:14:22 +00:00
oSumAtrIX
989094309f
chore: Merge branch dev to main (#196)
This pull request will Merge branch `dev` to `main`.
2025-02-04 01:12:19 +01:00
semantic-release-bot
5b447aa62d chore: Release v1.6.0-dev.3 [skip ci]
# [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](8a957cd797))
* Add support for prereleases ([c25bc8b](c25bc8b4ba))
2024-12-25 10:52:04 +00:00
oSumAtrIX
c25bc8b4ba
feat: Add support for prereleases 2024-12-25 11:50:06 +01:00
oSumAtrIX
8a957cd797
feat: Add status page link to about 2024-12-25 11:50:05 +01:00
semantic-release-bot
712ab3be8c chore: Release v1.6.0-dev.2 [skip ci]
# [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](db22874f06))
2024-12-20 23:27:48 +00:00
oSumAtrIX
db22874f06
feat: Make some announcements schema fields nullable 2024-12-21 00:25:26 +01:00
semantic-release-bot
5d5533a920 chore: Release v1.6.0-dev.1 [skip ci]
# [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](7f6e29de52))
2024-11-23 18:26:28 +00:00
oSumAtrIX
7f6e29de52
feat: Allow setting Announcement.createdAt when creating an announcement 2024-11-13 22:23:05 +01:00
semantic-release-bot
48469d32c2 chore: Release v1.5.0 [skip ci]
# [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](58ba4cb11c))
* Move spec url to versioned path ([e871b23](e871b23210))
* Simplify log pattern ([d5d9e04](d5d9e04325))
2024-11-06 21:17:51 +00:00
oSumAtrIX
7f9159fef1
chore: Merge branch dev to main(#195) 2024-11-06 22:15:06 +01:00
semantic-release-bot
b063b4daf2 chore: Release v1.5.0-dev.2 [skip ci]
# [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](58ba4cb11c))
* Simplify log pattern ([d5d9e04](d5d9e04325))
2024-11-06 15:36:26 +00:00
oSumAtrIX
58ba4cb11c
feat: Allow updating createdAt field for announcements 2024-11-06 16:34:18 +01:00
semantic-release-bot
55e3774f07 chore: Release v1.5.0-dev.1 [skip ci]
# [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](e871b23210))
* Simplify log pattern ([d5d9e04](d5d9e04325))
2024-11-06 04:41:55 +00:00
oSumAtrIX
d5d9e04325
feat: Simplify log pattern 2024-11-06 05:39:39 +01:00
semantic-release-bot
6c8153ba98 chore: Release v1.5.0-dev.1 [skip ci]
# [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](e871b23210))
2024-11-06 04:32:10 +00:00
oSumAtrIX
e871b23210
feat: Move spec url to versioned path 2024-11-06 05:30:09 +01:00
semantic-release-bot
e22ec16e40 chore: Release v1.4.0 [skip ci]
# [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](3b6212065a))
* Use new patches file extension ([d42a3a3](d42a3a3933))

### Features

* Add URL and use friendly name for `APIContributable` ([a5498ab](a5498aba2b))
* Allow versioning by arbitrary path string ([814d3c9](814d3c946e))
* Improve announcements API ([#192](https://github.com/ReVanced/revanced-api/issues/192)) ([56a00dd](56a00ddb85))
* Make backend configurable ([f91f3a6](f91f3a65c5))
* Remove "archived" query parameter ([8ad614e](8ad614ef4f))
* Remove deprecated routes and old API ([eca40a6](eca40a6979))
* Remove ReVanced Integrations ([f1c1092](f1c10928ae))
* Use tag name directly instead of ID ([fc40427](fc40427fba))
2024-11-06 04:06:21 +00:00
oSumAtrIX
440fbbc6c2
chore: Merge branch dev to main (#194) 2024-11-06 05:04:05 +01:00
semantic-release-bot
f74012993e chore: Release v1.4.0-dev.6 [skip ci]
# [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](814d3c946e))
* Remove deprecated routes and old API ([eca40a6](eca40a6979))
2024-11-06 03:39:26 +00:00
oSumAtrIX
814d3c946e
feat: Allow versioning by arbitrary path string 2024-11-06 04:37:10 +01:00
oSumAtrIX
eca40a6979
feat: Remove deprecated routes and old API
Backwards compatibility should be and is now handled by the reverse proxy.
2024-11-06 04:37:10 +01:00
oSumAtrIX
0b66fc2bca
refactor: Move file to upper package 2024-11-06 01:38:02 +01:00
semantic-release-bot
1a09b028b7 chore: Release v1.4.0-dev.5 [skip ci]
# [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)
2024-11-05 19:01:39 +00:00
oSumAtrIX
0ddbf5beda
build(Needs bump): Bump dependencies 2024-11-05 19:58:20 +01:00
semantic-release-bot
bf41fa1596 chore: Release v1.4.0-dev.4 [skip ci]
# [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](8ad614ef4f))
* Use tag name directly instead of ID ([fc40427](fc40427fba))
2024-11-01 23:59:33 +00:00
oSumAtrIX
8ad614ef4f
feat: Remove "archived" query parameter
It doesn't seem to be necessary for the purpose of viewing announcements.
2024-11-02 00:57:42 +01:00
oSumAtrIX
fc40427fba
feat: Use tag name directly instead of ID 2024-11-02 00:57:42 +01:00
semantic-release-bot
e8dfefe6ae chore: Release v1.4.0-dev.3 [skip ci]
# [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](d42a3a3933))
2024-11-01 22:13:35 +00:00
oSumAtrIX
d42a3a3933
fix: Use new patches file extension 2024-11-01 23:11:37 +01:00
semantic-release-bot
bb7aa5b0b4 chore: Release v1.4.0-dev.2 [skip ci]
# [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](3b6212065a))

### Features

* Add URL and use friendly name for `APIContributable` ([a5498ab](a5498aba2b))
* Make backend configurable ([f91f3a6](f91f3a65c5))
* Remove ReVanced Integrations ([f1c1092](f1c10928ae))
2024-11-01 18:13:29 +00:00
oSumAtrIX
3b6212065a
fix: Add missing logging level environment variable to .env.example 2024-11-01 19:11:33 +01:00
oSumAtrIX
3d3b7a7af8
chore: Use tables in configuration for readability 2024-11-01 19:09:01 +01:00
oSumAtrIX
f1c10928ae
feat: Remove ReVanced Integrations
There is no need for them anymore in Patcher v20+
2024-11-01 19:04:22 +01:00
oSumAtrIX
a5498aba2b
feat: Add URL and use friendly name for APIContributable 2024-11-01 18:43:39 +01:00
oSumAtrIX
f91f3a65c5
feat: Make backend configurable 2024-11-01 18:41:43 +01:00
semantic-release-bot
65ee2700e7 chore: Release v1.4.0-dev.1 [skip ci]
# [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](56a00ddb85))
2024-11-01 01:51:53 +00:00
oSumAtrIX
56a00ddb85
feat: Improve announcements API (#192)
Announcements can have tags now instead of being grouped into a single channel. You can get an announcement using its ID. You can page announcements and filter them by tags and whether they are archived. You can see a list of all available tags. Some route API overhaul.
2024-11-01 02:49:36 +01:00
semantic-release-bot
50b81fd337 chore: Release v1.3.0 [skip ci]
# [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](1181be12e2))
* Allow more necessary HTTP methods for CORS ([080e2e5](080e2e582c))
* Configure CORS properly to allow authorization and content-type header ([6442757](6442757927))
* Expire token relative to current date time instead of just time ([c26e129](c26e129bda))
* Expose www-authenticate header to JS ([9ed724e](9ed724e161))
* Respond with JSON when returning token ([1e3e46f](1e3e46ff4f))
* Specify a validation function to fix authentication ([53c3600](53c36002e9))

### Features

* Add missing parameter and response documentation ([491533d](491533d3f4))
* Customize logging level through environment variable ([8b17d88](8b17d8894d))
* Improve response info description wording ([977d252](977d252497))
* Only allow requests from HTTPs ([a6d7da1](a6d7da1205))
2024-10-07 20:36:41 +00:00
oSumAtrIX
c51db8da72
chore: Merge branch dev to main (#190) 2024-10-07 22:34:27 +02:00
semantic-release-bot
06098415f1 chore: Release v1.3.0-dev.6 [skip ci]
# [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](9ed724e161))

### Features

* Only allow requests from HTTPs ([a6d7da1](a6d7da1205))
2024-10-02 04:59:16 +00:00
oSumAtrIX
9ed724e161
fix: Expose www-authenticate header to JS 2024-10-02 06:57:20 +02:00
oSumAtrIX
a6d7da1205
feat: Only allow requests from HTTPs 2024-10-02 06:57:19 +02:00
oSumAtrIX
87174eadd6
ci: Use permissions and regular GitHub token instead of PAT 2024-10-02 06:57:19 +02:00
oSumAtrIX
fde2857915
build(Needs bump): Update dependencies 2024-09-30 23:21:46 +02:00
oSumAtrIX
fae8cb6b23
ci: Adjust release commit message 2024-09-30 22:34:25 +02:00
semantic-release-bot
a754159800 chore(release): 1.3.0-dev.5 [skip ci]
# [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](080e2e582c))
2024-09-30 15:35:42 +00:00
oSumAtrIX
080e2e582c
fix: Allow more necessary HTTP methods for CORS 2024-09-30 17:33:15 +02:00
semantic-release-bot
8ff1bbd41f chore(release): 1.3.0-dev.4 [skip ci]
# [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](6442757927))
2024-09-29 23:29:17 +00:00
oSumAtrIX
6442757927
fix: Configure CORS properly to allow authorization and content-type header 2024-09-30 01:27:24 +02:00
semantic-release-bot
710416ff36 chore(release): 1.3.0-dev.3 [skip ci]
# [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](1181be12e2))
* Respond with JSON when returning token ([1e3e46f](1e3e46ff4f))
* Specify a validation function to fix authentication ([53c3600](53c36002e9))

### Features

* Customize logging level through environment variable ([8b17d88](8b17d8894d))
* Improve response info description wording ([977d252](977d252497))
2024-09-29 21:19:18 +00:00
oSumAtrIX
1181be12e2
fix: Add missing OK response to routes 2024-09-29 23:15:35 +02:00
oSumAtrIX
53c36002e9
fix: Specify a validation function to fix authentication 2024-09-29 23:13:13 +02:00
oSumAtrIX
8b17d8894d
feat: Customize logging level through environment variable 2024-09-29 01:03:49 +02:00
oSumAtrIX
1e3e46ff4f
fix: Respond with JSON when returning token 2024-09-27 20:19:27 +02:00
oSumAtrIX
977d252497
feat: Improve response info description wording 2024-09-27 19:18:32 +02:00
oSumAtrIX
96bcd7719a
chore: Remove unnecessary JWT token field 2024-09-27 19:16:34 +02:00
semantic-release-bot
2d85ce17f6 chore(release): 1.3.0-dev.2 [skip ci]
# [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](c26e129bda))
2024-09-27 12:23:28 +00:00
oSumAtrIX
c26e129bda
fix: Expire token relative to current date time instead of just time 2024-09-27 14:16:45 +02:00
semantic-release-bot
84ea5e4a41 chore(release): 1.3.0-dev.1 [skip ci]
# [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](491533d3f4))
2024-09-11 14:22:06 +00:00
oSumAtrIX
491533d3f4
feat: Add missing parameter and response documentation 2024-09-09 11:59:03 +02:00
semantic-release-bot
ef5f0b5ddd chore(release): 1.2.0 [skip ci]
# [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](9f0eb5bfe9))
* Make sure, expected paths in configuration exist ([32bedb7](32bedb7fad))
* Return correct GPG keys url ([#187](https://github.com/ReVanced/revanced-api/issues/187)) ([74e5389](74e53891a1))

### Features

* Move /latest routes to parent ([4e8e83d](4e8e83db1a))
* Respond to all ping request methods ([df116bd](df116bd221))
2024-09-06 23:23:38 +00:00
oSumAtrIX
c0dc763f99
chore: Merge branch dev to main (#188)
This pull request will Merge branch `dev` to `main`.
2024-09-07 03:21:40 +04:00
semantic-release-bot
11327af879 chore(release): 1.2.0-dev.4 [skip ci]
# [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](9f0eb5bfe9))
* Make sure, expected paths in configuration exist ([32bedb7](32bedb7fad))
2024-09-06 08:27:20 +00:00
oSumAtrIX
9f0eb5bfe9
fix: Add back deprecated routes for backwards compatibility 2024-09-06 12:25:10 +04:00
oSumAtrIX
d2575d5191
docs: Add missing documentation and setup steps 2024-09-06 11:59:33 +04:00
oSumAtrIX
32bedb7fad
fix: Make sure, expected paths in configuration exist 2024-09-06 11:40:10 +04:00
semantic-release-bot
d605efd54a chore(release): 1.2.0-dev.3 [skip ci]
# [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](74e53891a1))
2024-09-04 15:20:13 +00:00
Ushie
74e53891a1
fix: Return correct GPG keys url (#187)
Co-authored-by: oSumAtrIX <johan.melkonyan1@web.de>
2024-09-04 17:18:21 +02:00
semantic-release-bot
27b18c62f5 chore(release): 1.2.0-dev.2 [skip ci]
# [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](df116bd221))
2024-08-24 20:34:53 +00:00
oSumAtrIX
df116bd221
feat: Respond to all ping request methods 2024-08-24 22:32:37 +02:00
semantic-release-bot
2d7b4e7b7f chore(release): 1.2.0-dev.1 [skip ci]
# [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](4e8e83db1a))
2024-08-16 23:03:43 +00:00
oSumAtrIX
4e8e83db1a
feat: Move /latest routes to parent
There is only ever current releases, so a latest route doesn't make sense.
2024-08-17 01:01:33 +02:00
oSumAtrIX
e113daa089
build: Bump dependencies to correctly serialize patches as JSON 2024-08-17 00:53:13 +02:00
oSumAtrIX
170edd3157
build: Bump Gradle 2024-08-17 00:20:07 +02:00
semantic-release-bot
d18e09cba3 chore(release): 1.1.0 [skip ci]
# [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](435beae383))

### Features

* Add static file paths to remove env specific files in resources ([39d0b78](39d0b78c79))
* Convert static about file to documented route & add key parameter to about route ([dfe6df3](dfe6df3ef6))
2024-07-15 18:20:46 +00:00
oSumAtrIX
bc919b85f5
chore: Merge branch dev to main (#183) 2024-07-15 20:18:36 +02:00
semantic-release-bot
665de7bcd0 chore(release): 1.1.0-dev.1 [skip ci]
# [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](435beae383))

### Features

* Add static file paths to remove env specific files in resources ([39d0b78](39d0b78c79))
* Convert static about file to documented route & add key parameter to about route ([dfe6df3](dfe6df3ef6))
2024-07-15 01:15:03 +00:00
oSumAtrIX
dfe6df3ef6
feat: Convert static about file to documented route & add key parameter to about route 2024-07-15 03:12:39 +02:00
oSumAtrIX
39d0b78c79
feat: Add static file paths to remove env specific files in resources 2024-07-15 02:30:21 +02:00
oSumAtrIX
435beae383
fix: Don't encode public keys & instead send them raw 2024-07-15 01:52:16 +02:00
semantic-release-bot
03fb28cd10 chore(release): 1.0.0 [skip ci]
# 1.0.0 (2024-07-13)

### Bug Fixes

* Add missing auth realm ([a6008a2](a6008a2fb6))
* Add missing OpenAPI docs ([d4ac471](d4ac47194e))
* add required headers ([#8](https://github.com/ReVanced/revanced-api/issues/8)) ([f4c10dc](f4c10dc064))
* Add uri to rate limiter request key ([e8c2488](e8c2488bc6))
* **ci:** add git dep ([d61ddcc](d61ddcc8ac))
* **ci:** fix git deps ([5564c2d](5564c2da09))
* Configure CORS correctly ([2ed4cf3](2ed4cf3b40))
* Correct env var comment ([9d7b049](9d7b049349))
* Correct persistence directory name ([6238e33](6238e33c42))
* **deps:** fix requirements file ([5f0ab26](5f0ab26ced))
* **deps:** missing pydantic ([089f29e](089f29e95f))
* **deps:** update poetry.lock ([b2c2fa7](b2c2fa7136))
* Don't configure server ([280dbc3](280dbc30f6))
* Encode defaults to fix OpenAPI spec ([e9d1c8f](e9d1c8fae0))
* Finish DB Model to API model transformation inside transaction ([89a577e](89a577e91a))
* fix codeql issues ([b5568db](b5568db79f))
* Fix OpenAPI docs casing of a word ([541783d](541783d959))
* Fix spelling mistake ([17ecf58](17ecf58e55))
* Increase pool size to mitigate overflow ([#113](https://github.com/ReVanced/revanced-api/issues/113)) ([5aed3d6](5aed3d6ce6))
* Move old API endpoint configuration from env to configuration file ([7e99e49](7e99e49af2))
* Move robots.txt to root ([2ade550](2ade550d58))
* Only list public members ([97a5d11](97a5d119ec))
* Remove punctuation ([f9cae1e](f9cae1ea56))
* remove revanced-api from tools map ([4800ee9](4800ee96a8))
* Serialize response correctly ([1dccfd2](1dccfd2def))
* Set body for all eligible request methods ([c6cacef](c6cacef907))
* unversioned endpoints placement under v0 should be at root ([8b29f49](8b29f49805))
* Use correct persistance folder path ([500a589](500a5896e3))
* Use correct proxy path ([ef92768](ef927688a3))
* Use correct resource path ([4dffd32](4dffd32c99))
* Use multidict 6.0.5, add setup tools as dev dependency ([af19446](af19446a67))

### Features

* Add `/list` route ([6c930ff](6c930fff9a))
* Add `preferred` field to socials ([#100](https://github.com/ReVanced/revanced-api/issues/100)) ([24c8f60](24c8f60a70))
* Add announcements API ([42f7318](42f731854d))
* Add announcements endpoints ([#91](https://github.com/ReVanced/revanced-api/issues/91)) ([8583e2a](8583e2a2bb))
* Add backend rate limit route ([b967170](b9671703be))
* Add bio field for team members ([c40d50c](c40d50c013))
* Add CLI ([a988ffb](a988ffbd23))
* Add configuration to specify public key id ([ad7d4b2](ad7d4b226f))
* add download_count to releases ([#118](https://github.com/ReVanced/revanced-api/issues/118)) ([665b913](665b913c04))
* add friendly crypto names ([#53](https://github.com/ReVanced/revanced-api/issues/53)) ([7b70780](7b707807cc))
* Add GPG key to team members ([71f58cf](71f58cf352))
* Add local ReVanced API server ([cd5d57f](cd5d57f8f8))
* Add manager route ([f814fe5](f814fe5825))
* add member gpg keys ([80cdb3b](80cdb3be6a))
* Add OpenAPI docs and cache to routes ([6ea63be](6ea63be490))
* add preferred field to donations ([#56](https://github.com/ReVanced/revanced-api/issues/56)) ([6b705b1](6b705b1054))
* Add proxy for old API ([39f54bb](39f54bbb32))
* Add rate limiting to routes ([80403f7](80403f7130))
* added branding ([#104](https://github.com/ReVanced/revanced-api/issues/104)) ([edcad62](edcad620f2))
* API Fixes and Adjustments ([#23](https://github.com/ReVanced/revanced-api/issues/23)) ([b18097e](b18097e030))
* API rewrite ([#2](https://github.com/ReVanced/revanced-api/issues/2)) ([45ef337](45ef33741c))
* better versioning engine ([8d36663](8d36663610))
* Change default port to avoid using existing port ([9825865](9825865bbc))
* Disallow all web crawlers ([#111](https://github.com/ReVanced/revanced-api/issues/111)) ([b69acfa](b69acfa8d7))
* Do not ignore, if `.env` file is missing ([24c6f4e](24c6f4e435))
* favicon ([a8126d7](a8126d785f))
* hostname filtering ([5482d9c](5482d9c442))
* Implement more routes and add configuration ([9999b24](9999b242ad))
* Improve routing paths ([df999c0](df999c00c4))
* info endpoint ([#71](https://github.com/ReVanced/revanced-api/issues/71)) ([9bbd056](9bbd056c1b))
* Initialize project ([8ae50b5](8ae50b543e))
* List more repository contributors ([19ebc82](19ebc827bf))
* Load system properties ([e373d26](e373d26998))
* manager-related endpoints ([3a128c4](3a128c4661))
* Move config file to CLI argument ([6a9f0ca](6a9f0cadac))
* **observability:** sentry ([99e645c](99e645c5f5))
* project init ([856fc66](856fc66717))
* remove appinfo capabilities ([10f5225](10f5225f51))
* remove codecov tests ([9c69fa3](9c69fa3b92))
* Remove Swagger and OpenAPI ([af0b086](af0b0865f4))
* Setup CI/CD ([c736a75](c736a75d92))
* Setup cors and cache ([205bcde](205bcde77a))
* Show default CLI option values ([db0bfc3](db0bfc3be5))
* Use auth digest instead of basic auth ([89e2acf](89e2acfebb))
* Use Jetty instead of Netty ([c23cd5c](c23cd5cdad))
* use objects for /socials and /donations ([#51](https://github.com/ReVanced/revanced-api/issues/51)) ([d4eac5c](d4eac5c757))
* use objects for contact field ([5922830](5922830e0b))

### Performance Improvements

* Cache latest announcements for constant access time ([1ca9952](1ca9952de8))
* Cache patches list instead of just the patches file ([7a1957d](7a1957d013))
* Make async db transactions and use List instead of Set ([a7d1892](a7d1892343))
2024-07-13 14:30:19 +00:00
oSumAtrIX
7214214d42
chore: Merge branch dev to main (#180) 2024-07-13 16:28:12 +02:00
47 changed files with 4095 additions and 2197 deletions

View File

@ -13,3 +13,6 @@ AUTH_SHA256_DIGEST=
JWT_SECRET= JWT_SECRET=
JWT_ISSUER= JWT_ISSUER=
JWT_VALIDITY_IN_MIN= JWT_VALIDITY_IN_MIN=
# Logging level for the application
LOG_LEVEL=INFO

View File

@ -12,7 +12,7 @@ jobs:
name: Release name: Release
runs-on: ubuntu-latest runs-on: ubuntu-latest
permissions: permissions:
contents: read contents: write
packages: write packages: write
steps: steps:
- name: Checkout - name: Checkout
@ -60,7 +60,7 @@ jobs:
DOCKER_REGISTRY_USER: ${{ github.actor }} DOCKER_REGISTRY_USER: ${{ github.actor }}
DOCKER_REGISTRY_PASSWORD: ${{ secrets.GITHUB_TOKEN }} DOCKER_REGISTRY_PASSWORD: ${{ secrets.GITHUB_TOKEN }}
GITHUB_ACTOR: ${{ github.actor }} GITHUB_ACTOR: ${{ github.actor }}
GITHUB_TOKEN: ${{ secrets.REPOSITORY_PUSH_ACCESS }} GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: npm exec semantic-release run: npm exec semantic-release
- name: Set Portainer stack webhook URL based on branch - name: Set Portainer stack webhook URL based on branch

3
.gitignore vendored
View File

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

View File

@ -21,10 +21,10 @@
"@semantic-release/git", "@semantic-release/git",
{ {
"assets": [ "assets": [
"README.md",
"CHANGELOG.md", "CHANGELOG.md",
"gradle.properties" "gradle.properties"
] ],
"message": "chore: Release v${nextRelease.version} [skip ci]\n\n${nextRelease.notes}"
} }
], ],
[ [

View File

@ -1,3 +1,374 @@
# [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) # [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)

View File

@ -68,19 +68,19 @@ API server for ReVanced.
## ❓ About ## ❓ About
ReVanced API is a server that is used as the backend for ReVanced. 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. with updates and ReVanced Patches.
## 💪 Features ## 💪 Features
Some of the features ReVanced API include: Some of the features ReVanced API include:
- 📢 **Announcements**: Post and get announcements grouped by channels - 📢 **Announcements**: Post and get announcements
- **About**: Get more information such as a description, ways to donate to, - **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 - 🧩 **Patches**: Get the latest updates of ReVanced Patches, directly from ReVanced API
- 👥 **Contributors**: List all contributors involved in the project - 👥 **Contributors**: List all contributors involved in the project
- 🔄 **Backwards compatibility**: Proxy an old API for migration purposes and backwards compatibility
## 🚀 How to get started ## 🚀 How to get started
@ -90,26 +90,35 @@ 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. 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, 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 ### 🗄️ Docker Compose
1. Create an `.env` file using [.env.example](.env.example) as a template 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 2. Create a `configuration.toml` file using [configuration.example.toml](configuration.example.toml) as a template
3. Create a `docker-compose.yml` file using [docker-compose.example.yml](docker-compose.example.yml) as a template 3. Create an `about.json` file using [about.example.json](about.example.json) as a template
4. Run `docker-compose up -d` to start the server 4. Create a `docker-compose.yml` file using [docker-compose.example.yml](docker-compose.example.yml) as a template
5. Run `docker-compose up -d` to start the server
### 💻 Docker CLI ### 💻 Docker CLI
1. Create an `.env` file using [.env.example](.env.example) as a template 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 2. Create a `configuration.toml` file using [configuration.example.toml](configuration.example.toml) as a template
3. Start the container using the following command: 3. Create an `about.json` file using [about.example.json](about.example.json) as a template
4. Start the container using the following command:
```shell ```shell
docker run -d --name revanced-api \ docker run -d --name revanced-api \
# Mount the .env file # Mount the .env file
-v $(pwd)/.env:/app/.env \ -v $(pwd)/.env:/app/.env \
# Mount the configuration.toml file # Mount the configuration.toml file
-v $(pwd)/configuration.toml:/app/configuration.toml \ -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 # Mount the persistence folder
-v $(pwd)/persistence:/app/persistence \ -v $(pwd)/persistence:/app/persistence \
# Expose the port 8888 # Expose the port 8888
@ -132,7 +141,8 @@ A Java Runtime Environment (JRE) must be installed.
2. In the same folder, create an `.env` file using [.env.example](.env.example) as a template 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 3. In the same folder, create a `configuration.toml` file
using [configuration.example.toml](configuration.example.toml) as a template using [configuration.example.toml](configuration.example.toml) as a template
4. Run `java -jar revanced-api.jar start` to start the server 4. In the same folder, create an `about.json` file using [about.example.json](about.example.json) as a template
5. Run `java -jar revanced-api.jar start` to start the server
### 🛠️ From source ### 🛠️ From source
@ -141,13 +151,15 @@ 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 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 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 3. Copy [configuration.example.toml](configuration.example.toml) to `configuration.toml` and fill in the required values
4. Run `gradlew run --args=start` to start the server 4. Copy [about.example.json](about.example.json) to `about.json` and fill in the required values
5. Run `gradlew run --args=start` to start the server
## 📚 Everything else ## 📚 Everything else
### 📙 Contributing ### 📙 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 ### 🛠️ Building

View File

@ -1,9 +1,11 @@
{ {
"name": "ReVanced", "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.", "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": { "branding": {
"logo": "https://raw.githubusercontent.com/ReVanced/revanced-branding/main/assets/revanced-logo/revanced-logo.svg" "logo": "https://raw.githubusercontent.com/ReVanced/revanced-branding/main/assets/revanced-logo/revanced-logo.svg"
}, },
"status": "https://status.revanced.app",
"contact": { "contact": {
"email": "contact@revanced.app" "email": "contact@revanced.app"
}, },

View File

@ -48,6 +48,12 @@ kotlin {
} }
} }
tasks {
test {
useJUnitPlatform()
}
}
repositories { repositories {
mavenCentral() mavenCentral()
google() google()
@ -98,6 +104,8 @@ dependencies {
implementation(libs.caffeine) implementation(libs.caffeine)
implementation(libs.bouncy.castle.provider) implementation(libs.bouncy.castle.provider)
implementation(libs.bouncy.castle.pgp) implementation(libs.bouncy.castle.pgp)
testImplementation(kotlin("test"))
} }
// The maven-publish plugin is necessary to make signing work. // The maven-publish plugin is necessary to make signing work.

View File

@ -1,19 +1,29 @@
organization = "revanced" api-version = "v1"
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" }
manager = { repository = "revanced-manager", asset-regex = "apk$" }
contributors-repositories = [
"revanced-patcher",
"revanced-patches",
"revanced-integrations",
"revanced-website",
"revanced-cli",
"revanced-manager",
]
api-version = 1
cors-allowed-hosts = [ cors-allowed-hosts = [
"revanced.app", "revanced.app",
"*.revanced.app" "*.revanced.app"
] ]
endpoint = "https://api.revanced.app" endpoint = "https://api.revanced.app"
old-api-endpoint = "https://old-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"

View File

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

View File

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

View File

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

Binary file not shown.

View File

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

5
gradlew vendored
View File

@ -15,6 +15,8 @@
# See the License for the specific language governing permissions and # See the License for the specific language governing permissions and
# limitations under the License. # limitations under the License.
# #
# SPDX-License-Identifier: Apache-2.0
#
############################################################################## ##############################################################################
# #
@ -84,7 +86,8 @@ done
# shellcheck disable=SC2034 # shellcheck disable=SC2034
APP_BASE_NAME=${0##*/} APP_BASE_NAME=${0##*/}
# Discard cd standard output in case $CDPATH is set (https://github.com/gradle/gradle/issues/25036) # Discard cd standard output in case $CDPATH is set (https://github.com/gradle/gradle/issues/25036)
APP_HOME=$( cd "${APP_HOME:-./}" > /dev/null && pwd -P ) || exit APP_HOME=$( cd -P "${APP_HOME:-./}" > /dev/null && printf '%s
' "$PWD" ) || exit
# Use the maximum available, or set MAX_FD != -1 to use that value. # Use the maximum available, or set MAX_FD != -1 to use that value.
MAX_FD=maximum MAX_FD=maximum

2
gradlew.bat vendored
View File

@ -13,6 +13,8 @@
@rem See the License for the specific language governing permissions and @rem See the License for the specific language governing permissions and
@rem limitations under the License. @rem limitations under the License.
@rem @rem
@rem SPDX-License-Identifier: Apache-2.0
@rem
@if "%DEBUG%"=="" @echo off @if "%DEBUG%"=="" @echo off
@rem ########################################################################## @rem ##########################################################################

3648
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", "@saithodev/semantic-release-backmerge": "^4.0.1",
"@semantic-release/changelog": "^6.0.3", "@semantic-release/changelog": "^6.0.3",
"@semantic-release/git": "^10.0.1", "@semantic-release/git": "^10.0.1",
"gradle-semantic-release-plugin": "^1.9.2", "gradle-semantic-release-plugin": "^1.10.1",
"semantic-release": "^24.0.0" "semantic-release": "^24.1.2"
} }
} }

View File

@ -1,6 +1,7 @@
package app.revanced.api.command package app.revanced.api.command
import app.revanced.api.configuration.* import app.revanced.api.configuration.*
import io.github.cdimascio.dotenv.Dotenv
import io.ktor.server.engine.* import io.ktor.server.engine.*
import io.ktor.server.jetty.* import io.ktor.server.jetty.*
import picocli.CommandLine import picocli.CommandLine
@ -33,6 +34,8 @@ internal object StartAPICommand : Runnable {
private var configFile = File("configuration.toml") private var configFile = File("configuration.toml")
override fun run() { override fun run() {
Dotenv.configure().systemProperties().load()
embeddedServer(Jetty, port, host) { embeddedServer(Jetty, port, host) {
configureDependencies(configFile) configureDependencies(configFile)
configureHTTP() configureHTTP()

View File

@ -0,0 +1,170 @@
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,131 +5,51 @@ import app.revanced.api.configuration.repository.BackendRepository
import app.revanced.api.configuration.repository.ConfigurationRepository import app.revanced.api.configuration.repository.ConfigurationRepository
import app.revanced.api.configuration.repository.GitHubBackendRepository import app.revanced.api.configuration.repository.GitHubBackendRepository
import app.revanced.api.configuration.services.* 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.Toml
import com.akuleshov7.ktoml.source.decodeFromStream 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 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.Database
import org.jetbrains.exposed.sql.transactions.TransactionManager
import org.koin.core.module.dsl.singleOf import org.koin.core.module.dsl.singleOf
import org.koin.core.parameter.parameterArrayOf
import org.koin.dsl.module import org.koin.dsl.module
import org.koin.ktor.plugin.Koin import org.koin.ktor.plugin.Koin
import java.io.File import java.io.File
@OptIn(ExperimentalSerializationApi::class)
fun Application.configureDependencies( fun Application.configureDependencies(
configFile: File, configFile: File,
) { ) {
val globalModule = module {
single {
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 { val repositoryModule = module {
single<BackendRepository> { single<ConfigurationRepository> { Toml.decodeFromStream(configFile.inputStream()) }
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)
},
)
}
single<ConfigurationRepository> {
Toml.decodeFromStream(configFile.inputStream())
}
single { single {
val dotenv = get<Dotenv>() Database.connect(
url = System.getProperty("DB_URL"),
TransactionManager.defaultDatabase = Database.connect( user = System.getProperty("DB_USER"),
url = dotenv["DB_URL"], password = System.getProperty("DB_PASSWORD"),
user = dotenv["DB_USER"], )
password = dotenv["DB_PASSWORD"], }
singleOf(::AnnouncementRepository)
singleOf(::GitHubBackendRepository)
single<BackendRepository> {
val backendServices = mapOf(
GitHubBackendRepository.SERVICE_NAME to { get<GitHubBackendRepository>() },
// Implement more backend services here.
) )
AnnouncementRepository() val configuration = get<ConfigurationRepository>()
val backendFactory = backendServices[configuration.backendServiceName]!!
backendFactory()
} }
} }
val serviceModule = module { val serviceModule = module {
single { single {
val dotenv = get<Dotenv>() val jwtSecret = System.getProperty("JWT_SECRET")
val issuer = System.getProperty("JWT_ISSUER")
val validityInMin = System.getProperty("JWT_VALIDITY_IN_MIN").toLong()
val jwtSecret = dotenv["JWT_SECRET"] val authSHA256DigestString = System.getProperty("AUTH_SHA256_DIGEST")
val issuer = dotenv["JWT_ISSUER"]
val validityInMin = dotenv["JWT_VALIDITY_IN_MIN"].toInt()
val authSHA256DigestString = dotenv["AUTH_SHA256_DIGEST"] AuthenticationService(issuer, validityInMin, jwtSecret, authSHA256DigestString)
AuthService(issuer, validityInMin, jwtSecret, authSHA256DigestString)
}
single {
val configuration = get<ConfigurationRepository>()
OldApiService(
get {
parameterArrayOf(configuration.oldApiEndpoint)
},
)
} }
singleOf(::AnnouncementService) singleOf(::AnnouncementService)
singleOf(::SignatureService) singleOf(::SignatureService)
@ -140,7 +60,6 @@ fun Application.configureDependencies(
install(Koin) { install(Koin) {
modules( modules(
globalModule,
repositoryModule, repositoryModule,
serviceModule, serviceModule,
) )

View File

@ -1,11 +1,16 @@
package app.revanced.api.configuration package app.revanced.api.configuration
import io.bkbn.kompendium.core.metadata.MethodInfo
import io.bkbn.kompendium.core.plugin.NotarizedRoute import io.bkbn.kompendium.core.plugin.NotarizedRoute
import io.ktor.http.* import io.ktor.http.*
import io.ktor.http.content.* import io.ktor.http.content.*
import io.ktor.server.application.* import io.ktor.server.application.*
import io.ktor.server.http.content.*
import io.ktor.server.plugins.cachingheaders.* import io.ktor.server.plugins.cachingheaders.*
import io.ktor.server.response.* import io.ktor.server.response.*
import io.ktor.server.routing.*
import java.io.File
import java.nio.file.Path
import kotlin.time.Duration import kotlin.time.Duration
internal suspend fun ApplicationCall.respondOrNotFound(value: Any?) = respond(value ?: HttpStatusCode.NotFound) internal suspend fun ApplicationCall.respondOrNotFound(value: Any?) = respond(value ?: HttpStatusCode.NotFound)
@ -25,3 +30,22 @@ internal fun ApplicationCallPipeline.installCache(cacheControl: CacheControl) =
internal fun ApplicationCallPipeline.installNotarizedRoute(configure: NotarizedRoute.Config.() -> Unit = {}) = internal fun ApplicationCallPipeline.installNotarizedRoute(configure: NotarizedRoute.Config.() -> Unit = {}) =
install(NotarizedRoute(), configure) 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,6 +1,7 @@
package app.revanced.api.configuration package app.revanced.api.configuration
import app.revanced.api.configuration.repository.ConfigurationRepository import app.revanced.api.configuration.repository.ConfigurationRepository
import io.ktor.http.*
import io.ktor.server.application.* import io.ktor.server.application.*
import io.ktor.server.plugins.* import io.ktor.server.plugins.*
import io.ktor.server.plugins.cors.routing.* import io.ktor.server.plugins.cors.routing.*
@ -10,14 +11,19 @@ import org.koin.ktor.ext.get
import kotlin.time.Duration.Companion.minutes import kotlin.time.Duration.Companion.minutes
fun Application.configureHTTP() { fun Application.configureHTTP() {
val configurationRepository = get<ConfigurationRepository>() val configuration = get<ConfigurationRepository>()
install(CORS) { install(CORS) {
configurationRepository.corsAllowedHosts.forEach { host -> HttpMethod.DefaultMethods.minus(HttpMethod.Options).forEach(::allowMethod)
allowHost(
host = host, allowHeader(HttpHeaders.ContentType)
schemes = listOf("http", "https") allowHeader(HttpHeaders.Authorization)
) exposeHeader(HttpHeaders.WWWAuthenticate)
allowCredentials = true
configuration.corsAllowedHosts.forEach { host ->
allowHost(host = host, schemes = listOf("https"))
} }
} }

View File

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

View File

@ -4,13 +4,11 @@ import app.revanced.api.configuration.repository.ConfigurationRepository
import app.revanced.api.configuration.routes.* import app.revanced.api.configuration.routes.*
import app.revanced.api.configuration.routes.announcementsRoute import app.revanced.api.configuration.routes.announcementsRoute
import app.revanced.api.configuration.routes.apiRoute 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.patchesRoute
import io.bkbn.kompendium.core.routes.redoc import io.bkbn.kompendium.core.routes.redoc
import io.bkbn.kompendium.core.routes.swagger import io.bkbn.kompendium.core.routes.swagger
import io.ktor.http.* import io.ktor.http.*
import io.ktor.server.application.* import io.ktor.server.application.*
import io.ktor.server.http.content.*
import io.ktor.server.routing.* import io.ktor.server.routing.*
import kotlin.time.Duration.Companion.minutes import kotlin.time.Duration.Companion.minutes
import org.koin.ktor.ext.get as koinGet import org.koin.ktor.ext.get as koinGet
@ -20,21 +18,41 @@ internal fun Application.configureRouting() = routing {
installCache(5.minutes) installCache(5.minutes)
route("/v${configuration.apiVersion}") { route("/${configuration.apiVersion}") {
announcementsRoute() announcementsRoute()
patchesRoute() patchesRoute()
managerRoute() managerRoute()
apiRoute() apiRoute()
} }
staticResources("/", "/app/revanced/api/static/root") { staticFiles("/", configuration.staticFilesPath) {
contentType { ContentType.Application.Json } contentType {
extensions("json") 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 = "/") extensions("json", "asc")
redoc(pageTitle = "ReVanced API", path = "/redoc") }
// TODO: Remove, once migration period from v2 API is over (In 1-2 years). val specUrl = "/${configuration.apiVersion}/openapi.json"
oldApiRoute() swagger(pageTitle = "ReVanced API", path = "/", specUrl = specUrl)
redoc(pageTitle = "ReVanced API", path = "/redoc", specUrl = specUrl)
} }

View File

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

View File

@ -1,12 +1,11 @@
package app.revanced.api.configuration.repository package app.revanced.api.configuration.repository
import app.revanced.api.configuration.schema.APIAnnouncement import app.revanced.api.configuration.ApiAnnouncement
import app.revanced.api.configuration.schema.APIResponseAnnouncement import app.revanced.api.configuration.ApiAnnouncementTag
import app.revanced.api.configuration.schema.APIResponseAnnouncementId import app.revanced.api.configuration.ApiResponseAnnouncement
import app.revanced.api.configuration.ApiResponseAnnouncementId
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.awaitAll
import kotlinx.coroutines.runBlocking import kotlinx.coroutines.runBlocking
import kotlinx.datetime.*
import org.jetbrains.exposed.dao.IntEntity import org.jetbrains.exposed.dao.IntEntity
import org.jetbrains.exposed.dao.IntEntityClass import org.jetbrains.exposed.dao.IntEntityClass
import org.jetbrains.exposed.dao.id.EntityID import org.jetbrains.exposed.dao.id.EntityID
@ -15,136 +14,178 @@ import org.jetbrains.exposed.sql.*
import org.jetbrains.exposed.sql.kotlin.datetime.CurrentDateTime import org.jetbrains.exposed.sql.kotlin.datetime.CurrentDateTime
import org.jetbrains.exposed.sql.kotlin.datetime.datetime import org.jetbrains.exposed.sql.kotlin.datetime.datetime
import org.jetbrains.exposed.sql.transactions.experimental.newSuspendedTransaction import org.jetbrains.exposed.sql.transactions.experimental.newSuspendedTransaction
import org.jetbrains.exposed.sql.transactions.experimental.suspendedTransactionAsync
internal class AnnouncementRepository { internal class AnnouncementRepository(private val database: Database) {
// This is better than doing a maxByOrNull { it.id }. // This is better than doing a maxByOrNull { it.id } on every request.
private var latestAnnouncement: Announcement? = null private var latestAnnouncement: Announcement? = null
private val latestAnnouncementByChannel = mutableMapOf<String, Announcement>() private val latestAnnouncementByTag = mutableMapOf<String, Announcement>()
private fun updateLatestAnnouncement(new: Announcement) {
if (latestAnnouncement?.id?.value == new.id.value) {
latestAnnouncement = new
latestAnnouncementByChannel[new.channel ?: return] = new
}
}
init { init {
runBlocking { runBlocking {
transaction { transaction {
SchemaUtils.create(Announcements, Attachments) SchemaUtils.create(
Announcements,
Attachments,
Tags,
AnnouncementTags,
)
// Initialize the latest announcement. initializeLatestAnnouncements()
latestAnnouncement = Announcement.all().onEach {
latestAnnouncementByChannel[it.channel ?: return@onEach] = it
}.maxByOrNull { it.id } ?: return@transaction
} }
} }
} }
suspend fun all() = transaction { private fun initializeLatestAnnouncements() {
Announcement.all().map { it.toApi() } latestAnnouncement = Announcement.all().orderBy(Announcements.id to SortOrder.DESC).firstOrNull()
Tag.all().map { it.name }.forEach(::updateLatestAnnouncementForTag)
} }
suspend fun all(channel: String) = transaction { private fun updateLatestAnnouncement(new: Announcement) {
Announcement.find { Announcements.channel eq channel }.map { it.toApi() } 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 delete(id: Int) = transaction { suspend fun delete(id: Int) = transaction {
val announcement = Announcement.findById(id) ?: return@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() announcement.delete()
// In case the latest announcement was deleted, query the new latest announcement again. // If the deleted announcement is the latest announcement, set the new latest announcement.
if (latestAnnouncement?.id?.value == id) { if (latestAnnouncement?.id?.value == id) {
latestAnnouncement = Announcement.all().maxByOrNull { it.id } latestAnnouncement = Announcement.all().orderBy(Announcements.id to SortOrder.DESC).firstOrNull()
// If no latest announcement was found, remove it from the channel map.
if (latestAnnouncement == null) {
latestAnnouncementByChannel.remove(announcement.channel)
} else {
latestAnnouncementByChannel[latestAnnouncement!!.channel ?: return@transaction] = latestAnnouncement!!
} }
// The new announcement may be the latest for a specific tag. Set the new latest announcement for that tag.
latestAnnouncementByTag.keys.forEach { tag ->
updateLatestAnnouncementForTag(tag)
} }
} }
fun latest() = latestAnnouncement?.toApi() suspend fun tags() = transaction {
Tag.all().toList().toApiTag()
fun latest(channel: String) = latestAnnouncementByChannel[channel]?.toApi()
fun latestId() = latest()?.id?.toApi()
fun latestId(channel: String) = latest(channel)?.id?.toApi()
suspend fun archive(
id: Int,
archivedAt: LocalDateTime?,
) = transaction {
Announcement.findByIdAndUpdate(id) {
it.archivedAt = archivedAt ?: java.time.LocalDateTime.now().toKotlinLocalDateTime()
}?.also(::updateLatestAnnouncement)
} }
suspend fun unarchive(id: Int) = transaction { private suspend fun <T> transaction(statement: suspend Transaction.() -> T) = newSuspendedTransaction(Dispatchers.IO, database, statement = statement)
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() { private object Announcements : IntIdTable() {
val author = varchar("author", 32).nullable() val author = varchar("author", 32).nullable()
val title = varchar("title", 64) val title = varchar("title", 64)
val content = text("content").nullable() val content = text("content").nullable()
val channel = varchar("channel", 16).nullable()
val createdAt = datetime("createdAt").defaultExpression(CurrentDateTime) val createdAt = datetime("createdAt").defaultExpression(CurrentDateTime)
val archivedAt = datetime("archivedAt").nullable() val archivedAt = datetime("archivedAt").nullable()
val level = integer("level") val level = integer("level")
@ -155,6 +196,19 @@ internal class AnnouncementRepository {
val announcement = reference("announcement", Announcements, onDelete = ReferenceOption.CASCADE) 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) { class Announcement(id: EntityID<Int>) : IntEntity(id) {
companion object : IntEntityClass<Announcement>(Announcements) companion object : IntEntityClass<Announcement>(Announcements)
@ -162,7 +216,7 @@ internal class AnnouncementRepository {
var title by Announcements.title var title by Announcements.title
var content by Announcements.content var content by Announcements.content
val attachments by Attachment referrersOn Attachments.announcement val attachments by Attachment referrersOn Attachments.announcement
var channel by Announcements.channel var tags by Tag via AnnouncementTags
var createdAt by Announcements.createdAt var createdAt by Announcements.createdAt
var archivedAt by Announcements.archivedAt var archivedAt by Announcements.archivedAt
var level by Announcements.level var level by Announcements.level
@ -175,17 +229,32 @@ internal class AnnouncementRepository {
var announcement by Announcement referencedOn Attachments.announcement var announcement by Announcement referencedOn Attachments.announcement
} }
private fun Announcement.toApi() = APIResponseAnnouncement( class Tag(id: EntityID<Int>) : IntEntity(id) {
companion object : IntEntityClass<Tag>(Tags)
var name by Tags.name
var announcements by Announcement via AnnouncementTags
}
private fun Announcement?.toApiResponseAnnouncement() = this?.let {
ApiResponseAnnouncement(
id.value, id.value,
author, author,
title, title,
content, content,
attachments.map { it.url }, attachments.map { it.url },
channel, tags.map { it.name },
createdAt, createdAt,
archivedAt, archivedAt,
level, level,
) )
}
private fun Int.toApi() = APIResponseAnnouncementId(this)
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() }
} }

View File

@ -1,16 +1,59 @@
package app.revanced.api.configuration.repository package app.revanced.api.configuration.repository
import io.ktor.client.* 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.datetime.LocalDateTime
import kotlinx.serialization.json.Json
import kotlinx.serialization.json.JsonNamingStrategy
/** /**
* The backend of the API used to get data. * The backend of the API used to get data.
* *
* @param client The HTTP client to use for requests. * @param defaultRequestUri The URI to use for requests.
* @param website The site of the backend users can visit.
*/ */
abstract class BackendRepository internal constructor( abstract class BackendRepository internal constructor(
protected val client: HttpClient, defaultRequestUri: String,
internal val website: String,
) { ) {
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. * A user.
* *
@ -92,12 +135,14 @@ abstract class BackendRepository internal constructor(
* @property tag The tag of the release. * @property tag The tag of the release.
* @property assets The assets of the release. * @property assets The assets of the release.
* @property createdAt The date and time the release was created. * @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. * @property releaseNote The release note of the release.
*/ */
class BackendRelease( class BackendRelease(
val tag: String, val tag: String,
val releaseNote: String, val releaseNote: String,
val createdAt: LocalDateTime, val createdAt: LocalDateTime,
val prerelease: Boolean,
// Using a list instead of a set because set semantics are unnecessary here. // Using a list instead of a set because set semantics are unnecessary here.
val assets: List<BackendAsset>, val assets: List<BackendAsset>,
) { ) {
@ -137,13 +182,13 @@ abstract class BackendRepository internal constructor(
* *
* @param owner The owner of the repository. * @param owner The owner of the repository.
* @param repository The name of the repository. * @param repository The name of the repository.
* @param tag The tag of the release. If null, the latest release is returned. * @param prerelease Whether to get a prerelease.
* @return The release. * @return The release.
*/ */
abstract suspend fun release( abstract suspend fun release(
owner: String, owner: String,
repository: String, repository: String,
tag: String? = null, prerelease: Boolean,
): BackendOrganization.BackendRepository.BackendRelease ): BackendOrganization.BackendRepository.BackendRelease
/** /**
@ -153,7 +198,10 @@ abstract class BackendRepository internal constructor(
* @param repository The name of the repository. * @param repository The name of the repository.
* @return The contributors. * @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. * Get the members of an organization.

View File

@ -1,7 +1,9 @@
package app.revanced.api.configuration.repository 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.ManagerService
import app.revanced.api.configuration.services.PatchesService import app.revanced.api.configuration.services.PatchesService
import kotlinx.serialization.ExperimentalSerializationApi
import kotlinx.serialization.KSerializer import kotlinx.serialization.KSerializer
import kotlinx.serialization.SerialName import kotlinx.serialization.SerialName
import kotlinx.serialization.Serializable import kotlinx.serialization.Serializable
@ -10,37 +12,58 @@ import kotlinx.serialization.descriptors.PrimitiveSerialDescriptor
import kotlinx.serialization.descriptors.SerialDescriptor import kotlinx.serialization.descriptors.SerialDescriptor
import kotlinx.serialization.encoding.Decoder import kotlinx.serialization.encoding.Decoder
import kotlinx.serialization.encoding.Encoder 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.io.File
import java.nio.file.Path
import kotlin.io.path.createDirectories
/** /**
* The repository storing the configuration for the API. * The repository storing the configuration for the API.
* *
* @property organization The API backends organization name where the repositories for the patches and integrations are. * @property organization The API backends organization name where the repositories are.
* @property patches The source of the patches. * @property patches The source of the patches.
* @property integrations The source of the integrations.
* @property manager The source of the manager. * @property manager The source of the manager.
* @property contributorsRepositoryNames The names of the repositories to get contributors from. * @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 apiVersion The version to use for the API. * @property apiVersion The version to use for the API.
* @property corsAllowedHosts The hosts allowed to make requests to the API. * @property corsAllowedHosts The hosts allowed to make requests to the API.
* @property endpoint The endpoint of the API. * @property endpoint The endpoint of the API.
* @property oldApiEndpoint The endpoint of the old API to proxy requests to. * @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 @Serializable
internal class ConfigurationRepository( internal class ConfigurationRepository(
val organization: String, val organization: String,
val patches: SignedAssetConfiguration, val patches: SignedAssetConfiguration,
val integrations: SignedAssetConfiguration,
val manager: AssetConfiguration, val manager: AssetConfiguration,
@SerialName("contributors-repositories") @SerialName("contributors-repositories")
val contributorsRepositoryNames: Set<String>, val contributorsRepositoryNames: Map<String, String>,
@SerialName("backend-service-name")
val backendServiceName: String,
@SerialName("api-version") @SerialName("api-version")
val apiVersion: Int = 1, val apiVersion: String = "v1",
@SerialName("cors-allowed-hosts") @SerialName("cors-allowed-hosts")
val corsAllowedHosts: Set<String>, val corsAllowedHosts: Set<String>,
val endpoint: String, val endpoint: String,
@SerialName("old-api-endpoint") @Serializable(with = PathSerializer::class)
val oldApiEndpoint: String, @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. * Am asset configuration whose asset is signed.
* *
@ -108,3 +131,23 @@ private object FileSerializer : KSerializer<File> {
override fun deserialize(decoder: Decoder) = File(decoder.decodeString()) 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,25 +8,26 @@ import app.revanced.api.configuration.repository.GitHubOrganization.GitHubReposi
import app.revanced.api.configuration.repository.GitHubOrganization.GitHubRepository.GitHubRelease 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.Contributors
import app.revanced.api.configuration.repository.Organization.Repository.Releases import app.revanced.api.configuration.repository.Organization.Repository.Releases
import io.ktor.client.*
import io.ktor.client.call.* import io.ktor.client.call.*
import io.ktor.client.plugins.resources.* import io.ktor.client.plugins.resources.*
import io.ktor.resources.* import io.ktor.resources.*
import kotlinx.coroutines.* import kotlinx.coroutines.async
import kotlinx.coroutines.awaitAll
import kotlinx.coroutines.coroutineScope
import kotlinx.datetime.Instant import kotlinx.datetime.Instant
import kotlinx.datetime.TimeZone import kotlinx.datetime.TimeZone
import kotlinx.datetime.toLocalDateTime import kotlinx.datetime.toLocalDateTime
import kotlinx.serialization.SerialName import kotlinx.serialization.SerialName
import kotlinx.serialization.Serializable import kotlinx.serialization.Serializable
class GitHubBackendRepository(client: HttpClient) : BackendRepository(client) { class GitHubBackendRepository : BackendRepository("https://api.github.com", "https://github.com") {
override suspend fun release( override suspend fun release(
owner: String, owner: String,
repository: String, repository: String,
tag: String?, prerelease: Boolean,
): BackendRelease { ): BackendRelease {
val release: GitHubRelease = if (tag != null) { val release: GitHubRelease = if (prerelease) {
client.get(Releases.Tag(owner, repository, tag)).body() client.get(Releases(owner, repository)).body<List<GitHubRelease>>().first { it.prerelease }
} else { } else {
client.get(Releases.Latest(owner, repository)).body() client.get(Releases.Latest(owner, repository)).body()
} }
@ -35,6 +36,7 @@ class GitHubBackendRepository(client: HttpClient) : BackendRepository(client) {
tag = release.tagName, tag = release.tagName,
releaseNote = release.body, releaseNote = release.body,
createdAt = release.createdAt.toLocalDateTime(TimeZone.UTC), createdAt = release.createdAt.toLocalDateTime(TimeZone.UTC),
prerelease = release.prerelease,
assets = release.assets.map { assets = release.assets.map {
BackendAsset( BackendAsset(
name = it.name, name = it.name,
@ -67,7 +69,8 @@ class GitHubBackendRepository(client: HttpClient) : BackendRepository(client) {
override suspend fun members(organization: String): List<BackendMember> { override suspend fun members(organization: String): List<BackendMember> {
// Get the list of members of the organization. // Get the list of members of the organization.
val publicMembers: List<GitHubOrganization.GitHubMember> = client.get(Organization.PublicMembers(organization)).body() val publicMembers: List<GitHubOrganization.GitHubMember> =
client.get(Organization.PublicMembers(organization)).body()
return coroutineScope { return coroutineScope {
publicMembers.map { member -> publicMembers.map { member ->
@ -98,7 +101,7 @@ class GitHubBackendRepository(client: HttpClient) : BackendRepository(client) {
gpgKeys = gpgKeys =
BackendMember.GpgKeys( BackendMember.GpgKeys(
ids = gpgKeys.map { it.keyId }, ids = gpgKeys.map { it.keyId },
url = "https://api.github.com/users/${user.login}.gpg", url = "https://github.com/${user.login}.gpg",
), ),
) )
} }
@ -113,6 +116,10 @@ class GitHubBackendRepository(client: HttpClient) : BackendRepository(client) {
reset = Instant.fromEpochSeconds(rateLimit.rate.reset).toLocalDateTime(TimeZone.UTC), reset = Instant.fromEpochSeconds(rateLimit.rate.reset).toLocalDateTime(TimeZone.UTC),
) )
} }
companion object {
const val SERVICE_NAME = "GitHub"
}
} }
interface IGitHubUser { interface IGitHubUser {
@ -157,6 +164,7 @@ class GitHubOrganization {
// Using a list instead of a set because set semantics are unnecessary here. // Using a list instead of a set because set semantics are unnecessary here.
val assets: List<GitHubAsset>, val assets: List<GitHubAsset>,
val createdAt: Instant, val createdAt: Instant,
val prerelease: Boolean,
val body: String, val body: String,
) { ) {
@Serializable @Serializable
@ -194,10 +202,8 @@ class Organization {
@Resource("/repos/{owner}/{repo}/contributors") @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, @SerialName("per_page") val perPage: Int = 100)
class Releases { @Resource("/repos/{owner}/{repo}/releases")
@Resource("/repos/{owner}/{repo}/releases/tags/{tag}") class Releases(val owner: String, val repo: String) {
class Tag(val owner: String, val repo: String, val tag: String)
@Resource("/repos/{owner}/{repo}/releases/latest") @Resource("/repos/{owner}/{repo}/releases/latest")
class Latest(val owner: String, val repo: String) class Latest(val owner: String, val repo: String)
} }

View File

@ -1,12 +1,12 @@
package app.revanced.api.configuration.routes 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.installCache
import app.revanced.api.configuration.installNotarizedRoute import app.revanced.api.configuration.installNotarizedRoute
import app.revanced.api.configuration.respondOrNotFound 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 app.revanced.api.configuration.services.AnnouncementService
import io.bkbn.kompendium.core.metadata.DeleteInfo import io.bkbn.kompendium.core.metadata.DeleteInfo
import io.bkbn.kompendium.core.metadata.GetInfo import io.bkbn.kompendium.core.metadata.GetInfo
@ -18,7 +18,6 @@ import io.ktor.http.*
import io.ktor.server.application.* import io.ktor.server.application.*
import io.ktor.server.auth.* import io.ktor.server.auth.*
import io.ktor.server.plugins.ratelimit.* import io.ktor.server.plugins.ratelimit.*
import io.ktor.server.request.*
import io.ktor.server.response.* import io.ktor.server.response.*
import io.ktor.server.routing.* import io.ktor.server.routing.*
import io.ktor.server.util.* import io.ktor.server.util.*
@ -34,139 +33,177 @@ internal fun Route.announcementsRoute() = route("announcements") {
rateLimit(RateLimitName("strong")) { rateLimit(RateLimitName("strong")) {
get { get {
call.respond(announcementService.all()) val cursor = call.parameters["cursor"]?.toInt() ?: Int.MAX_VALUE
val count = call.parameters["count"]?.toInt() ?: 16
val tags = call.parameters.getAll("tag")
call.respond(announcementService.paged(cursor, count, tags?.toSet()))
} }
} }
rateLimit(RateLimitName("strong")) { rateLimit(RateLimitName("weak")) {
route("{channel}/latest") { authenticate("jwt") {
installLatestChannelAnnouncementRouteDocumentation() post<ApiAnnouncement> { announcement ->
announcementService.new(announcement)
get { call.respond(HttpStatusCode.OK)
val channel: String by call.parameters
call.respondOrNotFound(announcementService.latest(channel))
}
route("id") {
installLatestChannelAnnouncementIdRouteDocumentation()
get {
val channel: String by call.parameters
call.respondOrNotFound(announcementService.latestId(channel))
}
}
} }
} }
rateLimit(RateLimitName("strong")) {
route("{channel}") {
installChannelAnnouncementsRouteDocumentation()
get {
val channel: String by call.parameters
call.respond(announcementService.all(channel))
}
}
}
rateLimit(RateLimitName("strong")) {
route("latest") { route("latest") {
installLatestAnnouncementRouteDocumentation() installAnnouncementsLatestRouteDocumentation()
get { get {
val tags = call.parameters.getAll("tag")
if (tags?.isNotEmpty() == true) {
call.respond(announcementService.latest(tags.toSet()))
} else {
call.respondOrNotFound(announcementService.latest()) call.respondOrNotFound(announcementService.latest())
} }
}
route("id") { route("id") {
installLatestAnnouncementIdRouteDocumentation() installAnnouncementsLatestIdRouteDocumentation()
get { get {
val tags = call.parameters.getAll("tag")
if (tags?.isNotEmpty() == true) {
call.respond(announcementService.latestId(tags.toSet()))
} else {
call.respondOrNotFound(announcementService.latestId()) call.respondOrNotFound(announcementService.latestId())
} }
} }
} }
} }
rateLimit(RateLimitName("strong")) { route("{id}") {
authenticate("jwt") { installAnnouncementsIdRouteDocumentation()
installAnnouncementRouteDocumentation()
post<APIAnnouncement> { announcement -> get {
announcementService.new(announcement) val id: Int by call.parameters
call.respondOrNotFound(announcementService.get(id))
} }
route("{id}") { authenticate("jwt") {
installAnnouncementIdRouteDocumentation() patch<ApiAnnouncement> { announcement ->
patch<APIAnnouncement> { announcement ->
val id: Int by call.parameters val id: Int by call.parameters
announcementService.update(id, announcement) announcementService.update(id, announcement)
call.respond(HttpStatusCode.OK)
} }
delete { delete {
val id: Int by call.parameters val id: Int by call.parameters
announcementService.delete(id) announcementService.delete(id)
call.respond(HttpStatusCode.OK)
} }
route("archive") {
installAnnouncementArchiveRouteDocumentation()
post {
val id: Int by call.parameters
val archivedAt = call.receiveNullable<APIAnnouncementArchivedAt>()?.archivedAt
announcementService.archive(id, archivedAt)
} }
} }
route("unarchive") { route("tags") {
installAnnouncementUnarchiveRouteDocumentation() installAnnouncementsTagsRouteDocumentation()
post { get {
val id: Int by call.parameters call.respond(announcementService.tags())
announcementService.unarchive(id)
}
}
} }
} }
} }
} }
private fun Route.installAnnouncementRouteDocumentation() = installNotarizedRoute { private val authHeaderParameter = Parameter(
name = "Authorization",
`in` = Parameter.Location.header,
schema = TypeDefinition.STRING,
required = true,
examples = mapOf("Bearer authentication" to Parameter.Example("Bearer abc123")),
)
private fun Route.installAnnouncementsRouteDocumentation() = installNotarizedRoute {
tags = setOf("Announcements") 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 { post = PostInfo.builder {
description("Create a new announcement") description("Create a new announcement")
summary("Create announcement") summary("Create announcement")
parameters(authHeaderParameter)
request { request {
requestType<APIAnnouncement>() requestType<ApiAnnouncement>()
description("The new announcement") description("The new announcement")
} }
response { response {
description("When the announcement was created") description("The announcement is created")
responseCode(HttpStatusCode.OK) responseCode(HttpStatusCode.OK)
responseType<Unit>() responseType<Unit>()
} }
canRespondUnauthorized()
} }
} }
private fun Route.installLatestAnnouncementRouteDocumentation() = installNotarizedRoute { private fun Route.installAnnouncementsLatestRouteDocumentation() = installNotarizedRoute {
tags = setOf("Announcements") tags = setOf("Announcements")
get = GetInfo.builder { get = GetInfo.builder {
description("Get the latest announcement") description("Get the latest announcement")
summary("Get 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 { response {
responseCode(HttpStatusCode.OK) responseCode(HttpStatusCode.OK)
mediaTypes("application/json") mediaTypes("application/json")
description("The latest announcement") description("The latest announcement")
responseType<APIResponseAnnouncement>() responseType<ApiResponseAnnouncement>()
}
canRespond {
responseCode(HttpStatusCode.OK)
mediaTypes("application/json")
description("The latest announcements")
responseType<Set<ApiResponseAnnouncement>>()
} }
canRespond { canRespond {
responseCode(HttpStatusCode.NotFound) responseCode(HttpStatusCode.NotFound)
@ -176,83 +213,42 @@ private fun Route.installLatestAnnouncementRouteDocumentation() = installNotariz
} }
} }
private fun Route.installLatestAnnouncementIdRouteDocumentation() = installNotarizedRoute { private fun Route.installAnnouncementsLatestIdRouteDocumentation() = installNotarizedRoute {
tags = setOf("Announcements") tags = setOf("Announcements")
get = GetInfo.builder { get = GetInfo.builder {
description("Get the id of the latest announcement") description("Get the ID of the latest announcement")
summary("Get id of latest announcement") summary("Get ID of latest announcement")
response { parameters(
responseCode(HttpStatusCode.OK)
mediaTypes("application/json")
description("The id of the latest announcement")
responseType<APIResponseAnnouncementId>()
}
canRespond {
responseCode(HttpStatusCode.NotFound)
description("No announcement exists")
responseType<Unit>()
}
}
}
private fun Route.installChannelAnnouncementsRouteDocumentation() = installNotarizedRoute {
tags = setOf("Announcements")
parameters = listOf(
Parameter( Parameter(
name = "channel", name = "tag",
`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(
Parameter(
name = "id",
`in` = Parameter.Location.path,
schema = TypeDefinition.INT,
description = "The id of the announcement to archive",
required = true,
),
Parameter(
name = "archivedAt",
`in` = Parameter.Location.query, `in` = Parameter.Location.query,
schema = TypeDefinition.STRING, schema = TypeDefinition.STRING,
description = "The date and time the announcement to be archived", description = "The tags to filter the latest announcements by",
required = false, required = false,
), ),
) )
post = PostInfo.builder {
description("Archive an announcement")
summary("Archive announcement")
response { response {
description("When the announcement was archived")
responseCode(HttpStatusCode.OK) 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>>()
}
canRespond {
responseCode(HttpStatusCode.NotFound)
description("No announcement exists")
responseType<Unit>() responseType<Unit>()
} }
} }
} }
private fun Route.installAnnouncementUnarchiveRouteDocumentation() = installNotarizedRoute { private fun Route.installAnnouncementsIdRouteDocumentation() = installNotarizedRoute {
tags = setOf("Announcements") tags = setOf("Announcements")
parameters = listOf( parameters = listOf(
@ -260,131 +256,65 @@ private fun Route.installAnnouncementUnarchiveRouteDocumentation() = installNota
name = "id", name = "id",
`in` = Parameter.Location.path, `in` = Parameter.Location.path,
schema = TypeDefinition.INT, schema = TypeDefinition.INT,
description = "The id of the announcement to unarchive", description = "The ID of the announcement to update",
required = true, required = true,
), ),
authHeaderParameter,
) )
post = PostInfo.builder { get = GetInfo.builder {
description("Unarchive an announcement") description("Get an announcement")
summary("Unarchive announcement") summary("Get announcement")
response { response {
description("When announcement was unarchived") description("The announcement")
responseCode(HttpStatusCode.OK) responseCode(HttpStatusCode.OK)
responseType<ApiResponseAnnouncement>()
}
canRespond {
responseCode(HttpStatusCode.NotFound)
description("The announcement does not exist")
responseType<Unit>() 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 { patch = PatchInfo.builder {
description("Update an announcement") description("Update an announcement")
summary("Update announcement") summary("Update announcement")
request { request {
requestType<APIAnnouncement>() requestType<ApiAnnouncement>()
description("The new announcement") description("The new announcement")
} }
response { response {
description("When announcement was updated") description("The announcement is updated")
responseCode(HttpStatusCode.OK) responseCode(HttpStatusCode.OK)
responseType<Unit>() responseType<Unit>()
} }
canRespondUnauthorized()
} }
delete = DeleteInfo.builder { delete = DeleteInfo.builder {
description("Delete an announcement") description("Delete an announcement")
summary("Delete announcement") summary("Delete announcement")
response { response {
description("When the announcement was deleted") description("The announcement is deleted")
responseCode(HttpStatusCode.OK) responseCode(HttpStatusCode.OK)
responseType<Unit>() responseType<Unit>()
} }
canRespondUnauthorized()
} }
} }
private fun Route.installAnnouncementsRouteDocumentation() = installNotarizedRoute { private fun Route.installAnnouncementsTagsRouteDocumentation() = installNotarizedRoute {
tags = setOf("Announcements") tags = setOf("Announcements")
get = GetInfo.builder { get = GetInfo.builder {
description("Get the announcements") description("Get all announcement tags")
summary("Get announcements") summary("Get announcement tags")
response { response {
responseCode(HttpStatusCode.OK) responseCode(HttpStatusCode.OK)
mediaTypes("application/json") mediaTypes("application/json")
description("The announcements") description("The announcement tags")
responseType<Set<APIResponseAnnouncement>>() responseType<Set<String>>()
}
}
}
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,19 +1,19 @@
package app.revanced.api.configuration.routes package app.revanced.api.configuration.routes
import app.revanced.api.configuration.*
import app.revanced.api.configuration.installCache import app.revanced.api.configuration.installCache
import app.revanced.api.configuration.installNoCache import app.revanced.api.configuration.installNoCache
import app.revanced.api.configuration.installNotarizedRoute import app.revanced.api.configuration.installNotarizedRoute
import app.revanced.api.configuration.repository.ConfigurationRepository
import app.revanced.api.configuration.respondOrNotFound 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.ApiService
import app.revanced.api.configuration.services.AuthService import app.revanced.api.configuration.services.AuthenticationService
import io.bkbn.kompendium.core.metadata.* import io.bkbn.kompendium.core.metadata.*
import io.bkbn.kompendium.json.schema.definition.TypeDefinition
import io.bkbn.kompendium.oas.payload.Parameter
import io.ktor.http.* import io.ktor.http.*
import io.ktor.server.application.* import io.ktor.server.application.*
import io.ktor.server.auth.* import io.ktor.server.auth.*
import io.ktor.server.http.content.*
import io.ktor.server.plugins.ratelimit.* import io.ktor.server.plugins.ratelimit.*
import io.ktor.server.response.* import io.ktor.server.response.*
import io.ktor.server.routing.* import io.ktor.server.routing.*
@ -22,7 +22,7 @@ import org.koin.ktor.ext.get as koinGet
internal fun Route.apiRoute() { internal fun Route.apiRoute() {
val apiService = koinGet<ApiService>() val apiService = koinGet<ApiService>()
val authService = koinGet<AuthService>() val authenticationService = koinGet<AuthenticationService>()
rateLimit(RateLimitName("strong")) { rateLimit(RateLimitName("strong")) {
authenticate("auth-digest") { authenticate("auth-digest") {
@ -30,7 +30,7 @@ internal fun Route.apiRoute() {
installTokenRouteDocumentation() installTokenRouteDocumentation()
get { get {
call.respond(authService.newToken()) call.respond(authenticationService.newToken())
} }
} }
} }
@ -56,12 +56,22 @@ internal fun Route.apiRoute() {
} }
} }
route("about") {
installCache(1.days)
installAboutRouteDocumentation()
get {
call.respond(apiService.about)
}
}
route("ping") { route("ping") {
installNoCache() installNoCache()
installPingRouteDocumentation() installPingRouteDocumentation()
head { handle {
call.respond(HttpStatusCode.NoContent) call.respond(HttpStatusCode.NoContent)
} }
} }
@ -75,9 +85,21 @@ internal fun Route.apiRoute() {
} }
} }
staticResources("/", "/app/revanced/api/static/versioned") { staticFiles("/", apiService.versionedStaticFilesPath)
contentType { ContentType.Application.Json } }
extensions("json") }
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>()
} }
} }
} }
@ -92,7 +114,7 @@ private fun Route.installRateLimitRouteDocumentation() = installNotarizedRoute {
description("The rate limit of the backend") description("The rate limit of the backend")
mediaTypes("application/json") mediaTypes("application/json")
responseCode(HttpStatusCode.OK) responseCode(HttpStatusCode.OK)
responseType<APIRateLimit>() responseType<ApiRateLimit>()
} }
} }
} }
@ -121,7 +143,7 @@ private fun Route.installTeamRouteDocumentation() = installNotarizedRoute {
description("The list of team members") description("The list of team members")
mediaTypes("application/json") mediaTypes("application/json")
responseCode(HttpStatusCode.OK) responseCode(HttpStatusCode.OK)
responseType<Set<APIMember>>() responseType<Set<ApiMember>>()
} }
} }
} }
@ -142,16 +164,38 @@ private fun Route.installContributorsRouteDocumentation() = installNotarizedRout
} }
private fun Route.installTokenRouteDocumentation() = installNotarizedRoute { private fun Route.installTokenRouteDocumentation() = installNotarizedRoute {
val configuration = koinGet<ConfigurationRepository>()
tags = setOf("API") tags = setOf("API")
get = GetInfo.builder { get = GetInfo.builder {
description("Get a new authorization token") description("Get a new authorization token")
summary("Get 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 { response {
description("The authorization token") description("The authorization token")
mediaTypes("application/json") mediaTypes("application/json")
responseCode(HttpStatusCode.OK) responseCode(HttpStatusCode.OK)
responseType<String>() responseType<ApiToken>()
} }
canRespondUnauthorized()
} }
} }

View File

@ -1,11 +1,12 @@
package app.revanced.api.configuration.routes 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.installNotarizedRoute
import app.revanced.api.configuration.schema.APIManagerAsset
import app.revanced.api.configuration.schema.APIRelease
import app.revanced.api.configuration.schema.APIReleaseVersion
import app.revanced.api.configuration.services.ManagerService import app.revanced.api.configuration.services.ManagerService
import io.bkbn.kompendium.core.metadata.GetInfo 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.http.*
import io.ktor.server.application.* import io.ktor.server.application.*
import io.ktor.server.plugins.ratelimit.* import io.ktor.server.plugins.ratelimit.*
@ -16,51 +17,63 @@ import org.koin.ktor.ext.get as koinGet
internal fun Route.managerRoute() = route("manager") { internal fun Route.managerRoute() = route("manager") {
val managerService = koinGet<ManagerService>() val managerService = koinGet<ManagerService>()
route("latest") { installManagerRouteDocumentation()
installLatestManagerRouteDocumentation()
rateLimit(RateLimitName("weak")) { rateLimit(RateLimitName("weak")) {
get { get {
call.respond(managerService.latestRelease()) val prerelease = call.parameters["prerelease"]?.toBoolean() ?: false
call.respond(managerService.latestRelease(prerelease))
} }
route("version") { route("version") {
installLatestManagerVersionRouteDocumentation() installManagerVersionRouteDocumentation()
get { get {
call.respond(managerService.latestVersion()) val prerelease = call.parameters["prerelease"]?.toBoolean() ?: false
}
call.respond(managerService.latestVersion(prerelease))
} }
} }
} }
} }
private fun Route.installLatestManagerRouteDocumentation() = installNotarizedRoute { 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") tags = setOf("Manager")
get = GetInfo.builder { get = GetInfo.builder {
description("Get the latest manager release") description("Get the current manager release")
summary("Get latest manager release") summary("Get current manager release")
parameters(prereleaseParameter)
response { response {
description("The latest manager release") description("The latest manager release")
mediaTypes("application/json") mediaTypes("application/json")
responseCode(HttpStatusCode.OK) responseCode(HttpStatusCode.OK)
responseType<APIRelease<APIManagerAsset>>() responseType<ApiRelease>()
} }
} }
} }
private fun Route.installLatestManagerVersionRouteDocumentation() = installNotarizedRoute { private fun Route.installManagerVersionRouteDocumentation() = installNotarizedRoute {
tags = setOf("Manager") tags = setOf("Manager")
get = GetInfo.builder { get = GetInfo.builder {
description("Get the latest manager release version") description("Get the current manager release version")
summary("Get latest manager release version") summary("Get current manager release version")
parameters(prereleaseParameter)
response { response {
description("The latest manager release version") description("The current manager release version")
mediaTypes("application/json") mediaTypes("application/json")
responseCode(HttpStatusCode.OK) responseCode(HttpStatusCode.OK)
responseType<APIReleaseVersion>() responseType<ApiReleaseVersion>()
} }
} }
} }

View File

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

View File

@ -1,122 +0,0 @@
package app.revanced.api.configuration.schema
import kotlinx.datetime.LocalDateTime
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,
// Using a list instead of a set because set semantics are unnecessary here.
val contributors: List<APIContributor>,
)
@Serializable
class APIRelease<T>(
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<T>,
)
@Serializable
class APIManagerAsset(
val downloadUrl: String,
)
@Serializable
class APIPatchesAsset(
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,35 +1,29 @@
package app.revanced.api.configuration.services package app.revanced.api.configuration.services
import app.revanced.api.configuration.ApiAnnouncement
import app.revanced.api.configuration.repository.AnnouncementRepository 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( internal class AnnouncementService(
private val announcementRepository: AnnouncementRepository, private val announcementRepository: AnnouncementRepository,
) { ) {
fun latestId(channel: String): APIResponseAnnouncementId? = announcementRepository.latestId(channel) suspend fun latest(tags: Set<String>) = announcementRepository.latest(tags)
fun latestId(): APIResponseAnnouncementId? = announcementRepository.latestId()
fun latest(channel: String) = announcementRepository.latest(channel) suspend fun latest() = announcementRepository.latest()
fun latest() = announcementRepository.latest()
suspend fun all(channel: String) = announcementRepository.all(channel) fun latestId(tags: Set<String>) = announcementRepository.latestId(tags)
suspend fun all() = announcementRepository.all()
suspend fun new(new: APIAnnouncement) { fun latestId() = announcementRepository.latestId()
announcementRepository.new(new)
} suspend fun paged(cursor: Int, limit: Int, tags: Set<String>?) =
suspend fun archive(id: Int, archivedAt: LocalDateTime?) { announcementRepository.paged(cursor, limit, tags)
announcementRepository.archive(id, archivedAt)
} suspend fun get(id: Int) = announcementRepository.get(id)
suspend fun unarchive(id: Int) {
announcementRepository.unarchive(id) suspend fun update(id: Int, new: ApiAnnouncement) = announcementRepository.update(id, new)
}
suspend fun update(id: Int, new: APIAnnouncement) { suspend fun delete(id: Int) = announcementRepository.delete(id)
announcementRepository.update(id, new)
} suspend fun new(new: ApiAnnouncement) = announcementRepository.new(new)
suspend fun delete(id: Int) {
announcementRepository.delete(id) suspend fun tags() = announcementRepository.tags()
}
} }

View File

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

View File

@ -1,49 +0,0 @@
package app.revanced.api.configuration.services
import com.auth0.jwt.JWT
import com.auth0.jwt.algorithms.Algorithm
import io.ktor.server.application.*
import io.ktor.server.auth.*
import io.ktor.server.auth.jwt.*
import java.util.*
import kotlin.text.HexFormat
import kotlin.time.Duration.Companion.minutes
internal class AuthService private constructor(
private val issuer: String,
private val validityInMin: Int,
private val jwtSecret: String,
private val authSHA256Digest: ByteArray,
) {
@OptIn(ExperimentalStdlibApi::class)
constructor(issuer: String, validityInMin: Int, jwtSecret: String, authSHA256DigestString: String) : this(
issuer,
validityInMin,
jwtSecret,
authSHA256DigestString.hexToByteArray(HexFormat.Default),
)
val configureSecurity: Application.() -> Unit = {
install(Authentication) {
jwt("jwt") {
realm = "ReVanced"
verifier(JWT.require(Algorithm.HMAC256(jwtSecret)).withIssuer(issuer).build())
}
digest("auth-digest") {
realm = "ReVanced"
algorithmName = "SHA-256"
digestProvider { _, _ ->
authSHA256Digest
}
}
}
}
fun newToken(): String = JWT.create()
.withIssuer(issuer)
.withExpiresAt(Date(System.currentTimeMillis() + validityInMin.minutes.inWholeMilliseconds))
.sign(Algorithm.HMAC256(jwtSecret))
}

View File

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

View File

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

View File

@ -12,7 +12,7 @@ import java.security.MessageDigest
internal class SignatureService { internal class SignatureService {
private val signatureCache = Caffeine private val signatureCache = Caffeine
.newBuilder() .newBuilder()
.maximumSize(2) // Assuming this is enough for patches and integrations. .maximumSize(2) // 2 because currently only the latest release and prerelease patches are needed.
.build<ByteArray, Boolean>() // Hash -> Verified. .build<ByteArray, Boolean>() // Hash -> Verified.
fun verify( fun verify(

View File

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

View File

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

View File

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