mirror of
https://github.com/revanced/revanced-api.git
synced 2025-04-29 22:24:31 +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
|
||||
__pycache__/
|
||||
*.py[cod]
|
||||
*$py.class
|
||||
|
||||
# C extensions
|
||||
*.so
|
||||
|
||||
# Distribution / packaging
|
||||
.Python
|
||||
.gradle
|
||||
build/
|
||||
develop-eggs/
|
||||
dist/
|
||||
downloads/
|
||||
eggs/
|
||||
.eggs/
|
||||
lib/
|
||||
lib64/
|
||||
parts/
|
||||
sdist/
|
||||
var/
|
||||
wheels/
|
||||
share/python-wheels/
|
||||
*.egg-info/
|
||||
.installed.cfg
|
||||
*.egg
|
||||
MANIFEST
|
||||
!gradle/wrapper/gradle-wrapper.jar
|
||||
!**/src/main/**/build/
|
||||
!**/src/test/**/build/
|
||||
|
||||
# PyInstaller
|
||||
# Usually these files are written by a python script from a template
|
||||
# before PyInstaller builds the exe, so as to inject date/other infos into it.
|
||||
*.manifest
|
||||
*.spec
|
||||
### STS ###
|
||||
.apt_generated
|
||||
.classpath
|
||||
.factorypath
|
||||
.project
|
||||
.settings
|
||||
.springBeans
|
||||
.sts4-cache
|
||||
bin/
|
||||
!**/src/main/**/bin/
|
||||
!**/src/test/**/bin/
|
||||
|
||||
# Installer logs
|
||||
pip-log.txt
|
||||
pip-delete-this-directory.txt
|
||||
### IntelliJ IDEA ###
|
||||
.idea
|
||||
*.iws
|
||||
*.iml
|
||||
*.ipr
|
||||
out/
|
||||
!**/src/main/**/out/
|
||||
!**/src/test/**/out/
|
||||
|
||||
# Unit test / coverage reports
|
||||
htmlcov/
|
||||
.tox/
|
||||
.nox/
|
||||
.coverage
|
||||
.coverage.*
|
||||
.cache
|
||||
nosetests.xml
|
||||
coverage.xml
|
||||
*.cover
|
||||
*.py,cover
|
||||
.hypothesis/
|
||||
.pytest_cache/
|
||||
cover/
|
||||
### NetBeans ###
|
||||
/nbproject/private/
|
||||
/nbbuild/
|
||||
/dist/
|
||||
/nbdist/
|
||||
/.nb-gradle/
|
||||
|
||||
# Translations
|
||||
*.mo
|
||||
*.pot
|
||||
### VS Code ###
|
||||
.vscode/
|
||||
|
||||
# Django stuff:
|
||||
*.log
|
||||
local_settings.py
|
||||
db.sqlite3
|
||||
db.sqlite3-journal
|
||||
|
||||
# Flask stuff:
|
||||
instance/
|
||||
.webassets-cache
|
||||
|
||||
# Scrapy stuff:
|
||||
.scrapy
|
||||
|
||||
# Sphinx documentation
|
||||
docs/_build/
|
||||
|
||||
# PyBuilder
|
||||
.pybuilder/
|
||||
target/
|
||||
|
||||
# Jupyter Notebook
|
||||
.ipynb_checkpoints
|
||||
|
||||
# IPython
|
||||
profile_default/
|
||||
ipython_config.py
|
||||
|
||||
# pyenv
|
||||
# For a library or package, you might want to ignore these files since the code is
|
||||
# intended to run in multiple environments; otherwise, check them in:
|
||||
# .python-version
|
||||
|
||||
# pipenv
|
||||
# According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control.
|
||||
# However, in case of collaboration, if having platform-specific dependencies or dependencies
|
||||
# having no cross-platform support, pipenv may install dependencies that don't work, or not
|
||||
# install all needed dependencies.
|
||||
#Pipfile.lock
|
||||
|
||||
# poetry
|
||||
# Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control.
|
||||
# This is especially recommended for binary packages to ensure reproducibility, and is more
|
||||
# commonly ignored for libraries.
|
||||
# https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control
|
||||
#poetry.lock
|
||||
|
||||
# pdm
|
||||
# Similar to Pipfile.lock, it is generally recommended to include pdm.lock in version control.
|
||||
#pdm.lock
|
||||
# pdm stores project-wide configurations in .pdm.toml, but it is recommended to not include it
|
||||
# in version control.
|
||||
# https://pdm.fming.dev/#use-with-ide
|
||||
.pdm.toml
|
||||
|
||||
# PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-project/pdm
|
||||
__pypackages__/
|
||||
|
||||
# Celery stuff
|
||||
celerybeat-schedule
|
||||
celerybeat.pid
|
||||
|
||||
# SageMath parsed files
|
||||
*.sage.py
|
||||
|
||||
# Environments
|
||||
.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
|
||||
### Project ###
|
||||
.env
|
@ -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