mirror of
https://github.com/revanced/revanced-releases-api.git
synced 2025-05-02 07:04:28 +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