mirror of
https://github.com/MuRuLOSE/limoka.git
synced 2026-06-16 14:34:17 +02:00
223 lines
7.4 KiB
Python
223 lines
7.4 KiB
Python
# ______ ___ ___ _ _
|
||
# ____ | ___ \ | \/ | | | | |
|
||
# / __ \| |_/ / _| . . | ___ __| |_ _| | ___
|
||
# / / _` | __/ | | | |\/| |/ _ \ / _` | | | | |/ _ \
|
||
# | | (_| | | | |_| | | | | (_) | (_| | |_| | | __/
|
||
# \ \__,_\_| \__, \_| |_/\___/ \__,_|\__,_|_|\___|
|
||
# \____/ __/ |
|
||
# |___/
|
||
|
||
# На модуль распространяется лицензия "GNU General Public License v3.0"
|
||
# https://github.com/all-licenses/GNU-General-Public-License-v3.0
|
||
|
||
# meta developer: @pymodule
|
||
# requires: aiohttp
|
||
|
||
import asyncio
|
||
import hashlib
|
||
import hmac
|
||
import logging
|
||
import re
|
||
import time
|
||
|
||
from .. import loader
|
||
|
||
logger = logging.getLogger(__name__)
|
||
|
||
BLOB_RE = re.compile(r"https://github\.com/([^/]+)/([^/]+)/blob/([^/]+)/(.+)")
|
||
RAW_RE = re.compile(r"https://raw\.githubusercontent\.com/([^/]+)/([^/]+)/([^/]+)/(.+)")
|
||
|
||
API_BASE = "https://37a5bcc11453.vps.myjino.ru"
|
||
BOT_USERNAME = "pyupdater_bot"
|
||
GITHUB_TOKEN = ""
|
||
|
||
TOKEN_DB_KEY = "user_token"
|
||
TOKEN_PREFIX = "pyut_"
|
||
|
||
|
||
def _parse(url: str):
|
||
for pat in (BLOB_RE, RAW_RE):
|
||
m = pat.match(url.strip())
|
||
if m:
|
||
return m.groups()
|
||
return None
|
||
|
||
|
||
def _gh_headers() -> dict:
|
||
h = {"Accept": "application/vnd.github+json", "X-GitHub-Api-Version": "2022-11-28"}
|
||
if GITHUB_TOKEN:
|
||
h["Authorization"] = f"Bearer {GITHUB_TOKEN}"
|
||
return h
|
||
|
||
|
||
def _sign(secret_key: str, api_key: str) -> str:
|
||
bucket = (int(time.time()) // 60) * 60
|
||
msg = f"{api_key}:{bucket}".encode()
|
||
return hmac.new(secret_key.encode(), msg, hashlib.sha256).hexdigest()
|
||
|
||
|
||
class PyUpdaterLib(loader.Library):
|
||
developer = "@pyupdater"
|
||
version = (1, 0, 0)
|
||
|
||
async def init(self):
|
||
await self._refresh_token()
|
||
|
||
async def _refresh_token(self):
|
||
"""Каждый раз при загрузке библиотеки запрашивает свежий токен у бота."""
|
||
try:
|
||
sent = await self.client.send_message(BOT_USERNAME, "/token")
|
||
|
||
response = None
|
||
for _ in range(20):
|
||
await asyncio.sleep(0.5)
|
||
msgs = await self.client.get_messages(BOT_USERNAME, limit=1)
|
||
if (
|
||
msgs
|
||
and msgs[0].id != sent.id
|
||
and msgs[0].text
|
||
and msgs[0].text.startswith(TOKEN_PREFIX)
|
||
):
|
||
response = msgs[0]
|
||
break
|
||
|
||
if response is None:
|
||
logger.warning("PyUpdater: не получили токен от бота")
|
||
return
|
||
|
||
self._lib_set(TOKEN_DB_KEY, response.text.strip())
|
||
logger.info("PyUpdater: токен обновлён")
|
||
|
||
async def _cleanup():
|
||
await asyncio.sleep(3)
|
||
try:
|
||
await sent.delete()
|
||
await response.delete()
|
||
except Exception:
|
||
pass
|
||
|
||
asyncio.create_task(_cleanup())
|
||
|
||
except Exception:
|
||
logger.exception("PyUpdater: ошибка при получении токена")
|
||
|
||
async def check(
|
||
self,
|
||
module: loader.Module,
|
||
github_url: str,
|
||
api_key: str,
|
||
secret_key: str,
|
||
) -> None:
|
||
parts = _parse(github_url)
|
||
if parts is None:
|
||
logger.warning("PyUpdater: cannot parse URL %r", github_url)
|
||
return
|
||
|
||
owner, repo, branch, path = parts
|
||
module_name = module.strings["name"]
|
||
|
||
asyncio.create_task(self._ping(api_key, secret_key, module.tg_id, module_name))
|
||
asyncio.create_task(self._maybe_confirm_test(api_key, module.tg_id))
|
||
|
||
commits_url = (
|
||
f"https://api.github.com/repos/{owner}/{repo}/commits"
|
||
f"?path={path}&sha={branch}&per_page=1"
|
||
)
|
||
raw_url = f"https://raw.githubusercontent.com/{owner}/{repo}/{branch}/{path}"
|
||
db_key = f"pyu_sha_{owner}_{repo}_{path.replace('/', '_')}"
|
||
saved_sha: str = module._db.get(module_name, db_key, "")
|
||
|
||
try:
|
||
import aiohttp
|
||
async with aiohttp.ClientSession() as session:
|
||
async with session.get(
|
||
commits_url,
|
||
headers=_gh_headers(),
|
||
timeout=aiohttp.ClientTimeout(total=10),
|
||
) as resp:
|
||
if resp.status in (403, 429):
|
||
logger.warning("PyUpdater: GitHub rate limit for %s", module_name)
|
||
return
|
||
resp.raise_for_status()
|
||
commits = await resp.json()
|
||
|
||
if not commits:
|
||
return
|
||
|
||
latest_sha: str = commits[0]["sha"]
|
||
if latest_sha == saved_sha:
|
||
return
|
||
|
||
async with session.get(
|
||
raw_url,
|
||
headers=_gh_headers(),
|
||
timeout=aiohttp.ClientTimeout(total=20),
|
||
) as resp:
|
||
resp.raise_for_status()
|
||
new_code = await resp.text()
|
||
|
||
except asyncio.TimeoutError:
|
||
logger.warning("PyUpdater: timeout for %s", module_name)
|
||
return
|
||
except Exception:
|
||
logger.exception("PyUpdater: error checking %s", module_name)
|
||
return
|
||
|
||
module._db.set(module_name, db_key, latest_sha)
|
||
logger.info("PyUpdater: reloading %s (commit %s)", module_name, latest_sha[:7])
|
||
|
||
loader_mod = next(
|
||
(m for m in module.allmodules.modules if m.__class__.__name__ == "LoaderMod"),
|
||
None,
|
||
)
|
||
if loader_mod is None:
|
||
logger.error("PyUpdater: LoaderMod not found")
|
||
return
|
||
|
||
asyncio.create_task(loader_mod.load_module(new_code, None, save_fs=True))
|
||
|
||
async def _maybe_confirm_test(self, api_key: str, user_tg_id: int) -> None:
|
||
try:
|
||
import aiohttp
|
||
async with aiohttp.ClientSession() as session:
|
||
async with session.get(
|
||
f"{API_BASE}/test/pending",
|
||
params={"api_key": api_key},
|
||
timeout=aiohttp.ClientTimeout(total=8),
|
||
) as resp:
|
||
if resp.status != 200:
|
||
return
|
||
data = await resp.json()
|
||
if not data.get("pending"):
|
||
return
|
||
|
||
await session.post(
|
||
f"{API_BASE}/test/confirm",
|
||
json={"api_key": api_key, "user_tg_id": user_tg_id},
|
||
timeout=aiohttp.ClientTimeout(total=8),
|
||
)
|
||
except Exception:
|
||
pass
|
||
|
||
async def _ping(self, api_key: str, secret_key: str, user_tg_id: int, module_name: str) -> None:
|
||
user_token: str = self._lib_get(TOKEN_DB_KEY, "")
|
||
payload = {
|
||
"api_key": api_key,
|
||
"signature": _sign(secret_key, api_key),
|
||
"user_tg_id": user_tg_id,
|
||
"module_name": module_name,
|
||
}
|
||
if user_token:
|
||
payload["user_token"] = user_token
|
||
|
||
try:
|
||
import aiohttp
|
||
async with aiohttp.ClientSession() as session:
|
||
await session.post(
|
||
f"{API_BASE}/ping",
|
||
json=payload,
|
||
timeout=aiohttp.ClientTimeout(total=8),
|
||
)
|
||
except Exception:
|
||
pass
|