Add project files

This commit is contained in:
Alexandre Teles 2022-08-28 20:05:37 -03:00
parent 7de9f2348d
commit 0c929689d8
9 changed files with 360 additions and 0 deletions

14
Dockerfile Normal file
View File

@ -0,0 +1,14 @@
FROM python:3-slim
ARG GITHUB_TOKEN
ENV GITHUB_TOKEN $GITHUB_TOKEN
WORKDIR /usr/src/app
COPY . .
RUN apt update && \
apt-get install gcc -y && \
pip install --no-cache-dir -r requirements.txt
CMD [ "python3", "./main.py" ]

42
config.toml Normal file
View File

@ -0,0 +1,42 @@
[docs]
title = "ReVanced Releases API"
description = """
This website provides a JSON API for ReVanced Releases 🚀
Changelogs are not included but can be found on the [ReVanced Repositories](https://github.com/revanced/).
The team also have a [Discord Server](https://revanced.app/discord) if you need help.
### API Endpoints
* [apps](/apps) - Returns all currently patchable apps
* [tools](/tools) - Returns the latest version of all ReVanced tools and Vanced MicroG
* [patches](/patches) - Returns the latest version of all ReVanced patches
There is no cache on this API because we trust our fellow developers to implement a cache on their end.
So please be kind and don't blow up the API with requests or we will have to block your IP address.
Godspeed 💀
"""
version = "0.1 alpha"
[license]
name = "AGPL-3.0"
url = "https://www.gnu.org/licenses/agpl-3.0.en.html"
[uvicorn]
host = "0.0.0.0"
port = 8000
[slowapi]
limit = "15/minute"
[app]
repositories = ["TeamVanced/VancedMicroG", "revanced/revanced-cli", "revanced/revanced-patches", "revanced/revanced-integrations"]

BIN
dump.rdb Normal file

Binary file not shown.

82
main.py Normal file
View File

@ -0,0 +1,82 @@
#!/usr/bin/env python3
import toml
import uvicorn
from fastapi import FastAPI, Request, Response
from modules.Releases import Releases
from fastapi.responses import RedirectResponse
import modules.ResponseModels as ResponseModels
from slowapi import Limiter, _rate_limit_exceeded_handler
from slowapi.util import get_remote_address
from slowapi.errors import RateLimitExceeded
"""Get latest ReVanced releases from GitHub API."""
# Load config
config = toml.load("config.toml")
# Create releases instance
releases = Releases()
# 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']
})
# 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)
# Routes
@app.get("/", response_class=RedirectResponse, status_code=301)
@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.LatestTools)
@limiter.limit(config['slowapi']['limit'])
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('/apps', response_model=ResponseModels.SupportedApps)
@limiter.limit(config['slowapi']['limit'])
async def apps(request: Request, response: Response) -> dict:
"""Get patchable apps.
Returns:
json: list of supported apps
"""
return await releases.get_patchable_apps()
@app.get('/patches', response_model=ResponseModels.LatestPatches)
@limiter.limit(config['slowapi']['limit'])
async def patches(request: Request, response: Response) -> dict:
"""Get latest patches.
Returns:
json: list of latest patches
"""
return await releases.get_latest_patches()
# Run app
if __name__ == '__main__':
uvicorn.run(app, host=config['uvicorn']['host'], port=config['uvicorn']['port'])

116
modules/Releases.py Normal file
View File

