loader.py 45 KB

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697989910010110210310410510610710810911011111211311411511611711811912012112212312412512612712812913013113213313413513613713813914014114214314414514614714814915015115215315415515615715815916016116216316416516616716816917017117217317417517617717817918018118218318418518618718818919019119219319419519619719819920020120220320420520620720820921021121221321421521621721821922022122222322422522622722822923023123223323423523623723823924024124224324424524624724824925025125225325425525625725825926026126226326426526626726826927027127227327427527627727827928028128228328428528628728828929029129229329429529629729829930030130230330430530630730830931031131231331431531631731831932032132232332432532632732832933033133233333433533633733833934034134234334434534634734834935035135235335435535635735835936036136236336436536636736836937037137237337437537637737837938038138238338438538638738838939039139239339439539639739839940040140240340440540640740840941041141241341441541641741841942042142242342442542642742842943043143243343443543643743843944044144244344444544644744844945045145245345445545645745845946046146246346446546646746846947047147247347447547647747847948048148248348448548648748848949049149249349449549649749849950050150250350450550650750850951051151251351451551651751851952052152252352452552652752852953053153253353453553653753853954054154254354454554654754854955055155255355455555655755855956056156256356456556656756856957057157257357457557657757857958058158258358458558658758858959059159259359459559659759859960060160260360460560660760860961061161261361461561661761861962062162262362462562662762862963063163263363463563663763863964064164264364464564664764864965065165265365465565665765865966066166266366466566666766866967067167267367467567667767867968068168268368468568668768868969069169269369469569669769869970070170270370470570670770870971071171271371471571671771871972072172272372472572672772872973073173273373473573673773873974074174274374474574674774874975075175275375475575675775875976076176276376476576676776876977077177277377477577677777877978078178278378478578678778878979079179279379479579679779879980080180280380480580680780880981081181281381481581681781881982082182282382482582682782882983083183283383483583683783883984084184284384484584684784884985085185285385485585685785885986086186286386486586686786886987087187287387487587687787887988088188288388488588688788888989089189289389489589689789889990090190290390490590690790890991091191291391491591691791891992092192292392492592692792892993093193293393493593693793893994094194294394494594694794894995095195295395495595695795895996096196296396496596696796896997097197297397497597697797897998098198298398498598698798898999099199299399499599699799899910001001100210031004100510061007100810091010101110121013101410151016101710181019102010211022102310241025102610271028102910301031103210331034103510361037103810391040104110421043104410451046104710481049105010511052105310541055105610571058105910601061106210631064106510661067106810691070107110721073107410751076107710781079108010811082108310841085108610871088108910901091109210931094109510961097109810991100110111021103110411051106110711081109111011111112111311141115111611171118111911201121112211231124112511261127112811291130113111321133113411351136113711381139114011411142114311441145114611471148114911501151115211531154115511561157115811591160116111621163116411651166116711681169117011711172117311741175117611771178117911801181118211831184118511861187118811891190119111921193119411951196119711981199120012011202120312041205120612071208120912101211121212131214121512161217121812191220122112221223122412251226122712281229123012311232123312341235123612371238123912401241124212431244124512461247124812491250125112521253125412551256125712581259126012611262
  1. """Loads and registers modules"""
  2. # ©️ Dan Gazizullin, 2021-2023
  3. # This file is a part of Hikka Userbot
  4. # 🌐 https://github.com/hikariatama/Hikka
  5. # You can redistribute it and/or modify it under the terms of the GNU AGPLv3
  6. # 🔑 https://www.gnu.org/licenses/agpl-3.0.html
  7. import ast
  8. import asyncio
  9. import contextlib
  10. import copy
  11. import functools
  12. import importlib
  13. import inspect
  14. import logging
  15. import os
  16. import re
  17. import shutil
  18. import sys
  19. import time
  20. import typing
  21. import uuid
  22. from collections import ChainMap
  23. from importlib.machinery import ModuleSpec
  24. from urllib.parse import urlparse
  25. import requests
  26. from hikkatl.errors.rpcerrorlist import MediaCaptionTooLongError
  27. from hikkatl.tl.functions.channels import JoinChannelRequest
  28. from hikkatl.tl.types import Channel, Message
  29. from .. import loader, main, utils
  30. from .._local_storage import RemoteStorage
  31. from ..compat import dragon, geek
  32. from ..compat.pyroproxy import PyroProxyClient
  33. from ..inline.types import InlineCall
  34. from ..types import CoreOverwriteError, CoreUnloadError, DragonModule
  35. logger = logging.getLogger(__name__)
  36. class FakeLock:
  37. async def __aenter__(self, *args):
  38. pass
  39. async def __aexit__(self, *args):
  40. pass
  41. class FakeNotifier:
  42. def __enter__(self):
  43. pass
  44. def __exit__(self, *args):
  45. pass
  46. @loader.tds
  47. class LoaderMod(loader.Module):
  48. """Loads modules"""
  49. strings = {"name": "Loader"}
  50. def __init__(self):
  51. self.fully_loaded = False
  52. self._links_cache = {}
  53. self._storage: RemoteStorage = None
  54. self.config = loader.ModuleConfig(
  55. loader.ConfigValue(
  56. "MODULES_REPO",
  57. "https://mods.hikariatama.ru",
  58. lambda: self.strings("repo_config_doc"),
  59. validator=loader.validators.Link(),
  60. ),
  61. loader.ConfigValue(
  62. "ADDITIONAL_REPOS",
  63. # Currenly the trusted developers are specified
  64. [
  65. "https://github.com/hikariatama/host/raw/master",
  66. "https://github.com/MoriSummerz/ftg-mods/raw/main",
  67. "https://gitlab.com/CakesTwix/friendly-userbot-modules/-/raw/master",
  68. ],
  69. lambda: self.strings("add_repo_config_doc"),
  70. validator=loader.validators.Series(validator=loader.validators.Link()),
  71. ),
  72. loader.ConfigValue(
  73. "share_link",
  74. doc=lambda: self.strings("share_link_doc"),
  75. validator=loader.validators.Boolean(),
  76. ),
  77. loader.ConfigValue(
  78. "basic_auth",
  79. None,
  80. lambda: self.strings("basic_auth_doc"),
  81. validator=loader.validators.Hidden(
  82. loader.validators.RegExp(r"^.*:.*$")
  83. ),
  84. ),
  85. )
  86. async def _async_init(self):
  87. modules = list(
  88. filter(
  89. lambda x: not x.startswith("https://mods.hikariatama.ru"),
  90. utils.array_sum(
  91. map(
  92. lambda x: list(x.values()),
  93. (await self.get_repo_list()).values(),
  94. )
  95. ),
  96. )
  97. )
  98. logger.debug("Modules: %s", modules)
  99. asyncio.ensure_future(self._storage.preload(modules))
  100. asyncio.ensure_future(self._storage.preload_main_repo())
  101. async def client_ready(self):
  102. while not (settings := self.lookup("settings")):
  103. await asyncio.sleep(0.5)
  104. self._storage = RemoteStorage(self._client)
  105. self.allmodules.add_aliases(settings.get("aliases", {}))
  106. main.hikka.ready.set()
  107. asyncio.ensure_future(self._update_modules())
  108. asyncio.ensure_future(self._async_init())
  109. @loader.loop(interval=3, wait_before=True, autostart=True)
  110. async def _config_autosaver(self):
  111. for mod in self.allmodules.modules:
  112. if (
  113. not hasattr(mod, "config")
  114. or not mod.config
  115. or not isinstance(mod.config, loader.ModuleConfig)
  116. ):
  117. continue
  118. for option, config in mod.config._config.items():
  119. if not hasattr(config, "_save_marker"):
  120. continue
  121. delattr(mod.config._config[option], "_save_marker")
  122. mod.pointer("__config__", {})[option] = config.value
  123. for lib in self.allmodules.libraries:
  124. if (
  125. not hasattr(lib, "config")
  126. or not lib.config
  127. or not isinstance(lib.config, loader.ModuleConfig)
  128. ):
  129. continue
  130. for option, config in lib.config._config.items():
  131. if not hasattr(config, "_save_marker"):
  132. continue
  133. delattr(lib.config._config[option], "_save_marker")
  134. lib._lib_pointer("__config__", {})[option] = config.value
  135. self._db.save()
  136. def update_modules_in_db(self):
  137. if self.allmodules.secure_boot:
  138. return
  139. self.set(
  140. "loaded_modules",
  141. {
  142. **{
  143. module.__class__.__name__: module.__origin__
  144. for module in self.allmodules.modules
  145. if module.__origin__.startswith("http")
  146. },
  147. **{
  148. module.name: module.url
  149. for module in self.allmodules.dragon_modules
  150. if module.url
  151. },
  152. },
  153. )
  154. @loader.command(alias="dlm")
  155. async def dlmod(self, message: Message):
  156. if args := utils.get_args(message):
  157. args = args[0]
  158. await self.download_and_install(args, message)
  159. if self.fully_loaded:
  160. self.update_modules_in_db()
  161. else:
  162. await self.inline.list(
  163. message,
  164. [
  165. self.strings("avail_header")
  166. + f"\n☁️ {repo.strip('/')}\n\n"
  167. + "\n".join(
  168. [
  169. " | ".join(chunk)
  170. for chunk in utils.chunks(
  171. [
  172. f"<code>{i}</code>"
  173. for i in sorted(
  174. [
  175. utils.escape_html(
  176. i.split("/")[-1].split(".")[0]
  177. )
  178. for i in mods.values()
  179. ]
  180. )
  181. ],
  182. 5,
  183. )
  184. ]
  185. )
  186. for repo, mods in (await self.get_repo_list()).items()
  187. ],
  188. )
  189. async def _get_modules_to_load(self):
  190. todo = self.get("loaded_modules", {})
  191. logger.debug("Loading modules: %s", todo)
  192. return todo
  193. async def _get_repo(self, repo: str) -> str:
  194. repo = repo.strip("/")
  195. if self._links_cache.get(repo, {}).get("exp", 0) >= time.time():
  196. return self._links_cache[repo]["data"]
  197. res = await utils.run_sync(
  198. requests.get,
  199. f"{repo}/full.txt",
  200. auth=(
  201. tuple(self.config["basic_auth"].split(":", 1))
  202. if self.config["basic_auth"]
  203. else None
  204. ),
  205. )
  206. if not str(res.status_code).startswith("2"):
  207. logger.debug(
  208. "Can't load repo %s contents because of %s status code",
  209. repo,
  210. res.status_code,
  211. )
  212. return []
  213. self._links_cache[repo] = {
  214. "exp": time.time() + 5 * 60,
  215. "data": [link for link in res.text.strip().splitlines() if link],
  216. }
  217. return self._links_cache[repo]["data"]
  218. async def get_repo_list(
  219. self,
  220. only_primary: bool = False,
  221. ) -> dict:
  222. return {
  223. repo: {
  224. f"Mod/{repo_id}/{i}": f'{repo.strip("/")}/{link}.py'
  225. for i, link in enumerate(set(await self._get_repo(repo)))
  226. }
  227. for repo_id, repo in enumerate(
  228. [self.config["MODULES_REPO"]]
  229. + ([] if only_primary else self.config["ADDITIONAL_REPOS"])
  230. )
  231. if repo.startswith("http")
  232. }
  233. async def get_links_list(self) -> typing.List[str]:
  234. links = await self.get_repo_list()
  235. main_repo = list(links.pop(self.config["MODULES_REPO"]).values())
  236. return main_repo + list(dict(ChainMap(*list(links.values()))).values())
  237. async def _find_link(self, module_name: str) -> typing.Union[str, bool]:
  238. return next(
  239. filter(
  240. lambda link: link.lower().endswith(f"/{module_name.lower()}.py"),
  241. await self.get_links_list(),
  242. ),
  243. False,
  244. )
  245. async def download_and_install(
  246. self,
  247. module_name: str,
  248. message: typing.Optional[Message] = None,
  249. ):
  250. try:
  251. blob_link = False
  252. module_name = module_name.strip()
  253. if urlparse(module_name).netloc:
  254. url = module_name
  255. if re.match(
  256. r"^(https:\/\/github\.com\/.*?\/.*?\/blob\/.*\.py)|"
  257. r"(https:\/\/gitlab\.com\/.*?\/.*?\/-\/blob\/.*\.py)$",
  258. url,
  259. ):
  260. url = url.replace("/blob/", "/raw/")
  261. blob_link = True
  262. else:
  263. url = await self._find_link(module_name)
  264. if not url:
  265. if message is not None:
  266. await utils.answer(message, self.strings("no_module"))
  267. return False
  268. if message:
  269. message = await utils.answer(
  270. message,
  271. self.strings("installing").format(module_name),
  272. )
  273. try:
  274. r = await self._storage.fetch(url, auth=self.config["basic_auth"])
  275. except requests.exceptions.HTTPError:
  276. if message is not None:
  277. await utils.answer(message, self.strings("no_module"))
  278. return False
  279. return await self.load_module(
  280. r,
  281. message,
  282. module_name,
  283. url,
  284. blob_link=blob_link,
  285. )
  286. except Exception:
  287. logger.exception("Failed to load %s", module_name)
  288. async def _inline__load(
  289. self,
  290. call: InlineCall,
  291. doc: str,
  292. path_: str,
  293. mode: str,
  294. ):
  295. save = False
  296. if mode == "all_yes":
  297. self._db.set(main.__name__, "permanent_modules_fs", True)
  298. self._db.set(main.__name__, "disable_modules_fs", False)
  299. await call.answer(self.strings("will_save_fs"))
  300. save = True
  301. elif mode == "all_no":
  302. self._db.set(main.__name__, "disable_modules_fs", True)
  303. self._db.set(main.__name__, "permanent_modules_fs", False)
  304. elif mode == "once":
  305. save = True
  306. await self.load_module(doc, call, origin=path_ or "<string>", save_fs=save)
  307. @loader.command(alias="lm")
  308. async def loadmod(self, message: Message):
  309. args = utils.get_args_raw(message)
  310. if "-fs" in args:
  311. force_save = True
  312. args = args.replace("-fs", "").strip()
  313. else:
  314. force_save = False
  315. msg = message if message.file else (await message.get_reply_message())
  316. if msg is None or msg.media is None:
  317. await utils.answer(message, self.strings("provide_module"))
  318. return
  319. path_ = None
  320. doc = await msg.download_media(bytes)
  321. logger.debug("Loading external module...")
  322. try:
  323. doc = doc.decode()
  324. except UnicodeDecodeError:
  325. await utils.answer(message, self.strings("bad_unicode"))
  326. return
  327. if (
  328. not self._db.get(
  329. main.__name__,
  330. "disable_modules_fs",
  331. False,
  332. )
  333. and not self._db.get(main.__name__, "permanent_modules_fs", False)
  334. and not force_save
  335. ):
  336. if message.file:
  337. await message.edit("")
  338. message = await message.respond("🌘", reply_to=utils.get_topic(message))
  339. if await self.inline.form(
  340. self.strings("module_fs"),
  341. message=message,
  342. reply_markup=[
  343. [
  344. {
  345. "text": self.strings("save"),
  346. "callback": self._inline__load,
  347. "args": (doc, path_, "once"),
  348. },
  349. {
  350. "text": self.strings("no_save"),
  351. "callback": self._inline__load,
  352. "args": (doc, path_, "no"),
  353. },
  354. ],
  355. [
  356. {
  357. "text": self.strings("save_for_all"),
  358. "callback": self._inline__load,
  359. "args": (doc, path_, "all_yes"),
  360. }
  361. ],
  362. [
  363. {
  364. "text": self.strings("never_save"),
  365. "callback": self._inline__load,
  366. "args": (doc, path_, "all_no"),
  367. }
  368. ],
  369. ],
  370. ):
  371. return
  372. if path_ is not None:
  373. await self.load_module(
  374. doc,
  375. message,
  376. origin=path_,
  377. save_fs=(
  378. force_save
  379. or self._db.get(main.__name__, "permanent_modules_fs", False)
  380. and not self._db.get(main.__name__, "disable_modules_fs", False)
  381. ),
  382. )
  383. else:
  384. await self.load_module(
  385. doc,
  386. message,
  387. save_fs=(
  388. force_save
  389. or self._db.get(main.__name__, "permanent_modules_fs", False)
  390. and not self._db.get(main.__name__, "disable_modules_fs", False)
  391. ),
  392. )
  393. async def load_module(
  394. self,
  395. doc: str,
  396. message: Message,
  397. name: typing.Optional[str] = None,
  398. origin: str = "<string>",
  399. did_requirements: bool = False,
  400. save_fs: bool = False,
  401. blob_link: bool = False,
  402. ):
  403. if any(
  404. line.replace(" ", "") == "#scope:ffmpeg" for line in doc.splitlines()
  405. ) and os.system("ffmpeg -version 1>/dev/null 2>/dev/null"):
  406. if isinstance(message, Message):
  407. await utils.answer(message, self.strings("ffmpeg_required"))
  408. return
  409. if (
  410. any(line.replace(" ", "") == "#scope:inline" for line in doc.splitlines())
  411. and not self.inline.init_complete
  412. ):
  413. if isinstance(message, Message):
  414. await utils.answer(message, self.strings("inline_init_failed"))
  415. return
  416. if re.search(r"# ?scope: ?hikka_min", doc):
  417. ver = re.search(r"# ?scope: ?hikka_min ((?:\d+\.){2}\d+)", doc).group(1)
  418. ver_ = tuple(map(int, ver.split(".")))
  419. if main.__version__ < ver_:
  420. if isinstance(message, Message):
  421. if getattr(message, "file", None):
  422. m = utils.get_chat_id(message)
  423. await message.edit("")
  424. else:
  425. m = message
  426. await self.inline.form(
  427. self.strings("version_incompatible").format(ver),
  428. m,
  429. reply_markup=[
  430. {
  431. "text": self.lookup("updater").strings("btn_update"),
  432. "callback": self.lookup("updater").inline_update,
  433. },
  434. {
  435. "text": self.lookup("updater").strings("cancel"),
  436. "action": "close",
  437. },
  438. ],
  439. )
  440. return
  441. developer = re.search(r"# ?meta developer: ?(.+)", doc)
  442. developer = developer.group(1) if developer else False
  443. blob_link = self.strings("blob_link") if blob_link else ""
  444. if utils.check_url(name):
  445. url = copy.deepcopy(name)
  446. elif utils.check_url(origin):
  447. url = copy.deepcopy(origin)
  448. else:
  449. url = None
  450. if name is None:
  451. try:
  452. node = ast.parse(doc)
  453. uid = next(
  454. n.name
  455. for n in node.body
  456. if isinstance(n, ast.ClassDef)
  457. and any(
  458. isinstance(base, ast.Attribute)
  459. and base.value.id == "Module"
  460. or isinstance(base, ast.Name)
  461. and base.id == "Module"
  462. for base in n.bases
  463. )
  464. )
  465. except Exception:
  466. logger.debug(
  467. "Can't parse classname from code, using legacy uid instead",
  468. exc_info=True,
  469. )
  470. uid = "__extmod_" + str(uuid.uuid4())
  471. else:
  472. if name.startswith(self.config["MODULES_REPO"]):
  473. name = name.split("/")[-1].split(".py")[0]
  474. uid = name.replace("%", "%%").replace(".", "%d")
  475. is_dragon = "@Client.on_message" in doc
  476. if is_dragon:
  477. module_name = f"dragon.modules.{uid}"
  478. if not self._client.pyro_proxy:
  479. self._client.pyro_proxy = PyroProxyClient(self._client)
  480. await self._client.pyro_proxy.start()
  481. await self._client.pyro_proxy.dispatcher.start()
  482. dragon.apply_compat(self._client)
  483. else:
  484. module_name = f"hikka.modules.{uid}"
  485. doc = geek.compat(doc)
  486. async def core_overwrite(e: CoreOverwriteError):
  487. nonlocal message
  488. with contextlib.suppress(Exception):
  489. self.allmodules.modules.remove(instance)
  490. if not message:
  491. return
  492. await utils.answer(
  493. message,
  494. self.strings(f"overwrite_{e.type}").format(
  495. *(
  496. (e.target,)
  497. if e.type == "module"
  498. else (utils.escape_html(self.get_prefix()), e.target)
  499. )
  500. ),
  501. )
  502. async with (dragon.import_lock if is_dragon else lambda _: FakeLock())(
  503. self._client
  504. ):
  505. with (
  506. self._client.dragon_compat.misc.modules_help.get_notifier
  507. if is_dragon
  508. else FakeNotifier
  509. )() as notifier:
  510. try:
  511. try:
  512. spec = ModuleSpec(
  513. module_name,
  514. loader.StringLoader(doc, f"<external {module_name}>"),
  515. origin=f"<external {module_name}>",
  516. )
  517. instance = await self.allmodules.register_module(
  518. spec,
  519. module_name,
  520. origin,
  521. save_fs=save_fs,
  522. is_dragon=is_dragon,
  523. )
  524. if is_dragon:
  525. dragon_module, instance = instance
  526. instance.url = url
  527. except ImportError as e:
  528. logger.info(
  529. (
  530. "Module loading failed, attemping dependency"
  531. " installation (%s)"
  532. ),
  533. e.name,
  534. )
  535. # Let's try to reinstall dependencies
  536. try:
  537. requirements = list(
  538. filter(
  539. lambda x: not x.startswith(("-", "_", ".")),
  540. map(
  541. str.strip,
  542. loader.VALID_PIP_PACKAGES.search(doc)[
  543. 1
  544. ].split(),
  545. ),
  546. )
  547. )
  548. except TypeError:
  549. logger.warning(
  550. "No valid pip packages specified in code, attemping"
  551. " installation from error"
  552. )
  553. requirements = [
  554. {
  555. "sklearn": "scikit-learn",
  556. "pil": "Pillow",
  557. "hikkatl": "Hikka-TL",
  558. "pyrogram": "Hikka-Pyro",
  559. }.get(e.name.lower(), e.name)
  560. ]
  561. if not requirements:
  562. raise Exception("Nothing to install") from e
  563. logger.debug("Installing requirements: %s", requirements)
  564. if did_requirements:
  565. if message is not None:
  566. await utils.answer(
  567. message,
  568. self.strings("requirements_restart").format(e.name),
  569. )
  570. return
  571. if message is not None:
  572. await utils.answer(
  573. message,
  574. self.strings("requirements_installing").format(
  575. "\n".join(
  576. "<emoji"
  577. " document_id=4971987363145188045>▫️</emoji>"
  578. f" {req}"
  579. for req in requirements
  580. )
  581. ),
  582. )
  583. pip = await asyncio.create_subprocess_exec(
  584. sys.executable,
  585. "-m",
  586. "pip",
  587. "install",
  588. "--upgrade",
  589. "-q",
  590. "--disable-pip-version-check",
  591. "--no-warn-script-location",
  592. *["--user"] if loader.USER_INSTALL else [],
  593. *requirements,
  594. )
  595. rc = await pip.wait()
  596. if rc != 0:
  597. if message is not None:
  598. if "com.termux" in os.environ.get("PREFIX", ""):
  599. await utils.answer(
  600. message,
  601. self.strings("requirements_failed_termux"),
  602. )
  603. else:
  604. await utils.answer(
  605. message,
  606. self.strings("requirements_failed"),
  607. )
  608. return
  609. importlib.invalidate_caches()
  610. kwargs = utils.get_kwargs()
  611. kwargs["did_requirements"] = True
  612. return await self.load_module(**kwargs) # Try again
  613. except CoreOverwriteError as e:
  614. await core_overwrite(e)
  615. return
  616. except loader.LoadError as e:
  617. with contextlib.suppress(Exception):
  618. await self.allmodules.unload_module(
  619. instance.__class__.__name__
  620. )
  621. with contextlib.suppress(Exception):
  622. self.allmodules.modules.remove(instance)
  623. if message:
  624. await utils.answer(
  625. message,
  626. (
  627. "<emoji document_id=5454225457916420314>😖</emoji>"
  628. f" <b>{utils.escape_html(str(e))}</b>"
  629. ),
  630. )
  631. return
  632. except Exception as e:
  633. logger.exception("Loading external module failed due to %s", e)
  634. if message is not None:
  635. await utils.answer(message, self.strings("load_failed"))
  636. return
  637. if hasattr(instance, "__version__") and isinstance(
  638. instance.__version__, tuple
  639. ):
  640. version = (
  641. "<b><i>"
  642. f" (v{'.'.join(list(map(str, list(instance.__version__))))})</i></b>"
  643. )
  644. else:
  645. version = ""
  646. try:
  647. try:
  648. self.allmodules.send_config_one(instance)
  649. async def inner_proxy():
  650. nonlocal instance, message
  651. while True:
  652. if hasattr(instance, "hikka_wait_channel_approve"):
  653. if message:
  654. (
  655. module,
  656. channel,
  657. reason,
  658. ) = instance.hikka_wait_channel_approve
  659. message = await utils.answer(
  660. message,
  661. self.strings("wait_channel_approve").format(
  662. module,
  663. channel.username,
  664. utils.escape_html(channel.title),
  665. utils.escape_html(reason),
  666. self.inline.bot_username,
  667. ),
  668. )
  669. return
  670. await asyncio.sleep(0.1)
  671. task = asyncio.ensure_future(inner_proxy())
  672. await self.allmodules.send_ready_one(
  673. instance,
  674. no_self_unload=True,
  675. from_dlmod=bool(message),
  676. )
  677. task.cancel()
  678. except CoreOverwriteError as e:
  679. await core_overwrite(e)
  680. return
  681. except loader.LoadError as e:
  682. with contextlib.suppress(Exception):
  683. await self.allmodules.unload_module(
  684. instance.__class__.__name__
  685. )
  686. with contextlib.suppress(Exception):
  687. self.allmodules.modules.remove(instance)
  688. if message:
  689. await utils.answer(
  690. message,
  691. (
  692. "<emoji document_id=5454225457916420314>😖</emoji>"
  693. f" <b>{utils.escape_html(str(e))}</b>"
  694. ),
  695. )
  696. return
  697. except loader.SelfUnload as e:
  698. logger.debug(
  699. "Unloading %s, because it raised `SelfUnload`", instance
  700. )
  701. with contextlib.suppress(Exception):
  702. await self.allmodules.unload_module(
  703. instance.__class__.__name__
  704. )
  705. with contextlib.suppress(Exception):
  706. self.allmodules.modules.remove(instance)
  707. if message:
  708. await utils.answer(
  709. message,
  710. (
  711. "<emoji document_id=5454225457916420314>😖</emoji>"
  712. f" <b>{utils.escape_html(str(e))}</b>"
  713. ),
  714. )
  715. return
  716. except loader.SelfSuspend as e:
  717. logger.debug(
  718. "Suspending %s, because it raised `SelfSuspend`", instance
  719. )
  720. if message:
  721. await utils.answer(
  722. message,
  723. (
  724. "🥶 <b>Module suspended itself\nReason:"
  725. f" {utils.escape_html(str(e))}</b>"
  726. ),
  727. )
  728. return
  729. except Exception as e:
  730. logger.exception("Module threw because of %s", e)
  731. if message is not None:
  732. await utils.answer(message, self.strings("load_failed"))
  733. return
  734. instance.hikka_meta_pic = next(
  735. (
  736. line.replace(" ", "").split("#metapic:", maxsplit=1)[1]
  737. for line in doc.splitlines()
  738. if line.replace(" ", "").startswith("#metapic:")
  739. ),
  740. None,
  741. )
  742. pack_url = next(
  743. (
  744. line.replace(" ", "").split("#packurl:", maxsplit=1)[1]
  745. for line in doc.splitlines()
  746. if line.replace(" ", "").startswith("#packurl:")
  747. ),
  748. None,
  749. )
  750. if pack_url and (
  751. transations := await self.allmodules.translator.load_module_translations(
  752. pack_url
  753. )
  754. ):
  755. instance.strings.external_strings = transations
  756. if is_dragon:
  757. instance.name = (
  758. f"Dragon{notifier.modname[0].upper()}{notifier.modname[1:]}"
  759. )
  760. instance.commands = notifier.commands
  761. self.allmodules.register_dragon(dragon_module, instance)
  762. else:
  763. for alias, cmd in (
  764. self.lookup("settings").get("aliases", {}).items()
  765. ):
  766. if cmd in instance.commands:
  767. self.allmodules.add_alias(alias, cmd)
  768. try:
  769. modname = instance.strings("name")
  770. except (KeyError, AttributeError):
  771. modname = getattr(instance, "name", instance.__class__.__name__)
  772. try:
  773. developer_entity = await (
  774. self._client.force_get_entity
  775. if (
  776. developer in self._client.hikka_entity_cache
  777. and getattr(
  778. await self._client.get_entity(developer),
  779. "left",
  780. True,
  781. )
  782. )
  783. else self._client.get_entity
  784. )(developer)
  785. except Exception:
  786. developer_entity = None
  787. if not isinstance(developer_entity, Channel):
  788. developer_entity = None
  789. if message is None:
  790. return
  791. modhelp = ""
  792. if instance.__doc__:
  793. modhelp += (
  794. "<i>\n<emoji document_id=5787544344906959608>ℹ️</emoji>"
  795. f" {utils.escape_html(inspect.getdoc(instance))}</i>\n"
  796. )
  797. subscribe = ""
  798. subscribe_markup = None
  799. depends_from = []
  800. for key in dir(instance):
  801. value = getattr(instance, key)
  802. if isinstance(value, loader.Library):
  803. depends_from.append(
  804. "<emoji document_id=4971987363145188045>▫️</emoji>"
  805. " <code>{}</code> <b>{}</b> <code>{}</code>".format(
  806. value.__class__.__name__,
  807. self.strings("by"),
  808. (
  809. value.developer
  810. if isinstance(getattr(value, "developer", None), str)
  811. else "Unknown"
  812. ),
  813. )
  814. )
  815. depends_from = (
  816. self.strings("depends_from").format("\n".join(depends_from))
  817. if depends_from
  818. else ""
  819. )
  820. def loaded_msg(use_subscribe: bool = True):
  821. nonlocal modname, version, modhelp, developer, origin, subscribe, blob_link, depends_from
  822. return self.strings("loaded").format(
  823. modname.strip(),
  824. version,
  825. utils.ascii_face(),
  826. modhelp,
  827. developer if not subscribe or not use_subscribe else "",
  828. depends_from,
  829. (
  830. self.strings("modlink").format(origin)
  831. if origin != "<string>" and self.config["share_link"]
  832. else ""
  833. ),
  834. blob_link,
  835. subscribe if use_subscribe else "",
  836. )
  837. if developer:
  838. if developer.startswith("@") and developer not in self.get(
  839. "do_not_subscribe", []
  840. ):
  841. if (
  842. developer_entity
  843. and getattr(developer_entity, "left", True)
  844. and self._db.get(main.__name__, "suggest_subscribe", True)
  845. ):
  846. subscribe = self.strings("suggest_subscribe").format(
  847. f"@{utils.escape_html(developer_entity.username)}"
  848. )
  849. subscribe_markup = [
  850. {
  851. "text": self.strings("subscribe"),
  852. "callback": self._inline__subscribe,
  853. "args": (
  854. developer_entity.id,
  855. functools.partial(loaded_msg, use_subscribe=False),
  856. True,
  857. ),
  858. },
  859. {
  860. "text": self.strings("no_subscribe"),
  861. "callback": self._inline__subscribe,
  862. "args": (
  863. developer,
  864. functools.partial(loaded_msg, use_subscribe=False),
  865. False,
  866. ),
  867. },
  868. ]
  869. developer = self.strings("developer").format(
  870. utils.escape_html(developer)
  871. if isinstance(developer_entity, Channel)
  872. else f"<code>{utils.escape_html(developer)}</code>"
  873. )
  874. else:
  875. developer = ""
  876. if any(
  877. line.replace(" ", "") == "#scope:disable_onload_docs"
  878. for line in doc.splitlines()
  879. ):
  880. await utils.answer(message, loaded_msg(), reply_markup=subscribe_markup)
  881. return
  882. for _name, fun in sorted(
  883. instance.commands.items(),
  884. key=lambda x: x[0],
  885. ):
  886. modhelp += "\n{} <code>{}{}</code> {}".format(
  887. (
  888. dragon.DRAGON_EMOJI
  889. if is_dragon
  890. else "<emoji document_id=4971987363145188045>▫️</emoji>"
  891. ),
  892. utils.escape_html(self.get_prefix("dragon" if is_dragon else None)),
  893. _name,
  894. (
  895. utils.escape_html(fun)
  896. if is_dragon
  897. else (
  898. utils.escape_html(inspect.getdoc(fun))
  899. if fun.__doc__
  900. else self.strings("undoc")
  901. )
  902. ),
  903. )
  904. if self.inline.init_complete and not is_dragon:
  905. for _name, fun in sorted(
  906. instance.inline_handlers.items(),
  907. key=lambda x: x[0],
  908. ):
  909. modhelp += self.strings("ihandler").format(
  910. f"@{self.inline.bot_username} {_name}",
  911. (
  912. utils.escape_html(inspect.getdoc(fun))
  913. if fun.__doc__
  914. else self.strings("undoc")
  915. ),
  916. )
  917. try:
  918. await utils.answer(message, loaded_msg(), reply_markup=subscribe_markup)
  919. except MediaCaptionTooLongError:
  920. await message.reply(loaded_msg(False))
  921. async def _inline__subscribe(
  922. self,
  923. call: InlineCall,
  924. entity: int,
  925. msg: typing.Callable[[], str],
  926. subscribe: bool,
  927. ):
  928. if not subscribe:
  929. self.set("do_not_subscribe", self.get("do_not_subscribe", []) + [entity])
  930. await utils.answer(call, msg())
  931. await call.answer(self.strings("not_subscribed"))
  932. return
  933. await self._client(JoinChannelRequest(entity))
  934. await utils.answer(call, msg())
  935. await call.answer(self.strings("subscribed"))
  936. @loader.command(alias="ulm")
  937. async def unloadmod(self, message: Message):
  938. if not (args := utils.get_args_raw(message)):
  939. await utils.answer(message, self.strings("no_class"))
  940. return
  941. instance = self.lookup(args, include_dragon=True)
  942. if issubclass(instance.__class__, loader.Library):
  943. await utils.answer(message, self.strings("cannot_unload_lib"))
  944. return
  945. is_dragon = isinstance(instance, DragonModule)
  946. if is_dragon:
  947. worked = [instance.name] if self.allmodules.unload_dragon(instance) else []
  948. else:
  949. try:
  950. worked = await self.allmodules.unload_module(args)
  951. except CoreUnloadError as e:
  952. await utils.answer(
  953. message,
  954. self.strings("unload_core").format(e.module),
  955. )
  956. return
  957. if not self.allmodules.secure_boot:
  958. self.set(
  959. "loaded_modules",
  960. {
  961. mod: link
  962. for mod, link in self.get("loaded_modules", {}).items()
  963. if mod not in worked
  964. },
  965. )
  966. msg = (
  967. self.strings("unloaded").format(
  968. (
  969. dragon.DRAGON_EMOJI
  970. if is_dragon
  971. else "<emoji document_id=5784993237412351403>✅</emoji>"
  972. ),
  973. ", ".join(
  974. [(mod[:-3] if mod.endswith("Mod") else mod) for mod in worked]
  975. ),
  976. )
  977. if worked
  978. else self.strings("not_unloaded")
  979. )
  980. await utils.answer(message, msg)
  981. @loader.command()
  982. async def clearmodules(self, message: Message):
  983. await self.inline.form(
  984. self.strings("confirm_clearmodules"),
  985. message,
  986. reply_markup=[
  987. {
  988. "text": self.strings("clearmodules"),
  989. "callback": self._inline__clearmodules,
  990. },
  991. {
  992. "text": self.strings("cancel"),
  993. "action": "close",
  994. },
  995. ],
  996. )
  997. @loader.command()
  998. async def addrepo(self, message: Message):
  999. if not (args := utils.get_args_raw(message)) or (
  1000. not utils.check_url(args) and not utils.check_url(f"https://{args}")
  1001. ):
  1002. await utils.answer(message, self.strings("no_repo"))
  1003. return
  1004. if args.endswith("/"):
  1005. args = args[:-1]
  1006. if not args.startswith("https://") and not args.startswith("http://"):
  1007. args = f"https://{args}"
  1008. try:
  1009. r = await utils.run_sync(
  1010. requests.get,
  1011. f"{args}/full.txt",
  1012. auth=(
  1013. tuple(self.config["basic_auth"].split(":", 1))
  1014. if self.config["basic_auth"]
  1015. else None
  1016. ),
  1017. )
  1018. r.raise_for_status()
  1019. if not r.text.strip():
  1020. raise ValueError
  1021. except Exception:
  1022. await utils.answer(message, self.strings("no_repo"))
  1023. return
  1024. if args in self.config["ADDITIONAL_REPOS"]:
  1025. await utils.answer(message, self.strings("repo_exists").format(args))
  1026. return
  1027. self.config["ADDITIONAL_REPOS"] += [args]
  1028. await utils.answer(message, self.strings("repo_added").format(args))
  1029. @loader.command()
  1030. async def delrepo(self, message: Message):
  1031. if not (args := utils.get_args_raw(message)) or not utils.check_url(args):
  1032. await utils.answer(message, self.strings("no_repo"))
  1033. return
  1034. if args.endswith("/"):
  1035. args = args[:-1]
  1036. if args not in self.config["ADDITIONAL_REPOS"]:
  1037. await utils.answer(message, self.strings("repo_not_exists"))
  1038. return
  1039. self.config["ADDITIONAL_REPOS"].remove(args)
  1040. await utils.answer(message, self.strings("repo_deleted").format(args))
  1041. async def _inline__clearmodules(self, call: InlineCall):
  1042. self.set("loaded_modules", {})
  1043. for file in os.scandir(loader.LOADED_MODULES_DIR):
  1044. try:
  1045. shutil.rmtree(file.path)
  1046. except Exception:
  1047. logger.debug("Failed to remove %s", file.path, exc_info=True)
  1048. await utils.answer(call, self.strings("all_modules_deleted"))
  1049. await self.lookup("Updater").restart_common(call)
  1050. async def _update_modules(self):
  1051. todo = await self._get_modules_to_load()
  1052. self._secure_boot = False
  1053. if self._db.get(loader.__name__, "secure_boot", False):
  1054. self._db.set(loader.__name__, "secure_boot", False)
  1055. self._secure_boot = True
  1056. else:
  1057. for mod in todo.values():
  1058. await self.download_and_install(mod)
  1059. self.update_modules_in_db()
  1060. aliases = {
  1061. alias: cmd
  1062. for alias, cmd in self.lookup("settings").get("aliases", {}).items()
  1063. if self.allmodules.add_alias(alias, cmd)
  1064. }
  1065. self.lookup("settings").set("aliases", aliases)
  1066. self.fully_loaded = True
  1067. with contextlib.suppress(AttributeError):
  1068. await self.lookup("Updater").full_restart_complete(self._secure_boot)
  1069. def flush_cache(self) -> int:
  1070. """Flush the cache of links to modules"""
  1071. count = sum(map(len, self._links_cache.values()))
  1072. self._links_cache = {}
  1073. return count
  1074. def inspect_cache(self) -> int:
  1075. """Inspect the cache of links to modules"""
  1076. return sum(map(len, self._links_cache.values()))
  1077. async def reload_core(self) -> int:
  1078. """Forcefully reload all core modules"""
  1079. self.fully_loaded = False
  1080. if self._secure_boot:
  1081. self._db.set(loader.__name__, "secure_boot", True)
  1082. if not self._db.get(main.__name__, "remove_core_protection", False):
  1083. for module in self.allmodules.modules:
  1084. if module.__origin__.startswith("<core"):
  1085. module.__origin__ = "<reload-core>"
  1086. loaded = await self.allmodules.register_all(no_external=True)
  1087. for instance in loaded:
  1088. self.allmodules.send_config_one(instance)
  1089. await self.allmodules.send_ready_one(
  1090. instance,
  1091. no_self_unload=False,
  1092. from_dlmod=False,
  1093. )
  1094. self.fully_loaded = True
  1095. return len(loaded)