mirror of
https://github.com/revanced/revanced-api.git
synced 2025-05-01 23:24:39 +02:00
feat: Initialize project
This commit is contained in:
parent
bf5eaa8940
commit
8ae50b543e
@ -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"
|
|
||||||
}
|
|
2
.env.example
Normal file
2
.env.example
Normal file
@ -0,0 +1,2 @@
|
|||||||
|
GITHUB_TOKEN=
|
||||||
|
API_VERSION=
|
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"
|
|
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 }}
|
|
191
.gitignore
vendored
191
.gitignore
vendored
@ -1,164 +1,39 @@
|
|||||||
# Byte-compiled / optimized / DLL files
|
.gradle
|
||||||
__pycache__/
|
|
||||||
*.py[cod]
|
|
||||||
*$py.class
|
|
||||||
|
|
||||||
# C extensions
|
|
||||||
*.so
|
|
||||||
|
|
||||||
# Distribution / packaging
|
|
||||||
.Python
|
|
||||||
build/
|
build/
|
||||||
develop-eggs/
|
!gradle/wrapper/gradle-wrapper.jar
|
||||||
dist/
|
!**/src/main/**/build/
|
||||||
downloads/
|
!**/src/test/**/build/
|
||||||
eggs/
|
|
||||||
.eggs/
|
|
||||||
lib/
|
|
||||||
lib64/
|
|
||||||
parts/
|
|
||||||
sdist/
|
|
||||||
var/
|
|
||||||
wheels/
|
|
||||||
share/python-wheels/
|
|
||||||
*.egg-info/
|
|
||||||
.installed.cfg
|
|
||||||
*.egg
|
|
||||||
MANIFEST
|
|
||||||
|
|
||||||
# PyInstaller
|
### STS ###
|
||||||
# Usually these files are written by a python script from a template
|
.apt_generated
|
||||||
# before PyInstaller builds the exe, so as to inject date/other infos into it.
|
.classpath
|
||||||
*.manifest
|
.factorypath
|
||||||
*.spec
|
.project
|
||||||
|
.settings
|
||||||
|
.springBeans
|
||||||
|
.sts4-cache
|
||||||
|
bin/
|
||||||
|
!**/src/main/**/bin/
|
||||||
|
!**/src/test/**/bin/
|
||||||
|
|
||||||
# Installer logs
|
### IntelliJ IDEA ###
|
||||||
pip-log.txt
|
.idea
|
||||||
pip-delete-this-directory.txt
|
*.iws
|
||||||
|
*.iml
|
||||||
|
*.ipr
|
||||||
|
out/
|
||||||
|
!**/src/main/**/out/
|
||||||
|
!**/src/test/**/out/
|
||||||
|
|
||||||
# Unit test / coverage reports
|
### NetBeans ###
|
||||||
htmlcov/
|
/nbproject/private/
|
||||||
.tox/
|
/nbbuild/
|
||||||
.nox/
|
/dist/
|
||||||
.coverage
|
/nbdist/
|
||||||
.coverage.*
|
/.nb-gradle/
|
||||||
.cache
|
|
||||||
nosetests.xml
|
|
||||||
coverage.xml
|
|
||||||
*.cover
|
|
||||||
*.py,cover
|
|
||||||
.hypothesis/
|
|
||||||
.pytest_cache/
|
|
||||||
cover/
|
|
||||||
|
|
||||||
# Translations
|
### VS Code ###
|
||||||
*.mo
|
.vscode/
|
||||||
*.pot
|
|
||||||
|
|
||||||
# Django stuff:
|
### Project ###
|
||||||
*.log
|
.env
|
||||||
local_settings.py
|
|
||||||
db.sqlite3
|
|
||||||
db.sqlite3-journal
|
|
||||||
|
|
||||||
# Flask stuff:
|
|
||||||
instance/
|
|
||||||
.webassets-cache
|
|
||||||
|
|
||||||
# Scrapy stuff:
|
|
||||||
.scrapy
|
|
||||||
|
|
||||||
# Sphinx documentation
|
|
||||||
docs/_build/
|
|
||||||
|
|
||||||
# PyBuilder
|
|
||||||
.pybuilder/
|
|
||||||
target/
|
|
||||||
|
|
||||||
# Jupyter Notebook
|
|
||||||
.ipynb_checkpoints
|
|
||||||
|
|
||||||
# IPython
|
|
||||||
profile_default/
|
|
||||||
ipython_config.py
|
|
||||||
|
|
||||||
# pyenv
|
|
||||||
# For a library or package, you might want to ignore these files since the code is
|
|
||||||
# intended to run in multiple environments; otherwise, check them in:
|
|
||||||
# .python-version
|
|
||||||
|
|
||||||
# pipenv
|
|
||||||
# According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control.
|
|
||||||
# However, in case of collaboration, if having platform-specific dependencies or dependencies
|
|
||||||
# having no cross-platform support, pipenv may install dependencies that don't work, or not
|
|
||||||
# install all needed dependencies.
|
|
||||||
#Pipfile.lock
|
|
||||||
|
|
||||||
# poetry
|
|
||||||
# Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control.
|
|
||||||
# This is especially recommended for binary packages to ensure reproducibility, and is more
|
|
||||||
# commonly ignored for libraries.
|
|
||||||
# https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control
|
|
||||||
#poetry.lock
|
|
||||||
|
|
||||||
# pdm
|
|
||||||
# Similar to Pipfile.lock, it is generally recommended to include pdm.lock in version control.
|
|
||||||
#pdm.lock
|
|
||||||
# pdm stores project-wide configurations in .pdm.toml, but it is recommended to not include it
|
|
||||||
# in version control.
|
|
||||||
# https://pdm.fming.dev/#use-with-ide
|
|
||||||
.pdm.toml
|
|
||||||
|
|
||||||
# PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-project/pdm
|
|
||||||
__pypackages__/
|
|
||||||
|
|
||||||
# Celery stuff
|
|
||||||
celerybeat-schedule
|
|
||||||
celerybeat.pid
|
|
||||||
|
|
||||||
# SageMath parsed files
|
|
||||||
*.sage.py
|
|
||||||
|
|
||||||
# Environments
|
|
||||||
.env
|
|
||||||
.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
|
|
@ -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"
|
|
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"
|
|
||||||
]
|
|
||||||
}
|
|
19
Dockerfile
19
Dockerfile
@ -1,19 +0,0 @@
|
|||||||
FROM python:3.11-slim
|
|
||||||
|
|
||||||
ARG GITHUB_TOKEN
|
|
||||||
ARG SENTRY_DSN
|
|
||||||
|
|
||||||
ENV GITHUB_TOKEN $GITHUB_TOKEN
|
|
||||||
ENV SENTRY_DSN $SENTRY_DSN
|
|
||||||
|
|
||||||
WORKDIR /usr/src/app
|
|
||||||
|
|
||||||
COPY . .
|
|
||||||
|
|
||||||
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
|
|
||||||
|
|
||||||
VOLUME persistence
|
|
||||||
|
|
||||||
CMD [ "python3", "-m" , "sanic", "app:app", "--fast", "--access-logs", "--motd", "--noisy-exceptions", "-H", "0.0.0.0"]
|
|
661
LICENSE
661
LICENSE
@ -1,661 +0,0 @@
|
|||||||
GNU AFFERO GENERAL PUBLIC LICENSE
|
|
||||||
Version 3, 19 November 2007
|
|
||||||
|
|
||||||
Copyright (C) 2007 Free Software Foundation, Inc. <https://fsf.org/>
|
|
||||||
Everyone is permitted to copy and distribute verbatim copies
|
|
||||||
of this license document, but changing it is not allowed.
|
|
||||||
|
|
||||||
Preamble
|
|
||||||
|
|
||||||
The GNU Affero General Public License is a free, copyleft license for
|
|
||||||
software and other kinds of works, specifically designed to ensure
|
|
||||||
cooperation with the community in the case of network server software.
|
|
||||||
|
|
||||||
The licenses for most software and other practical works are designed
|
|
||||||
to take away your freedom to share and change the works. By contrast,
|
|
||||||
our General Public Licenses are intended to guarantee your freedom to
|
|
||||||
share and change all versions of a program--to make sure it remains free
|
|
||||||
software for all its users.
|
|
||||||
|
|
||||||
When we speak of free software, we are referring to freedom, not
|
|
||||||
price. Our General Public Licenses are designed to make sure that you
|
|
||||||
have the freedom to distribute copies of free software (and charge for
|
|
||||||
them if you wish), that you receive source code or can get it if you
|
|
||||||
want it, that you can change the software or use pieces of it in new
|
|
||||||
free programs, and that you know you can do these things.
|
|
||||||
|
|
||||||
Developers that use our General Public Licenses protect your rights
|
|
||||||
with two steps: (1) assert copyright on the software, and (2) offer
|
|
||||||
you this License which gives you legal permission to copy, distribute
|
|
||||||
and/or modify the software.
|
|
||||||
|
|
||||||
A secondary benefit of defending all users' freedom is that
|
|
||||||
improvements made in alternate versions of the program, if they
|
|
||||||
receive widespread use, become available for other developers to
|
|
||||||
incorporate. Many developers of free software are heartened and
|
|
||||||
encouraged by the resulting cooperation. However, in the case of
|
|
||||||
software used on network servers, this result may fail to come about.
|
|
||||||
The GNU General Public License permits making a modified version and
|
|
||||||
letting the public access it on a server without ever releasing its
|
|
||||||
source code to the public.
|
|
||||||
|
|
||||||
The GNU Affero General Public License is designed specifically to
|
|
||||||
ensure that, in such cases, the modified source code becomes available
|
|
||||||
to the community. It requires the operator of a network server to
|
|
||||||
provide the source code of the modified version running there to the
|
|
||||||
users of that server. Therefore, public use of a modified version, on
|
|
||||||
a publicly accessible server, gives the public access to the source
|
|
||||||
code of the modified version.
|
|
||||||
|
|
||||||
An older license, called the Affero General Public License and
|
|
||||||
published by Affero, was designed to accomplish similar goals. This is
|
|
||||||
a different license, not a version of the Affero GPL, but Affero has
|
|
||||||
released a new version of the Affero GPL which permits relicensing under
|
|
||||||
this license.
|
|
||||||
|
|
||||||
The precise terms and conditions for copying, distribution and
|
|
||||||
modification follow.
|
|
||||||
|
|
||||||
TERMS AND CONDITIONS
|
|
||||||
|
|
||||||
0. Definitions.
|
|
||||||
|
|
||||||
"This License" refers to version 3 of the GNU Affero General Public License.
|
|
||||||
|
|
||||||
"Copyright" also means copyright-like laws that apply to other kinds of
|
|
||||||
works, such as semiconductor masks.
|
|
||||||
|
|
||||||
"The Program" refers to any copyrightable work licensed under this
|
|
||||||
License. Each licensee is addressed as "you". "Licensees" and
|
|
||||||
"recipients" may be individuals or organizations.
|
|
||||||
|
|
||||||
To "modify" a work means to copy from or adapt all or part of the work
|
|
||||||
in a fashion requiring copyright permission, other than the making of an
|
|
||||||
exact copy. The resulting work is called a "modified version" of the
|
|
||||||
earlier work or a work "based on" the earlier work.
|
|
||||||
|
|
||||||
A "covered work" means either the unmodified Program or a work based
|
|
||||||
on the Program.
|
|
||||||
|
|
||||||
To "propagate" a work means to do anything with it that, without
|
|
||||||
permission, would make you directly or secondarily liable for
|
|
||||||
infringement under applicable copyright law, except executing it on a
|
|
||||||
computer or modifying a private copy. Propagation includes copying,
|
|
||||||
distribution (with or without modification), making available to the
|
|
||||||
public, and in some countries other activities as well.
|
|
||||||
|
|
||||||
To "convey" a work means any kind of propagation that enables other
|
|
||||||
parties to make or receive copies. Mere interaction with a user through
|
|
||||||
a computer network, with no transfer of a copy, is not conveying.
|
|
||||||
|
|
||||||
An interactive user interface displays "Appropriate Legal Notices"
|
|
||||||
to the extent that it includes a convenient and prominently visible
|
|
||||||
feature that (1) displays an appropriate copyright notice, and (2)
|
|
||||||
tells the user that there is no warranty for the work (except to the
|
|
||||||
extent that warranties are provided), that licensees may convey the
|
|
||||||
work under this License, and how to view a copy of this License. If
|
|
||||||
the interface presents a list of user commands or options, such as a
|
|
||||||
menu, a prominent item in the list meets this criterion.
|
|
||||||
|
|
||||||
1. Source Code.
|
|
||||||
|
|
||||||
The "source code" for a work means the preferred form of the work
|
|
||||||
for making modifications to it. "Object code" means any non-source
|
|
||||||
form of a work.
|
|
||||||
|
|
||||||
A "Standard Interface" means an interface that either is an official
|
|
||||||
standard defined by a recognized standards body, or, in the case of
|
|
||||||
interfaces specified for a particular programming language, one that
|
|
||||||
is widely used among developers working in that language.
|
|
||||||
|
|
||||||
The "System Libraries" of an executable work include anything, other
|
|
||||||
than the work as a whole, that (a) is included in the normal form of
|
|
||||||
packaging a Major Component, but which is not part of that Major
|
|
||||||
Component, and (b) serves only to enable use of the work with that
|
|
||||||
Major Component, or to implement a Standard Interface for which an
|
|
||||||
implementation is available to the public in source code form. A
|
|
||||||
"Major Component", in this context, means a major essential component
|
|
||||||
(kernel, window system, and so on) of the specific operating system
|
|
||||||
(if any) on which the executable work runs, or a compiler used to
|
|
||||||
produce the work, or an object code interpreter used to run it.
|
|
||||||
|
|
||||||
The "Corresponding Source" for a work in object code form means all
|
|
||||||
the source code needed to generate, install, and (for an executable
|
|
||||||
work) run the object code and to modify the work, including scripts to
|
|
||||||
control those activities. However, it does not include the work's
|
|
||||||
System Libraries, or general-purpose tools or generally available free
|
|
||||||
programs which are used unmodified in performing those activities but
|
|
||||||
which are not part of the work. For example, Corresponding Source
|
|
||||||
includes interface definition files associated with source files for
|
|
||||||
the work, and the source code for shared libraries and dynamically
|
|
||||||
linked subprograms that the work is specifically designed to require,
|
|
||||||
such as by intimate data communication or control flow between those
|
|
||||||
subprograms and other parts of the work.
|
|
||||||
|
|
||||||
The Corresponding Source need not include anything that users
|
|
||||||
can regenerate automatically from other parts of the Corresponding
|
|
||||||
Source.
|
|
||||||
|
|
||||||
The Corresponding Source for a work in source code form is that
|
|
||||||
same work.
|
|
||||||
|
|
||||||
2. Basic Permissions.
|
|
||||||
|
|
||||||
All rights granted under this License are granted for the term of
|
|
||||||
copyright on the Program, and are irrevocable provided the stated
|
|
||||||
conditions are met. This License explicitly affirms your unlimited
|
|
||||||
permission to run the unmodified Program. The output from running a
|
|
||||||
covered work is covered by this License only if the output, given its
|
|
||||||
content, constitutes a covered work. This License acknowledges your
|
|
||||||
rights of fair use or other equivalent, as provided by copyright law.
|
|
||||||
|
|
||||||
You may make, run and propagate covered works that you do not
|
|
||||||
convey, without conditions so long as your license otherwise remains
|
|
||||||
in force. You may convey covered works to others for the sole purpose
|
|
||||||
of having them make modifications exclusively for you, or provide you
|
|
||||||
with facilities for running those works, provided that you comply with
|
|
||||||
the terms of this License in conveying all material for which you do
|
|
||||||
not control copyright. Those thus making or running the covered works
|
|
||||||
for you must do so exclusively on your behalf, under your direction
|
|
||||||
and control, on terms that prohibit them from making any copies of
|
|
||||||
your copyrighted material outside their relationship with you.
|
|
||||||
|
|
||||||
Conveying under any other circumstances is permitted solely under
|
|
||||||
the conditions stated below. Sublicensing is not allowed; section 10
|
|
||||||
makes it unnecessary.
|
|
||||||
|
|
||||||
3. Protecting Users' Legal Rights From Anti-Circumvention Law.
|
|
||||||
|
|
||||||
No covered work shall be deemed part of an effective technological
|
|
||||||
measure under any applicable law fulfilling obligations under article
|
|
||||||
11 of the WIPO copyright treaty adopted on 20 December 1996, or
|
|
||||||
similar laws prohibiting or restricting circumvention of such
|
|
||||||
measures.
|
|
||||||
|
|
||||||
When you convey a covered work, you waive any legal power to forbid
|
|
||||||
circumvention of technological measures to the extent such circumvention
|
|
||||||
is effected by exercising rights under this License with respect to
|
|
||||||
the covered work, and you disclaim any intention to limit operation or
|
|
||||||
modification of the work as a means of enforcing, against the work's
|
|
||||||
users, your or third parties' legal rights to forbid circumvention of
|
|
||||||
technological measures.
|
|
||||||
|
|
||||||
4. Conveying Verbatim Copies.
|
|
||||||
|
|
||||||
You may convey verbatim copies of the Program's source code as you
|
|
||||||
receive it, in any medium, provided that you conspicuously and
|
|
||||||
appropriately publish on each copy an appropriate copyright notice;
|
|
||||||
keep intact all notices stating that this License and any
|
|
||||||
non-permissive terms added in accord with section 7 apply to the code;
|
|
||||||
keep intact all notices of the absence of any warranty; and give all
|
|
||||||
recipients a copy of this License along with the Program.
|
|
||||||
|
|
||||||
You may charge any price or no price for each copy that you convey,
|
|
||||||
and you may offer support or warranty protection for a fee.
|
|
||||||
|
|
||||||
5. Conveying Modified Source Versions.
|
|
||||||
|
|
||||||
You may convey a work based on the Program, or the modifications to
|
|
||||||
produce it from the Program, in the form of source code under the
|
|
||||||
terms of section 4, provided that you also meet all of these conditions:
|
|
||||||
|
|
||||||
a) The work must carry prominent notices stating that you modified
|
|
||||||
it, and giving a relevant date.
|
|
||||||
|
|
||||||
b) The work must carry prominent notices stating that it is
|
|
||||||
released under this License and any conditions added under section
|
|
||||||
7. This requirement modifies the requirement in section 4 to
|
|
||||||
"keep intact all notices".
|
|
||||||
|
|
||||||
c) You must license the entire work, as a whole, under this
|
|
||||||
License to anyone who comes into possession of a copy. This
|
|
||||||
License will therefore apply, along with any applicable section 7
|
|
||||||
additional terms, to the whole of the work, and all its parts,
|
|
||||||
regardless of how they are packaged. This License gives no
|
|
||||||
permission to license the work in any other way, but it does not
|
|
||||||
invalidate such permission if you have separately received it.
|
|
||||||
|
|
||||||
d) If the work has interactive user interfaces, each must display
|
|
||||||
Appropriate Legal Notices; however, if the Program has interactive
|
|
||||||
interfaces that do not display Appropriate Legal Notices, your
|
|
||||||
work need not make them do so.
|
|
||||||
|
|
||||||
A compilation of a covered work with other separate and independent
|
|
||||||
works, which are not by their nature extensions of the covered work,
|
|
||||||
and which are not combined with it such as to form a larger program,
|
|
||||||
in or on a volume of a storage or distribution medium, is called an
|
|
||||||
"aggregate" if the compilation and its resulting copyright are not
|
|
||||||
used to limit the access or legal rights of the compilation's users
|
|
||||||
beyond what the individual works permit. Inclusion of a covered work
|
|
||||||
in an aggregate does not cause this License to apply to the other
|
|
||||||
parts of the aggregate.
|
|
||||||
|
|
||||||
6. Conveying Non-Source Forms.
|
|
||||||
|
|
||||||
You may convey a covered work in object code form under the terms
|
|
||||||
of sections 4 and 5, provided that you also convey the
|
|
||||||
machine-readable Corresponding Source under the terms of this License,
|
|
||||||
in one of these ways:
|
|
||||||
|
|
||||||
a) Convey the object code in, or embodied in, a physical product
|
|
||||||
(including a physical distribution medium), accompanied by the
|
|
||||||
Corresponding Source fixed on a durable physical medium
|
|
||||||
customarily used for software interchange.
|
|
||||||
|
|
||||||
b) Convey the object code in, or embodied in, a physical product
|
|
||||||
(including a physical distribution medium), accompanied by a
|
|
||||||
written offer, valid for at least three years and valid for as
|
|
||||||
long as you offer spare parts or customer support for that product
|
|
||||||
model, to give anyone who possesses the object code either (1) a
|
|
||||||
copy of the Corresponding Source for all the software in the
|
|
||||||
product that is covered by this License, on a durable physical
|
|
||||||
medium customarily used for software interchange, for a price no
|
|
||||||
more than your reasonable cost of physically performing this
|
|
||||||
conveying of source, or (2) access to copy the
|
|
||||||
Corresponding Source from a network server at no charge.
|
|
||||||
|
|
||||||
c) Convey individual copies of the object code with a copy of the
|
|
||||||
written offer to provide the Corresponding Source. This
|
|
||||||
alternative is allowed only occasionally and noncommercially, and
|
|
||||||
only if you received the object code with such an offer, in accord
|
|
||||||
with subsection 6b.
|
|
||||||
|
|
||||||
d) Convey the object code by offering access from a designated
|
|
||||||
place (gratis or for a charge), and offer equivalent access to the
|
|
||||||
Corresponding Source in the same way through the same place at no
|
|
||||||
further charge. You need not require recipients to copy the
|
|
||||||
Corresponding Source along with the object code. If the place to
|
|
||||||
copy the object code is a network server, the Corresponding Source
|
|
||||||
may be on a different server (operated by you or a third party)
|
|
||||||
that supports equivalent copying facilities, provided you maintain
|
|
||||||
clear directions next to the object code saying where to find the
|
|
||||||
Corresponding Source. Regardless of what server hosts the
|
|
||||||
Corresponding Source, you remain obligated to ensure that it is
|
|
||||||
available for as long as needed to satisfy these requirements.
|
|
||||||
|
|
||||||
e) Convey the object code using peer-to-peer transmission, provided
|
|
||||||
you inform other peers where the object code and Corresponding
|
|
||||||
Source of the work are being offered to the general public at no
|
|
||||||
charge under subsection 6d.
|
|
||||||
|
|
||||||
A separable portion of the object code, whose source code is excluded
|
|
||||||
from the Corresponding Source as a System Library, need not be
|
|
||||||
included in conveying the object code work.
|
|
||||||
|
|
||||||
A "User Product" is either (1) a "consumer product", which means any
|
|
||||||
tangible personal property which is normally used for personal, family,
|
|
||||||
or household purposes, or (2) anything designed or sold for incorporation
|
|
||||||
into a dwelling. In determining whether a product is a consumer product,
|
|
||||||
doubtful cases shall be resolved in favor of coverage. For a particular
|
|
||||||
product received by a particular user, "normally used" refers to a
|
|
||||||
typical or common use of that class of product, regardless of the status
|
|
||||||
of the particular user or of the way in which the particular user
|
|
||||||
actually uses, or expects or is expected to use, the product. A product
|
|
||||||
is a consumer product regardless of whether the product has substantial
|
|
||||||
commercial, industrial or non-consumer uses, unless such uses represent
|
|
||||||
the only significant mode of use of the product.
|
|
||||||
|
|
||||||
"Installation Information" for a User Product means any methods,
|
|
||||||
procedures, authorization keys, or other information required to install
|
|
||||||
and execute modified versions of a covered work in that User Product from
|
|
||||||
a modified version of its Corresponding Source. The information must
|
|
||||||
suffice to ensure that the continued functioning of the modified object
|
|
||||||
code is in no case prevented or interfered with solely because
|
|
||||||
modification has been made.
|
|
||||||
|
|
||||||
If you convey an object code work under this section in, or with, or
|
|
||||||
specifically for use in, a User Product, and the conveying occurs as
|
|
||||||
part of a transaction in which the right of possession and use of the
|
|
||||||
User Product is transferred to the recipient in perpetuity or for a
|
|
||||||
fixed term (regardless of how the transaction is characterized), the
|
|
||||||
Corresponding Source conveyed under this section must be accompanied
|
|
||||||
by the Installation Information. But this requirement does not apply
|
|
||||||
if neither you nor any third party retains the ability to install
|
|
||||||
modified object code on the User Product (for example, the work has
|
|
||||||
been installed in ROM).
|
|
||||||
|
|
||||||
The requirement to provide Installation Information does not include a
|
|
||||||
requirement to continue to provide support service, warranty, or updates
|
|
||||||
for a work that has been modified or installed by the recipient, or for
|
|
||||||
the User Product in which it has been modified or installed. Access to a
|
|
||||||
network may be denied when the modification itself materially and
|
|
||||||
adversely affects the operation of the network or violates the rules and
|
|
||||||
protocols for communication across the network.
|
|
||||||
|
|
||||||
Corresponding Source conveyed, and Installation Information provided,
|
|
||||||
in accord with this section must be in a format that is publicly
|
|
||||||
documented (and with an implementation available to the public in
|
|
||||||
source code form), and must require no special password or key for
|
|
||||||
unpacking, reading or copying.
|
|
||||||
|
|
||||||
7. Additional Terms.
|
|
||||||
|
|
||||||
"Additional permissions" are terms that supplement the terms of this
|
|
||||||
License by making exceptions from one or more of its conditions.
|
|
||||||
Additional permissions that are applicable to the entire Program shall
|
|
||||||
be treated as though they were included in this License, to the extent
|
|
||||||
that they are valid under applicable law. If additional permissions
|
|
||||||
apply only to part of the Program, that part may be used separately
|
|
||||||
under those permissions, but the entire Program remains governed by
|
|
||||||
this License without regard to the additional permissions.
|
|
||||||
|
|
||||||
When you convey a copy of a covered work, you may at your option
|
|
||||||
remove any additional permissions from that copy, or from any part of
|
|
||||||
it. (Additional permissions may be written to require their own
|
|
||||||
removal in certain cases when you modify the work.) You may place
|
|
||||||
additional permissions on material, added by you to a covered work,
|
|
||||||
for which you have or can give appropriate copyright permission.
|
|
||||||
|
|
||||||
Notwithstanding any other provision of this License, for material you
|
|
||||||
add to a covered work, you may (if authorized by the copyright holders of
|
|
||||||
that material) supplement the terms of this License with terms:
|
|
||||||
|
|
||||||
a) Disclaiming warranty or limiting liability differently from the
|
|
||||||
terms of sections 15 and 16 of this License; or
|
|
||||||
|
|
||||||
b) Requiring preservation of specified reasonable legal notices or
|
|
||||||
author attributions in that material or in the Appropriate Legal
|
|
||||||
Notices displayed by works containing it; or
|
|
||||||
|
|
||||||
c) Prohibiting misrepresentation of the origin of that material, or
|
|
||||||
requiring that modified versions of such material be marked in
|
|
||||||
reasonable ways as different from the original version; or
|
|
||||||
|
|
||||||
d) Limiting the use for publicity purposes of names of licensors or
|
|
||||||
authors of the material; or
|
|
||||||
|
|
||||||
e) Declining to grant rights under trademark law for use of some
|
|
||||||
trade names, trademarks, or service marks; or
|
|
||||||
|
|
||||||
f) Requiring indemnification of licensors and authors of that
|
|
||||||
material by anyone who conveys the material (or modified versions of
|
|
||||||
it) with contractual assumptions of liability to the recipient, for
|
|
||||||
any liability that these contractual assumptions directly impose on
|
|
||||||
those licensors and authors.
|
|
||||||
|
|
||||||
All other non-permissive additional terms are considered "further
|
|
||||||
restrictions" within the meaning of section 10. If the Program as you
|
|
||||||
received it, or any part of it, contains a notice stating that it is
|
|
||||||
governed by this License along with a term that is a further
|
|
||||||
restriction, you may remove that term. If a license document contains
|
|
||||||
a further restriction but permits relicensing or conveying under this
|
|
||||||
License, you may add to a covered work material governed by the terms
|
|
||||||
of that license document, provided that the further restriction does
|
|
||||||
not survive such relicensing or conveying.
|
|
||||||
|
|
||||||
If you add terms to a covered work in accord with this section, you
|
|
||||||
must place, in the relevant source files, a statement of the
|
|
||||||
additional terms that apply to those files, or a notice indicating
|
|
||||||
where to find the applicable terms.
|
|
||||||
|
|
||||||
Additional terms, permissive or non-permissive, may be stated in the
|
|
||||||
form of a separately written license, or stated as exceptions;
|
|
||||||
the above requirements apply either way.
|
|
||||||
|
|
||||||
8. Termination.
|
|
||||||
|
|
||||||
You may not propagate or modify a covered work except as expressly
|
|
||||||
provided under this License. Any attempt otherwise to propagate or
|
|
||||||
modify it is void, and will automatically terminate your rights under
|
|
||||||
this License (including any patent licenses granted under the third
|
|
||||||
paragraph of section 11).
|
|
||||||
|
|
||||||
However, if you cease all violation of this License, then your
|
|
||||||
license from a particular copyright holder is reinstated (a)
|
|
||||||
provisionally, unless and until the copyright holder explicitly and
|
|
||||||
finally terminates your license, and (b) permanently, if the copyright
|
|
||||||
holder fails to notify you of the violation by some reasonable means
|
|
||||||
prior to 60 days after the cessation.
|
|
||||||
|
|
||||||
Moreover, your license from a particular copyright holder is
|
|
||||||
reinstated permanently if the copyright holder notifies you of the
|
|
||||||
violation by some reasonable means, this is the first time you have
|
|
||||||
received notice of violation of this License (for any work) from that
|
|
||||||
copyright holder, and you cure the violation prior to 30 days after
|
|
||||||
your receipt of the notice.
|
|
||||||
|
|
||||||
Termination of your rights under this section does not terminate the
|
|
||||||
licenses of parties who have received copies or rights from you under
|
|
||||||
this License. If your rights have been terminated and not permanently
|
|
||||||
reinstated, you do not qualify to receive new licenses for the same
|
|
||||||
material under section 10.
|
|
||||||
|
|
||||||
9. Acceptance Not Required for Having Copies.
|
|
||||||
|
|
||||||
You are not required to accept this License in order to receive or
|
|
||||||
run a copy of the Program. Ancillary propagation of a covered work
|
|
||||||
occurring solely as a consequence of using peer-to-peer transmission
|
|
||||||
to receive a copy likewise does not require acceptance. However,
|
|
||||||
nothing other than this License grants you permission to propagate or
|
|
||||||
modify any covered work. These actions infringe copyright if you do
|
|
||||||
not accept this License. Therefore, by modifying or propagating a
|
|
||||||
covered work, you indicate your acceptance of this License to do so.
|
|
||||||
|
|
||||||
10. Automatic Licensing of Downstream Recipients.
|
|
||||||
|
|
||||||
Each time you convey a covered work, the recipient automatically
|
|
||||||
receives a license from the original licensors, to run, modify and
|
|
||||||
propagate that work, subject to this License. You are not responsible
|
|
||||||
for enforcing compliance by third parties with this License.
|
|
||||||
|
|
||||||
An "entity transaction" is a transaction transferring control of an
|
|
||||||
organization, or substantially all assets of one, or subdividing an
|
|
||||||
organization, or merging organizations. If propagation of a covered
|
|
||||||
work results from an entity transaction, each party to that
|
|
||||||
transaction who receives a copy of the work also receives whatever
|
|
||||||
licenses to the work the party's predecessor in interest had or could
|
|
||||||
give under the previous paragraph, plus a right to possession of the
|
|
||||||
Corresponding Source of the work from the predecessor in interest, if
|
|
||||||
the predecessor has it or can get it with reasonable efforts.
|
|
||||||
|
|
||||||
You may not impose any further restrictions on the exercise of the
|
|
||||||
rights granted or affirmed under this License. For example, you may
|
|
||||||
not impose a license fee, royalty, or other charge for exercise of
|
|
||||||
rights granted under this License, and you may not initiate litigation
|
|
||||||
(including a cross-claim or counterclaim in a lawsuit) alleging that
|
|
||||||
any patent claim is infringed by making, using, selling, offering for
|
|
||||||
sale, or importing the Program or any portion of it.
|
|
||||||
|
|
||||||
11. Patents.
|
|
||||||
|
|
||||||
A "contributor" is a copyright holder who authorizes use under this
|
|
||||||
License of the Program or a work on which the Program is based. The
|
|
||||||
work thus licensed is called the contributor's "contributor version".
|
|
||||||
|
|
||||||
A contributor's "essential patent claims" are all patent claims
|
|
||||||
owned or controlled by the contributor, whether already acquired or
|
|
||||||
hereafter acquired, that would be infringed by some manner, permitted
|
|
||||||
by this License, of making, using, or selling its contributor version,
|
|
||||||
but do not include claims that would be infringed only as a
|
|
||||||
consequence of further modification of the contributor version. For
|
|
||||||
purposes of this definition, "control" includes the right to grant
|
|
||||||
patent sublicenses in a manner consistent with the requirements of
|
|
||||||
this License.
|
|
||||||
|
|
||||||
Each contributor grants you a non-exclusive, worldwide, royalty-free
|
|
||||||
patent license under the contributor's essential patent claims, to
|
|
||||||
make, use, sell, offer for sale, import and otherwise run, modify and
|
|
||||||
propagate the contents of its contributor version.
|
|
||||||
|
|
||||||
In the following three paragraphs, a "patent license" is any express
|
|
||||||
agreement or commitment, however denominated, not to enforce a patent
|
|
||||||
(such as an express permission to practice a patent or covenant not to
|
|
||||||
sue for patent infringement). To "grant" such a patent license to a
|
|
||||||
party means to make such an agreement or commitment not to enforce a
|
|
||||||
patent against the party.
|
|
||||||
|
|
||||||
If you convey a covered work, knowingly relying on a patent license,
|
|
||||||
and the Corresponding Source of the work is not available for anyone
|
|
||||||
to copy, free of charge and under the terms of this License, through a
|
|
||||||
publicly available network server or other readily accessible means,
|
|
||||||
then you must either (1) cause the Corresponding Source to be so
|
|
||||||
available, or (2) arrange to deprive yourself of the benefit of the
|
|
||||||
patent license for this particular work, or (3) arrange, in a manner
|
|
||||||
consistent with the requirements of this License, to extend the patent
|
|
||||||
license to downstream recipients. "Knowingly relying" means you have
|
|
||||||
actual knowledge that, but for the patent license, your conveying the
|
|
||||||
covered work in a country, or your recipient's use of the covered work
|
|
||||||
in a country, would infringe one or more identifiable patents in that
|
|
||||||
country that you have reason to believe are valid.
|
|
||||||
|
|
||||||
If, pursuant to or in connection with a single transaction or
|
|
||||||
arrangement, you convey, or propagate by procuring conveyance of, a
|
|
||||||
covered work, and grant a patent license to some of the parties
|
|
||||||
receiving the covered work authorizing them to use, propagate, modify
|
|
||||||
or convey a specific copy of the covered work, then the patent license
|
|
||||||
you grant is automatically extended to all recipients of the covered
|
|
||||||
work and works based on it.
|
|
||||||
|
|
||||||
A patent license is "discriminatory" if it does not include within
|
|
||||||
the scope of its coverage, prohibits the exercise of, or is
|
|
||||||
conditioned on the non-exercise of one or more of the rights that are
|
|
||||||
specifically granted under this License. You may not convey a covered
|
|
||||||
work if you are a party to an arrangement with a third party that is
|
|
||||||
in the business of distributing software, under which you make payment
|
|
||||||
to the third party based on the extent of your activity of conveying
|
|
||||||
the work, and under which the third party grants, to any of the
|
|
||||||
parties who would receive the covered work from you, a discriminatory
|
|
||||||
patent license (a) in connection with copies of the covered work
|
|
||||||
conveyed by you (or copies made from those copies), or (b) primarily
|
|
||||||
for and in connection with specific products or compilations that
|
|
||||||
contain the covered work, unless you entered into that arrangement,
|
|
||||||
or that patent license was granted, prior to 28 March 2007.
|
|
||||||
|
|
||||||
Nothing in this License shall be construed as excluding or limiting
|
|
||||||
any implied license or other defenses to infringement that may
|
|
||||||
otherwise be available to you under applicable patent law.
|
|
||||||
|
|
||||||
12. No Surrender of Others' Freedom.
|
|
||||||
|
|
||||||
If conditions are imposed on you (whether by court order, agreement or
|
|
||||||
otherwise) that contradict the conditions of this License, they do not
|
|
||||||
excuse you from the conditions of this License. If you cannot convey a
|
|
||||||
covered work so as to satisfy simultaneously your obligations under this
|
|
||||||
License and any other pertinent obligations, then as a consequence you may
|
|
||||||
not convey it at all. For example, if you agree to terms that obligate you
|
|
||||||
to collect a royalty for further conveying from those to whom you convey
|
|
||||||
the Program, the only way you could satisfy both those terms and this
|
|
||||||
License would be to refrain entirely from conveying the Program.
|
|
||||||
|
|
||||||
13. Remote Network Interaction; Use with the GNU General Public License.
|
|
||||||
|
|
||||||
Notwithstanding any other provision of this License, if you modify the
|
|
||||||
Program, your modified version must prominently offer all users
|
|
||||||
interacting with it remotely through a computer network (if your version
|
|
||||||
supports such interaction) an opportunity to receive the Corresponding
|
|
||||||
Source of your version by providing access to the Corresponding Source
|
|
||||||
from a network server at no charge, through some standard or customary
|
|
||||||
means of facilitating copying of software. This Corresponding Source
|
|
||||||
shall include the Corresponding Source for any work covered by version 3
|
|
||||||
of the GNU General Public License that is incorporated pursuant to the
|
|
||||||
following paragraph.
|
|
||||||
|
|
||||||
Notwithstanding any other provision of this License, you have
|
|
||||||
permission to link or combine any covered work with a work licensed
|
|
||||||
under version 3 of the GNU General Public License into a single
|
|
||||||
combined work, and to convey the resulting work. The terms of this
|
|
||||||
License will continue to apply to the part which is the covered work,
|
|
||||||
but the work with which it is combined will remain governed by version
|
|
||||||
3 of the GNU General Public License.
|
|
||||||
|
|
||||||
14. Revised Versions of this License.
|
|
||||||
|
|
||||||
The Free Software Foundation may publish revised and/or new versions of
|
|
||||||
the GNU Affero General Public License from time to time. Such new versions
|
|
||||||
will be similar in spirit to the present version, but may differ in detail to
|
|
||||||
address new problems or concerns.
|
|
||||||
|
|
||||||
Each version is given a distinguishing version number. If the
|
|
||||||
Program specifies that a certain numbered version of the GNU Affero General
|
|
||||||
Public License "or any later version" applies to it, you have the
|
|
||||||
option of following the terms and conditions either of that numbered
|
|
||||||
version or of any later version published by the Free Software
|
|
||||||
Foundation. If the Program does not specify a version number of the
|
|
||||||
GNU Affero General Public License, you may choose any version ever published
|
|
||||||
by the Free Software Foundation.
|
|
||||||
|
|
||||||
If the Program specifies that a proxy can decide which future
|
|
||||||
versions of the GNU Affero General Public License can be used, that proxy's
|
|
||||||
public statement of acceptance of a version permanently authorizes you
|
|
||||||
to choose that version for the Program.
|
|
||||||
|
|
||||||
Later license versions may give you additional or different
|
|
||||||
permissions. However, no additional obligations are imposed on any
|
|
||||||
author or copyright holder as a result of your choosing to follow a
|
|
||||||
later version.
|
|
||||||
|
|
||||||
15. Disclaimer of Warranty.
|
|
||||||
|
|
||||||
THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY
|
|
||||||
APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT
|
|
||||||
HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY
|
|
||||||
OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO,
|
|
||||||
THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
|
|
||||||
PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM
|
|
||||||
IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF
|
|
||||||
ALL NECESSARY SERVICING, REPAIR OR CORRECTION.
|
|
||||||
|
|
||||||
16. Limitation of Liability.
|
|
||||||
|
|
||||||
IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING
|
|
||||||
WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS
|
|
||||||
THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY
|
|
||||||
GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE
|
|
||||||
USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF
|
|
||||||
DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD
|
|
||||||
PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS),
|
|
||||||
EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF
|
|
||||||
SUCH DAMAGES.
|
|
||||||
|
|
||||||
17. Interpretation of Sections 15 and 16.
|
|
||||||
|
|
||||||
If the disclaimer of warranty and limitation of liability provided
|
|
||||||
above cannot be given local legal effect according to their terms,
|
|
||||||
reviewing courts shall apply local law that most closely approximates
|
|
||||||
an absolute waiver of all civil liability in connection with the
|
|
||||||
Program, unless a warranty or assumption of liability accompanies a
|
|
||||||
copy of the Program in return for a fee.
|
|
||||||
|
|
||||||
END OF TERMS AND CONDITIONS
|
|
||||||
|
|
||||||
How to Apply These Terms to Your New Programs
|
|
||||||
|
|
||||||
If you develop a new program, and you want it to be of the greatest
|
|
||||||
possible use to the public, the best way to achieve this is to make it
|
|
||||||
free software which everyone can redistribute and change under these terms.
|
|
||||||
|
|
||||||
To do so, attach the following notices to the program. It is safest
|
|
||||||
to attach them to the start of each source file to most effectively
|
|
||||||
state the exclusion of warranty; and each file should have at least
|
|
||||||
the "copyright" line and a pointer to where the full notice is found.
|
|
||||||
|
|
||||||
<one line to give the program's name and a brief idea of what it does.>
|
|
||||||
Copyright (C) <year> <name of author>
|
|
||||||
|
|
||||||
This program is free software: you can redistribute it and/or modify
|
|
||||||
it under the terms of the GNU Affero General Public License as published
|
|
||||||
by the Free Software Foundation, either version 3 of the License, or
|
|
||||||
(at your option) any later version.
|
|
||||||
|
|
||||||
This program is distributed in the hope that it will be useful,
|
|
||||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
||||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
||||||
GNU Affero General Public License for more details.
|
|
||||||
|
|
||||||
You should have received a copy of the GNU Affero General Public License
|
|
||||||
along with this program. If not, see <https://www.gnu.org/licenses/>.
|
|
||||||
|
|
||||||
Also add information on how to contact you by electronic and paper mail.
|
|
||||||
|
|
||||||
If your software can interact with users remotely through a computer
|
|
||||||
network, you should also make sure that it provides a way for users to
|
|
||||||
get its source. For example, if your program is a web application, its
|
|
||||||
interface could display a "Source" link that leads users to an archive
|
|
||||||
of the code. There are many ways you could offer source, and different
|
|
||||||
solutions will be better for different programs; see section 13 for the
|
|
||||||
specific requirements.
|
|
||||||
|
|
||||||
You should also get your employer (if you work as a programmer) or school,
|
|
||||||
if any, to sign a "copyright disclaimer" for the program, if necessary.
|
|
||||||
For more information on this, and how to apply and follow the GNU AGPL, see
|
|
||||||
<https://www.gnu.org/licenses/>.
|
|
44
README.md
44
README.md
@ -1,44 +0,0 @@
|
|||||||
# ReVanced Releases API
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||