@ -0,0 +1,116 @@
import os
import pycmarkgfm
import httpx_cache
from typing import Dict, List
from base64 import b64decode
from bs4 import BeautifulSoup
class Releases:
"""Implements the methods required to get the latest releases and patches from revanced repositories."""
headers = {'Accept': "application/vnd.github+json",
'Authorization': "token " + os.environ['GITHUB_TOKEN']
}
async def get_release(self, client: httpx_cache.AsyncClient, repository: str) -> list:
"""Get assets from latest release in a given repository.
Args:
client (httpx_cache.AsyncClient): httpx_cache reusable async client
repository (str): Github's standard username/repository notation
Returns:
dict: dictionary of filename and download url
"""
assets = []
response = await client.get(f"https://api.github.com/repos/{repository}/releases/latest")
release_assets = response.json()['assets']
for asset in release_assets:
assets.append({ 'repository': repository,
'name': asset['name'],
'size': asset['size'],
'browser_download_url': asset['browser_download_url'],
'content_type': asset['content_type']
})
return assets
async def get_latest_releases(self, repositories: list) -> dict:
"""Runs get_release() in parallel for each repository.
Args:
repositories (list): List of repositories in Github's standard username/repository notation
Returns:
dict: A dictionary containing assets from each repository
"""
releases: Dict[str, List] = {}
releases['tools'] = []
async with httpx_cache.AsyncClient(headers=self.headers, http2=True) as client:
for repository in repositories:
files = await self.get_release(client, repository)
for file in files:
releases['tools'].append(file)
return releases
async def get_patches_readme(self, client: httpx_cache.AsyncClient) -> str:
"""Get revanced-patches repository's README.md.
Returns:
str: README.md content
"""
response = await client.get(f"https://api.github.com/repos/revanced/revanced-patches/contents/README.md")
return b64decode(response.json()['content']).decode('utf-8')
async def get_patchable_apps(self) -> dict:
"""Get patchable apps from revanced-patches repository.
Returns:
dict: Apps available for patching
"""
packages: Dict[str, List] = {}
packages['apps'] = []
async with httpx_cache.AsyncClient(headers=self.headers, http2=True) as client:
content = await self.get_patches_readme(client)
for line in content.splitlines():
if line.startswith(u'###'):
packages['apps'].append(line.split('`')[1])
return packages
async def get_latest_patches(self) -> dict:
"""Get latest patches from revanced-patches repository.
Returns:
dict: Patches available for a given app
"""
patches: Dict[str, List] = {}
patches['patches'] = []
async with httpx_cache.AsyncClient(headers=self.headers, http2=True) as client:
content = await self.get_patches_readme(client)
html = pycmarkgfm.gfm_to_html(content)
soup = BeautifulSoup(html, 'lxml')
headings = soup.find_all('h3')
for heading in headings:
app_name = heading.text.split(' ')[1]
for patch in heading.find_next_sibling().find_all('tr')[1:]:
app_patches = patch.find_all('td')
patches['patches'].append({"target_app": app_name,
"patch_name" : app_patches[0].text,
"description": app_patches[1].text,
"target_version": app_patches[2].text
})
return patches

28
modules/ResponseFields.py Normal file
View File

@ -0,0 +1,28 @@
from enum import Enum
class LatestToolsFields(str, Enum):
"""Implements the fields for the /tools endpoint.
Args:
str (str): String
Enum (Enum): Enum from pydantic
"""
repository = 'repository'
name = 'name'
size = 'size'
browser_download_url = 'browser_download_url'
content_type = 'content_type'
class LatestPatchesFields(str, Enum):
"""Implements the fields for the /patches endpoint.
Args:
str (str): String
Enum (Enum): Enum from pydantic
"""
target_app = "target_app"
patch_name = "patch_name"
description = "description"
target_version = "target_version"

69
modules/ResponseModels.py Normal file
View File

@ -0,0 +1,69 @@
import modules.ResponseFields as ResponseFields
from typing import Dict, Union, List
from pydantic import BaseModel, create_model
"""Implements pydantic models and model generator for the API's responses."""
class ModelGenerator():
"""Generates a pydantic model from a dictionary."""
def __make_model(self, v, name):
# Parses a dictionary and creates a pydantic model from it.
#
# Args:
# v: key value
# name: key name
#
# Returns:
# pydantic.BaseModel: Generated pydantic model
if type(v) is dict:
return create_model(name, **{k: self.__make_model(v, k) for k, v in v.items()}), ...
return None, v
def generate(self, v: Dict, name: str):
"""Returns a pydantic model from a dictionary.
Args:
v (Dict): JSON dictionary
name (str): Model name
Returns:
pydantic.BaseModel: Generated pydantic model
"""
return self.__make_model(v, name)[0]
class SupportedApps(BaseModel):
"""Implements the JSON response model for the /apps endpoint.
Args:
BaseModel (pydantic.BaseModel): BaseModel from pydantic
"""
apps: List[str]
class LatestTools(BaseModel):
"""Implements the JSON response model for the /tools endpoint.
Args:
BaseModel (pydantic.BaseModel): BaseModel from pydantic
"""
tools: List[ Dict[ ResponseFields.LatestToolsFields, str ] ]
class Config:
use_enum_values = True
class LatestPatches(BaseModel):
"""Implements the JSON response model for the /patches endpoint.
Args:
BaseModel (pydantic.BaseModel): BaseModel from pydantic
"""
patches: List[ Dict[ ResponseFields.LatestPatchesFields, str ] ]
class Config:
use_enum_values = True

0
modules/__init__.py Normal file
View File

9
requirements.txt Normal file
View File

@ -0,0 +1,9 @@
fastapi>=0.81.0
uvicorn[standard]>=0.18.3
httpx[http2]>=0.23.0
httpx-cache>=0.6.0
pycmarkgfm>=1.1.0
beautifulsoup4>=4.11.1
lxml>=4.9.1
toml>=0.10.2
slowapi>=0.1.6