diff --git a/hikka/modules/loader.py b/hikka/modules/loader.py index e0c5251..cf4f515 100755 --- a/hikka/modules/loader.py +++ b/hikka/modules/loader.py @@ -29,6 +29,7 @@ # scope: inline import asyncio +import contextlib import importlib import inspect import logging @@ -36,6 +37,7 @@ import os import re import ast import sys +import time import uuid from collections import ChainMap from importlib.machinery import ModuleSpec @@ -52,66 +54,15 @@ from ..inline.types import InlineCall logger = logging.getLogger(__name__) -VALID_URL = r"[-[\]_.~:/?#@!$&'()*+,;%<=>a-zA-Z0-9]+" - VALID_PIP_PACKAGES = re.compile( - r"^\s*# ?requires:(?: ?)((?:{url} )*(?:{url}))\s*$".format(url=VALID_URL), + r"^\s*# ?requires:(?: ?)((?:{url} )*(?:{url}))\s*$".format( + url=r"[-[\]_.~:/?#@!$&'()*+,;%<=>a-zA-Z0-9]+" + ), re.MULTILINE, ) USER_INSTALL = "PIP_TARGET" not in os.environ and "VIRTUAL_ENV" not in os.environ -GIT_REGEX = re.compile( - r"^https?://github\.com((?:/[a-z0-9-]+){2})(?:/tree/([a-z0-9-]+)((?:/[a-z0-9-]+)*))?/?$", - flags=re.IGNORECASE, -) - - -def unescape_percent(text): - i = 0 - ln = len(text) - is_handling_percent = False - out = "" - - while i < ln: - char = text[i] - - if char == "%" and not is_handling_percent: - is_handling_percent = True - i += 1 - continue - - if char == "d" and is_handling_percent: - out += "." - is_handling_percent = False - i += 1 - continue - - out += char - is_handling_percent = False - i += 1 - - return out - - -def get_git_api(url): - m = GIT_REGEX.search(url) - - if m is None: - return None - - branch = m.group(2) - path_ = m.group(3) - api_url = f"https://api.github.com/repos{m.group(1)}/contents" - - if path_ is not None and len(path_) > 0: - api_url += path_ - - if branch: - api_url += f"?ref={branch}" - - return api_url - @loader.tds class LoaderMod(loader.Module): @@ -129,7 +80,7 @@ class LoaderMod(loader.Module): "provide_module": "⚠️ Provide a module to load", "bad_unicode": "🚫 Invalid Unicode formatting in module", "load_failed": "🚫 Loading failed. See logs for details", - "loaded": "🔭 Module {}{} loaded {}{}{}{}", + "loaded": "🔭 Module {}{} loaded {}{}{}{}\n\n{}", "no_class": "What class needs to be unloaded?", "unloaded": "🧹 Module unloaded.", "not_unloaded": "🚫 Module not unloaded.", @@ -159,6 +110,7 @@ class LoaderMod(loader.Module): "add_repo_config_doc": "Additional repos to load from", "share_link_doc": "Share module link in result message of .dlmod", "modlink": "\n🌍 Link: {}", + "blob_link": "🚸 Do not use `blob` links to download modules. Consider switching to `raw` instead", } strings_ru = { @@ -173,7 +125,7 @@ class LoaderMod(loader.Module): "provide_module": "⚠️ Укажи модуль для загрузки", "bad_unicode": "🚫 Неверная кодировка модуля", "load_failed": "🚫 Загрузка не увенчалась успехом. Смотри логи.", - "loaded": "🔭 Модуль {}{} загружен {}{}{}{}", + "loaded": "🔭 Модуль {}{} загружен {}{}{}{}\n\n{}", "no_class": "А что выгружать то?", "unloaded": "🧹 Модуль выгружен.", "not_unloaded": "🚫 Модуль не выгружен.", @@ -203,13 +155,14 @@ class LoaderMod(loader.Module): "_cls_doc": "Загружает модули", "share_link_doc": "Указывать ссылку на модуль после загрузки через .dlmod", "modlink": "\n🌍 Ссылка: {}", + "blob_link": "🚸 Не используй `blob` ссылки для загрузки модулей. Лучше загружать из `raw`", } def __init__(self): self.config = loader.ModuleConfig( loader.ConfigValue( "MODULES_REPO", - "https://mods.hikariatama.ru/", + "https://mods.hikariatama.ru", lambda: self.strings("repo_config_doc"), validator=loader.validators.Link(), ), @@ -217,17 +170,17 @@ class LoaderMod(loader.Module): "ADDITIONAL_REPOS", # Currenly the trusted developers are specified [ - "https://github.com/hikariatama/host/raw/master/", - "https://github.com/MoriSummerz/ftg-mods/raw/main/", - "https://gitlab.com/CakesTwix/friendly-userbot-modules/-/raw/master/", + "https://github.com/hikariatama/host/raw/master", + "https://github.com/MoriSummerz/ftg-mods/raw/main", + "https://github.com/iamnalinor/FTG-modules/raw/main", + "https://gitlab.com/CakesTwix/friendly-userbot-modules/-/raw/master", ], lambda: self.strings("add_repo_config_doc"), validator=loader.validators.Series(validator=loader.validators.Link()), ), loader.ConfigValue( "share_link", - False, - lambda: self.strings("share_link_doc"), + doc=lambda: self.strings("share_link_doc"), validator=loader.validators.Boolean(), ), ) @@ -283,22 +236,12 @@ class LoaderMod(loader.Module): async def dlpresetcmd(self, message: Message) -> None: """Set modules preset""" args = utils.get_args(message) - if not args: await utils.answer(message, self.strings("select_preset")) return - try: - await self.get_repo_list(args[0]) - except requests.exceptions.HTTPError as e: - if e.response.status_code == 404: - await utils.answer(message, self.strings("no_preset")) - return - - raise - + await self.get_repo_list(args[0]) self.set("chosen_preset", args[0]) - self.set("loaded_modules", {}) await utils.answer(message, self.strings("preset_loaded")) await self.allmodules.commands["restart"]( @@ -306,7 +249,7 @@ class LoaderMod(loader.Module): ) async def _get_modules_to_load(self): - preset = self.get("chosen_preset", None) + preset = self.get("chosen_preset") if preset != "disable": possible_mods = (await self.get_repo_list(preset)).values() @@ -318,15 +261,27 @@ class LoaderMod(loader.Module): return todo async def _get_repo(self, repo: str, preset: str) -> str: + repo = repo.strip("/") + preset_id = f"{repo}/{preset}" + + if self._links_cache.get(preset_id, {}).get("exp", 0) >= time.time(): + return self._links_cache[preset_id]["data"] + res = await utils.run_sync( requests.get, - f'{repo.strip("/")}/{preset}.txt', + f"{repo}/{preset}.txt", ) + if not str(res.status_code).startswith("2"): logger.debug(f"Can't load {repo=}, {preset=}, {res.status_code=}") - return "" + return [] - return res.text + self._links_cache[preset_id] = { + "exp": time.time() + 5 * 60, + "data": [link for link in res.text.strip().splitlines() if link], + } + + return self._links_cache[preset_id]["data"] async def get_repo_list(self, preset: Optional[str] = None): if preset is None or preset == "none": @@ -334,15 +289,8 @@ class LoaderMod(loader.Module): return { repo: { - f"Preset_mod_{repo_id}_{i}": f'{repo.strip("/")}/{link}.py' - for i, link in enumerate( - set( - filter( - lambda x: x, - (await self._get_repo(repo, preset)).split("\n"), - ) - ) - ) + f"Mod/{repo_id}/{i}": f'{repo.strip("/")}/{link}.py' + for i, link in enumerate(set(await self._get_repo(repo, preset))) } for repo_id, repo in enumerate( [self.config["MODULES_REPO"]] + self.config["ADDITIONAL_REPOS"] @@ -360,24 +308,38 @@ class LoaderMod(loader.Module): del links[self.config["MODULES_REPO"]] return main_repo + converter(links) + async def _find_link(self, module_name: str) -> Union[str, bool]: + links = await self.get_links_list() + return next( + ( + link + for link in links + if link.lower().endswith(f"/{module_name.lower()}.py") + ), + False, + ) + async def download_and_install( self, module_name: str, message: Optional[Message] = None, ): try: + blob_link = False + module_name = module_name.strip() if urlparse(module_name).netloc: url = module_name + if re.match( + r"^(https:\/\/github\.com\/.*?\/.*?\/blob\/.*\.py)|" + r"(https:\/\/gitlab\.com\/.*?\/.*?\/-\/blob\/.*\.py)$", + url, + ): + url = url.replace("/blob/", "/raw/") + blob_link = True else: - links = await self.get_links_list() + url = await self._find_link(module_name) - try: - url = next( - link - for link in links - if link.lower().endswith(f"/{module_name.lower()}.py") - ) - except Exception: + if not url: if message is not None: await utils.answer(message, self.strings("no_module")) @@ -398,6 +360,7 @@ class LoaderMod(loader.Module): message, module_name, url, + blob_link=blob_link, ) except Exception: logger.exception(f"Failed to load {module_name}") @@ -421,10 +384,7 @@ class LoaderMod(loader.Module): elif mode == "once": save = True - if path_ is not None: - await self.load_module(doc, call, origin=path_, save_fs=save) - else: - await self.load_module(doc, call, save_fs=save) + await self.load_module(doc, call, origin=path_ or "", save_fs=save) @loader.owner async def loadmodcmd(self, message: Message) -> None: @@ -526,6 +486,7 @@ class LoaderMod(loader.Module): origin: Optional[str] = "", did_requirements: Optional[bool] = False, save_fs: Optional[bool] = False, + blob_link: Optional[bool] = False, ) -> None: if any( line.replace(" ", "") == "#scope:ffmpeg" for line in doc.splitlines() @@ -543,10 +504,7 @@ class LoaderMod(loader.Module): return if re.search(r"# ?scope: ?hikka_min", doc): - ver = re.search( - r"# ?scope: ?hikka_min ([0-9]+\.[0-9]+\.[0-9]+)", - doc, - ).group(1) + ver = re.search(r"# ?scope: ?hikka_min ((\d+\.){2}\d+)", doc).group(1) ver_ = tuple(map(int, ver.split("."))) if main.__version__ < ver_: if isinstance(message, Message): @@ -580,6 +538,8 @@ class LoaderMod(loader.Module): else "" ) + blob_link = self.strings("blob_link") if blob_link else "" + if name is None: try: node = ast.parse(doc) @@ -625,7 +585,7 @@ class LoaderMod(loader.Module): lambda x: not x.startswith(("-", "_", ".")), map( str.strip, - VALID_PIP_PACKAGES.search(doc)[1].split(" "), + VALID_PIP_PACKAGES.search(doc)[1].split(), ), ) ) @@ -681,19 +641,10 @@ class LoaderMod(loader.Module): importlib.invalidate_caches() - return await self.load_module( - doc, - message, - name, - origin, - True, - save_fs, - ) # Try again + return await self.load_module(utils.get_kwargs()) # Try again except loader.LoadError as e: - try: + with contextlib.suppress(ValueError): self.allmodules.modules.remove(instance) # skipcq: PYL-E0601 - except ValueError: - pass if message: await utils.answer(message, f"🚫 {utils.escape_html(str(e))}") @@ -725,20 +676,16 @@ class LoaderMod(loader.Module): from_dlmod=bool(message), ) except loader.LoadError as e: - try: + with contextlib.suppress(ValueError): self.allmodules.modules.remove(instance) - except ValueError: - pass if message: await utils.answer(message, f"🚫 {utils.escape_html(str(e))}") return except loader.SelfUnload as e: logging.debug(f"Unloading {instance}, because it raised `SelfUnload`") - try: + with contextlib.suppress(ValueError): self.allmodules.modules.remove(instance) - except ValueError: - pass if message: await utils.answer(message, f"🚫 {utils.escape_html(str(e))}") @@ -768,23 +715,23 @@ class LoaderMod(loader.Module): if instance.__doc__: modhelp += f"\nℹ️ {utils.escape_html(inspect.getdoc(instance))}\n" + loaded_msg = lambda: self.strings("loaded").format( + modname.strip(), + version, + utils.ascii_face(), + modhelp, + developer, + self.strings("modlink").format(origin) + if origin != "" and self.config["share_link"] + else "", + blob_link, + ) + if any( line.replace(" ", "") == "#scope:disable_onload_docs" for line in doc.splitlines() ): - await utils.answer( - message, - self.strings("loaded").format( - modname.strip(), - version, - utils.ascii_face(), - modhelp, - developer, - self.strings("modlink").format(origin) - if origin != "" and self.config["share_link"] - else "", - ), - ) + await utils.answer(message, loaded_msg()) return for _name, fun in sorted( @@ -817,32 +764,9 @@ class LoaderMod(loader.Module): ) try: - await utils.answer( - message, - self.strings("loaded").format( - modname.strip(), - version, - utils.ascii_face(), - modhelp, - developer, - self.strings("modlink").format(origin) - if origin != "" and self.config["share_link"] - else "", - ), - ) + await utils.answer(message, loaded_msg()) except telethon.errors.rpcerrorlist.MediaCaptionTooLongError: - await message.reply( - self.strings("loaded").format( - modname.strip(), - version, - utils.ascii_face(), - modhelp, - developer, - self.strings("modlink").format(origin) - if origin != "" and self.config["share_link"] - else "", - ) - ) + await message.reply(loaded_msg()) @loader.owner async def unloadmodcmd(self, message: Message) -> None: @@ -913,6 +837,8 @@ class LoaderMod(loader.Module): self._client = client self._fully_loaded = False + self._links_cache = {} + self.allmodules.add_aliases(self.lookup("settings").get("aliases", {})) main.hikka.ready.set() @@ -932,6 +858,7 @@ class LoaderMod(loader.Module): ) asyncio.ensure_future(self._update_modules()) + asyncio.ensure_future(self.get_repo_list("full")) @loader.loop(interval=3, wait_before=True, autostart=True) async def _modules_config_autosaver(self): @@ -944,7 +871,7 @@ class LoaderMod(loader.Module): continue delattr(mod.config._config[option], "_save_marker") - self._db.setdefault(mod.__class__.__name__, {},).setdefault( + self._db.setdefault(mod.__class__.__name__, {}).setdefault( "__config__", {} )[option] = config.value self._db.save() diff --git a/web-resources/root.jinja2 b/web-resources/root.jinja2 index 23cc682..ea3d432 100755 --- a/web-resources/root.jinja2 +++ b/web-resources/root.jinja2 @@ -85,6 +85,7 @@
+
Hikka is installed
{% if heroku %} diff --git a/web-resources/static/base.css b/web-resources/static/base.css index f7ed30e..41b8b4c 100644 --- a/web-resources/static/base.css +++ b/web-resources/static/base.css @@ -162,6 +162,18 @@ body { top: 0; height: 50vh; } + + .title { + height: auto; + line-height: auto; + } + + .finish_block, .auth { + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + } } @keyframes floating { @@ -248,3 +260,8 @@ label { box-sizing: border-box; position: absolute; } + +#installation_icon { + height: 200px; + margin-bottom: 35px; +} diff --git a/web-resources/static/root.js b/web-resources/static/root.js index 1f1d70e..1fdd71d 100644 --- a/web-resources/static/root.js +++ b/web-resources/static/root.js @@ -1,16 +1,16 @@ function auth(callback) { + bodymovin.loadAnimation({ + container: document.getElementById("tg_icon"), + renderer: 'canvas', + loop: true, + autoplay: true, + path: 'https://raw.githubusercontent.com/hikariatama/Hikka/master/assets/noface.json', + rendererSettings: { + clearCanvas: true, + } + }); $(".main").fadeOut(500, () => { $(".auth").hide().fadeIn(500); - bodymovin.loadAnimation({ - container: document.getElementById("tg_icon"), - renderer: 'canvas', - loop: true, - autoplay: true, - path: 'https://raw.githubusercontent.com/hikariatama/Hikka/master/assets/noface.json', - rendererSettings: { - clearCanvas: true, - } - }); fetch("/web_auth", { method: "POST", credentials: "include", @@ -80,6 +80,16 @@ function finish_login() { .then(() => { window.expanse = true; $(".installation").fadeOut(2000, () => { + bodymovin.loadAnimation({ + container: document.getElementById("installation_icon"), + renderer: 'canvas', + loop: true, + autoplay: true, + path: 'https://assets1.lottiefiles.com/animated_stickers/lf_tgs_j7miwfxd.json', + rendererSettings: { + clearCanvas: true, + } + }); $(".finish_block").fadeIn(300); }); })