|
|
||||||
[](https://github.com/revanced/revanced-api/actions/workflows/main.yml)
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
This is a simple API that proxies requests needed to feed the ReVanced Manager and website with data.
|
|
||||||
|
|
||||||
## Usage
|
|
||||||
|
|
||||||
To run this API, you need Python 3.11.x. You can install the dependencies with poetry:
|
|
||||||
|
|
||||||
```shell
|
|
||||||
poetry install
|
|
||||||
```
|
|
||||||
|
|
||||||
Create the following environment variables:
|
|
||||||
|
|
||||||
- `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
|
|
||||||
|
|
||||||
Then, you can run the API in development mode with:
|
|
||||||
|
|
||||||
```shell
|
|
||||||
poetry run sanic app:app --dev
|
|
||||||
```
|
|
||||||
|
|
||||||
or in production mode with:
|
|
||||||
|
|
||||||
```shell
|
|
||||||
poetry run sanic app:app --fast
|
|
||||||
```
|
|
||||||
|
|
||||||
## Contributing
|
|
||||||
|
|
||||||
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.
|
|
||||||
|
|
||||||
## License
|
|
||||||
|
|
||||||
This project is licensed under the AGPLv3 License - see the [LICENSE](LICENSE) file for details.
|
|
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",
|
|
||||||
)
|
|
47
build.gradle.kts
Normal file
47
build.gradle.kts
Normal file
@ -0,0 +1,47 @@
|
|||||||
|
plugins {
|
||||||
|
alias(libs.plugins.kotlin)
|
||||||
|
alias(libs.plugins.ktor)
|
||||||
|
alias(libs.plugins.serilization)
|
||||||
|
}
|
||||||
|
|
||||||
|
group = "app.revanced"
|
||||||
|
|
||||||
|
application {
|
||||||
|
mainClass.set("app.revanced.api.ApplicationKt")
|
||||||
|
|
||||||
|
val isDevelopment: Boolean = project.ext.has("development")
|
||||||
|
applicationDefaultJvmArgs = listOf("-Dio.ktor.development=$isDevelopment")
|
||||||
|
}
|
||||||
|
|
||||||
|
repositories {
|
||||||
|
mavenCentral()
|
||||||
|
}
|
||||||
|
|
||||||
|
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.swagger)
|
||||||
|
implementation(libs.ktor.server.openapi)
|
||||||
|
implementation(libs.ktor.server.cors)
|
||||||
|
implementation(libs.ktor.server.caching.headers)
|
||||||
|
implementation(libs.ktor.server.host.common)
|
||||||
|
implementation(libs.ktor.server.netty)
|
||||||
|
implementation(libs.ktor.server.conditional.headers)
|
||||||
|
implementation(libs.ktor.serialization.kotlinx.json)
|
||||||
|
implementation(libs.koin.ktor)
|
||||||
|
implementation(libs.h2)
|
||||||
|
implementation(libs.logback.classic)
|
||||||
|
implementation(libs.exposed.core)
|
||||||
|
implementation(libs.exposed.jdbc)
|
||||||
|
implementation(libs.dotenv.kotlin)
|
||||||
|
testImplementation(libs.ktor.server.tests)
|
||||||
|
testImplementation(libs.kotlin.test.junit)
|
||||||
|
}
|
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},
|
|
||||||
}
|
|
@ -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)
|
|
@ -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
|
|
23
docs/.swagger-codegen-ignore
Normal file
23
docs/.swagger-codegen-ignore
Normal file
@ -0,0 +1,23 @@
|
|||||||
|
# Swagger Codegen Ignore
|
||||||
|
# Generated by swagger-codegen https://github.com/swagger-api/swagger-codegen
|
||||||
|
|
||||||
|
# Use this file to prevent files from being overwritten by the generator.
|
||||||
|
# The patterns follow closely to .gitignore or .dockerignore.
|
||||||
|
|
||||||
|
# As an example, the C# client generator defines ApiClient.cs.
|
||||||
|
# You can make changes and tell Swagger Codgen to ignore just this file by uncommenting the following line:
|
||||||
|
#ApiClient.cs
|
||||||
|
|
||||||
|
# You can match any string of characters against a directory, file or extension with a single asterisk (*):
|
||||||
|
#foo/*/qux
|
||||||
|
# The above matches foo/bar/qux and foo/baz/qux, but not foo/bar/baz/qux
|
||||||
|
|
||||||
|
# You can recursively match patterns against a directory, file or extension with a double asterisk (**):
|
||||||
|
#foo/**/qux
|
||||||
|
# This matches foo/bar/qux, foo/baz/qux, and foo/bar/baz/qux
|
||||||
|
|
||||||
|
# You can also negate patterns with an exclamation (!).
|
||||||
|
# For example, you can ignore all files in a docs folder with the file extension .md:
|
||||||
|
#docs/*.md
|
||||||
|
# Then explicitly reverse the ignore rule for a single file:
|
||||||
|
#!docs/README.md
|
1
docs/.swagger-codegen/VERSION
Normal file
1
docs/.swagger-codegen/VERSION
Normal file
@ -0,0 +1 @@
|
|||||||
|
3.0.41
|
2752
docs/index.html
Normal file
2752
docs/index.html
Normal file
File diff suppressed because one or more lines are too long
4
gradle.properties
Normal file
4
gradle.properties
Normal file
@ -0,0 +1,4 @@
|
|||||||
|
org.gradle.parallel = true
|
||||||
|
org.gradle.caching = true
|
||||||
|
kotlin.code.style = official
|
||||||
|
version = 0.0.1
|
41
gradle/libs.versions.toml
Normal file
41
gradle/libs.versions.toml
Normal file
@ -0,0 +1,41 @@
|
|||||||
|
[versions]
|
||||||
|
kotlin="1.9.22"
|
||||||
|
logback="1.4.14"
|
||||||
|
exposed="0.41.1"
|
||||||
|
h2="2.1.214"
|
||||||
|
koin="3.5.3"
|
||||||
|
dotenv="6.4.1"
|
||||||
|
ktor = "2.3.7"
|
||||||
|
|
||||||
|
[libraries]
|
||||||
|
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-conditional-headers = { module = "io.ktor:ktor-server-conditional-headers" }
|
||||||
|
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-swagger = { module = "io.ktor:ktor-server-swagger" }
|
||||||
|
ktor-server-openapi = { module = "io.ktor:ktor-server-openapi" }
|
||||||
|
ktor-server-cors = { module = "io.ktor:ktor-server-cors" }
|
||||||
|
ktor-server-caching-headers = { module = "io.ktor:ktor-server-caching-headers" }
|
||||||
|
ktor-server-host-common = { module = "io.ktor:ktor-server-host-common" }
|
||||||
|
ktor-server-netty = { module = "io.ktor:ktor-server-netty" }
|
||||||
|
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" }
|
||||||
|
dotenv-kotlin = { module = "io.github.cdimascio:dotenv-kotlin", version.ref = "dotenv" }
|
||||||
|
ktor-server-tests = { module = "io.ktor:ktor-server-tests" }
|
||||||
|
kotlin-test-junit = { module = "org.jetbrains.kotlin:kotlin-test-junit", version.ref = "kotlin" }
|
||||||
|
|
||||||
|
[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.
5
gradle/wrapper/gradle-wrapper.properties
vendored
Normal file
5
gradle/wrapper/gradle-wrapper.properties
vendored
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
distributionBase=GRADLE_USER_HOME
|
||||||
|
distributionPath=wrapper/dists
|
||||||
|
distributionUrl=https\://services.gradle.org/distributions/gradle-8.4-bin.zip
|
||||||
|
zipStoreBase=GRADLE_USER_HOME
|
||||||
|
zipStorePath=wrapper/dists
|
234
gradlew
vendored
Normal file
234
gradlew
vendored
Normal file
@ -0,0 +1,234 @@
|
|||||||
|
#!/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/master/subprojects/plugins/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
|
||||||
|
|
||||||
|
APP_HOME=$( cd "${APP_HOME:-./}" && pwd -P ) || exit
|
||||||
|
|
||||||
|
APP_NAME="Gradle"
|
||||||
|
APP_BASE_NAME=${0##*/}
|
||||||
|
|
||||||
|
# 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"'
|
||||||
|
|
||||||
|
# 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
|
||||||
|
which java >/dev/null 2>&1 || 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
|
||||||
|
|
||||||
|
# Increase the maximum file descriptors if we can.
|
||||||
|
if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then
|
||||||
|
case $MAX_FD in #(
|
||||||
|
max*)
|
||||||
|
MAX_FD=$( ulimit -H -n ) ||
|
||||||
|
warn "Could not query maximum file descriptor limit"
|
||||||
|
esac
|
||||||
|
case $MAX_FD in #(
|
||||||
|
'' | soft) :;; #(
|
||||||
|
*)
|
||||||
|
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
|
||||||
|
|
||||||
|
# Collect all arguments for the java command;
|
||||||
|
# * $DEFAULT_JVM_OPTS, $JAVA_OPTS, and $GRADLE_OPTS can contain fragments of
|
||||||
|
# shell script including quotes and variable substitutions, so put them in
|
||||||
|
# double quotes to make sure that they get re-expanded; and
|
||||||
|
# * put everything else in single quotes, so that it's not re-expanded.
|
||||||
|
|
||||||
|
set -- \
|
||||||
|
"-Dorg.gradle.appname=$APP_BASE_NAME" \
|
||||||
|
-classpath "$CLASSPATH" \
|
||||||
|
org.gradle.wrapper.GradleWrapperMain \
|
||||||
|
"$@"
|
||||||
|
|
||||||
|
# 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" "$@"
|
89
gradlew.bat
vendored
Normal file
89
gradlew.bat
vendored
Normal file
@ -0,0 +1,89 @@
|
|||||||
|
@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=.
|
||||||
|
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%" == "0" goto execute
|
||||||
|
|
||||||
|
echo.
|
||||||
|
echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
|
||||||
|
echo.
|
||||||
|
echo Please set the JAVA_HOME variable in your environment to match the
|
||||||
|
echo location of your Java installation.
|
||||||
|
|
||||||
|
goto fail
|
||||||
|
|
||||||
|
:findJavaFromJavaHome
|
||||||
|
set JAVA_HOME=%JAVA_HOME:"=%
|
||||||
|
set JAVA_EXE=%JAVA_HOME%/bin/java.exe
|
||||||
|
|
||||||
|
if exist "%JAVA_EXE%" goto execute
|
||||||
|
|
||||||
|
echo.
|
||||||
|
echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME%
|
||||||
|
echo.
|
||||||
|
echo Please set the JAVA_HOME variable in your environment to match the
|
||||||
|
echo location of your Java installation.
|
||||||
|
|
||||||
|
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%"=="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!
|
||||||
|
if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1
|
||||||
|
exit /b 1
|
||||||
|
|
||||||
|
: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
|
|
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 = "app.revanced.revanced-api"
|
||||||
|
|
||||||
|
buildCache {
|
||||||
|
local {
|
||||||
|
isEnabled = "CI" !in System.getenv()
|
||||||
|
}
|
||||||
|
}
|
24
src/main/kotlin/app/revanced/api/Application.kt
Normal file
24
src/main/kotlin/app/revanced/api/Application.kt
Normal file
@ -0,0 +1,24 @@
|
|||||||
|
package app.revanced.api
|
||||||
|
|
||||||
|
import app.revanced.api.plugins.*
|
||||||
|
import io.github.cdimascio.dotenv.Dotenv
|
||||||
|
import io.ktor.server.application.*
|
||||||
|
import io.ktor.server.engine.*
|
||||||
|
import io.ktor.server.netty.*
|
||||||
|
|
||||||
|
fun main() {
|
||||||
|
Dotenv.load()
|
||||||
|
|
||||||
|
embeddedServer(Netty, port = 8080, host = "0.0.0.0", configure = {
|
||||||
|
connectionGroupSize = 1
|
||||||
|
workerGroupSize = 1
|
||||||
|
callGroupSize = 1
|
||||||
|
}) {
|
||||||
|
configureHTTP()
|
||||||
|
configureSerialization()
|
||||||
|
configureDatabases()
|
||||||
|
configureSecurity()
|
||||||
|
configureDependencies()
|
||||||
|
configureRouting()
|
||||||
|
}.start(wait = true)
|
||||||
|
}
|
140
src/main/kotlin/app/revanced/api/backend/Backend.kt
Normal file
140
src/main/kotlin/app/revanced/api/backend/Backend.kt
Normal file
@ -0,0 +1,140 @@
|
|||||||
|
package app.revanced.api.backend
|
||||||
|
|
||||||
|
import io.ktor.client.*
|
||||||
|
import io.ktor.client.engine.okhttp.*
|
||||||
|
import kotlinx.serialization.Serializable
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The backend of the application used to get data for the API.
|
||||||
|
*
|
||||||
|
* @param httpClientConfig The configuration of the HTTP client.
|
||||||
|
*/
|
||||||
|
abstract class Backend(
|
||||||
|
httpClientConfig: HttpClientConfig<OkHttpConfig>.() -> Unit = {}
|
||||||
|
) {
|
||||||
|
protected val client: HttpClient = HttpClient(OkHttp, httpClientConfig)
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A user.
|
||||||
|
*
|
||||||
|
* @property name The name of the user.
|
||||||
|
* @property avatarUrl The URL to the avatar of the user.
|
||||||
|
* @property profileUrl The URL to the profile of the user.
|
||||||
|
*/
|
||||||
|
interface User {
|
||||||
|
val name: String
|
||||||
|
val avatarUrl: String
|
||||||
|
val profileUrl: String
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* An organization.
|
||||||
|
*
|
||||||
|
* @property members The members of the organization.
|
||||||
|
*/
|
||||||
|
class Organization(
|
||||||
|
val members: Set<Member>
|
||||||
|
) {
|
||||||
|
/**
|
||||||
|
* A member of an organization.
|
||||||
|
*
|
||||||
|
* @property name The name of the member.
|
||||||
|
* @property avatarUrl The URL to the avatar of the member.
|
||||||
|
* @property profileUrl The URL to the profile of the member.
|
||||||
|
* @property bio The bio of the member.
|
||||||
|
* @property gpgKeysUrl The URL to the GPG keys of the member.
|
||||||
|
*/
|
||||||
|
@Serializable
|
||||||
|
class Member (
|
||||||
|
override val name: String,
|
||||||
|
override val avatarUrl: String,
|
||||||
|
override val profileUrl: String,
|
||||||
|
val bio: String?,
|
||||||
|
val gpgKeysUrl: String?
|
||||||
|
) : User
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A repository of an organization.
|
||||||
|
*
|
||||||
|
* @property contributors The contributors of the repository.
|
||||||
|
*/
|
||||||
|
class Repository(
|
||||||
|
val contributors: Set<Contributor>
|
||||||
|
) {
|
||||||
|
/**
|
||||||
|
* A contributor of a repository.
|
||||||
|
*
|
||||||
|
* @property name The name of the contributor.
|
||||||
|
* @property avatarUrl The URL to the avatar of the contributor.
|
||||||
|
* @property profileUrl The URL to the profile of the contributor.
|
||||||
|
*/
|
||||||
|
@Serializable
|
||||||
|
class Contributor(
|
||||||
|
override val name: String,
|
||||||
|
override val avatarUrl: String,
|
||||||
|
override val profileUrl: String
|
||||||
|
) : User
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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.
|
||||||
|
*/
|
||||||
|
@Serializable
|
||||||
|
class Release(
|
||||||
|
val tag: String,
|
||||||
|
val releaseNote: String,
|
||||||
|
val createdAt: String,
|
||||||
|
val assets: Set<Asset>
|
||||||
|
) {
|
||||||
|
/**
|
||||||
|
* An asset of a release.
|
||||||
|
*
|
||||||
|
* @property downloadUrl The URL to download the asset.
|
||||||
|
*/
|
||||||
|
@Serializable
|
||||||
|
class Asset(
|
||||||
|
val downloadUrl: String
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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.
|
||||||
|
* @param preRelease Whether to return a pre-release.
|
||||||
|
* If no pre-release exists, the latest release is returned.
|
||||||
|
* If tag is not null, this parameter is ignored.
|
||||||
|
* @return The release.
|
||||||
|
*/
|
||||||
|
abstract suspend fun getRelease(
|
||||||
|
owner: String,
|
||||||
|
repository: String,
|
||||||
|
tag: String? = null,
|
||||||
|
preRelease: Boolean = false
|
||||||
|
): Organization.Repository.Release
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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 getContributors(owner: String, repository: String): Set<Organization.Repository.Contributor>
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the members of an organization.
|
||||||
|
*
|
||||||
|
* @param organization The name of the organization.
|
||||||
|
* @return The members.
|
||||||
|
*/
|
||||||
|
abstract suspend fun getMembers(organization: String): Set<Organization.Member>
|
||||||
|
}
|
116
src/main/kotlin/app/revanced/api/backend/github/GitHubBackend.kt
Normal file
116
src/main/kotlin/app/revanced/api/backend/github/GitHubBackend.kt
Normal file
@ -0,0 +1,116 @@
|
|||||||
|
package app.revanced.api.backend.github
|
||||||
|
|
||||||
|
import app.revanced.api.backend.Backend
|
||||||
|
import app.revanced.api.backend.github.api.Request
|
||||||
|
import io.ktor.client.call.*
|
||||||
|
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 app.revanced.api.backend.github.api.Request.Organization.Repository.Releases
|
||||||
|
import app.revanced.api.backend.github.api.Request.Organization.Repository.Contributors
|
||||||
|
import app.revanced.api.backend.github.api.Request.Organization.Members
|
||||||
|
import app.revanced.api.backend.github.api.Response
|
||||||
|
import app.revanced.api.backend.github.api.Response.Organization.Repository.Release
|
||||||
|
import app.revanced.api.backend.github.api.Response.Organization.Repository.Contributor
|
||||||
|
import app.revanced.api.backend.github.api.Response.Organization.Member
|
||||||
|
import io.ktor.client.plugins.resources.Resources
|
||||||
|
import io.ktor.serialization.kotlinx.json.*
|
||||||
|
import kotlinx.coroutines.*
|
||||||
|
import kotlinx.serialization.ExperimentalSerializationApi
|
||||||
|
import kotlinx.serialization.json.Json
|
||||||
|
import kotlinx.serialization.json.JsonNamingStrategy
|
||||||
|
|
||||||
|
@OptIn(ExperimentalSerializationApi::class)
|
||||||
|
class GitHubBackend(token: String? = null) : Backend({
|
||||||
|
install(HttpCache)
|
||||||
|
install(Resources)
|
||||||
|
install(ContentNegotiation) {
|
||||||
|
json(Json {
|
||||||
|
ignoreUnknownKeys = true
|
||||||
|
namingStrategy = JsonNamingStrategy.SnakeCase
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
defaultRequest { url("https://api.github.com") }
|
||||||
|
|
||||||
|
token?.let {
|
||||||
|
install(Auth) {
|
||||||
|
bearer {
|
||||||
|
loadTokens {
|
||||||
|
BearerTokens(
|
||||||
|
accessToken = it,
|
||||||
|
refreshToken = "" // Required dummy value
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
sendWithoutRequest { true }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}) {
|
||||||
|
override suspend fun getRelease(
|
||||||
|
owner: String,
|
||||||
|
repository: String,
|
||||||
|
tag: String?,
|
||||||
|
preRelease: Boolean
|
||||||
|
): Organization.Repository.Release {
|
||||||
|
val release = if (preRelease) {
|
||||||
|
val releases: Set<Release> = client.get(Releases(owner, repository)).body()
|
||||||
|
releases.firstOrNull { it.preReleases } ?: releases.first() // Latest pre-release or latest release
|
||||||
|
} else {
|
||||||
|
client.get(
|
||||||
|
tag?.let { Releases.Tag(owner, repository, it) }
|
||||||
|
?: Releases.Latest(owner, repository)
|
||||||
|
).body()
|
||||||
|
}
|
||||||
|
|
||||||
|
return Organization.Repository.Release(
|
||||||
|
tag = release.tagName,
|
||||||
|
releaseNote = release.body,
|
||||||
|
createdAt = release.createdAt,
|
||||||
|
assets = release.assets.map {
|
||||||
|
Organization.Repository.Release.Asset(
|
||||||
|
downloadUrl = it.browserDownloadUrl
|
||||||
|
)
|
||||||
|
}.toSet()
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
override suspend fun getContributors(owner: String, repository: String): Set<Organization.Repository.Contributor> {
|
||||||
|
val contributors: Set<Contributor> = client.get(Contributors(owner, repository)).body()
|
||||||
|
|
||||||
|
return contributors.map {
|
||||||
|
Organization.Repository.Contributor(
|
||||||
|
name = it.login,
|
||||||
|
avatarUrl = it.avatarUrl,
|
||||||
|
profileUrl = it.url
|
||||||
|
)
|
||||||
|
}.toSet()
|
||||||
|
}
|
||||||
|
|
||||||
|
override suspend fun getMembers(organization: String): Set<Organization.Member> {
|
||||||
|
// Get the list of members of the organization.
|
||||||
|
val members: Set<Member> = client.get(Members(organization)).body<Set<Member>>()
|
||||||
|
|
||||||
|
return runBlocking(Dispatchers.Default) {
|
||||||
|
members.map { member ->
|
||||||
|
// Map the member to a user in order to get the bio.
|
||||||
|
async {
|
||||||
|
client.get(Request.User(member.login)).body<Response.User>()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}.awaitAll().map { user ->
|
||||||
|
// Map the user back to a member.
|
||||||
|
Organization.Member(
|
||||||
|
name = user.login,
|
||||||
|
avatarUrl = user.avatarUrl,
|
||||||
|
profileUrl = user.url,
|
||||||
|
bio = user.bio,
|
||||||
|
gpgKeysUrl = "https://github.com/${user.login}.gpg",
|
||||||
|
)
|
||||||
|
}.toSet()
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,26 @@
|
|||||||
|
package app.revanced.api.backend.github.api
|
||||||
|
|
||||||
|
import io.ktor.resources.*
|
||||||
|
|
||||||
|
class Request {
|
||||||
|
@Resource("/users/{username}")
|
||||||
|
class User(val username: 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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,52 @@
|
|||||||
|
package app.revanced.api.backend.github.api
|
||||||
|
|
||||||
|
import kotlinx.serialization.Serializable
|
||||||
|
|
||||||
|
|
||||||
|
class Response {
|
||||||
|
interface IUser {
|
||||||
|
val login: String
|
||||||
|
val avatarUrl: String
|
||||||
|
val url: String
|
||||||
|
}
|
||||||
|
|
||||||
|
@Serializable
|
||||||
|
class User (
|
||||||
|
override val login: String,
|
||||||
|
override val avatarUrl: String,
|
||||||
|
override val url: String,
|
||||||
|
val bio: String?,
|
||||||
|
) : IUser
|
||||||
|
|
||||||
|
class Organization {
|
||||||
|
@Serializable
|
||||||
|
class Member(
|
||||||
|
override val login: String,
|
||||||
|
override val avatarUrl: String,
|
||||||
|
override val url: String,
|
||||||
|
) : IUser
|
||||||
|
|
||||||
|
class Repository {
|
||||||
|
@Serializable
|
||||||
|
class Contributor(
|
||||||
|
override val login: String,
|
||||||
|
override val avatarUrl: String,
|
||||||
|
override val url: String,
|
||||||
|
) : IUser
|
||||||
|
|
||||||
|
@Serializable
|
||||||
|
class Release(
|
||||||
|
val tagName: String,
|
||||||
|
val assets: Set<Asset>,
|
||||||
|
val preReleases: Boolean,
|
||||||
|
val createdAt: String,
|
||||||
|
val body: String
|
||||||
|
) {
|
||||||
|
@Serializable
|
||||||
|
class Asset(
|
||||||
|
val browserDownloadUrl: String
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
49
src/main/kotlin/app/revanced/api/plugins/Databases.kt
Normal file
49
src/main/kotlin/app/revanced/api/plugins/Databases.kt
Normal file
@ -0,0 +1,49 @@
|
|||||||
|
package app.revanced.api.plugins
|
||||||
|
|
||||||
|
import io.ktor.http.*
|
||||||
|
import io.ktor.server.application.*
|
||||||
|
import io.ktor.server.request.*
|
||||||
|
import io.ktor.server.response.*
|
||||||
|
import io.ktor.server.routing.*
|
||||||
|
import org.jetbrains.exposed.sql.*
|
||||||
|
|
||||||
|
fun Application.configureDatabases() {
|
||||||
|
val database = Database.connect(
|
||||||
|
url = "jdbc:h2:mem:test;DB_CLOSE_DELAY=-1",
|
||||||
|
user = "root",
|
||||||
|
driver = "org.h2.Driver",
|
||||||
|
password = ""
|
||||||
|
)
|
||||||
|
val userService = UserService(database)
|
||||||
|
routing {
|
||||||
|
// Create user
|
||||||
|
post("/users") {
|
||||||
|
val user = call.receive<ExposedUser>()
|
||||||
|
val id = userService.create(user)
|
||||||
|
call.respond(HttpStatusCode.Created, id)
|
||||||
|
}
|
||||||
|
// Read user
|
||||||
|
get("/users/{id}") {
|
||||||
|
val id = call.parameters["id"]?.toInt() ?: throw IllegalArgumentException("Invalid ID")
|
||||||
|
val user = userService.read(id)
|
||||||
|
if (user != null) {
|
||||||
|
call.respond(HttpStatusCode.OK, user)
|
||||||
|
} else {
|
||||||
|
call.respond(HttpStatusCode.NotFound)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// Update user
|
||||||
|
put("/users/{id}") {
|
||||||
|
val id = call.parameters["id"]?.toInt() ?: throw IllegalArgumentException("Invalid ID")
|
||||||
|
val user = call.receive<ExposedUser>()
|
||||||
|
userService.update(id, user)
|
||||||
|
call.respond(HttpStatusCode.OK)
|
||||||
|
}
|
||||||
|
// Delete user
|
||||||
|
delete("/users/{id}") {
|
||||||
|
val id = call.parameters["id"]?.toInt() ?: throw IllegalArgumentException("Invalid ID")
|
||||||
|
userService.delete(id)
|
||||||
|
call.respond(HttpStatusCode.OK)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
21
src/main/kotlin/app/revanced/api/plugins/Dependencies.kt
Normal file
21
src/main/kotlin/app/revanced/api/plugins/Dependencies.kt
Normal file
@ -0,0 +1,21 @@
|
|||||||
|
package app.revanced.api.plugins
|
||||||
|
|
||||||
|
import app.revanced.api.backend.github.GitHubBackend
|
||||||
|
import io.github.cdimascio.dotenv.Dotenv
|
||||||
|
import io.ktor.server.application.*
|
||||||
|
import org.koin.core.context.startKoin
|
||||||
|
import org.koin.dsl.module
|
||||||
|
import org.koin.ktor.ext.inject
|
||||||
|
import org.koin.ktor.plugin.Koin
|
||||||
|
|
||||||
|
fun Application.configureDependencies() {
|
||||||
|
|
||||||
|
install(Koin) {
|
||||||
|
modules(
|
||||||
|
module {
|
||||||
|
single { Dotenv.load() }
|
||||||
|
single { GitHubBackend(get<Dotenv>().get("GITHUB_TOKEN")) }
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
37
src/main/kotlin/app/revanced/api/plugins/HTTP.kt
Normal file
37
src/main/kotlin/app/revanced/api/plugins/HTTP.kt
Normal file
@ -0,0 +1,37 @@
|
|||||||
|
package app.revanced.api.plugins
|
||||||
|
|
||||||
|
import io.ktor.http.*
|
||||||
|
import io.ktor.http.content.*
|
||||||
|
import io.ktor.server.application.*
|
||||||
|
import io.ktor.server.plugins.cachingheaders.*
|
||||||
|
import io.ktor.server.plugins.conditionalheaders.*
|
||||||
|
import io.ktor.server.plugins.cors.routing.*
|
||||||
|
import io.ktor.server.plugins.openapi.*
|
||||||
|
import io.ktor.server.plugins.swagger.*
|
||||||
|
import io.ktor.server.routing.*
|
||||||
|
|
||||||
|
fun Application.configureHTTP() {
|
||||||
|
install(ConditionalHeaders)
|
||||||
|
routing {
|
||||||
|
swaggerUI(path = "openapi")
|
||||||
|
}
|
||||||
|
routing {
|
||||||
|
openAPI(path = "openapi")
|
||||||
|
}
|
||||||
|
install(CORS) {
|
||||||
|
allowMethod(HttpMethod.Options)
|
||||||
|
allowMethod(HttpMethod.Put)
|
||||||
|
allowMethod(HttpMethod.Delete)
|
||||||
|
allowMethod(HttpMethod.Patch)
|
||||||
|
allowHeader(HttpHeaders.Authorization)
|
||||||
|
anyHost() // @TODO: Don't do this in production if possible. Try to limit it.
|
||||||
|
}
|
||||||
|
install(CachingHeaders) {
|
||||||
|
options { _, outgoingContent ->
|
||||||
|
when (outgoingContent.contentType?.withoutParameters()) {
|
||||||
|
ContentType.Text.CSS -> CachingOptions(CacheControl.MaxAge(maxAgeSeconds = 24 * 60 * 60))
|
||||||
|
else -> null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
45
src/main/kotlin/app/revanced/api/plugins/Routing.kt
Normal file
45
src/main/kotlin/app/revanced/api/plugins/Routing.kt
Normal file
@ -0,0 +1,45 @@
|
|||||||
|
package app.revanced.api.plugins
|
||||||
|
|
||||||
|
import app.revanced.api.backend.github.GitHubBackend
|
||||||
|
import io.github.cdimascio.dotenv.Dotenv
|
||||||
|
import io.ktor.http.*
|
||||||
|
import io.ktor.server.application.*
|
||||||
|
import io.ktor.server.http.content.*
|
||||||
|
import io.ktor.server.response.*
|
||||||
|
import io.ktor.server.routing.*
|
||||||
|
import org.koin.ktor.ext.inject
|
||||||
|
|
||||||
|
fun Application.configureRouting() {
|
||||||
|
val backend by inject<GitHubBackend>()
|
||||||
|
val dotenv by inject<Dotenv>()
|
||||||
|
|
||||||
|
routing {
|
||||||
|
route("/v${dotenv.get("API_VERSION", "1")}") {
|
||||||
|
route("/manager") {
|
||||||
|
get("/contributors") {
|
||||||
|
val contributors = backend.getContributors("revanced", "revanced-patches")
|
||||||
|
|
||||||
|
call.respond(contributors)
|
||||||
|
}
|
||||||
|
|
||||||
|
get("/members") {
|
||||||
|
val members = backend.getMembers("revanced")
|
||||||
|
|
||||||
|
call.respond(members)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
route("/patches") {
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
route("/ping") {
|
||||||
|
handle {
|
||||||
|
call.respond(HttpStatusCode.NoContent)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
staticResources("/", "static")
|
||||||
|
}
|
||||||
|
}
|
30
src/main/kotlin/app/revanced/api/plugins/Security.kt
Normal file
30
src/main/kotlin/app/revanced/api/plugins/Security.kt
Normal file
@ -0,0 +1,30 @@
|
|||||||
|
package app.revanced.api.plugins
|
||||||
|
|
||||||
|
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.*
|
||||||
|
|
||||||
|
fun Application.configureSecurity() {
|
||||||
|
// Please read the jwt property from the config file if you are using EngineMain
|
||||||
|
val jwtAudience = "jwt-audience"
|
||||||
|
val jwtDomain = "https://jwt-provider-domain/"
|
||||||
|
val jwtRealm = "ktor sample app"
|
||||||
|
val jwtSecret = "secret"
|
||||||
|
authentication {
|
||||||
|
jwt {
|
||||||
|
realm = jwtRealm
|
||||||
|
verifier(
|
||||||
|
JWT
|
||||||
|
.require(Algorithm.HMAC256(jwtSecret))
|
||||||
|
.withAudience(jwtAudience)
|
||||||
|
.withIssuer(jwtDomain)
|
||||||
|
.build()
|
||||||
|
)
|
||||||
|
validate { credential ->
|
||||||
|
if (credential.payload.audience.contains(jwtAudience)) JWTPrincipal(credential.payload) else null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
11
src/main/kotlin/app/revanced/api/plugins/Serialization.kt
Normal file
11
src/main/kotlin/app/revanced/api/plugins/Serialization.kt
Normal file
@ -0,0 +1,11 @@
|
|||||||
|
package app.revanced.api.plugins
|
||||||
|
|
||||||
|
import io.ktor.serialization.kotlinx.json.*
|
||||||
|
import io.ktor.server.application.*
|
||||||
|
import io.ktor.server.plugins.contentnegotiation.*
|
||||||
|
|
||||||
|
fun Application.configureSerialization() {
|
||||||
|
install(ContentNegotiation) {
|
||||||
|
json()
|
||||||
|
}
|
||||||
|
}
|
59
src/main/kotlin/app/revanced/api/plugins/UsersSchema.kt
Normal file
59
src/main/kotlin/app/revanced/api/plugins/UsersSchema.kt
Normal file
@ -0,0 +1,59 @@
|
|||||||
|
package app.revanced.api.plugins
|
||||||
|
|
||||||
|
import org.jetbrains.exposed.sql.transactions.transaction
|
||||||
|
import org.jetbrains.exposed.sql.transactions.experimental.newSuspendedTransaction
|
||||||
|
import org.jetbrains.exposed.sql.SqlExpressionBuilder.eq
|
||||||
|
import kotlinx.serialization.Serializable
|
||||||
|
import kotlinx.coroutines.Dispatchers
|
||||||
|
import org.jetbrains.exposed.sql.*
|
||||||
|
|
||||||
|
@Serializable
|
||||||
|
data class ExposedUser(val name: String, val age: Int)
|
||||||
|
class UserService(private val database: Database) {
|
||||||
|
object Users : Table() {
|
||||||
|
val id = integer("id").autoIncrement()
|
||||||
|
val name = varchar("name", length = 50)
|
||||||
|
val age = integer("age")
|
||||||
|
|
||||||
|
override val primaryKey = PrimaryKey(id)
|
||||||
|
}
|
||||||
|
|
||||||
|
init {
|
||||||
|
transaction(database) {
|
||||||
|
SchemaUtils.create(Users)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
suspend fun <T> dbQuery(block: suspend () -> T): T =
|
||||||
|
newSuspendedTransaction(Dispatchers.IO) { block() }
|
||||||
|
|
||||||
|
suspend fun create(user: ExposedUser): Int = dbQuery {
|
||||||
|
Users.insert {
|
||||||
|
it[name] = user.name
|
||||||
|
it[age] = user.age
|
||||||
|
}[Users.id]
|
||||||
|
}
|
||||||
|
|
||||||
|
suspend fun read(id: Int): ExposedUser? {
|
||||||
|
return dbQuery {
|
||||||
|
Users.select { Users.id eq id }
|
||||||
|
.map { ExposedUser(it[Users.name], it[Users.age]) }
|
||||||
|
.singleOrNull()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
suspend fun update(id: Int, user: ExposedUser) {
|
||||||
|
dbQuery {
|
||||||
|
Users.update({ Users.id eq id }) {
|
||||||
|
it[name] = user.name
|
||||||
|
it[age] = user.age
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
suspend fun delete(id: Int) {
|
||||||
|
dbQuery {
|
||||||
|
Users.deleteWhere { Users.id.eq(id) }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
12
src/main/resources/logback.xml
Normal file
12
src/main/resources/logback.xml
Normal file
@ -0,0 +1,12 @@
|
|||||||
|
<configuration>
|
||||||
|
<appender name="STDOUT" class="ch.qos.logback.core.ConsoleAppender">
|
||||||
|
<encoder>
|
||||||
|
<pattern>%d{YYYY-MM-dd HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n</pattern>
|
||||||
|
</encoder>
|
||||||
|
</appender>
|
||||||
|
<root level="trace">
|
||||||
|
<appender-ref ref="STDOUT"/>
|
||||||
|
</root>
|
||||||
|
<logger name="org.eclipse.jetty" level="INFO"/>
|
||||||
|
<logger name="io.netty" level="INFO"/>
|
||||||
|
</configuration>
|
23
src/main/resources/openapi/documentation.yaml
Normal file
23
src/main/resources/openapi/documentation.yaml
Normal file
@ -0,0 +1,23 @@
|
|||||||
|
openapi: "3.0.3"
|
||||||
|
info:
|
||||||
|
title: "Application API"
|
||||||
|
description: "Application API"
|
||||||
|
version: "1.0.0"
|
||||||
|
servers:
|
||||||
|
- url: "http://0.0.0.0:8080"
|
||||||
|
paths:
|
||||||
|
/:
|
||||||
|
get:
|
||||||
|
description: "Hello World!"
|
||||||
|
responses:
|
||||||
|
"200":
|
||||||
|
description: "OK"
|
||||||
|
content:
|
||||||
|
text/plain:
|
||||||
|
schema:
|
||||||
|
type: "string"
|
||||||
|
examples:
|
||||||
|
Example#1:
|
||||||
|
value: "Hello World!"
|
||||||
|
components:
|
||||||
|
schemas:
|
99
src/main/resources/static/about.json
Normal file
99
src/main/resources/static/about.json
Normal file
@ -0,0 +1,99 @@
|
|||||||
|
{
|
||||||
|
"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":
|
||||||
|
[
|
||||||
|
{
|
||||||
|
"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
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"donations":
|
||||||
|
{
|
||||||
|
"wallets":
|
||||||
|
[
|
||||||
|
{
|
||||||
|
"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":
|
||||||
|
[
|
||||||
|
{
|
||||||
|
"name": "Open Collective",
|
||||||
|
"url": "https://opencollective.com/revanced",
|
||||||
|
"preferred": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "GitHub Sponsors",
|
||||||
|
"url": "https://github.com/sponsors/ReVanced",
|
||||||
|
"preferred": false
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
2
src/main/resources/static/robots.txt
Normal file
2
src/main/resources/static/robots.txt
Normal file
@ -0,0 +1,2 @@
|
|||||||
|
User-agent: *
|
||||||
|
Disallow: /
|
21
src/test/kotlin/app/revanced/ApplicationTest.kt
Normal file
21
src/test/kotlin/app/revanced/ApplicationTest.kt
Normal file
@ -0,0 +1,21 @@
|
|||||||
|
package app.revanced
|
||||||
|
|
||||||
|
import app.revanced.api.plugins.configureRouting
|
||||||
|
import io.ktor.client.request.*
|
||||||
|
import io.ktor.client.statement.*
|
||||||
|
import io.ktor.http.*
|
||||||
|
import io.ktor.server.testing.*
|
||||||
|
import kotlin.test.*
|
||||||
|
|
||||||
|
class ApplicationTest {
|
||||||
|
@Test
|
||||||
|
fun testRoot() = testApplication {
|
||||||
|
application {
|
||||||
|
configureRouting()
|
||||||
|
}
|
||||||
|
client.get("/").apply {
|
||||||
|
assertEquals(HttpStatusCode.OK, status)
|
||||||
|
assertEquals("Hello World!", bodyAsText())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
Binary file not shown.
Before Width: | Height: | Size: 20 KiB |
Loading…
x
Reference in New Issue
Block a user