Auto-Rename / bot /telegraph.py
github-actions[bot]
Automated sync from GitHub
5f400f8
Raw History Blame Contribute Delete
5.33 kB
"""A minimal Telegraph (telegra.ph) client: one account for the bot, pages created from node lists."""
from __future__ import annotations
import asyncio
import json
import logging
from typing import Any, Awaitable, Callable, Dict, List, Optional
import aiohttp
log = logging.getLogger(__name__)
API = "https://api.telegra.ph"
CONTENT_LIMIT = 60_000 # Telegraph refuses a page whose content is over 64 KB; stay clear of it
TITLE_LIMIT = 256
Node = Dict[str, Any]
class TelegraphError(RuntimeError):
"""Telegraph refused or could not be reached; the message is safe to show to the user."""
def __init__(self, message: str, code: str = ""):
super().__init__(message)
self.code = code
def content_size(nodes: List[Node]) -> int:
"""Bytes the content takes as it is sent."""
return len(dump(nodes).encode("utf-8"))
def dump(nodes: List[Node]) -> str:
return json.dumps(nodes, ensure_ascii=False, separators=(",", ":"))
class Telegraph:
"""Creates pages under one account. The access token comes from ``load_token`` and is saved with ``save_token``."""
def __init__(
self,
load_token: Callable[[], Awaitable[Optional[str]]],
save_token: Callable[[str], Awaitable[None]],
*,
api: str = API,
short_name: str = "AutoRename",
author: str = "Auto Rename Bot",
timeout: float = 30,
):
self._load = load_token
self._save = save_token
self.api = api.rstrip("/")
self.short_name = short_name
self.author = author
self.author_url = ""
self.timeout = timeout
self._token: Optional[str] = None
self._lock = asyncio.Lock()
def set_author(self, name: str, url: str = "") -> None:
"""The name (and link) shown under the title of the pages; the bot's own, once it is known."""
if name.strip():
self.author = name.strip()[:128]
self.author_url = url.strip()[:512]
async def _call(self, method: str, **params: str) -> Dict[str, Any]:
try:
async with aiohttp.ClientSession(timeout=aiohttp.ClientTimeout(total=self.timeout)) as session:
async with session.post(f"{self.api}/{method}", data=params) as resp:
data = await resp.json(content_type=None)
except asyncio.TimeoutError:
raise TelegraphError("Telegraph took too long to answer") from None
except (aiohttp.ClientError, ValueError) as exc:
raise TelegraphError(f"I couldn't reach Telegraph ({type(exc).__name__})") from exc
if not isinstance(data, dict) or not data.get("ok"):
code = str(data.get("error") if isinstance(data, dict) else "") or "UNKNOWN"
if code.startswith("FLOOD_WAIT_"):
raise TelegraphError(f"Telegraph asks to wait {code.rsplit('_', 1)[-1]} seconds", code)
raise TelegraphError(f"Telegraph said {code}", code)
result = data.get("result")
if not isinstance(result, dict):
raise TelegraphError("Telegraph sent an answer I don't understand")
return result
async def token(self) -> str:
"""The account's access token: remembered, else loaded, else a new account is made."""
async with self._lock:
if self._token:
return self._token
saved = await self._load()
if saved:
self._token = saved
return saved
result = await self._call("createAccount", short_name=self.short_name, author_name=self.author)
token = str(result.get("access_token") or "")
if not token:
raise TelegraphError("Telegraph did not give an access token")
self._token = token
await self._save(token)
return token
async def _forget_token(self) -> None:
async with self._lock:
self._token = None
await self._save("")
async def create_page(self, title: str, nodes: List[Node]) -> str:
"""Create a page and return its URL."""
content = dump(nodes)
if len(content.encode("utf-8")) > 64 * 1024:
raise TelegraphError("The page is too big for Telegraph", "CONTENT_TOO_BIG")
for attempt in range(2):
token = await self.token()
params = {"access_token": token, "title": (title.strip() or "MediaInfo")[:TITLE_LIMIT],
"author_name": self.author, "content": content, "return_content": "false"}
if self.author_url:
params["author_url"] = self.author_url
try:
result = await self._call("createPage", **params)
except TelegraphError as exc:
if exc.code == "ACCESS_TOKEN_INVALID" and attempt == 0:
log.warning("the saved Telegraph token was rejected; making a new account")
await self._forget_token()
continue
raise
url = str(result.get("url") or "")
if not url:
raise TelegraphError("Telegraph did not return the page address")
return url
raise TelegraphError("Telegraph rejected the account twice") # pragma: no cover - the loop returns or raises