|
from asyncio import create_subprocess_shell, create_task, gather, run, sleep |
|
from json import dumps |
|
from logging import ERROR, INFO, basicConfig, getLogger |
|
from pathlib import Path |
|
from shutil import rmtree |
|
from subprocess import CalledProcessError, PIPE |
|
from typing import Any, List |
|
from uuid import uuid4 |
|
|
|
from PIL import Image |
|
from aiorentry.client import Client as RentryClient |
|
from fastapi import FastAPI, HTTPException |
|
from fastapi.responses import PlainTextResponse |
|
from httpx import AsyncClient, HTTPStatusError, RequestError |
|
from lxml import html |
|
from markdown import markdown |
|
from pydantic import BaseModel, HttpUrl |
|
from uvicorn import run as uvicorn_run |
|
|
|
proxy_endpoint = 'https://telegraphprxver.vercel.app' |
|
|
|
need_logging = False |
|
|
|
basicConfig(level=INFO if need_logging else ERROR) |
|
logger = getLogger(__name__) |
|
|
|
oxipng_bin = Path(__file__).parent / 'oxipng' |
|
if not oxipng_bin.stat().st_mode & 0o111: |
|
oxipng_bin.chmod(0o755) |
|
|
|
tokens = [ |
|
|
|
'7e0ea3da6a73d77003c1abba7f0ea13c', |
|
'bc2e68b5918e5bb59ebca6c05d73daf9', |
|
'fecbfbe0938bcd1df27b7a9be1702cc9', |
|
'04e9981d4d0981964cb4c9753173244d', |
|
'dee75b07981c7aa211628ea7c7cbc03d', |
|
] |
|
|
|
|
|
class UserInfo: |
|
def __init__(self, token: str, short_name: str, author_name: str, author_url: str, auth_link: str): |
|
self.token = token |
|
self.short_name = short_name |
|
self.author_name = author_name |
|
self.author_url = author_url |
|
self.auth_link = auth_link |
|
|
|
def get_token(self) -> str: |
|
return self.token |
|
|
|
def get_short_name(self) -> str: |
|
return self.short_name |
|
|
|
def get_author_name(self) -> str: |
|
return self.author_name |
|
|
|
def get_author_url(self) -> str: |
|
return self.author_url |
|
|
|
def get_auth_link(self) -> str: |
|
return self.auth_link |
|
|
|
def __repr__(self): |
|
return f'UserAuthInfo(token={self.token}, short_name={self.short_name}, author_name={self.author_name}, author_url={self.author_url}, auth_link={self.auth_link})' |
|
|
|
|
|
async def create_account(short_name: str, author_name: str, author_url: str) -> UserInfo: |
|
params = { |
|
'short_name': short_name, |
|
'author_name': author_name, |
|
'author_url': author_url |
|
} |
|
async with AsyncClient(verify=False, follow_redirects=True, timeout=30.0) as client: |
|
response = await client.get(f'{proxy_endpoint}/createAccount', params=params) |
|
json_response: dict = response.json() |
|
if json_response.get('ok'): |
|
result = json_response.get('result', {}) |
|
return UserInfo(result['access_token'], result['short_name'], result['author_name'], result['author_url'], result['auth_url']) |
|
|
|
|
|
tgph_acc = run(create_account('prolapse', 'prolapse', '')) |
|
|
|
|
|
def md_to_dom(markdown_text: str) -> list[dict[str, str | list | dict | None]]: |
|
def handle_heading(element) -> dict[str, str | list | dict | None]: |
|
if element.tag == 'h1': |
|
return {'tag': 'h3', 'children': parse_children(element)} |
|
elif element.tag == 'h2': |
|
return {'tag': 'h4', 'children': parse_children(element)} |
|
else: |
|
return {'tag': 'p', 'children': [{'tag': 'strong', 'children': parse_children(element)}]} |
|
|
|
def handle_list(element) -> dict[str, str | list | dict | None]: |
|
return {'tag': element.tag, 'children': [{'tag': 'li', 'children': parse_children(li)} for li in element.xpath('./li')]} |
|
|
|
def handle_link(element) -> dict[str, str | list | dict | None]: |
|
return {'tag': 'a', 'attrs': {'href': element.get('href')}, 'children': parse_children(element)} |
|
|
|
def handle_media(element) -> dict[str, str | list | dict | None]: |
|
return {'tag': element.tag, 'attrs': {'src': element.get('src')}} |
|
|
|
def parse_children(element) -> list[str | dict[str, str | list | dict | None]]: |
|
return [parse_element(child) if child.tag else child.strip() for child in element.iterchildren() if child.tag or (isinstance(child, str) and child.strip())] |
|
|
|
def parse_element(element) -> dict[str, str | list | dict | None]: |
|
handlers = {'h1': handle_heading, 'h2': handle_heading, 'h3': handle_heading, 'h4': handle_heading, 'h5': handle_heading, 'h6': handle_heading, 'ul': handle_list, 'ol': handle_list, 'a': handle_link, 'img': handle_media, 'iframe': handle_media} |
|
handler = handlers.get(element.tag, lambda e: {'tag': e.tag, 'children': parse_children(e)}) |
|
return handler(element) |
|
|
|
html_content = markdown(markdown_text, extensions=['extra', 'sane_lists']) |
|
tree = html.fromstring(html_content) |
|
return [parse_element(element) for element in tree.body] |
|
|
|
|
|
async def tgph_create_page(token: str, title: str, markdown_text: str) -> str: |
|
params = { |
|
'access_token': token, |
|
'title': title, |
|
'content': dumps(md_to_dom(markdown_text)), |
|
'return_content': False |
|
} |
|
async with AsyncClient(verify=False, follow_redirects=True, timeout=30.0) as client: |
|
response = await client.get(f'{proxy_endpoint}/createPage', params=params) |
|
json_response: dict = response.json() |
|
if json_response.get('ok'): |
|
result = json_response.get('result', {}) |
|
return result.get('path') |
|
|
|
|
|
async def tgph_edit_page(token: str, page: str, title: str, markdown_text: str) -> str: |
|
params = { |
|
'access_token': token, |
|
'path': page, |
|
'title': title, |
|
'content': dumps(md_to_dom(markdown_text)), |
|
'return_content': False |
|
} |
|
async with AsyncClient(verify=False, follow_redirects=True, timeout=30.0) as client: |
|
response = await client.get(f'{proxy_endpoint}/editPage', params=params) |
|
json_response = response.json() |
|
if json_response.get('ok'): |
|
result = json_response.get('result', {}) |
|
return result.get('path') |
|
|
|
|
|
async def download_png(url: str, folder: str, client: AsyncClient, retries: int = 5) -> Path: |
|
|
|
for attempt in range(retries): |
|
try: |
|
response = await client.get(url) |
|
response.raise_for_status() |
|
file_path = Path(__file__).parent / folder / f'{uuid4()}.png' |
|
file_path.parent.mkdir(parents=True, exist_ok=True) |
|
file_path.write_bytes(response.content) |
|
return file_path |
|
except (HTTPStatusError, RequestError) as e: |
|
if attempt < retries - 1: |
|
await sleep(2 ** attempt) |
|
else: |
|
raise e |
|
|
|
|
|
async def download_pngs(urls: str | list[str]) -> list[Any]: |
|
urls = [urls] if isinstance(urls, str) else urls |
|
|
|
|
|
valid_urls = [url for url in urls if url and '\n' not in url and '\\n' not in url and url.strip() != ''] |
|
if len(valid_urls) != len(urls): |
|
print(f'некорректные ссылки удалены из списка: {set(urls) - set(valid_urls)}') |
|
async with AsyncClient(verify=False, follow_redirects=True, timeout=30.0) as client: |
|
tasks = [download_png(url, str(uuid4()), client) for url in valid_urls] |
|
return list(await gather(*tasks)) |
|
|
|
|
|
async def optimize_png(image_path: Path, retries: int = 3) -> None: |
|
command = f'{oxipng_bin.resolve()} --opt 2 --strip safe --out {image_path} {image_path}' |
|
|
|
for attempt in range(retries): |
|
try: |
|
process = await create_subprocess_shell(command, stdout=PIPE, stderr=PIPE) |
|
stdout, stderr = await process.communicate() |
|
if process.returncode == 0: |
|
return |
|
else: |
|
raise CalledProcessError(process.returncode, command, output=stdout, stderr=stderr) |
|
except CalledProcessError as e: |
|
print(f'ошибка при оптимизации {image_path}') |
|
if attempt < retries - 1: |
|
await sleep(2 ** attempt) |
|
else: |
|
raise e |
|
|
|
|
|
async def convert_to_jpeg(image_path: Path) -> Path: |
|
|
|
try: |
|
image = Image.open(image_path) |
|
output_path = image_path.with_suffix('.jpg') |
|
image.save(output_path, 'JPEG', quality=98, optimize=True) |
|
image_path.unlink(missing_ok=True) |
|
return output_path |
|
except: |
|
print(f'ошибка при конвертации {image_path}') |
|
return image_path |
|
|
|
|
|
async def convert_to_jpegs(image_paths: list[str | Path] | str | Path) -> tuple[Path]: |
|
image_paths = [Path(image_file) for image_file in ([image_paths] if not isinstance(image_paths, list) else image_paths)] |
|
|
|
tasks = [convert_to_jpeg(image_path) for image_path in image_paths] |
|
return await gather(*tasks) |
|
|
|
|
|
async def optimize_pngs(image_paths: list[str | Path] | str | Path) -> None: |
|
image_paths = [Path(image_file) for image_file in ([image_paths] if not isinstance(image_paths, list) else image_paths)] |
|
|
|
tasks = [optimize_png(image_path) for image_path in image_paths] |
|
await gather(*tasks) |
|
|
|
|
|
async def upload_image_to_imgbb(file_path: Path, file_type: str = 'png') -> str | None: |
|
for token in tokens: |
|
url = f'https://api.imgbb.com/1/upload?key={token}' |
|
try: |
|
with file_path.open('rb') as file: |
|
files = {'image': (file_path.name, file, f'image/{file_type}')} |
|
data = {} |
|
async with AsyncClient(verify=False, follow_redirects=True, timeout=30.0) as client: |
|
response = await client.post(url, files=files, data=data, timeout=30) |
|
response.raise_for_status() |
|
json = response.json() |
|
if json.get('success'): |
|
return json['data']['url'] |
|
except Exception as e: |
|
print(f"ошибка при загрузке с {token}: {e}") |
|
continue |
|
return None |
|
|
|
|
|
async def upload_image_to_freeimagehost(image_path: Path, file_type: str = 'png') -> str | None: |
|
try: |
|
async with AsyncClient(verify=False, follow_redirects=True, timeout=30.0) as client: |
|
with image_path.open("rb") as image_file: |
|
files = {'source': (image_path.name, image_file, f'image/{file_type}')} |
|
payload = {'key': '6d207e02198a847aa98d0a2a901485a5', 'action': 'upload', 'format': 'json'} |
|
response = await client.post('https://freeimage.host/api/1/upload', data=payload, files=files) |
|
response.raise_for_status() |
|
response_data = response.json() |
|
return response_data['image']['url'] |
|
except Exception as e: |
|
print(f'ошибка при загрузке {image_path}: {e}') |
|
return None |
|
|
|
|
|
async def upload_image(file_path: Path | str, file_type: str = 'png') -> str | None: |
|
file_path = Path(file_path) |
|
return await upload_image_to_freeimagehost(file_path, file_type) or upload_image_to_imgbb(file_path, file_type) |
|
|
|
|
|
async def process_image(old_url: str, image_path: Path, convert: bool) -> tuple[str, Path]: |
|
new_url = await upload_image(image_path, 'png' if not convert else 'jpeg') |
|
if new_url: |
|
|
|
pass |
|
else: |
|
new_url = old_url |
|
print(f'не удалось загрузить изображение {image_path}, оставим старую ссылку: {old_url}') |
|
|
|
try: |
|
image_path.unlink() |
|
except Exception as e: |
|
print(f'не удалось удалить файл {image_path}: {e}') |
|
|
|
return new_url, image_path |
|
|
|
|
|
async def optimize_and_upload(images_urls: List[str] | str, convert: bool = False) -> List[str]: |
|
images_urls = [images_urls] if isinstance(images_urls, str) else images_urls |
|
if convert: |
|
try: |
|
ph_link = await tgph_create_page(tgph_acc.get_token(), 'DAll-E v3', '*изображения скоро появятся, обнови страницу...*') |
|
except Exception as e: |
|
print(f'не получилось создать страницу на телеграфе: {e}') |
|
ph_link = None |
|
async with RentryClient('https://rentry.org') as client: |
|
page = await client.new_page('изображения скоро появятся, обнови страницу...') |
|
page_id, code = page.url, page.edit_code |
|
continue_task = create_task(continue_optimizing_and_uploading(images_urls, page_id, code, ph_link)) |
|
print(f'https://telegra.ph/{ph_link}', f'https://rentry.co/{page_id}', f'https://rentry.org/{page_id}') |
|
return [f'https://telegra.ph/{ph_link}', f'https://rentry.co/{page_id}', f'https://rentry.org/{page_id}'] if ph_link else [f'https://rentry.co/{page_id}', f'https://rentry.org/{page_id}'] |
|
else: |
|
return await continue_optimizing_and_uploading(images_urls) |
|
|
|
|
|
async def continue_optimizing_and_uploading(images_urls: list[str], page_id: str = None, code: str = None, ph_link: str = None) -> list[str]: |
|
images_paths = await download_pngs(images_urls) |
|
|
|
if not page_id: |
|
await optimize_pngs(images_paths) |
|
|
|
new_images_urls = [] |
|
images_paths = images_paths if not page_id else await convert_to_jpegs(images_paths) |
|
|
|
tasks = [] |
|
for old_url, image_path in zip(images_urls, images_paths): |
|
tasks.append(process_image(old_url, image_path, page_id is not None)) |
|
|
|
results = await gather(*tasks) |
|
new_images_urls = [result[0] for result in results] |
|
print(f'новые ссылки: ({len(new_images_urls)}): {new_images_urls}') |
|
|
|
try: |
|
rmtree(images_paths[0].parent) |
|
except Exception as e: |
|
print(f'не удалось удалить файл {images_paths[0].parent}: {e}') |
|
|
|
content = '\n\n'.join([f'![]({url})' for url in new_images_urls]) |
|
if ph_link: |
|
try: |
|
await tgph_edit_page(tgph_acc.get_token(), ph_link, 'DAll-E v3', content) |
|
except Exception as e: |
|
print(f'не удалось создать отредактировать на телеграфе: {e}') |
|
if page_id and code: |
|
try: |
|
async with RentryClient('https://rentry.org') as client: |
|
await client.edit_page(text=content, url=page_id, edit_code=code) |
|
except Exception as e: |
|
print(f'не удалось создать страницу в rentry: {e}') |
|
|
|
return new_images_urls if len(new_images_urls) > 0 and new_images_urls[0] else images_urls |
|
|
|
|
|
app = FastAPI() |
|
|
|
|
|
class ImageURLs(BaseModel): |
|
urls: List[HttpUrl] |
|
|
|
|
|
@app.get('/') |
|
async def read_root(): |
|
return PlainTextResponse('ну пролапс, ну и что', status_code=200) |
|
|
|
|
|
@app.post('/pngopt_by_urls/') |
|
async def optimize_images_endpoint(image_urls: ImageURLs): |
|
try: |
|
optimized_urls = await optimize_and_upload([str(url) for url in image_urls.urls]) |
|
return {"optimized_urls": optimized_urls} |
|
except Exception as e: |
|
raise HTTPException(status_code=500, detail=str(e)) |
|
|
|
|
|
@app.post('/jpegs_by_urls/') |
|
async def optimize_images_endpoint(image_urls: ImageURLs): |
|
try: |
|
optimized_urls = await optimize_and_upload([str(url) for url in image_urls.urls], convert=True) |
|
return {"optimized_urls": optimized_urls} |
|
except Exception as e: |
|
raise HTTPException(status_code=500, detail=str(e)) |
|
|
|
|
|
if __name__ == "__main__": |
|
uvicorn_run(app, host='0.0.0.0', port=7860, timeout_keep_alive=90, log_level='info', use_colors=False) |
|
|