Merge pull request #102 from ReVanced/dev

This commit is contained in:
Alexandre Teles (afterSt0rm)
2023-10-19 17:24:39 -03:00
committed by GitHub
29 changed files with 1990 additions and 717 deletions

6
.dockerignore Normal file
View File

@@ -0,0 +1,6 @@
.idea
.devcontainer
.git
.gitignore
.github
.vscode

54
.github/workflows/codeql.yml vendored Normal file
View File

@@ -0,0 +1,54 @@
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@v4
with:
python-version: "3.11.6"
- 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@v2
with:
languages: ${{ matrix.language }}
queries: security-and-quality
- name: Autobuild
uses: github/codeql-action/autobuild@v2
- name: Perform CodeQL Analysis
uses: github/codeql-action/analyze@v2
with:
category: "/language:${{matrix.language}}"

View File

@@ -17,7 +17,7 @@ jobs:
runs-on: ubuntu-latest runs-on: ubuntu-latest
name: Security check name: Security check
steps: steps:
- uses: actions/checkout@v3 - uses: actions/checkout@v4
- name: Security Checks (PyCharm Security) - name: Security Checks (PyCharm Security)
uses: tonybaloney/pycharm-security@master uses: tonybaloney/pycharm-security@master
with: with:
@@ -33,24 +33,24 @@ jobs:
steps: steps:
- name: Checkout Dockerfile - name: Checkout Dockerfile
id: checkout id: checkout
uses: actions/checkout@v3 uses: actions/checkout@v4
with: with:
ref: ${{ inputs.branch }} ref: ${{ inputs.branch }}
- name: Setup QEMU - name: Setup QEMU
id: qemu id: qemu
uses: docker/setup-qemu-action@v2 uses: docker/setup-qemu-action@v3
with: with:
image: tonistiigi/binfmt:latest image: tonistiigi/binfmt:latest
platforms: all platforms: all
- name: Setup Docker Buildx - name: Setup Docker Buildx
id: buildx id: buildx
uses: docker/setup-buildx-action@v2 uses: docker/setup-buildx-action@v3
- name: Extract metadata (tags, labels) for Docker - name: Extract metadata (tags, labels) for Docker
id: meta id: meta
uses: docker/metadata-action@v4 uses: docker/metadata-action@v5
with: with:
images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }} images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}
flavor: | flavor: |
@@ -59,7 +59,7 @@ jobs:
- name: Build Docker image - name: Build Docker image
id: build id: build
uses: docker/build-push-action@v4 uses: docker/build-push-action@v5
with: with:
context: . context: .
file: ./Dockerfile file: ./Dockerfile

View File

@@ -22,22 +22,22 @@ jobs:
steps: steps:
- name: Checkout Dockerfile - name: Checkout Dockerfile
id: checkout id: checkout
uses: actions/checkout@v3 uses: actions/checkout@v4
- name: Setup QEMU - name: Setup QEMU
id: qemu id: qemu
uses: docker/setup-qemu-action@v2 uses: docker/setup-qemu-action@v3
with: with:
image: tonistiigi/binfmt:latest image: tonistiigi/binfmt:latest
platforms: all platforms: all
- name: Setup Docker Buildx - name: Setup Docker Buildx
id: buildx id: buildx
uses: docker/setup-buildx-action@v2 uses: docker/setup-buildx-action@v3
- name: Login to GitHub Container Registry - name: Login to GitHub Container Registry
id: ghcr id: ghcr
uses: docker/login-action@v2 uses: docker/login-action@v3
with: with:
registry: ${{ env.REGISTRY }} registry: ${{ env.REGISTRY }}
username: ${{ github.repository_owner }} username: ${{ github.repository_owner }}
@@ -45,7 +45,7 @@ jobs:
- name: Extract metadata (tags, labels) for Docker - name: Extract metadata (tags, labels) for Docker
id: meta id: meta
uses: docker/metadata-action@v4 uses: docker/metadata-action@v5
with: with:
images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }} images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}
flavor: | flavor: |
@@ -54,7 +54,7 @@ jobs:
- name: Build and push main Docker image - name: Build and push main Docker image
id: build id: build
uses: docker/build-push-action@v4 uses: docker/build-push-action@v5
with: with:
build-args: GH_TOKEN=${{ secrets.GH_TOKEN }} build-args: GH_TOKEN=${{ secrets.GH_TOKEN }}
context: . context: .

