mirror of
https://github.com/revanced/revanced-api.git
synced 2025-04-29 22:24:31 +02:00
chore: Use Kotlin for ReVanced API (#169)
This commit converts the entire project to a KTor project written in Kotlin. Various APIs have been updated, removed, or changed. A proxy is present to allow migration between the old and this API, which can serve requests to endpoints from the old API.
This commit is contained in:
commit
a2c97dd655
@ -1,46 +0,0 @@
|
|||||||
FROM mcr.microsoft.com/devcontainers/base:jammy
|
|
||||||
# FROM mcr.microsoft.com/devcontainers/base:jammy
|
|
||||||
|
|
||||||
ARG DEBIAN_FRONTEND=noninteractive
|
|
||||||
ARG USER=vscode
|
|
||||||
|
|
||||||
RUN DEBIAN_FRONTEND=noninteractive \
|
|
||||||
&& apt-get update \
|
|
||||||
&& apt-get install -y build-essential --no-install-recommends make \
|
|
||||||
ca-certificates \
|
|
||||||
git \
|
|
||||||
libssl-dev \
|
|
||||||
zlib1g-dev \
|
|
||||||
libbz2-dev \
|
|
||||||
libreadline-dev \
|
|
||||||
libsqlite3-dev \
|
|
||||||
wget \
|
|
||||||
curl \
|
|
||||||
llvm \
|
|
||||||
libncurses5-dev \
|
|
||||||
xz-utils \
|
|
||||||
tk-dev \
|
|
||||||
libxml2-dev \
|
|
||||||
libxmlsec1-dev \
|
|
||||||
libffi-dev \
|
|
||||||
liblzma-dev \
|
|
||||||
&& rm -rf /var/lib/apt/lists/*
|
|
||||||
|
|
||||||
# Python and poetry installation
|
|
||||||
USER $USER
|
|
||||||
ARG HOME="/home/$USER"
|
|
||||||
ARG PYTHON_VERSION=3.11
|
|
||||||
# ARG PYTHON_VERSION=3.10
|
|
||||||
|
|
||||||
ENV PYENV_ROOT="${HOME}/.pyenv"
|
|
||||||
ENV PATH="${PYENV_ROOT}/shims:${PYENV_ROOT}/bin:${HOME}/.local/bin:$PATH"
|
|
||||||
|
|
||||||
RUN echo "done 0" \
|
|
||||||
&& curl https://pyenv.run | bash \
|
|
||||||
&& echo "done 1" \
|
|
||||||
&& pyenv install ${PYTHON_VERSION} \
|
|
||||||
&& echo "done 2" \
|
|
||||||
&& pyenv global ${PYTHON_VERSION} \
|
|
||||||
&& echo "done 3" \
|
|
||||||
&& curl -sSL https://install.python-poetry.org | python3 - \
|
|
||||||
&& poetry config virtualenvs.in-project true
|
|
@ -1,65 +0,0 @@
|
|||||||
{
|
|
||||||
"name": "poetry3-poetry-pyenv",
|
|
||||||
"build": {
|
|
||||||
"dockerfile": "Dockerfile"
|
|
||||||
},
|
|
||||||
|
|
||||||
// 👇 Features to add to the Dev Container. More info: https://containers.dev/implementors/features.
|
|
||||||
// "features": {},
|
|
||||||
|
|
||||||
// 👇 Use 'forwardPorts' to make a list of ports inside the container available locally.
|
|
||||||
// "forwardPorts": [],
|
|
||||||
|
|
||||||
// 👇 Use 'postCreateCommand' to run commands after the container is created.
|
|
||||||
// "postCreateCommand": "",
|
|
||||||
|
|
||||||
// 👇 Configure tool-specific properties.
|
|
||||||
"customizations": {
|
|
||||||
"vscode": {
|
|
||||||
"extensions": [
|
|
||||||
"ms-python.python",
|
|
||||||
"njpwerner.autodocstring",
|
|
||||||
"ms-azuretools.vscode-docker",
|
|
||||||
"github.copilot-labs",
|
|
||||||
"github.copilot-nightly",
|
|
||||||
"eamodio.gitlens",
|
|
||||||
"visualstudioexptteam.intellicode-api-usage-examples",
|
|
||||||
"ms-python.isort",
|
|
||||||
"ms-vsliveshare.vsliveshare",
|
|
||||||
"matangover.mypy",
|
|
||||||
"ms-python.vscode-pylance",
|
|
||||||
"mgesbert.python-path",
|
|
||||||
"zeshuaro.vscode-python-poetry",
|
|
||||||
"njqdev.vscode-python-typehint",
|
|
||||||
"ms-python.black-formatter"
|
|
||||||
]
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"features": {
|
|
||||||
"ghcr.io/devcontainers/features/docker-outside-of-docker:1": {},
|
|
||||||
"ghcr.io/devcontainers/features/github-cli:1": {},
|
|
||||||
"ghcr.io/devcontainers/features/sshd:1": {},
|
|
||||||
"ghcr.io/devcontainers-contrib/features/black:2": {},
|
|
||||||
"ghcr.io/devcontainers-contrib/features/curl-apt-get:1": {},
|
|
||||||
"ghcr.io/devcontainers-contrib/features/ffmpeg-apt-get:1": {},
|
|
||||||
"ghcr.io/devcontainers-contrib/features/micro:1": {},
|
|
||||||
"ghcr.io/devcontainers-contrib/features/mosh-apt-get:1": {},
|
|
||||||
"ghcr.io/devcontainers-contrib/features/mypy:2": {},
|
|
||||||
"ghcr.io/devcontainers-contrib/features/poetry:2": {},
|
|
||||||
"ghcr.io/devcontainers-contrib/features/wget-apt-get:1": {},
|
|
||||||
"ghcr.io/stuartleeks/dev-container-features/shell-history:0": {},
|
|
||||||
"ghcr.io/jckimble/devcontainer-features/ngrok:3": {},
|
|
||||||
"ghcr.io/devcontainers/features/common-utils:2": {
|
|
||||||
"installZsh": true,
|
|
||||||
"configureZshAsDefaultShell": true,
|
|
||||||
"installOhMyZsh": true,
|
|
||||||
"upgradePackages": true,
|
|
||||||
"username": "codespace",
|
|
||||||
"userUid": "automatic",
|
|
||||||
"userGid": "automatic"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 👇 Uncomment to connect as root instead. More info: https://aka.ms/dev-containers-non-root.
|
|
||||||
// "remoteUser": "root"
|
|
||||||
}
|
|
3
.editorconfig
Normal file
3
.editorconfig
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
[*.{kt,kts}]
|
||||||
|
ktlint_code_style = intellij_idea
|
||||||
|
ktlint_standard_no-wildcard-imports = disabled
|
17
.env.example
Normal file
17
.env.example
Normal file
@ -0,0 +1,17 @@
|
|||||||
|
# Optional token for API calls to the backend
|
||||||
|
BACKEND_API_TOKEN=
|
||||||
|
# A URL to the old API to proxy for migration purposes
|
||||||
|
OLD_API_URL=
|
||||||
|
|
||||||
|
# Database connection details
|
||||||
|
DB_URL=jdbc:h2:./persistence/revanced-api
|
||||||
|
DB_USER=
|
||||||
|
DB_PASSWORD=
|
||||||
|
|
||||||
|
# Digest auth to issue JWT tokens in the format SHA256("username:ReVanced:password")
|
||||||
|
AUTH_SHA256_DIGEST=
|
||||||
|
|
||||||
|
# JWT configuration for authenticated API endpoints
|
||||||
|
JWT_SECRET=
|
||||||
|
JWT_ISSUER=
|
||||||
|
JWT_VALIDITY_IN_MIN=
|
9
.gitattributes
vendored
Normal file
9
.gitattributes
vendored
Normal file
@ -0,0 +1,9 @@
|
|||||||
|
#
|
||||||
|
# https://help.github.com/articles/dealing-with-line-endings/
|
||||||
|
#
|
||||||
|
# Linux start script should use lf
|
||||||
|
/gradlew text eol=lf
|
||||||
|
|
||||||
|
# These are Windows script files and should use crlf
|
||||||
|
*.bat text eol=crlf
|
||||||
|
|
109
.github/ISSUE_TEMPLATE/bug_report.yml
vendored
Normal file
109
.github/ISSUE_TEMPLATE/bug_report.yml
vendored
Normal file
@ -0,0 +1,109 @@
|
|||||||
|
name: 🐞 Bug report
|
||||||
|
description: Report a bug or an issue.
|
||||||
|
title: 'bug: '
|
||||||
|
labels: ['Bug report']
|
||||||
|
body:
|
||||||
|
- type: markdown
|
||||||
|
attributes:
|
||||||
|
value: |
|
||||||
|
<p align="center">
|
||||||
|
<picture>
|
||||||
|
<source
|
||||||
|
width="256px"
|
||||||
|
media="(prefers-color-scheme: dark)"
|
||||||
|
srcset="https://raw.githubusercontent.com/revanced/revanced-api/main/assets/revanced-headline/revanced-headline-vertical-dark.svg"
|
||||||
|
>
|
||||||
|
<img
|
||||||
|
width="256px"
|
||||||
|
src="https://raw.githubusercontent.com/revanced/revanced-api/main/assets/revanced-headline/revanced-headline-vertical-light.svg"
|
||||||
|
>
|
||||||
|
</picture>
|
||||||
|
<br>
|
||||||
|
<a href="https://revanced.app/">
|
||||||
|
<picture>
|
||||||
|
<source height="24px" media="(prefers-color-scheme: dark)" srcset="https://raw.githubusercontent.com/revanced/revanced-api/main/assets/revanced-logo/revanced-logo.svg" />
|
||||||
|
<img height="24px" src="https://raw.githubusercontent.com/revanced/revanced-api/main/assets/revanced-logo/revanced-logo.svg" />
|
||||||
|
</picture>
|
||||||
|
</a>
|
||||||
|
<a href="https://github.com/ReVanced">
|
||||||
|
<picture>
|
||||||
|
<source height="24px" media="(prefers-color-scheme: dark)" srcset="https://i.ibb.co/dMMmCrW/Git-Hub-Mark.png" />
|
||||||
|
<img height="24px" src="https://i.ibb.co/9wV3HGF/Git-Hub-Mark-Light.png" />
|
||||||
|
</picture>
|
||||||
|
</a>
|
||||||
|
<a href="http://revanced.app/discord">
|
||||||
|
<picture>
|
||||||
|
<source height="24px" media="(prefers-color-scheme: dark)" srcset="https://user-images.githubusercontent.com/13122796/178032563-d4e084b7-244e-4358-af50-26bde6dd4996.png" />
|
||||||
|
<img height="24px" src="https://user-images.githubusercontent.com/13122796/178032563-d4e084b7-244e-4358-af50-26bde6dd4996.png" />
|
||||||
|
</picture>
|
||||||
|
</a>
|
||||||
|
<a href="https://reddit.com/r/revancedapp">
|
||||||
|
<picture>
|
||||||
|
<source height="24px" media="(prefers-color-scheme: dark)" srcset="https://user-images.githubusercontent.com/13122796/178032351-9d9d5619-8ef7-470a-9eec-2744ece54553.png" />
|
||||||
|
<img height="24px" src="https://user-images.githubusercontent.com/13122796/178032351-9d9d5619-8ef7-470a-9eec-2744ece54553.png" />
|
||||||
|
</picture>
|
||||||
|
</a>
|
||||||
|
<a href="https://t.me/app_revanced">
|
||||||
|
<picture>
|
||||||
|
<source height="24px" media="(prefers-color-scheme: dark)" srcset="https://user-images.githubusercontent.com/13122796/178032213-faf25ab8-0bc3-4a94-a730-b524c96df124.png" />
|
||||||
|
<img height="24px" src="https://user-images.githubusercontent.com/13122796/178032213-faf25ab8-0bc3-4a94-a730-b524c96df124.png" />
|
||||||
|
</picture>
|
||||||
|
</a>
|
||||||
|
<a href="https://x.com/revancedapp">
|
||||||
|
<picture>
|
||||||
|
<source media="(prefers-color-scheme: dark)" srcset="https://user-images.githubusercontent.com/93124920/270180600-7c1b38bf-889b-4d68-bd5e-b9d86f91421a.png">
|
||||||
|
<img height="24px" src="https://user-images.githubusercontent.com/93124920/270108715-d80743fa-b330-4809-b1e6-79fbdc60d09c.png" />
|
||||||
|
</picture>
|
||||||
|
</a>
|
||||||
|
<a href="https://www.youtube.com/@ReVanced">
|
||||||
|
<picture>
|
||||||
|
<source height="24px" media="(prefers-color-scheme: dark)" srcset="https://user-images.githubusercontent.com/13122796/178032714-c51c7492-0666-44ac-99c2-f003a695ab50.png" />
|
||||||
|
<img height="24px" src="https://user-images.githubusercontent.com/13122796/178032714-c51c7492-0666-44ac-99c2-f003a695ab50.png" />
|
||||||
|
</picture>
|
||||||
|
</a>
|
||||||
|
<br>
|
||||||
|
<br>
|
||||||
|
Continuing the legacy of Vanced
|
||||||
|
</p>
|
||||||
|
|
||||||
|
# ReVanced API bug report
|
||||||
|
|
||||||
|
Before creating a new bug report, please keep the following in mind:
|
||||||
|
|
||||||
|
- **Do not submit a duplicate bug report**: Search for existing bug reports [here](https://github.com/ReVanced/revanced-api/issues?q=label%3A%22Bug+report%22).
|
||||||
|
- **Review the contribution guidelines**: Make sure your bug report adheres to it. You can find the guidelines [here](https://github.com/ReVanced/revanced-api/blob/main/CONTRIBUTING.md).
|
||||||
|
- **Do not use the issue page for support**: If you need help or have questions, check out other platforms on [revanced.app](https://revanced.app).
|
||||||
|
- type: textarea
|
||||||
|
attributes:
|
||||||
|
label: Bug description
|
||||||
|
description: |
|
||||||
|
- Describe your bug in detail
|
||||||
|
- Add steps to reproduce the bug if possible (Step 1. ... Step 2. ...)
|
||||||
|
- Add images and videos if possible
|
||||||
|
validations:
|
||||||
|
required: true
|
||||||
|
- type: textarea
|
||||||
|
attributes:
|
||||||
|
label: Error logs
|
||||||
|
description: Exceptions can be captured by running `logcat | grep AndroidRuntime` in a shell.
|
||||||
|
render: shell
|
||||||
|
- type: textarea
|
||||||
|
attributes:
|
||||||
|
label: Solution
|
||||||
|
description: If applicable, add a possible solution to the bug.
|
||||||
|
- type: textarea
|
||||||
|
attributes:
|
||||||
|
label: Additional context
|
||||||
|
description: Add additional context here.
|
||||||
|
- type: checkboxes
|
||||||
|
id: acknowledgements
|
||||||
|
attributes:
|
||||||
|
label: Acknowledgements
|
||||||
|
description: Your bug report will be closed if you don't follow the checklist below.
|
||||||
|
options:
|
||||||
|
- label: I have checked all open and closed bug reports and this is not a duplicate.
|
||||||
|
required: true
|
||||||
|
- label: I have chosen an appropriate title.
|
||||||
|
required: true
|
||||||
|
- label: All requested information has been provided properly.
|
||||||
|
required: true
|
5
.github/ISSUE_TEMPLATE/config.yml
vendored
Normal file
5
.github/ISSUE_TEMPLATE/config.yml
vendored
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
blank_issues_enabled: false
|
||||||
|
contact_links:
|
||||||
|
- name: 🗨 Discussions
|
||||||
|
url: https://github.com/revanced/revanced-suggestions/discussions
|
||||||
|
about: Have something unspecific to ReVanced APi in mind? Search for or start a new discussion!
|
105
.github/ISSUE_TEMPLATE/feature_request.yml
vendored
Normal file
105
.github/ISSUE_TEMPLATE/feature_request.yml
vendored
Normal file
@ -0,0 +1,105 @@
|
|||||||
|
name: ⭐ Feature request
|
||||||
|
description: Create a detailed request for a new feature.
|
||||||
|
title: 'feat: '
|
||||||
|
labels: ['Feature request']
|
||||||
|
body:
|
||||||
|
- type: markdown
|
||||||
|
attributes:
|
||||||
|
value: |
|
||||||
|
<p align="center">
|
||||||
|
<picture>
|
||||||
|
<source
|
||||||
|
width="256px"
|
||||||
|
media="(prefers-color-scheme: dark)"
|
||||||
|
srcset="https://raw.githubusercontent.com/revanced/revanced-api/main/assets/revanced-headline/revanced-headline-vertical-dark.svg"
|
||||||
|
>
|
||||||
|
<img
|
||||||
|
width="256px"
|
||||||
|
src="https://raw.githubusercontent.com/revanced/revanced-api/main/assets/revanced-headline/revanced-headline-vertical-light.svg"
|
||||||
|
>
|
||||||
|
</picture>
|
||||||
|
<br>
|
||||||
|
<a href="https://revanced.app/">
|
||||||
|
<picture>
|
||||||
|
<source height="24px" media="(prefers-color-scheme: dark)" srcset="https://raw.githubusercontent.com/revanced/revanced-api/main/assets/revanced-logo/revanced-logo.svg" />
|
||||||
|
<img height="24px" src="https://raw.githubusercontent.com/revanced/revanced-api/main/assets/revanced-logo/revanced-logo.svg" />
|
||||||
|
</picture>
|
||||||
|
</a>
|
||||||
|
<a href="https://github.com/ReVanced">
|
||||||
|
<picture>
|
||||||
|
<source height="24px" media="(prefers-color-scheme: dark)" srcset="https://i.ibb.co/dMMmCrW/Git-Hub-Mark.png" />
|
||||||
|
<img height="24px" src="https://i.ibb.co/9wV3HGF/Git-Hub-Mark-Light.png" />
|
||||||
|
</picture>
|
||||||
|
</a>
|
||||||
|
<a href="http://revanced.app/discord">
|
||||||
|
<picture>
|
||||||
|
<source height="24px" media="(prefers-color-scheme: dark)" srcset="https://user-images.githubusercontent.com/13122796/178032563-d4e084b7-244e-4358-af50-26bde6dd4996.png" />
|
||||||
|
<img height="24px" src="https://user-images.githubusercontent.com/13122796/178032563-d4e084b7-244e-4358-af50-26bde6dd4996.png" />
|
||||||
|
</picture>
|
||||||
|
</a>
|
||||||
|
<a href="https://reddit.com/r/revancedapp">
|
||||||
|
<picture>
|
||||||
|
<source height="24px" media="(prefers-color-scheme: dark)" srcset="https://user-images.githubusercontent.com/13122796/178032351-9d9d5619-8ef7-470a-9eec-2744ece54553.png" />
|
||||||
|
<img height="24px" src="https://user-images.githubusercontent.com/13122796/178032351-9d9d5619-8ef7-470a-9eec-2744ece54553.png" />
|
||||||
|
</picture>
|
||||||
|
</a>
|
||||||
|
<a href="https://t.me/app_revanced">
|
||||||
|
<picture>
|
||||||
|
<source height="24px" media="(prefers-color-scheme: dark)" srcset="https://user-images.githubusercontent.com/13122796/178032213-faf25ab8-0bc3-4a94-a730-b524c96df124.png" />
|
||||||
|
<img height="24px" src="https://user-images.githubusercontent.com/13122796/178032213-faf25ab8-0bc3-4a94-a730-b524c96df124.png" />
|
||||||
|
</picture>
|
||||||
|
</a>
|
||||||
|
<a href="https://x.com/revancedapp">
|
||||||
|
<picture>
|
||||||
|
<source media="(prefers-color-scheme: dark)" srcset="https://user-images.githubusercontent.com/93124920/270180600-7c1b38bf-889b-4d68-bd5e-b9d86f91421a.png">
|
||||||
|
<img height="24px" src="https://user-images.githubusercontent.com/93124920/270108715-d80743fa-b330-4809-b1e6-79fbdc60d09c.png" />
|
||||||
|
</picture>
|
||||||
|
</a>
|
||||||
|
<a href="https://www.youtube.com/@ReVanced">
|
||||||
|
<picture>
|
||||||
|
<source height="24px" media="(prefers-color-scheme: dark)" srcset="https://user-images.githubusercontent.com/13122796/178032714-c51c7492-0666-44ac-99c2-f003a695ab50.png" />
|
||||||
|
<img height="24px" src="https://user-images.githubusercontent.com/13122796/178032714-c51c7492-0666-44ac-99c2-f003a695ab50.png" />
|
||||||
|
</picture>
|
||||||
|
</a>
|
||||||
|
<br>
|
||||||
|
<br>
|
||||||
|
Continuing the legacy of Vanced
|
||||||
|
</p>
|
||||||
|
|
||||||
|
# ReVanced APi feature request
|
||||||
|
|
||||||
|
Before creating a new feature request, please keep the following in mind:
|
||||||
|
|
||||||
|
- **Do not submit a duplicate feature request**: Search for existing feature requests [here](https://github.com/ReVanced/revanced-api/issues?q=label%3A%22Feature+request%22).
|
||||||
|
- **Review the contribution guidelines**: Make sure your feature request adheres to it. You can find the guidelines [here](https://github.com/ReVanced/revanced-api/blob/main/CONTRIBUTING.md).
|
||||||
|
- **Do not use the issue page for support**: If you need help or have questions, check out other platforms on [revanced.app](https://revanced.app).
|
||||||
|
- type: textarea
|
||||||
|
attributes:
|
||||||
|
label: Feature description
|
||||||
|
description: |
|
||||||
|
- Describe your feature in detail
|
||||||
|
- Add images, videos, links, examples, references, etc. if possible
|
||||||
|
- type: textarea
|
||||||
|
attributes:
|
||||||
|
label: Motivation
|
||||||
|
description: |
|
||||||
|
A strong motivation is necessary for a feature request to be considered.
|
||||||
|
|
||||||
|
- Why should this feature be implemented?
|
||||||
|
- What is the explicit use case?
|
||||||
|
- What are the benefits?
|
||||||
|
- What makes this feature important?
|
||||||
|
validations:
|
||||||
|
required: true
|
||||||
|
- type: checkboxes
|
||||||
|
id: acknowledgements
|
||||||
|
attributes:
|
||||||
|
label: Acknowledgements
|
||||||
|
description: Your feature request will be closed if you don't follow the checklist below.
|
||||||
|
options:
|
||||||
|
- label: I have checked all open and closed feature requests and this is not a duplicate
|
||||||
|
required: true
|
||||||
|
- label: I have chosen an appropriate title.
|
||||||
|
required: true
|
||||||
|
- label: All requested information has been provided properly.
|
||||||
|
required: true
|
2
.github/config.yml
vendored
Normal file
2
.github/config.yml
vendored
Normal file
@ -0,0 +1,2 @@
|
|||||||
|
firstPRMergeComment: >
|
||||||
|
Thank you for contributing to ReVanced. Join us on [Discord](https://revanced.app/discord) to receive a role for your contribution.
|
25
.github/dependabot.yml
vendored
25
.github/dependabot.yml
vendored
@ -1,25 +0,0 @@
|
|||||||
version: 2
|
|
||||||
updates:
|
|
||||||
- package-ecosystem: "github-actions"
|
|
||||||
directory: "/"
|
|
||||||
schedule:
|
|
||||||
interval: "daily"
|
|
||||||
target-branch: "dev"
|
|
||||||
assignees:
|
|
||||||
- "alexandreteles"
|
|
||||||
|
|
||||||
- package-ecosystem: "pip"
|
|
||||||
directory: "/"
|
|
||||||
schedule:
|
|
||||||
interval: "daily"
|
|
||||||
target-branch: "dev"
|
|
||||||
assignees:
|
|
||||||
- "alexandreteles"
|
|
||||||
|
|
||||||
- package-ecosystem: "docker"
|
|
||||||
directory: "/"
|
|
||||||
schedule:
|
|
||||||
interval: "daily"
|
|
||||||
target-branch: "dev"
|
|
||||||
assignees:
|
|
||||||
- "alexandreteles"
|
|
25
.github/workflows/build_pull_request.yml
vendored
Normal file
25
.github/workflows/build_pull_request.yml
vendored
Normal file
@ -0,0 +1,25 @@
|
|||||||
|
name: Build pull request
|
||||||
|
|
||||||
|
on:
|
||||||
|
workflow_dispatch:
|
||||||
|
pull_request:
|
||||||
|
branches:
|
||||||
|
- dev
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
release:
|
||||||
|
name: Build
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- name: Checkout
|
||||||
|
uses: actions/checkout@v4
|
||||||
|
with:
|
||||||
|
fetch-depth: 0
|
||||||
|
|
||||||
|
- name: Cache Gradle
|
||||||
|
uses: burrunan/gradle-cache-action@v1
|
||||||
|
|
||||||
|
- name: Build
|
||||||
|
env:
|
||||||
|
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||||
|
run: ./gradlew build --no-daemon
|
54
.github/workflows/codeql.yml
vendored
54
.github/workflows/codeql.yml
vendored
@ -1,54 +0,0 @@
|
|||||||
name: "CodeQL"
|
|
||||||
|
|
||||||
on:
|
|
||||||
push:
|
|
||||||
branches: [dev]
|
|
||||||
pull_request:
|
|
||||||
types: [opened, reopened, edited, synchronize]
|
|
||||||
schedule:
|
|
||||||
- cron: "29 5 * * 5"
|
|
||||||
workflow_dispatch:
|
|
||||||
|
|
||||||
jobs:
|
|
||||||
analyze:
|
|
||||||
name: Analyze
|
|
||||||
runs-on: ${{ (matrix.language == 'swift' && 'macos-latest') || 'ubuntu-latest' }}
|
|
||||||
timeout-minutes: ${{ (matrix.language == 'swift' && 120) || 360 }}
|
|
||||||
permissions:
|
|
||||||
actions: read
|
|
||||||
contents: read
|
|
||||||
security-events: write
|
|
||||||
strategy:
|
|
||||||
fail-fast: false
|
|
||||||
matrix:
|
|
||||||
language: ["python"]
|
|
||||||
|
|
||||||
steps:
|
|
||||||
- name: Checkout repository
|
|
||||||
uses: actions/checkout@v4
|
|
||||||
|
|
||||||
- name: Set up Python
|
|
||||||
uses: actions/setup-python@v5
|
|
||||||
with:
|
|
||||||
python-version: "3.11.7"
|
|
||||||
|
|
||||||
- name: Install project dependencies
|
|
||||||
run: |
|
|
||||||
python -m pip install --upgrade pip
|
|
||||||
if [ -f requirements.txt ];
|
|
||||||
then pip install -r requirements.txt;
|
|
||||||
fi
|
|
||||||
|
|
||||||
- name: Initialize CodeQL
|
|
||||||
uses: github/codeql-action/init@v3
|
|
||||||
with:
|
|
||||||
languages: ${{ matrix.language }}
|
|
||||||
queries: security-and-quality
|
|
||||||
|
|
||||||
- name: Autobuild
|
|
||||||
uses: github/codeql-action/autobuild@v3
|
|
||||||
|
|
||||||
- name: Perform CodeQL Analysis
|
|
||||||
uses: github/codeql-action/analyze@v3
|
|
||||||
with:
|
|
||||||
category: "/language:${{matrix.language}}"
|
|
69
.github/workflows/dev.yml
vendored
69
.github/workflows/dev.yml
vendored
@ -1,69 +0,0 @@
|
|||||||
name: Build dev branch
|
|
||||||
|
|
||||||
on:
|
|
||||||
push:
|
|
||||||
branches: [ "dev" ]
|
|
||||||
schedule:
|
|
||||||
- cron: '24 9 * * 6'
|
|
||||||
workflow_dispatch:
|
|
||||||
|
|
||||||
env:
|
|
||||||
REGISTRY: ghcr.io
|
|
||||||
IMAGE_NAME: ${{ github.repository }}
|
|
||||||
IMAGE_TAG: ${{ github.sha }}
|
|
||||||
|
|
||||||
jobs:
|
|
||||||
security_checks:
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
name: Security check
|
|
||||||
steps:
|
|
||||||
- uses: actions/checkout@v4
|
|
||||||
- name: Security Checks (PyCharm Security)
|
|
||||||
uses: tonybaloney/pycharm-security@master
|
|
||||||
with:
|
|
||||||
path: .
|
|
||||||
|
|
||||||
build:
|
|
||||||
needs: security_checks
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
permissions:
|
|
||||||
contents: read
|
|
||||||
packages: write
|
|
||||||
|
|
||||||
steps:
|
|
||||||
- name: Checkout Dockerfile
|
|
||||||
id: checkout
|
|
||||||
uses: actions/checkout@v4
|
|
||||||
with:
|
|
||||||
ref: ${{ inputs.branch }}
|
|
||||||
|
|
||||||
- name: Setup QEMU
|
|
||||||
id: qemu
|
|
||||||
uses: docker/setup-qemu-action@v3
|
|
||||||
with:
|
|
||||||
image: tonistiigi/binfmt:latest
|
|
||||||
platforms: all
|
|
||||||
|
|
||||||
- name: Setup Docker Buildx
|
|
||||||
id: buildx
|
|
||||||
uses: docker/setup-buildx-action@v3
|
|
||||||
|
|
||||||
- name: Extract metadata (tags, labels) for Docker
|
|
||||||
id: meta
|
|
||||||
uses: docker/metadata-action@v5
|
|
||||||
with:
|
|
||||||
images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}
|
|
||||||
flavor: |
|
|
||||||
latest=${{ startsWith(github.ref, 'refs/heads/main') }}
|
|
||||||
suffix=-${{ github.sha }}
|
|
||||||
|
|
||||||
- name: Build Docker image
|
|
||||||
id: build
|
|
||||||
uses: docker/build-push-action@v5
|
|
||||||
with:
|
|
||||||
context: .
|
|
||||||
file: ./Dockerfile
|
|
||||||
platforms: linux/amd64,linux/arm64/v8
|
|
||||||
push: false
|
|
||||||
tags: ${{ steps.meta.outputs.tags }}
|
|
||||||
labels: ${{ steps.meta.outputs.labels }}
|
|
67
.github/workflows/main.yml
vendored
67
.github/workflows/main.yml
vendored
@ -1,67 +0,0 @@
|
|||||||
name: Build and Publish Docker Image
|
|
||||||
|
|
||||||
on:
|
|
||||||
push:
|
|
||||||
branches: [main]
|
|
||||||
schedule:
|
|
||||||
- cron: "24 9 * * 6"
|
|
||||||
workflow_dispatch:
|
|
||||||
|
|
||||||
env:
|
|
||||||
REGISTRY: ghcr.io
|
|
||||||
IMAGE_NAME: ${{ github.repository }}
|
|
||||||
IMAGE_TAG: ${{ github.sha }}
|
|
||||||
|
|
||||||
jobs:
|
|
||||||
build:
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
permissions:
|
|
||||||
contents: read
|
|
||||||
packages: write
|
|
||||||
|
|
||||||
steps:
|
|
||||||
- name: Checkout Dockerfile
|
|
||||||
id: checkout
|
|
||||||
uses: actions/checkout@v4
|
|
||||||
|
|
||||||
- name: Setup QEMU
|
|
||||||
id: qemu
|
|
||||||
uses: docker/setup-qemu-action@v3
|
|
||||||
with:
|
|
||||||
image: tonistiigi/binfmt:latest
|
|
||||||
platforms: all
|
|
||||||
|
|
||||||
- name: Setup Docker Buildx
|
|
||||||
id: buildx
|
|
||||||
uses: docker/setup-buildx-action@v3
|
|
||||||
|
|
||||||
- name: Login to GitHub Container Registry
|
|
||||||
id: ghcr
|
|
||||||
uses: docker/login-action@v3
|
|
||||||
with:
|
|
||||||
registry: ${{ env.REGISTRY }}
|
|
||||||
username: ${{ github.repository_owner }}
|
|
||||||
password: ${{ secrets.GH_TOKEN }}
|
|
||||||
|
|
||||||
- name: Extract metadata (tags, labels) for Docker
|
|
||||||
id: meta
|
|
||||||
uses: docker/metadata-action@v5
|
|
||||||
with:
|
|
||||||
images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}
|
|
||||||
flavor: |
|
|
||||||
latest=${{ startsWith(github.ref, 'refs/heads/main') }}
|
|
||||||
suffix=-${{ github.sha }}
|
|
||||||
|
|
||||||
- name: Build and push main Docker image
|
|
||||||
id: build
|
|
||||||
uses: docker/build-push-action@v5
|
|
||||||
with:
|
|
||||||
build-args: GH_TOKEN=${{ secrets.GH_TOKEN }}
|
|
||||||
context: .
|
|
||||||
file: ./Dockerfile
|
|
||||||
platforms: linux/amd64,linux/arm64/v8
|
|
||||||
cache-to: type=gha,mode=max,ignore-error=true
|
|
||||||
cache-from: type=gha
|
|
||||||
push: true
|
|
||||||
tags: ${{ steps.meta.outputs.tags }}
|
|
||||||
labels: ${{ steps.meta.outputs.labels }}
|
|
26
.github/workflows/open_pull_request.yml
vendored
Normal file
26
.github/workflows/open_pull_request.yml
vendored
Normal file
@ -0,0 +1,26 @@
|
|||||||
|
name: Open a PR to main
|
||||||
|
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
branches:
|
||||||
|
- dev
|
||||||
|
workflow_dispatch:
|
||||||
|
|
||||||
|
env:
|
||||||
|
MESSAGE: Merge branch `${{ github.head_ref || github.ref_name }}` to `main`
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
pull-request:
|
||||||
|
name: Open pull request
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- name: Checkout
|
||||||
|
uses: actions/checkout@v4
|
||||||
|
|
||||||
|
- name: Open pull request
|
||||||
|
uses: repo-sync/pull-request@v2
|
||||||
|
with:
|
||||||
|
destination_branch: 'main'
|
||||||
|
pr_title: 'chore: ${{ env.MESSAGE }}'
|
||||||
|
pr_body: 'This pull request will ${{ env.MESSAGE }}.'
|
||||||
|
pr_draft: true
|
87
.github/workflows/release.yml
vendored
Normal file
87
.github/workflows/release.yml
vendored
Normal file
@ -0,0 +1,87 @@
|
|||||||
|
name: Release
|
||||||
|
|
||||||
|
on:
|
||||||
|
workflow_dispatch:
|
||||||
|
push:
|
||||||
|
branches:
|
||||||
|
- main
|
||||||
|
- dev
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
release:
|
||||||
|
name: Release
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
permissions:
|
||||||
|
contents: read
|
||||||
|
packages: write
|
||||||
|
steps:
|
||||||
|
- name: Checkout
|
||||||
|
uses: actions/checkout@v4
|
||||||
|
with:
|
||||||
|
# Make sure the release step uses its own credentials:
|
||||||
|
# https://github.com/cycjimmy/semantic-release-action#private-packages
|
||||||
|
persist-credentials: false
|
||||||
|
fetch-depth: 0
|
||||||
|
|
||||||
|
- name: Cache Gradle
|
||||||
|
uses: burrunan/gradle-cache-action@v1
|
||||||
|
|
||||||
|
- name: Build
|
||||||
|
env:
|
||||||
|
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||||
|
run: ./gradlew startShadowScripts clean
|
||||||
|
|
||||||
|
- name: Setup Node.js
|
||||||
|
uses: actions/setup-node@v4
|
||||||
|
with:
|
||||||
|
node-version: "lts/*"
|
||||||
|
cache: "npm"
|
||||||
|
|
||||||
|
- name: Install dependencies
|
||||||
|
run: npm install
|
||||||
|
|
||||||
|
- name: Import GPG key
|
||||||
|
uses: crazy-max/ghaction-import-gpg@v6
|
||||||
|
with:
|
||||||
|
gpg_private_key: ${{ secrets.GPG_PRIVATE_KEY }}
|
||||||
|
passphrase: ${{ secrets.GPG_PASSPHRASE }}
|
||||||
|
fingerprint: ${{ env.GPG_FINGERPRINT }}
|
||||||
|
|
||||||
|
- name: Setup QEMU
|
||||||
|
uses: docker/setup-qemu-action@v3
|
||||||
|
with:
|
||||||
|
platforms: amd64, arm64
|
||||||
|
|
||||||
|
- name: Setup Docker Buildx
|
||||||
|
uses: docker/setup-buildx-action@v3
|
||||||
|
|
||||||
|
- name: Release
|
||||||
|
env:
|
||||||
|
DOCKER_REGISTRY_USER: ${{ github.actor }}
|
||||||
|
DOCKER_REGISTRY_PASSWORD: ${{ secrets.GITHUB_TOKEN }}
|
||||||
|
GITHUB_ACTOR: ${{ github.actor }}
|
||||||
|
GITHUB_TOKEN: ${{ secrets.REPOSITORY_PUSH_ACCESS }}
|
||||||
|
run: npm exec semantic-release
|
||||||
|
|
||||||
|
- name: Set Portainer stack webhook URL based on branch
|
||||||
|
run: |
|
||||||
|
if [[ "${{ github.ref }}" == "refs/heads/main" ]]; then
|
||||||
|
PORTAINER_WEBHOOK_URL=${{ secrets.PORTAINER_WEBHOOK_MAIN_URL }}
|
||||||
|
else
|
||||||
|
PORTAINER_WEBHOOK_URL=${{ secrets.PORTAINER_WEBHOOK_DEV_URL }}
|
||||||
|
fi
|
||||||
|
echo "PORTAINER_WEBHOOK_URL=$PORTAINER_WEBHOOK_URL" >> $GITHUB_ENV
|
||||||
|
|
||||||
|
- name: Trigger Portainer stack update
|
||||||
|
uses: newarifrh/portainer-service-webhook@v1
|
||||||
|
with:
|
||||||
|
webhook_url: ${{ env.PORTAINER_WEBHOOK_URL }}
|
||||||
|
|
||||||
|
- name: Purge outdated images
|
||||||
|
uses: actions/delete-package-versions@v5
|
||||||
|
with:
|
||||||
|
package-name: 'revanced-api'
|
||||||
|
package-type: 'container'
|
||||||
|
min-versions-to-keep: 5
|
||||||
|
delete-only-untagged-versions: 'true'
|
||||||
|
token: ${{ secrets.GITHUB_TOKEN }}
|
195
.gitignore
vendored
195
.gitignore
vendored
@ -1,164 +1,45 @@
|
|||||||
# Byte-compiled / optimized / DLL files
|
.gradle
|
||||||
__pycache__/
|
|
||||||
*.py[cod]
|
|
||||||
*$py.class
|
|
||||||
|
|
||||||
# C extensions
|
|
||||||
*.so
|
|
||||||
|
|
||||||
# Distribution / packaging
|
|
||||||
.Python
|
|
||||||
build/
|
build/
|
||||||
develop-eggs/
|
!gradle/wrapper/gradle-wrapper.jar
|
||||||
dist/
|
!**/src/main/**/build/
|
||||||
downloads/
|
!**/src/test/**/build/
|
||||||
eggs/
|
|
||||||
.eggs/
|
|
||||||
lib/
|
|
||||||
lib64/
|
|
||||||
parts/
|
|
||||||
sdist/
|
|
||||||
var/
|
|
||||||
wheels/
|
|
||||||
share/python-wheels/
|
|
||||||
*.egg-info/
|
|
||||||
.installed.cfg
|
|
||||||
*.egg
|
|
||||||
MANIFEST
|
|
||||||
|
|
||||||
# PyInstaller
|
### STS ###
|
||||||
# Usually these files are written by a python script from a template
|
.apt_generated
|
||||||
# before PyInstaller builds the exe, so as to inject date/other infos into it.
|
.classpath
|
||||||
*.manifest
|
.factorypath
|
||||||
*.spec
|
.project
|
||||||
|
.settings
|
||||||
|
.springBeans
|
||||||
|
.sts4-cache
|
||||||
|
bin/
|
||||||
|
!**/src/main/**/bin/
|
||||||
|
!**/src/test/**/bin/
|
||||||
|
|
||||||
# Installer logs
|
### IntelliJ IDEA ###
|
||||||
pip-log.txt
|
.idea
|
||||||
pip-delete-this-directory.txt
|
*.iws
|
||||||
|
*.iml
|
||||||
|
*.ipr
|
||||||
|
out/
|
||||||
|
!**/src/main/**/out/
|
||||||
|
!**/src/test/**/out/
|
||||||
|
|
||||||
# Unit test / coverage reports
|
### NetBeans ###
|
||||||
htmlcov/
|
/nbproject/private/
|
||||||
.tox/
|
/nbbuild/
|
||||||
.nox/
|
/dist/
|
||||||
.coverage
|
/nbdist/
|
||||||
.coverage.*
|
/.nb-gradle/
|
||||||
.cache
|
|
||||||
nosetests.xml
|
|
||||||
coverage.xml
|
|
||||||
*.cover
|
|
||||||
*.py,cover
|
|
||||||
.hypothesis/
|
|
||||||
.pytest_cache/
|
|
||||||
cover/
|
|
||||||
|
|
||||||
# Translations
|
### VS Code ###
|
||||||
*.mo
|
.vscode/
|
||||||
*.pot
|
|
||||||
|
|
||||||
# Django stuff:
|
### Project ###
|
||||||
*.log
|
|
||||||
local_settings.py
|
|
||||||
db.sqlite3
|
|
||||||
db.sqlite3-journal
|
|
||||||
|
|
||||||
# Flask stuff:
|
|
||||||
instance/
|
|
||||||
.webassets-cache
|
|
||||||
|
|
||||||
# Scrapy stuff:
|
|
||||||
.scrapy
|
|
||||||
|
|
||||||
# Sphinx documentation
|
|
||||||
docs/_build/
|
|
||||||
|
|
||||||
# PyBuilder
|
|
||||||
.pybuilder/
|
|
||||||
target/
|
|
||||||
|
|
||||||
# Jupyter Notebook
|
|
||||||
.ipynb_checkpoints
|
|
||||||
|
|
||||||
# IPython
|
|
||||||
profile_default/
|
|
||||||
ipython_config.py
|
|
||||||
|
|
||||||
# pyenv
|
|
||||||
# For a library or package, you might want to ignore these files since the code is
|
|
||||||
# intended to run in multiple environments; otherwise, check them in:
|
|
||||||
# .python-version
|
|
||||||
|
|
||||||
# pipenv
|
|
||||||
# According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control.
|
|
||||||
# However, in case of collaboration, if having platform-specific dependencies or dependencies
|
|
||||||
# having no cross-platform support, pipenv may install dependencies that don't work, or not
|
|
||||||
# install all needed dependencies.
|
|
||||||
#Pipfile.lock
|
|
||||||
|
|
||||||
# poetry
|
|
||||||
# Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control.
|
|
||||||
# This is especially recommended for binary packages to ensure reproducibility, and is more
|
|
||||||
# commonly ignored for libraries.
|
|
||||||
# https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control
|
|
||||||
#poetry.lock
|
|
||||||
|
|
||||||
# pdm
|
|
||||||
# Similar to Pipfile.lock, it is generally recommended to include pdm.lock in version control.
|
|
||||||
#pdm.lock
|
|
||||||
# pdm stores project-wide configurations in .pdm.toml, but it is recommended to not include it
|
|
||||||
# in version control.
|
|
||||||
# https://pdm.fming.dev/#use-with-ide
|
|
||||||
.pdm.toml
|
|
||||||
|
|
||||||
# PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-project/pdm
|
|
||||||
__pypackages__/
|
|
||||||
|
|
||||||
# Celery stuff
|
|
||||||
celerybeat-schedule
|
|
||||||
celerybeat.pid
|
|
||||||
|
|
||||||
# SageMath parsed files
|
|
||||||
*.sage.py
|
|
||||||
|
|
||||||
# Environments
|
|
||||||
.env
|
.env
|
||||||
.venv
|
persistence/
|
||||||
env/
|
configuration.toml
|
||||||
venv/
|
docker-compose.yml
|
||||||
ENV/
|
patches-public-key.asc
|
||||||
env.bak/
|
integrations-public-key.asc
|
||||||
venv.bak/
|
node_modules/
|
||||||
|
|
||||||
# Spyder project settings
|
|
||||||
.spyderproject
|
|
||||||
.spyproject
|
|
||||||
|
|
||||||
# Rope project settings
|
|
||||||
.ropeproject
|
|
||||||
|
|
||||||
# mkdocs documentation
|
|
||||||
/site
|
|
||||||
|
|
||||||
# mypy
|
|
||||||
.mypy_cache/
|
|
||||||
.dmypy.json
|
|
||||||
dmypy.json
|
|
||||||
|
|
||||||
# Pyre type checker
|
|
||||||
.pyre/
|
|
||||||
|
|
||||||
# pytype static type analyzer
|
|
||||||
.pytype/
|
|
||||||
|
|
||||||
# Cython debug symbols
|
|
||||||
cython_debug/
|
|
||||||
|
|
||||||
# PyCharm
|
|
||||||
# JetBrains specific template is maintained in a separate JetBrains.gitignore that can
|
|
||||||
# be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore
|
|
||||||
# and can be added to the global gitignore or merged into this file. For a more nuclear
|
|
||||||
# option (not recommended) you can uncomment the following to ignore the entire idea folder.
|
|
||||||
#.idea/
|
|
||||||
|
|
||||||
# custom
|
|
||||||
env.sh
|
|
||||||
persistence/database.db
|
|
@ -1,20 +0,0 @@
|
|||||||
repos:
|
|
||||||
- repo: https://github.com/pre-commit/pre-commit-hooks
|
|
||||||
rev: v4.6.0
|
|
||||||
hooks:
|
|
||||||
- id: trailing-whitespace
|
|
||||||
- id: end-of-file-fixer
|
|
||||||
- id: check-yaml
|
|
||||||
- id: check-added-large-files
|
|
||||||
- id: check-docstring-first
|
|
||||||
- id: debug-statements
|
|
||||||
- id: requirements-txt-fixer
|
|
||||||
- id: check-toml
|
|
||||||
- id: check-merge-conflict
|
|
||||||
- repo: https://github.com/psf/black
|
|
||||||
rev: 24.3.0
|
|
||||||
hooks:
|
|
||||||
- id: black
|
|
||||||
language_version: python3.11
|
|
||||||
ci:
|
|
||||||
autoupdate_branch: "dev"
|
|
70
.releaserc
Normal file
70
.releaserc
Normal file
@ -0,0 +1,70 @@
|
|||||||
|
{
|
||||||
|
"branches": [
|
||||||
|
"main",
|
||||||
|
{
|
||||||
|
"name": "dev",
|
||||||
|
"prerelease": true
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"plugins": [
|
||||||
|
[
|
||||||
|
"@semantic-release/commit-analyzer", {
|
||||||
|
"releaseRules": [
|
||||||
|
{ "type": "build", "scope": "Needs bump", "release": "patch" }
|
||||||
|
]
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"@semantic-release/release-notes-generator",
|
||||||
|
"@semantic-release/changelog",
|
||||||
|
"gradle-semantic-release-plugin",
|
||||||
|
[
|
||||||
|
"@semantic-release/git",
|
||||||
|
{
|
||||||
|
"assets": [
|
||||||
|
"README.md",
|
||||||
|
"CHANGELOG.md",
|
||||||
|
"gradle.properties"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
],
|
||||||
|
[
|
||||||
|
"@semantic-release/github",
|
||||||
|
{
|
||||||
|
"assets": [
|
||||||
|
{
|
||||||
|
"path": "build/libs/*"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"successComment": false
|
||||||
|
}
|
||||||
|
],
|
||||||
|
[
|
||||||
|
"@codedependant/semantic-release-docker",
|
||||||
|
{
|
||||||
|
"dockerImage": "revanced-api",
|
||||||
|
"dockerTags": [
|
||||||
|
"{{#if prerelease.[0]}}dev{{else}}main{{/if}}",
|
||||||
|
"{{#unless prerelease.[0]}}latest{{/unless}}",
|
||||||
|
"{{version}}"
|
||||||
|
],
|
||||||
|
"dockerRegistry": "ghcr.io",
|
||||||
|
"dockerProject": "revanced",
|
||||||
|
"dockerPlatform": [
|
||||||
|
"linux/amd64",
|
||||||
|
"linux/arm64"
|
||||||
|
],
|
||||||
|
"dockerArgs": {
|
||||||
|
"GITHUB_ACTOR": null,
|
||||||
|
"GITHUB_TOKEN": null,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
[
|
||||||
|
"@saithodev/semantic-release-backmerge",
|
||||||
|
{
|
||||||
|
"backmergeBranches": [{"from": "main", "to": "dev"}],
|
||||||
|
"clearWorkspace": true
|
||||||
|
}
|
||||||
|
]
|
||||||
|
]
|
||||||
|
}
|
10
.vscode/settings.json
vendored
10
.vscode/settings.json
vendored
@ -1,10 +0,0 @@
|
|||||||
{
|
|
||||||
"python.analysis.typeCheckingMode": "off",
|
|
||||||
"spellright.language": [
|
|
||||||
"pt"
|
|
||||||
],
|
|
||||||
"spellright.documentTypes": [
|
|
||||||
"markdown",
|
|
||||||
"latex"
|
|
||||||
]
|
|
||||||
}
|
|
97
CONTRIBUTING.md
Normal file
97
CONTRIBUTING.md
Normal file
@ -0,0 +1,97 @@
|
|||||||
|
<p align="center">
|
||||||
|
<picture>
|
||||||
|
<source
|
||||||
|
width="256px"
|
||||||
|
media="(prefers-color-scheme: dark)"
|
||||||
|
srcset="assets/revanced-headline/revanced-headline-vertical-dark.svg"
|
||||||
|
>
|
||||||
|
<img
|
||||||
|
width="256px"
|
||||||
|
src="assets/revanced-headline/revanced-headline-vertical-light.svg"
|
||||||
|
>
|
||||||
|
</picture>
|
||||||
|
<br>
|
||||||
|
<a href="https://revanced.app/">
|
||||||
|
<picture>
|
||||||
|
<source height="24px" media="(prefers-color-scheme: dark)" srcset="assets/revanced-logo/revanced-logo.svg" />
|
||||||
|
<img height="24px" src="assets/revanced-logo/revanced-logo.svg" />
|
||||||
|
</picture>
|
||||||
|
</a>
|
||||||
|
<a href="https://github.com/ReVanced">
|
||||||
|
<picture>
|
||||||
|
<source height="24px" media="(prefers-color-scheme: dark)" srcset="https://i.ibb.co/dMMmCrW/Git-Hub-Mark.png" />
|
||||||
|
<img height="24px" src="https://i.ibb.co/9wV3HGF/Git-Hub-Mark-Light.png" />
|
||||||
|
</picture>
|
||||||
|
</a>
|
||||||
|
<a href="http://revanced.app/discord">
|
||||||
|
<picture>
|
||||||
|
<source height="24px" media="(prefers-color-scheme: dark)" srcset="https://user-images.githubusercontent.com/13122796/178032563-d4e084b7-244e-4358-af50-26bde6dd4996.png" />
|
||||||
|
<img height="24px" src="https://user-images.githubusercontent.com/13122796/178032563-d4e084b7-244e-4358-af50-26bde6dd4996.png" />
|
||||||
|
</picture>
|
||||||
|
</a>
|
||||||
|
<a href="https://reddit.com/r/revancedapp">
|
||||||
|
<picture>
|
||||||
|
<source height="24px" media="(prefers-color-scheme: dark)" srcset="https://user-images.githubusercontent.com/13122796/178032351-9d9d5619-8ef7-470a-9eec-2744ece54553.png" />
|
||||||
|
<img height="24px" src="https://user-images.githubusercontent.com/13122796/178032351-9d9d5619-8ef7-470a-9eec-2744ece54553.png" />
|
||||||
|
</picture>
|
||||||
|
</a>
|
||||||
|
<a href="https://t.me/app_revanced">
|
||||||
|
<picture>
|
||||||
|
<source height="24px" media="(prefers-color-scheme: dark)" srcset="https://user-images.githubusercontent.com/13122796/178032213-faf25ab8-0bc3-4a94-a730-b524c96df124.png" />
|
||||||
|
<img height="24px" src="https://user-images.githubusercontent.com/13122796/178032213-faf25ab8-0bc3-4a94-a730-b524c96df124.png" />
|
||||||
|
</picture>
|
||||||
|
</a>
|
||||||
|
<a href="https://x.com/revancedapp">
|
||||||
|
<picture>
|
||||||
|
<source media="(prefers-color-scheme: dark)" srcset="https://user-images.githubusercontent.com/93124920/270180600-7c1b38bf-889b-4d68-bd5e-b9d86f91421a.png">
|
||||||
|
<img height="24px" src="https://user-images.githubusercontent.com/93124920/270108715-d80743fa-b330-4809-b1e6-79fbdc60d09c.png" />
|
||||||
|
</picture>
|
||||||
|
</a>
|
||||||
|
<a href="https://www.youtube.com/@ReVanced">
|
||||||
|
<picture>
|
||||||
|
<source height="24px" media="(prefers-color-scheme: dark)" srcset="https://user-images.githubusercontent.com/13122796/178032714-c51c7492-0666-44ac-99c2-f003a695ab50.png" />
|
||||||
|
<img height="24px" src="https://user-images.githubusercontent.com/13122796/178032714-c51c7492-0666-44ac-99c2-f003a695ab50.png" />
|
||||||
|
</picture>
|
||||||
|
</a>
|
||||||
|
<br>
|
||||||
|
<br>
|
||||||
|
Continuing the legacy of Vanced
|
||||||
|
</p>
|
||||||
|
|
||||||
|
# 👋 Contribution guidelines
|
||||||
|
|
||||||
|
This document describes how to contribute to ReVanced API.
|
||||||
|
|
||||||
|
## 📖 Resources to help you get started
|
||||||
|
|
||||||
|
* [Our backlog](https://github.com/orgs/ReVanced/projects/12) is where we keep track of what we're working on
|
||||||
|
* [Issues](https://github.com/ReVanced/revanced-api/issues) are where we keep track of bugs and feature requests
|
||||||
|
|
||||||
|
## 🙏 Submitting a feature request
|
||||||
|
|
||||||
|
Features can be requested by opening an issue using the
|
||||||
|
[Feature request issue template](https://github.com/ReVanced/revanced-api/issues/new?assignees=&labels=Feature+request&projects=&template=feature_request.yml&title=feat%3A+).
|
||||||
|
|
||||||
|
> **Note**
|
||||||
|
> Requests can be accepted or rejected at the discretion of maintainers of ReVanced API.
|
||||||
|
> Good motivation has to be provided for a request to be accepted.
|
||||||
|
|
||||||
|
## 🐞 Submitting a bug report
|
||||||
|
|
||||||
|
If you encounter a bug while using ReVanced API, open an issue using the
|
||||||
|
[Bug report issue template](https://github.com/ReVanced/revanced-api/issues/new?assignees=&labels=Bug+report&projects=&template=bug_report.yml&title=bug%3A+).
|
||||||
|
|
||||||
|
## 📝 How to contribute
|
||||||
|
|
||||||
|
1. Before contributing, it is recommended to open an issue to discuss your change
|
||||||
|
with the maintainers of ReVanced API. This will help you determine whether your change is acceptable
|
||||||
|
and whether it is worth your time to implement it
|
||||||
|
2. Development happens on the `dev` branch. Fork the repository and create your branch from `dev`
|
||||||
|
3. Commit your changes
|
||||||
|
4. Submit a pull request to the `dev` branch of the repository and reference issues
|
||||||
|
that your pull request closes in the description of your pull request
|
||||||
|
5. Our team will review your pull request and provide feedback. Once your pull request is approved,
|
||||||
|
it will be merged into the `dev` branch and will be included in the next release of ReVanced API
|
||||||
|
|
||||||
|
❤️ Thank you for considering contributing to ReVanced API,
|
||||||
|
ReVanced
|
24
Dockerfile
24
Dockerfile
@ -1,19 +1,19 @@
|
|||||||
FROM python:3.11-slim
|
# Build the application
|
||||||
|
FROM gradle:latest AS build
|
||||||
|
|
||||||
|
ARG GITHUB_ACTOR
|
||||||
ARG GITHUB_TOKEN
|
ARG GITHUB_TOKEN
|
||||||
ARG SENTRY_DSN
|
|
||||||
|
|
||||||
ENV GITHUB_TOKEN $GITHUB_TOKEN
|
ENV GITHUB_ACTOR=$GITHUB_ACTOR
|
||||||
ENV SENTRY_DSN $SENTRY_DSN
|
ENV GITHUB_TOKEN=$GITHUB_TOKEN
|
||||||
|
|
||||||
WORKDIR /usr/src/app
|
|
||||||
|
|
||||||
|
WORKDIR /app
|
||||||
COPY . .
|
COPY . .
|
||||||
|
RUN gradle startShadowScript --no-daemon
|
||||||
|
|
||||||
RUN apt update && \
|
# Build the runtime container
|
||||||
apt-get install git build-essential libffi-dev libssl-dev openssl --no-install-recommends -y \
|
FROM eclipse-temurin:latest
|
||||||
&& pip install --no-cache-dir -r requirements.txt
|
|
||||||
|
|
||||||
VOLUME persistence
|
WORKDIR /app
|
||||||
|
COPY --from=build /app/build/libs/revanced-api-*.jar revanced-api.jar
|
||||||
CMD [ "python3", "-m" , "sanic", "app:app", "--fast", "--access-logs", "--motd", "--noisy-exceptions", "-H", "0.0.0.0"]
|
CMD java -jar revanced-api.jar $COMMAND
|
||||||
|
171
README.md
171
README.md
@ -1,44 +1,165 @@
|
|||||||
# ReVanced Releases API
|
<p align="center">
|
||||||
|
<picture>
|
||||||
|
<source
|
||||||
|
width="256px"
|
||||||
|
media="(prefers-color-scheme: dark)"
|
||||||
|
srcset="assets/revanced-headline/revanced-headline-vertical-dark.svg"
|
||||||
|
>
|
||||||
|
<img
|
||||||
|
width="256px"
|
||||||
|
src="assets/revanced-headline/revanced-headline-vertical-light.svg"
|
||||||
|
>
|
||||||
|
</picture>
|
||||||
|
<br>
|
||||||
|
<a href="https://revanced.app/">
|
||||||
|
<picture>
|
||||||
|
<source height="24px" media="(prefers-color-scheme: dark)" srcset="assets/revanced-logo/revanced-logo.svg" />
|
||||||
|
<img height="24px" src="assets/revanced-logo/revanced-logo.svg" />
|
||||||
|
</picture>
|
||||||
|
</a>
|
||||||
|
<a href="https://github.com/ReVanced">
|
||||||
|
<picture>
|
||||||
|
<source height="24px" media="(prefers-color-scheme: dark)" srcset="https://i.ibb.co/dMMmCrW/Git-Hub-Mark.png" />
|
||||||
|
<img height="24px" src="https://i.ibb.co/9wV3HGF/Git-Hub-Mark-Light.png" />
|
||||||
|
</picture>
|
||||||
|
</a>
|
||||||
|
<a href="http://revanced.app/discord">
|
||||||
|
<picture>
|
||||||
|
<source height="24px" media="(prefers-color-scheme: dark)" srcset="https://user-images.githubusercontent.com/13122796/178032563-d4e084b7-244e-4358-af50-26bde6dd4996.png" />
|
||||||
|
<img height="24px" src="https://user-images.githubusercontent.com/13122796/178032563-d4e084b7-244e-4358-af50-26bde6dd4996.png" />
|
||||||
|
</picture>
|
||||||
|
</a>
|
||||||
|
<a href="https://reddit.com/r/revancedapp">
|
||||||
|
<picture>
|
||||||
|
<source height="24px" media="(prefers-color-scheme: dark)" srcset="https://user-images.githubusercontent.com/13122796/178032351-9d9d5619-8ef7-470a-9eec-2744ece54553.png" />
|
||||||
|
<img height="24px" src="https://user-images.githubusercontent.com/13122796/178032351-9d9d5619-8ef7-470a-9eec-2744ece54553.png" />
|
||||||
|
</picture>
|
||||||
|
</a>
|
||||||
|
<a href="https://t.me/app_revanced">
|
||||||
|
<picture>
|
||||||
|
<source height="24px" media="(prefers-color-scheme: dark)" srcset="https://user-images.githubusercontent.com/13122796/178032213-faf25ab8-0bc3-4a94-a730-b524c96df124.png" />
|
||||||
|
<img height="24px" src="https://user-images.githubusercontent.com/13122796/178032213-faf25ab8-0bc3-4a94-a730-b524c96df124.png" />
|
||||||
|
</picture>
|
||||||
|
</a>
|
||||||
|
<a href="https://x.com/revancedapp">
|
||||||
|
<picture>
|
||||||
|
<source media="(prefers-color-scheme: dark)" srcset="https://user-images.githubusercontent.com/93124920/270180600-7c1b38bf-889b-4d68-bd5e-b9d86f91421a.png">
|
||||||
|
<img height="24px" src="https://user-images.githubusercontent.com/93124920/270108715-d80743fa-b330-4809-b1e6-79fbdc60d09c.png" />
|
||||||
|
</picture>
|
||||||
|
</a>
|
||||||
|
<a href="https://www.youtube.com/@ReVanced">
|
||||||
|
<picture>
|
||||||
|
<source height="24px" media="(prefers-color-scheme: dark)" srcset="https://user-images.githubusercontent.com/13122796/178032714-c51c7492-0666-44ac-99c2-f003a695ab50.png" />
|
||||||
|
<img height="24px" src="https://user-images.githubusercontent.com/13122796/178032714-c51c7492-0666-44ac-99c2-f003a695ab50.png" />
|
||||||
|
</picture>
|
||||||
|
</a>
|
||||||
|
<br>
|
||||||
|
<br>
|
||||||
|
Continuing the legacy of Vanced
|
||||||
|
</p>
|
||||||
|
|
||||||
---
|
# 🚀 ReVanced API
|
||||||
|
|
||||||

|

|
||||||
[](https://github.com/revanced/revanced-api/actions/workflows/main.yml)
|

|
||||||
|
|
||||||
---
|
API server for ReVanced.
|
||||||
|
|
||||||
This is a simple API that proxies requests needed to feed the ReVanced Manager and website with data.
|
## ❓ About
|
||||||
|
|
||||||
## Usage
|
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)
|
||||||
|
with updates and ReVanced Patches.
|
||||||
|
|
||||||
To run this API, you need Python 3.11.x. You can install the dependencies with poetry:
|
## 💪 Features
|
||||||
|
|
||||||
|
Some of the features ReVanced API include:
|
||||||
|
|
||||||
|
- 📢 **Announcements**: Post and get announcements grouped by channels
|
||||||
|
- ℹ️ **About**: Get more information such as a description, ways to donate to,
|
||||||
|
and links of the hoster of ReVanced API
|
||||||
|
- 🧩 **Patches**: Get the latest updates of ReVanced Patches, directly from ReVanced API
|
||||||
|
- 👥 **Contributors**: List all contributors involved in the project
|
||||||
|
- 🔄 **Backwards compatibility**: Proxy an old API for migration purposes and backwards compatibility
|
||||||
|
|
||||||
|
## 🚀 How to get started
|
||||||
|
|
||||||
|
ReVanced API can be deployed as a Docker container or used standalone.
|
||||||
|
|
||||||
|
## 🐳 Docker
|
||||||
|
|
||||||
|
To deploy ReVanced API as a Docker container, you can use Docker Compose or Docker CLI.
|
||||||
|
The Docker image is published on GitHub Container registry,
|
||||||
|
so before you can pull the image, you need to [authenticate to the Container registry](https://docs.github.com/en/packages/working-with-a-github-packages-registry/working-with-the-container-registry#authenticating-to-the-container-registry).
|
||||||
|
|
||||||
|
### 🗄️ Docker Compose
|
||||||
|
|
||||||
|
1. Create an `.env` file using [.env.example](.env.example) as a template
|
||||||
|
2. Create a `configuration.toml` file using [configuration.example.toml](configuration.example.toml) as a template
|
||||||
|
3. Create a `docker-compose.yml` file using [docker-compose.example.yml](docker-compose.example.yml) as a template
|
||||||
|
4. Run `docker-compose up -d` to start the server
|
||||||
|
|
||||||
|
### 💻 Docker CLI
|
||||||
|
|
||||||
|
1. Create an `.env` file using [.env.example](.env.example) as a template
|
||||||
|
2. Create a `configuration.toml` file using [configuration.example.toml](configuration.example.toml) as a template
|
||||||
|
3. Start the container using the following command:
|
||||||
```shell
|
```shell
|
||||||
poetry install
|
docker run -d --name revanced-api \
|
||||||
|
# Mount the .env file
|
||||||
|
-v $(pwd)/.env:/app/.env \
|
||||||
|
# Mount the configuration.toml file
|
||||||
|
-v $(pwd)/configuration.toml:/app/configuration.toml \
|
||||||
|
# Mount the persistence folder
|
||||||
|
-v $(pwd)/persistence:/app/persistence \
|
||||||
|
# Expose the port 8888
|
||||||
|
-p 8888:8888 \
|
||||||
|
# Use the start command to start the server
|
||||||
|
-e COMMAND=start \
|
||||||
|
# Pull the image from the GitHub Container registry
|
||||||
|
ghcr.io/revanced/revanced-api:latest
|
||||||
```
|
```
|
||||||
|
|
||||||
Create the following environment variables:
|
## 🖥️ Standalone
|
||||||
|
|
||||||
- `GITHUB_TOKEN` with a valid GitHub token with read access to public repositories
|
To deploy ReVanced API standalone, you can either use the pre-built executable or build it from source.
|
||||||
- `SECRET_KEY` to salt login sessions
|
|
||||||
- `USERNAME` & `PASSWORD` to initialize the database with a user to login with to authenticated endpoints
|
|
||||||
|
|
||||||
Then, you can run the API in development mode with:
|
### 📦 Pre-built executable
|
||||||
|
|
||||||
```shell
|
A Java Runtime Environment (JRE) must be installed.
|
||||||
poetry run sanic app:app --dev
|
|
||||||
```
|
|
||||||
|
|
||||||
or in production mode with:
|
1. [Download](https://github.com/ReVanced/revanced-api/releases/latest) ReVanced API to a folder
|
||||||
|
2. In the same folder, create an `.env` file using [.env.example](.env.example) as a template
|
||||||
|
3. In the same folder, create a `configuration.toml` file
|
||||||
|
using [configuration.example.toml](configuration.example.toml) as a template
|
||||||
|
4. Run `java -jar revanced-api.jar start` to start the server
|
||||||
|
|
||||||
```shell
|
### 🛠️ From source
|
||||||
poetry run sanic app:app --fast
|
|
||||||
```
|
|
||||||
|
|
||||||
## Contributing
|
A Java Development Kit (JDK) and Git must be installed.
|
||||||
|
|
||||||
If you want to contribute to this project, feel free to open a pull request or an issue. We don't do much here, so it's pretty easy to contribute.
|
1. Run `git clone git@github.com:ReVanced/revanced-api.git` to clone the repository
|
||||||
|
2. Copy [.env.example](.env.example) to `.env` and fill in the required values
|
||||||
|
3. Copy [configuration.example.toml](configuration.example.toml) to `configuration.toml` and fill in the required values
|
||||||
|
4. Run `gradlew run --args=start` to start the server
|
||||||
|
|
||||||
## License
|
## 📚 Everything else
|
||||||
|
|
||||||
This project is licensed under the AGPLv3 License - see the [LICENSE](LICENSE) file for details.
|
### 📙 Contributing
|
||||||
|
|
||||||
|
Thank you for considering contributing to ReVanced API. You can find the contribution guidelines [here](CONTRIBUTING.md).
|
||||||
|
|
||||||
|
### 🛠️ Building
|
||||||
|
|
||||||
|
To build ReVanced API, a Java Development Kit (JDK) and Git must be installed.
|
||||||
|
Follow the steps below to build ReVanced API:
|
||||||
|
|
||||||
|
1. Run `git clone git@github.com:ReVanced/revanced-api.git` to clone the repository
|
||||||
|
2. Run `gradlew build` to build the project
|
||||||
|
|
||||||
|
## 📜 Licence
|
||||||
|
|
||||||
|
ReVanced API is licensed under the AGPLv3 licence. Please see the [licence file](LICENSE) for more information.
|
||||||
|
[tl;dr](https://www.tldrlegal.com/license/gnu-affero-general-public-license-v3-agpl-3-0) you may copy, distribute and
|
||||||
|
modify ReVanced API as long as you track changes/dates in source files.
|
||||||
|
Any modifications to ReVanced API must also be made available under the GPL along with build & install instructions.
|
||||||
|
11
SECURITY.md
11
SECURITY.md
@ -1,11 +0,0 @@
|
|||||||
# Security Policy
|
|
||||||
|
|
||||||
## Supported Tags
|
|
||||||
|
|
||||||
| Tag | ReVanced Version |
|
|
||||||
| ------- | ------------------ |
|
|
||||||
| latest | latest upstream |
|
|
||||||
|
|
||||||
## Reporting a Vulnerability
|
|
||||||
|
|
||||||
To report a vulnerability, please open an Issue in our issue tracker here on GitHub.
|
|
@ -1,27 +0,0 @@
|
|||||||
# api/__init__.py
|
|
||||||
from sanic import Blueprint
|
|
||||||
import importlib
|
|
||||||
import pkgutil
|
|
||||||
from api.utils.versioning import get_version
|
|
||||||
|
|
||||||
# Dynamically import all modules in the 'api' package, excluding subdirectories
|
|
||||||
versioned_blueprints: dict[str, list] = {}
|
|
||||||
for finder, module_name, ispkg in pkgutil.iter_modules(["api"]):
|
|
||||||
if not ispkg:
|
|
||||||
# Import the module
|
|
||||||
module = importlib.import_module(f"api.{module_name}")
|
|
||||||
|
|
||||||
# Add the module's blueprint to the versioned list, if it exists
|
|
||||||
if hasattr(module, module_name):
|
|
||||||
blueprint = getattr(module, module_name)
|
|
||||||
version = get_version(module_name)
|
|
||||||
versioned_blueprints.setdefault(version, []).append(blueprint)
|
|
||||||
|
|
||||||
# Create Blueprint groups for each version
|
|
||||||
api = []
|
|
||||||
for version, blueprints in versioned_blueprints.items():
|
|
||||||
if version == "old" or version == "v0":
|
|
||||||
group = Blueprint.group(*blueprints, url_prefix="/")
|
|
||||||
else:
|
|
||||||
group = Blueprint.group(*blueprints, version=version, url_prefix="/")
|
|
||||||
api.append(group)
|
|
@ -1,242 +0,0 @@
|
|||||||
"""
|
|
||||||
This module provides a blueprint for the announcements endpoint.
|
|
||||||
|
|
||||||
Routes:
|
|
||||||
- GET /announcements: Get a list of announcements from all channels.
|
|
||||||
- GET /announcements/<channel:str>: Get a list of announcement from a channel.
|
|
||||||
- GET /announcements/latest: Get the latest announcement.
|
|
||||||
- GET /announcements/<channel:str>/latest: Get the latest announcement from a channel.
|
|
||||||
- POST /announcements/<channel:str>: Create an announcement.
|
|
||||||
- DELETE /announcements/<announcement_id:int>: Delete an announcement.
|
|
||||||
"""
|
|
||||||
|
|
||||||
import os
|
|
||||||
import datetime
|
|
||||||
from sanic import Blueprint, Request
|
|
||||||
from sanic.response import JSONResponse, json
|
|
||||||
from sanic_ext import openapi
|
|
||||||
from data.database import Session
|
|
||||||
from data.models import AnnouncementDbModel, AttachmentDbModel
|
|
||||||
|
|
||||||
import sanic_beskar
|
|
||||||
|
|
||||||
from api.models.announcements import AnnouncementResponseModel
|
|
||||||
from api.utils.limiter import limiter
|
|
||||||
|
|
||||||
announcements: Blueprint = Blueprint(os.path.basename(__file__).strip(".py"))
|
|
||||||
|
|
||||||
|
|
||||||
@announcements.get("/announcements")
|
|
||||||
@openapi.definition(
|
|
||||||
summary="Get a list of announcements",
|
|
||||||
response=[[AnnouncementResponseModel]],
|
|
||||||
)
|
|
||||||
async def get_announcements(request: Request) -> JSONResponse:
|
|
||||||
"""
|
|
||||||
Retrieve a list of announcements.
|
|
||||||
|
|
||||||
**Returns:**
|
|
||||||
- JSONResponse: A Sanic JSONResponse object containing a list of announcements from all channels.
|
|
||||||
"""
|
|
||||||
|
|
||||||
session = Session()
|
|
||||||
|
|
||||||
announcements = [
|
|
||||||
AnnouncementResponseModel.to_response(announcement)
|
|
||||||
for announcement in session.query(AnnouncementDbModel).all()
|
|
||||||
]
|
|
||||||
|
|
||||||
session.close()
|
|
||||||
|
|
||||||
return json(announcements, status=200)
|
|
||||||
|
|
||||||
|
|
||||||
@announcements.get("/announcements/<channel:str>")
|
|
||||||
@openapi.definition(
|
|
||||||
summary="Get a list of announcements from a channel",
|
|
||||||
response=[[AnnouncementResponseModel]],
|
|
||||||
)
|
|
||||||
async def get_announcements_for_channel(request: Request, channel: str) -> JSONResponse:
|
|
||||||
"""
|
|
||||||
Retrieve a list of announcements from a channel.
|
|
||||||
|
|
||||||
**Args:**
|
|
||||||
- channel (str): The channel to retrieve announcements from.
|
|
||||||
|
|
||||||
**Returns:**
|
|
||||||
- JSONResponse: A Sanic JSONResponse object containing a list of announcements from a channel.
|
|
||||||
"""
|
|
||||||
|
|
||||||
session = Session()
|
|
||||||
|
|
||||||
announcements = [
|
|
||||||
AnnouncementResponseModel.to_response(announcement)
|
|
||||||
for announcement in session.query(AnnouncementDbModel)
|
|
||||||
.filter_by(channel=channel)
|
|
||||||
.all()
|
|
||||||
]
|
|
||||||
|
|
||||||
session.close()
|
|
||||||
|
|
||||||
return json(announcements, status=200)
|
|
||||||
|
|
||||||
|
|
||||||
@announcements.get("/announcements/latest")
|
|
||||||
@openapi.definition(
|
|
||||||
summary="Get the latest announcement",
|
|
||||||
response=AnnouncementResponseModel,
|
|
||||||
)
|
|
||||||
async def get_latest_announcement(request: Request) -> JSONResponse:
|
|
||||||
"""
|
|
||||||
Retrieve the latest announcement.
|
|
||||||
|
|
||||||
**Returns:**
|
|
||||||
- JSONResponse: A Sanic JSONResponse object containing the latest announcement.
|
|
||||||
"""
|
|
||||||
|
|
||||||
session = Session()
|
|
||||||
|
|
||||||
announcement = (
|
|
||||||
session.query(AnnouncementDbModel)
|
|
||||||
.order_by(AnnouncementDbModel.id.desc())
|
|
||||||
.first()
|
|
||||||
)
|
|
||||||
|
|
||||||
if not announcement:
|
|
||||||
return json({"error": "No announcement found"}, status=404)
|
|
||||||
|
|
||||||
announcement_response = AnnouncementResponseModel.to_response(announcement)
|
|
||||||
|
|
||||||
session.close()
|
|
||||||
|
|
||||||
return json(announcement_response, status=200)
|
|
||||||
|
|
||||||
|
|
||||||
# for specific channel
|
|
||||||
|
|
||||||
|
|
||||||
@announcements.get("/announcements/<channel:str>/latest")
|
|
||||||
@openapi.definition(
|
|
||||||
summary="Get the latest announcement from a channel",
|
|
||||||
response=AnnouncementResponseModel,
|
|
||||||
)
|
|
||||||
async def get_latest_announcement_for_channel(
|
|
||||||
request: Request, channel: str
|
|
||||||
) -> JSONResponse:
|
|
||||||
"""
|
|
||||||
Retrieve the latest announcement from a channel.
|
|
||||||
|
|
||||||
**Args:**
|
|
||||||
- channel (str): The channel to retrieve the latest announcement from.
|
|
||||||
|
|
||||||
**Returns:**
|
|
||||||
- JSONResponse: A Sanic JSONResponse object containing the latest announcement from a channel.
|
|
||||||
"""
|
|
||||||
|
|
||||||
session = Session()
|
|
||||||
|
|
||||||
announcement = (
|
|
||||||
session.query(AnnouncementDbModel)
|
|
||||||
.filter_by(channel=channel)
|
|
||||||
.order_by(AnnouncementDbModel.id.desc())
|
|
||||||
.first()
|
|
||||||
)
|
|
||||||
|
|
||||||
if not announcement:
|
|
||||||
return json({"error": "No announcement found"}, status=404)
|
|
||||||
|
|
||||||
announcement_response = AnnouncementResponseModel.to_response(announcement)
|
|
||||||
|
|
||||||
session.close()
|
|
||||||
|
|
||||||
return json(announcement_response, status=200)
|
|
||||||
|
|
||||||
|
|
||||||
@announcements.post("/announcements/<channel:str>")
|
|
||||||
@limiter.limit("16 per hour")
|
|
||||||
@sanic_beskar.auth_required
|
|
||||||
@openapi.definition(
|
|
||||||
summary="Create an announcement",
|
|
||||||
body=AnnouncementResponseModel,
|
|
||||||
response=AnnouncementResponseModel,
|
|
||||||
)
|
|
||||||
async def post_announcement(request: Request, channel: str) -> JSONResponse:
|
|
||||||
"""
|
|
||||||
Create an announcement.
|
|
||||||
|
|
||||||
**Args:**
|
|
||||||
- author (str | None): The author of the announcement.
|
|
||||||
- title (str): The title of the announcement.
|
|
||||||
- content (ContentFields | None): The content of the announcement.
|
|
||||||
- channel (str): The channel to create the announcement in.
|
|
||||||
- nevel (int | None): The severity of the announcement.
|
|
||||||
"""
|
|
||||||
session = Session()
|
|
||||||
|
|
||||||
if not request.json:
|
|
||||||
return json({"error": "Missing request body"}, status=400)
|
|
||||||
|
|
||||||
content = request.json.get("content", None)
|
|
||||||
|
|
||||||
author = request.json.get("author", None)
|
|
||||||
title = request.json.get("title")
|
|
||||||
message = content["message"] if content and "message" in content else None
|
|
||||||
attachments = (
|
|
||||||
list(
|
|
||||||
map(
|
|
||||||
lambda url: AttachmentDbModel(attachment_url=url),
|
|
||||||
content["attachments"],
|
|
||||||
)
|
|
||||||
)
|
|
||||||
if content and "attachments" in content
|
|
||||||
else []
|
|
||||||
)
|
|
||||||
level = request.json.get("level", None)
|
|
||||||
created_at = datetime.datetime.now()
|
|
||||||
|
|
||||||
announcement = AnnouncementDbModel(
|
|
||||||
author=author,
|
|
||||||
title=title,
|
|
||||||
message=message,
|
|
||||||
attachments=attachments,
|
|
||||||
channel=channel,
|
|
||||||
created_at=created_at,
|
|
||||||
level=level,
|
|
||||||
)
|
|
||||||
|
|
||||||
session.add(announcement)
|
|
||||||
session.commit()
|
|
||||||
session.close()
|
|
||||||
|
|
||||||
return json({}, status=200)
|
|
||||||
|
|
||||||
|
|
||||||
@announcements.delete("/announcements/<announcement_id:int>")
|
|
||||||
@sanic_beskar.auth_required
|
|
||||||
@openapi.definition(
|
|
||||||
summary="Delete an announcement",
|
|
||||||
)
|
|
||||||
async def delete_announcement(request: Request, announcement_id: int) -> JSONResponse:
|
|
||||||
"""
|
|
||||||
Delete an announcement.
|
|
||||||
|
|
||||||
**Args:**
|
|
||||||
- announcement_id (int): The ID of the announcement to delete.
|
|
||||||
|
|
||||||
**Exceptions:**
|
|
||||||
- 404: Announcement not found.
|
|
||||||
"""
|
|
||||||
session = Session()
|
|
||||||
|
|
||||||
announcement = (
|
|
||||||
session.query(AnnouncementDbModel).filter_by(id=announcement_id).first()
|
|
||||||
)
|
|
||||||
|
|
||||||
if not announcement:
|
|
||||||
return json({"error": "Announcement not found"}, status=404)
|
|
||||||
|
|
||||||
session.delete(announcement)
|
|
||||||
session.commit()
|
|
||||||
session.close()
|
|
||||||
|
|
||||||
return json({}, status=200)
|
|
@ -1,91 +0,0 @@
|
|||||||
from abc import abstractmethod
|
|
||||||
from typing import Any, Protocol
|
|
||||||
|
|
||||||
from api.backends.entities import *
|
|
||||||
|
|
||||||
|
|
||||||
class Backend(Protocol):
|
|
||||||
"""Interface for a generic backend.
|
|
||||||
|
|
||||||
Attributes:
|
|
||||||
name (str): Name of the backend.
|
|
||||||
base_url (str): Base URL of the backend.
|
|
||||||
|
|
||||||
Methods:
|
|
||||||
list_releases: Retrieve a list of releases.
|
|
||||||
get_release_by_tag_name: Retrieve a release by its tag name.
|
|
||||||
get_latest_release: Retrieve the latest release.
|
|
||||||
get_latest_pre_release: Retrieve the latest pre-release.
|
|
||||||
get_release_notes: Retrieve the release notes of a specific release.
|
|
||||||
get_contributors: Retrieve the list of contributors.
|
|
||||||
get_patches: Retrieve the patches of a specific release.
|
|
||||||
"""
|
|
||||||
|
|
||||||
name: str
|
|
||||||
base_url: str
|
|
||||||
|
|
||||||
def __init__(self, name: str, base_url: str):
|
|
||||||
self.name = name
|
|
||||||
self.base_url = base_url
|
|
||||||
|
|
||||||
@abstractmethod
|
|
||||||
async def list_releases(self, *args: Any, **kwargs: Any) -> list[Release]:
|
|
||||||
raise NotImplementedError
|
|
||||||
|
|
||||||
@abstractmethod
|
|
||||||
async def get_release_by_tag_name(self, *args: Any, **kwargs: Any) -> Release:
|
|
||||||
raise NotImplementedError
|
|
||||||
|
|
||||||
@abstractmethod
|
|
||||||
async def get_latest_release(self, *args: Any, **kwargs: Any) -> Release:
|
|
||||||
raise NotImplementedError
|
|
||||||
|
|
||||||
@abstractmethod
|
|
||||||
async def get_latest_pre_release(self, *args: Any, **kwargs: Any) -> Release:
|
|
||||||
raise NotImplementedError
|
|
||||||
|
|
||||||
@abstractmethod
|
|
||||||
async def get_contributors(self, *args: Any, **kwargs: Any) -> list[Contributor]:
|
|
||||||
raise NotImplementedError
|
|
||||||
|
|
||||||
@abstractmethod
|
|
||||||
async def get_patches(self, *args: Any, **kwargs: Any) -> list[dict]:
|
|
||||||
raise NotImplementedError
|
|
||||||
|
|
||||||
@abstractmethod
|
|
||||||
async def get_team_members(self, *args: Any, **kwargs: Any) -> list[Contributor]:
|
|
||||||
raise NotImplementedError
|
|
||||||
|
|
||||||
|
|
||||||
class Repository:
|
|
||||||
"""A repository that communicates with a specific backend.
|
|
||||||
|
|
||||||
Attributes:
|
|
||||||
backend (Backend): The backend instance used to communicate with the repository.
|
|
||||||
"""
|
|
||||||
|
|
||||||
def __init__(self, backend: Backend):
|
|
||||||
self.backend = backend
|
|
||||||
|
|
||||||
|
|
||||||
class AppInfoProvider(Protocol):
|
|
||||||
"""Interface for a generic app info provider.
|
|
||||||
|
|
||||||
Attributes:
|
|
||||||
name (str): Name of the app info provider.
|
|
||||||
base_url (str): Base URL of the app info provider.
|
|
||||||
|
|
||||||
Methods:
|
|
||||||
get_app_info: Retrieve information about an app.
|
|
||||||
"""
|
|
||||||
|
|
||||||
name: str
|
|
||||||
base_url: str
|
|
||||||
|
|
||||||
def __init__(self, name: str, base_url: str):
|
|
||||||
self.name = name
|
|
||||||
self.base_url = base_url
|
|
||||||
|
|
||||||
@abstractmethod
|
|
||||||
async def get_app_info(self, *args: Any, **kwargs: Any) -> AppInfo:
|
|
||||||
raise NotImplementedError
|
|
@ -1,173 +0,0 @@
|
|||||||
from typing import Optional
|
|
||||||
from dataclasses import dataclass
|
|
||||||
|
|
||||||
|
|
||||||
@dataclass
|
|
||||||
class Metadata(dict):
|
|
||||||
"""
|
|
||||||
Represents the metadata of a release.
|
|
||||||
|
|
||||||
Attributes:
|
|
||||||
- tag_name (str): The name of the release tag.
|
|
||||||
- name (str): The name of the release.
|
|
||||||
- body (str): The body of the release.
|
|
||||||
- draft (bool): Whether the release is a draft.
|
|
||||||
- prerelease (bool): Whether the release is a prerelease.
|
|
||||||
- created_at (str): The creation date of the release.
|
|
||||||
- published_at (str): The publication date of the release.
|
|
||||||
"""
|
|
||||||
|
|
||||||
def __init__(
|
|
||||||
self,
|
|
||||||
tag_name: str,
|
|
||||||
name: str,
|
|
||||||
draft: bool,
|
|
||||||
prerelease: bool,
|
|
||||||
created_at: str,
|
|
||||||
published_at: str,
|
|
||||||
body: str,
|
|
||||||
repository: Optional[str] = None,
|
|
||||||
):
|
|
||||||
dict.__init__(
|
|
||||||
self,
|
|
||||||
tag_name=tag_name,
|
|
||||||
name=name,
|
|
||||||
draft=draft,
|
|
||||||
prerelease=prerelease,
|
|
||||||
created_at=created_at,
|
|
||||||
published_at=published_at,
|
|
||||||
body=body,
|
|
||||||
repository=repository,
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
@dataclass
|
|
||||||
class Asset(dict):
|
|
||||||
"""
|
|
||||||
Represents an asset in a release.
|
|
||||||
|
|
||||||
Attributes:
|
|
||||||
- name (str): The name of the asset.
|
|
||||||
- content_type (str): The MIME type of the asset content.
|
|
||||||
- download_count (int): The number of times the asset has been downloaded.
|
|
||||||
- download_url (str): The URL to download the asset.
|
|
||||||
"""
|
|
||||||
|
|
||||||
def __init__(
|
|
||||||
self,
|
|
||||||
name: str,
|
|
||||||
content_type: str,
|
|
||||||
download_count: int,
|
|
||||||
browser_download_url: str,
|
|
||||||
):
|
|
||||||
dict.__init__(
|
|
||||||
self,
|
|
||||||
name=name,
|
|
||||||
content_type=content_type,
|
|
||||||
download_count=download_count,
|
|
||||||
browser_download_url=browser_download_url,
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
@dataclass
|
|
||||||
class Release(dict):
|
|
||||||
"""
|
|
||||||
Represents a release.
|
|
||||||
|
|
||||||
Attributes:
|
|
||||||
- metadata (Metadata): The metadata of the release.
|
|
||||||
- assets (list[Asset]): The assets of the release.
|
|
||||||
"""
|
|
||||||
|
|
||||||
def __init__(self, metadata: Metadata, assets: list[Asset]):
|
|
||||||
dict.__init__(self, metadata=metadata, assets=assets)
|
|
||||||
|
|
||||||
|
|
||||||
@dataclass
|
|
||||||
class Contributor(dict):
|
|
||||||
"""
|
|
||||||
Represents a contributor to a repository.
|
|
||||||
|
|
||||||
Attributes:
|
|
||||||
- login (str): The GitHub username of the contributor.
|
|
||||||
- avatar_url (str): The URL to the contributor's avatar image.
|
|
||||||
- html_url (str): The URL to the contributor's GitHub profile.
|
|
||||||
- contributions (Optional[int]): The number of contributions the contributor has made to the repository.
|
|
||||||
"""
|
|
||||||
|
|
||||||
def __init__(
|
|
||||||
self,
|
|
||||||
login: str,
|
|
||||||
avatar_url: str,
|
|
||||||
html_url: str,
|
|
||||||
contributions: Optional[int] = None,
|
|
||||||
bio: Optional[str] = None,
|
|
||||||
keys: Optional[str] = None,
|
|
||||||
):
|
|
||||||
match contributions, bio, keys:
|
|
||||||
case None, None, None:
|
|
||||||
dict.__init__(
|
|
||||||
self,
|
|
||||||
login=login,
|
|
||||||
avatar_url=avatar_url,
|
|
||||||
html_url=html_url,
|
|
||||||
bio=bio,
|
|
||||||
keys=keys,
|
|
||||||
)
|
|
||||||
case int(_), None, None:
|
|
||||||
dict.__init__(
|
|
||||||
self,
|
|
||||||
login=login,
|
|
||||||
avatar_url=avatar_url,
|
|
||||||
html_url=html_url,
|
|
||||||
contributions=contributions,
|
|
||||||
)
|
|
||||||
case None, str(_), None:
|
|
||||||
dict.__init__(
|
|
||||||
self,
|
|
||||||
login=login,
|
|
||||||
avatar_url=avatar_url,
|
|
||||||
html_url=html_url,
|
|
||||||
bio=bio,
|
|
||||||
)
|
|
||||||
case int(_), str(_), str(_):
|
|
||||||
dict.__init__(
|
|
||||||
self,
|
|
||||||
login=login,
|
|
||||||
avatar_url=avatar_url,
|
|
||||||
html_url=html_url,
|
|
||||||
contributions=contributions,
|
|
||||||
bio=bio,
|
|
||||||
keys=keys,
|
|
||||||
)
|
|
||||||
case None, str(_), str(_):
|
|
||||||
dict.__init__(
|
|
||||||
self,
|
|
||||||
login=login,
|
|
||||||
avatar_url=avatar_url,
|
|
||||||
html_url=html_url,
|
|
||||||
bio=bio,
|
|
||||||
keys=keys,
|
|
||||||
)
|
|
||||||
case _:
|
|
||||||
raise ValueError("Invalid arguments")
|
|
||||||
|
|
||||||
|
|
||||||
@dataclass
|
|
||||||
class AppInfo(dict):
|
|
||||||
"""
|
|
||||||
Represents the information of an app.
|
|
||||||
|
|
||||||
Attributes:
|
|
||||||
- name (str): The name of the app.
|
|
||||||
- category (str): The app category.
|
|
||||||
- logo (str): The base64 enconded app logo.
|
|
||||||
"""
|
|
||||||
|
|
||||||
def __init__(self, name: str, category: str, logo: str):
|
|
||||||
dict.__init__(
|
|
||||||
self,
|
|
||||||
name=name,
|
|
||||||
category=category,
|
|
||||||
logo=logo,
|
|
||||||
)
|
|
@ -1,455 +0,0 @@
|
|||||||
import asyncio
|
|
||||||
import os
|
|
||||||
from operator import eq
|
|
||||||
from typing import Any, Optional
|
|
||||||
|
|
||||||
import ujson
|
|
||||||
from aiohttp import ClientResponse
|
|
||||||
from sanic import SanicException
|
|
||||||
from cytoolz import filter, map, partial, curry, pipe
|
|
||||||
from cytoolz.dicttoolz import get_in, keyfilter
|
|
||||||
from cytoolz.itertoolz import mapcat, pluck
|
|
||||||
|
|
||||||
from api.backends.backend import Backend, Repository
|
|
||||||
from api.backends.entities import *
|
|
||||||
from api.backends.entities import Contributor
|
|
||||||
from api.utils.http_utils import http_get
|
|
||||||
|
|
||||||
repo_name: str = "github"
|
|
||||||
base_url: str = "https://api.github.com"
|
|
||||||
|
|
||||||
|
|
||||||
class GithubRepository(Repository):
|
|
||||||
"""
|
|
||||||
A repository class that represents a GitHub repository.
|
|
||||||
|
|
||||||
Args:
|
|
||||||
owner (str): The username of the owner of the GitHub repository.
|
|
||||||
name (str): The name of the GitHub repository.
|
|
||||||
"""
|
|
||||||
|
|
||||||
def __init__(self, owner: str, name: str):
|
|
||||||
"""
|
|
||||||
Initializes a new instance of the GithubRepository class.
|
|
||||||
|
|
||||||
Args:
|
|
||||||
owner (str): The username of the owner of the GitHub repository.
|
|
||||||
name (str): The name of the GitHub repository.
|
|
||||||
"""
|
|
||||||
super().__init__(Github())
|
|
||||||
self.owner = owner
|
|
||||||
self.name = name
|
|
||||||
|
|
||||||
|
|
||||||
class Github(Backend):
|
|
||||||
"""
|
|
||||||
A backend class that interacts with the GitHub API.
|
|
||||||
|
|
||||||
Attributes:
|
|
||||||
name (str): The name of the GitHub backend.
|
|
||||||
base_url (str): The base URL of the GitHub API.
|
|
||||||
token (str): The GitHub access token used for authentication.
|
|
||||||
headers (dict[str, str]): The HTTP headers to be sent with each request to the GitHub API.
|
|
||||||
"""
|
|
||||||
|
|
||||||
def __init__(self):
|
|
||||||
"""
|
|
||||||
Initializes a new instance of the GitHub class.
|
|
||||||
"""
|
|
||||||
super().__init__(repo_name, base_url)
|
|
||||||
self.token: Optional[str] = os.getenv("GITHUB_TOKEN")
|
|
||||||
self.headers: dict[str, str] = {
|
|
||||||
"Authorization": f"Bearer {self.token}",
|
|
||||||
"Accept": "application/vnd.github+json",
|
|
||||||
"X-GitHub-Api-Version": "2022-11-28",
|
|
||||||
}
|
|
||||||
self.repositories_rest_endpoint: str = f"{base_url}/repos"
|
|
||||||
|
|
||||||
@staticmethod
|
|
||||||
async def __assemble_release(release: dict) -> Release:
|
|
||||||
async def __assemble_asset(asset: dict) -> Asset:
|
|
||||||
asset_data: dict = keyfilter(
|
|
||||||
lambda key: key
|
|
||||||
in {"name", "content_type", "download_count", "browser_download_url"},
|
|
||||||
asset,
|
|
||||||
)
|
|
||||||
return Asset(**asset_data)
|
|
||||||
|
|
||||||
filter_metadata = keyfilter(
|
|
||||||
lambda key: key
|
|
||||||
in {
|
|
||||||
"tag_name",
|
|
||||||
"name",
|
|
||||||
"draft",
|
|
||||||
"prerelease",
|
|
||||||
"created_at",
|
|
||||||
"published_at",
|
|
||||||
"body",
|
|
||||||
},
|
|
||||||
release,
|
|
||||||
)
|
|
||||||
metadata = Metadata(**filter_metadata)
|
|
||||||
assets = await asyncio.gather(*map(__assemble_asset, release["assets"]))
|
|
||||||
return Release(metadata=metadata, assets=assets)
|
|
||||||
|
|
||||||
@staticmethod
|
|
||||||
async def __assemble_contributor(
|
|
||||||
contributor: dict, team_view: bool = False
|
|
||||||
) -> Contributor:
|
|
||||||
match team_view:
|
|
||||||
case True:
|
|
||||||
keys = {"login", "avatar_url", "html_url", "bio"}
|
|
||||||
case _:
|
|
||||||
keys = {"login", "avatar_url", "html_url", "contributions"}
|
|
||||||
|
|
||||||
filter_contributor = keyfilter(
|
|
||||||
lambda key: key in keys,
|
|
||||||
contributor,
|
|
||||||
)
|
|
||||||
|
|
||||||
if team_view:
|
|
||||||
filter_contributor["keys"] = (
|
|
||||||
f"{base_url.replace('api.', '')}/{filter_contributor['login']}.gpg"
|
|
||||||
)
|
|
||||||
|
|
||||||
return Contributor(**filter_contributor)
|
|
||||||
|
|
||||||
@staticmethod
|
|
||||||
async def __validate_request(_response: ClientResponse) -> None:
|
|
||||||
if _response.status != 200:
|
|
||||||
raise SanicException(
|
|
||||||
context=await _response.json(loads=ujson.loads),
|
|
||||||
status_code=_response.status,
|
|
||||||
)
|
|
||||||
|
|
||||||
async def list_releases(
|
|
||||||
self, repository: GithubRepository, per_page: int = 30, page: int = 1
|
|
||||||
) -> list[Release]:
|
|
||||||
"""
|
|
||||||
Returns a list of Release objects for a given GitHub repository.
|
|
||||||
|
|
||||||
Args:
|
|
||||||
repository (GithubRepository): The GitHub repository for which to retrieve the releases.
|
|
||||||
per_page (int): The number of releases to return per page.
|
|
||||||
page (int): The page number of the releases to return.
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
list[Release]: A list of Release objects.
|
|
||||||
"""
|
|
||||||
list_releases_endpoint: str = (
|
|
||||||
f"{self.repositories_rest_endpoint}/{repository.owner}/{repository.name}/releases?per_page={per_page}&page={page}"
|
|
||||||
)
|
|
||||||
response: ClientResponse = await http_get(
|
|
||||||
headers=self.headers, url=list_releases_endpoint
|
|
||||||
)
|
|
||||||
await self.__validate_request(response)
|
|
||||||
releases: list[Release] = await asyncio.gather(
|
|
||||||
*map(
|
|
||||||
lambda release: self.__assemble_release(release),
|
|
||||||
await response.json(loads=ujson.loads),
|
|
||||||
)
|
|
||||||
)
|
|
||||||
return releases
|
|
||||||
|
|
||||||
async def get_release_by_tag_name(
|
|
||||||
self, repository: GithubRepository, tag_name: str
|
|
||||||
) -> Release:
|
|
||||||
"""
|
|
||||||
Retrieves a specific release for a given GitHub repository by its tag name.
|
|
||||||
|
|
||||||
Args:
|
|
||||||
repository (GithubRepository): The GitHub repository for which to retrieve the release.
|
|
||||||
tag_name (str): The tag name of the release to retrieve.
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
Release: The Release object representing the retrieved release.
|
|
||||||
"""
|
|
||||||
release_by_tag_endpoint: str = (
|
|
||||||
f"{self.repositories_rest_endpoint}/{repository.owner}/{repository.name}/releases/tags/{tag_name}"
|
|
||||||
)
|
|
||||||
response: ClientResponse = await http_get(
|
|
||||||
headers=self.headers, url=release_by_tag_endpoint
|
|
||||||
)
|
|
||||||
await self.__validate_request(response)
|
|
||||||
return await self.__assemble_release(await response.json(loads=ujson.loads))
|
|
||||||
|
|
||||||
async def get_latest_release(
|
|
||||||
self,
|
|
||||||
repository: GithubRepository,
|
|
||||||
) -> Release:
|
|
||||||
"""Get the latest release for a given repository.
|
|
||||||
|
|
||||||
Args:
|
|
||||||
repository (GithubRepository): The GitHub repository for which to retrieve the release.
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
Release: The latest release for the given repository.
|
|
||||||
"""
|
|
||||||
latest_release_endpoint: str = (
|
|
||||||
f"{self.repositories_rest_endpoint}/{repository.owner}/{repository.name}/releases/latest"
|
|
||||||
)
|
|
||||||
response: ClientResponse = await http_get(
|
|
||||||
headers=self.headers, url=latest_release_endpoint
|
|
||||||
)
|
|
||||||
await self.__validate_request(response)
|
|
||||||
return await self.__assemble_release(await response.json(loads=ujson.loads))
|
|
||||||
|
|
||||||
async def get_latest_pre_release(
|
|
||||||
self,
|
|
||||||
repository: GithubRepository,
|
|
||||||
) -> Release:
|
|
||||||
"""Get the latest pre-release for a given repository.
|
|
||||||
|
|
||||||
Args:
|
|
||||||
repository (GithubRepository): The GitHub repository for which to retrieve the release.
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
Release: The latest pre-release for the given repository.
|
|
||||||
"""
|
|
||||||
list_releases_endpoint: str = (
|
|
||||||
f"{self.repositories_rest_endpoint}/{repository.owner}/{repository.name}/releases?per_page=10&page=1"
|
|
||||||
)
|
|
||||||
response: ClientResponse = await http_get(
|
|
||||||
headers=self.headers, url=list_releases_endpoint
|
|
||||||
)
|
|
||||||
await self.__validate_request(response)
|
|
||||||
latest_pre_release = next(
|
|
||||||
filter(
|
|
||||||
lambda release: release["prerelease"],
|
|
||||||
await response.json(loads=ujson.loads),
|
|
||||||
)
|
|
||||||
)
|
|
||||||
return await self.__assemble_release(latest_pre_release)
|
|
||||||
|
|
||||||
async def get_contributors(self, repository: GithubRepository) -> list[Contributor]:
|
|
||||||
"""Get a list of contributors for a given repository.
|
|
||||||
|
|
||||||
Args:
|
|
||||||
repository (GithubRepository): The repository for which to retrieve contributors.
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
list[Contributor]: A list of contributors for the given repository.
|
|
||||||
"""
|
|
||||||
|
|
||||||
contributors_endpoint: str = (
|
|
||||||
f"{self.repositories_rest_endpoint}/{repository.owner}/{repository.name}/contributors"
|
|
||||||
)
|
|
||||||
response: ClientResponse = await http_get(
|
|
||||||
headers=self.headers, url=contributors_endpoint
|
|
||||||
)
|
|
||||||
await self.__validate_request(response)
|
|
||||||
contributors: list[Contributor] = await asyncio.gather(
|
|
||||||
*map(self.__assemble_contributor, await response.json(loads=ujson.loads))
|
|
||||||
)
|
|
||||||
|
|
||||||
return contributors
|
|
||||||
|
|
||||||
async def get_patches(
|
|
||||||
self, repository: GithubRepository, tag_name: str = "latest", dev: bool = False
|
|
||||||
) -> list[dict]:
|
|
||||||
"""Get a dictionary of patch URLs for a given repository.
|
|
||||||
|
|
||||||
Args:
|
|
||||||
repository (GithubRepository): The repository for which to retrieve patches.
|
|
||||||
tag_name (str): The name of the release tag.
|
|
||||||
dev (bool): If we should get the latest pre-release instead.
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
list[dict]: A JSON object containing the patches.
|
|
||||||
"""
|
|
||||||
|
|
||||||
async def __fetch_download_url(_release: Release) -> str:
|
|
||||||
asset = get_in(["assets"], _release)
|
|
||||||
patch_asset = next(
|
|
||||||
filter(lambda x: eq(get_in(["name"], x), "patches.json"), asset), None
|
|
||||||
)
|
|
||||||
return get_in(["browser_download_url"], patch_asset)
|
|
||||||
|
|
||||||
match tag_name:
|
|
||||||
case "latest":
|
|
||||||
match dev:
|
|
||||||
case True:
|
|
||||||
release = await self.get_latest_pre_release(repository)
|
|
||||||
case _:
|
|
||||||
release = await self.get_latest_release(repository)
|
|
||||||
case _:
|
|
||||||
release = await self.get_release_by_tag_name(
|
|
||||||
repository=repository, tag_name=tag_name
|
|
||||||
)
|
|
||||||
|
|
||||||
response: ClientResponse = await http_get(
|
|
||||||
headers=self.headers,
|
|
||||||
url=await __fetch_download_url(_release=release),
|
|
||||||
)
|
|
||||||
await self.__validate_request(response)
|
|
||||||
return ujson.loads(await response.read())
|
|
||||||
|
|
||||||
async def get_team_members(self, repository: GithubRepository) -> list[Contributor]:
|
|
||||||
"""Get the list of team members from the owner organization of a given repository.
|
|
||||||
|
|
||||||
Args:
|
|
||||||
repository (GithubRepository): The repository for which to retrieve team members in the owner organization.
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
list[Contributor]: A list of members in the owner organization.
|
|
||||||
"""
|
|
||||||
team_members_endpoint: str = f"{self.base_url}/orgs/{repository.owner}/members"
|
|
||||||
user_info_endpoint: str = f"{self.base_url}/users/"
|
|
||||||
response: ClientResponse = await http_get(
|
|
||||||
headers=self.headers, url=team_members_endpoint
|
|
||||||
)
|
|
||||||
await self.__validate_request(response)
|
|
||||||
logins: list[str] = list(pluck("login", await response.json()))
|
|
||||||
_http_get = partial(http_get, headers=self.headers)
|
|
||||||
user_data_response: list[dict] = await asyncio.gather(
|
|
||||||
*map(
|
|
||||||
lambda login: _http_get(url=f"{user_info_endpoint}{login}"),
|
|
||||||
logins,
|
|
||||||
)
|
|
||||||
)
|
|
||||||
user_data = await asyncio.gather(
|
|
||||||
*map(
|
|
||||||
lambda _response: _response.json(loads=ujson.loads),
|
|
||||||
user_data_response,
|
|
||||||
)
|
|
||||||
)
|
|
||||||
team_members: list[Contributor] = await asyncio.gather(
|
|
||||||
*map(
|
|
||||||
lambda member: self.__assemble_contributor(member, team_view=True),
|
|
||||||
list(user_data),
|
|
||||||
)
|
|
||||||
)
|
|
||||||
|
|
||||||
return team_members
|
|
||||||
|
|
||||||
async def compat_get_tools(
|
|
||||||
self, repositories: list[GithubRepository], dev: bool
|
|
||||||
) -> list:
|
|
||||||
"""Get the latest releases for a set of repositories (v1 compat).
|
|
||||||
|
|
||||||
Args:
|
|
||||||
repositories (set[GithubRepository]): The repositories for which to retrieve releases.
|
|
||||||
dev: If we should get the latest pre-release instead.
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
list[dict[str, str]]: A JSON object containing the releases.
|
|
||||||
"""
|
|
||||||
|
|
||||||
def transform(data: dict, repository: GithubRepository):
|
|
||||||
"""Transforms a dictionary from the input list into a list of dictionaries with the desired structure.
|
|
||||||
|
|
||||||
Args:
|
|
||||||
data(dict): A dictionary from the input list.
|
|
||||||
repository(GithubRepository): The repository for which to retrieve releases.
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
_[list]: A list of dictionaries with the desired structure.
|
|
||||||
"""
|
|
||||||
|
|
||||||
def process_asset(asset: dict) -> dict:
|
|
||||||
"""Transforms an asset dictionary into a new dictionary with the desired structure.
|
|
||||||
|
|
||||||
Args:
|
|
||||||
asset(dict): An asset dictionary.
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
_[dict]: A new dictionary with the desired structure.
|
|
||||||
"""
|
|
||||||
return {
|
|
||||||
"repository": f"{repository.owner}/{repository.name}",
|
|
||||||
"version": data["metadata"]["tag_name"],
|
|
||||||
"timestamp": data["metadata"]["published_at"],
|
|
||||||
"name": asset["name"],
|
|
||||||
"browser_download_url": asset["browser_download_url"],
|
|
||||||
"content_type": asset["content_type"],
|
|
||||||
}
|
|
||||||
|
|
||||||
return map(process_asset, data["assets"])
|
|
||||||
|
|
||||||
results = await asyncio.gather(
|
|
||||||
*map(
|
|
||||||
lambda release: self.get_latest_release(release),
|
|
||||||
repositories,
|
|
||||||
)
|
|
||||||
)
|
|
||||||
|
|
||||||
return list(mapcat(lambda pair: transform(*pair), zip(results, repositories)))
|
|
||||||
|
|
||||||
async def compat_get_contributors(
|
|
||||||
self, repositories: list[GithubRepository]
|
|
||||||
) -> list:
|
|
||||||
"""Get the contributors for a set of repositories (v1 compat).
|
|
||||||
|
|
||||||
Args:
|
|
||||||
repositories (set[GithubRepository]): The repositories for which to retrieve contributors.
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
list[dict[str, str]]: A JSON object containing the contributors.
|
|
||||||
"""
|
|
||||||
|
|
||||||
def transform(data: dict, repository: GithubRepository) -> dict[str, Any]:
|
|
||||||
"""Transforms a dictionary from the input list into a list of dictionaries with the desired structure.
|
|
||||||
|
|
||||||
Args:
|
|
||||||
data(dict): A dictionary from the input list.
|
|
||||||
repository(GithubRepository): The repository for which to retrieve contributors.
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
_[list]: A list of dictionaries with the desired structure.
|
|
||||||
"""
|
|
||||||
return {
|
|
||||||
"name": f"{repository.owner}/{repository.name}",
|
|
||||||
"contributors": data,
|
|
||||||
}
|
|
||||||
|
|
||||||
results = await asyncio.gather(
|
|
||||||
*map(
|
|
||||||
lambda repository: self.get_contributors(repository),
|
|
||||||
repositories,
|
|
||||||
)
|
|
||||||
)
|
|
||||||
|
|
||||||
return list(map(lambda pair: transform(*pair), zip(results, repositories)))
|
|
||||||
|
|
||||||
async def generate_custom_sources(
|
|
||||||
self, repositories: list[GithubRepository], dev: bool
|
|
||||||
) -> dict[str, dict[str, str]]:
|
|
||||||
"""Generate a custom sources dictionary for a set of repositories.
|
|
||||||
|
|
||||||
Args:
|
|
||||||
repositories (list[GithubRepository]): The repositories for which to generate the sources.
|
|
||||||
dev (bool): If we should get the latest pre-release instead.
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
dict[str, dict[str, str]]: A dictionary containing the custom sources.
|
|
||||||
"""
|
|
||||||
|
|
||||||
# Helper functions
|
|
||||||
filter_by_name = curry(lambda name, item: name in item["name"])
|
|
||||||
filter_patches_jar = curry(
|
|
||||||
lambda item: "patches" in item["name"] and item["name"].endswith(".jar")
|
|
||||||
)
|
|
||||||
get_fields = curry(
|
|
||||||
lambda fields, item: {field: item[field] for field in fields}
|
|
||||||
)
|
|
||||||
rename_key = curry(
|
|
||||||
lambda old, new, d: {new if k == old else k: v for k, v in d.items()}
|
|
||||||
)
|
|
||||||
|
|
||||||
sources = await self.compat_get_tools(repositories, dev)
|
|
||||||
|
|
||||||
patches = pipe(
|
|
||||||
sources,
|
|
||||||
lambda items: next(filter(filter_patches_jar, items), None),
|
|
||||||
get_fields(["version", "browser_download_url"]),
|
|
||||||
rename_key("browser_download_url", "url"),
|
|
||||||
)
|
|
||||||
|
|
||||||
integrations = pipe(
|
|
||||||
sources,
|
|
||||||
lambda items: next(filter(filter_by_name("integrations"), items), None),
|
|
||||||
get_fields(["version", "browser_download_url"]),
|
|
||||||
rename_key("browser_download_url", "url"),
|
|
||||||
)
|
|
||||||
|
|
||||||
return {"patches": patches, "integrations": integrations}
|
|
@ -1,88 +0,0 @@
|
|||||||
"""
|
|
||||||
This module provides endpoints for compatibility with the old API.
|
|
||||||
|
|
||||||
Routes:
|
|
||||||
- GET /<repo:str>/releases: Retrieve a list of releases for a GitHub repository.
|
|
||||||
- GET /<repo:str>/releases/latest: Retrieve the latest release for a GitHub repository.
|
|
||||||
- GET /<repo:str>/releases/tag/<tag:str>: Retrieve a specific release for a GitHub repository by its tag name.
|
|
||||||
- GET /<repo:str>/contributors: Retrieve a list of contributors for a GitHub repository.
|
|
||||||
- GET /patches/<tag:str>: Retrieve a list of patches for a given release tag.
|
|
||||||
|
|
||||||
"""
|
|
||||||
|
|
||||||
import os
|
|
||||||
from sanic import Blueprint, Request
|
|
||||||
from sanic.response import JSONResponse, json
|
|
||||||
from sanic_ext import openapi
|
|
||||||
|
|
||||||
from api.backends.github import Github, GithubRepository
|
|
||||||
from api.models.github import *
|
|
||||||
from api.models.compat import ToolsResponseModel, ContributorsResponseModel
|
|
||||||
from config import compat_repositories, owner
|
|
||||||
|
|
||||||
compat: Blueprint = Blueprint(os.path.basename(__file__).strip(".py"))
|
|
||||||
|
|
||||||
github_backend: Github = Github()
|
|
||||||
|
|
||||||
|
|
||||||
@compat.get("/tools")
|
|
||||||
@openapi.definition(
|
|
||||||
summary="Get patching tools' latest version.", response=[ToolsResponseModel]
|
|
||||||
)
|
|
||||||
async def tools(request: Request) -> JSONResponse:
|
|
||||||
"""
|
|
||||||
Retrieve a list of releases for a GitHub repository.
|
|
||||||
|
|
||||||
**Args:**
|
|
||||||
- repo (str): The name of the GitHub repository to retrieve releases for.
|
|
||||||
|
|
||||||
**Query Parameters:**
|
|
||||||
- per_page (int): The number of releases to retrieve per page.
|
|
||||||
- page (int): The page number of the releases to retrieve.
|
|
||||||
|
|
||||||
**Returns:**
|
|
||||||
- JSONResponse: A Sanic JSONResponse object containing the list of releases.
|
|
||||||
|
|
||||||
**Raises:**
|
|
||||||
- HTTPException: If there is an error retrieving the releases.
|
|
||||||
"""
|
|
||||||
|
|
||||||
data: dict[str, list] = {
|
|
||||||
"tools": await github_backend.compat_get_tools(
|
|
||||||
repositories=[
|
|
||||||
GithubRepository(owner=owner, name=repo)
|
|
||||||
for repo in compat_repositories
|
|
||||||
if repo
|
|
||||||
not in ["revanced-api", "revanced-releases-api", "revanced-website"]
|
|
||||||
],
|
|
||||||
dev=True if request.args.get("dev") else False,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
return json(data, status=200)
|
|
||||||
|
|
||||||
|
|
||||||
@compat.get("/contributors")
|
|
||||||
@openapi.definition(
|
|
||||||
summary="Get organization-wise contributors.", response=[ContributorsResponseModel]
|
|
||||||
)
|
|
||||||
async def contributors(request: Request) -> JSONResponse:
|
|
||||||
"""
|
|
||||||
Retrieve a list of releases for a GitHub repository.
|
|
||||||
|
|
||||||
**Returns:**
|
|
||||||
- JSONResponse: A Sanic JSONResponse object containing the list of releases.
|
|
||||||
|
|
||||||
**Raises:**
|
|
||||||
- HTTPException: If there is an error retrieving the releases.
|
|
||||||
"""
|
|
||||||
|
|
||||||
data: dict[str, list] = {
|
|
||||||
"repositories": await github_backend.compat_get_contributors(
|
|
||||||
repositories=[
|
|
||||||
GithubRepository(owner=owner, name=repo) for repo in compat_repositories
|
|
||||||
]
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
return json(data, status=200)
|
|
@ -1,38 +0,0 @@
|
|||||||
"""
|
|
||||||
This module provides a blueprint for the donations endpoint.
|
|
||||||
|
|
||||||
Routes:
|
|
||||||
- GET /donations: Get ReVanced donation links and wallets.
|
|
||||||
"""
|
|
||||||
|
|
||||||
import os
|
|
||||||
|
|
||||||
from sanic import Blueprint, Request
|
|
||||||
from sanic.response import JSONResponse, json
|
|
||||||
from sanic_ext import openapi
|
|
||||||
|
|
||||||
from api.models.donations import DonationsResponseModel
|
|
||||||
from config import wallets, links
|
|
||||||
|
|
||||||
donations: Blueprint = Blueprint(os.path.basename(__file__).strip(".py"))
|
|
||||||
|
|
||||||
|
|
||||||
@donations.get("/donations")
|
|
||||||
@openapi.definition(
|
|
||||||
summary="Get ReVanced donation links and wallets",
|
|
||||||
response=[DonationsResponseModel],
|
|
||||||
)
|
|
||||||
async def root(request: Request) -> JSONResponse:
|
|
||||||
"""
|
|
||||||
Returns a JSONResponse with a dictionary containing ReVanced donation links and wallets.
|
|
||||||
|
|
||||||
**Returns:**
|
|
||||||
- JSONResponse: A Sanic JSONResponse instance containing a dictionary with the donation links and wallets.
|
|
||||||
"""
|
|
||||||
data: dict[str, dict] = {
|
|
||||||
"donations": {
|
|
||||||
"wallets": wallets,
|
|
||||||
"links": links,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return json(data, status=200)
|
|
220
api/github.py
220
api/github.py
@ -1,220 +0,0 @@
|
|||||||
"""
|
|
||||||
This module provides endpoints for interacting with the GitHub API.
|
|
||||||
|
|
||||||
Routes:
|
|
||||||
- GET /<repo:str>/releases: Retrieve a list of releases for a GitHub repository.
|
|
||||||
- GET /<repo:str>/releases/latest: Retrieve the latest release for a GitHub repository.
|
|
||||||
- GET /<repo:str>/releases/tag/<tag:str>: Retrieve a specific release for a GitHub repository by its tag name.
|
|
||||||
- GET /<repo:str>/contributors: Retrieve a list of contributors for a GitHub repository.
|
|
||||||
- GET /patches/<tag:str>: Retrieve a list of patches for a given release tag.
|
|
||||||
|
|
||||||
"""
|
|
||||||
|
|
||||||
import os
|
|
||||||
from sanic import Blueprint, Request
|
|
||||||
from sanic.response import JSONResponse, json
|
|
||||||
from sanic_ext import openapi
|
|
||||||
|
|
||||||
from api.backends.entities import Release, Contributor
|
|
||||||
from api.backends.github import Github, GithubRepository
|
|
||||||
from api.models.github import *
|
|
||||||
from config import owner, default_repository
|
|
||||||
|
|
||||||
github: Blueprint = Blueprint(os.path.basename(__file__).strip(".py"))
|
|
||||||
|
|
||||||
github_backend: Github = Github()
|
|
||||||
|
|
||||||
|
|
||||||
@github.get("/<repo:str>/releases")
|
|
||||||
@openapi.definition(
|
|
||||||
summary="Get releases for a repository", response=[ReleaseListResponseModel]
|
|
||||||
)
|
|
||||||
async def list_releases(request: Request, repo: str) -> JSONResponse:
|
|
||||||
"""
|
|
||||||
Retrieve a list of releases for a GitHub repository.
|
|
||||||
|
|
||||||
**Args:**
|
|
||||||
- repo (str): The name of the GitHub repository to retrieve releases for.
|
|
||||||
|
|
||||||
**Query Parameters:**
|
|
||||||
- per_page (int): The number of releases to retrieve per page.
|
|
||||||
- page (int): The page number of the releases to retrieve.
|
|
||||||
|
|
||||||
**Returns:**
|
|
||||||
- JSONResponse: A Sanic JSONResponse object containing the list of releases.
|
|
||||||
|
|
||||||
**Raises:**
|
|
||||||
- HTTPException: If there is an error retrieving the releases.
|
|
||||||
"""
|
|
||||||
|
|
||||||
per_page = int(request.args.get("per_page")) if request.args.get("per_page") else 30
|
|
||||||
page = int(request.args.get("page")) if request.args.get("page") else 1
|
|
||||||
|
|
||||||
data: dict[str, list[Release]] = {
|
|
||||||
"releases": await github_backend.list_releases(
|
|
||||||
repository=GithubRepository(owner=owner, name=repo),
|
|
||||||
per_page=per_page,
|
|
||||||
page=page,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
return json(data, status=200)
|
|
||||||
|
|
||||||
|
|
||||||
@github.get("/<repo:str>/releases/latest")
|
|
||||||
@openapi.definition(
|
|
||||||
summary="Get the latest release for a repository",
|
|
||||||
response=SingleReleaseResponseModel,
|
|
||||||
)
|
|
||||||
async def latest_release(request: Request, repo: str) -> JSONResponse:
|
|
||||||
"""
|
|
||||||
Retrieve the latest release for a GitHub repository.
|
|
||||||
|
|
||||||
**Args:**
|
|
||||||
- repo (str): The name of the GitHub repository to retrieve the release for.
|
|
||||||
|
|
||||||
**Query Parameters:**
|
|
||||||
- dev (bool): Whether or not to retrieve the latest development release.
|
|
||||||
|
|
||||||
**Returns:**
|
|
||||||
- JSONResponse: A Sanic JSONResponse object containing the release.
|
|
||||||
|
|
||||||
**Raises:**
|
|
||||||
- HTTPException: If there is an error retrieving the releases.
|
|
||||||
"""
|
|
||||||
|
|
||||||
data: dict[str, Release]
|
|
||||||
|
|
||||||
match request.args.get("dev"):
|
|
||||||
case "true":
|
|
||||||
data = {
|
|
||||||
"release": await github_backend.get_latest_pre_release(
|
|
||||||
repository=GithubRepository(owner=owner, name=repo)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
case _:
|
|
||||||
data = {
|
|
||||||
"release": await github_backend.get_latest_release(
|
|
||||||
repository=GithubRepository(owner=owner, name=repo)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
return json(data, status=200)
|
|
||||||
|
|
||||||
|
|
||||||
@github.get("/<repo:str>/releases/tag/<tag:str>")
|
|
||||||
@openapi.definition(
|
|
||||||
summary="Retrieve a release for a GitHub repository by its tag name.",
|
|
||||||
response=SingleReleaseResponseModel,
|
|
||||||
)
|
|
||||||
async def get_release_by_tag_name(
|
|
||||||
request: Request, repo: str, tag: str
|
|
||||||
) -> JSONResponse:
|
|
||||||
"""
|
|
||||||
Retrieve a release for a GitHub repository by its tag name.
|
|
||||||
|
|
||||||
**Args:**
|
|
||||||
- repo (str): The name of the GitHub repository to retrieve the release for.
|
|
||||||
- tag (str): The tag for the release to be retrieved.
|
|
||||||
|
|
||||||
**Returns:**
|
|
||||||
- JSONResponse: A Sanic JSONResponse object containing the release.
|
|
||||||
|
|
||||||
**Raises:**
|
|
||||||
- HTTPException: If there is an error retrieving the releases.
|
|
||||||
"""
|
|
||||||
|
|
||||||
data: dict[str, Release] = {
|
|
||||||
"release": await github_backend.get_release_by_tag_name(
|
|
||||||
repository=GithubRepository(owner=owner, name=repo), tag_name=tag
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
return json(data, status=200)
|
|
||||||
|
|
||||||
|
|
||||||
@github.get("/<repo:str>/contributors")
|
|
||||||
@openapi.definition(
|
|
||||||
summary="Retrieve a list of contributors for a repository.",
|
|
||||||
response=ContributorsModel,
|
|
||||||
)
|
|
||||||
async def get_contributors(request: Request, repo: str) -> JSONResponse:
|
|
||||||
"""
|
|
||||||
Retrieve a list of contributors for a repository.
|
|
||||||
|
|
||||||
**Args:**
|
|
||||||
- repo (str): The name of the GitHub repository to retrieve the contributors for.
|
|
||||||
|
|
||||||
**Returns:**
|
|
||||||
- JSONResponse: A Sanic JSONResponse object containing the list of contributors.
|
|
||||||
|
|
||||||
**Raises:**
|
|
||||||
- HTTPException: If there is an error retrieving the contributors.
|
|
||||||
"""
|
|
||||||
|
|
||||||
data: dict[str, list[Contributor]] = {
|
|
||||||
"contributors": await github_backend.get_contributors(
|
|
||||||
repository=GithubRepository(owner=owner, name=repo)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
return json(data, status=200)
|
|
||||||
|
|
||||||
|
|
||||||
@github.get("/patches/<tag:str>")
|
|
||||||
@openapi.definition(
|
|
||||||
summary="Retrieve a list of patches for a release.", response=PatchesModel
|
|
||||||
)
|
|
||||||
async def get_patches(request: Request, tag: str) -> JSONResponse:
|
|
||||||
"""
|
|
||||||
Retrieve a list of patches for a release.
|
|
||||||
|
|
||||||
**Args:**
|
|
||||||
- tag (str): The tag for the patches to be retrieved.
|
|
||||||
|
|
||||||
**Query Parameters:**
|
|
||||||
- dev (bool): Whether or not to retrieve the latest development release.
|
|
||||||
|
|
||||||
**Returns:**
|
|
||||||
- JSONResponse: A Sanic JSONResponse object containing the list of patches.
|
|
||||||
|
|
||||||
**Raises:**
|
|
||||||
- HTTPException: If there is an error retrieving the patches.
|
|
||||||
"""
|
|
||||||
|
|
||||||
repo: str = "revanced-patches"
|
|
||||||
|
|
||||||
dev: bool = bool(request.args.get("dev"))
|
|
||||||
|
|
||||||
data: dict[str, list[dict]] = {
|
|
||||||
"patches": await github_backend.get_patches(
|
|
||||||
repository=GithubRepository(owner=owner, name=repo), tag_name=tag, dev=dev
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
return json(data, status=200)
|
|
||||||
|
|
||||||
|
|
||||||
@github.get("/team/members")
|
|
||||||
@openapi.definition(
|
|
||||||
summary="Retrieve a list of team members for the Revanced organization.",
|
|
||||||
response=TeamMembersModel,
|
|
||||||
)
|
|
||||||
async def get_team_members(request: Request) -> JSONResponse:
|
|
||||||
"""
|
|
||||||
Retrieve a list of team members for the Revanced organization.
|
|
||||||
|
|
||||||
**Returns:**
|
|
||||||
- JSONResponse: A Sanic JSONResponse object containing the list of team members.
|
|
||||||
|
|
||||||
**Raises:**
|
|
||||||
- HTTPException: If there is an error retrieving the team members.
|
|
||||||
"""
|
|
||||||
|
|
||||||
data: dict[str, list[Contributor]] = {
|
|
||||||
"members": await github_backend.get_team_members(
|
|
||||||
repository=GithubRepository(owner=owner, name=default_repository)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
return json(data, status=200)
|
|
32
api/info.py
32
api/info.py
@ -1,32 +0,0 @@
|
|||||||
"""
|
|
||||||
This module provides a blueprint for the info endpoint.
|
|
||||||
|
|
||||||
Routes:
|
|
||||||
- GET /info: Get info about the owner of the API.
|
|
||||||
"""
|
|
||||||
|
|
||||||
import os
|
|
||||||
from sanic import Blueprint, Request
|
|
||||||
from sanic.response import JSONResponse, json
|
|
||||||
from sanic_ext import openapi
|
|
||||||
|
|
||||||
from api.models.info import InfoResponseModel
|
|
||||||
from config import default_info
|
|
||||||
|
|
||||||
info: Blueprint = Blueprint(os.path.basename(__file__).strip(".py"))
|
|
||||||
|
|
||||||
|
|
||||||
@info.get("/info")
|
|
||||||
@openapi.definition(
|
|
||||||
summary="Information about the API",
|
|
||||||
response=[InfoResponseModel],
|
|
||||||
)
|
|
||||||
async def root(request: Request) -> JSONResponse:
|
|
||||||
"""
|
|
||||||
Returns a JSONResponse with a dictionary containing info about the owner of the API.
|
|
||||||
|
|
||||||
**Returns:**
|
|
||||||
- JSONResponse: A Sanic JSONResponse instance containing a dictionary with the info about the owner of the API.
|
|
||||||
"""
|
|
||||||
data: dict[str, dict] = {"info": default_info}
|
|
||||||
return json(data, status=200)
|
|
52
api/login.py
52
api/login.py
@ -1,52 +0,0 @@
|
|||||||
"""
|
|
||||||
This module provides a blueprint for the login endpoint.
|
|
||||||
|
|
||||||
Routes:
|
|
||||||
- POST /login: Login to the API
|
|
||||||
"""
|
|
||||||
|
|
||||||
import os
|
|
||||||
from sanic import Blueprint, Request
|
|
||||||
from sanic.response import JSONResponse, json
|
|
||||||
from sanic_ext import openapi
|
|
||||||
from sanic_beskar.exceptions import AuthenticationError
|
|
||||||
|
|
||||||
from api.utils.auth import beskar
|
|
||||||
from api.utils.limiter import limiter
|
|
||||||
|
|
||||||
login: Blueprint = Blueprint(os.path.basename(__file__).strip(".py"))
|
|
||||||
|
|
||||||
|
|
||||||
@login.post("/login")
|
|
||||||
@openapi.definition(
|
|
||||||
summary="Login to the API",
|
|
||||||
)
|
|
||||||
@limiter.limit("3 per hour")
|
|
||||||
async def login_user(request: Request) -> JSONResponse:
|
|
||||||
"""
|
|
||||||
Login to the API.
|
|
||||||
|
|
||||||
**Args:**
|
|
||||||
- username (str): The username of the user to login.
|
|
||||||
- password (str): The password of the user to login.
|
|
||||||
|
|
||||||
**Returns:**
|
|
||||||
- JSONResponse: A Sanic JSONResponse object containing the access token.
|
|
||||||
"""
|
|
||||||
|
|
||||||
req = request.json
|
|
||||||
username = req.get("username", None)
|
|
||||||
password = req.get("password", None)
|
|
||||||
if not username or not password:
|
|
||||||
return json({"error": "Missing username or password"}, status=400)
|
|
||||||
|
|
||||||
try:
|
|
||||||
user = await beskar.authenticate(username, password)
|
|
||||||
except AuthenticationError:
|
|
||||||
return json({"error": "Invalid username or password"}, status=403)
|
|
||||||
|
|
||||||
if not user:
|
|
||||||
return json({"error": "Invalid username or password"}, status=403)
|
|
||||||
|
|
||||||
ret = {"access_token": await beskar.encode_token(user)}
|
|
||||||
return json(ret, status=200)
|
|
@ -1,61 +0,0 @@
|
|||||||
"""
|
|
||||||
This module provides ReVanced Manager specific endpoints.
|
|
||||||
|
|
||||||
Routes:
|
|
||||||
- GET /manager/bootstrap: Get a list of the main ReVanced tools.
|
|
||||||
- GET /manager/sources: Get a list of ReVanced sources.
|
|
||||||
"""
|
|
||||||
|
|
||||||
import os
|
|
||||||
from sanic import Blueprint, Request
|
|
||||||
from sanic.response import JSONResponse, json
|
|
||||||
from sanic_ext import openapi
|
|
||||||
|
|
||||||
from api.backends.github import GithubRepository, Github
|
|
||||||
|
|
||||||
from api.models.manager import BootsrapResponseModel, CustomSourcesResponseModel
|
|
||||||
from config import compat_repositories, owner
|
|
||||||
|
|
||||||
manager: Blueprint = Blueprint(os.path.basename(__file__).strip(".py"))
|
|
||||||
|
|
||||||
github_backend: Github = Github()
|
|
||||||
|
|
||||||
|
|
||||||
@manager.get("/manager/bootstrap")
|
|
||||||
@openapi.definition(
|
|
||||||
summary="Get a list of the main ReVanced tools",
|
|
||||||
response=[BootsrapResponseModel],
|
|
||||||
)
|
|
||||||
async def bootstrap(request: Request) -> JSONResponse:
|
|
||||||
"""
|
|
||||||
Returns a JSONResponse with a list of the main ReVanced tools.
|
|
||||||
|
|
||||||
**Returns:**
|
|
||||||
- JSONResponse: A Sanic JSONResponse instance containing a list with the tool names.
|
|
||||||
"""
|
|
||||||
data: dict[str, dict] = {"tools": compat_repositories}
|
|
||||||
return json(data, status=200)
|
|
||||||
|
|
||||||
|
|
||||||
@manager.get("/manager/custom-source")
|
|
||||||
@openapi.definition(
|
|
||||||
summary="Get a list of ReVanced sources",
|
|
||||||
response=[CustomSourcesResponseModel],
|
|
||||||
)
|
|
||||||
async def custom_sources(request: Request) -> JSONResponse:
|
|
||||||
"""
|
|
||||||
Returns a JSONResponse with a list of the main ReVanced sources.
|
|
||||||
|
|
||||||
**Returns:**
|
|
||||||
- JSONResponse: A Sanic JSONResponse instance containing a list with the source names.
|
|
||||||
"""
|
|
||||||
data = await github_backend.generate_custom_sources(
|
|
||||||
repositories=[
|
|
||||||
GithubRepository(owner=owner, name=repo)
|
|
||||||
for repo in compat_repositories
|
|
||||||
if "patches" in repo or "integrations" in repo
|
|
||||||
],
|
|
||||||
dev=True if request.args.get("dev") else False,
|
|
||||||
)
|
|
||||||
|
|
||||||
return json(data, status=200)
|
|
@ -1,40 +0,0 @@
|
|||||||
from data.models import AnnouncementDbModel
|
|
||||||
|
|
||||||
|
|
||||||
class ContentFields(dict):
|
|
||||||
message: str | None
|
|
||||||
attachment_urls: list[str] | None
|
|
||||||
|
|
||||||
|
|
||||||
class AnnouncementResponseModel(dict):
|
|
||||||
id: int
|
|
||||||
author: str | None
|
|
||||||
title: str
|
|
||||||
content: ContentFields | None
|
|
||||||
channel: str
|
|
||||||
created_at: str
|
|
||||||
level: int | None
|
|
||||||
|
|
||||||
@staticmethod
|
|
||||||
def to_response(announcement: AnnouncementDbModel):
|
|
||||||
response = AnnouncementResponseModel(
|
|
||||||
id=announcement.id,
|
|
||||||
author=announcement.author,
|
|
||||||
title=announcement.title,
|
|
||||||
content=(
|
|
||||||
ContentFields(
|
|
||||||
message=announcement.message,
|
|
||||||
attachment_urls=[
|
|
||||||
attachment.attachment_url
|
|
||||||
for attachment in announcement.attachments
|
|
||||||
],
|
|
||||||
)
|
|
||||||
if announcement.message or announcement.attachments
|
|
||||||
else None
|
|
||||||
),
|
|
||||||
channel=announcement.channel,
|
|
||||||
created_at=str(announcement.created_at),
|
|
||||||
level=announcement.level,
|
|
||||||
)
|
|
||||||
|
|
||||||
return response
|
|
@ -1,49 +0,0 @@
|
|||||||
from pydantic import BaseModel
|
|
||||||
from api.models.github import ContributorsFields
|
|
||||||
|
|
||||||
|
|
||||||
class ToolsResponseFields(BaseModel):
|
|
||||||
"""Implements the fields for the /tools endpoint.
|
|
||||||
|
|
||||||
Args:
|
|
||||||
BaseModel (pydantic.BaseModel): BaseModel from pydantic
|
|
||||||
"""
|
|
||||||
|
|
||||||
repository: str
|
|
||||||
version: str
|
|
||||||
timestamp: str
|
|
||||||
name: str
|
|
||||||
size: str | None = None
|
|
||||||
browser_download_url: str
|
|
||||||
content_type: str
|
|
||||||
|
|
||||||
|
|
||||||
class ToolsResponseModel(BaseModel):
|
|
||||||
"""Implements the JSON response model for the /tools endpoint.
|
|
||||||
|
|
||||||
Args:
|
|
||||||
BaseModel (pydantic.BaseModel): BaseModel from pydantic
|
|
||||||
"""
|
|
||||||
|
|
||||||
tools: list[ToolsResponseFields]
|
|
||||||
|
|
||||||
|
|
||||||
class ContributorsResponseFields(BaseModel):
|
|
||||||
"""Implements the fields for the /contributors endpoint.
|
|
||||||
|
|
||||||
Args:
|
|
||||||
BaseModel (pydantic.BaseModel): BaseModel from pydantic
|
|
||||||
"""
|
|
||||||
|
|
||||||
name: str
|
|
||||||
contributors: list[ContributorsFields]
|
|
||||||
|
|
||||||
|
|
||||||
class ContributorsResponseModel(BaseModel):
|
|
||||||
"""Implements the JSON response model for the /contributors endpoint.
|
|
||||||
|
|
||||||
Args:
|
|
||||||
BaseModel (pydantic.BaseModel): BaseModel from pydantic
|
|
||||||
"""
|
|
||||||
|
|
||||||
repositories: list[ContributorsResponseFields]
|
|
@ -1,39 +0,0 @@
|
|||||||
from pydantic import BaseModel
|
|
||||||
|
|
||||||
|
|
||||||
class WalletFields(BaseModel):
|
|
||||||
"""
|
|
||||||
Implements the fields for a crypto wallet.
|
|
||||||
"""
|
|
||||||
|
|
||||||
network: str
|
|
||||||
currency_code: str
|
|
||||||
address: str
|
|
||||||
preferred: bool
|
|
||||||
|
|
||||||
|
|
||||||
class LinkFields(BaseModel):
|
|
||||||
"""
|
|
||||||
Implements the fields for a donation link.
|
|
||||||
"""
|
|
||||||
|
|
||||||
name: str
|
|
||||||
url: str
|
|
||||||
preferred: bool
|
|
||||||
|
|
||||||
|
|
||||||
class DonationFields(BaseModel):
|
|
||||||
"""
|
|
||||||
A Pydantic BaseModel that represents all the donation links and wallets.
|
|
||||||
"""
|
|
||||||
|
|
||||||
wallets: list[WalletFields]
|
|
||||||
links: list[LinkFields]
|
|
||||||
|
|
||||||
|
|
||||||
class DonationsResponseModel(BaseModel):
|
|
||||||
"""
|
|
||||||
A Pydantic BaseModel that represents a dictionary of donation links.
|
|
||||||
"""
|
|
||||||
|
|
||||||
donations: DonationFields
|
|
@ -1,129 +0,0 @@
|
|||||||
from typing import Any, Optional
|
|
||||||
from pydantic import BaseModel
|
|
||||||
|
|
||||||
|
|
||||||
class MetadataFields(BaseModel):
|
|
||||||
"""
|
|
||||||
Metadata fields for a GitHub release.
|
|
||||||
"""
|
|
||||||
|
|
||||||
tag_name: str
|
|
||||||
name: str
|
|
||||||
draft: bool
|
|
||||||
prerelease: bool
|
|
||||||
created_at: str
|
|
||||||
published_at: str
|
|
||||||
body: str
|
|
||||||
|
|
||||||
|
|
||||||
class AssetFields(BaseModel):
|
|
||||||
"""
|
|
||||||
Asset fields for a GitHub release.
|
|
||||||
"""
|
|
||||||
|
|
||||||
name: str
|
|
||||||
content_type: str
|
|
||||||
download_count: int
|
|
||||||
browser_download_url: str
|
|
||||||
|
|
||||||
|
|
||||||
class ReleaseResponseModel(BaseModel):
|
|
||||||
"""
|
|
||||||
Response model for a GitHub release.
|
|
||||||
"""
|
|
||||||
|
|
||||||
metadata: MetadataFields
|
|
||||||
assets: list[AssetFields]
|
|
||||||
|
|
||||||
|
|
||||||
class SingleReleaseResponseModel(BaseModel):
|
|
||||||
"""
|
|
||||||
Response model for a GitHub release.
|
|
||||||
"""
|
|
||||||
|
|
||||||
release: ReleaseResponseModel
|
|
||||||
|
|
||||||
|
|
||||||
class ReleaseListResponseModel(BaseModel):
|
|
||||||
"""
|
|
||||||
Response model for a list of GitHub releases.
|
|
||||||
"""
|
|
||||||
|
|
||||||
releases: list[ReleaseResponseModel]
|
|
||||||
|
|
||||||
|
|
||||||
class CompatiblePackagesResponseFields(BaseModel):
|
|
||||||
"""
|
|
||||||
Implements the fields for compatible packages in the PatchesResponseFields class.
|
|
||||||
"""
|
|
||||||
|
|
||||||
name: str
|
|
||||||
versions: list[str] | None
|
|
||||||
|
|
||||||
|
|
||||||
class PatchesOptionsResponseFields(BaseModel):
|
|
||||||
key: str
|
|
||||||
title: str
|
|
||||||
description: str
|
|
||||||
required: bool
|
|
||||||
choices: list[Any] | None
|
|
||||||
|
|
||||||
|
|
||||||
class PatchesResponseFields(BaseModel):
|
|
||||||
"""
|
|
||||||
Implements the fields for the /patches endpoint.
|
|
||||||
"""
|
|
||||||
|
|
||||||
name: str
|
|
||||||
description: str
|
|
||||||
version: str
|
|
||||||
excluded: bool
|
|
||||||
dependencies: list[str] | None
|
|
||||||
options: list[PatchesOptionsResponseFields] | None
|
|
||||||
compatiblePackages: list[CompatiblePackagesResponseFields]
|
|
||||||
|
|
||||||
|
|
||||||
class PatchesModel(BaseModel):
|
|
||||||
"""
|
|
||||||
Response model for a list of GitHub releases.
|
|
||||||
"""
|
|
||||||
|
|
||||||
patches: list[PatchesResponseFields]
|
|
||||||
|
|
||||||
|
|
||||||
class ContributorsFields(BaseModel):
|
|
||||||
"""
|
|
||||||
Implements the fields for a contributor.
|
|
||||||
"""
|
|
||||||
|
|
||||||
login: str
|
|
||||||
avatar_url: str
|
|
||||||
html_url: str
|
|
||||||
contributions: Optional[int]
|
|
||||||
|
|
||||||
|
|
||||||
class ContributorsModel(BaseModel):
|
|
||||||
"""
|
|
||||||
Response model for a list of contributors.
|
|
||||||
"""
|
|
||||||
|
|
||||||
contributors: list[ContributorsFields]
|
|
||||||
|
|
||||||
|
|
||||||
class TeamMemberFields(BaseModel):
|
|
||||||
"""
|
|
||||||
Implements the fields for a team member.
|
|
||||||
"""
|
|
||||||
|
|
||||||
login: str
|
|
||||||
avatar_url: str
|
|
||||||
html_url: str
|
|
||||||
bio: Optional[str]
|
|
||||||
|
|
||||||
|
|
||||||
class TeamMembersModel(BaseModel):
|
|
||||||
"""
|
|
||||||
Responde model for a list of team members.
|
|
||||||
"""
|
|
||||||
|
|
||||||
members: list[TeamMemberFields]
|
|
@ -1,40 +0,0 @@
|
|||||||
from api.models.donations import DonationFields
|
|
||||||
from api.models.socials import SocialFields
|
|
||||||
from pydantic import BaseModel
|
|
||||||
|
|
||||||
|
|
||||||
class ContactFields(BaseModel):
|
|
||||||
"""
|
|
||||||
Implements the fields for the API owner contact info.
|
|
||||||
"""
|
|
||||||
|
|
||||||
email: str
|
|
||||||
|
|
||||||
|
|
||||||
class BrandingFields(BaseModel):
|
|
||||||
"""
|
|
||||||
Implements the fields for the API owner branding info.
|
|
||||||
"""
|
|
||||||
|
|
||||||
logo: str
|
|
||||||
|
|
||||||
|
|
||||||
class InfoFields(BaseModel):
|
|
||||||
"""
|
|
||||||
Implements the fields for the API owner info.
|
|
||||||
"""
|
|
||||||
|
|
||||||
name: str
|
|
||||||
about: str
|
|
||||||
branding: BrandingFields
|
|
||||||
contact: ContactFields
|
|
||||||
socials: list[SocialFields]
|
|
||||||
donations: DonationFields
|
|
||||||
|
|
||||||
|
|
||||||
class InfoResponseModel(BaseModel):
|
|
||||||
"""
|
|
||||||
A Pydantic BaseModel that represents a dictionary of info.
|
|
||||||
"""
|
|
||||||
|
|
||||||
info: InfoFields
|
|
@ -1,32 +0,0 @@
|
|||||||
from pydantic import BaseModel
|
|
||||||
|
|
||||||
|
|
||||||
class BootsrapResponseModel(BaseModel):
|
|
||||||
"""
|
|
||||||
A Pydantic BaseModel that represents a list of available tools.
|
|
||||||
"""
|
|
||||||
|
|
||||||
tools: list[str]
|
|
||||||
"""
|
|
||||||
A list of available tools.
|
|
||||||
"""
|
|
||||||
|
|
||||||
|
|
||||||
class CustomSourcesFields(BaseModel):
|
|
||||||
"""
|
|
||||||
Implements the fields for a source.
|
|
||||||
"""
|
|
||||||
|
|
||||||
url: str
|
|
||||||
preferred: bool
|
|
||||||
|
|
||||||
|
|
||||||
class CustomSourcesResponseModel(BaseModel):
|
|
||||||
"""
|
|
||||||
A Pydantic BaseModel that represents a list of available sources.
|
|
||||||
"""
|
|
||||||
|
|
||||||
_: dict[str, CustomSourcesFields]
|
|
||||||
"""
|
|
||||||
A list of available sources.
|
|
||||||
"""
|
|
@ -1,23 +0,0 @@
|
|||||||
from pydantic import BaseModel
|
|
||||||
|
|
||||||
|
|
||||||
class SocialFields(BaseModel):
|
|
||||||
"""
|
|
||||||
Implements the fields for a social network link.
|
|
||||||
"""
|
|
||||||
|
|
||||||
name: str
|
|
||||||
url: str
|
|
||||||
preferred: bool
|
|
||||||
|
|
||||||
|
|
||||||
class SocialsResponseModel(BaseModel):
|
|
||||||
"""
|
|
||||||
A Pydantic BaseModel that represents a dictionary of social links.
|
|
||||||
"""
|
|
||||||
|
|
||||||
socials: list[SocialFields]
|
|
||||||
"""
|
|
||||||
A dictionary where the keys are the names of the social networks, and
|
|
||||||
the values are the links to the profiles or pages.
|
|
||||||
"""
|
|
24
api/ping.py
24
api/ping.py
@ -1,24 +0,0 @@
|
|||||||
"""
|
|
||||||
This module provides endpoints for pinging the API.
|
|
||||||
|
|
||||||
Routes:
|
|
||||||
- GET /ping: Ping the API.
|
|
||||||
"""
|
|
||||||
|
|
||||||
import os
|
|
||||||
from sanic import Blueprint, HTTPResponse, Request, response
|
|
||||||
from sanic_ext import openapi
|
|
||||||
|
|
||||||
ping: Blueprint = Blueprint(os.path.basename(__file__).rstrip(".py"))
|
|
||||||
|
|
||||||
|
|
||||||
@ping.get("/ping")
|
|
||||||
@openapi.summary("Ping the API")
|
|
||||||
async def root(request: Request) -> HTTPResponse:
|
|
||||||
"""
|
|
||||||
Endpoint for pinging the API.
|
|
||||||
|
|
||||||
**Returns:**
|
|
||||||
- Empty response with status code 204.
|
|
||||||
"""
|
|
||||||
return response.empty(status=204)
|
|
@ -1,11 +0,0 @@
|
|||||||
import os
|
|
||||||
from sanic import Blueprint
|
|
||||||
from sanic.response import text
|
|
||||||
|
|
||||||
|
|
||||||
robots: Blueprint = Blueprint(os.path.basename(__file__).strip(".py"))
|
|
||||||
|
|
||||||
|
|
||||||
@robots.get("/robots.txt")
|
|
||||||
async def robots_txt(request):
|
|
||||||
return text("User-agent: *\nDisallow: /", content_type="text/plain")
|
|
@ -1,32 +0,0 @@
|
|||||||
"""
|
|
||||||
This module provides a blueprint for the socials endpoint.
|
|
||||||
|
|
||||||
Routes:
|
|
||||||
- GET /socials: Get ReVanced socials.
|
|
||||||
"""
|
|
||||||
|
|
||||||
import os
|
|
||||||
from sanic import Blueprint, Request
|
|
||||||
from sanic.response import JSONResponse, json
|
|
||||||
from sanic_ext import openapi
|
|
||||||
|
|
||||||
from api.models.socials import SocialsResponseModel
|
|
||||||
from config import social_links
|
|
||||||
|
|
||||||
socials: Blueprint = Blueprint(os.path.basename(__file__).strip(".py"))
|
|
||||||
|
|
||||||
|
|
||||||
@socials.get("/socials")
|
|
||||||
@openapi.definition(
|
|
||||||
summary="Get ReVanced socials",
|
|
||||||
response=[SocialsResponseModel],
|
|
||||||
)
|
|
||||||
async def root(request: Request) -> JSONResponse:
|
|
||||||
"""
|
|
||||||
Returns a JSONResponse with a dictionary containing ReVanced social links.
|
|
||||||
|
|
||||||
**Returns:**
|
|
||||||
- JSONResponse: A Sanic JSONResponse instance containing a dictionary with the social links.
|
|
||||||
"""
|
|
||||||
data: dict[str, dict] = {"socials": social_links}
|
|
||||||
return json(data, status=200)
|
|
@ -1,40 +0,0 @@
|
|||||||
import os
|
|
||||||
import secrets
|
|
||||||
import string
|
|
||||||
from data.database import Session
|
|
||||||
|
|
||||||
from sanic_beskar import Beskar
|
|
||||||
|
|
||||||
from data.models import UserDbModel
|
|
||||||
|
|
||||||
beskar = Beskar()
|
|
||||||
|
|
||||||
|
|
||||||
def configure_auth(app):
|
|
||||||
app.config.SECRET_KEY = os.environ.get("SECRET_KEY").join(
|
|
||||||
secrets.choice(string.ascii_letters) for i in range(15)
|
|
||||||
)
|
|
||||||
app.config["TOKEN_ACCESS_LIFESPAN"] = {"hours": 24}
|
|
||||||
app.config["TOKEN_REFRESH_LIFESPAN"] = {"days": 30}
|
|
||||||
beskar.init_app(app, UserDbModel)
|
|
||||||
|
|
||||||
_init_default_user()
|
|
||||||
|
|
||||||
|
|
||||||
def _init_default_user():
|
|
||||||
username = os.environ.get("USERNAME")
|
|
||||||
password = os.environ.get("PASSWORD")
|
|
||||||
|
|
||||||
if not username or not password:
|
|
||||||
raise Exception("Missing USERNAME or PASSWORD environment variables")
|
|
||||||
|
|
||||||
session = Session()
|
|
||||||
|
|
||||||
existing_user = session.query(UserDbModel).filter_by(username=username).first()
|
|
||||||
if not existing_user:
|
|
||||||
session.add(
|
|
||||||
UserDbModel(username=username, password=beskar.hash_password(password))
|
|
||||||
)
|
|
||||||
session.commit()
|
|
||||||
|
|
||||||
session.close()
|
|
@ -1,26 +0,0 @@
|
|||||||
from typing import Optional
|
|
||||||
|
|
||||||
import ujson
|
|
||||||
from aiohttp import ClientSession
|
|
||||||
|
|
||||||
_client: Optional[ClientSession] = None
|
|
||||||
|
|
||||||
|
|
||||||
async def http_get(headers, url):
|
|
||||||
"""
|
|
||||||
Performs a GET HTTP request to a given URL with the provided headers.
|
|
||||||
|
|
||||||
Args:
|
|
||||||
headers (dict): A dictionary containing headers to be included in the HTTP request.
|
|
||||||
url (str): The URL to which the HTTP request will be made.
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
The HTTP response returned by the server.
|
|
||||||
"""
|
|
||||||
global _client
|
|
||||||
if _client is None:
|
|
||||||
_client = ClientSession(json_serialize=ujson.dumps)
|
|
||||||
return await _client.get(url, headers=headers)
|
|
||||||
else:
|
|
||||||
assert isinstance(_client, ClientSession)
|
|
||||||
return await _client.get(url, headers=headers)
|
|
@ -1,7 +0,0 @@
|
|||||||
from sanic_limiter import Limiter, get_remote_address
|
|
||||||
|
|
||||||
limiter = Limiter(key_func=get_remote_address)
|
|
||||||
|
|
||||||
|
|
||||||
def configure_limiter(app):
|
|
||||||
limiter.init_app(app)
|
|
@ -1,8 +0,0 @@
|
|||||||
from cytoolz import keyfilter
|
|
||||||
from config import api_versions
|
|
||||||
|
|
||||||
|
|
||||||
def get_version(value: str) -> str:
|
|
||||||
result = keyfilter(lambda key: value in api_versions[key], api_versions)
|
|
||||||
|
|
||||||
return list(result.keys())[0] if result else "v0"
|
|
87
app.py
87
app.py
@ -1,87 +0,0 @@
|
|||||||
# app.py
|
|
||||||
|
|
||||||
import os
|
|
||||||
from typing import Any
|
|
||||||
|
|
||||||
from sanic import HTTPResponse, Sanic
|
|
||||||
import sanic.response
|
|
||||||
from sanic_ext import Config
|
|
||||||
|
|
||||||
from api import api
|
|
||||||
from config import openapi_title, openapi_version, openapi_description, hostnames
|
|
||||||
|
|
||||||
from api.utils.limiter import configure_limiter
|
|
||||||
from api.utils.auth import configure_auth
|
|
||||||
|
|
||||||
import sentry_sdk
|
|
||||||
|
|
||||||
if os.environ.get("SENTRY_DSN"):
|
|
||||||
sentry_sdk.init(
|
|
||||||
dsn=os.environ["SENTRY_DSN"],
|
|
||||||
enable_tracing=True,
|
|
||||||
traces_sample_rate=1.0,
|
|
||||||
)
|
|
||||||
else:
|
|
||||||
print("WARNING: Sentry DSN not set, not enabling Sentry")
|
|
||||||
|
|
||||||
REDIRECTS = {
|
|
||||||
"/": "/docs/swagger",
|
|
||||||
}
|
|
||||||
|
|
||||||
app = Sanic("revanced-api")
|
|
||||||
app.extend(config=Config(oas_ignore_head=False))
|
|
||||||
app.ext.openapi.describe(
|
|
||||||
title=openapi_title,
|
|
||||||
version=openapi_version,
|
|
||||||
description=openapi_description,
|
|
||||||
)
|
|
||||||
app.config.CORS_ALWAYS_SEND = True
|
|
||||||
app.config.CORS_AUTOMATIC_OPTIONS = True
|
|
||||||
app.config.CORS_VARY_HEADER = True
|
|
||||||
app.config.CORS_METHODS = ["GET", "HEAD", "OPTIONS"]
|
|
||||||
app.config.CORS_SUPPORTS_CREDENTIALS = True
|
|
||||||
app.config.CORS_SEND_WILDCARD = True
|
|
||||||
app.config.CORS_ORIGINS = "*"
|
|
||||||
|
|
||||||
# sanic-beskar
|
|
||||||
configure_auth(app)
|
|
||||||
|
|
||||||
# sanic-limiter
|
|
||||||
configure_limiter(app)
|
|
||||||
|
|
||||||
app.blueprint(api)
|
|
||||||
|
|
||||||
# https://sanic.dev/en/guide/how-to/static-redirects.html
|
|
||||||
|
|
||||||
|
|
||||||
def get_static_function(value) -> Any:
|
|
||||||
return lambda *_, **__: value
|
|
||||||
|
|
||||||
|
|
||||||
for src, dest in REDIRECTS.items():
|
|
||||||
app.route(src)(get_static_function(sanic.response.redirect(dest)))
|
|
||||||
|
|
||||||
|
|
||||||
@app.on_request
|
|
||||||
async def domain_check(request) -> HTTPResponse:
|
|
||||||
if request.host not in hostnames:
|
|
||||||
return sanic.response.redirect(f"https://api.revanced.app/{request.path}")
|
|
||||||
|
|
||||||
|
|
||||||
@app.on_response
|
|
||||||
async def add_cache_control(_, response):
|
|
||||||
response.headers["Cache-Control"] = "public, max-age=300"
|
|
||||||
|
|
||||||
|
|
||||||
@app.on_response
|
|
||||||
async def add_csp(_, response):
|
|
||||||
response.headers["Content-Security-Policy"] = (
|
|
||||||
"default-src * 'unsafe-inline' 'unsafe-eval' data: blob:;"
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
app.static(
|
|
||||||
"/favicon.ico",
|
|
||||||
"static/img/favicon.ico",
|
|
||||||
name="favicon",
|
|
||||||
)
|
|
File diff suppressed because one or more lines are too long
After Width: | Height: | Size: 11 KiB |
File diff suppressed because one or more lines are too long
After Width: | Height: | Size: 11 KiB |
1
assets/revanced-logo/revanced-logo.svg
Normal file
1
assets/revanced-logo/revanced-logo.svg
Normal file
@ -0,0 +1 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8" standalone="no"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg width="100%" height="100%" viewBox="0 0 800 800" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" xml:space="preserve" xmlns:serif="http://www.serif.com/" style="fill-rule:evenodd;clip-rule:evenodd;stroke-linejoin:round;stroke-miterlimit:2;"><g id="Logo"><g id="Ring"><circle id="Ring-Background" serif:id="Ring Background" cx="400" cy="400" r="400" style="fill:#1b1b1b;"/><path id="Ring1" serif:id="Ring" d="M400,0c220.766,0 400,179.234 400,400c-0,220.766 -179.234,400 -400,400c-220.766,-0 -400,-179.234 -400,-400c0,-220.766 179.234,-400 400,-400Zm-0,36c200.897,-0 364,163.103 364,364c0,200.897 -163.103,364 -364,364c-200.897,0 -364,-163.103 -364,-364c-0,-200.897 163.103,-364 364,-364Z" style="fill:url(#_Linear1);"/></g><g id="Shape"><path id="V-Shape" serif:id="V Shape" d="M538.74,269.872c1.481,-3.382 1.157,-7.283 -0.863,-10.373c-2.021,-3.091 -5.464,-4.954 -9.156,-4.954c-5.148,0 -10.435,0 -14.165,0c-3.1,0 -5.907,1.834 -7.153,4.672c-12.468,28.396 -78.273,178.273 -100.25,228.328c-1.246,2.838 -4.053,4.671 -7.154,4.671c-3.1,0 -5.907,-1.833 -7.153,-4.671c-21.977,-50.055 -87.782,-199.932 -100.25,-228.328c-1.246,-2.838 -4.053,-4.672 -7.153,-4.672c-3.73,0 -9.017,0 -14.164,0c-3.693,0 -7.135,1.863 -9.156,4.954c-2.02,3.09 -2.344,6.991 -0.863,10.373c23.557,53.766 101.872,232.519 117.871,269.034c1.743,3.979 5.674,6.549 10.018,6.549c6.293,-0 15.408,-0 21.701,-0c4.344,-0 8.275,-2.57 10.018,-6.549c15.999,-36.515 94.315,-215.268 117.872,-269.034Z" style="fill:#fff;"/><path id="Diamond" d="M408.119,395.312c-1.675,2.901 -4.77,4.688 -8.119,4.688c-3.349,-0 -6.444,-1.787 -8.119,-4.688c-16.997,-29.44 -56.156,-97.264 -73.153,-126.704c-1.675,-2.901 -1.675,-6.474 0,-9.375c1.675,-2.901 4.77,-4.688 8.119,-4.688c33.995,0 112.311,0 146.306,0c3.349,0 6.444,1.787 8.119,4.688c1.675,2.901 1.675,6.474 -0,9.375c-16.997,29.44 -56.156,97.264 -73.153,126.704Z" style="fill:url(#_Linear2);"/></g></g><defs><linearGradient id="_Linear1" x1="0" y1="0" x2="1" y2="0" gradientUnits="userSpaceOnUse" gradientTransform="matrix(4.89859e-14,800,-800,4.89859e-14,400.001,3.31681e-10)"><stop offset="0" style="stop-color:#f04e98;stop-opacity:1"/><stop offset="0.5" style="stop-color:#5f65d4;stop-opacity:1"/><stop offset="1" style="stop-color:#4e98f0;stop-opacity:1"/></linearGradient><linearGradient id="_Linear2" x1="0" y1="0" x2="1" y2="0" gradientUnits="userSpaceOnUse" gradientTransform="matrix(1.77155e-14,289.317,-282.535,1.73003e-14,400,254.545)"><stop offset="0" style="stop-color:#f04e98;stop-opacity:1"/><stop offset="0.5" style="stop-color:#5f65d4;stop-opacity:1"/><stop offset="1" style="stop-color:#4e98f0;stop-opacity:1"/></linearGradient></defs></svg>
|
After Width: | Height: | Size: 2.8 KiB |
120
build.gradle.kts
Normal file
120
build.gradle.kts
Normal file
@ -0,0 +1,120 @@
|
|||||||
|
import org.jetbrains.kotlin.gradle.dsl.JvmTarget
|
||||||
|
|
||||||
|
plugins {
|
||||||
|
alias(libs.plugins.kotlin)
|
||||||
|
alias(libs.plugins.ktor)
|
||||||
|
alias(libs.plugins.serilization)
|
||||||
|
`maven-publish`
|
||||||
|
signing
|
||||||
|
}
|
||||||
|
|
||||||
|
group = "app.revanced"
|
||||||
|
|
||||||
|
tasks {
|
||||||
|
processResources {
|
||||||
|
expand("projectVersion" to project.version)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Used by gradle-semantic-release-plugin.
|
||||||
|
// Tracking: https://github.com/KengoTODA/gradle-semantic-release-plugin/issues/435.
|
||||||
|
publish {
|
||||||
|
dependsOn(shadowJar)
|
||||||
|
}
|
||||||
|
|
||||||
|
shadowJar {
|
||||||
|
// Needed for Jetty to work.
|
||||||
|
mergeServiceFiles()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
application {
|
||||||
|
mainClass.set("app.revanced.api.command.MainCommandKt")
|
||||||
|
}
|
||||||
|
|
||||||
|
ktor {
|
||||||
|
fatJar {
|
||||||
|
archiveFileName.set("${project.name}-${project.version}.jar")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
java {
|
||||||
|
sourceCompatibility = JavaVersion.VERSION_21
|
||||||
|
targetCompatibility = JavaVersion.VERSION_21
|
||||||
|
}
|
||||||
|
|
||||||
|
kotlin {
|
||||||
|
compilerOptions {
|
||||||
|
jvmTarget = JvmTarget.JVM_21
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
repositories {
|
||||||
|
mavenCentral()
|
||||||
|
google()
|
||||||
|
mavenLocal()
|
||||||
|
maven {
|
||||||
|
// A repository must be specified for some reason. "registry" is a dummy.
|
||||||
|
url = uri("https://maven.pkg.github.com/revanced/registry")
|
||||||
|
credentials {
|
||||||
|
username = project.findProperty("gpr.user") as String? ?: System.getenv("GITHUB_ACTOR")
|
||||||
|
password = project.findProperty("gpr.key") as String? ?: System.getenv("GITHUB_TOKEN")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
dependencies {
|
||||||
|
implementation(libs.ktor.client.core)
|
||||||
|
implementation(libs.ktor.client.cio)
|
||||||
|
implementation(libs.ktor.client.okhttp)
|
||||||
|
implementation(libs.ktor.client.auth)
|
||||||
|
implementation(libs.ktor.client.resources)
|
||||||
|
implementation(libs.ktor.client.content.negotiation)
|
||||||
|
implementation(libs.ktor.server.core)
|
||||||
|
implementation(libs.ktor.server.content.negotiation)
|
||||||
|
implementation(libs.ktor.server.auth)
|
||||||
|
implementation(libs.ktor.server.auth.jwt)
|
||||||
|
implementation(libs.ktor.server.cors)
|
||||||
|
implementation(libs.ktor.server.caching.headers)
|
||||||
|
implementation(libs.ktor.server.rate.limit)
|
||||||
|
implementation(libs.ktor.server.host.common)
|
||||||
|
implementation(libs.ktor.server.jetty)
|
||||||
|
implementation(libs.ktor.server.call.logging)
|
||||||
|
implementation(libs.ktor.serialization.kotlinx.json)
|
||||||
|
implementation(libs.koin.ktor)
|
||||||
|
implementation(libs.kompendium.core)
|
||||||
|
implementation(libs.h2)
|
||||||
|
implementation(libs.logback.classic)
|
||||||
|
implementation(libs.exposed.core)
|
||||||
|
implementation(libs.exposed.jdbc)
|
||||||
|
implementation(libs.exposed.dao)
|
||||||
|
implementation(libs.exposed.kotlin.datetime)
|
||||||
|
implementation(libs.dotenv.kotlin)
|
||||||
|
implementation(libs.ktoml.core)
|
||||||
|
implementation(libs.ktoml.file)
|
||||||
|
implementation(libs.picocli)
|
||||||
|
implementation(libs.kotlinx.datetime)
|
||||||
|
implementation(libs.revanced.patcher)
|
||||||
|
implementation(libs.revanced.library)
|
||||||
|
implementation(libs.caffeine)
|
||||||
|
implementation(libs.bouncy.castle.provider)
|
||||||
|
implementation(libs.bouncy.castle.pgp)
|
||||||
|
}
|
||||||
|
|
||||||
|
// The maven-publish plugin is necessary to make signing work.
|
||||||
|
publishing {
|
||||||
|
repositories {
|
||||||
|
mavenLocal()
|
||||||
|
}
|
||||||
|
|
||||||
|
publications {
|
||||||
|
create<MavenPublication>("revanced-api-publication") {
|
||||||
|
from(components["java"])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
signing {
|
||||||
|
useGpgCmd()
|
||||||
|
|
||||||
|
sign(publishing.publications["revanced-api-publication"])
|
||||||
|
}
|
152
config.py
152
config.py
@ -1,152 +0,0 @@
|
|||||||
# API Configuration
|
|
||||||
|
|
||||||
from typing import Any
|
|
||||||
|
|
||||||
|
|
||||||
backend: str = "github"
|
|
||||||
redis: dict[str, str | int] = {"host": "localhost", "port": 6379}
|
|
||||||
hostnames: list[str] = [
|
|
||||||
"api.revanced.app",
|
|
||||||
"deimos.revanced.app",
|
|
||||||
"localhost:8000",
|
|
||||||
"127.0.0.1:8000",
|
|
||||||
]
|
|
||||||
|
|
||||||
# GitHub Backend Configuration
|
|
||||||
|
|
||||||
owner: str = "revanced"
|
|
||||||
default_repository: str = ".github"
|
|
||||||
|
|
||||||
# API Versioning
|
|
||||||
|
|
||||||
api_versions: dict[str, list[str]] = {
|
|
||||||
"old": ["compat"],
|
|
||||||
"v2": [
|
|
||||||
"announcements",
|
|
||||||
"donations",
|
|
||||||
"github",
|
|
||||||
"info",
|
|
||||||
"login",
|
|
||||||
"ping",
|
|
||||||
"socials",
|
|
||||||
"manager",
|
|
||||||
],
|
|
||||||
}
|
|
||||||
|
|
||||||
api_version: str = "v2"
|
|
||||||
openapi_version: str = "2.0.0"
|
|
||||||
openapi_title: str = "ReVanced API"
|
|
||||||
openapi_description: str = """
|
|
||||||
## The official JSON API for ReVanced Releases 🚀
|
|
||||||
|
|
||||||
### Links
|
|
||||||
|
|
||||||
- [Changelogs](https://github.com/revanced/)
|
|
||||||
- [Official links to ReVanced](https://revanced.app)
|
|
||||||
|
|
||||||
### Important Information
|
|
||||||
|
|
||||||
* Rate Limiting - 60 requests per minute
|
|
||||||
* Cache - 5 minutes
|
|
||||||
|
|
||||||
### Additional Notes
|
|
||||||
|
|
||||||
1. Breaking changes are to be expected
|
|
||||||
2. Client side caching is advised to avoid unnecessary requests
|
|
||||||
3. Abuse of the API will result in IP blocks
|
|
||||||
"""
|
|
||||||
|
|
||||||
# Testing Configuration
|
|
||||||
|
|
||||||
github_testing_repository: str = "revanced-patches"
|
|
||||||
github_testing_tag: str = "v2.173.0"
|
|
||||||
apkdl_testing_package: str = "com.google.android.youtube"
|
|
||||||
|
|
||||||
# Old API Configuration
|
|
||||||
|
|
||||||
compat_api_version: str = "v1"
|
|
||||||
compat_repositories: list = [
|
|
||||||
"revanced-patcher",
|
|
||||||
"revanced-patches",
|
|
||||||
"revanced-integrations",
|
|
||||||
"revanced-manager",
|
|
||||||
"revanced-cli",
|
|
||||||
"revanced-website",
|
|
||||||
"revanced-api",
|
|
||||||
"revanced-releases-api",
|
|
||||||
]
|
|
||||||
|
|
||||||
# Social Links
|
|
||||||
|
|
||||||
social_links: list[dict[str, str | bool]] = [
|
|
||||||
{"name": "Website", "url": "https://revanced.app", "preferred": True},
|
|
||||||
{"name": "GitHub", "url": "https://github.com/revanced", "preferred": False},
|
|
||||||
{"name": "Twitter", "url": "https://twitter.com/revancedapp", "preferred": False},
|
|
||||||
{"name": "Discord", "url": "https://revanced.app/discord", "preferred": True},
|
|
||||||
{
|
|
||||||
"name": "Reddit",
|
|
||||||
"url": "https://www.reddit.com/r/revancedapp",
|
|
||||||
"preferred": False,
|
|
||||||
},
|
|
||||||
{"name": "Telegram", "url": "https://t.me/app_revanced", "preferred": False},
|
|
||||||
{"name": "YouTube", "url": "https://www.youtube.com/@ReVanced", "preferred": False},
|
|
||||||
]
|
|
||||||
|
|
||||||
# Donation info
|
|
||||||
|
|
||||||
wallets: list[dict[str, str | bool]] = [
|
|
||||||
{
|
|
||||||
"network": "Bitcoin",
|
|
||||||
"currency_code": "BTC",
|
|
||||||
"address": "bc1q4x8j6mt27y5gv0q625t8wkr87ruy8fprpy4v3f",
|
|
||||||
"preferred": False,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"network": "Dogecoin",
|
|
||||||
"currency_code": "DOGE",
|
|
||||||
"address": "D8GH73rNjudgi6bS2krrXWEsU9KShedLXp",
|
|
||||||
"preferred": True,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"network": "Ethereum",
|
|
||||||
"currency_code": "ETH",
|
|
||||||
"address": "0x7ab4091e00363654bf84B34151225742cd92FCE5",
|
|
||||||
"preferred": False,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"network": "Litecoin",
|
|
||||||
"currency_code": "LTC",
|
|
||||||
"address": "LbJi8EuoDcwaZvykcKmcrM74jpjde23qJ2",
|
|
||||||
"preferred": False,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"network": "Monero",
|
|
||||||
"currency_code": "XMR",
|
|
||||||
"address": "46YwWDbZD6jVptuk5mLHsuAmh1BnUMSjSNYacozQQEraWSQ93nb2yYVRHoMR6PmFYWEHsLHg9tr1cH5M8Rtn7YaaGQPCjSh",
|
|
||||||
"preferred": False,
|
|
||||||
},
|
|
||||||
]
|
|
||||||
|
|
||||||
links: list[dict[str, str | bool]] = [
|
|
||||||
{
|
|
||||||
"name": "Open Collective",
|
|
||||||
"url": "https://opencollective.com/revanced",
|
|
||||||
"preferred": True,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "GitHub Sponsors",
|
|
||||||
"url": "https://github.com/sponsors/ReVanced",
|
|
||||||
"preferred": False,
|
|
||||||
},
|
|
||||||
]
|
|
||||||
|
|
||||||
default_info: dict[str, Any] = {
|
|
||||||
"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.",
|
|
||||||
"branding": {
|
|
||||||
"logo": "https://raw.githubusercontent.com/ReVanced/revanced-branding/main/assets/revanced-logo/revanced-logo.svg"
|
|
||||||
},
|
|
||||||
"contact": {"email": "contact@revanced.app"},
|
|
||||||
"socials": social_links,
|
|
||||||
"donations": {"wallets": wallets, "links": links},
|
|
||||||
}
|
|
14
configuration.example.toml
Normal file
14
configuration.example.toml
Normal file
@ -0,0 +1,14 @@
|
|||||||
|
organization = "revanced"
|
||||||
|
patches = { repository = "revanced-patches", asset-regex = "jar$", signature-asset-regex = "asc$", public-key-file = "patches-public-key.asc" }
|
||||||
|
integrations = { repository = "revanced-integrations", asset-regex = "apk$", signature-asset-regex = "asc$", public-key-file = "integrations-public-key.asc" }
|
||||||
|
contributors-repositories = [
|
||||||
|
"revanced-patcher",
|
||||||
|
"revanced-patches",
|
||||||
|
"revanced-integrations",
|
||||||
|
"revanced-website",
|
||||||
|
"revanced-cli",
|
||||||
|
"revanced-manager",
|
||||||
|
]
|
||||||
|
api-version = 1
|
||||||
|
cors = { host = "*.revanced.app", sub-domains = [] }
|
||||||
|
endpoint = "https://api.revanced.app"
|
14
configuration.toml
Normal file
14
configuration.toml
Normal file
@ -0,0 +1,14 @@
|
|||||||
|
organization = "revanced"
|
||||||
|
patches = { repository = "revanced-patches", asset-regex = "jar$", signature-asset-regex = "asc$", public-key-file = "key.asc" }
|
||||||
|
integrations = { repository = "revanced-integrations", asset-regex = "apk$", signature-asset-regex = "asc$", public-key-file = "key.asc" }
|
||||||
|
contributors-repositories = [
|
||||||
|
"revanced-patcher",
|
||||||
|
"revanced-patches",
|
||||||
|
"revanced-integrations",
|
||||||
|
"revanced-website",
|
||||||
|
"revanced-cli",
|
||||||
|
"revanced-manager",
|
||||||
|
]
|
||||||
|
api-version = 1
|
||||||
|
cors = { host = "*.127.0.0.1:8888", sub-domains = [] }
|
||||||
|
endpoint = "http://127.0.0.1:8888/"
|
@ -1,6 +0,0 @@
|
|||||||
from sqlalchemy import create_engine
|
|
||||||
from sqlalchemy.orm import sessionmaker
|
|
||||||
|
|
||||||
engine = create_engine("sqlite:///persistence/database.db", pool_size=20)
|
|
||||||
|
|
||||||
Session = sessionmaker(bind=engine)
|
|
@ -1,77 +0,0 @@
|
|||||||
from sqlalchemy import Column, Integer, String, DateTime
|
|
||||||
from sqlalchemy.ext.declarative import declarative_base
|
|
||||||
from sqlalchemy.orm import relationship
|
|
||||||
from sqlalchemy import ForeignKey
|
|
||||||
|
|
||||||
from data.database import Session, engine
|
|
||||||
|
|
||||||
Base = declarative_base()
|
|
||||||
|
|
||||||
|
|
||||||
class AnnouncementDbModel(Base):
|
|
||||||
__tablename__ = "announcements"
|
|
||||||
|
|
||||||
id = Column(Integer, primary_key=True, autoincrement=True)
|
|
||||||
author = Column(String, nullable=True)
|
|
||||||
title = Column(String, nullable=False)
|
|
||||||
message = Column(String, nullable=True)
|
|
||||||
attachments = relationship("AttachmentDbModel", back_populates="announcements")
|
|
||||||
channel = Column(String, nullable=True)
|
|
||||||
created_at = Column(DateTime, nullable=False)
|
|
||||||
level = Column(Integer, nullable=True)
|
|
||||||
|
|
||||||
|
|
||||||
class AttachmentDbModel(Base):
|
|
||||||
__tablename__ = "attachments"
|
|
||||||
|
|
||||||
id = Column(Integer, primary_key=True, autoincrement=True)
|
|
||||||
announcement_id = Column(Integer, ForeignKey("announcements.id"))
|
|
||||||
attachment_url = Column(String, nullable=False)
|
|
||||||
|
|
||||||
announcements = relationship("AnnouncementDbModel", back_populates="attachments")
|
|
||||||
|
|
||||||
|
|
||||||
class UserDbModel(Base):
|
|
||||||
__tablename__ = "users"
|
|
||||||
|
|
||||||
id = Column(Integer, primary_key=True, autoincrement=True)
|
|
||||||
username = Column(String, nullable=False)
|
|
||||||
password = Column(String, nullable=False)
|
|
||||||
|
|
||||||
# Required by sanic-beskar
|
|
||||||
@property
|
|
||||||
def rolenames(self):
|
|
||||||
return []
|
|
||||||
|
|
||||||
@classmethod
|
|
||||||
async def lookup(cls, username=None):
|
|
||||||
try:
|
|
||||||
session = Session()
|
|
||||||
|
|
||||||
user = session.query(UserDbModel).filter_by(username=username).first()
|
|
||||||
|
|
||||||
session.close()
|
|
||||||
|
|
||||||
return user
|
|
||||||
except:
|
|
||||||
return None
|
|
||||||
|
|
||||||
@classmethod
|
|
||||||
async def identify(cls, id):
|
|
||||||
try:
|
|
||||||
session = Session()
|
|
||||||
|
|
||||||
user = session.query(UserDbModel).filter_by(id=id).first()
|
|
||||||
|
|
||||||
session.close()
|
|
||||||
|
|
||||||
return user
|
|
||||||
except:
|
|
||||||
return None
|
|
||||||
|
|
||||||
@property
|
|
||||||
def identity(self):
|
|
||||||
return self.id
|
|
||||||
|
|
||||||
|
|
||||||
Base.metadata.create_all(engine)
|
|
15
docker-compose.example.yml
Normal file
15
docker-compose.example.yml
Normal file
@ -0,0 +1,15 @@
|
|||||||
|
services:
|
||||||
|
revanced-api:
|
||||||
|
container_name: revanced-api
|
||||||
|
image: ghcr.io/revanced/revanced-api:latest
|
||||||
|
volumes:
|
||||||
|
- /data/revanced-api/persistence:/app/persistence
|
||||||
|
- /data/revanced-api/.env:/app/.env
|
||||||
|
- /data/revanced-api/configuration.toml:/app/configuration.toml
|
||||||
|
- /data/revanced-api/patches-public-key.asc:/app/patches-public-key.asc
|
||||||
|
- /data/revanced-api/integrations-public-key.asc:/app/integrations-public-key.asc
|
||||||
|
environment:
|
||||||
|
- COMMAND=start
|
||||||
|
ports:
|
||||||
|
- 8888:8888
|
||||||
|
restart: unless-stopped
|
@ -1,16 +0,0 @@
|
|||||||
version: "3.8"
|
|
||||||
|
|
||||||
services:
|
|
||||||
revanced-api:
|
|
||||||
container_name: revanced-api
|
|
||||||
image: ghcr.io/revanced/revanced-api:latest
|
|
||||||
volumes:
|
|
||||||
- /data/revanced-api:/usr/src/app/persistence
|
|
||||||
environment:
|
|
||||||
- GITHUB_TOKEN=YOUR_GITHUB_TOKEN
|
|
||||||
- SECRET_KEY=YOUR_SECRET_KEY
|
|
||||||
- USERNAME=YOUR_USERNAME
|
|
||||||
- PASSWORD=YOUR_PASSWORD
|
|
||||||
ports:
|
|
||||||
- 127.0.0.1:7934:8000
|
|
||||||
restart: unless-stopped
|
|
3
gradle.properties
Normal file
3
gradle.properties
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
org.gradle.parallel = true
|
||||||
|
org.gradle.caching = true
|
||||||
|
kotlin.code.style = official
|
58
gradle/libs.versions.toml
Normal file
58
gradle/libs.versions.toml
Normal file
@ -0,0 +1,58 @@
|
|||||||
|
[versions]
|
||||||
|
kompendium-core = "3.14.4"
|
||||||
|
kotlin = "2.0.0"
|
||||||
|
logback = "1.5.6"
|
||||||
|
exposed = "0.52.0"
|
||||||
|
h2 = "2.2.224"
|
||||||
|
koin = "3.5.3"
|
||||||
|
dotenv = "6.4.1"
|
||||||
|
ktor = "2.3.7"
|
||||||
|
ktoml = "0.5.2"
|
||||||
|
picocli = "4.7.6"
|
||||||
|
datetime = "0.6.0"
|
||||||
|
revanced-patcher = "19.3.1"
|
||||||
|
revanced-library = "2.3.0"
|
||||||
|
caffeine = "3.1.8"
|
||||||
|
bouncy-castle = "1.78.1"
|
||||||
|
|
||||||
|
[libraries]
|
||||||
|
kompendium-core = { module = "io.bkbn:kompendium-core", version.ref = "kompendium-core" }
|
||||||
|
ktor-client-core = { module = "io.ktor:ktor-client-core" }
|
||||||
|
ktor-client-cio = { module = "io.ktor:ktor-client-cio" }
|
||||||
|
ktor-client-okhttp = { module = "io.ktor:ktor-client-okhttp" }
|
||||||
|
ktor-client-resources = { module = "io.ktor:ktor-client-resources" }
|
||||||
|
ktor-client-auth = { module = "io.ktor:ktor-client-auth" }
|
||||||
|
ktor-client-content-negotiation = { module = "io.ktor:ktor-client-content-negotiation" }
|
||||||
|
ktor-server-core = { module = "io.ktor:ktor-server-core" }
|
||||||
|
ktor-server-content-negotiation = { module = "io.ktor:ktor-server-content-negotiation" }
|
||||||
|
ktor-server-auth = { module = "io.ktor:ktor-server-auth" }
|
||||||
|
ktor-server-auth-jwt = { module = "io.ktor:ktor-server-auth-jwt" }
|
||||||
|
ktor-server-cors = { module = "io.ktor:ktor-server-cors" }
|
||||||
|
ktor-server-caching-headers = { module = "io.ktor:ktor-server-caching-headers" }
|
||||||
|
ktor-server-rate-limit = { module = "io.ktor:ktor-server-rate-limit" }
|
||||||
|
ktor-server-host-common = { module = "io.ktor:ktor-server-host-common" }
|
||||||
|
ktor-server-jetty = { module = "io.ktor:ktor-server-jetty" }
|
||||||
|
ktor-server-call-logging = { module = "io.ktor:ktor-server-call-logging" }
|
||||||
|
ktor-serialization-kotlinx-json = { module = "io.ktor:ktor-serialization-kotlinx-json" }
|
||||||
|
koin-ktor = { module = "io.insert-koin:koin-ktor", version.ref = "koin" }
|
||||||
|
h2 = { module = "com.h2database:h2", version.ref = "h2" }
|
||||||
|
logback-classic = { module = "ch.qos.logback:logback-classic", version.ref = "logback" }
|
||||||
|
exposed-core = { module = "org.jetbrains.exposed:exposed-core", version.ref = "exposed" }
|
||||||
|
exposed-jdbc = { module = "org.jetbrains.exposed:exposed-jdbc", version.ref = "exposed" }
|
||||||
|
exposed-dao = { module = "org.jetbrains.exposed:exposed-dao", version.ref = "exposed" }
|
||||||
|
exposed-kotlin-datetime = { module = "org.jetbrains.exposed:exposed-kotlin-datetime", version.ref = "exposed" }
|
||||||
|
dotenv-kotlin = { module = "io.github.cdimascio:dotenv-kotlin", version.ref = "dotenv" }
|
||||||
|
ktoml-core = { module = "com.akuleshov7:ktoml-core", version.ref = "ktoml" }
|
||||||
|
ktoml-file = { module = "com.akuleshov7:ktoml-file", version.ref = "ktoml" }
|
||||||
|
picocli = { module = "info.picocli:picocli", version.ref = "picocli" }
|
||||||
|
kotlinx-datetime = { module = "org.jetbrains.kotlinx:kotlinx-datetime", version.ref = "datetime" }
|
||||||
|
revanced-patcher = { module = "app.revanced:revanced-patcher", version.ref = "revanced-patcher" }
|
||||||
|
revanced-library = { module = "app.revanced:revanced-library", version.ref = "revanced-library" }
|
||||||
|
caffeine = { module = "com.github.ben-manes.caffeine:caffeine", version.ref = "caffeine" }
|
||||||
|
bouncy-castle-provider = { module = "org.bouncycastle:bcprov-jdk18on", version.ref = "bouncy-castle" }
|
||||||
|
bouncy-castle-pgp = { module = "org.bouncycastle:bcpg-jdk18on", version.ref = "bouncy-castle" }
|
||||||
|
|
||||||
|
[plugins]
|
||||||
|
serilization = { id = "org.jetbrains.kotlin.plugin.serialization", version.ref = "kotlin" }
|
||||||
|
ktor = { id = "io.ktor.plugin", version.ref = "ktor" }
|
||||||
|
kotlin = { id = "org.jetbrains.kotlin.jvm", version.ref = "kotlin" }
|
BIN
gradle/wrapper/gradle-wrapper.jar
vendored
Normal file
BIN
gradle/wrapper/gradle-wrapper.jar
vendored
Normal file
Binary file not shown.
8
gradle/wrapper/gradle-wrapper.properties
vendored
Normal file
8
gradle/wrapper/gradle-wrapper.properties
vendored
Normal file
@ -0,0 +1,8 @@
|
|||||||
|
distributionBase=GRADLE_USER_HOME
|
||||||
|
distributionPath=wrapper/dists
|
||||||
|
distributionSha256Sum=a4b4158601f8636cdeeab09bd76afb640030bb5b144aafe261a5e8af027dc612
|
||||||
|
distributionUrl=https\://services.gradle.org/distributions/gradle-8.8-bin.zip
|
||||||
|
networkTimeout=10000
|
||||||
|
validateDistributionUrl=true
|
||||||
|
zipStoreBase=GRADLE_USER_HOME
|
||||||
|
zipStorePath=wrapper/dists
|
249
gradlew
vendored
Executable file
249
gradlew
vendored
Executable file
@ -0,0 +1,249 @@
|
|||||||
|
#!/bin/sh
|
||||||
|
|
||||||
|
#
|
||||||
|
# Copyright © 2015-2021 the original authors.
|
||||||
|
#
|
||||||
|
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
# you may not use this file except in compliance with the License.
|
||||||
|
# You may obtain a copy of the License at
|
||||||
|
#
|
||||||
|
# https://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
#
|
||||||
|
# Unless required by applicable law or agreed to in writing, software
|
||||||
|
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
# See the License for the specific language governing permissions and
|
||||||
|
# limitations under the License.
|
||||||
|
#
|
||||||
|
|
||||||
|
##############################################################################
|
||||||
|
#
|
||||||
|
# Gradle start up script for POSIX generated by Gradle.
|
||||||
|
#
|
||||||
|
# Important for running:
|
||||||
|
#
|
||||||
|
# (1) You need a POSIX-compliant shell to run this script. If your /bin/sh is
|
||||||
|
# noncompliant, but you have some other compliant shell such as ksh or
|
||||||
|
# bash, then to run this script, type that shell name before the whole
|
||||||
|
# command line, like:
|
||||||
|
#
|
||||||
|
# ksh Gradle
|
||||||
|
#
|
||||||
|
# Busybox and similar reduced shells will NOT work, because this script
|
||||||
|
# requires all of these POSIX shell features:
|
||||||
|
# * functions;
|
||||||
|
# * expansions «$var», «${var}», «${var:-default}», «${var+SET}»,
|
||||||
|
# «${var#prefix}», «${var%suffix}», and «$( cmd )»;
|
||||||
|
# * compound commands having a testable exit status, especially «case»;
|
||||||
|
# * various built-in commands including «command», «set», and «ulimit».
|
||||||
|
#
|
||||||
|
# Important for patching:
|
||||||
|
#
|
||||||
|
# (2) This script targets any POSIX shell, so it avoids extensions provided
|
||||||
|
# by Bash, Ksh, etc; in particular arrays are avoided.
|
||||||
|
#
|
||||||
|
# The "traditional" practice of packing multiple parameters into a
|
||||||
|
# space-separated string is a well documented source of bugs and security
|
||||||
|
# problems, so this is (mostly) avoided, by progressively accumulating
|
||||||
|
# options in "$@", and eventually passing that to Java.
|
||||||
|
#
|
||||||
|
# Where the inherited environment variables (DEFAULT_JVM_OPTS, JAVA_OPTS,
|
||||||
|
# and GRADLE_OPTS) rely on word-splitting, this is performed explicitly;
|
||||||
|
# see the in-line comments for details.
|
||||||
|
#
|
||||||
|
# There are tweaks for specific operating systems such as AIX, CygWin,
|
||||||
|
# Darwin, MinGW, and NonStop.
|
||||||
|
#
|
||||||
|
# (3) This script is generated from the Groovy template
|
||||||
|
# https://github.com/gradle/gradle/blob/HEAD/platforms/jvm/plugins-application/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt
|
||||||
|
# within the Gradle project.
|
||||||
|
#
|
||||||
|
# You can find Gradle at https://github.com/gradle/gradle/.
|
||||||
|
#
|
||||||
|
##############################################################################
|
||||||
|
|
||||||
|
# Attempt to set APP_HOME
|
||||||
|
|
||||||
|
# Resolve links: $0 may be a link
|
||||||
|
app_path=$0
|
||||||
|
|
||||||
|
# Need this for daisy-chained symlinks.
|
||||||
|
while
|
||||||
|
APP_HOME=${app_path%"${app_path##*/}"} # leaves a trailing /; empty if no leading path
|
||||||
|
[ -h "$app_path" ]
|
||||||
|
do
|
||||||
|
ls=$( ls -ld "$app_path" )
|
||||||
|
link=${ls#*' -> '}
|
||||||
|
case $link in #(
|
||||||
|
/*) app_path=$link ;; #(
|
||||||
|
*) app_path=$APP_HOME$link ;;
|
||||||
|
esac
|
||||||
|
done
|
||||||
|
|
||||||
|
# This is normally unused
|
||||||
|
# shellcheck disable=SC2034
|
||||||
|
APP_BASE_NAME=${0##*/}
|
||||||
|
# Discard cd standard output in case $CDPATH is set (https://github.com/gradle/gradle/issues/25036)
|
||||||
|
APP_HOME=$( cd "${APP_HOME:-./}" > /dev/null && pwd -P ) || exit
|
||||||
|
|
||||||
|
# Use the maximum available, or set MAX_FD != -1 to use that value.
|
||||||
|
MAX_FD=maximum
|
||||||
|
|
||||||
|
warn () {
|
||||||
|
echo "$*"
|
||||||
|
} >&2
|
||||||
|
|
||||||
|
die () {
|
||||||
|
echo
|
||||||
|
echo "$*"
|
||||||
|
echo
|
||||||
|
exit 1
|
||||||
|
} >&2
|
||||||
|
|
||||||
|
# OS specific support (must be 'true' or 'false').
|
||||||
|
cygwin=false
|
||||||
|
msys=false
|
||||||
|
darwin=false
|
||||||
|
nonstop=false
|
||||||
|
case "$( uname )" in #(
|
||||||
|
CYGWIN* ) cygwin=true ;; #(
|
||||||
|
Darwin* ) darwin=true ;; #(
|
||||||
|
MSYS* | MINGW* ) msys=true ;; #(
|
||||||
|
NONSTOP* ) nonstop=true ;;
|
||||||
|
esac
|
||||||
|
|
||||||
|
CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar
|
||||||
|
|
||||||
|
|
||||||
|
# Determine the Java command to use to start the JVM.
|
||||||
|
if [ -n "$JAVA_HOME" ] ; then
|
||||||
|
if [ -x "$JAVA_HOME/jre/sh/java" ] ; then
|
||||||
|
# IBM's JDK on AIX uses strange locations for the executables
|
||||||
|
JAVACMD=$JAVA_HOME/jre/sh/java
|
||||||
|
else
|
||||||
|
JAVACMD=$JAVA_HOME/bin/java
|
||||||
|
fi
|
||||||
|
if [ ! -x "$JAVACMD" ] ; then
|
||||||
|
die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME
|
||||||
|
|
||||||
|
Please set the JAVA_HOME variable in your environment to match the
|
||||||
|
location of your Java installation."
|
||||||
|
fi
|
||||||
|
else
|
||||||
|
JAVACMD=java
|
||||||
|
if ! command -v java >/dev/null 2>&1
|
||||||
|
then
|
||||||
|
die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
|
||||||
|
|
||||||
|
Please set the JAVA_HOME variable in your environment to match the
|
||||||
|
location of your Java installation."
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Increase the maximum file descriptors if we can.
|
||||||
|
if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then
|
||||||
|
case $MAX_FD in #(
|
||||||
|
max*)
|
||||||
|
# In POSIX sh, ulimit -H is undefined. That's why the result is checked to see if it worked.
|
||||||
|
# shellcheck disable=SC2039,SC3045
|
||||||
|
MAX_FD=$( ulimit -H -n ) ||
|
||||||
|
warn "Could not query maximum file descriptor limit"
|
||||||
|
esac
|
||||||
|
case $MAX_FD in #(
|
||||||
|
'' | soft) :;; #(
|
||||||
|
*)
|
||||||
|
# In POSIX sh, ulimit -n is undefined. That's why the result is checked to see if it worked.
|
||||||
|
# shellcheck disable=SC2039,SC3045
|
||||||
|
ulimit -n "$MAX_FD" ||
|
||||||
|
warn "Could not set maximum file descriptor limit to $MAX_FD"
|
||||||
|
esac
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Collect all arguments for the java command, stacking in reverse order:
|
||||||
|
# * args from the command line
|
||||||
|
# * the main class name
|
||||||
|
# * -classpath
|
||||||
|
# * -D...appname settings
|
||||||
|
# * --module-path (only if needed)
|
||||||
|
# * DEFAULT_JVM_OPTS, JAVA_OPTS, and GRADLE_OPTS environment variables.
|
||||||
|
|
||||||
|
# For Cygwin or MSYS, switch paths to Windows format before running java
|
||||||
|
if "$cygwin" || "$msys" ; then
|
||||||
|
APP_HOME=$( cygpath --path --mixed "$APP_HOME" )
|
||||||
|
CLASSPATH=$( cygpath --path --mixed "$CLASSPATH" )
|
||||||
|
|
||||||
|
JAVACMD=$( cygpath --unix "$JAVACMD" )
|
||||||
|
|
||||||
|
# Now convert the arguments - kludge to limit ourselves to /bin/sh
|
||||||
|
for arg do
|
||||||
|
if
|
||||||
|
case $arg in #(
|
||||||
|
-*) false ;; # don't mess with options #(
|
||||||
|
/?*) t=${arg#/} t=/${t%%/*} # looks like a POSIX filepath
|
||||||
|
[ -e "$t" ] ;; #(
|
||||||
|
*) false ;;
|
||||||
|
esac
|
||||||
|
then
|
||||||
|
arg=$( cygpath --path --ignore --mixed "$arg" )
|
||||||
|
fi
|
||||||
|
# Roll the args list around exactly as many times as the number of
|
||||||
|
# args, so each arg winds up back in the position where it started, but
|
||||||
|
# possibly modified.
|
||||||
|
#
|
||||||
|
# NB: a `for` loop captures its iteration list before it begins, so
|
||||||
|
# changing the positional parameters here affects neither the number of
|
||||||
|
# iterations, nor the values presented in `arg`.
|
||||||
|
shift # remove old arg
|
||||||
|
set -- "$@" "$arg" # push replacement arg
|
||||||
|
done
|
||||||
|
fi
|
||||||
|
|
||||||
|
|
||||||
|
# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
|
||||||
|
DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"'
|
||||||
|
|
||||||
|
# Collect all arguments for the java command:
|
||||||
|
# * DEFAULT_JVM_OPTS, JAVA_OPTS, JAVA_OPTS, and optsEnvironmentVar are not allowed to contain shell fragments,
|
||||||
|
# and any embedded shellness will be escaped.
|
||||||
|
# * For example: A user cannot expect ${Hostname} to be expanded, as it is an environment variable and will be
|
||||||
|
# treated as '${Hostname}' itself on the command line.
|
||||||
|
|
||||||
|
set -- \
|
||||||
|
"-Dorg.gradle.appname=$APP_BASE_NAME" \
|
||||||
|
-classpath "$CLASSPATH" \
|
||||||
|
org.gradle.wrapper.GradleWrapperMain \
|
||||||
|
"$@"
|
||||||
|
|
||||||
|
# Stop when "xargs" is not available.
|
||||||
|
if ! command -v xargs >/dev/null 2>&1
|
||||||
|
then
|
||||||
|
die "xargs is not available"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Use "xargs" to parse quoted args.
|
||||||
|
#
|
||||||
|
# With -n1 it outputs one arg per line, with the quotes and backslashes removed.
|
||||||
|
#
|
||||||
|
# In Bash we could simply go:
|
||||||
|
#
|
||||||
|
# readarray ARGS < <( xargs -n1 <<<"$var" ) &&
|
||||||
|
# set -- "${ARGS[@]}" "$@"
|
||||||
|
#
|
||||||
|
# but POSIX shell has neither arrays nor command substitution, so instead we
|
||||||
|
# post-process each arg (as a line of input to sed) to backslash-escape any
|
||||||
|
# character that might be a shell metacharacter, then use eval to reverse
|
||||||
|
# that process (while maintaining the separation between arguments), and wrap
|
||||||
|
# the whole thing up as a single "set" statement.
|
||||||
|
#
|
||||||
|
# This will of course break if any of these variables contains a newline or
|
||||||
|
# an unmatched quote.
|
||||||
|
#
|
||||||
|
|
||||||
|
eval "set -- $(
|
||||||
|
printf '%s\n' "$DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS" |
|
||||||
|
xargs -n1 |
|
||||||
|
sed ' s~[^-[:alnum:]+,./:=@_]~\\&~g; ' |
|
||||||
|
tr '\n' ' '
|
||||||
|
)" '"$@"'
|
||||||
|
|
||||||
|
exec "$JAVACMD" "$@"
|
92
gradlew.bat
vendored
Executable file
92
gradlew.bat
vendored
Executable file
@ -0,0 +1,92 @@
|
|||||||
|
@rem
|
||||||
|
@rem Copyright 2015 the original author or authors.
|
||||||
|
@rem
|
||||||
|
@rem Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
@rem you may not use this file except in compliance with the License.
|
||||||
|
@rem You may obtain a copy of the License at
|
||||||
|
@rem
|
||||||
|
@rem https://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
@rem
|
||||||
|
@rem Unless required by applicable law or agreed to in writing, software
|
||||||
|
@rem distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
@rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
@rem See the License for the specific language governing permissions and
|
||||||
|
@rem limitations under the License.
|
||||||
|
@rem
|
||||||
|
|
||||||
|
@if "%DEBUG%"=="" @echo off
|
||||||
|
@rem ##########################################################################
|
||||||
|
@rem
|
||||||
|
@rem Gradle startup script for Windows
|
||||||
|
@rem
|
||||||
|
@rem ##########################################################################
|
||||||
|
|
||||||
|
@rem Set local scope for the variables with windows NT shell
|
||||||
|
if "%OS%"=="Windows_NT" setlocal
|
||||||
|
|
||||||
|
set DIRNAME=%~dp0
|
||||||
|
if "%DIRNAME%"=="" set DIRNAME=.
|
||||||
|
@rem This is normally unused
|
||||||
|
set APP_BASE_NAME=%~n0
|
||||||
|
set APP_HOME=%DIRNAME%
|
||||||
|
|
||||||
|
@rem Resolve any "." and ".." in APP_HOME to make it shorter.
|
||||||
|
for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi
|
||||||
|
|
||||||
|
@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
|
||||||
|
set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m"
|
||||||
|
|
||||||
|
@rem Find java.exe
|
||||||
|
if defined JAVA_HOME goto findJavaFromJavaHome
|
||||||
|
|
||||||
|
set JAVA_EXE=java.exe
|
||||||
|
%JAVA_EXE% -version >NUL 2>&1
|
||||||
|
if %ERRORLEVEL% equ 0 goto execute
|
||||||
|
|
||||||
|
echo. 1>&2
|
||||||
|
echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 1>&2
|
||||||
|
echo. 1>&2
|
||||||
|
echo Please set the JAVA_HOME variable in your environment to match the 1>&2
|
||||||
|
echo location of your Java installation. 1>&2
|
||||||
|
|
||||||
|
goto fail
|
||||||
|
|
||||||
|
:findJavaFromJavaHome
|
||||||
|
set JAVA_HOME=%JAVA_HOME:"=%
|
||||||
|
set JAVA_EXE=%JAVA_HOME%/bin/java.exe
|
||||||
|
|
||||||
|
if exist "%JAVA_EXE%" goto execute
|
||||||
|
|
||||||
|
echo. 1>&2
|
||||||
|
echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 1>&2
|
||||||
|
echo. 1>&2
|
||||||
|
echo Please set the JAVA_HOME variable in your environment to match the 1>&2
|
||||||
|
echo location of your Java installation. 1>&2
|
||||||
|
|
||||||
|
goto fail
|
||||||
|
|
||||||
|
:execute
|
||||||
|
@rem Setup the command line
|
||||||
|
|
||||||
|
set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar
|
||||||
|
|
||||||
|
|
||||||
|
@rem Execute Gradle
|
||||||
|
"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %*
|
||||||
|
|
||||||
|
:end
|
||||||
|
@rem End local scope for the variables with windows NT shell
|
||||||
|
if %ERRORLEVEL% equ 0 goto mainEnd
|
||||||
|
|
||||||
|
:fail
|
||||||
|
rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of
|
||||||
|
rem the _cmd.exe /c_ return code!
|
||||||
|
set EXIT_CODE=%ERRORLEVEL%
|
||||||
|
if %EXIT_CODE% equ 0 set EXIT_CODE=1
|
||||||
|
if not ""=="%GRADLE_EXIT_CONSOLE%" exit %EXIT_CODE%
|
||||||
|
exit /b %EXIT_CODE%
|
||||||
|
|
||||||
|
:mainEnd
|
||||||
|
if "%OS%"=="Windows_NT" endlocal
|
||||||
|
|
||||||
|
:omega
|
30
mypy.ini
30
mypy.ini
@ -1,30 +0,0 @@
|
|||||||
[mypy]
|
|
||||||
python_version = 3.11
|
|
||||||
pretty = true
|
|
||||||
follow_imports = normal
|
|
||||||
namespace_packages = true
|
|
||||||
show_column_numbers = true
|
|
||||||
show_error_codes = true
|
|
||||||
allow_redefinition = false
|
|
||||||
check_untyped_defs = true
|
|
||||||
implicit_reexport = false
|
|
||||||
strict_optional = true
|
|
||||||
strict_equality = true
|
|
||||||
warn_no_return = true
|
|
||||||
warn_redundant_casts = true
|
|
||||||
warn_unused_configs = true
|
|
||||||
warn_unused_ignores = true
|
|
||||||
warn_unreachable = true
|
|
||||||
plugins = pydantic.mypy
|
|
||||||
|
|
||||||
[mypy-toolz.*]
|
|
||||||
ignore_missing_imports = True
|
|
||||||
|
|
||||||
[mypy-sanic_testing.*]
|
|
||||||
ignore_missing_imports = True
|
|
||||||
|
|
||||||
[mypy-fire.*]
|
|
||||||
ignore_missing_imports = True
|
|
||||||
|
|
||||||
[mypy-cytoolz.*]
|
|
||||||
ignore_missing_imports = True
|
|
6895
package-lock.json
generated
Normal file
6895
package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load Diff
10
package.json
Normal file
10
package.json
Normal file
@ -0,0 +1,10 @@
|
|||||||
|
{
|
||||||
|
"devDependencies": {
|
||||||
|
"@codedependant/semantic-release-docker": "^5.0.3",
|
||||||
|
"@saithodev/semantic-release-backmerge": "^4.0.1",
|
||||||
|
"@semantic-release/changelog": "^6.0.3",
|
||||||
|
"@semantic-release/git": "^10.0.1",
|
||||||
|
"gradle-semantic-release-plugin": "^1.9.2",
|
||||||
|
"semantic-release": "^24.0.0"
|
||||||
|
}
|
||||||
|
}
|
2599
poetry.lock
generated
2599
poetry.lock
generated
File diff suppressed because it is too large
Load Diff
@ -1,45 +0,0 @@
|
|||||||
[tool.poetry]
|
|
||||||
name = "revanced-api"
|
|
||||||
version = "0.1.0"
|
|
||||||
description = ""
|
|
||||||
authors = ["Alexandre Teles <alexandre.teles@ufba.br>"]
|
|
||||||
license = "AGPLv3"
|
|
||||||
readme = "README.md"
|
|
||||||
|
|
||||||
[tool.poetry.dependencies]
|
|
||||||
python = ">=3.11,<3.13"
|
|
||||||
aiohttp = { version = "^3.9.1", extras = ["speedups"] }
|
|
||||||
sanic = { version = "^23.12.1", extras = ["ext"] }
|
|
||||||
ujson = "^5.9.0"
|
|
||||||
pydantic = "^1.10.13"
|
|
||||||
asyncstdlib = "^3.12.0"
|
|
||||||
cytoolz = "^0.12.2"
|
|
||||||
beautifulsoup4 = "^4.12.2"
|
|
||||||
lxml = "^5.1.0"
|
|
||||||
sqlalchemy = "^2.0.25"
|
|
||||||
sanic-beskar = "^2.3.2"
|
|
||||||
bson = "^0.5.10"
|
|
||||||
fastpbkdf2 = "^0.2"
|
|
||||||
cryptography = "^41.0.7"
|
|
||||||
sanic-limiter = { git = "https://github.com/Omegastick/sanic-limiter" }
|
|
||||||
sentry-sdk = { extras = ["sanic"], version = "^1.39.2" }
|
|
||||||
|
|
||||||
[tool.poetry.dev-dependencies]
|
|
||||||
mypy = "^1.8.0"
|
|
||||||
types-ujson = "^5.9.0.0"
|
|
||||||
types-aiofiles = "^23.2.0.20240106"
|
|
||||||
types-beautifulsoup4 = "^4.12.0.20240106"
|
|
||||||
|
|
||||||
[tool.poetry.group.dev.dependencies]
|
|
||||||
setuptools = "^69.2.0"
|
|
||||||
|
|
||||||
[tool.pytest.ini_options]
|
|
||||||
asyncio_mode = "auto"
|
|
||||||
filterwarnings = [
|
|
||||||
"ignore::DeprecationWarning",
|
|
||||||
"ignore::pytest.PytestCollectionWarning",
|
|
||||||
]
|
|
||||||
|
|
||||||
[build-system]
|
|
||||||
requires = ["poetry-core"]
|
|
||||||
build-backend = "poetry.core.masonry.api"
|
|
@ -1,62 +0,0 @@
|
|||||||
aiodns==3.1.1 ; (sys_platform == "linux" or sys_platform == "darwin") and python_version >= "3.11" and python_version < "3.13"
|
|
||||||
aiofiles==23.2.1 ; python_version >= "3.11" and python_version < "3.13"
|
|
||||||
aiohttp[speedups]==3.9.1 ; python_version >= "3.11" and python_version < "3.13"
|
|
||||||
aiosignal==1.3.1 ; python_version >= "3.11" and python_version < "3.13"
|
|
||||||
argon2-cffi==23.1.0 ; python_version >= "3.11" and python_version < "3.13"
|
|
||||||
argon2-cffi-bindings==21.2.0 ; python_version >= "3.11" and python_version < "3.13"
|
|
||||||
asyncstdlib==3.12.0 ; python_version >= "3.11" and python_version < "3.13"
|
|
||||||
attrs==23.2.0 ; python_version >= "3.11" and python_version < "3.13"
|
|
||||||
beautifulsoup4==4.12.2 ; python_version >= "3.11" and python_version < "3.13"
|
|
||||||
brotli==1.1.0 ; platform_python_implementation == "CPython" and python_version >= "3.11" and python_version < "3.13"
|
|
||||||
brotlicffi==1.1.0.0 ; platform_python_implementation != "CPython" and python_version >= "3.11" and python_version < "3.13"
|
|
||||||
bson==0.5.10 ; python_version >= "3.11" and python_version < "3.13"
|
|
||||||
certifi==2023.11.17 ; python_version >= "3.11" and python_version < "3.13"
|
|
||||||
cffi==1.16.0 ; python_version >= "3.11" and python_version < "3.13"
|
|
||||||
cryptography==41.0.7 ; python_version >= "3.11" and python_version < "3.13"
|
|
||||||
cytoolz==0.12.2 ; python_version >= "3.11" and python_version < "3.13"
|
|
||||||
deprecated==1.2.14 ; python_version >= "3.11" and python_version < "3.13"
|
|
||||||
fastpbkdf2==0.2 ; python_version >= "3.11" and python_version < "3.13"
|
|
||||||
frozenlist==1.4.1 ; python_version >= "3.11" and python_version < "3.13"
|
|
||||||
greenlet==3.0.3 ; python_version >= "3.11" and python_version < "3.13" and (platform_machine == "aarch64" or platform_machine == "ppc64le" or platform_machine == "x86_64" or platform_machine == "amd64" or platform_machine == "AMD64" or platform_machine == "win32" or platform_machine == "WIN32")
|
|
||||||
html5tagger==1.3.0 ; python_version >= "3.11" and python_version < "3.13"
|
|
||||||
httptools==0.6.1 ; python_version >= "3.11" and python_version < "3.13"
|
|
||||||
idna==3.6 ; python_version >= "3.11" and python_version < "3.13"
|
|
||||||
importlib-resources==6.1.1 ; python_version >= "3.11" and python_version < "3.13"
|
|
||||||
iso8601==2.1.0 ; python_version >= "3.11" and python_version < "3.13"
|
|
||||||
jinja2==3.1.3 ; python_version >= "3.11" and python_version < "3.13"
|
|
||||||
limits==3.7.0 ; python_version >= "3.11" and python_version < "3.13"
|
|
||||||
lxml==5.1.0 ; python_version >= "3.11" and python_version < "3.13"
|
|
||||||
markupsafe==2.1.3 ; python_version >= "3.11" and python_version < "3.13"
|
|
||||||
multidict==6.0.5 ; python_version >= "3.11" and python_version < "3.13"
|
|
||||||
packaging==23.2 ; python_version >= "3.11" and python_version < "3.13"
|
|
||||||
passlib==1.7.4 ; python_version >= "3.11" and python_version < "3.13"
|
|
||||||
pendulum==3.0.0 ; python_version >= "3.11" and python_version < "3.13"
|
|
||||||
py-buzz==4.1.0 ; python_version >= "3.11" and python_version < "3.13"
|
|
||||||
pycares==4.4.0 ; (sys_platform == "linux" or sys_platform == "darwin") and python_version >= "3.11" and python_version < "3.13"
|
|
||||||
pycparser==2.21 ; python_version >= "3.11" and python_version < "3.13"
|
|
||||||
pycryptodomex==3.20.0 ; python_version >= "3.11" and python_version < "3.13"
|
|
||||||
pydantic==1.10.13 ; python_version >= "3.11" and python_version < "3.13"
|
|
||||||
pyjwt==2.8.0 ; python_version >= "3.11" and python_version < "3.13"
|
|
||||||
pyseto==1.7.7 ; python_version >= "3.11" and python_version < "3.13"
|
|
||||||
python-dateutil==2.8.2 ; python_version >= "3.11" and python_version < "3.13"
|
|
||||||
pyyaml==6.0.1 ; python_version >= "3.11" and python_version < "3.13"
|
|
||||||
sanic==23.12.1 ; python_version >= "3.11" and python_version < "3.13"
|
|
||||||
sanic-beskar==2.3.2 ; python_version >= "3.11" and python_version < "3.13"
|
|
||||||
sanic-ext==23.12.0 ; python_version >= "3.11" and python_version < "3.13"
|
|
||||||
sanic-limiter @ git+https://github.com/Omegastick/sanic-limiter@843e13144aa21d843ce212a7c1db31b72ce8a103 ; python_version >= "3.11" and python_version < "3.13"
|
|
||||||
sanic-routing==23.12.0 ; python_version >= "3.11" and python_version < "3.13"
|
|
||||||
sanic[ext]==23.12.1 ; python_version >= "3.11" and python_version < "3.13"
|
|
||||||
sentry-sdk[sanic]==1.39.2 ; python_version >= "3.11" and python_version < "3.13"
|
|
||||||
six==1.16.0 ; python_version >= "3.11" and python_version < "3.13"
|
|
||||||
soupsieve==2.5 ; python_version >= "3.11" and python_version < "3.13"
|
|
||||||
sqlalchemy==2.0.25 ; python_version >= "3.11" and python_version < "3.13"
|
|
||||||
toolz==0.12.0 ; python_version >= "3.11" and python_version < "3.13"
|
|
||||||
tracerite==1.1.1 ; python_version >= "3.11" and python_version < "3.13"
|
|
||||||
typing-extensions==4.9.0 ; python_version >= "3.11" and python_version < "3.13"
|
|
||||||
tzdata==2023.4 ; python_version >= "3.11" and python_version < "3.13"
|
|
||||||
ujson==5.9.0 ; python_version >= "3.11" and python_version < "3.13"
|
|
||||||
urllib3==2.1.0 ; python_version >= "3.11" and python_version < "3.13"
|
|
||||||
uvloop==0.19.0 ; sys_platform != "win32" and implementation_name == "cpython" and python_version >= "3.11" and python_version < "3.13"
|
|
||||||
websockets==12.0 ; python_version >= "3.11" and python_version < "3.13"
|
|
||||||
wrapt==1.16.0 ; python_version >= "3.11" and python_version < "3.13"
|
|
||||||
yarl==1.9.4 ; python_version >= "3.11" and python_version < "3.13"
|
|
7
settings.gradle.kts
Normal file
7
settings.gradle.kts
Normal file
@ -0,0 +1,7 @@
|
|||||||
|
rootProject.name = "revanced-api"
|
||||||
|
|
||||||
|
buildCache {
|
||||||
|
local {
|
||||||
|
isEnabled = "CI" !in System.getenv()
|
||||||
|
}
|
||||||
|
}
|
34
src/main/kotlin/app/revanced/api/command/MainCommand.kt
Normal file
34
src/main/kotlin/app/revanced/api/command/MainCommand.kt
Normal file
@ -0,0 +1,34 @@
|
|||||||
|
package app.revanced.api.command
|
||||||
|
|
||||||
|
import picocli.CommandLine
|
||||||
|
import java.util.*
|
||||||
|
|
||||||
|
internal val applicationVersion = MainCommand::class.java.getResourceAsStream(
|
||||||
|
"/app/revanced/api/version.properties",
|
||||||
|
)?.use { stream ->
|
||||||
|
Properties().apply {
|
||||||
|
load(stream)
|
||||||
|
}.getProperty("version")
|
||||||
|
} ?: "v0.0.0"
|
||||||
|
|
||||||
|
fun main(args: Array<String>) {
|
||||||
|
CommandLine(MainCommand).execute(*args).let(System::exit)
|
||||||
|
}
|
||||||
|
|
||||||
|
private object CLIVersionProvider : CommandLine.IVersionProvider {
|
||||||
|
override fun getVersion() =
|
||||||
|
arrayOf(
|
||||||
|
"ReVanced API $applicationVersion",
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
@CommandLine.Command(
|
||||||
|
name = "revanced-api",
|
||||||
|
description = ["API server for ReVanced"],
|
||||||
|
mixinStandardHelpOptions = true,
|
||||||
|
versionProvider = CLIVersionProvider::class,
|
||||||
|
subcommands = [
|
||||||
|
StartAPICommand::class,
|
||||||
|
],
|
||||||
|
)
|
||||||
|
private object MainCommand
|
46
src/main/kotlin/app/revanced/api/command/StartAPICommand.kt
Normal file
46
src/main/kotlin/app/revanced/api/command/StartAPICommand.kt
Normal file
@ -0,0 +1,46 @@
|
|||||||
|
package app.revanced.api.command
|
||||||
|
|
||||||
|
import app.revanced.api.configuration.*
|
||||||
|
import io.ktor.server.engine.*
|
||||||
|
import io.ktor.server.jetty.*
|
||||||
|
import picocli.CommandLine
|
||||||
|
import java.io.File
|
||||||
|
|
||||||
|
@CommandLine.Command(
|
||||||
|
name = "start",
|
||||||
|
description = ["Start the API server"],
|
||||||
|
)
|
||||||
|
internal object StartAPICommand : Runnable {
|
||||||
|
@CommandLine.Option(
|
||||||
|
names = ["-h", "--host"],
|
||||||
|
description = ["The host address to bind to."],
|
||||||
|
showDefaultValue = CommandLine.Help.Visibility.ALWAYS,
|
||||||
|
)
|
||||||
|
private var host: String = "0.0.0.0"
|
||||||
|
|
||||||
|
@CommandLine.Option(
|
||||||
|
names = ["-p", "--port"],
|
||||||
|
description = ["The port to listen on."],
|
||||||
|
showDefaultValue = CommandLine.Help.Visibility.ALWAYS,
|
||||||
|
)
|
||||||
|
private var port: Int = 8888
|
||||||
|
|
||||||
|
@CommandLine.Option(
|
||||||
|
names = ["-c", "--config"],
|
||||||
|
description = ["The path to the configuration file."],
|
||||||
|
showDefaultValue = CommandLine.Help.Visibility.ALWAYS,
|
||||||
|
)
|
||||||
|
private var configFile = File("configuration.toml")
|
||||||
|
|
||||||
|
override fun run() {
|
||||||
|
embeddedServer(Jetty, port, host) {
|
||||||
|
configureDependencies(configFile)
|
||||||
|
configureHTTP()
|
||||||
|
configureSerialization()
|
||||||
|
configureSecurity()
|
||||||
|
configureOpenAPI()
|
||||||
|
configureLogging()
|
||||||
|
configureRouting()
|
||||||
|
}.start(wait = true)
|
||||||
|
}
|
||||||
|
}
|
146
src/main/kotlin/app/revanced/api/configuration/Dependencies.kt
Normal file
146
src/main/kotlin/app/revanced/api/configuration/Dependencies.kt
Normal file
@ -0,0 +1,146 @@
|
|||||||
|
package app.revanced.api.configuration
|
||||||
|
|
||||||
|
import app.revanced.api.configuration.repository.AnnouncementRepository
|
||||||
|
import app.revanced.api.configuration.repository.BackendRepository
|
||||||
|
import app.revanced.api.configuration.repository.ConfigurationRepository
|
||||||
|
import app.revanced.api.configuration.repository.GitHubBackendRepository
|
||||||
|
import app.revanced.api.configuration.services.*
|
||||||
|
import app.revanced.api.configuration.services.AnnouncementService
|
||||||
|
import app.revanced.api.configuration.services.ApiService
|
||||||
|
import app.revanced.api.configuration.services.AuthService
|
||||||
|
import app.revanced.api.configuration.services.OldApiService
|
||||||
|
import app.revanced.api.configuration.services.PatchesService
|
||||||
|
import com.akuleshov7.ktoml.Toml
|
||||||
|
import com.akuleshov7.ktoml.source.decodeFromStream
|
||||||
|
import io.github.cdimascio.dotenv.Dotenv
|
||||||
|
import io.ktor.client.*
|
||||||
|
import io.ktor.client.engine.okhttp.*
|
||||||
|
import io.ktor.client.plugins.*
|
||||||
|
import io.ktor.client.plugins.auth.*
|
||||||
|
import io.ktor.client.plugins.auth.providers.*
|
||||||
|
import io.ktor.client.plugins.cache.*
|
||||||
|
import io.ktor.client.plugins.contentnegotiation.*
|
||||||
|
import io.ktor.client.plugins.resources.*
|
||||||
|
import io.ktor.serialization.kotlinx.json.*
|
||||||
|
import io.ktor.server.application.*
|
||||||
|
import kotlinx.serialization.ExperimentalSerializationApi
|
||||||
|
import kotlinx.serialization.json.Json
|
||||||
|
import kotlinx.serialization.json.JsonNamingStrategy
|
||||||
|
import org.jetbrains.exposed.sql.Database
|
||||||
|
import org.jetbrains.exposed.sql.transactions.TransactionManager
|
||||||
|
import org.koin.core.module.dsl.singleOf
|
||||||
|
import org.koin.core.parameter.parameterArrayOf
|
||||||
|
import org.koin.dsl.module
|
||||||
|
import org.koin.ktor.plugin.Koin
|
||||||
|
import java.io.File
|
||||||
|
|
||||||
|
@OptIn(ExperimentalSerializationApi::class)
|
||||||
|
fun Application.configureDependencies(
|
||||||
|
configFile: File,
|
||||||
|
) {
|
||||||
|
val 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 {
|
||||||
|
single<BackendRepository> {
|
||||||
|
GitHubBackendRepository(
|
||||||
|
get {
|
||||||
|
val defaultRequestUri = "https://api.github.com"
|
||||||
|
val configBlock: HttpClientConfig<OkHttpConfig>.() -> Unit = {
|
||||||
|
install(HttpCache)
|
||||||
|
install(Resources)
|
||||||
|
install(ContentNegotiation) {
|
||||||
|
json(
|
||||||
|
Json {
|
||||||
|
ignoreUnknownKeys = true
|
||||||
|
namingStrategy = JsonNamingStrategy.SnakeCase
|
||||||
|
},
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
get<Dotenv>()["BACKEND_API_TOKEN"]?.let {
|
||||||
|
install(Auth) {
|
||||||
|
bearer {
|
||||||
|
loadTokens {
|
||||||
|
BearerTokens(
|
||||||
|
accessToken = it,
|
||||||
|
refreshToken = "", // Required dummy value
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
sendWithoutRequest { true }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
parameterArrayOf(defaultRequestUri, configBlock)
|
||||||
|
},
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
single<ConfigurationRepository> {
|
||||||
|
Toml.decodeFromStream(configFile.inputStream())
|
||||||
|
}
|
||||||
|
|
||||||
|
single {
|
||||||
|
val dotenv = get<Dotenv>()
|
||||||
|
|
||||||
|
TransactionManager.defaultDatabase = Database.connect(
|
||||||
|
url = dotenv["DB_URL"],
|
||||||
|
user = dotenv["DB_USER"],
|
||||||
|
password = dotenv["DB_PASSWORD"],
|
||||||
|
)
|
||||||
|
|
||||||
|
AnnouncementRepository()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
val serviceModule = module {
|
||||||
|
single {
|
||||||
|
val dotenv = get<Dotenv>()
|
||||||
|
|
||||||
|
val jwtSecret = dotenv["JWT_SECRET"]
|
||||||
|
val issuer = dotenv["JWT_ISSUER"]
|
||||||
|
val validityInMin = dotenv["JWT_VALIDITY_IN_MIN"].toInt()
|
||||||
|
|
||||||
|
val authSHA256DigestString = dotenv["AUTH_SHA256_DIGEST"]
|
||||||
|
|
||||||
|
AuthService(issuer, validityInMin, jwtSecret, authSHA256DigestString)
|
||||||
|
}
|
||||||
|
single {
|
||||||
|
OldApiService(
|
||||||
|
get {
|
||||||
|
val defaultRequestUri = get<Dotenv>()["OLD_API_URL"]
|
||||||
|
parameterArrayOf(defaultRequestUri)
|
||||||
|
},
|
||||||
|
)
|
||||||
|
}
|
||||||
|
singleOf(::AnnouncementService)
|
||||||
|
singleOf(::SignatureService)
|
||||||
|
singleOf(::PatchesService)
|
||||||
|
singleOf(::ApiService)
|
||||||
|
}
|
||||||
|
|
||||||
|
install(Koin) {
|
||||||
|
modules(
|
||||||
|
globalModule,
|
||||||
|
repositoryModule,
|
||||||
|
serviceModule,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
27
src/main/kotlin/app/revanced/api/configuration/Extensions.kt
Normal file
27
src/main/kotlin/app/revanced/api/configuration/Extensions.kt
Normal file
@ -0,0 +1,27 @@
|
|||||||
|
package app.revanced.api.configuration
|
||||||
|
|
||||||
|
import io.bkbn.kompendium.core.plugin.NotarizedRoute
|
||||||
|
import io.ktor.http.*
|
||||||
|
import io.ktor.http.content.*
|
||||||
|
import io.ktor.server.application.*
|
||||||
|
import io.ktor.server.plugins.cachingheaders.*
|
||||||
|
import io.ktor.server.response.*
|
||||||
|
import kotlin.time.Duration
|
||||||
|
|
||||||
|
internal suspend fun ApplicationCall.respondOrNotFound(value: Any?) = respond(value ?: HttpStatusCode.NotFound)
|
||||||
|
|
||||||
|
internal fun ApplicationCallPipeline.installCache(maxAge: Duration) =
|
||||||
|
installCache(CacheControl.MaxAge(maxAgeSeconds = maxAge.inWholeSeconds.toInt()))
|
||||||
|
|
||||||
|
internal fun ApplicationCallPipeline.installNoCache() =
|
||||||
|
installCache(CacheControl.NoCache(null))
|
||||||
|
|
||||||
|
internal fun ApplicationCallPipeline.installCache(cacheControl: CacheControl) =
|
||||||
|
install(CachingHeaders) {
|
||||||
|
options { _, _ ->
|
||||||
|
CachingOptions(cacheControl)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
internal fun ApplicationCallPipeline.installNotarizedRoute(configure: NotarizedRoute.Config.() -> Unit = {}) =
|
||||||
|
install(NotarizedRoute(), configure)
|
38
src/main/kotlin/app/revanced/api/configuration/HTTP.kt
Normal file
38
src/main/kotlin/app/revanced/api/configuration/HTTP.kt
Normal file
@ -0,0 +1,38 @@
|
|||||||
|
package app.revanced.api.configuration
|
||||||
|
|
||||||
|
import app.revanced.api.configuration.repository.ConfigurationRepository
|
||||||
|
import io.ktor.server.application.*
|
||||||
|
import io.ktor.server.plugins.*
|
||||||
|
import io.ktor.server.plugins.cors.routing.*
|
||||||
|
import io.ktor.server.plugins.ratelimit.*
|
||||||
|
import io.ktor.server.request.*
|
||||||
|
import org.koin.ktor.ext.get
|
||||||
|
import kotlin.time.Duration.Companion.minutes
|
||||||
|
|
||||||
|
fun Application.configureHTTP() {
|
||||||
|
val configurationRepository = get<ConfigurationRepository>()
|
||||||
|
|
||||||
|
install(CORS) {
|
||||||
|
allowHost(
|
||||||
|
host = configurationRepository.cors.host,
|
||||||
|
subDomains = configurationRepository.cors.subDomains,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
install(RateLimit) {
|
||||||
|
fun rateLimit(name: String, block: RateLimitProviderConfig.() -> Unit) = register(RateLimitName(name)) {
|
||||||
|
requestKey {
|
||||||
|
it.request.uri + it.request.origin.remoteAddress
|
||||||
|
}
|
||||||
|
|
||||||
|
block()
|
||||||
|
}
|
||||||
|
|
||||||
|
rateLimit("weak") {
|
||||||
|
rateLimiter(limit = 30, refillPeriod = 2.minutes)
|
||||||
|
}
|
||||||
|
rateLimit("strong") {
|
||||||
|
rateLimiter(limit = 5, refillPeriod = 1.minutes)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
16
src/main/kotlin/app/revanced/api/configuration/Logging.kt
Normal file
16
src/main/kotlin/app/revanced/api/configuration/Logging.kt
Normal file
@ -0,0 +1,16 @@
|
|||||||
|
package app.revanced.api.configuration
|
||||||
|
|
||||||
|
import io.ktor.server.application.*
|
||||||
|
import io.ktor.server.plugins.callloging.*
|
||||||
|
import io.ktor.server.request.*
|
||||||
|
|
||||||
|
internal fun Application.configureLogging() {
|
||||||
|
install(CallLogging) {
|
||||||
|
format { call ->
|
||||||
|
val status = call.response.status()
|
||||||
|
val httpMethod = call.request.httpMethod.value
|
||||||
|
val uri = call.request.uri
|
||||||
|
"$status $httpMethod $uri"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
51
src/main/kotlin/app/revanced/api/configuration/OpenAPI.kt
Normal file
51
src/main/kotlin/app/revanced/api/configuration/OpenAPI.kt
Normal file
@ -0,0 +1,51 @@
|
|||||||
|
package app.revanced.api.configuration
|
||||||
|
|
||||||
|
import app.revanced.api.command.applicationVersion
|
||||||
|
import app.revanced.api.configuration.repository.ConfigurationRepository
|
||||||
|
import io.bkbn.kompendium.core.plugin.NotarizedApplication
|
||||||
|
import io.bkbn.kompendium.json.schema.KotlinXSchemaConfigurator
|
||||||
|
import io.bkbn.kompendium.oas.OpenApiSpec
|
||||||
|
import io.bkbn.kompendium.oas.component.Components
|
||||||
|
import io.bkbn.kompendium.oas.info.Contact
|
||||||
|
import io.bkbn.kompendium.oas.info.Info
|
||||||
|
import io.bkbn.kompendium.oas.info.License
|
||||||
|
import io.bkbn.kompendium.oas.security.BearerAuth
|
||||||
|
import io.bkbn.kompendium.oas.server.Server
|
||||||
|
import io.ktor.server.application.*
|
||||||
|
import org.koin.ktor.ext.get
|
||||||
|
import java.net.URI
|
||||||
|
|
||||||
|
internal fun Application.configureOpenAPI() {
|
||||||
|
val configurationRepository = get<ConfigurationRepository>()
|
||||||
|
|
||||||
|
install(NotarizedApplication()) {
|
||||||
|
spec = OpenApiSpec(
|
||||||
|
info = Info(
|
||||||
|
title = "ReVanced API",
|
||||||
|
version = applicationVersion,
|
||||||
|
description = "API server for ReVanced.",
|
||||||
|
contact = Contact(
|
||||||
|
name = "ReVanced",
|
||||||
|
url = URI("https://revanced.app"),
|
||||||
|
email = "contact@revanced.app",
|
||||||
|
),
|
||||||
|
license = License(
|
||||||
|
name = "AGPLv3",
|
||||||
|
url = URI("https://github.com/ReVanced/revanced-api/blob/main/LICENSE"),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
components = Components(
|
||||||
|
securitySchemes = mutableMapOf(
|
||||||
|
"bearer" to BearerAuth(),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
).apply {
|
||||||
|
servers += Server(
|
||||||
|
url = URI(configurationRepository.endpoint),
|
||||||
|
description = "ReVanced API server",
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
schemaConfigurator = KotlinXSchemaConfigurator()
|
||||||
|
}
|
||||||
|
}
|
31
src/main/kotlin/app/revanced/api/configuration/Routing.kt
Normal file
31
src/main/kotlin/app/revanced/api/configuration/Routing.kt
Normal file
@ -0,0 +1,31 @@
|
|||||||
|
package app.revanced.api.configuration
|
||||||
|
|
||||||
|
import app.revanced.api.configuration.repository.ConfigurationRepository
|
||||||
|
import app.revanced.api.configuration.routes.announcementsRoute
|
||||||
|
import app.revanced.api.configuration.routes.oldApiRoute
|
||||||
|
import app.revanced.api.configuration.routes.patchesRoute
|
||||||
|
import app.revanced.api.configuration.routes.rootRoute
|
||||||
|
import io.bkbn.kompendium.core.routes.redoc
|
||||||
|
import io.bkbn.kompendium.core.routes.swagger
|
||||||
|
import io.ktor.server.application.*
|
||||||
|
import io.ktor.server.routing.*
|
||||||
|
import kotlin.time.Duration.Companion.minutes
|
||||||
|
import org.koin.ktor.ext.get as koinGet
|
||||||
|
|
||||||
|
internal fun Application.configureRouting() = routing {
|
||||||
|
val configuration = koinGet<ConfigurationRepository>()
|
||||||
|
|
||||||
|
installCache(5.minutes)
|
||||||
|
|
||||||
|
route("/v${configuration.apiVersion}") {
|
||||||
|
patchesRoute()
|
||||||
|
announcementsRoute()
|
||||||
|
rootRoute()
|
||||||
|
}
|
||||||
|
|
||||||
|
swagger(pageTitle = "ReVanced API", path = "/")
|
||||||
|
redoc(pageTitle = "ReVanced API", path = "/redoc")
|
||||||
|
|
||||||
|
// TODO: Remove, once migration period from v2 API is over (In 1-2 years).
|
||||||
|
oldApiRoute()
|
||||||
|
}
|
@ -0,0 +1,9 @@
|
|||||||
|
package app.revanced.api.configuration
|
||||||
|
|
||||||
|
import app.revanced.api.configuration.services.AuthService
|
||||||
|
import io.ktor.server.application.*
|
||||||
|
import org.koin.ktor.ext.get
|
||||||
|
|
||||||
|
fun Application.configureSecurity() {
|
||||||
|
get<AuthService>().configureSecurity(this)
|
||||||
|
}
|
@ -0,0 +1,23 @@
|
|||||||
|
package app.revanced.api.configuration
|
||||||
|
|
||||||
|
import io.bkbn.kompendium.oas.serialization.KompendiumSerializersModule
|
||||||
|
import io.ktor.serialization.kotlinx.json.*
|
||||||
|
import io.ktor.server.application.*
|
||||||
|
import io.ktor.server.plugins.contentnegotiation.*
|
||||||
|
import kotlinx.serialization.ExperimentalSerializationApi
|
||||||
|
import kotlinx.serialization.json.Json
|
||||||
|
import kotlinx.serialization.json.JsonNamingStrategy
|
||||||
|
|
||||||
|
@OptIn(ExperimentalSerializationApi::class)
|
||||||
|
fun Application.configureSerialization() {
|
||||||
|
install(ContentNegotiation) {
|
||||||
|
json(
|
||||||
|
Json {
|
||||||
|
serializersModule = KompendiumSerializersModule.module
|
||||||
|
namingStrategy = JsonNamingStrategy.SnakeCase
|
||||||
|
explicitNulls = false
|
||||||
|
encodeDefaults = true
|
||||||
|
},
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,191 @@
|
|||||||
|
package app.revanced.api.configuration.repository
|
||||||
|
|
||||||
|
import app.revanced.api.configuration.schema.APIAnnouncement
|
||||||
|
import app.revanced.api.configuration.schema.APIResponseAnnouncement
|
||||||
|
import app.revanced.api.configuration.schema.APIResponseAnnouncementId
|
||||||
|
import kotlinx.coroutines.Dispatchers
|
||||||
|
import kotlinx.coroutines.awaitAll
|
||||||
|
import kotlinx.coroutines.runBlocking
|
||||||
|
import kotlinx.datetime.*
|
||||||
|
import org.jetbrains.exposed.dao.IntEntity
|
||||||
|
import org.jetbrains.exposed.dao.IntEntityClass
|
||||||
|
import org.jetbrains.exposed.dao.id.EntityID
|
||||||
|
import org.jetbrains.exposed.dao.id.IntIdTable
|
||||||
|
import org.jetbrains.exposed.sql.*
|
||||||
|
import org.jetbrains.exposed.sql.kotlin.datetime.CurrentDateTime
|
||||||
|
import org.jetbrains.exposed.sql.kotlin.datetime.datetime
|
||||||
|
import org.jetbrains.exposed.sql.transactions.experimental.newSuspendedTransaction
|
||||||
|
import org.jetbrains.exposed.sql.transactions.experimental.suspendedTransactionAsync
|
||||||
|
|
||||||
|
internal class AnnouncementRepository {
|
||||||
|
// This is better than doing a maxByOrNull { it.id }.
|
||||||
|
private var latestAnnouncement: Announcement? = null
|
||||||
|
private val latestAnnouncementByChannel = mutableMapOf<String, Announcement>()
|
||||||
|
|
||||||
|
private fun updateLatestAnnouncement(new: Announcement) {
|
||||||
|
if (latestAnnouncement?.id?.value == new.id.value) {
|
||||||
|
latestAnnouncement = new
|
||||||
|
latestAnnouncementByChannel[new.channel ?: return] = new
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
init {
|
||||||
|
runBlocking {
|
||||||
|
transaction {
|
||||||
|
SchemaUtils.create(Announcements, Attachments)
|
||||||
|
|
||||||
|
// Initialize the latest announcement.
|
||||||
|
latestAnnouncement = Announcement.all().onEach {
|
||||||
|
latestAnnouncementByChannel[it.channel ?: return@onEach] = it
|
||||||
|
}.maxByOrNull { it.id } ?: return@transaction
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
suspend fun all() = transaction {
|
||||||
|
Announcement.all().map { it.toApi() }
|
||||||
|
}
|
||||||
|
|
||||||
|
suspend fun all(channel: String) = transaction {
|
||||||
|
Announcement.find { Announcements.channel eq channel }.map { it.toApi() }
|
||||||
|
}
|
||||||
|
|
||||||
|
suspend fun delete(id: Int) = transaction {
|
||||||
|
val announcement = Announcement.findById(id) ?: return@transaction
|
||||||
|
|
||||||
|
announcement.delete()
|
||||||
|
|
||||||
|
// In case the latest announcement was deleted, query the new latest announcement again.
|
||||||
|
if (latestAnnouncement?.id?.value == id) {
|
||||||
|
latestAnnouncement = Announcement.all().maxByOrNull { it.id }
|
||||||
|
|
||||||
|
// 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!!
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun latest() = latestAnnouncement?.toApi()
|
||||||
|
|
||||||
|
fun latest(channel: String) = latestAnnouncementByChannel[channel]?.toApi()
|
||||||
|
|
||||||
|
fun latestId() = latest()?.id?.toApi()
|
||||||
|
|
||||||
|
fun latestId(channel: String) = latest(channel)?.id?.toApi()
|
||||||
|
|
||||||
|
suspend fun archive(
|
||||||
|
id: Int,
|
||||||
|
archivedAt: LocalDateTime?,
|
||||||
|
) = transaction {
|
||||||
|
Announcement.findByIdAndUpdate(id) {
|
||||||
|
it.archivedAt = archivedAt ?: java.time.LocalDateTime.now().toKotlinLocalDateTime()
|
||||||
|
}?.also(::updateLatestAnnouncement)
|
||||||
|
}
|
||||||
|
|
||||||
|
suspend fun unarchive(id: Int) = transaction {
|
||||||
|
Announcement.findByIdAndUpdate(id) {
|
||||||
|
it.archivedAt = null
|
||||||
|
}?.also(::updateLatestAnnouncement)
|
||||||
|
}
|
||||||
|
|
||||||
|
suspend fun new(new: APIAnnouncement) = transaction {
|
||||||
|
Announcement.new {
|
||||||
|
author = new.author
|
||||||
|
title = new.title
|
||||||
|
content = new.content
|
||||||
|
channel = new.channel
|
||||||
|
archivedAt = new.archivedAt
|
||||||
|
level = new.level
|
||||||
|
}.also { newAnnouncement ->
|
||||||
|
new.attachmentUrls.map { newUrl ->
|
||||||
|
suspendedTransactionAsync {
|
||||||
|
Attachment.new {
|
||||||
|
url = newUrl
|
||||||
|
announcement = newAnnouncement
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}.awaitAll()
|
||||||
|
}.also(::updateLatestAnnouncement)
|
||||||
|
}
|
||||||
|
|
||||||
|
suspend fun update(id: Int, new: APIAnnouncement) = transaction {
|
||||||
|
Announcement.findByIdAndUpdate(id) {
|
||||||
|
it.author = new.author
|
||||||
|
it.title = new.title
|
||||||
|
it.content = new.content
|
||||||
|
it.channel = new.channel
|
||||||
|
it.archivedAt = new.archivedAt
|
||||||
|
it.level = new.level
|
||||||
|
}?.also { newAnnouncement ->
|
||||||
|
newAnnouncement.attachments.map {
|
||||||
|
suspendedTransactionAsync {
|
||||||
|
it.delete()
|
||||||
|
}
|
||||||
|
}.awaitAll()
|
||||||
|
|
||||||
|
new.attachmentUrls.map { newUrl ->
|
||||||
|
suspendedTransactionAsync {
|
||||||
|
Attachment.new {
|
||||||
|
url = newUrl
|
||||||
|
announcement = newAnnouncement
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}.awaitAll()
|
||||||
|
}?.also(::updateLatestAnnouncement)
|
||||||
|
}
|
||||||
|
|
||||||
|
private suspend fun <T> transaction(statement: suspend Transaction.() -> T) =
|
||||||
|
newSuspendedTransaction(Dispatchers.IO, statement = statement)
|
||||||
|
|
||||||
|
private object Announcements : IntIdTable() {
|
||||||
|
val author = varchar("author", 32).nullable()
|
||||||
|
val title = varchar("title", 64)
|
||||||
|
val content = text("content").nullable()
|
||||||
|
val channel = varchar("channel", 16).nullable()
|
||||||
|
val createdAt = datetime("createdAt").defaultExpression(CurrentDateTime)
|
||||||
|
val archivedAt = datetime("archivedAt").nullable()
|
||||||
|
val level = integer("level")
|
||||||
|
}
|
||||||
|
|
||||||
|
private object Attachments : IntIdTable() {
|
||||||
|
val url = varchar("url", 256)
|
||||||
|
val announcement = reference("announcement", Announcements, onDelete = ReferenceOption.CASCADE)
|
||||||
|
}
|
||||||
|
|
||||||
|
class Announcement(id: EntityID<Int>) : IntEntity(id) {
|
||||||
|
companion object : IntEntityClass<Announcement>(Announcements)
|
||||||
|
|
||||||
|
var author by Announcements.author
|
||||||
|
var title by Announcements.title
|
||||||
|
var content by Announcements.content
|
||||||
|
val attachments by Attachment referrersOn Attachments.announcement
|
||||||
|
var channel by Announcements.channel
|
||||||
|
var createdAt by Announcements.createdAt
|
||||||
|
var archivedAt by Announcements.archivedAt
|
||||||
|
var level by Announcements.level
|
||||||
|
}
|
||||||
|
|
||||||
|
class Attachment(id: EntityID<Int>) : IntEntity(id) {
|
||||||
|
companion object : IntEntityClass<Attachment>(Attachments)
|
||||||
|
|
||||||
|
var url by Attachments.url
|
||||||
|
var announcement by Announcement referencedOn Attachments.announcement
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun Announcement.toApi() = APIResponseAnnouncement(
|
||||||
|
id.value,
|
||||||
|
author,
|
||||||
|
title,
|
||||||
|
content,
|
||||||
|
attachments.map { it.url },
|
||||||
|
channel,
|
||||||
|
createdAt,
|
||||||
|
archivedAt,
|
||||||
|
level,
|
||||||
|
)
|
||||||
|
|
||||||
|
private fun Int.toApi() = APIResponseAnnouncementId(this)
|
||||||
|
}
|
@ -0,0 +1,172 @@
|
|||||||
|
package app.revanced.api.configuration.repository
|
||||||
|
|
||||||
|
import io.ktor.client.*
|
||||||
|
import kotlinx.datetime.LocalDateTime
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The backend of the API used to get data.
|
||||||
|
*
|
||||||
|
* @param client The HTTP client to use for requests.
|
||||||
|
*/
|
||||||
|
abstract class BackendRepository internal constructor(
|
||||||
|
protected val client: HttpClient,
|
||||||
|
) {
|
||||||
|
/**
|
||||||
|
* A user.
|
||||||
|
*
|
||||||
|
* @property name The name of the user.
|
||||||
|
* @property avatarUrl The URL to the avatar of the user.
|
||||||
|
* @property url The URL to the profile of the user.
|
||||||
|
*/
|
||||||
|
interface BackendUser {
|
||||||
|
val name: String
|
||||||
|
val avatarUrl: String
|
||||||
|
val url: String
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* An organization.
|
||||||
|
*
|
||||||
|
* @property members The members of the organization.
|
||||||
|
*/
|
||||||
|
class BackendOrganization(
|
||||||
|
// Using a list instead of a set because set semantics are unnecessary here.
|
||||||
|
val members: List<BackendMember>,
|
||||||
|
) {
|
||||||
|
/**
|
||||||
|
* A member of an organization.
|
||||||
|
*
|
||||||
|
* @property name The name of the member.
|
||||||
|
* @property avatarUrl The URL to the avatar of the member.
|
||||||
|
* @property url The URL to the profile of the member.
|
||||||
|
* @property bio The bio of the member.
|
||||||
|
* @property gpgKeys The GPG key of the member.
|
||||||
|
*/
|
||||||
|
class BackendMember(
|
||||||
|
override val name: String,
|
||||||
|
override val avatarUrl: String,
|
||||||
|
override val url: String,
|
||||||
|
val bio: String?,
|
||||||
|
val gpgKeys: GpgKeys,
|
||||||
|
) : BackendUser {
|
||||||
|
/**
|
||||||
|
* The GPG keys of a member.
|
||||||
|
*
|
||||||
|
* @property ids The IDs of the GPG keys.
|
||||||
|
* @property url The URL to the GPG master key.
|
||||||
|
*/
|
||||||
|
class GpgKeys(
|
||||||
|
// Using a list instead of a set because set semantics are unnecessary here.
|
||||||
|
val ids: List<String>,
|
||||||
|
val url: String,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A repository of an organization.
|
||||||
|
*
|
||||||
|
* @property contributors The contributors of the repository.
|
||||||
|
*/
|
||||||
|
class BackendRepository(
|
||||||
|
// Using a list instead of a set because set semantics are unnecessary here.
|
||||||
|
val contributors: List<BackendContributor>,
|
||||||
|
) {
|
||||||
|
/**
|
||||||
|
* A contributor of a repository.
|
||||||
|
*
|
||||||
|
* @property name The name of the contributor.
|
||||||
|
* @property avatarUrl The URL to the avatar of the contributor.
|
||||||
|
* @property url The URL to the profile of the contributor.
|
||||||
|
* @property contributions The number of contributions of the contributor.
|
||||||
|
*/
|
||||||
|
class BackendContributor(
|
||||||
|
override val name: String,
|
||||||
|
override val avatarUrl: String,
|
||||||
|
override val url: String,
|
||||||
|
val contributions: Int,
|
||||||
|
) : BackendUser
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A release of a repository.
|
||||||
|
*
|
||||||
|
* @property tag The tag of the release.
|
||||||
|
* @property assets The assets of the release.
|
||||||
|
* @property createdAt The date and time the release was created.
|
||||||
|
* @property releaseNote The release note of the release.
|
||||||
|
*/
|
||||||
|
class BackendRelease(
|
||||||
|
val tag: String,
|
||||||
|
val releaseNote: String,
|
||||||
|
val createdAt: LocalDateTime,
|
||||||
|
// Using a list instead of a set because set semantics are unnecessary here.
|
||||||
|
val assets: List<BackendAsset>,
|
||||||
|
) {
|
||||||
|
companion object {
|
||||||
|
fun List<BackendAsset>.first(assetRegex: Regex) = first { assetRegex.containsMatchIn(it.name) }
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* An asset of a release.
|
||||||
|
*
|
||||||
|
* @property name The name of the asset.
|
||||||
|
* @property downloadUrl The URL to download the asset.
|
||||||
|
*/
|
||||||
|
class BackendAsset(
|
||||||
|
val name: String,
|
||||||
|
val downloadUrl: String,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The rate limit of the backend.
|
||||||
|
*
|
||||||
|
* @property limit The limit of the rate limit.
|
||||||
|
* @property remaining The remaining requests of the rate limit.
|
||||||
|
* @property reset The date and time the rate limit resets.
|
||||||
|
*/
|
||||||
|
class BackendRateLimit(
|
||||||
|
val limit: Int,
|
||||||
|
val remaining: Int,
|
||||||
|
val reset: LocalDateTime,
|
||||||
|
)
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get a release of a repository.
|
||||||
|
*
|
||||||
|
* @param owner The owner of the repository.
|
||||||
|
* @param repository The name of the repository.
|
||||||
|
* @param tag The tag of the release. If null, the latest release is returned.
|
||||||
|
* @return The release.
|
||||||
|
*/
|
||||||
|
abstract suspend fun release(
|
||||||
|
owner: String,
|
||||||
|
repository: String,
|
||||||
|
tag: String? = null,
|
||||||
|
): BackendOrganization.BackendRepository.BackendRelease
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the contributors of a repository.
|
||||||
|
*
|
||||||
|
* @param owner The owner of the repository.
|
||||||
|
* @param repository The name of the repository.
|
||||||
|
* @return The contributors.
|
||||||
|
*/
|
||||||
|
abstract suspend fun contributors(owner: String, repository: String): List<BackendOrganization.BackendRepository.BackendContributor>
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the members of an organization.
|
||||||
|
*
|
||||||
|
* @param organization The name of the organization.
|
||||||
|
* @return The members.
|
||||||
|
*/
|
||||||
|
abstract suspend fun members(organization: String): List<BackendOrganization.BackendMember>
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the rate limit of the backend.
|
||||||
|
*
|
||||||
|
* @return The rate limit.
|
||||||
|
*/
|
||||||
|
abstract suspend fun rateLimit(): BackendRateLimit?
|
||||||
|
}
|
@ -0,0 +1,93 @@
|
|||||||
|
package app.revanced.api.configuration.repository
|
||||||
|
|
||||||
|
import app.revanced.api.configuration.services.PatchesService
|
||||||
|
import kotlinx.serialization.KSerializer
|
||||||
|
import kotlinx.serialization.SerialName
|
||||||
|
import kotlinx.serialization.Serializable
|
||||||
|
import kotlinx.serialization.descriptors.PrimitiveKind
|
||||||
|
import kotlinx.serialization.descriptors.PrimitiveSerialDescriptor
|
||||||
|
import kotlinx.serialization.descriptors.SerialDescriptor
|
||||||
|
import kotlinx.serialization.encoding.Decoder
|
||||||
|
import kotlinx.serialization.encoding.Encoder
|
||||||
|
import java.io.File
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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 patches The source of the patches.
|
||||||
|
* @property integrations The source of the integrations.
|
||||||
|
* @property contributorsRepositoryNames The names of the repositories to get contributors from.
|
||||||
|
* @property apiVersion The version to use for the API.
|
||||||
|
* @property cors The CORS configuration.
|
||||||
|
* @property endpoint The endpoint of the API.
|
||||||
|
*/
|
||||||
|
@Serializable
|
||||||
|
internal class ConfigurationRepository(
|
||||||
|
val organization: String,
|
||||||
|
val patches: AssetConfiguration,
|
||||||
|
val integrations: AssetConfiguration,
|
||||||
|
@SerialName("contributors-repositories")
|
||||||
|
val contributorsRepositoryNames: Set<String>,
|
||||||
|
@SerialName("api-version")
|
||||||
|
val apiVersion: Int = 1,
|
||||||
|
val cors: Cors,
|
||||||
|
val endpoint: String,
|
||||||
|
) {
|
||||||
|
/**
|
||||||
|
* An asset configuration.
|
||||||
|
*
|
||||||
|
* [PatchesService] uses [BackendRepository] to get assets from its releases.
|
||||||
|
* A release contains multiple assets.
|
||||||
|
*
|
||||||
|
* This configuration is used in [ConfigurationRepository]
|
||||||
|
* to determine which release assets from repositories to get and to verify them.
|
||||||
|
*
|
||||||
|
* @property repository The repository in which releases are made to get an asset.
|
||||||
|
* @property assetRegex The regex matching the asset name.
|
||||||
|
* @property signatureAssetRegex The regex matching the signature asset name to verify the asset.
|
||||||
|
* @property publicKeyFile The public key file to verify the signature of the asset.
|
||||||
|
*/
|
||||||
|
@Serializable
|
||||||
|
internal class AssetConfiguration(
|
||||||
|
val repository: String,
|
||||||
|
@Serializable(with = RegexSerializer::class)
|
||||||
|
@SerialName("asset-regex")
|
||||||
|
val assetRegex: Regex,
|
||||||
|
@Serializable(with = RegexSerializer::class)
|
||||||
|
@SerialName("signature-asset-regex")
|
||||||
|
val signatureAssetRegex: Regex,
|
||||||
|
@Serializable(with = FileSerializer::class)
|
||||||
|
@SerialName("public-key-file")
|
||||||
|
val publicKeyFile: File,
|
||||||
|
)
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The CORS configuration.
|
||||||
|
*
|
||||||
|
* @property host The host of the API to configure CORS.
|
||||||
|
* @property subDomains The subdomains to allow for CORS.
|
||||||
|
*/
|
||||||
|
@Serializable
|
||||||
|
internal class Cors(
|
||||||
|
val host: String,
|
||||||
|
@SerialName("sub-domains")
|
||||||
|
val subDomains: List<String>,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
private object RegexSerializer : KSerializer<Regex> {
|
||||||
|
override val descriptor: SerialDescriptor = PrimitiveSerialDescriptor("Regex", PrimitiveKind.STRING)
|
||||||
|
|
||||||
|
override fun serialize(encoder: Encoder, value: Regex) = encoder.encodeString(value.pattern)
|
||||||
|
|
||||||
|
override fun deserialize(decoder: Decoder) = Regex(decoder.decodeString())
|
||||||
|
}
|
||||||
|
|
||||||
|
private object FileSerializer : KSerializer<File> {
|
||||||
|
override val descriptor: SerialDescriptor = PrimitiveSerialDescriptor("File", PrimitiveKind.STRING)
|
||||||
|
|
||||||
|
override fun serialize(encoder: Encoder, value: File) = encoder.encodeString(value.path)
|
||||||
|
|
||||||
|
override fun deserialize(decoder: Decoder) = File(decoder.decodeString())
|
||||||
|
}
|
@ -0,0 +1,208 @@
|
|||||||
|
package app.revanced.api.configuration.repository
|
||||||
|
|
||||||
|
import app.revanced.api.configuration.repository.BackendRepository.BackendOrganization.BackendMember
|
||||||
|
import app.revanced.api.configuration.repository.BackendRepository.BackendOrganization.BackendRepository.BackendContributor
|
||||||
|
import app.revanced.api.configuration.repository.BackendRepository.BackendOrganization.BackendRepository.BackendRelease
|
||||||
|
import app.revanced.api.configuration.repository.BackendRepository.BackendOrganization.BackendRepository.BackendRelease.BackendAsset
|
||||||
|
import app.revanced.api.configuration.repository.GitHubOrganization.GitHubRepository.GitHubContributor
|
||||||
|
import app.revanced.api.configuration.repository.GitHubOrganization.GitHubRepository.GitHubRelease
|
||||||
|
import app.revanced.api.configuration.repository.Organization.Repository.Contributors
|
||||||
|
import app.revanced.api.configuration.repository.Organization.Repository.Releases
|
||||||
|
import io.ktor.client.*
|
||||||
|
import io.ktor.client.call.*
|
||||||
|
import io.ktor.client.plugins.resources.*
|
||||||
|
import io.ktor.resources.*
|
||||||
|
import kotlinx.coroutines.*
|
||||||
|
import kotlinx.datetime.Instant
|
||||||
|
import kotlinx.datetime.TimeZone
|
||||||
|
import kotlinx.datetime.toLocalDateTime
|
||||||
|
import kotlinx.serialization.Serializable
|
||||||
|
|
||||||
|
class GitHubBackendRepository(client: HttpClient) : BackendRepository(client) {
|
||||||
|
override suspend fun release(
|
||||||
|
owner: String,
|
||||||
|
repository: String,
|
||||||
|
tag: String?,
|
||||||
|
): BackendRelease {
|
||||||
|
val release: GitHubRelease = if (tag != null) {
|
||||||
|
client.get(Releases.Tag(owner, repository, tag)).body()
|
||||||
|
} else {
|
||||||
|
client.get(Releases.Latest(owner, repository)).body()
|
||||||
|
}
|
||||||
|
|
||||||
|
return BackendRelease(
|
||||||
|
tag = release.tagName,
|
||||||
|
releaseNote = release.body,
|
||||||
|
createdAt = release.createdAt.toLocalDateTime(TimeZone.UTC),
|
||||||
|
assets = release.assets.map {
|
||||||
|
BackendAsset(
|
||||||
|
name = it.name,
|
||||||
|
downloadUrl = it.browserDownloadUrl,
|
||||||
|
)
|
||||||
|
},
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
override suspend fun contributors(
|
||||||
|
owner: String,
|
||||||
|
repository: String,
|
||||||
|
): List<BackendContributor> {
|
||||||
|
val contributors: List<GitHubContributor> = client.get(
|
||||||
|
Contributors(
|
||||||
|
owner,
|
||||||
|
repository,
|
||||||
|
),
|
||||||
|
).body()
|
||||||
|
|
||||||
|
return contributors.map {
|
||||||
|
BackendContributor(
|
||||||
|
name = it.login,
|
||||||
|
avatarUrl = it.avatarUrl,
|
||||||
|
url = it.htmlUrl,
|
||||||
|
contributions = it.contributions,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override suspend fun members(organization: String): List<BackendMember> {
|
||||||
|
// Get the list of members of the organization.
|
||||||
|
val members: List<GitHubOrganization.GitHubMember> = client.get(Organization.Members(organization)).body()
|
||||||
|
|
||||||
|
return coroutineScope {
|
||||||
|
members.map { member ->
|
||||||
|
async {
|
||||||
|
awaitAll(
|
||||||
|
async {
|
||||||
|
// Get the user.
|
||||||
|
client.get(User(member.login)).body<GitHubUser>()
|
||||||
|
},
|
||||||
|
async {
|
||||||
|
// Get the GPG key of the user.
|
||||||
|
client.get(User.GpgKeys(member.login)).body<List<GitHubUser.GitHubGpgKey>>()
|
||||||
|
},
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}.awaitAll().map { responses ->
|
||||||
|
val user = responses[0] as GitHubUser
|
||||||
|
|
||||||
|
@Suppress("UNCHECKED_CAST")
|
||||||
|
val gpgKeys = responses[1] as List<GitHubUser.GitHubGpgKey>
|
||||||
|
|
||||||
|
BackendMember(
|
||||||
|
name = user.login,
|
||||||
|
avatarUrl = user.avatarUrl,
|
||||||
|
url = user.htmlUrl,
|
||||||
|
bio = user.bio,
|
||||||
|
gpgKeys =
|
||||||
|
BackendMember.GpgKeys(
|
||||||
|
ids = gpgKeys.map { it.keyId },
|
||||||
|
url = "https://api.github.com/users/${user.login}.gpg",
|
||||||
|
),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override suspend fun rateLimit(): BackendRateLimit {
|
||||||
|
val rateLimit: GitHubRateLimit = client.get(RateLimit()).body()
|
||||||
|
|
||||||
|
return BackendRateLimit(
|
||||||
|
limit = rateLimit.rate.limit,
|
||||||
|
remaining = rateLimit.rate.remaining,
|
||||||
|
reset = Instant.fromEpochSeconds(rateLimit.rate.reset).toLocalDateTime(TimeZone.UTC),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
interface IGitHubUser {
|
||||||
|
val login: String
|
||||||
|
val avatarUrl: String
|
||||||
|
val htmlUrl: String
|
||||||
|
}
|
||||||
|
|
||||||
|
@Serializable
|
||||||
|
class GitHubUser(
|
||||||
|
override val login: String,
|
||||||
|
override val avatarUrl: String,
|
||||||
|
override val htmlUrl: String,
|
||||||
|
val bio: String?,
|
||||||
|
) : IGitHubUser {
|
||||||
|
@Serializable
|
||||||
|
class GitHubGpgKey(
|
||||||
|
val keyId: String,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
class GitHubOrganization {
|
||||||
|
@Serializable
|
||||||
|
class GitHubMember(
|
||||||
|
override val login: String,
|
||||||
|
override val avatarUrl: String,
|
||||||
|
override val htmlUrl: String,
|
||||||
|
) : IGitHubUser
|
||||||
|
|
||||||
|
class GitHubRepository {
|
||||||
|
@Serializable
|
||||||
|
class GitHubContributor(
|
||||||
|
override val login: String,
|
||||||
|
override val avatarUrl: String,
|
||||||
|
override val htmlUrl: String,
|
||||||
|
val contributions: Int,
|
||||||
|
) : IGitHubUser
|
||||||
|
|
||||||
|
@Serializable
|
||||||
|
class GitHubRelease(
|
||||||
|
val tagName: String,
|
||||||
|
// Using a list instead of a set because set semantics are unnecessary here.
|
||||||
|
val assets: List<GitHubAsset>,
|
||||||
|
val createdAt: Instant,
|
||||||
|
val body: String,
|
||||||
|
) {
|
||||||
|
@Serializable
|
||||||
|
class GitHubAsset(
|
||||||
|
val name: String,
|
||||||
|
val browserDownloadUrl: String,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Serializable
|
||||||
|
class GitHubRateLimit(
|
||||||
|
val rate: Rate,
|
||||||
|
) {
|
||||||
|
@Serializable
|
||||||
|
class Rate(
|
||||||
|
val limit: Int,
|
||||||
|
val remaining: Int,
|
||||||
|
val reset: Long,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Resource("/users/{login}")
|
||||||
|
class User(val login: String) {
|
||||||
|
@Resource("/users/{login}/gpg_keys")
|
||||||
|
class GpgKeys(val login: String)
|
||||||
|
}
|
||||||
|
|
||||||
|
class Organization {
|
||||||
|
@Resource("/orgs/{org}/members")
|
||||||
|
class Members(val org: String)
|
||||||
|
|
||||||
|
class Repository {
|
||||||
|
@Resource("/repos/{owner}/{repo}/contributors")
|
||||||
|
class Contributors(val owner: String, val repo: String)
|
||||||
|
|
||||||
|
@Resource("/repos/{owner}/{repo}/releases")
|
||||||
|
class Releases(val owner: String, val repo: String) {
|
||||||
|
@Resource("/repos/{owner}/{repo}/releases/tags/{tag}")
|
||||||
|
class Tag(val owner: String, val repo: String, val tag: String)
|
||||||
|
|
||||||
|
@Resource("/repos/{owner}/{repo}/releases/latest")
|
||||||
|
class Latest(val owner: String, val repo: String)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Resource("/rate_limit")
|
||||||
|
class RateLimit
|
@ -0,0 +1,390 @@
|
|||||||
|
package app.revanced.api.configuration.routes
|
||||||
|
|
||||||
|
import app.revanced.api.configuration.installCache
|
||||||
|
import app.revanced.api.configuration.installNotarizedRoute
|
||||||
|
import app.revanced.api.configuration.respondOrNotFound
|
||||||
|
import app.revanced.api.configuration.schema.APIAnnouncement
|
||||||
|
import app.revanced.api.configuration.schema.APIAnnouncementArchivedAt
|
||||||
|
import app.revanced.api.configuration.schema.APIResponseAnnouncement
|
||||||
|
import app.revanced.api.configuration.schema.APIResponseAnnouncementId
|
||||||
|
import app.revanced.api.configuration.services.AnnouncementService
|
||||||
|
import io.bkbn.kompendium.core.metadata.DeleteInfo
|
||||||
|
import io.bkbn.kompendium.core.metadata.GetInfo
|
||||||
|
import io.bkbn.kompendium.core.metadata.PatchInfo
|
||||||
|
import io.bkbn.kompendium.core.metadata.PostInfo
|
||||||
|
import io.bkbn.kompendium.json.schema.definition.TypeDefinition
|
||||||
|
import io.bkbn.kompendium.oas.payload.Parameter
|
||||||
|
import io.ktor.http.*
|
||||||
|
import io.ktor.server.application.*
|
||||||
|
import io.ktor.server.auth.*
|
||||||
|
import io.ktor.server.plugins.ratelimit.*
|
||||||
|
import io.ktor.server.request.*
|
||||||
|
import io.ktor.server.response.*
|
||||||
|
import io.ktor.server.routing.*
|
||||||
|
import io.ktor.server.util.*
|
||||||
|
import kotlin.time.Duration.Companion.minutes
|
||||||
|
import org.koin.ktor.ext.get as koinGet
|
||||||
|
|
||||||
|
internal fun Route.announcementsRoute() = route("announcements") {
|
||||||
|
val announcementService = koinGet<AnnouncementService>()
|
||||||
|
|
||||||
|
installCache(5.minutes)
|
||||||
|
|
||||||
|
installAnnouncementsRouteDocumentation()
|
||||||
|
|
||||||
|
rateLimit(RateLimitName("strong")) {
|
||||||
|
get {
|
||||||
|
call.respond(announcementService.all())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
rateLimit(RateLimitName("strong")) {
|
||||||
|
route("{channel}/latest") {
|
||||||
|
installLatestChannelAnnouncementRouteDocumentation()
|
||||||
|
|
||||||
|
get {
|
||||||
|
val channel: String by call.parameters
|
||||||
|
|
||||||
|
call.respondOrNotFound(announcementService.latest(channel))
|
||||||
|
}
|
||||||
|
|
||||||
|
route("id") {
|
||||||
|
installLatestChannelAnnouncementIdRouteDocumentation()
|
||||||
|
|
||||||
|
get {
|
||||||
|
val channel: String by call.parameters
|
||||||
|
|
||||||
|
call.respondOrNotFound(announcementService.latestId(channel))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
rateLimit(RateLimitName("strong")) {
|
||||||
|
route("{channel}") {
|
||||||
|
installChannelAnnouncementsRouteDocumentation()
|
||||||
|
|
||||||
|
get {
|
||||||
|
val channel: String by call.parameters
|
||||||
|
|
||||||
|
call.respond(announcementService.all(channel))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
rateLimit(RateLimitName("strong")) {
|
||||||
|
route("latest") {
|
||||||
|
installLatestAnnouncementRouteDocumentation()
|
||||||
|
|
||||||
|
get {
|
||||||
|
call.respondOrNotFound(announcementService.latest())
|
||||||
|
}
|
||||||
|
|
||||||
|
route("id") {
|
||||||
|
installLatestAnnouncementIdRouteDocumentation()
|
||||||
|
|
||||||
|
get {
|
||||||
|
call.respondOrNotFound(announcementService.latestId())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
rateLimit(RateLimitName("strong")) {
|
||||||
|
authenticate("jwt") {
|
||||||
|
installAnnouncementRouteDocumentation()
|
||||||
|
|
||||||
|
post<APIAnnouncement> { announcement ->
|
||||||
|
announcementService.new(announcement)
|
||||||
|
}
|
||||||
|
|
||||||
|
route("{id}") {
|
||||||
|
installAnnouncementIdRouteDocumentation()
|
||||||
|
|
||||||
|
patch<APIAnnouncement> { announcement ->
|
||||||
|
val id: Int by call.parameters
|
||||||
|
|
||||||
|
announcementService.update(id, announcement)
|
||||||
|
}
|
||||||
|
|
||||||
|
delete {
|
||||||
|
val id: Int by call.parameters
|
||||||
|
|
||||||
|
announcementService.delete(id)
|
||||||
|
}
|
||||||
|
|
||||||
|
route("archive") {
|
||||||
|
installAnnouncementArchiveRouteDocumentation()
|
||||||
|
|
||||||
|
post {
|
||||||
|
val id: Int by call.parameters
|
||||||
|
val archivedAt = call.receiveNullable<APIAnnouncementArchivedAt>()?.archivedAt
|
||||||
|
|
||||||
|
announcementService.archive(id, archivedAt)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
route("unarchive") {
|
||||||
|
installAnnouncementUnarchiveRouteDocumentation()
|
||||||
|
|
||||||
|
post {
|
||||||
|
val id: Int by call.parameters
|
||||||
|
|
||||||
|
announcementService.unarchive(id)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun Route.installAnnouncementRouteDocumentation() = installNotarizedRoute {
|
||||||
|
tags = setOf("Announcements")
|
||||||
|
|
||||||
|
post = PostInfo.builder {
|
||||||
|
description("Create a new announcement")
|
||||||
|
summary("Create announcement")
|
||||||
|
request {
|
||||||
|
requestType<APIAnnouncement>()
|
||||||
|
description("The new announcement")
|
||||||
|
}
|
||||||
|
response {
|
||||||
|
description("When the announcement was created")
|
||||||
|
responseCode(HttpStatusCode.OK)
|
||||||
|
responseType<Unit>()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun Route.installLatestAnnouncementRouteDocumentation() = installNotarizedRoute {
|
||||||
|
tags = setOf("Announcements")
|
||||||
|
|
||||||
|
get = GetInfo.builder {
|
||||||
|
description("Get the latest announcement")
|
||||||
|
summary("Get latest announcement")
|
||||||
|
response {
|
||||||
|
responseCode(HttpStatusCode.OK)
|
||||||
|
mediaTypes("application/json")
|
||||||
|
description("The latest announcement")
|
||||||
|
responseType<APIResponseAnnouncement>()
|
||||||
|
}
|
||||||
|
canRespond {
|
||||||
|
responseCode(HttpStatusCode.NotFound)
|
||||||
|
description("No announcement exists")
|
||||||
|
responseType<Unit>()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun Route.installLatestAnnouncementIdRouteDocumentation() = installNotarizedRoute {
|
||||||
|
tags = setOf("Announcements")
|
||||||
|
|
||||||
|
get = GetInfo.builder {
|
||||||
|
description("Get the id of the latest announcement")
|
||||||
|
summary("Get id of latest announcement")
|
||||||
|
response {
|
||||||
|
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(
|
||||||
|
name = "channel",
|
||||||
|
`in` = Parameter.Location.path,
|
||||||
|
schema = TypeDefinition.STRING,
|
||||||
|
description = "The channel to get the announcements from",
|
||||||
|
required = true,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|
||||||
|
get = GetInfo.builder {
|
||||||
|
description("Get the announcements from a channel")
|
||||||
|
summary("Get announcements from channel")
|
||||||
|
response {
|
||||||
|
responseCode(HttpStatusCode.OK)
|
||||||
|
mediaTypes("application/json")
|
||||||
|
description("The announcements in the channel")
|
||||||
|
responseType<Set<APIResponseAnnouncement>>()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun Route.installAnnouncementArchiveRouteDocumentation() = installNotarizedRoute {
|
||||||
|
tags = setOf("Announcements")
|
||||||
|
|
||||||
|
parameters = listOf(
|
||||||
|
Parameter(
|
||||||
|
name = "id",
|
||||||
|
`in` = Parameter.Location.path,
|
||||||
|
schema = TypeDefinition.INT,
|
||||||
|
description = "The id of the announcement to archive",
|
||||||
|
required = true,
|
||||||
|
),
|
||||||
|
Parameter(
|
||||||
|
name = "archivedAt",
|
||||||
|
`in` = Parameter.Location.query,
|
||||||
|
schema = TypeDefinition.STRING,
|
||||||
|
description = "The date and time the announcement to be archived",
|
||||||
|
required = false,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|
||||||
|
post = PostInfo.builder {
|
||||||
|
description("Archive an announcement")
|
||||||
|
summary("Archive announcement")
|
||||||
|
response {
|
||||||
|
description("When the announcement was archived")
|
||||||
|
responseCode(HttpStatusCode.OK)
|
||||||
|
responseType<Unit>()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun Route.installAnnouncementUnarchiveRouteDocumentation() = installNotarizedRoute {
|
||||||
|
tags = setOf("Announcements")
|
||||||
|
|
||||||
|
parameters = listOf(
|
||||||
|
Parameter(
|
||||||
|
name = "id",
|
||||||
|
`in` = Parameter.Location.path,
|
||||||
|
schema = TypeDefinition.INT,
|
||||||
|
description = "The id of the announcement to unarchive",
|
||||||
|
required = true,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|
||||||
|
post = PostInfo.builder {
|
||||||
|
description("Unarchive an announcement")
|
||||||
|
summary("Unarchive announcement")
|
||||||
|
response {
|
||||||
|
description("When announcement was unarchived")
|
||||||
|
responseCode(HttpStatusCode.OK)
|
||||||
|
responseType<Unit>()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun Route.installAnnouncementIdRouteDocumentation() = installNotarizedRoute {
|
||||||
|
tags = setOf("Announcements")
|
||||||
|
|
||||||
|
parameters = listOf(
|
||||||
|
Parameter(
|
||||||
|
name = "id",
|
||||||
|
`in` = Parameter.Location.path,
|
||||||
|
schema = TypeDefinition.INT,
|
||||||
|
description = "The id of the announcement to update",
|
||||||
|
required = true,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|
||||||
|
patch = PatchInfo.builder {
|
||||||
|
description("Update an announcement")
|
||||||
|
summary("Update announcement")
|
||||||
|
request {
|
||||||
|
requestType<APIAnnouncement>()
|
||||||
|
description("The new announcement")
|
||||||
|
}
|
||||||
|
response {
|
||||||
|
description("When announcement was updated")
|
||||||
|
responseCode(HttpStatusCode.OK)
|
||||||
|
responseType<Unit>()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
delete = DeleteInfo.builder {
|
||||||
|
description("Delete an announcement")
|
||||||
|
summary("Delete announcement")
|
||||||
|
response {
|
||||||
|
description("When the announcement was deleted")
|
||||||
|
responseCode(HttpStatusCode.OK)
|
||||||
|
responseType<Unit>()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun Route.installAnnouncementsRouteDocumentation() = installNotarizedRoute {
|
||||||
|
tags = setOf("Announcements")
|
||||||
|
|
||||||
|
get = GetInfo.builder {
|
||||||
|
description("Get the announcements")
|
||||||
|
summary("Get announcements")
|
||||||
|
response {
|
||||||
|
responseCode(HttpStatusCode.OK)
|
||||||
|
mediaTypes("application/json")
|
||||||
|
description("The announcements")
|
||||||
|
responseType<Set<APIResponseAnnouncement>>()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun Route.installLatestChannelAnnouncementRouteDocumentation() = installNotarizedRoute {
|
||||||
|
tags = setOf("Announcements")
|
||||||
|
|
||||||
|
parameters = listOf(
|
||||||
|
Parameter(
|
||||||
|
name = "channel",
|
||||||
|
`in` = Parameter.Location.path,
|
||||||
|
schema = TypeDefinition.STRING,
|
||||||
|
description = "The channel to get the latest announcement from",
|
||||||
|
required = true,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|
||||||
|
get = GetInfo.builder {
|
||||||
|
description("Get the latest announcement from a channel")
|
||||||
|
summary("Get latest channel announcement")
|
||||||
|
response {
|
||||||
|
responseCode(HttpStatusCode.OK)
|
||||||
|
mediaTypes("application/json")
|
||||||
|
description("The latest announcement in the channel")
|
||||||
|
responseType<APIResponseAnnouncement>()
|
||||||
|
}
|
||||||
|
canRespond {
|
||||||
|
responseCode(HttpStatusCode.NotFound)
|
||||||
|
description("The channel does not exist")
|
||||||
|
responseType<Unit>()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun Route.installLatestChannelAnnouncementIdRouteDocumentation() = installNotarizedRoute {
|
||||||
|
tags = setOf("Announcements")
|
||||||
|
|
||||||
|
parameters = listOf(
|
||||||
|
Parameter(
|
||||||
|
name = "channel",
|
||||||
|
`in` = Parameter.Location.path,
|
||||||
|
schema = TypeDefinition.STRING,
|
||||||
|
description = "The channel to get the latest announcement id from",
|
||||||
|
required = true,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|
||||||
|
get = GetInfo.builder {
|
||||||
|
description("Get the id of the latest announcement from a channel")
|
||||||
|
summary("Get id of latest announcement from channel")
|
||||||
|
response {
|
||||||
|
responseCode(HttpStatusCode.OK)
|
||||||
|
mediaTypes("application/json")
|
||||||
|
description("The id of the latest announcement from the channel")
|
||||||
|
responseType<APIResponseAnnouncementId>()
|
||||||
|
}
|
||||||
|
canRespond {
|
||||||
|
responseCode(HttpStatusCode.NotFound)
|
||||||
|
description("The channel does not exist")
|
||||||
|
responseType<Unit>()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,157 @@
|
|||||||
|
package app.revanced.api.configuration.routes
|
||||||
|
|
||||||
|
import app.revanced.api.configuration.installCache
|
||||||
|
import app.revanced.api.configuration.installNoCache
|
||||||
|
import app.revanced.api.configuration.installNotarizedRoute
|
||||||
|
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.AuthService
|
||||||
|
import io.bkbn.kompendium.core.metadata.*
|
||||||
|
import io.ktor.http.*
|
||||||
|
import io.ktor.server.application.*
|
||||||
|
import io.ktor.server.auth.*
|
||||||
|
import io.ktor.server.http.content.*
|
||||||
|
import io.ktor.server.plugins.ratelimit.*
|
||||||
|
import io.ktor.server.response.*
|
||||||
|
import io.ktor.server.routing.*
|
||||||
|
import kotlin.time.Duration.Companion.days
|
||||||
|
import org.koin.ktor.ext.get as koinGet
|
||||||
|
|
||||||
|
internal fun Route.rootRoute() {
|
||||||
|
val apiService = koinGet<ApiService>()
|
||||||
|
val authService = koinGet<AuthService>()
|
||||||
|
|
||||||
|
rateLimit(RateLimitName("strong")) {
|
||||||
|
authenticate("auth-digest") {
|
||||||
|
route("token") {
|
||||||
|
installTokenRouteDocumentation()
|
||||||
|
|
||||||
|
get {
|
||||||
|
call.respond(authService.newToken())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
route("contributors") {
|
||||||
|
installCache(1.days)
|
||||||
|
|
||||||
|
installContributorsRouteDocumentation()
|
||||||
|
|
||||||
|
get {
|
||||||
|
call.respond(apiService.contributors())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
route("team") {
|
||||||
|
installCache(1.days)
|
||||||
|
|
||||||
|
installTeamRouteDocumentation()
|
||||||
|
|
||||||
|
get {
|
||||||
|
call.respond(apiService.team())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
route("ping") {
|
||||||
|
installNoCache()
|
||||||
|
|
||||||
|
installPingRouteDocumentation()
|
||||||
|
|
||||||
|
head {
|
||||||
|
call.respond(HttpStatusCode.NoContent)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
rateLimit(RateLimitName("weak")) {
|
||||||
|
route("backend/rate_limit") {
|
||||||
|
installRateLimitRouteDocumentation()
|
||||||
|
|
||||||
|
get {
|
||||||
|
call.respondOrNotFound(apiService.rateLimit())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
staticResources("/", "/app/revanced/api/static") {
|
||||||
|
contentType { ContentType.Application.Json }
|
||||||
|
extensions("json")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun Route.installRateLimitRouteDocumentation() = installNotarizedRoute {
|
||||||
|
tags = setOf("API")
|
||||||
|
|
||||||
|
get = GetInfo.builder {
|
||||||
|
description("Get the rate limit of the backend")
|
||||||
|
summary("Get rate limit of backend")
|
||||||
|
response {
|
||||||
|
description("The rate limit of the backend")
|
||||||
|
mediaTypes("application/json")
|
||||||
|
responseCode(HttpStatusCode.OK)
|
||||||
|
responseType<APIRateLimit>()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun Route.installPingRouteDocumentation() = installNotarizedRoute {
|
||||||
|
tags = setOf("API")
|
||||||
|
|
||||||
|
head = HeadInfo.builder {
|
||||||
|
description("Ping the server")
|
||||||
|
summary("Ping")
|
||||||
|
response {
|
||||||
|
description("The server is reachable")
|
||||||
|
responseCode(HttpStatusCode.NoContent)
|
||||||
|
responseType<Unit>()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun Route.installTeamRouteDocumentation() = installNotarizedRoute {
|
||||||
|
tags = setOf("API")
|
||||||
|
|
||||||
|
get = GetInfo.builder {
|
||||||
|
description("Get the list of team members")
|
||||||
|
summary("Get team members")
|
||||||
|
response {
|
||||||
|
description("The list of team members")
|
||||||
|
mediaTypes("application/json")
|
||||||
|
responseCode(HttpStatusCode.OK)
|
||||||
|
responseType<Set<APIMember>>()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun Route.installContributorsRouteDocumentation() = installNotarizedRoute {
|
||||||
|
tags = setOf("API")
|
||||||
|
|
||||||
|
get = GetInfo.builder {
|
||||||
|
description("Get the list of contributors")
|
||||||
|
summary("Get contributors")
|
||||||
|
response {
|
||||||
|
description("The list of contributors")
|
||||||
|
mediaTypes("application/json")
|
||||||
|
responseCode(HttpStatusCode.OK)
|
||||||
|
responseType<Set<APIContributable>>()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun Route.installTokenRouteDocumentation() = installNotarizedRoute {
|
||||||
|
tags = setOf("API")
|
||||||
|
|
||||||
|
get = GetInfo.builder {
|
||||||
|
description("Get a new authorization token")
|
||||||
|
summary("Get authorization token")
|
||||||
|
response {
|
||||||
|
description("The authorization token")
|
||||||
|
mediaTypes("application/json")
|
||||||
|
responseCode(HttpStatusCode.OK)
|
||||||
|
responseType<String>()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,19 @@
|
|||||||
|
package app.revanced.api.configuration.routes
|
||||||
|
|
||||||
|
import app.revanced.api.configuration.services.OldApiService
|
||||||
|
import io.ktor.server.application.*
|
||||||
|
import io.ktor.server.plugins.ratelimit.*
|
||||||
|
import io.ktor.server.routing.*
|
||||||
|
import org.koin.ktor.ext.get
|
||||||
|
|
||||||
|
internal fun Route.oldApiRoute() {
|
||||||
|
val oldApiService = get<OldApiService>()
|
||||||
|
|
||||||
|
rateLimit(RateLimitName("weak")) {
|
||||||
|
route(Regex("/(v2|tools|contributors).*")) {
|
||||||
|
handle {
|
||||||
|
oldApiService.proxy(call)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,120 @@
|
|||||||
|
package app.revanced.api.configuration.routes
|
||||||
|
|
||||||
|
import app.revanced.api.configuration.installCache
|
||||||
|
import app.revanced.api.configuration.installNotarizedRoute
|
||||||
|
import app.revanced.api.configuration.schema.APIAssetPublicKeys
|
||||||
|
import app.revanced.api.configuration.schema.APIRelease
|
||||||
|
import app.revanced.api.configuration.schema.APIReleaseVersion
|
||||||
|
import app.revanced.api.configuration.services.PatchesService
|
||||||
|
import io.bkbn.kompendium.core.metadata.GetInfo
|
||||||
|
import io.ktor.http.*
|
||||||
|
import io.ktor.server.application.*
|
||||||
|
import io.ktor.server.plugins.ratelimit.*
|
||||||
|
import io.ktor.server.response.*
|
||||||
|
import io.ktor.server.routing.*
|
||||||
|
import kotlin.time.Duration.Companion.days
|
||||||
|
import org.koin.ktor.ext.get as koinGet
|
||||||
|
|
||||||
|
internal fun Route.patchesRoute() = route("patches") {
|
||||||
|
val patchesService = koinGet<PatchesService>()
|
||||||
|
|
||||||
|
route("latest") {
|
||||||
|
installLatestPatchesRouteDocumentation()
|
||||||
|
|
||||||
|
rateLimit(RateLimitName("weak")) {
|
||||||
|
get {
|
||||||
|
call.respond(patchesService.latestRelease())
|
||||||
|
}
|
||||||
|
|
||||||
|
route("version") {
|
||||||
|
installLatestPatchesVersionRouteDocumentation()
|
||||||
|
|
||||||
|
get {
|
||||||
|
call.respond(patchesService.latestVersion())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
rateLimit(RateLimitName("strong")) {
|
||||||
|
route("list") {
|
||||||
|
installLatestPatchesListRouteDocumentation()
|
||||||
|
|
||||||
|
get {
|
||||||
|
call.respondBytes(ContentType.Application.Json) { patchesService.list() }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
rateLimit(RateLimitName("strong")) {
|
||||||
|
route("keys") {
|
||||||
|
installCache(356.days)
|
||||||
|
|
||||||
|
installPatchesPublicKeyRouteDocumentation()
|
||||||
|
|
||||||
|
get {
|
||||||
|
call.respond(patchesService.publicKeys())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun Route.installLatestPatchesRouteDocumentation() = installNotarizedRoute {
|
||||||
|
tags = setOf("Patches")
|
||||||
|
|
||||||
|
get = GetInfo.builder {
|
||||||
|
description("Get the latest patches release")
|
||||||
|
summary("Get latest patches release")
|
||||||
|
response {
|
||||||
|
description("The latest patches release")
|
||||||
|
mediaTypes("application/json")
|
||||||
|
responseCode(HttpStatusCode.OK)
|
||||||
|
responseType<APIRelease>()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun Route.installLatestPatchesVersionRouteDocumentation() = installNotarizedRoute {
|
||||||
|
tags = setOf("Patches")
|
||||||
|
|
||||||
|
get = GetInfo.builder {
|
||||||
|
description("Get the latest patches release version")
|
||||||
|
summary("Get latest patches release version")
|
||||||
|
response {
|
||||||
|
description("The latest patches release version")
|
||||||
|
mediaTypes("application/json")
|
||||||
|
responseCode(HttpStatusCode.OK)
|
||||||
|
responseType<APIReleaseVersion>()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun Route.installLatestPatchesListRouteDocumentation() = installNotarizedRoute {
|
||||||
|
tags = setOf("Patches")
|
||||||
|
|
||||||
|
get = GetInfo.builder {
|
||||||
|
description("Get the list of patches from the latest patches release")
|
||||||
|
summary("Get list of patches from latest patches release")
|
||||||
|
response {
|
||||||
|
description("The list of patches")
|
||||||
|
mediaTypes("application/json")
|
||||||
|
responseCode(HttpStatusCode.OK)
|
||||||
|
responseType<String>()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun Route.installPatchesPublicKeyRouteDocumentation() = installNotarizedRoute {
|
||||||
|
tags = setOf("Patches")
|
||||||
|
|
||||||
|
get = GetInfo.builder {
|
||||||
|
description("Get the public keys for verifying patches and integrations assets")
|
||||||
|
summary("Get patches and integrations public keys")
|
||||||
|
response {
|
||||||
|
description("The public keys")
|
||||||
|
mediaTypes("application/json")
|
||||||
|
responseCode(HttpStatusCode.OK)
|
||||||
|
responseType<APIAssetPublicKeys>()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,116 @@
|
|||||||
|
package app.revanced.api.configuration.schema
|
||||||
|
|
||||||
|
import kotlinx.datetime.LocalDateTime
|
||||||
|
import kotlinx.serialization.Serializable
|
||||||
|
|
||||||
|
@Serializable
|
||||||
|
class APIRelease(
|
||||||
|
val version: String,
|
||||||
|
val createdAt: LocalDateTime,
|
||||||
|
val description: String,
|
||||||
|
// Using a list instead of a set because set semantics are unnecessary here.
|
||||||
|
val assets: List<APIAsset>,
|
||||||
|
)
|
||||||
|
|
||||||
|
interface APIUser {
|
||||||
|
val name: String
|
||||||
|
val avatarUrl: String
|
||||||
|
val url: String
|
||||||
|
}
|
||||||
|
|
||||||
|
@Serializable
|
||||||
|
class APIMember(
|
||||||
|
override val name: String,
|
||||||
|
override val avatarUrl: String,
|
||||||
|
override val url: String,
|
||||||
|
val gpgKey: APIGpgKey?,
|
||||||
|
) : APIUser
|
||||||
|
|
||||||
|
@Serializable
|
||||||
|
class APIGpgKey(
|
||||||
|
val id: String,
|
||||||
|
val url: String,
|
||||||
|
)
|
||||||
|
|
||||||
|
@Serializable
|
||||||
|
class APIContributor(
|
||||||
|
override val name: String,
|
||||||
|
override val avatarUrl: String,
|
||||||
|
override val url: String,
|
||||||
|
val contributions: Int,
|
||||||
|
) : APIUser
|
||||||
|
|
||||||
|
@Serializable
|
||||||
|
class APIContributable(
|
||||||
|
val name: String,
|
||||||
|
// Using a list instead of a set because set semantics are unnecessary here.
|
||||||
|
val contributors: List<APIContributor>,
|
||||||
|
)
|
||||||
|
|
||||||
|
@Serializable
|
||||||
|
class APIAsset(
|
||||||
|
val downloadUrl: String,
|
||||||
|
val signatureDownloadUrl: String,
|
||||||
|
// TODO: Remove this eventually when integrations are merged into patches.
|
||||||
|
val name: APIAssetName,
|
||||||
|
)
|
||||||
|
|
||||||
|
@Serializable
|
||||||
|
enum class APIAssetName {
|
||||||
|
PATCHES,
|
||||||
|
INTEGRATION,
|
||||||
|
}
|
||||||
|
|
||||||
|
@Serializable
|
||||||
|
class APIReleaseVersion(
|
||||||
|
val version: String,
|
||||||
|
)
|
||||||
|
|
||||||
|
@Serializable
|
||||||
|
class APIAnnouncement(
|
||||||
|
val author: String? = null,
|
||||||
|
val title: String,
|
||||||
|
val content: String? = null,
|
||||||
|
// Using a list instead of a set because set semantics are unnecessary here.
|
||||||
|
val attachmentUrls: List<String> = emptyList(),
|
||||||
|
val channel: String? = null,
|
||||||
|
val archivedAt: LocalDateTime? = null,
|
||||||
|
val level: Int = 0,
|
||||||
|
)
|
||||||
|
|
||||||
|
@Serializable
|
||||||
|
class APIResponseAnnouncement(
|
||||||
|
val id: Int,
|
||||||
|
val author: String? = null,
|
||||||
|
val title: String,
|
||||||
|
val content: String? = null,
|
||||||
|
// Using a list instead of a set because set semantics are unnecessary here.
|
||||||
|
val attachmentUrls: List<String> = emptyList(),
|
||||||
|
val channel: String? = null,
|
||||||
|
val createdAt: LocalDateTime,
|
||||||
|
val archivedAt: LocalDateTime? = null,
|
||||||
|
val level: Int = 0,
|
||||||
|
)
|
||||||
|
|
||||||
|
@Serializable
|
||||||
|
class APIResponseAnnouncementId(
|
||||||
|
val id: Int,
|
||||||
|
)
|
||||||
|
|
||||||
|
@Serializable
|
||||||
|
class APIAnnouncementArchivedAt(
|
||||||
|
val archivedAt: LocalDateTime,
|
||||||
|
)
|
||||||
|
|
||||||
|
@Serializable
|
||||||
|
class APIRateLimit(
|
||||||
|
val limit: Int,
|
||||||
|
val remaining: Int,
|
||||||
|
val reset: LocalDateTime,
|
||||||
|
)
|
||||||
|
|
||||||
|
@Serializable
|
||||||
|
class APIAssetPublicKeys(
|
||||||
|
val patchesPublicKey: String,
|
||||||
|
val integrationsPublicKey: String,
|
||||||
|
)
|
@ -0,0 +1,35 @@
|
|||||||
|
package app.revanced.api.configuration.services
|
||||||
|
|
||||||
|
import app.revanced.api.configuration.repository.AnnouncementRepository
|
||||||
|
import app.revanced.api.configuration.schema.APIAnnouncement
|
||||||
|
import app.revanced.api.configuration.schema.APIResponseAnnouncementId
|
||||||
|
import kotlinx.datetime.LocalDateTime
|
||||||
|
|
||||||
|
internal class AnnouncementService(
|
||||||
|
private val announcementRepository: AnnouncementRepository,
|
||||||
|
) {
|
||||||
|
fun latestId(channel: String): APIResponseAnnouncementId? = announcementRepository.latestId(channel)
|
||||||
|
fun latestId(): APIResponseAnnouncementId? = announcementRepository.latestId()
|
||||||
|
|
||||||
|
fun latest(channel: String) = announcementRepository.latest(channel)
|
||||||
|
fun latest() = announcementRepository.latest()
|
||||||
|
|
||||||
|
suspend fun all(channel: String) = announcementRepository.all(channel)
|
||||||
|
suspend fun all() = announcementRepository.all()
|
||||||
|
|
||||||
|
suspend fun new(new: APIAnnouncement) {
|
||||||
|
announcementRepository.new(new)
|
||||||
|
}
|
||||||
|
suspend fun archive(id: Int, archivedAt: LocalDateTime?) {
|
||||||
|
announcementRepository.archive(id, archivedAt)
|
||||||
|
}
|
||||||
|
suspend fun unarchive(id: Int) {
|
||||||
|
announcementRepository.unarchive(id)
|
||||||
|
}
|
||||||
|
suspend fun update(id: Int, new: APIAnnouncement) {
|
||||||
|
announcementRepository.update(id, new)
|
||||||
|
}
|
||||||
|
suspend fun delete(id: Int) {
|
||||||
|
announcementRepository.delete(id)
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,49 @@
|
|||||||
|
package app.revanced.api.configuration.services
|
||||||
|
|
||||||
|
import app.revanced.api.configuration.repository.BackendRepository
|
||||||
|
import app.revanced.api.configuration.repository.ConfigurationRepository
|
||||||
|
import app.revanced.api.configuration.schema.*
|
||||||
|
import kotlinx.coroutines.Dispatchers
|
||||||
|
import kotlinx.coroutines.async
|
||||||
|
import kotlinx.coroutines.awaitAll
|
||||||
|
import kotlinx.coroutines.withContext
|
||||||
|
|
||||||
|
internal class ApiService(
|
||||||
|
private val backendRepository: BackendRepository,
|
||||||
|
private val configurationRepository: ConfigurationRepository,
|
||||||
|
) {
|
||||||
|
suspend fun contributors() = withContext(Dispatchers.IO) {
|
||||||
|
configurationRepository.contributorsRepositoryNames.map {
|
||||||
|
async {
|
||||||
|
APIContributable(
|
||||||
|
it,
|
||||||
|
backendRepository.contributors(configurationRepository.organization, it).map {
|
||||||
|
APIContributor(it.name, it.avatarUrl, it.url, it.contributions)
|
||||||
|
},
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}.awaitAll()
|
||||||
|
|
||||||
|
suspend fun team() = backendRepository.members(configurationRepository.organization).map { member ->
|
||||||
|
APIMember(
|
||||||
|
member.name,
|
||||||
|
member.avatarUrl,
|
||||||
|
member.url,
|
||||||
|
if (member.gpgKeys.ids.isNotEmpty()) {
|
||||||
|
APIGpgKey(
|
||||||
|
// Must choose one of the GPG keys, because it does not make sense to have multiple GPG keys for the API.
|
||||||
|
member.gpgKeys.ids.first(),
|
||||||
|
member.gpgKeys.url,
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
null
|
||||||
|
},
|
||||||
|
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
suspend fun rateLimit() = backendRepository.rateLimit()?.let {
|
||||||
|
APIRateLimit(it.limit, it.remaining, it.reset)
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,49 @@
|
|||||||
|
package app.revanced.api.configuration.services
|
||||||
|
|
||||||
|
import com.auth0.jwt.JWT
|
||||||
|
import com.auth0.jwt.algorithms.Algorithm
|
||||||
|
import io.ktor.server.application.*
|
||||||
|
import io.ktor.server.auth.*
|
||||||
|
import io.ktor.server.auth.jwt.*
|
||||||
|
import java.util.*
|
||||||
|
import kotlin.text.HexFormat
|
||||||
|
import kotlin.time.Duration.Companion.minutes
|
||||||
|
|
||||||
|
internal class AuthService private constructor(
|
||||||
|
private val issuer: String,
|
||||||
|
private val validityInMin: Int,
|
||||||
|
private val jwtSecret: String,
|
||||||
|
private val authSHA256Digest: ByteArray,
|
||||||
|
) {
|
||||||
|
@OptIn(ExperimentalStdlibApi::class)
|
||||||
|
constructor(issuer: String, validityInMin: Int, jwtSecret: String, authSHA256DigestString: String) : this(
|
||||||
|
issuer,
|
||||||
|
validityInMin,
|
||||||
|
jwtSecret,
|
||||||
|
authSHA256DigestString.hexToByteArray(HexFormat.Default),
|
||||||
|
)
|
||||||
|
|
||||||
|
val configureSecurity: Application.() -> Unit = {
|
||||||
|
install(Authentication) {
|
||||||
|
jwt("jwt") {
|
||||||
|
realm = "ReVanced"
|
||||||
|
|
||||||
|
verifier(JWT.require(Algorithm.HMAC256(jwtSecret)).withIssuer(issuer).build())
|
||||||
|
}
|
||||||
|
|
||||||
|
digest("auth-digest") {
|
||||||
|
realm = "ReVanced"
|
||||||
|
algorithmName = "SHA-256"
|
||||||
|
|
||||||
|
digestProvider { _, _ ->
|
||||||
|
authSHA256Digest
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun newToken(): String = JWT.create()
|
||||||
|
.withIssuer(issuer)
|
||||||
|
.withExpiresAt(Date(System.currentTimeMillis() + validityInMin.minutes.inWholeMilliseconds))
|
||||||
|
.sign(Algorithm.HMAC256(jwtSecret))
|
||||||
|
}
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
x
Reference in New Issue
Block a user