mirror of
https://github.com/revanced/revanced-api.git
synced 2025-05-02 07:34:29 +02:00
feat: Add announcements endpoints (#91)
Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com> Co-authored-by: Alexandre Teles (afterSt0rm) <alexandre.teles@ufba.br>
This commit is contained in:
parent
c65b43aff3
commit
8583e2a2bb
54
.github/workflows/codeql.yml
vendored
Normal file
54
.github/workflows/codeql.yml
vendored
Normal 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@v3
|
||||||
|
|
||||||
|
- 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}}"
|
2
.github/workflows/pytest.yml
vendored
2
.github/workflows/pytest.yml
vendored
@ -26,7 +26,7 @@ jobs:
|
|||||||
- 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: |
|
||||||
|
45
.github/workflows/qodana.yml
vendored
45
.github/workflows/qodana.yml
vendored
@ -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@v4
|
|
||||||
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.8
|
|
||||||
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
|
|
1
.gitignore
vendored
1
.gitignore
vendored
@ -161,3 +161,4 @@ cython_debug/
|
|||||||
|
|
||||||
# custom
|
# custom
|
||||||
env.sh
|
env.sh
|
||||||
|
persistance/database.db
|
||||||
|
@ -16,11 +16,5 @@ repos:
|
|||||||
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"
|
||||||
|
@ -4,7 +4,7 @@ FROM python:3.11-slim as dependencies
|
|||||||
WORKDIR /usr/src/app
|
WORKDIR /usr/src/app
|
||||||
|
|
||||||
RUN apt-get update && \
|
RUN apt-get update && \
|
||||||
apt-get install -y --no-install-recommends gcc \
|
apt-get install -y --no-install-recommends gcc git \
|
||||||
&& rm -rf /var/lib/apt/lists/*
|
&& rm -rf /var/lib/apt/lists/*
|
||||||
|
|
||||||
RUN python -m venv /opt/venv
|
RUN python -m venv /opt/venv
|
||||||
@ -28,6 +28,8 @@ ENV PATH="/opt/venv/bin:$PATH"
|
|||||||
COPY --from=dependencies /opt/venv /opt/venv
|
COPY --from=dependencies /opt/venv /opt/venv
|
||||||
COPY . .
|
COPY . .
|
||||||
|
|
||||||
|
VOLUME persistance
|
||||||
|
|
||||||
CMD docker/run-backend.sh
|
CMD docker/run-backend.sh
|
||||||
HEALTHCHECK CMD docker/run-healthcheck.sh
|
HEALTHCHECK CMD docker/run-healthcheck.sh
|
||||||
|
|
||||||
|
@ -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:
|
||||||
|
|
||||||
|
@ -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
242
api/announcements.py
Normal 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
54
api/login.py
Normal 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)
|
37
api/models/announcements.py
Normal file
37
api/models/announcements.py
Normal 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
|
10
app.py
10
app.py
@ -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
40
auth.py
Normal 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()
|
6
data/database.py
Normal file
6
data/database.py
Normal 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
77
data/models.py
Normal 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)
|
@ -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
|
||||||
|
7
limiter.py
Normal file
7
limiter.py
Normal 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
0
persistance/.gitkeep
Normal file
1260
poetry.lock
generated
1260
poetry.lock
generated
File diff suppressed because it is too large
Load Diff
@ -28,13 +28,19 @@ pytest-emoji = "^0.2.0"
|
|||||||
coverage = "^7.3.2"
|
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.21"
|
||||||
|
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"]
|
||||||
|
@ -1,50 +1,75 @@
|
|||||||
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==4.0.0 ; 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.1.0 ; 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.1 ; 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.1 ; 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.18.0 ; 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.25.0 ; 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"
|
||||||
|
passlib==1.7.4 ; 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"
|
pluggy==1.3.0 ; python_version >= "3.11" and python_version < "4.0"
|
||||||
pycares==4.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"
|
||||||
|
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==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.2.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.5 ; 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"
|
||||||
@ -55,4 +80,5 @@ 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"
|
||||||
|
Loading…
x
Reference in New Issue
Block a user