View File

@@ -21,12 +21,12 @@ jobs:
steps: steps:
- name: Checkout repository - name: Checkout repository
uses: actions/checkout@v3 uses: actions/checkout@v4
- name: Set up Python - name: Set up Python
uses: actions/setup-python@v4 uses: actions/setup-python@v4
with: with:
python-version: "3.11.4" python-version: "3.11.6"
- name: Install project dependencies - name: Install project dependencies
run: | run: |

View File

@@ -1,45 +0,0 @@
name: "Qodana | Code Quality Scan and Static Analysis"
on:
push:
branches: [dev]
pull_request:
types: [opened, reopened, edited, synchronize]
workflow_dispatch:
env:
default_branch: dev
jobs:
qodana:
timeout-minutes: 15
runs-on: ubuntu-latest
steps:
- name: "Checkout"
uses: actions/checkout@v3
with:
fetch-depth: 0
- name: Set up Python
uses: actions/setup-python@v4
with:
python-version: "3.11.4"
- name: Install project dependencies
run: |
python -m pip install --upgrade pip
if [ -f requirements.txt ];
then pip install -r requirements.txt;
fi
- name: "Qodana Scan"
uses: JetBrains/qodana-action@v2023.2.1
env:
QODANA_TOKEN: ${{ secrets.QODANA_TOKEN }}
with:
args: --baseline,qodana.sarif.json
- name: "Upload Qodana Report"
uses: github/codeql-action/upload-sarif@v2
with:
sarif_file: ${{ runner.temp }}/qodana/results/qodana.sarif.json

3
.gitignore vendored
View File

@@ -157,7 +157,8 @@ cython_debug/
# be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore # 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 # 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. # option (not recommended) you can uncomment the following to ignore the entire idea folder.
#.idea/ .idea/
# custom # custom
env.sh env.sh
persistance/database.db

View File

@@ -1,6 +1,6 @@
repos: repos:
- repo: https://github.com/pre-commit/pre-commit-hooks - repo: https://github.com/pre-commit/pre-commit-hooks
rev: v4.4.0 rev: v4.5.0
hooks: hooks:
- id: trailing-whitespace - id: trailing-whitespace
- id: end-of-file-fixer - id: end-of-file-fixer
@@ -12,15 +12,9 @@ repos:
- id: check-toml - id: check-toml
- id: check-merge-conflict - id: check-merge-conflict
- repo: https://github.com/psf/black - repo: https://github.com/psf/black
rev: 23.7.0 rev: 23.9.1
hooks: hooks:
- id: black - id: black
language_version: python3.11 language_version: python3.11
- repo: https://github.com/pryorda/dockerfilelint-precommit-hooks
rev: v0.1.0
hooks:
- id: dockerfilelint
stages: [commit]
ci: ci:
autoupdate_branch: "dev" autoupdate_branch: "dev"

View File

