mirror of
https://github.com/revanced/revanced-releases-api.git
synced 2025-04-29 22:14:28 +02:00
* refactor: import routers from old branch * refactor: import InternalCache removal * refactor: move routes into dedicated routers * fix: fixes entrypoint * refactor: add documentation and bump libs
This commit is contained in:
parent
6133b4f776
commit
0ce5780a4e
@ -20,4 +20,4 @@ RUN apt update && \
|
||||
apt-get install build-essential libffi-dev -y \
|
||||
&& pip install --no-cache-dir -r requirements.txt
|
||||
|
||||
CMD [ "/bin/bash", "./run.sh" ]
|
||||
CMD [ "python3", "./run.py" ]
|
@ -19,7 +19,6 @@ You can deploy your own instance by cloning this repository, editing the `docker
|
||||
| `REDIS_PORT` | The port of your redis server. |
|
||||
| `HYPERCORN_HOST` | The hostname/IP of the API. |
|
||||
| `HYPERCORN_PORT` | The port of the API. |
|
||||
| `HYPERCORN_LOG_LEVEL` | The log level of the API. |
|
||||
| `SENTRY_DSN` | The DSN of your Sentry instance. |
|
||||
|
||||
Please note that there are no default values for any of these variables.
|
||||
|
@ -1,10 +1,10 @@
|
||||
import toml
|
||||
from redis import asyncio as aioredis
|
||||
|
||||
import src.utils.Logger as Logger
|
||||
from src.utils.Generators import Generators
|
||||
from src.models.AnnouncementModels import AnnouncementCreateModel
|
||||
from src.utils.RedisConnector import RedisConnector
|
||||
import app.utils.Logger as Logger
|
||||
from app.utils.Generators import Generators
|
||||
from app.models.AnnouncementModels import AnnouncementCreateModel
|
||||
from app.utils.RedisConnector import RedisConnector
|
||||
|
||||
config: dict = toml.load("config.toml")
|
||||
|
@ -7,10 +7,10 @@ from redis import asyncio as aioredis
|
||||
import aiofiles
|
||||
import uvloop
|
||||
|
||||
import src.utils.Logger as Logger
|
||||
from src.utils.Generators import Generators
|
||||
from src.models.ClientModels import ClientModel
|
||||
from src.utils.RedisConnector import RedisConnector
|
||||
import app.utils.Logger as Logger
|
||||
from app.utils.Generators import Generators
|
||||
from app.models.ClientModels import ClientModel
|
||||
from app.utils.RedisConnector import RedisConnector
|
||||
|
||||
config: dict = toml.load("config.toml")
|
||||
|
@ -3,8 +3,7 @@ import asyncio
|
||||
import uvloop
|
||||
import orjson
|
||||
from base64 import b64decode
|
||||
from src.utils.HTTPXClient import HTTPXClient
|
||||
from src.utils.InternalCache import InternalCache
|
||||
from app.utils.HTTPXClient import HTTPXClient
|
||||
|
||||
|
||||
class Releases:
|
||||
@ -15,8 +14,6 @@ class Releases:
|
||||
|
||||
httpx_client = HTTPXClient.create()
|
||||
|
||||
InternalCache = InternalCache()
|
||||
|
||||
async def __get_release(self, repository: str) -> list:
|
||||
# Get assets from latest release in a given repository.
|
||||
#
|
||||
@ -65,21 +62,14 @@ class Releases:
|
||||
dict: A dictionary containing assets from each repository
|
||||
"""
|
||||
|
||||
releases: dict[str, list]
|
||||
releases: dict[str, list] = {}
|
||||
releases['tools'] = []
|
||||
|
||||
if await self.InternalCache.exists('releases'):
|
||||
releases = await self.InternalCache.get('releases')
|
||||
else:
|
||||
releases = {}
|
||||
releases['tools'] = []
|
||||
|
||||
results: list = await asyncio.gather(*[self.__get_release(repository) for repository in repositories])
|
||||
|
||||
for result in results:
|
||||
for asset in result:
|
||||
releases['tools'].append(asset)
|
||||
|
||||
await self.InternalCache.store('releases', releases)
|
||||
results: list = await asyncio.gather(*[self.__get_release(repository) for repository in repositories])
|
||||
|
||||
for result in results:
|
||||
for asset in result:
|
||||
releases['tools'].append(asset)
|
||||
|
||||
return releases
|
||||
|
||||
@ -101,11 +91,7 @@ class Releases:
|
||||
Returns:
|
||||
dict: Patches available for a given app
|
||||
"""
|
||||
if await self.InternalCache.exists('patches'):
|
||||
patches = await self.InternalCache.get('patches')
|
||||
else:
|
||||
patches = await self.__get_patches_json()
|
||||
await self.InternalCache.store('patches', patches)
|
||||
patches: dict = await self.__get_patches_json()
|
||||
|
||||
return patches
|
||||
|
||||
@ -139,22 +125,17 @@ class Releases:
|
||||
|
||||
contributors: dict[str, list]
|
||||
|
||||
if await self.InternalCache.exists('contributors'):
|
||||
contributors = await self.InternalCache.get('contributors')
|
||||
else:
|
||||
contributors = {}
|
||||
contributors['repositories'] = []
|
||||
|
||||
revanced_repositories = [repository for repository in repositories if 'revanced' in repository]
|
||||
|
||||
results: list[dict] = await asyncio.gather(*[self.__get_contributors(repository) for repository in revanced_repositories])
|
||||
|
||||
for key, value in zip(revanced_repositories, results):
|
||||
data = { 'name': key, 'contributors': value }
|
||||
contributors['repositories'].append(data)
|
||||
|
||||
await self.InternalCache.store('contributors', contributors)
|
||||
contributors = {}
|
||||
contributors['repositories'] = []
|
||||
|
||||
revanced_repositories = [repository for repository in repositories if 'revanced' in repository]
|
||||
|
||||
results: list[dict] = await asyncio.gather(*[self.__get_contributors(repository) for repository in revanced_repositories])
|
||||
|
||||
for key, value in zip(revanced_repositories, results):
|
||||
data = { 'name': key, 'contributors': value }
|
||||
contributors['repositories'].append(data)
|
||||
|
||||
return contributors
|
||||
|
||||
async def get_commits(self, org: str, repository: str, path: str) -> dict:
|
||||
@ -180,36 +161,29 @@ class Releases:
|
||||
payload["commits"] = []
|
||||
|
||||
if org == 'revanced' or org == 'vancedapp':
|
||||
key: str = f"{org}/{repository}/{path}"
|
||||
if await self.InternalCache.exists(key):
|
||||
return await self.InternalCache.get(key)
|
||||
else:
|
||||
|
||||
_releases = await self.httpx_client.get(
|
||||
f"https://api.github.com/repos/{org}/{repository}/releases?per_page=2"
|
||||
)
|
||||
|
||||
releases = _releases.json()
|
||||
|
||||
since = releases[1]['created_at']
|
||||
until = releases[0]['created_at']
|
||||
|
||||
_response = await self.httpx_client.get(
|
||||
f"https://api.github.com/repos/{org}/{repository}/commits?path={path}&since={since}&until={until}"
|
||||
)
|
||||
|
||||
response = _response.json()
|
||||
|
||||
for commit in response:
|
||||
data: dict[str, str] = {}
|
||||
data["sha"] = commit["sha"]
|
||||
data["author"] = commit["commit"]["author"]["name"]
|
||||
data["message"] = commit["commit"]["message"]
|
||||
data["html_url"] = commit["html_url"]
|
||||
payload['commits'].append(data)
|
||||
|
||||
await self.InternalCache.store(key, payload)
|
||||
|
||||
return payload
|
||||
_releases = await self.httpx_client.get(
|
||||
f"https://api.github.com/repos/{org}/{repository}/releases?per_page=2"
|
||||
)
|
||||
|
||||
releases = _releases.json()
|
||||
|
||||
since = releases[1]['created_at']
|
||||
until = releases[0]['created_at']
|
||||
|
||||
_response = await self.httpx_client.get(
|
||||
f"https://api.github.com/repos/{org}/{repository}/commits?path={path}&since={since}&until={until}"
|
||||
)
|
||||
|
||||
response = _response.json()
|
||||
|
||||
for commit in response:
|
||||
data: dict[str, str] = {}
|
||||
data["sha"] = commit["sha"]
|
||||
data["author"] = commit["commit"]["author"]["name"]
|
||||
data["message"] = commit["commit"]["message"]
|
||||
data["html_url"] = commit["html_url"]
|
||||
payload['commits'].append(data)
|
||||
|
||||
return payload
|
||||
else:
|
||||
raise Exception("Invalid organization.")
|
9
app/dependencies.py
Normal file
9
app/dependencies.py
Normal file
@ -0,0 +1,9 @@
|
||||
import toml
|
||||
|
||||
def load_config() -> dict:
|
||||
"""Loads the config.toml file.
|
||||
|
||||
Returns:
|
||||
dict: the config.toml file as a dict
|
||||
"""
|
||||
return toml.load("config.toml")
|
156
app/main.py
Executable file
156
app/main.py
Executable file
@ -0,0 +1,156 @@
|
||||
#!/usr/bin/env python3
|
||||
|
||||
import os
|
||||
import toml
|
||||
import binascii
|
||||
|
||||
from fastapi import FastAPI, Request, status
|
||||
from fastapi.responses import JSONResponse, UJSONResponse
|
||||
|
||||
from slowapi.util import get_remote_address
|
||||
from slowapi.middleware import SlowAPIMiddleware
|
||||
from slowapi import Limiter, _rate_limit_exceeded_handler
|
||||
|
||||
from fastapi_cache import FastAPICache
|
||||
from fastapi_cache.decorator import cache
|
||||
from slowapi.errors import RateLimitExceeded
|
||||
from fastapi_cache.backends.redis import RedisBackend
|
||||
|
||||
from fastapi_paseto_auth import AuthPASETO
|
||||
from fastapi_paseto_auth.exceptions import AuthPASETOException
|
||||
|
||||
import app.controllers.Auth as Auth
|
||||
from app.controllers.Clients import Clients
|
||||
from app.utils.RedisConnector import RedisConnector
|
||||
|
||||
import app.models.GeneralErrors as GeneralErrors
|
||||
|
||||
from app.routers import root
|
||||
from app.routers import ping
|
||||
from app.routers import auth
|
||||
from app.routers import tools
|
||||
from app.routers import clients
|
||||
from app.routers import patches
|
||||
from app.routers import changelogs
|
||||
from app.routers import contributors
|
||||
from app.routers import announcement
|
||||
|
||||
"""Get latest ReVanced releases from GitHub API."""
|
||||
|
||||
# Load config
|
||||
|
||||
config: dict = toml.load("config.toml")
|
||||
|
||||
# Create FastAPI instance
|
||||
|
||||
app = FastAPI(title=config['docs']['title'],
|
||||
description=config['docs']['description'],
|
||||
version=config['docs']['version'],
|
||||
license_info={"name": config['license']['name'],
|
||||
"url": config['license']['url']
|
||||
},
|
||||
default_response_class=UJSONResponse
|
||||
)
|
||||
|
||||
# Hook up rate limiter
|
||||
limiter = Limiter(key_func=get_remote_address,
|
||||
default_limits=[
|
||||
config['slowapi']['limit']
|
||||
],
|
||||
headers_enabled=True,
|
||||
storage_uri=f"redis://{os.environ['REDIS_URL']}:{os.environ['REDIS_PORT']}/{config['slowapi']['database']}"
|
||||
)
|
||||
app.state.limiter = limiter
|
||||
app.add_exception_handler(RateLimitExceeded, _rate_limit_exceeded_handler)
|
||||
app.add_middleware(SlowAPIMiddleware)
|
||||
|
||||
# Setup routes
|
||||
|
||||
app.include_router(root.router)
|
||||
app.include_router(tools.router)
|
||||
app.include_router(patches.router)
|
||||
app.include_router(contributors.router)
|
||||
app.include_router(changelogs.router)
|
||||
app.include_router(auth.router)
|
||||
app.include_router(clients.router)
|
||||
app.include_router(announcement.router)
|
||||
app.include_router(ping.router)
|
||||
|
||||
# Setup cache
|
||||
|
||||
@cache()
|
||||
async def get_cache() -> int:
|
||||
"""Get cache TTL from config.
|
||||
|
||||
Returns:
|
||||
int: Cache TTL
|
||||
"""
|
||||
return 1
|
||||
|
||||
# Setup PASETO
|
||||
|
||||
@AuthPASETO.load_config
|
||||
def get_config() -> Auth.PasetoSettings:
|
||||
"""Get PASETO config from Auth module
|
||||
|
||||
Returns:
|
||||
PasetoSettings: PASETO config
|
||||
"""
|
||||
return Auth.PasetoSettings()
|
||||
|
||||
# Setup custom error handlers
|
||||
|
||||
@app.exception_handler(AuthPASETOException)
|
||||
async def authpaseto_exception_handler(request: Request, exc: AuthPASETOException) -> JSONResponse:
|
||||
"""Handle AuthPASETOException
|
||||
|
||||
Args:
|
||||
request (Request): Request
|
||||
exc (AuthPASETOException): Exception
|
||||
|
||||
Returns:
|
||||
JSONResponse: Response
|
||||
"""
|
||||
return JSONResponse(status_code=exc.status_code, content={"detail": exc.message})
|
||||
|
||||
@app.exception_handler(AttributeError)
|
||||
async def validation_exception_handler(request, exc) -> JSONResponse:
|
||||
"""Handle AttributeError
|
||||
|
||||
Args:
|
||||
request (Request): Request
|
||||
exc (AttributeError): Exception
|
||||
|
||||
Returns:
|
||||
JSONResponse: Response
|
||||
"""
|
||||
return JSONResponse(status_code=status.HTTP_422_UNPROCESSABLE_ENTITY, content={
|
||||
"error": "Unprocessable Entity"
|
||||
})
|
||||
|
||||
@app.exception_handler(binascii.Error)
|
||||
async def invalid_token_exception_handler(request, exc) -> JSONResponse:
|
||||
"""Handle binascii.Error
|
||||
|
||||
Args:
|
||||
request (Request): Request
|
||||
exc (binascii.Error): Exception
|
||||
|
||||
Returns:
|
||||
JSONResponse: Response
|
||||
"""
|
||||
return JSONResponse(status_code=status.HTTP_401_UNAUTHORIZED, content={
|
||||
"error": GeneralErrors.Unauthorized().error,
|
||||
"message": GeneralErrors.Unauthorized().message
|
||||
})
|
||||
|
||||
@app.on_event("startup")
|
||||
async def startup() -> None:
|
||||
"""Startup event handler"""
|
||||
|
||||
clients = Clients()
|
||||
await clients.setup_admin()
|
||||
FastAPICache.init(RedisBackend(RedisConnector.connect(config['cache']['database'])),
|
||||
prefix="fastapi-cache")
|
||||
|
||||
return None
|
@ -1,5 +1,5 @@
|
||||
from pydantic import BaseModel
|
||||
import src.models.ResponseFields as ResponseFields
|
||||
import app.models.ResponseFields as ResponseFields
|
||||
|
||||
"""Implements pydantic models and model generator for the API's responses."""
|
||||
|
92
app/routers/announcement.py
Normal file
92
app/routers/announcement.py
Normal file
@ -0,0 +1,92 @@
|
||||
from fastapi_paseto_auth import AuthPASETO
|
||||
from fastapi import APIRouter, Request, Response, Depends, status, HTTPException
|
||||
from app.dependencies import load_config
|
||||
from app.controllers.Announcements import Announcements
|
||||
from app.controllers.Clients import Clients
|
||||
import app.models.AnnouncementModels as AnnouncementModels
|
||||
import app.models.GeneralErrors as GeneralErrors
|
||||
|
||||
router = APIRouter(
|
||||
prefix="/announcement",
|
||||
tags=['Announcement']
|
||||
)
|
||||
|
||||
clients = Clients()
|
||||
announcements = Announcements()
|
||||
config: dict = load_config()
|
||||
|
||||
@router.post('/', response_model=AnnouncementModels.AnnouncementCreatedResponse,
|
||||
status_code=status.HTTP_201_CREATED)
|
||||
async def create_announcement(request: Request, response: Response,
|
||||
announcement: AnnouncementModels.AnnouncementCreateModel,
|
||||
Authorize: AuthPASETO = Depends()) -> dict:
|
||||
"""Create a new announcement.
|
||||
|
||||
Returns:
|
||||
json: announcement information
|
||||
"""
|
||||
Authorize.paseto_required()
|
||||
|
||||
if await clients.auth_checks(Authorize.get_subject(), Authorize.get_jti()):
|
||||
announcement_created: bool = await announcements.store(announcement=announcement,
|
||||
author=Authorize.get_subject())
|
||||
|
||||
if announcement_created:
|
||||
return {"created": announcement_created}
|
||||
else:
|
||||
raise HTTPException(status_code=500, detail={
|
||||
"error": GeneralErrors.InternalServerError().error,
|
||||
"message": GeneralErrors.InternalServerError().message
|
||||
}
|
||||
)
|
||||
else:
|
||||
raise HTTPException(status_code=401, detail={
|
||||
"error": GeneralErrors.Unauthorized().error,
|
||||
"message": GeneralErrors.Unauthorized().message
|
||||
}
|
||||
)
|
||||
|
||||
@router.get('/', response_model=AnnouncementModels.AnnouncementModel)
|
||||
async def get_announcement(request: Request, response: Response) -> dict:
|
||||
"""Get an announcement.
|
||||
|
||||
Returns:
|
||||
json: announcement information
|
||||
"""
|
||||
if await announcements.exists():
|
||||
return await announcements.get()
|
||||
else:
|
||||
raise HTTPException(status_code=404, detail={
|
||||
"error": GeneralErrors.AnnouncementNotFound().error,
|
||||
"message": GeneralErrors.AnnouncementNotFound().message
|
||||
}
|
||||
)
|
||||
|
||||
@router.delete('/',
|
||||
response_model=AnnouncementModels.AnnouncementDeleted,
|
||||
status_code=status.HTTP_200_OK)
|
||||
async def delete_announcement(request: Request, response: Response,
|
||||
Authorize: AuthPASETO = Depends()) -> dict:
|
||||
"""Delete an announcement.
|
||||
|
||||
Returns:
|
||||
json: deletion status
|
||||
"""
|
||||
|
||||
Authorize.paseto_required()
|
||||
|
||||
if await clients.auth_checks(Authorize.get_subject(), Authorize.get_jti()):
|
||||
if await announcements.exists():
|
||||
return {"deleted": await announcements.delete()}
|
||||
else:
|
||||
raise HTTPException(status_code=404, detail={
|
||||
"error": GeneralErrors.AnnouncementNotFound().error,
|
||||
"message": GeneralErrors.AnnouncementNotFound().message
|
||||
}
|
||||
)
|
||||
else:
|
||||
raise HTTPException(status_code=401, detail={
|
||||
"error": GeneralErrors.Unauthorized().error,
|
||||
"message": GeneralErrors.Unauthorized().message
|
||||
}
|
||||
)
|
78
app/routers/auth.py
Normal file
78
app/routers/auth.py
Normal file
@ -0,0 +1,78 @@
|
||||
from fastapi_paseto_auth import AuthPASETO
|
||||
from fastapi import APIRouter, Request, Response, Depends, status, HTTPException
|
||||
from app.dependencies import load_config
|
||||
from app.controllers.Clients import Clients
|
||||
import app.models.ClientModels as ClientModels
|
||||
import app.models.GeneralErrors as GeneralErrors
|
||||
import app.models.ResponseModels as ResponseModels
|
||||
|
||||
router = APIRouter(
|
||||
prefix="/auth",
|
||||
tags=['Authentication']
|
||||
)
|
||||
clients = Clients()
|
||||
config: dict = load_config()
|
||||
|
||||
@router.post('/', response_model=ResponseModels.ClientAuthTokenResponse, status_code=status.HTTP_200_OK)
|
||||
async def auth(request: Request, response: Response, client: ClientModels.ClientAuthModel, Authorize: AuthPASETO = Depends()) -> dict:
|
||||
"""Authenticate a client and get an auth token.
|
||||
|
||||
Returns:
|
||||
access_token: auth token
|
||||
refresh_token: refresh token
|
||||
"""
|
||||
|
||||
admin_claim: dict[str, bool]
|
||||
|
||||
if await clients.exists(client.id):
|
||||
authenticated: bool = await clients.authenticate(client.id, client.secret)
|
||||
|
||||
if not authenticated:
|
||||
raise HTTPException(status_code=401, detail={
|
||||
"error": GeneralErrors.Unauthorized().error,
|
||||
"message": GeneralErrors.Unauthorized().message
|
||||
}
|
||||
)
|
||||
else:
|
||||
if await clients.is_admin(client.id):
|
||||
admin_claim = {"admin": True}
|
||||
else:
|
||||
admin_claim = {"admin": False}
|
||||
|
||||
access_token = Authorize.create_access_token(subject=client.id,
|
||||
user_claims=admin_claim,
|
||||
fresh=True)
|
||||
|
||||
refresh_token = Authorize.create_refresh_token(subject=client.id,
|
||||
user_claims=admin_claim)
|
||||
|
||||
return {"access_token": access_token, "refresh_token": refresh_token}
|
||||
else:
|
||||
raise HTTPException(status_code=401, detail={
|
||||
"error": GeneralErrors.Unauthorized().error,
|
||||
"message": GeneralErrors.Unauthorized().message
|
||||
}
|
||||
)
|
||||
|
||||
@router.post('/refresh', response_model=ResponseModels.ClientTokenRefreshResponse,
|
||||
status_code=status.HTTP_200_OK, tags=['Authentication'])
|
||||
async def refresh(request: Request, response: Response,
|
||||
Authorize: AuthPASETO = Depends()) -> dict:
|
||||
"""Refresh an auth token.
|
||||
|
||||
Returns:
|
||||
access_token: auth token
|
||||
"""
|
||||
|
||||
Authorize.paseto_required(refresh_token=True)
|
||||
|
||||
admin_claim: dict[str, bool] = {"admin": False}
|
||||
|
||||
current_user: str | int | None = Authorize.get_subject()
|
||||
|
||||
if 'admin' in Authorize.get_token_payload():
|
||||
admin_claim = {"admin": Authorize.get_token_payload()['admin']}
|
||||
|
||||
return {"access_token": Authorize.create_access_token(subject=current_user,
|
||||
user_claims=admin_claim,
|
||||
fresh=False)}
|
25
app/routers/changelogs.py
Normal file
25
app/routers/changelogs.py
Normal file
@ -0,0 +1,25 @@
|
||||
from fastapi import APIRouter, Request, Response
|
||||
from fastapi_cache.decorator import cache
|
||||
from app.dependencies import load_config
|
||||
from app.controllers.Releases import Releases
|
||||
import app.models.ResponseModels as ResponseModels
|
||||
|
||||
router = APIRouter()
|
||||
|
||||
releases = Releases()
|
||||
|
||||
config: dict = load_config()
|
||||
|
||||
@router.get('/changelogs/{org}/{repo}', response_model=ResponseModels.ChangelogsResponseModel, tags=['ReVanced Tools'])
|
||||
@cache(config['cache']['expire'])
|
||||
async def changelogs(request: Request, response: Response, org: str, repo: str, path: str) -> dict:
|
||||
"""Get the latest changes from a repository.
|
||||
|
||||
Returns:
|
||||
json: list of commits
|
||||
"""
|
||||
return await releases.get_commits(
|
||||
org=org,
|
||||
repository=repo,
|
||||
path=path
|
||||
)
|
172
app/routers/clients.py
Normal file
172
app/routers/clients.py
Normal file
@ -0,0 +1,172 @@
|
||||
from fastapi_paseto_auth import AuthPASETO
|
||||
from fastapi import APIRouter, Request, Response, Depends, status, HTTPException
|
||||
from app.dependencies import load_config
|
||||
from app.controllers.Clients import Clients
|
||||
import app.models.ClientModels as ClientModels
|
||||
import app.models.ResponseModels as ResponseModels
|
||||
import app.models.GeneralErrors as GeneralErrors
|
||||
from app.utils.Generators import Generators
|
||||
|
||||
router = APIRouter(
|
||||
prefix="/client",
|
||||
tags=['Clients']
|
||||
)
|
||||
generators = Generators()
|
||||
clients = Clients()
|
||||
config: dict = load_config()
|
||||
|
||||
@router.post('/', response_model=ClientModels.ClientModel, status_code=status.HTTP_201_CREATED)
|
||||
async def create_client(request: Request, response: Response, admin: bool | None = False, Authorize: AuthPASETO = Depends()) -> ClientModels.ClientModel:
|
||||
"""Create a new API client.
|
||||
|
||||
Returns:
|
||||
json: client information
|
||||
"""
|
||||
|
||||
Authorize.paseto_required()
|
||||
|
||||
admin_claim: dict[str, bool] = {"admin": False}
|
||||
|
||||
current_user: str | int | None = Authorize.get_subject()
|
||||
|
||||
if 'admin' in Authorize.get_token_payload():
|
||||
admin_claim = {"admin": Authorize.get_token_payload()['admin']}
|
||||
|
||||
if ( await clients.auth_checks(Authorize.get_subject(), Authorize.get_jti()) and
|
||||
admin_claim['admin'] == True):
|
||||
|
||||
client: ClientModels.ClientModel = await clients.generate(admin=admin)
|
||||
await clients.store(client)
|
||||
|
||||
return client
|
||||
else:
|
||||
raise HTTPException(status_code=401, detail={
|
||||
"error": GeneralErrors.Unauthorized().error,
|
||||
"message": GeneralErrors.Unauthorized().message
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
@router.delete('/{client_id}', response_model=ResponseModels.ClientDeletedResponse, status_code=status.HTTP_200_OK)
|
||||
async def delete_client(request: Request, response: Response, client_id: str, Authorize: AuthPASETO = Depends()) -> dict:
|
||||
"""Delete an API client.
|
||||
|
||||
Returns:
|
||||
json: deletion status
|
||||
"""
|
||||
|
||||
Authorize.paseto_required()
|
||||
|
||||
admin_claim: dict[str, bool] = {"admin": False}
|
||||
|
||||
current_user: str | int | None = Authorize.get_subject()
|
||||
|
||||
if 'admin' in Authorize.get_token_payload():
|
||||
admin_claim = {"admin": Authorize.get_token_payload()['admin']}
|
||||
|
||||
if ( await clients.auth_checks(Authorize.get_subject(), Authorize.get_jti()) and
|
||||
( admin_claim['admin'] == True or
|
||||
current_user == client_id ) ):
|
||||
|
||||
if await clients.exists(client_id):
|
||||
return {"id": client_id, "deleted": await clients.delete(client_id)}
|
||||
else:
|
||||
raise HTTPException(status_code=404, detail={
|
||||
"error": GeneralErrors.ClientNotFound().error,
|
||||
"message": GeneralErrors.ClientNotFound().message
|
||||
}
|
||||
)
|
||||
else:
|
||||
raise HTTPException(status_code=401, detail={
|
||||
"error": GeneralErrors.Unauthorized().error,
|
||||
"message": GeneralErrors.Unauthorized().message
|
||||
}
|
||||
)
|
||||
|
||||
@router.patch('/{client_id}/secret', response_model=ResponseModels.ClientSecretUpdatedResponse, status_code=status.HTTP_200_OK)
|
||||
async def update_client(request: Request, response: Response, client_id: str, Authorize: AuthPASETO = Depends()) -> dict:
|
||||
"""Update an API client's secret.
|
||||
|
||||
Returns:
|
||||
json: client ID and secret
|
||||
"""
|
||||
|
||||
Authorize.paseto_required()
|
||||
|
||||
admin_claim: dict[str, bool] = {"admin": False}
|
||||
|
||||
current_user: str | int | None = Authorize.get_subject()
|
||||
|
||||
if 'admin' in Authorize.get_token_payload():
|
||||
admin_claim = {"admin": Authorize.get_token_payload()['admin']}
|
||||
|
||||
if ( await clients.auth_checks(Authorize.get_subject(), Authorize.get_jti()) and
|
||||
( admin_claim['admin'] == True or
|
||||
current_user == client_id ) ):
|
||||
|
||||
if await clients.exists(client_id):
|
||||
new_secret: str = await generators.generate_secret()
|
||||
|
||||
if await clients.update_secret(client_id, new_secret):
|
||||
return {"id": client_id, "secret": new_secret}
|
||||
else:
|
||||
raise HTTPException(status_code=500, detail={
|
||||
"error": GeneralErrors.InternalServerError().error,
|
||||
"message": GeneralErrors.InternalServerError().message
|
||||
}
|
||||
)
|
||||
else:
|
||||
raise HTTPException(status_code=404, detail={
|
||||
"error": GeneralErrors.ClientNotFound().error,
|
||||
"message": GeneralErrors.ClientNotFound().message
|
||||
}
|
||||
)
|
||||
else:
|
||||
raise HTTPException(status_code=401, detail={
|
||||
"error": GeneralErrors.Unauthorized().error,
|
||||
"message": GeneralErrors.Unauthorized().message
|
||||
}
|
||||
)
|
||||
|
||||
@router.patch('/client/{client_id}/status', response_model=ResponseModels.ClientStatusResponse, status_code=status.HTTP_200_OK)
|
||||
async def client_status(request: Request, response: Response, client_id: str, active: bool, Authorize: AuthPASETO = Depends()) -> dict:
|
||||
"""Activate or deactivate a client
|
||||
|
||||
Returns:
|
||||
json: json response containing client ID and activation status
|
||||
"""
|
||||
|
||||
Authorize.paseto_required()
|
||||
|
||||
admin_claim: dict[str, bool] = {"admin": False}
|
||||
|
||||
current_user: str | int | None = Authorize.get_subject()
|
||||
|
||||
if 'admin' in Authorize.get_token_payload():
|
||||
admin_claim = {"admin": Authorize.get_token_payload()['admin']}
|
||||
|
||||
if ( await clients.auth_checks(Authorize.get_subject(), Authorize.get_jti()) and
|
||||
( admin_claim['admin'] == True or
|
||||
current_user == client_id ) ):
|
||||
|
||||
if await clients.exists(client_id):
|
||||
if await clients.status(client_id, active):
|
||||
return {"id": client_id, "active": active}
|
||||
else:
|
||||
raise HTTPException(status_code=500, detail={
|
||||
"error": GeneralErrors.InternalServerError().error,
|
||||
"message": GeneralErrors.InternalServerError().message
|
||||
}
|
||||
)
|
||||
else:
|
||||
raise HTTPException(status_code=404, detail={
|
||||
"error": GeneralErrors.ClientNotFound().error,
|
||||
"message": GeneralErrors.ClientNotFound().message
|
||||
}
|
||||
)
|
||||
else:
|
||||
raise HTTPException(status_code=401, detail={
|
||||
"error": GeneralErrors.Unauthorized().error,
|
||||
"message": GeneralErrors.Unauthorized().message
|
||||
}
|
||||
)
|
21
app/routers/contributors.py
Normal file
21
app/routers/contributors.py
Normal file
@ -0,0 +1,21 @@
|
||||
from fastapi import APIRouter, Request, Response
|
||||
from fastapi_cache.decorator import cache
|
||||
from app.dependencies import load_config
|
||||
from app.controllers.Releases import Releases
|
||||
import app.models.ResponseModels as ResponseModels
|
||||
|
||||
router = APIRouter()
|
||||
|
||||
releases = Releases()
|
||||
|
||||
config: dict = load_config()
|
||||
|
||||
@router.get('/contributors', response_model=ResponseModels.ContributorsResponseModel, tags=['ReVanced Tools'])
|
||||
@cache(config['cache']['expire'])
|
||||
async def contributors(request: Request, response: Response) -> dict:
|
||||
"""Get contributors.
|
||||
|
||||
Returns:
|
||||
json: list of contributors
|
||||
"""
|
||||
return await releases.get_contributors(config['app']['repositories'])
|
22
app/routers/patches.py
Normal file
22
app/routers/patches.py
Normal file
@ -0,0 +1,22 @@
|
||||
from fastapi import APIRouter, Request, Response
|
||||
from fastapi_cache.decorator import cache
|
||||
from app.dependencies import load_config
|
||||
from app.controllers.Releases import Releases
|
||||
import app.models.ResponseModels as ResponseModels
|
||||
|
||||
router = APIRouter()
|
||||
|
||||
releases = Releases()
|
||||
|
||||
config: dict = load_config()
|
||||
|
||||
@router.get('/patches', response_model=ResponseModels.PatchesResponseModel, tags=['ReVanced Tools'])
|
||||
@cache(config['cache']['expire'])
|
||||
async def patches(request: Request, response: Response) -> dict:
|
||||
"""Get latest patches.
|
||||
|
||||
Returns:
|
||||
json: list of latest patches
|
||||
"""
|
||||
|
||||
return await releases.get_patches_json()
|
12
app/routers/ping.py
Normal file
12
app/routers/ping.py
Normal file
@ -0,0 +1,12 @@
|
||||
from fastapi import APIRouter, Request, Response
|
||||
|
||||
router = APIRouter()
|
||||
|
||||
@router.head('/ping', status_code=204, tags=['Ping'])
|
||||
async def ping(request: Request, response: Response) -> None:
|
||||
"""Check if the API is running.
|
||||
|
||||
Returns:
|
||||
None
|
||||
"""
|
||||
return None
|
14
app/routers/root.py
Normal file
14
app/routers/root.py
Normal file
@ -0,0 +1,14 @@
|
||||
from fastapi import APIRouter, Request, Response, status
|
||||
from fastapi.responses import RedirectResponse
|
||||
|
||||
router = APIRouter()
|
||||
|
||||
@router.get("/", response_class=RedirectResponse,
|
||||
status_code=status.HTTP_301_MOVED_PERMANENTLY, tags=['Root'])
|
||||
async def root(request: Request, response: Response) -> RedirectResponse:
|
||||
"""Brings up API documentation
|
||||
|
||||
Returns:
|
||||
None: Redirects to /docs
|
||||
"""
|
||||
return RedirectResponse(url="/docs")
|
21
app/routers/tools.py
Normal file
21
app/routers/tools.py
Normal file
@ -0,0 +1,21 @@
|
||||
from fastapi import APIRouter, Request, Response
|
||||
from fastapi_cache.decorator import cache
|
||||
from app.dependencies import load_config
|
||||
from app.controllers.Releases import Releases
|
||||
import app.models.ResponseModels as ResponseModels
|
||||
|
||||
router = APIRouter()
|
||||
|
||||
releases = Releases()
|
||||
|
||||
config: dict = load_config()
|
||||
|
||||
@router.get('/tools', response_model=ResponseModels.ToolsResponseModel, tags=['ReVanced Tools'])
|
||||
@cache(config['cache']['expire'])
|
||||
async def tools(request: Request, response: Response) -> dict:
|
||||
"""Get patching tools' latest version.
|
||||
|
||||
Returns:
|
||||
json: information about the patching tools' latest version
|
||||
"""
|
||||
return await releases.get_latest_releases(config['app']['repositories'])
|
@ -1,6 +1,6 @@
|
||||
import os
|
||||
import httpx_cache
|
||||
import src.utils.Logger as Logger
|
||||
import app.utils.Logger as Logger
|
||||
|
||||
class HTTPXClient:
|
||||
|
@ -1,52 +1,9 @@
|
||||
import sys
|
||||
import logging
|
||||
from loguru import logger
|
||||
from typing import Optional
|
||||
from types import FrameType
|
||||
from redis import RedisError
|
||||
from argon2.exceptions import VerifyMismatchError
|
||||
|
||||
class InterceptHandler(logging.Handler):
|
||||
"""Setups a loging handler for uvicorn and FastAPI.
|
||||
|
||||
Args:
|
||||
logging (logging.Handler)
|
||||
"""
|
||||
|
||||
def emit(self, record: logging.LogRecord) -> None:
|
||||
"""Emit a log record.
|
||||
|
||||
Args:
|
||||
record (LogRecord): Logging record
|
||||
"""
|
||||
|
||||
level: str | int
|
||||
frame: Optional[FrameType]
|
||||
depth: int
|
||||
|
||||
# Get corresponding Loguru level if it exists
|
||||
# If not, use default level
|
||||
|
||||
try:
|
||||
level = logger.level(record.levelname).name
|
||||
except ValueError:
|
||||
level = record.levelno
|
||||
|
||||
# Find caller from where originated the logged message
|
||||
# Set depth to 2 to avoid logging of loguru internal calls
|
||||
frame = logging.currentframe()
|
||||
depth = 2
|
||||
|
||||
# Find caller from where originated the logged message
|
||||
# The logging module uses a stack frame to keep track of where logging messages originate
|
||||
# This stack frame is used to find the correct place in the code where the logging message was generated
|
||||
# The mypy error is ignored because the logging module is not properly typed
|
||||
while frame.f_code.co_filename == logging.__file__:
|
||||
frame = frame.f_back
|
||||
depth += 1
|
||||
|
||||
logger.opt(depth=depth, exception=record.exc_info).log(level, record.getMessage())
|
||||
|
||||
class HTTPXLogger():
|
||||
"""Logger adapter for HTTPX."""
|
||||
|
0
app/utils/__init__.py
Normal file
0
app/utils/__init__.py
Normal file
13
config.toml
13
config.toml
@ -24,28 +24,23 @@ The team also have a [Discord Server](https://revanced.app/discord) if you need
|
||||
Godspeed 💀
|
||||
|
||||
"""
|
||||
version = "0.8 RC"
|
||||
version = "0.8.5 RC"
|
||||
|
||||
[license]
|
||||
|
||||
name = "AGPL-3.0"
|
||||
url = "https://www.gnu.org/licenses/agpl-3.0.en.html"
|
||||
|
||||
[slowapi]
|
||||
|
||||
limit = "60/minute"
|
||||
|
||||
[logging]
|
||||
|
||||
level = "INFO"
|
||||
json_logs = false
|
||||
|
||||
[cache]
|
||||
expire = 120
|
||||
expire = 300
|
||||
database = 0
|
||||
|
||||
[internal-cache]
|
||||
expire = 300
|
||||
[slowapi]
|
||||
limit = "60/minute"
|
||||
database = 1
|
||||
|
||||
[clients]
|
||||
|
501
main.py
501
main.py
@ -1,501 +0,0 @@
|
||||
#!/usr/bin/env python3
|
||||
|
||||
import binascii
|
||||
import os
|
||||
from typing import Coroutine
|
||||
import toml
|
||||
import sentry_sdk
|
||||
import asyncio
|
||||
import uvloop
|
||||
|
||||
from fastapi import FastAPI, Request, Response, status, HTTPException, Depends
|
||||
from fastapi.responses import RedirectResponse, JSONResponse, UJSONResponse
|
||||
|
||||
from slowapi.util import get_remote_address
|
||||
from slowapi import Limiter, _rate_limit_exceeded_handler
|
||||
|
||||
from fastapi_cache import FastAPICache
|
||||
from fastapi_cache.decorator import cache
|
||||
from slowapi.errors import RateLimitExceeded
|
||||
from fastapi_cache.backends.redis import RedisBackend
|
||||
from fastapi.exceptions import RequestValidationError
|
||||
|
||||
from fastapi_paseto_auth import AuthPASETO
|
||||
from fastapi_paseto_auth.exceptions import AuthPASETOException
|
||||
|
||||
from sentry_sdk.integrations.redis import RedisIntegration
|
||||
from sentry_sdk.integrations.httpx import HttpxIntegration
|
||||
from sentry_sdk.integrations.gnu_backtrace import GnuBacktraceIntegration
|
||||
|
||||
import src.controllers.Auth as Auth
|
||||
from src.controllers.Releases import Releases
|
||||
from src.controllers.Clients import Clients
|
||||
from src.controllers.Announcements import Announcements
|
||||
|
||||
from src.utils.Generators import Generators
|
||||
from src.utils.RedisConnector import RedisConnector
|
||||
|
||||
import src.models.ClientModels as ClientModels
|
||||
import src.models.GeneralErrors as GeneralErrors
|
||||
import src.models.ResponseModels as ResponseModels
|
||||
import src.models.AnnouncementModels as AnnouncementModels
|
||||
|
||||
import src.utils.Logger as Logger
|
||||
|
||||
# Enable sentry logging
|
||||
|
||||
sentry_sdk.init(os.environ['SENTRY_DSN'], traces_sample_rate=1.0, integrations=[
|
||||
RedisIntegration(),
|
||||
HttpxIntegration(),
|
||||
GnuBacktraceIntegration(),
|
||||
],)
|
||||
|
||||
"""Get latest ReVanced releases from GitHub API."""
|
||||
|
||||
# Load config
|
||||
|
||||
config: dict = toml.load("config.toml")
|
||||
|
||||
# Class instances
|
||||
|
||||
generators = Generators()
|
||||
|
||||
releases = Releases()
|
||||
|
||||
clients = Clients()
|
||||
|
||||
announcements = Announcements()
|
||||
|
||||
# Setup admin client
|
||||
uvloop.install()
|
||||
|
||||
loop: asyncio.AbstractEventLoop = asyncio.get_event_loop()
|
||||
coroutine: Coroutine = clients.setup_admin()
|
||||
loop.run_until_complete(coroutine)
|
||||
|
||||
# Create FastAPI instance
|
||||
|
||||
app = FastAPI(title=config['docs']['title'],
|
||||
description=config['docs']['description'],
|
||||
version=config['docs']['version'],
|
||||
license_info={"name": config['license']['name'],
|
||||
"url": config['license']['url']
|
||||
},
|
||||
default_response_class=UJSONResponse
|
||||
)
|
||||
|
||||
# Slowapi limiter
|
||||
|
||||
limiter = Limiter(key_func=get_remote_address, headers_enabled=True)
|
||||
app.state.limiter = limiter
|
||||
app.add_exception_handler(RateLimitExceeded, _rate_limit_exceeded_handler)
|
||||
|
||||
# Setup cache
|
||||
|
||||
@cache()
|
||||
async def get_cache() -> int:
|
||||
return 1
|
||||
|
||||
# Setup PASETO
|
||||
|
||||
@AuthPASETO.load_config
|
||||
def get_config():
|
||||
return Auth.PasetoSettings()
|
||||
|
||||
# Setup custom error handlers
|
||||
|
||||
@app.exception_handler(AuthPASETOException)
|
||||
async def authpaseto_exception_handler(request: Request, exc: AuthPASETOException):
|
||||
return JSONResponse(status_code=exc.status_code, content={"detail": exc.message})
|
||||
|
||||
@app.exception_handler(AttributeError)
|
||||
async def validation_exception_handler(request, exc):
|
||||
return JSONResponse(status_code=status.HTTP_422_UNPROCESSABLE_ENTITY, content={
|
||||
"error": "Unprocessable Entity"
|
||||
})
|
||||
|
||||
@app.exception_handler(binascii.Error)
|
||||
async def invalid_token_exception_handler(request, exc):
|
||||
return JSONResponse(status_code=status.HTTP_401_UNAUTHORIZED, content={
|
||||
"error": GeneralErrors.Unauthorized().error,
|
||||
"message": GeneralErrors.Unauthorized().message
|
||||
})
|
||||
|
||||
# Routes
|
||||
|
||||
@app.get("/", response_class=RedirectResponse,
|
||||
status_code=status.HTTP_301_MOVED_PERMANENTLY, tags=['Root'])
|
||||
@limiter.limit(config['slowapi']['limit'])
|
||||
async def root(request: Request, response: Response) -> RedirectResponse:
|
||||
"""Brings up API documentation
|
||||
|
||||
Returns:
|
||||
None: Redirects to /docs
|
||||
"""
|
||||
return RedirectResponse(url="/docs")
|
||||
|
||||
@app.get('/tools', response_model=ResponseModels.ToolsResponseModel, tags=['ReVanced Tools'])
|
||||
@limiter.limit(config['slowapi']['limit'])
|
||||
@cache(config['cache']['expire'])
|
||||
async def tools(request: Request, response: Response) -> dict:
|
||||
"""Get patching tools' latest version.
|
||||
|
||||
Returns:
|
||||
json: information about the patching tools' latest version
|
||||
"""
|
||||
return await releases.get_latest_releases(config['app']['repositories'])
|
||||
|
||||
@app.get('/patches', response_model=ResponseModels.PatchesResponseModel, tags=['ReVanced Tools'])
|
||||
@limiter.limit(config['slowapi']['limit'])
|
||||
@cache(config['cache']['expire'])
|
||||
async def patches(request: Request, response: Response) -> dict:
|
||||
"""Get latest patches.
|
||||
|
||||
Returns:
|
||||
json: list of latest patches
|
||||
"""
|
||||
|
||||
return await releases.get_patches_json()
|
||||
|
||||
@app.get('/contributors', response_model=ResponseModels.ContributorsResponseModel, tags=['ReVanced Tools'])
|
||||
@limiter.limit(config['slowapi']['limit'])
|
||||
@cache(config['cache']['expire'])
|
||||
async def contributors(request: Request, response: Response) -> dict:
|
||||
"""Get contributors.
|
||||
|
||||
Returns:
|
||||
json: list of contributors
|
||||
"""
|
||||
return await releases.get_contributors(config['app']['repositories'])
|
||||
|
||||
@app.get('/changelogs/{org}/{repo}', response_model=ResponseModels.ChangelogsResponseModel, tags=['ReVanced Tools'])
|
||||
@limiter.limit(config['slowapi']['limit'])
|
||||
@cache(config['cache']['expire'])
|
||||
async def changelogs(request: Request, response: Response, org: str, repo: str, path: str) -> dict:
|
||||
"""Get the latest changes from a repository.
|
||||
|
||||
Returns:
|
||||
json: list of commits
|
||||
"""
|
||||
return await releases.get_commits(
|
||||
org=org,
|
||||
repository=repo,
|
||||
path=path
|
||||
)
|
||||
|
||||
@app.post('/client', response_model=ClientModels.ClientModel, status_code=status.HTTP_201_CREATED, tags=['Clients'])
|
||||
@limiter.limit(config['slowapi']['limit'])
|
||||
async def create_client(request: Request, response: Response, admin: bool | None = False, Authorize: AuthPASETO = Depends()) -> ClientModels.ClientModel:
|
||||
"""Create a new API client.
|
||||
|
||||
Returns:
|
||||
json: client information
|
||||
"""
|
||||
|
||||
Authorize.paseto_required()
|
||||
|
||||
admin_claim: dict[str, bool] = {"admin": False}
|
||||
|
||||
current_user: str | int | None = Authorize.get_subject()
|
||||
|
||||
if 'admin' in Authorize.get_token_payload():
|
||||
admin_claim = {"admin": Authorize.get_token_payload()['admin']}
|
||||
|
||||
if ( await clients.auth_checks(Authorize.get_subject(), Authorize.get_jti()) and
|
||||
admin_claim['admin'] == True):
|
||||
|
||||
client: ClientModels.ClientModel = await clients.generate(admin=admin)
|
||||
await clients.store(client)
|
||||
|
||||
return client
|
||||
else:
|
||||
raise HTTPException(status_code=401, detail={
|
||||
"error": GeneralErrors.Unauthorized().error,
|
||||
"message": GeneralErrors.Unauthorized().message
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
@app.delete('/client/{client_id}', response_model=ResponseModels.ClientDeletedResponse, status_code=status.HTTP_200_OK, tags=['Clients'])
|
||||
@limiter.limit(config['slowapi']['limit'])
|
||||
async def delete_client(request: Request, response: Response, client_id: str, Authorize: AuthPASETO = Depends()) -> dict:
|
||||
"""Delete an API client.
|
||||
|
||||
Returns:
|
||||
json: deletion status
|
||||
"""
|
||||
|
||||
Authorize.paseto_required()
|
||||
|
||||
admin_claim: dict[str, bool] = {"admin": False}
|
||||
|
||||
current_user: str | int | None = Authorize.get_subject()
|
||||
|
||||
if 'admin' in Authorize.get_token_payload():
|
||||
admin_claim = {"admin": Authorize.get_token_payload()['admin']}
|
||||
|
||||
if ( await clients.auth_checks(Authorize.get_subject(), Authorize.get_jti()) and
|
||||
( admin_claim['admin'] == True or
|
||||
current_user == client_id ) ):
|
||||
|
||||
if await clients.exists(client_id):
|
||||
return {"id": client_id, "deleted": await clients.delete(client_id)}
|
||||
else:
|
||||
raise HTTPException(status_code=404, detail={
|
||||
"error": GeneralErrors.ClientNotFound().error,
|
||||
"message": GeneralErrors.ClientNotFound().message
|
||||
}
|
||||
)
|
||||
else:
|
||||
raise HTTPException(status_code=401, detail={
|
||||
"error": GeneralErrors.Unauthorized().error,
|
||||
"message": GeneralErrors.Unauthorized().message
|
||||
}
|
||||
)
|
||||
|
||||
@app.patch('/client/{client_id}/secret', response_model=ResponseModels.ClientSecretUpdatedResponse, status_code=status.HTTP_200_OK, tags=['Clients'])
|
||||
@limiter.limit(config['slowapi']['limit'])
|
||||
async def update_client(request: Request, response: Response, client_id: str, Authorize: AuthPASETO = Depends()) -> dict:
|
||||
"""Update an API client's secret.
|
||||
|
||||
Returns:
|
||||
json: client ID and secret
|
||||
"""
|
||||
|
||||
Authorize.paseto_required()
|
||||
|
||||
admin_claim: dict[str, bool] = {"admin": False}
|
||||
|
||||
current_user: str | int | None = Authorize.get_subject()
|
||||
|
||||
if 'admin' in Authorize.get_token_payload():
|
||||
admin_claim = {"admin": Authorize.get_token_payload()['admin']}
|
||||
|
||||
if ( await clients.auth_checks(Authorize.get_subject(), Authorize.get_jti()) and
|
||||
( admin_claim['admin'] == True or
|
||||
current_user == client_id ) ):
|
||||
|
||||
if await clients.exists(client_id):
|
||||
new_secret: str = await generators.generate_secret()
|
||||
|
||||
if await clients.update_secret(client_id, new_secret):
|
||||
return {"id": client_id, "secret": new_secret}
|
||||
else:
|
||||
raise HTTPException(status_code=500, detail={
|
||||
"error": GeneralErrors.InternalServerError().error,
|
||||
"message": GeneralErrors.InternalServerError().message
|
||||
}
|
||||
)
|
||||
else:
|
||||
raise HTTPException(status_code=404, detail={
|
||||
"error": GeneralErrors.ClientNotFound().error,
|
||||
"message": GeneralErrors.ClientNotFound().message
|
||||
}
|
||||
)
|
||||
else:
|
||||
raise HTTPException(status_code=401, detail={
|
||||
"error": GeneralErrors.Unauthorized().error,
|
||||
"message": GeneralErrors.Unauthorized().message
|
||||
}
|
||||
)
|
||||
|
||||
@app.patch('/client/{client_id}/status', response_model=ResponseModels.ClientStatusResponse, status_code=status.HTTP_200_OK, tags=['Clients'])
|
||||
async def client_status(request: Request, response: Response, client_id: str, active: bool, Authorize: AuthPASETO = Depends()) -> dict:
|
||||
"""Activate or deactivate a client
|
||||
|
||||
Returns:
|
||||
json: json response containing client ID and activation status
|
||||
"""
|
||||
|
||||
Authorize.paseto_required()
|
||||
|
||||
admin_claim: dict[str, bool] = {"admin": False}
|
||||
|
||||
current_user: str | int | None = Authorize.get_subject()
|
||||
|
||||
if 'admin' in Authorize.get_token_payload():
|
||||
admin_claim = {"admin": Authorize.get_token_payload()['admin']}
|
||||
|
||||
if ( await clients.auth_checks(Authorize.get_subject(), Authorize.get_jti()) and
|
||||
( admin_claim['admin'] == True or
|
||||
current_user == client_id ) ):
|
||||
|
||||
if await clients.exists(client_id):
|
||||
if await clients.status(client_id, active):
|
||||
return {"id": client_id, "active": active}
|
||||
else:
|
||||
raise HTTPException(status_code=500, detail={
|
||||
"error": GeneralErrors.InternalServerError().error,
|
||||
"message": GeneralErrors.InternalServerError().message
|
||||
}
|
||||
)
|
||||
else:
|
||||
raise HTTPException(status_code=404, detail={
|
||||
"error": GeneralErrors.ClientNotFound().error,
|
||||
"message": GeneralErrors.ClientNotFound().message
|
||||
}
|
||||
)
|
||||
else:
|
||||
raise HTTPException(status_code=401, detail={
|
||||
"error": GeneralErrors.Unauthorized().error,
|
||||
"message": GeneralErrors.Unauthorized().message
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
@app.post('/announcement', response_model=AnnouncementModels.AnnouncementCreatedResponse,
|
||||
status_code=status.HTTP_201_CREATED, tags=['Announcements'])
|
||||
@limiter.limit(config['slowapi']['limit'])
|
||||
async def create_announcement(request: Request, response: Response,
|
||||
announcement: AnnouncementModels.AnnouncementCreateModel,
|
||||
Authorize: AuthPASETO = Depends()) -> dict:
|
||||
"""Create a new announcement.
|
||||
|
||||
Returns:
|
||||
json: announcement information
|
||||
"""
|
||||
Authorize.paseto_required()
|
||||
|
||||
if await clients.auth_checks(Authorize.get_subject(), Authorize.get_jti()):
|
||||
announcement_created: bool = await announcements.store(announcement=announcement,
|
||||
author=Authorize.get_subject())
|
||||
|
||||
if announcement_created:
|
||||
return {"created": announcement_created}
|
||||
else:
|
||||
raise HTTPException(status_code=500, detail={
|
||||
"error": GeneralErrors.InternalServerError().error,
|
||||
"message": GeneralErrors.InternalServerError().message
|
||||
}
|
||||
)
|
||||
else:
|
||||
raise HTTPException(status_code=401, detail={
|
||||
"error": GeneralErrors.Unauthorized().error,
|
||||
"message": GeneralErrors.Unauthorized().message
|
||||
}
|
||||
)
|
||||
|
||||
@app.get('/announcement', response_model=AnnouncementModels.AnnouncementModel, tags=['Announcements'])
|
||||
@limiter.limit(config['slowapi']['limit'])
|
||||
async def get_announcement(request: Request, response: Response) -> dict:
|
||||
"""Get an announcement.
|
||||
|
||||
Returns:
|
||||
json: announcement information
|
||||
"""
|
||||
if await announcements.exists():
|
||||
return await announcements.get()
|
||||
else:
|
||||
raise HTTPException(status_code=404, detail={
|
||||
"error": GeneralErrors.AnnouncementNotFound().error,
|
||||
"message": GeneralErrors.AnnouncementNotFound().message
|
||||
}
|
||||
)
|
||||
|
||||
@app.delete('/announcement',
|
||||
response_model=AnnouncementModels.AnnouncementDeleted,
|
||||
status_code=status.HTTP_200_OK, tags=['Announcements'])
|
||||
@limiter.limit(config['slowapi']['limit'])
|
||||
async def delete_announcement(request: Request, response: Response,
|
||||
Authorize: AuthPASETO = Depends()) -> dict:
|
||||
"""Delete an announcement.
|
||||
|
||||
Returns:
|
||||
json: deletion status
|
||||
"""
|
||||
|
||||
Authorize.paseto_required()
|
||||
|
||||
if await clients.auth_checks(Authorize.get_subject(), Authorize.get_jti()):
|
||||
if await announcements.exists():
|
||||
return {"deleted": await announcements.delete()}
|
||||
else:
|
||||
raise HTTPException(status_code=404, detail={
|
||||
"error": GeneralErrors.AnnouncementNotFound().error,
|
||||
"message": GeneralErrors.AnnouncementNotFound().message
|
||||
}
|
||||
)
|
||||
else:
|
||||
raise HTTPException(status_code=401, detail={
|
||||
"error": GeneralErrors.Unauthorized().error,
|
||||
"message": GeneralErrors.Unauthorized().message
|
||||
}
|
||||
)
|
||||
|
||||
@app.post('/auth', response_model=ResponseModels.ClientAuthTokenResponse, status_code=status.HTTP_200_OK, tags=['Authentication'])
|
||||
@limiter.limit(config['slowapi']['limit'])
|
||||
async def auth(request: Request, response: Response, client: ClientModels.ClientAuthModel, Authorize: AuthPASETO = Depends()) -> dict:
|
||||
"""Authenticate a client and get an auth token.
|
||||
|
||||
Returns:
|
||||
access_token: auth token
|
||||
refresh_token: refresh token
|
||||
"""
|
||||
|
||||
admin_claim: dict[str, bool]
|
||||
|
||||
if await clients.exists(client.id):
|
||||
authenticated: bool = await clients.authenticate(client.id, client.secret)
|
||||
|
||||
if not authenticated:
|
||||
raise HTTPException(status_code=401, detail={
|
||||
"error": GeneralErrors.Unauthorized().error,
|
||||
"message": GeneralErrors.Unauthorized().message
|
||||
}
|
||||
)
|
||||
else:
|
||||
if await clients.is_admin(client.id):
|
||||
admin_claim = {"admin": True}
|
||||
else:
|
||||
admin_claim = {"admin": False}
|
||||
|
||||
access_token = Authorize.create_access_token(subject=client.id,
|
||||
user_claims=admin_claim,
|
||||
fresh=True)
|
||||
|
||||
refresh_token = Authorize.create_refresh_token(subject=client.id,
|
||||
user_claims=admin_claim)
|
||||
|
||||
return {"access_token": access_token, "refresh_token": refresh_token}
|
||||
else:
|
||||
raise HTTPException(status_code=401, detail={
|
||||
"error": GeneralErrors.Unauthorized().error,
|
||||
"message": GeneralErrors.Unauthorized().message
|
||||
}
|
||||
)
|
||||
|
||||
@app.post('/auth/refresh', response_model=ResponseModels.ClientTokenRefreshResponse,
|
||||
status_code=status.HTTP_200_OK, tags=['Authentication'])
|
||||
@limiter.limit(config['slowapi']['limit'])
|
||||
async def refresh(request: Request, response: Response,
|
||||
Authorize: AuthPASETO = Depends()) -> dict:
|
||||
"""Refresh an auth token.
|
||||
|
||||
Returns:
|
||||
access_token: auth token
|
||||
"""
|
||||
|
||||
Authorize.paseto_required(refresh_token=True)
|
||||
|
||||
admin_claim: dict[str, bool] = {"admin": False}
|
||||
|
||||
current_user: str | int | None = Authorize.get_subject()
|
||||
|
||||
if 'admin' in Authorize.get_token_payload():
|
||||
admin_claim = {"admin": Authorize.get_token_payload()['admin']}
|
||||
|
||||
return {"access_token": Authorize.create_access_token(subject=current_user,
|
||||
user_claims=admin_claim,
|
||||
fresh=False)}
|
||||
|
||||
@app.on_event("startup")
|
||||
async def startup() -> None:
|
||||
FastAPICache.init(RedisBackend(RedisConnector.connect(config['cache']['database'])),
|
||||
prefix="fastapi-cache")
|
||||
|
||||
return None
|
||||
|
||||
# setup right before running to make sure no other library overwrites it
|
||||
|
||||
Logger.setup_logging(LOG_LEVEL=config["logging"]["level"],
|
||||
JSON_LOGS=config["logging"]["json_logs"])
|
4
mypy.ini
4
mypy.ini
@ -67,4 +67,8 @@ ignore_missing_imports = True
|
||||
|
||||
[mypy-aiofiles.*]
|
||||
# No stubs available
|
||||
ignore_missing_imports = True
|
||||
|
||||
[mypy-gunicorn.*]
|
||||
# No stubs available
|
||||
ignore_missing_imports = True
|
26
poetry.lock
generated
26
poetry.lock
generated
@ -232,6 +232,20 @@ category = "main"
|
||||
optional = false
|
||||
python-versions = ">=3.6"
|
||||
|
||||
[[package]]
|
||||
name = "gunicorn"
|
||||
version = "20.1.0"
|
||||
description = "WSGI HTTP Server for UNIX"
|
||||
category = "main"
|
||||
optional = false
|
||||
python-versions = ">=3.5"
|
||||
|
||||
[package.extras]
|
||||
eventlet = ["eventlet (>=0.24.1)"]
|
||||
gevent = ["gevent (>=1.4.0)"]
|
||||
setproctitle = ["setproctitle"]
|
||||
tornado = ["tornado (>=0.2)"]
|
||||
|
||||
[[package]]
|
||||
name = "h11"
|
||||
version = "0.12.0"
|
||||
@ -309,7 +323,7 @@ socks = ["socksio (>=1.0.0,<2.0.0)"]
|
||||
|
||||
[[package]]
|
||||
name = "httpx-cache"
|
||||
version = "0.6.0"
|
||||
version = "0.6.1"
|
||||
description = "Simple caching transport for httpx."
|
||||
category = "main"
|
||||
optional = false
|
||||
@ -797,7 +811,7 @@ h11 = ">=0.9.0,<1"
|
||||
[metadata]
|
||||
lock-version = "1.1"
|
||||
python-versions = "^3.10"
|
||||
content-hash = "b35d9d99689d712256be20c9124a3786a777bcbb9854a57c76ee875a6a812931"
|
||||
content-hash = "424ef1ced5a48b1e652aefa77c0629df286a1f66d6d9286584b46dbac57e80ea"
|
||||
|
||||
[metadata.files]
|
||||
aiofiles = [
|
||||
@ -1055,6 +1069,10 @@ fasteners = [
|
||||
{file = "fasteners-0.17.3-py3-none-any.whl", hash = "sha256:cae0772df265923e71435cc5057840138f4e8b6302f888a567d06ed8e1cbca03"},
|
||||
{file = "fasteners-0.17.3.tar.gz", hash = "sha256:a9a42a208573d4074c77d041447336cf4e3c1389a256fd3e113ef59cf29b7980"},
|
||||
]
|
||||
gunicorn = [
|
||||
{file = "gunicorn-20.1.0-py3-none-any.whl", hash = "sha256:9dcc4547dbb1cb284accfb15ab5667a0e5d1881cc443e0677b4882a4067a807e"},
|
||||
{file = "gunicorn-20.1.0.tar.gz", hash = "sha256:e0a968b5ba15f8a328fdfd7ab1fcb5af4470c28aaf7e55df02a99bc13138e6e8"},
|
||||
]
|
||||
h11 = [
|
||||
{file = "h11-0.12.0-py3-none-any.whl", hash = "sha256:36a3cb8c0a032f56e2da7084577878a035d3b61d104230d4bd49c0c6b555a9c6"},
|
||||
{file = "h11-0.12.0.tar.gz", hash = "sha256:47222cb6067e4a307d535814917cd98fd0a57b6788ce715755fa2b6c28b56042"},
|
||||
@ -1119,8 +1137,8 @@ httpx = [
|
||||
{file = "httpx-0.23.0.tar.gz", hash = "sha256:f28eac771ec9eb4866d3fb4ab65abd42d38c424739e80c08d8d20570de60b0ef"},
|
||||
]
|
||||
httpx-cache = [
|
||||
{file = "httpx-cache-0.6.0.tar.gz", hash = "sha256:3d136ad207d8004e59a69283aa6fc40e1190dad5edbde5859b37778f6d1ecdbf"},
|
||||
{file = "httpx_cache-0.6.0-py3-none-any.whl", hash = "sha256:2b548d68fa55159e2fdc49ea151f513217c21cb4f0057e79fec8376ce15bfe7a"},
|
||||
{file = "httpx-cache-0.6.1.tar.gz", hash = "sha256:699f648e781f6d06c9f50fa398f7ae30fe5b7668711975760d277f14c601d124"},
|
||||
{file = "httpx_cache-0.6.1-py3-none-any.whl", hash = "sha256:b468b9f3d8d8063d3a0f401401cfd5237e37721f975c436598e4e62fd1f5685c"},
|
||||
]
|
||||
hypercorn = [
|
||||
{file = "Hypercorn-0.14.3-py3-none-any.whl", hash = "sha256:7c491d5184f28ee960dcdc14ab45d14633ca79d72ddd13cf4fcb4cb854d679ab"},
|
||||
|
@ -24,6 +24,8 @@ fastapi-paseto-auth = "^0.6.0"
|
||||
ujson = ">=5.5.0"
|
||||
hiredis = ">=2.0.0"
|
||||
aiofiles = ">=22.1.0"
|
||||
uvicorn = ">=0.18.3"
|
||||
gunicorn = ">=20.1.0"
|
||||
|
||||
[tool.poetry.dev-dependencies]
|
||||
mypy = ">=0.971"
|
||||
|
@ -16,12 +16,13 @@ fastapi-cache2==0.1.9; python_version >= "3.7" and python_version < "4.0"
|
||||
fastapi-paseto-auth==0.6.0; python_version >= "3.10"
|
||||
fastapi==0.85.0; python_version >= "3.7"
|
||||
fasteners==0.17.3; python_version >= "3.7" and python_version < "4.0"
|
||||
gunicorn==20.1.0; python_version >= "3.5"
|
||||
h11==0.12.0; python_version >= "3.7" and python_version < "4.0" and python_full_version >= "3.7.0"
|
||||
h2==4.1.0; python_version >= "3.7" and python_full_version >= "3.6.1" and python_version < "4.0"
|
||||
hiredis==2.0.0; python_version >= "3.6"
|
||||
hpack==4.0.0; python_version >= "3.7" and python_full_version >= "3.6.1"
|
||||
httpcore==0.15.0; python_version >= "3.7" and python_version < "4.0"
|
||||
httpx-cache==0.6.0; python_version >= "3.7" and python_version < "4.0"
|
||||
httpx-cache==0.6.1; python_version >= "3.7" and python_version < "4.0"
|
||||
httpx==0.23.0; python_version >= "3.7"
|
||||
hypercorn==0.14.3; python_version >= "3.7"
|
||||
hyperframe==6.0.1; python_version >= "3.7" and python_full_version >= "3.6.1"
|
||||
@ -54,7 +55,7 @@ toolz==0.12.0; python_version >= "3.5"
|
||||
typing-extensions==4.4.0; python_version >= "3.10"
|
||||
ujson==5.5.0; python_version >= "3.7"
|
||||
urllib3==1.26.12; python_version >= "3.6" and python_full_version < "3.0.0" or python_full_version >= "3.6.0" and python_version < "4" and python_version >= "3.6"
|
||||
uvicorn==0.18.3; python_version >= "3.7" and python_version < "4.0"
|
||||
uvicorn==0.18.3; python_version >= "3.7"
|
||||
uvloop==0.17.0; platform_system != "Windows" and python_version >= "3.7"
|
||||
win32-setctime==1.1.0; sys_platform == "win32" and python_version >= "3.5"
|
||||
wrapt==1.14.1; python_version >= "3.6" and python_full_version < "3.0.0" or python_full_version >= "3.5.0" and python_version >= "3.6"
|
||||
|
148
run.py
Executable file
148
run.py
Executable file
@ -0,0 +1,148 @@
|
||||
import os
|
||||
import sys
|
||||
import toml
|
||||
import logging
|
||||
import sentry_sdk
|
||||
from app.main import app
|
||||
from loguru import logger
|
||||
from fastapi import FastAPI
|
||||
from types import FrameType
|
||||
from typing import Any, Optional
|
||||
from multiprocessing import cpu_count
|
||||
from gunicorn.glogging import Logger
|
||||
from gunicorn.app.base import BaseApplication
|
||||
from sentry_sdk.integrations.redis import RedisIntegration
|
||||
from sentry_sdk.integrations.httpx import HttpxIntegration
|
||||
from sentry_sdk.integrations.gnu_backtrace import GnuBacktraceIntegration
|
||||
|
||||
config: dict = toml.load("config.toml")
|
||||
|
||||
# Enable sentry logging
|
||||
|
||||
sentry_sdk.init(os.environ['SENTRY_DSN'], traces_sample_rate=1.0, integrations=[
|
||||
RedisIntegration(),
|
||||
HttpxIntegration(),
|
||||
GnuBacktraceIntegration(),
|
||||
],)
|
||||
|
||||
LOG_LEVEL: Any = logging.getLevelName(config['logging']['level'])
|
||||
JSON_LOGS: bool = config['logging']['json_logs']
|
||||
WORKERS: int = int(cpu_count() + 1)
|
||||
BIND: str = f'{os.environ.get("HYPERCORN_HOST")}:{os.environ.get("HYPERCORN_PORT")}'
|
||||
|
||||
class InterceptHandler(logging.Handler):
|
||||
"""Intercept logs and forward them to Loguru.
|
||||
|
||||
Args:
|
||||
logging.Handler (Filterer): Handler to filter logs
|
||||
"""
|
||||
def emit(self, record: logging.LogRecord) -> None:
|
||||
"""Emit a log record."""
|
||||
|
||||
# Get corresponding Loguru level if it exists
|
||||
level: str | int
|
||||
frame: FrameType
|
||||
depth: int
|
||||
|
||||
try:
|
||||
level = logger.level(record.levelname).name
|
||||
except ValueError:
|
||||
level = record.levelno
|
||||
|
||||
# Find caller from where originated the logged message
|
||||
frame, depth = logging.currentframe(), 2
|
||||
while frame.f_code.co_filename == logging.__file__:
|
||||
frame = frame.f_back
|
||||
depth += 1
|
||||
|
||||
logger.opt(depth=depth, exception=record.exc_info).log(level, record.getMessage())
|
||||
|
||||
|
||||
class StubbedGunicornLogger(Logger):
|
||||
"""Defining a custom logger class to prevent gunicorn from logging to stdout
|
||||
|
||||
Args:
|
||||
Logger (object): Gunicon logger class
|
||||
"""
|
||||
def setup(self, cfg) -> None:
|
||||
"""Setup logger."""
|
||||
|
||||
handler: logging.NullHandler = logging.NullHandler()
|
||||
self.error_logger: Logger = logging.getLogger("gunicorn.error")
|
||||
|
||||
self.error_logger.addHandler(handler)
|
||||
|
||||
self.access_logger: Logger = logging.getLogger("gunicorn.access")
|
||||
|
||||
self.access_logger.addHandler(handler)
|
||||
self.error_logger.setLevel(LOG_LEVEL)
|
||||
self.access_logger.setLevel(LOG_LEVEL)
|
||||
|
||||
|
||||
class StandaloneApplication(BaseApplication):
|
||||
"""Defines a Guicorn application
|
||||
|
||||
Args:
|
||||
BaseApplication (object): Base class for Gunicorn applications
|
||||
"""
|
||||
|
||||
def __init__(self, app: FastAPI, options: dict | None = None):
|
||||
"""Initialize the application
|
||||
|
||||
Args:
|
||||
app (fastapi.FastAPI): FastAPI application
|
||||
options (dict, optional): Gunicorn options. Defaults to None.
|
||||
"""
|
||||
self.options: dict = options or {}
|
||||
self.application: FastAPI = app
|
||||
super().__init__()
|
||||
|
||||
def load_config(self) -> None:
|
||||
"""Load Gunicorn configuration."""
|
||||
config: dict = {
|
||||
key: value for key, value in self.options.items()
|
||||
if key in self.cfg.settings and value is not None
|
||||
}
|
||||
for key, value in config.items():
|
||||
self.cfg.set(key.lower(), value)
|
||||
|
||||
def load(self) -> FastAPI:
|
||||
"""Load the application
|
||||
|
||||
Returns:
|
||||
FastAPI: FastAPI application
|
||||
"""
|
||||
return self.application
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
intercept_handler = InterceptHandler()
|
||||
logging.root.setLevel(LOG_LEVEL)
|
||||
|
||||
seen: set = set()
|
||||
for name in [
|
||||
*logging.root.manager.loggerDict.keys(),
|
||||
"gunicorn",
|
||||
"gunicorn.access",
|
||||
"gunicorn.error",
|
||||
"uvicorn",
|
||||
"uvicorn.access",
|
||||
"uvicorn.error",
|
||||
]:
|
||||
if name not in seen:
|
||||
seen.add(name.split(".")[0])
|
||||
logging.getLogger(name).handlers = [intercept_handler]
|
||||
|
||||
logger.configure(handlers=[{"sink": sys.stdout, "serialize": JSON_LOGS}])
|
||||
|
||||
options: dict = {
|
||||
"bind": BIND,
|
||||
"workers": WORKERS,
|
||||
"accesslog": "-",
|
||||
"errorlog": "-",
|
||||
"worker_class": "uvicorn.workers.UvicornWorker",
|
||||
"logger_class": StubbedGunicornLogger,
|
||||
"preload": True,
|
||||
}
|
||||
|
||||
StandaloneApplication(app, options).run()
|
12
run.sh
12
run.sh
@ -1,12 +0,0 @@
|
||||
#!/bin/bash
|
||||
|
||||
# This script is used to run the application
|
||||
# It is used by the Dockerfile
|
||||
|
||||
# get number of cores
|
||||
CORES=$(grep -c ^processor /proc/cpuinfo)
|
||||
|
||||
# Start the application
|
||||
hypercorn main:app --bind="${HYPERCORN_HOST}:${HYPERCORN_PORT}" \
|
||||
--workers="$CORES" --log-level="$HYPERCORN_LOG_LEVEL" \
|
||||
--worker-class uvloop
|
@ -1,82 +0,0 @@
|
||||
import os
|
||||
import toml
|
||||
from typing import Any
|
||||
from redis import asyncio as aioredis
|
||||
|
||||
import src.utils.Logger as Logger
|
||||
from src.utils.RedisConnector import RedisConnector
|
||||
|
||||
config: dict = toml.load("config.toml")
|
||||
|
||||
class InternalCache:
|
||||
"""Implements an internal cache for ReVanced Releases API."""
|
||||
|
||||
redis = RedisConnector.connect(config['internal-cache']['database'])
|
||||
|
||||
InternalCacheLogger = Logger.InternalCacheLogger()
|
||||
|
||||
async def store(self, key: str, value: dict) -> None:
|
||||
"""Stores a key-value pair in the cache.
|
||||
|
||||
Args:
|
||||
key (str): the key to store
|
||||
value (dict): the JSON value to store
|
||||
"""
|
||||
try:
|
||||
await self.redis.json().set(key, '$', value)
|
||||
await self.redis.expire(key, config['internal-cache']['expire'])
|
||||
await self.InternalCacheLogger.log("SET", None, key)
|
||||
except aioredis.RedisError as e:
|
||||
await self.InternalCacheLogger.log("SET", e)
|
||||
|
||||
async def delete(self, key: str) -> None:
|
||||
"""Removes a key-value pair from the cache.
|
||||
|
||||
Args:
|
||||
key (str): the key to delete
|
||||
"""
|
||||
try:
|
||||
await self.redis.delete(key)
|
||||
await self.InternalCacheLogger.log("DEL", None, key)
|
||||
except aioredis.RedisError as e:
|
||||
await self.InternalCacheLogger.log("DEL", e)
|
||||
|
||||
async def get(self, key: str) -> dict:
|
||||
"""Gets a key-value pair from the cache.
|
||||
|
||||
Args:
|
||||
key (str): the key to retrieve
|
||||
|
||||
Returns:
|
||||
dict: the JSON value stored in the cache or an empty dict if key doesn't exist or an error occurred
|
||||
"""
|
||||
try:
|
||||
payload: dict[Any, Any] = await self.redis.json().get(key)
|
||||
await self.InternalCacheLogger.log("GET", None, key)
|
||||
return payload
|
||||
except aioredis.RedisError as e:
|
||||
await self.InternalCacheLogger.log("GET", e)
|
||||
return {}
|
||||
|
||||
async def exists(self, key: str) -> bool:
|
||||
"""Checks if a key exists in the cache.
|
||||
|
||||
Args:
|
||||
key (str): key to check
|
||||
|
||||
Returns:
|
||||
bool: True if key exists, False if key doesn't exist or an error occurred
|
||||
"""
|
||||
try:
|
||||
if await self.redis.exists(key):
|
||||
await self.InternalCacheLogger.log("EXISTS", None, key)
|
||||
return True
|
||||
else:
|
||||
await self.InternalCacheLogger.log("EXISTS", None, key)
|
||||
return False
|
||||
except aioredis.RedisError as e:
|
||||
await self.InternalCacheLogger.log("EXISTS", e)
|
||||
return False
|
||||
|
||||
|
||||
|
Loading…
x
Reference in New Issue
Block a user