mirror of
https://github.com/revanced/revanced-api.git
synced 2025-04-29 14:14:29 +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
|
||||
__pycache__/
|
||||
*.py[cod]
|
||||
*$py.class
|
||||
|
||||
# C extensions
|
||||
*.so
|
||||
|
||||
# Distribution / packaging
|
||||
.Python
|
||||
.gradle
|
||||
build/
|
||||
develop-eggs/
|
||||
dist/
|
||||
downloads/
|
||||
eggs/
|
||||
.eggs/
|
||||
lib/
|
||||
lib64/
|
||||
parts/
|
||||
sdist/
|
||||
var/
|
||||
wheels/
|
||||
share/python-wheels/
|
||||
*.egg-info/
|
||||
.installed.cfg
|
||||
*.egg
|
||||
MANIFEST
|
||||
!gradle/wrapper/gradle-wrapper.jar
|
||||
!**/src/main/**/build/
|
||||
!**/src/test/**/build/
|
||||
|
||||
# PyInstaller
|
||||
# Usually these files are written by a python script from a template
|
||||
# before PyInstaller builds the exe, so as to inject date/other infos into it.
|
||||
*.manifest
|
||||
*.spec
|
||||
### STS ###
|
||||
.apt_generated
|
||||
.classpath
|
||||
.factorypath
|
||||
.project
|
||||
.settings
|
||||
.springBeans
|
||||
.sts4-cache
|
||||
bin/
|
||||
!**/src/main/**/bin/
|
||||
!**/src/test/**/bin/
|
||||
|
||||
# Installer logs
|
||||
pip-log.txt
|
||||
pip-delete-this-directory.txt
|
||||
### IntelliJ IDEA ###
|
||||
.idea
|
||||
*.iws
|
||||
*.iml
|
||||
*.ipr
|
||||
out/
|
||||
!**/src/main/**/out/
|
||||
!**/src/test/**/out/
|
||||
|
||||
# Unit test / coverage reports
|
||||
htmlcov/
|
||||
.tox/
|
||||
.nox/
|
||||
.coverage
|
||||
.coverage.*
|
||||
.cache
|
||||
nosetests.xml
|
||||
coverage.xml
|
||||
*.cover
|
||||
*.py,cover
|
||||
.hypothesis/
|
||||
.pytest_cache/
|
||||
cover/
|
||||
### NetBeans ###
|
||||
/nbproject/private/
|
||||
/nbbuild/
|
||||
/dist/
|
||||
/nbdist/
|
||||
/.nb-gradle/
|
||||
|
||||
# Translations
|
||||
*.mo
|
||||
*.pot
|
||||
### VS Code ###
|
||||
.vscode/
|
||||
|
||||
# Django stuff:
|
||||
*.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
|
||||
### Project ###
|
||||
.env
|
||||
.venv
|
||||
env/
|
||||
venv/
|
||||
ENV/
|
||||
env.bak/
|
||||
venv.bak/
|
||||
|
||||
# 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
|
||||
persistence/
|
||||
configuration.toml
|
||||
docker-compose.yml
|
||||
patches-public-key.asc
|
||||
integrations-public-key.asc
|
||||
node_modules/
|
@ -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 SENTRY_DSN
|
||||
|
||||
ENV GITHUB_TOKEN $GITHUB_TOKEN
|
||||
ENV SENTRY_DSN $SENTRY_DSN
|
||||
|
||||
WORKDIR /usr/src/app
|
||||
ENV GITHUB_ACTOR=$GITHUB_ACTOR
|
||||
ENV GITHUB_TOKEN=$GITHUB_TOKEN
|
||||
|
||||
WORKDIR /app
|
||||
COPY . .
|
||||
RUN gradle startShadowScript --no-daemon
|
||||
|
||||
RUN apt update && \
|
||||
apt-get install git build-essential libffi-dev libssl-dev openssl --no-install-recommends -y \
|
||||
&& pip install --no-cache-dir -r requirements.txt
|
||||
# Build the runtime container
|
||||
FROM eclipse-temurin:latest
|
||||
|
||||
VOLUME persistence
|
||||
|
||||
CMD [ "python3", "-m" , "sanic", "app:app", "--fast", "--access-logs", "--motd", "--noisy-exceptions", "-H", "0.0.0.0"]
|
||||
WORKDIR /app
|
||||
COPY --from=build /app/build/libs/revanced-api-*.jar revanced-api.jar
|
||||
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
|
||||
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
|
||||
- `SECRET_KEY` to salt login sessions
|
||||
- `USERNAME` & `PASSWORD` to initialize the database with a user to login with to authenticated endpoints
|
||||
To deploy ReVanced API standalone, you can either use the pre-built executable or build it from source.
|
||||
|
||||
Then, you can run the API in development mode with:
|
||||
### 📦 Pre-built executable
|
||||
|
||||
```shell
|
||||
poetry run sanic app:app --dev
|
||||
```
|
||||
A Java Runtime Environment (JRE) must be installed.
|
||||
|
||||
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
|
||||
poetry run sanic app:app --fast
|
||||
```
|
||||
### 🛠️ From source
|
||||
|
||||
## 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