@@ -1,14 +1,36 @@
FROM python:3.11-slim ## Build dependencies
FROM python:3.11-slim as dependencies
ARG GITHUB_TOKEN
ENV GITHUB_TOKEN $GITHUB_TOKEN
WORKDIR /usr/src/app WORKDIR /usr/src/app
RUN apt-get update && \
apt-get install -y --no-install-recommends gcc git \
&& rm -rf /var/lib/apt/lists/*
RUN python -m venv /opt/venv
ENV PATH="/opt/venv/bin:$PATH"
COPY requirements.txt .
RUN pip install -r requirements.txt
## Image
FROM python:3.11-slim
WORKDIR /usr/src/app
RUN apt-get update && \
apt-get install -y --no-install-recommends curl \
&& rm -rf /var/lib/apt/lists/*
ENV PATH="/opt/venv/bin:$PATH"
COPY --from=dependencies /opt/venv /opt/venv
COPY . . COPY . .
RUN apt update && \ VOLUME persistance
apt-get install build-essential libffi-dev libssl-dev openssl --no-install-recommends -y \
&& pip install --no-cache-dir -r requirements.txt
CMD [ "python3", "-m" , "sanic", "app:app", "--fast", "--access-logs", "--motd", "--noisy-exceptions", "-H", "0.0.0.0"] CMD docker/run-backend.sh
HEALTHCHECK CMD docker/run-healthcheck.sh
EXPOSE 8000

View File

@@ -5,7 +5,7 @@
![License: AGPLv3](https://img.shields.io/github/license/revanced/revanced-api) ![License: AGPLv3](https://img.shields.io/github/license/revanced/revanced-api)
[![codecov](https://codecov.io/gh/ReVanced/revanced-api/branch/dev/graph/badge.svg?token=10H8D2CRQO)](https://codecov.io/gh/ReVanced/revanced-api) [![codecov](https://codecov.io/gh/ReVanced/revanced-api/branch/dev/graph/badge.svg?token=10H8D2CRQO)](https://codecov.io/gh/ReVanced/revanced-api)
[![Build and Publish Docker Image](https://github.com/revanced/revanced-api/actions/workflows/main.yml/badge.svg)](https://github.com/revanced/revanced-api/actions/workflows/main.yml) [![Build and Publish Docker Image](https://github.com/revanced/revanced-api/actions/workflows/main.yml/badge.svg)](https://github.com/revanced/revanced-api/actions/workflows/main.yml)
[![Qodana | Code Quality Scan](https://github.com/revanced/revanced-api/actions/workflows/quodana.yml/badge.svg)](https://github.com/revanced/revanced-api/actions/workflows/quodana.yml) [![Qodana | Code Quality Scan](https://github.com/revanced/revanced-api/actions/workflows/qodana.yml/badge.svg)](https://github.com/revanced/revanced-api/actions/workflows/qodana.yml)
[![PyTest | Testing and Code Coverage](https://github.com/revanced/revanced-api/actions/workflows/pytest.yml/badge.svg)](https://github.com/revanced/revanced-api/actions/workflows/pytest.yml) [![PyTest | Testing and Code Coverage](https://github.com/revanced/revanced-api/actions/workflows/pytest.yml/badge.svg)](https://github.com/revanced/revanced-api/actions/workflows/pytest.yml)
--- ---
@@ -20,7 +20,11 @@ To run this API, you need Python 3.11.x. You can install the dependencies with p
poetry install poetry install
``` ```
Create an environment variable called `GITHUB_TOKEN` with a valid GitHub token with read access to public repositories. 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: Then, you can run the API in development mode with:

View File

@@ -7,5 +7,9 @@ from api.socials import socials
from api.info import info from api.info import info
from api.compat import github as compat from api.compat import github as compat
from api.donations import donations from api.donations import donations
from api.announcements import announcements
from api.login import login
api = Blueprint.group(ping, github, info, socials, donations, compat, url_prefix="/") api = Blueprint.group(
login, ping, github, info, socials, donations, announcements, compat, url_prefix="/"
)

242
api/announcements.py Normal file
View File

@@ -0,0 +1,242 @@
"""
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 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 config import api_version
from limiter import limiter
announcements: Blueprint = Blueprint("announcements", version=api_version)
@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)

54
api/login.py Normal file
View File

@@ -0,0 +1,54 @@
"""
This module provides a blueprint for the login endpoint.
Routes:
- POST /login: Login to the API
"""
from sanic import Blueprint, Request
from sanic.response import JSONResponse, json
from sanic_ext import openapi
from sanic_beskar.exceptions import AuthenticationError
from auth import beskar
from limiter import limiter
from config import api_version
login: Blueprint = Blueprint("login", version=api_version)
@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)

View File

@@ -0,0 +1,37 @@
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

View File

@@ -11,6 +11,14 @@ class ContactFields(BaseModel):
email: str email: str
class BrandingFields(BaseModel):
"""
Implements the fields for the API owner branding info.
"""
logo: str
class InfoFields(BaseModel): class InfoFields(BaseModel):
""" """
Implements the fields for the API owner info. Implements the fields for the API owner info.
@@ -18,6 +26,7 @@ class InfoFields(BaseModel):
name: str name: str
about: str about: str
branding: BrandingFields
contact: ContactFields contact: ContactFields
socials: list[SocialFields] socials: list[SocialFields]
donations: DonationFields donations: DonationFields

View File

@@ -8,6 +8,7 @@ class SocialFields(BaseModel):
name: str name: str
url: str url: str
preferred: bool
class SocialsResponseModel(BaseModel): class SocialsResponseModel(BaseModel):

10
app.py
View File

@@ -6,6 +6,9 @@ from sanic_ext import Config
from api import api from api import api
from config import * from config import *
from limiter import configure_limiter
from auth import configure_auth
REDIRECTS = { REDIRECTS = {
"/": "/docs/swagger", "/": "/docs/swagger",
} }
@@ -25,8 +28,13 @@ app.config.CORS_SUPPORTS_CREDENTIALS = True
app.config.CORS_SEND_WILDCARD = True app.config.CORS_SEND_WILDCARD = True
app.config.CORS_ORIGINS = "*" app.config.CORS_ORIGINS = "*"
app.blueprint(api) # sanic-beskar
configure_auth(app)
# sanic-limiter
configure_limiter(app)
app.blueprint(api)
# https://sanic.dev/en/guide/how-to/static-redirects.html # https://sanic.dev/en/guide/how-to/static-redirects.html

40
auth.py Normal file
View File

@@ -0,0 +1,40 @@
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()

View File

@@ -55,14 +55,18 @@ compat_repositories: list = [
# Social Links # Social Links
social_links: list[dict[str, str]] = [ social_links: list[dict[str, str | bool]] = [
{"name": "Website", "url": "https://revanced.app"}, {"name": "Website", "url": "https://revanced.app", "preferred": True},
{"name": "GitHub", "url": "https://github.com/revanced"}, {"name": "GitHub", "url": "https://github.com/revanced", "preferred": False},
{"name": "Twitter", "url": "https://twitter.com/revancedapp"}, {"name": "Twitter", "url": "https://twitter.com/revancedapp", "preferred": False},
{"name": "Discord", "url": "https://revanced.app/discord"}, {"name": "Discord", "url": "https://revanced.app/discord", "preferred": True},
{"name": "Reddit", "url": "https://www.reddit.com/r/revancedapp"}, {
{"name": "Telegram", "url": "https://t.me/app_revanced"}, "name": "Reddit",
{"name": "YouTube", "url": "https://www.youtube.com/@ReVanced"}, "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 # Donation info
@@ -116,6 +120,9 @@ links: list[dict[str, str | bool]] = [
default_info: dict[str, str | list[str | bool] | bool] = { default_info: dict[str, str | list[str | bool] | bool] = {
"name": "ReVanced", "name": "ReVanced",
"about": "ReVanced was born out of Vanced's discontinuation and it is our goal to continue the legacy of what Vanced left behind. Thanks to ReVanced Patcher, it's possible to create long-lasting patches for nearly any Android app. ReVanced's patching system is designed to allow patches to work on new versions of the apps automatically with bare minimum maintenance.", "about": "ReVanced was born out of Vanced's discontinuation and it is our goal to continue the legacy of what Vanced left behind. Thanks to ReVanced Patcher, it's possible to create long-lasting patches for nearly any Android app. ReVanced's patching system is designed to allow patches to work on new versions of the apps automatically with bare minimum maintenance.",
"branding": {
"logo": "https://raw.githubusercontent.com/ReVanced/revanced-branding/main/assets/revanced-logo/revanced-logo.svg"
},
"contact": {"email": "contact@revanced.app"}, "contact": {"email": "contact@revanced.app"},
"socials": social_links, "socials": social_links,
"donations": {"wallets": wallets, "links": links}, "donations": {"wallets": wallets, "links": links},

6
data/database.py Normal file
View File

@@ -0,0 +1,6 @@
from sqlalchemy import create_engine
from sqlalchemy.orm import sessionmaker
engine = create_engine("sqlite:///persistance/database.db")
Session = sessionmaker(bind=engine)

77
data/models.py Normal file
View File

@@ -0,0 +1,77 @@
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)

View File

@@ -4,8 +4,13 @@ services:
revanced-api: revanced-api:
container_name: revanced-api container_name: revanced-api
image: ghcr.io/revanced/revanced-api:latest image: ghcr.io/revanced/revanced-api:latest
volumes:
- /data/revanced-api:/usr/src/app/persistence
environment: environment:
- GITHUB_TOKEN=YOUR_GITHUB_TOKEN - GITHUB_TOKEN=YOUR_GITHUB_TOKEN
- SECRET_KEY=YOUR_SECRET_KEY
- USERNAME=YOUR_USERNAME
- PASSWORD=YOUR_PASSWORD
ports: ports:
- 127.0.0.1:7934:8000 - 127.0.0.1:7934:8000
restart: unless-stopped restart: unless-stopped

1
docker/run-backend.sh Executable file
View File

@@ -0,0 +1 @@
python3 -m sanic /usr/src/app --fast --access-logs --motd --noisy-exceptions -H 0.0.0.0

1
docker/run-healthcheck.sh Executable file
View File

@@ -0,0 +1 @@
curl --fail http://0.0.0.0:8000/docs

7
limiter.py Normal file
View File

@@ -0,0 +1,7 @@
from sanic_limiter import Limiter, get_remote_address
limiter = Limiter(key_func=get_remote_address)
def configure_limiter(app):
limiter.init_app(app)

0
persistance/.gitkeep Normal file
View File

1910
poetry.lock generated

File diff suppressed because it is too large Load Diff

View File

@@ -8,16 +8,16 @@ readme = "README.md"
[tool.poetry.dependencies] [tool.poetry.dependencies]
python = "^3.11" python = "^3.11"
aiohttp = {version = "^3.8.5", extras = ["speedups"]} aiohttp = {version = "^3.8.6", extras = ["speedups"]}
sanic = {version = "^23.6.0", extras = ["ext"]} sanic = {version = "^23.6.0", extras = ["ext"]}
ujson = "^5.8.0" ujson = "^5.8.0"
asyncstdlib = "^3.10.8" asyncstdlib = "^3.10.9"
pydantic = "^1.10.12" pydantic = "^1.10.13"
cytoolz = "^0.12.2" cytoolz = "^0.12.2"
beautifulsoup4 = "^4.12.2" beautifulsoup4 = "^4.12.2"
setuptools = "^68.1.2" setuptools = "^68.1.2"
lxml = "^4.9.3" lxml = "^4.9.3"
mypy = "^1.5.1" mypy = "^1.6.1"
types-ujson = "^5.8.0.1" types-ujson = "^5.8.0.1"
types-aiofiles = "^23.2.0.0" types-aiofiles = "^23.2.0.0"
sanic-testing = "^23.6.0" sanic-testing = "^23.6.0"
@@ -25,16 +25,22 @@ pytest-asyncio = "^0.21.1"
types-beautifulsoup4 = "^4.12.0.6" types-beautifulsoup4 = "^4.12.0.6"
pytest-md = "^0.2.0" pytest-md = "^0.2.0"
pytest-emoji = "^0.2.0" pytest-emoji = "^0.2.0"
coverage = "^7.3.0" coverage = "^7.3.2"
pytest-cov = "^4.1.0" pytest-cov = "^4.1.0"
pytest = "^7.4.0" pytest = "^7.4.0"
sqlalchemy = "^2.0.22"
sanic-beskar = "^2.2.12"
bson = "^0.5.10"
fastpbkdf2 = "^0.2"
cryptography = "^41.0.4"
sanic-limiter = { git = "https://github.com/Omegastick/sanic-limiter" }
[tool.pytest.ini_options] [tool.pytest.ini_options]
asyncio_mode = "auto" asyncio_mode = "auto"
filterwarnings = [ filterwarnings = [
"ignore::DeprecationWarning", "ignore::DeprecationWarning",
"ignore::pytest.PytestCollectionWarning" "ignore::pytest.PytestCollectionWarning",
] ]
[build-system] [build-system]
requires = ["poetry-core"] requires = ["poetry-core"]

View File

@@ -1,58 +1,84 @@
aiodns==3.0.0 ; python_version >= "3.11" and python_version < "4.0" aiodns==3.1.0 ; python_version >= "3.11" and python_version < "4.0"
aiofiles==23.2.1 ; python_version >= "3.11" and python_version < "4.0" aiofiles==23.2.1 ; python_version >= "3.11" and python_version < "4.0"
aiohttp[speedups]==3.8.5 ; python_version >= "3.11" and python_version < "4.0" aiohttp[speedups]==3.8.6 ; python_version >= "3.11" and python_version < "4.0"
aiosignal==1.3.1 ; python_version >= "3.11" and python_version < "4.0" aiosignal==1.3.1 ; python_version >= "3.11" and python_version < "4.0"
anyio==3.7.1 ; python_version >= "3.11" and python_version < "4.0" anyio==4.0.0 ; python_version >= "3.11" and python_version < "4.0"
argon2-cffi==23.1.0 ; python_version >= "3.11" and python_version < "4.0"
argon2-cffi-bindings==21.2.0 ; python_version >= "3.11" and python_version < "4.0"
async-timeout==4.0.3 ; python_version >= "3.11" and python_version < "4.0" async-timeout==4.0.3 ; python_version >= "3.11" and python_version < "4.0"
asyncstdlib==3.10.8 ; python_version >= "3.11" and python_version < "4.0" asyncstdlib==3.10.9 ; python_version >= "3.11" and python_version < "4.0"
attrs==23.1.0 ; python_version >= "3.11" and python_version < "4.0" attrs==23.1.0 ; python_version >= "3.11" and python_version < "4.0"
beautifulsoup4==4.12.2 ; python_version >= "3.11" and python_version < "4.0" beautifulsoup4==4.12.2 ; python_version >= "3.11" and python_version < "4.0"
brotli==1.0.9 ; python_version >= "3.11" and python_version < "4.0" brotli==1.1.0 ; python_version >= "3.11" and python_version < "4.0"
bson==0.5.10 ; python_version >= "3.11" and python_version < "4.0"
certifi==2023.7.22 ; python_version >= "3.11" and python_version < "4.0" certifi==2023.7.22 ; python_version >= "3.11" and python_version < "4.0"
cffi==1.15.1 ; python_version >= "3.11" and python_version < "4.0" cffi==1.16.0 ; python_version >= "3.11" and python_version < "4.0"
charset-normalizer==3.2.0 ; python_version >= "3.11" and python_version < "4.0" charset-normalizer==3.3.0 ; python_version >= "3.11" and python_version < "4.0"
colorama==0.4.6 ; python_version >= "3.11" and python_version < "4.0" and sys_platform == "win32" colorama==0.4.6 ; python_version >= "3.11" and python_version < "4.0" and sys_platform == "win32"
coverage==7.3.0 ; python_version >= "3.11" and python_version < "4.0" coverage==7.3.2 ; python_version >= "3.11" and python_version < "4.0"
coverage[toml]==7.3.0 ; python_version >= "3.11" and python_version < "4.0" coverage[toml]==7.3.2 ; python_version >= "3.11" and python_version < "4.0"
cryptography==41.0.4 ; python_version >= "3.11" and python_version < "4.0"
cytoolz==0.12.2 ; python_version >= "3.11" and python_version < "4.0" cytoolz==0.12.2 ; python_version >= "3.11" and python_version < "4.0"
deprecated==1.2.14 ; python_version >= "3.11" and python_version < "4.0"
fastpbkdf2==0.2 ; python_version >= "3.11" and python_version < "4.0"
frozenlist==1.4.0 ; python_version >= "3.11" and python_version < "4.0" frozenlist==1.4.0 ; python_version >= "3.11" and python_version < "4.0"
greenlet==3.0.0 ; python_version >= "3.11" and python_version < "4.0" and platform_machine == "aarch64" or python_version >= "3.11" and python_version < "4.0" and platform_machine == "ppc64le" or python_version >= "3.11" and python_version < "4.0" and platform_machine == "x86_64" or python_version >= "3.11" and python_version < "4.0" and platform_machine == "amd64" or python_version >= "3.11" and python_version < "4.0" and platform_machine == "AMD64" or python_version >= "3.11" and python_version < "4.0" and platform_machine == "win32" or python_version >= "3.11" and python_version < "4.0" and platform_machine == "WIN32"
h11==0.14.0 ; python_version >= "3.11" and python_version < "4.0" h11==0.14.0 ; python_version >= "3.11" and python_version < "4.0"
html5tagger==1.3.0 ; python_version >= "3.11" and python_version < "4.0" html5tagger==1.3.0 ; python_version >= "3.11" and python_version < "4.0"
httpcore==0.17.3 ; python_version >= "3.11" and python_version < "4.0" httpcore==0.18.0 ; python_version >= "3.11" and python_version < "4.0"
httptools==0.6.0 ; python_version >= "3.11" and python_version < "4.0" httptools==0.6.0 ; python_version >= "3.11" and python_version < "4.0"
httpx==0.24.1 ; python_version >= "3.11" and python_version < "4.0" httpx==0.25.0 ; python_version >= "3.11" and python_version < "4.0"
idna==3.4 ; python_version >= "3.11" and python_version < "4.0" idna==3.4 ; python_version >= "3.11" and python_version < "4.0"
importlib-resources==6.1.0 ; python_version >= "3.11" and python_version < "4.0"
iniconfig==2.0.0 ; python_version >= "3.11" and python_version < "4.0" iniconfig==2.0.0 ; python_version >= "3.11" and python_version < "4.0"
iso8601==2.1.0 ; python_version >= "3.11" and python_version < "4.0"
jinja2==3.1.2 ; python_version >= "3.11" and python_version < "4.0"
limits==3.6.0 ; python_version >= "3.11" and python_version < "4.0"
lxml==4.9.3 ; python_version >= "3.11" and python_version < "4.0" lxml==4.9.3 ; python_version >= "3.11" and python_version < "4.0"
markupsafe==2.1.3 ; python_version >= "3.11" and python_version < "4.0"
multidict==6.0.4 ; python_version >= "3.11" and python_version < "4.0" multidict==6.0.4 ; python_version >= "3.11" and python_version < "4.0"
mypy==1.5.1 ; python_version >= "3.11" and python_version < "4.0" mypy==1.6.0 ; python_version >= "3.11" and python_version < "4.0"
mypy-extensions==1.0.0 ; python_version >= "3.11" and python_version < "4.0" mypy-extensions==1.0.0 ; python_version >= "3.11" and python_version < "4.0"
packaging==23.1 ; python_version >= "3.11" and python_version < "4.0" packaging==23.2 ; python_version >= "3.11" and python_version < "4.0"
pluggy==1.2.0 ; python_version >= "3.11" and python_version < "4.0" passlib==1.7.4 ; python_version >= "3.11" and python_version < "4.0"
pycares==4.3.0 ; python_version >= "3.11" and python_version < "4.0" pendulum==2.1.2 ; python_version >= "3.11" and python_version < "4.0"
pluggy==1.3.0 ; python_version >= "3.11" and python_version < "4.0"
py-buzz==4.1.0 ; python_version >= "3.11" and python_version < "4.0"
pycares==4.4.0 ; python_version >= "3.11" and python_version < "4.0"
pycparser==2.21 ; python_version >= "3.11" and python_version < "4.0" pycparser==2.21 ; python_version >= "3.11" and python_version < "4.0"
pydantic==1.10.12 ; python_version >= "3.11" and python_version < "4.0" pycryptodomex==3.19.0 ; python_version >= "3.11" and python_version < "4.0"
pytest==7.4.0 ; python_version >= "3.11" and python_version < "4.0" pydantic==1.10.13 ; python_version >= "3.11" and python_version < "4.0"
pyjwt==2.8.0 ; python_version >= "3.11" and python_version < "4.0"
pyseto==1.7.5 ; python_version >= "3.11" and python_version < "4.0"
pytest==7.4.2 ; python_version >= "3.11" and python_version < "4.0"
pytest-asyncio==0.21.1 ; python_version >= "3.11" and python_version < "4.0" pytest-asyncio==0.21.1 ; python_version >= "3.11" and python_version < "4.0"
pytest-cov==4.1.0 ; python_version >= "3.11" and python_version < "4.0" pytest-cov==4.1.0 ; python_version >= "3.11" and python_version < "4.0"
pytest-emoji==0.2.0 ; python_version >= "3.11" and python_version < "4.0" pytest-emoji==0.2.0 ; python_version >= "3.11" and python_version < "4.0"
pytest-md==0.2.0 ; python_version >= "3.11" and python_version < "4.0" pytest-md==0.2.0 ; python_version >= "3.11" and python_version < "4.0"
python-dateutil==2.8.2 ; python_version >= "3.11" and python_version < "4.0"
pytzdata==2020.1 ; python_version >= "3.11" and python_version < "4.0"
pyyaml==6.0.1 ; python_version >= "3.11" and python_version < "4.0" pyyaml==6.0.1 ; python_version >= "3.11" and python_version < "4.0"
sanic==23.6.0 ; python_version >= "3.11" and python_version < "4.0"
sanic-beskar==2.2.12 ; python_version >= "3.11" and python_version < "4.0"
sanic-ext==23.6.0 ; python_version >= "3.11" and python_version < "4.0" sanic-ext==23.6.0 ; python_version >= "3.11" and python_version < "4.0"
sanic-limiter @ git+https://github.com/Omegastick/sanic-limiter ; python_version >= "3.11" and python_version < "4.0"
sanic-routing==23.6.0 ; python_version >= "3.11" and python_version < "4.0" sanic-routing==23.6.0 ; python_version >= "3.11" and python_version < "4.0"
sanic-testing==23.6.0 ; python_version >= "3.11" and python_version < "4.0" sanic-testing==23.6.0 ; python_version >= "3.11" and python_version < "4.0"
sanic[ext]==23.6.0 ; python_version >= "3.11" and python_version < "4.0" sanic[ext]==23.6.0 ; python_version >= "3.11" and python_version < "4.0"
setuptools==68.1.2 ; python_version >= "3.11" and python_version < "4.0" setuptools==68.2.2 ; python_version >= "3.11" and python_version < "4.0"
six==1.16.0 ; python_version >= "3.11" and python_version < "4.0"
sniffio==1.3.0 ; python_version >= "3.11" and python_version < "4.0" sniffio==1.3.0 ; python_version >= "3.11" and python_version < "4.0"
soupsieve==2.4.1 ; python_version >= "3.11" and python_version < "4.0" soupsieve==2.5 ; python_version >= "3.11" and python_version < "4.0"
sqlalchemy==2.0.21 ; python_version >= "3.11" and python_version < "4.0"
toolz==0.12.0 ; python_version >= "3.11" and python_version < "4.0" toolz==0.12.0 ; python_version >= "3.11" and python_version < "4.0"
tracerite==1.1.0 ; python_version >= "3.11" and python_version < "4.0" tracerite==1.1.0 ; python_version >= "3.11" and python_version < "4.0"
types-aiofiles==23.2.0.0 ; python_version >= "3.11" and python_version < "4.0" types-aiofiles==23.2.0.0 ; python_version >= "3.11" and python_version < "4.0"
types-beautifulsoup4==4.12.0.6 ; python_version >= "3.11" and python_version < "4.0" types-beautifulsoup4==4.12.0.6 ; python_version >= "3.11" and python_version < "4.0"
types-html5lib==1.1.11.15 ; python_version >= "3.11" and python_version < "4.0" types-html5lib==1.1.11.15 ; python_version >= "3.11" and python_version < "4.0"
types-ujson==5.8.0.1 ; python_version >= "3.11" and python_version < "4.0" types-ujson==5.8.0.1 ; python_version >= "3.11" and python_version < "4.0"
typing-extensions==4.7.1 ; python_version >= "3.11" and python_version < "4.0" typing-extensions==4.8.0 ; python_version >= "3.11" and python_version < "4.0"
ujson==5.8.0 ; python_version >= "3.11" and python_version < "4.0" ujson==5.8.0 ; python_version >= "3.11" and python_version < "4.0"
uvloop==0.17.0 ; sys_platform != "win32" and implementation_name == "cpython" and python_version >= "3.11" and python_version < "4.0" uvloop==0.17.0 ; sys_platform != "win32" and implementation_name == "cpython" and python_version >= "3.11" and python_version < "4.0"
websockets==11.0.3 ; python_version >= "3.11" and python_version < "4.0" websockets==11.0.3 ; python_version >= "3.11" and python_version < "4.0"
wrapt==1.15.0 ; python_version >= "3.11" and python_version < "4.0"
yarl==1.9.2 ; python_version >= "3.11" and python_version < "4.0" yarl==1.9.2 ; python_version >= "3.11" and python_version < "4.0"