mirror of
https://github.com/revanced/revanced-releases-api.git
synced 2025-05-01 23:04:24 +02:00
Add project files
This commit is contained in:
parent
7de9f2348d
commit
0c929689d8
14
Dockerfile
Normal file
14
Dockerfile
Normal 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
42
config.toml
Normal 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"]
|
82
main.py
Normal file
82
main.py
Normal 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
116
modules/Releases.py
Normal 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
28
modules/ResponseFields.py
Normal 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
69
modules/ResponseModels.py
Normal 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
0
modules/__init__.py
Normal file
9
requirements.txt
Normal file
9
requirements.txt
Normal 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
|
Loading…
x
Reference in New Issue
Block a user