loader.py 45 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012101310141015101610171018101910201021102210231024102510261027102810291030103110321033103410351036103710381039104010411042104310441045104610471048104910501051105210531054105510561057105810591060106110621063106410651066106710681069107010711072107310741075107610771078107910801081108210831084108510861087108810891090109110921093109410951096109710981099110011011102110311041105110611071108110911101111111211131114111511161117111811191120112111221123112411251126112711281129113011311132113311341135113611371138113911401141114211431144114511461147114811491150115111521153115411551156115711581159116011611162116311641165116611671168116911701171117211731174117511761177117811791180118111821183118411851186118711881189119011911192119311941195119611971198119912001201120212031204120512061207120812091210121112121213121412151216121712181219122012211222122312241225122612271228122912301231123212331234123512361237123812391240124112421243124412451246124712481249125012511252125312541255125612571258125912601261126212631264126512661267126812691270127112721273127412751276127712781279128012811282128312841285128612871288128912901291129212931294129512961297129812991300130113021303130413051306130713081309131013111312131313141315131613171318131913201321132213231324132513261327132813291330
  1. """Registers modules"""
  2. # Friendly Telegram (telegram userbot)
  3. # Copyright (C) 2018-2021 The Authors
  4. # This program is free software: you can redistribute it and/or modify
  5. # it under the terms of the GNU Affero General Public License as published by
  6. # the Free Software Foundation, either version 3 of the License, or
  7. # (at your option) any later version.
  8. # This program is distributed in the hope that it will be useful,
  9. # but WITHOUT ANY WARRANTY; without even the implied warranty of
  10. # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
  11. # GNU Affero General Public License for more details.
  12. # You should have received a copy of the GNU Affero General Public License
  13. # along with this program. If not, see <https://www.gnu.org/licenses/>.
  14. # █ █ ▀ █▄▀ ▄▀█ █▀█ ▀
  15. # █▀█ █ █ █ █▀█ █▀▄ █
  16. # © Copyright 2022
  17. # https://t.me/hikariatama
  18. #
  19. # 🔒 Licensed under the GNU AGPLv3
  20. # 🌐 https://www.gnu.org/licenses/agpl-3.0.html
  21. import asyncio
  22. import contextlib
  23. import copy
  24. from functools import partial, wraps
  25. import importlib
  26. import importlib.util
  27. import inspect
  28. import logging
  29. import os
  30. import sys
  31. from importlib.machinery import ModuleSpec
  32. from types import FunctionType
  33. from typing import Any, Awaitable, Hashable, Optional, Union, List
  34. import requests
  35. from telethon import TelegramClient
  36. from telethon.tl.types import Message, InputPeerNotifySettings, Channel
  37. from telethon.tl.functions.account import UpdateNotifySettingsRequest
  38. from telethon.hints import EntityLike
  39. from . import security, utils, validators
  40. from ._types import (
  41. ConfigValue, # skipcq
  42. LoadError, # skipcq
  43. Module,
  44. Library, # skipcq
  45. ModuleConfig, # skipcq
  46. LibraryConfig, # skipcq
  47. SelfUnload,
  48. SelfSuspend,
  49. StopLoop,
  50. InlineMessage,
  51. CoreOverwriteError,
  52. StringLoader,
  53. )
  54. from .inline.core import InlineManager
  55. from .inline.types import InlineCall
  56. from .translations import Strings, Translator
  57. import gc as _gc
  58. import types as _types
  59. logger = logging.getLogger(__name__)
  60. owner = security.owner
  61. sudo = security.sudo
  62. support = security.support
  63. group_owner = security.group_owner
  64. group_admin_add_admins = security.group_admin_add_admins
  65. group_admin_change_info = security.group_admin_change_info
  66. group_admin_ban_users = security.group_admin_ban_users
  67. group_admin_delete_messages = security.group_admin_delete_messages
  68. group_admin_pin_messages = security.group_admin_pin_messages
  69. group_admin_invite_users = security.group_admin_invite_users
  70. group_admin = security.group_admin
  71. group_member = security.group_member
  72. pm = security.pm
  73. unrestricted = security.unrestricted
  74. inline_everyone = security.inline_everyone
  75. def proxy0(data):
  76. def proxy1():
  77. return data
  78. return proxy1
  79. _CELLTYPE = type(proxy0(None).__closure__[0])
  80. def replace_all_refs(replace_from: Any, replace_to: Any) -> Any:
  81. """
  82. :summary: Uses the :mod:`gc` module to replace all references to obj
  83. :attr:`replace_from` with :attr:`replace_to` (it tries it's best,
  84. anyway).
  85. :param replace_from: The obj you want to replace.
  86. :param replace_to: The new objject you want in place of the old one.
  87. :returns: The replace_from
  88. """
  89. # https://github.com/cart0113/pyjack/blob/dd1f9b70b71f48335d72f53ee0264cf70dbf4e28/pyjack.py
  90. _gc.collect()
  91. hit = False
  92. for referrer in _gc.get_referrers(replace_from):
  93. # FRAMES -- PASS THEM UP
  94. if isinstance(referrer, _types.FrameType):
  95. continue
  96. # DICTS
  97. if isinstance(referrer, dict):
  98. cls = None
  99. # THIS CODE HERE IS TO DEAL WITH DICTPROXY TYPES
  100. if "__dict__" in referrer and "__weakref__" in referrer:
  101. for cls in _gc.get_referrers(referrer):
  102. if inspect.isclass(cls) and cls.__dict__ == referrer:
  103. break
  104. for key, value in referrer.items():
  105. # REMEMBER TO REPLACE VALUES ...
  106. if value is replace_from:
  107. hit = True
  108. value = replace_to
  109. referrer[key] = value
  110. if cls: # AGAIN, CLEANUP DICTPROXY PROBLEM
  111. setattr(cls, key, replace_to)
  112. # AND KEYS.
  113. if key is replace_from:
  114. hit = True
  115. del referrer[key]
  116. referrer[replace_to] = value
  117. elif isinstance(referrer, list):
  118. for i, value in enumerate(referrer):
  119. if value is replace_from:
  120. hit = True
  121. referrer[i] = replace_to
  122. elif isinstance(referrer, set):
  123. referrer.remove(replace_from)
  124. referrer.add(replace_to)
  125. hit = True
  126. elif isinstance(
  127. referrer,
  128. (
  129. tuple,
  130. frozenset,
  131. ),
  132. ):
  133. new_tuple = []
  134. for obj in referrer:
  135. if obj is replace_from:
  136. new_tuple.append(replace_to)
  137. else:
  138. new_tuple.append(obj)
  139. replace_all_refs(referrer, type(referrer)(new_tuple))
  140. elif isinstance(referrer, _CELLTYPE):
  141. def _proxy0(data):
  142. def proxy1():
  143. return data
  144. return proxy1
  145. proxy = _proxy0(replace_to)
  146. newcell = proxy.__closure__[0]
  147. replace_all_refs(referrer, newcell)
  148. elif isinstance(referrer, _types.FunctionType):
  149. localsmap = {}
  150. for key in ["code", "globals", "name", "defaults", "closure"]:
  151. orgattr = getattr(referrer, f"__{key}__")
  152. localsmap[key] = replace_to if orgattr is replace_from else orgattr
  153. localsmap["argdefs"] = localsmap["defaults"]
  154. del localsmap["defaults"]
  155. newfn = _types.FunctionType(**localsmap)
  156. replace_all_refs(referrer, newfn)
  157. else:
  158. logging.debug(f"{referrer} is not supported.")
  159. if hit is False:
  160. raise AttributeError(f"Object '{replace_from}' not found")
  161. return replace_from
  162. async def stop_placeholder() -> bool:
  163. return True
  164. class Placeholder:
  165. """Placeholder"""
  166. class InfiniteLoop:
  167. _task = None
  168. status = False
  169. module_instance = None # Will be passed later
  170. def __init__(
  171. self,
  172. func: FunctionType,
  173. interval: int,
  174. autostart: bool,
  175. wait_before: bool,
  176. stop_clause: Union[str, None],
  177. ):
  178. self.func = func
  179. self.interval = interval
  180. self._wait_before = wait_before
  181. self._stop_clause = stop_clause
  182. self.autostart = autostart
  183. def _stop(self, *args, **kwargs):
  184. self._wait_for_stop.set()
  185. def stop(self, *args, **kwargs):
  186. with contextlib.suppress(AttributeError):
  187. _hikka_client_id_logging_tag = copy.copy(
  188. self.module_instance.allmodules.client.tg_id
  189. )
  190. if self._task:
  191. logger.debug(f"Stopped loop for {self.func}")
  192. self._wait_for_stop = asyncio.Event()
  193. self.status = False
  194. self._task.add_done_callback(self._stop)
  195. self._task.cancel()
  196. return asyncio.ensure_future(self._wait_for_stop.wait())
  197. logger.debug("Loop is not running")
  198. return asyncio.ensure_future(stop_placeholder())
  199. def start(self, *args, **kwargs):
  200. with contextlib.suppress(AttributeError):
  201. _hikka_client_id_logging_tag = copy.copy(
  202. self.module_instance.allmodules.client.tg_id
  203. )
  204. if not self._task:
  205. logger.debug(f"Started loop for {self.func}")
  206. self._task = asyncio.ensure_future(self.actual_loop(*args, **kwargs))
  207. else:
  208. logger.debug("Attempted to start already running loop")
  209. async def actual_loop(self, *args, **kwargs):
  210. # Wait for loader to set attribute
  211. while not self.module_instance:
  212. await asyncio.sleep(0.01)
  213. if isinstance(self._stop_clause, str) and self._stop_clause:
  214. self.module_instance.set(self._stop_clause, True)
  215. self.status = True
  216. while self.status:
  217. if self._wait_before:
  218. await asyncio.sleep(self.interval)
  219. if (
  220. isinstance(self._stop_clause, str)
  221. and self._stop_clause
  222. and not self.module_instance.get(self._stop_clause, False)
  223. ):
  224. break
  225. try:
  226. await self.func(self.module_instance, *args, **kwargs)
  227. except StopLoop:
  228. break
  229. except Exception:
  230. logger.exception("Error running loop!")
  231. if not self._wait_before:
  232. await asyncio.sleep(self.interval)
  233. self._wait_for_stop.set()
  234. self.status = False
  235. def __del__(self):
  236. self.stop()
  237. def loop(
  238. interval: int = 5,
  239. autostart: Optional[bool] = False,
  240. wait_before: Optional[bool] = False,
  241. stop_clause: Optional[str] = None,
  242. ) -> FunctionType:
  243. """
  244. Create new infinite loop from class method
  245. :param interval: Loop iterations delay
  246. :param autostart: Start loop once module is loaded
  247. :param wait_before: Insert delay before actual iteration, rather than after
  248. :param stop_clause: Database key, based on which the loop will run.
  249. This key will be set to `True` once loop is started,
  250. and will stop after key resets to `False`
  251. :attr status: Boolean, describing whether the loop is running
  252. """
  253. def wrapped(func):
  254. return InfiniteLoop(func, interval, autostart, wait_before, stop_clause)
  255. return wrapped
  256. MODULES_NAME = "modules"
  257. ru_keys = 'ёйцукенгшщзхъфывапролджэячсмитьбю.Ё"№;%:?ЙЦУКЕНГШЩЗХЪФЫВАПРОЛДЖЭ/ЯЧСМИТЬБЮ,'
  258. en_keys = "`qwertyuiop[]asdfghjkl;'zxcvbnm,./~@#$%^&QWERTYUIOP{}ASDFGHJKL:\"|ZXCVBNM<>?"
  259. BASE_DIR = (
  260. os.path.normpath(os.path.join(utils.get_base_dir(), ".."))
  261. if "OKTETO" not in os.environ and "DOCKER" not in os.environ
  262. else "/data"
  263. )
  264. LOADED_MODULES_DIR = os.path.join(BASE_DIR, "loaded_modules")
  265. if not os.path.isdir(LOADED_MODULES_DIR) and "DYNO" not in os.environ:
  266. os.mkdir(LOADED_MODULES_DIR, mode=0o755)
  267. def translatable_docstring(cls):
  268. """Decorator that makes triple-quote docstrings translatable"""
  269. @wraps(cls.config_complete)
  270. def config_complete(self, *args, **kwargs):
  271. for command_, func_ in get_commands(cls).items():
  272. try:
  273. func_.__doc__ = self.strings[f"_cmd_doc_{command_}"]
  274. except AttributeError:
  275. func_.__func__.__doc__ = self.strings[f"_cmd_doc_{command_}"]
  276. for inline_handler_, func_ in get_inline_handlers(cls).items():
  277. try:
  278. func_.__doc__ = self.strings[f"_ihandle_doc_{inline_handler_}"]
  279. except AttributeError:
  280. func_.__func__.__doc__ = self.strings[f"_ihandle_doc_{inline_handler_}"]
  281. self.__doc__ = self.strings["_cls_doc"]
  282. return self.config_complete._old_(self, *args, **kwargs)
  283. config_complete._old_ = cls.config_complete
  284. cls.config_complete = config_complete
  285. for command, func in get_commands(cls).items():
  286. cls.strings[f"_cmd_doc_{command}"] = inspect.getdoc(func)
  287. for inline_handler, func in get_inline_handlers(cls).items():
  288. cls.strings[f"_ihandle_doc_{inline_handler}"] = inspect.getdoc(func)
  289. cls.strings["_cls_doc"] = inspect.getdoc(cls)
  290. return cls
  291. tds = translatable_docstring # Shorter name for modules to use
  292. def ratelimit(func):
  293. """Decorator that causes ratelimiting for this command to be enforced more strictly"""
  294. func.ratelimit = True
  295. return func
  296. def get_commands(mod):
  297. """Introspect the module to get its commands"""
  298. return {
  299. method_name.rsplit("cmd", maxsplit=1)[0]: getattr(mod, method_name)
  300. for method_name in dir(mod)
  301. if callable(getattr(mod, method_name)) and method_name.endswith("cmd")
  302. }
  303. def get_inline_handlers(mod):
  304. """Introspect the module to get its inline handlers"""
  305. return {
  306. method_name.rsplit("_inline_handler", maxsplit=1)[0]: getattr(mod, method_name)
  307. for method_name in dir(mod)
  308. if callable(getattr(mod, method_name))
  309. and method_name.endswith("_inline_handler")
  310. }
  311. def get_callback_handlers(mod):
  312. """Introspect the module to get its callback handlers"""
  313. return {
  314. method_name.rsplit("_callback_handler", maxsplit=1)[0]: getattr(
  315. mod,
  316. method_name,
  317. )
  318. for method_name in dir(mod)
  319. if callable(getattr(mod, method_name))
  320. and method_name.endswith("_callback_handler")
  321. }
  322. class Modules:
  323. """Stores all registered modules"""
  324. def __init__(
  325. self,
  326. client: TelegramClient,
  327. db: "Database", # type: ignore
  328. allclients: list,
  329. translator: Translator,
  330. ):
  331. self._initial_registration = True
  332. self.commands = {}
  333. self.inline_handlers = {}
  334. self.callback_handlers = {}
  335. self.aliases = {}
  336. self.modules = [] # skipcq: PTC-W0052
  337. self.libraries = []
  338. self.watchers = []
  339. self._log_handlers = []
  340. self._core_commands = []
  341. self.__approve = []
  342. self.allclients = allclients
  343. self.client = client
  344. self._db = db
  345. self._translator = translator
  346. def register_all(self, mods: list = None):
  347. """Load all modules in the module directory"""
  348. external_mods = []
  349. if not mods:
  350. mods = [
  351. os.path.join(utils.get_base_dir(), MODULES_NAME, mod)
  352. for mod in filter(
  353. lambda x: (x.endswith(".py") and not x.startswith("_")),
  354. os.listdir(os.path.join(utils.get_base_dir(), MODULES_NAME)),
  355. )
  356. ]
  357. if "DYNO" not in os.environ and not self._db.get(
  358. __name__, "secure_boot", False
  359. ):
  360. external_mods = [
  361. os.path.join(LOADED_MODULES_DIR, mod)
  362. for mod in filter(
  363. lambda x: (
  364. x.endswith(f"{self.client.tg_id}.py")
  365. and not x.startswith("_")
  366. ),
  367. os.listdir(LOADED_MODULES_DIR),
  368. )
  369. ]
  370. else:
  371. external_mods = []
  372. self._register_modules(mods)
  373. self._register_modules(external_mods, "<file>")
  374. def _register_modules(self, modules: list, origin: str = "<core>"):
  375. with contextlib.suppress(AttributeError):
  376. _hikka_client_id_logging_tag = copy.copy(self.client.tg_id)
  377. for mod in modules:
  378. try:
  379. mod_shortname = (
  380. os.path.basename(mod)
  381. .rsplit(".py", maxsplit=1)[0]
  382. .rsplit("_", maxsplit=1)[0]
  383. )
  384. module_name = f"{__package__}.{MODULES_NAME}.{mod_shortname}"
  385. user_friendly_origin = (
  386. "<core {}>" if origin == "<core>" else "<file {}>"
  387. ).format(mod_shortname)
  388. logger.debug(f"Loading {module_name} from filesystem")
  389. with open(mod, "r") as file:
  390. spec = ModuleSpec(
  391. module_name,
  392. StringLoader(file.read(), user_friendly_origin),
  393. origin=user_friendly_origin,
  394. )
  395. self.register_module(spec, module_name, origin)
  396. except BaseException as e:
  397. logger.exception(f"Failed to load module {mod} due to {e}:")
  398. def register_module(
  399. self,
  400. spec: ModuleSpec,
  401. module_name: str,
  402. origin: str = "<core>",
  403. save_fs: bool = False,
  404. ) -> Module:
  405. """Register single module from importlib spec"""
  406. with contextlib.suppress(AttributeError):
  407. _hikka_client_id_logging_tag = copy.copy(self.client.tg_id)
  408. module = importlib.util.module_from_spec(spec)
  409. sys.modules[module_name] = module
  410. spec.loader.exec_module(module)
  411. ret = None
  412. ret = next(
  413. (
  414. value()
  415. for value in vars(module).values()
  416. if inspect.isclass(value) and issubclass(value, Module)
  417. ),
  418. None,
  419. )
  420. if hasattr(module, "__version__"):
  421. ret.__version__ = module.__version__
  422. if ret is None:
  423. ret = module.register(module_name)
  424. if not isinstance(ret, Module):
  425. raise TypeError(f"Instance is not a Module, it is {type(ret)}")
  426. self.complete_registration(ret)
  427. ret.__origin__ = origin
  428. cls_name = ret.__class__.__name__
  429. if save_fs and "DYNO" not in os.environ:
  430. path = os.path.join(
  431. LOADED_MODULES_DIR,
  432. f"{cls_name}_{self.client.tg_id}.py",
  433. )
  434. if origin == "<string>":
  435. with open(path, "w") as f:
  436. f.write(spec.loader.data.decode("utf-8"))
  437. logger.debug(f"Saved {cls_name=} to {path=}")
  438. return ret
  439. def add_aliases(self, aliases: dict):
  440. """Saves aliases and applies them to <core>/<file> modules"""
  441. self.aliases.update(aliases)
  442. for alias, cmd in aliases.items():
  443. self.add_alias(alias, cmd)
  444. def register_commands(self, instance: Module):
  445. """Register commands from instance"""
  446. with contextlib.suppress(AttributeError):
  447. _hikka_client_id_logging_tag = copy.copy(self.client.tg_id)
  448. if getattr(instance, "__origin__", "") == "<core>":
  449. self._core_commands += list(map(lambda x: x.lower(), instance.commands))
  450. for command in instance.commands.copy():
  451. # Restrict overwriting core modules' commands
  452. if (
  453. command.lower() in self._core_commands
  454. and getattr(instance, "__origin__", "") != "<core>"
  455. ):
  456. with contextlib.suppress(Exception):
  457. self.modules.remove(instance)
  458. raise CoreOverwriteError(command=command)
  459. # Verify that command does not already exist, or,
  460. # if it does, the command must be from the same class name
  461. if command.lower() in self.commands:
  462. if (
  463. hasattr(instance.commands[command], "__self__")
  464. and hasattr(self.commands[command], "__self__")
  465. and instance.commands[command].__self__.__class__.__name__
  466. != self.commands[command].__self__.__class__.__name__
  467. ):
  468. logger.debug(f"Duplicate command {command}")
  469. logger.debug(f"Replacing command for {self.commands[command]}")
  470. if not instance.commands[command].__doc__:
  471. logger.debug(f"Missing docs for {command}")
  472. self.commands.update({command.lower(): instance.commands[command]})
  473. for alias, cmd in self.aliases.items():
  474. if cmd in instance.commands:
  475. self.add_alias(alias, cmd)
  476. for handler in instance.inline_handlers.copy():
  477. if handler.lower() in self.inline_handlers:
  478. if (
  479. hasattr(instance.inline_handlers[handler], "__self__")
  480. and hasattr(self.inline_handlers[handler], "__self__")
  481. and instance.inline_handlers[handler].__self__.__class__.__name__
  482. != self.inline_handlers[handler].__self__.__class__.__name__
  483. ):
  484. logger.debug(f"Duplicate inline_handler {handler}")
  485. logger.debug(
  486. f"Replacing inline_handler for {self.inline_handlers[handler]}"
  487. )
  488. if not instance.inline_handlers[handler].__doc__:
  489. logger.debug(f"Missing docs for {handler}")
  490. self.inline_handlers.update(
  491. {handler.lower(): instance.inline_handlers[handler]}
  492. )
  493. for handler in instance.callback_handlers.copy():
  494. if handler.lower() in self.callback_handlers and (
  495. hasattr(instance.callback_handlers[handler], "__self__")
  496. and hasattr(self.callback_handlers[handler], "__self__")
  497. and instance.callback_handlers[handler].__self__.__class__.__name__
  498. != self.callback_handlers[handler].__self__.__class__.__name__
  499. ):
  500. logger.debug(f"Duplicate callback_handler {handler}")
  501. self.callback_handlers.update(
  502. {handler.lower(): instance.callback_handlers[handler]}
  503. )
  504. def register_watcher(self, instance: Module):
  505. """Register watcher from instance"""
  506. with contextlib.suppress(AttributeError):
  507. _hikka_client_id_logging_tag = copy.copy(self.client.tg_id)
  508. with contextlib.suppress(AttributeError):
  509. if instance.watcher:
  510. for watcher in self.watchers:
  511. if (
  512. hasattr(watcher, "__self__")
  513. and watcher.__self__.__class__.__name__
  514. == instance.watcher.__self__.__class__.__name__
  515. ):
  516. logger.debug(f"Removing watcher for update {watcher}")
  517. self.watchers.remove(watcher)
  518. self.watchers += [instance.watcher]
  519. def _lookup(self, modname: str):
  520. return next(
  521. (lib for lib in self.libraries if lib.name.lower() == modname.lower()),
  522. False,
  523. ) or next(
  524. (
  525. mod
  526. for mod in self.modules
  527. if mod.__class__.__name__.lower() == modname.lower()
  528. or mod.name.lower() == modname.lower()
  529. ),
  530. False,
  531. )
  532. @property
  533. def get_approved_channel(self):
  534. if not self.__approve:
  535. return None
  536. return self.__approve.pop(0)
  537. async def _approve(
  538. self,
  539. call: InlineCall,
  540. channel: EntityLike,
  541. event: asyncio.Event,
  542. ):
  543. local_event = asyncio.Event()
  544. self.__approve += [(channel, local_event)]
  545. await local_event.wait()
  546. event.status = local_event.status
  547. event.set()
  548. await call.edit(
  549. "💫 <b>Joined <a"
  550. f' href="https://t.me/{channel.username}">{utils.escape_html(channel.title)}</a></b>',
  551. gif="https://static.hikari.gay/0d32cbaa959e755ac8eef610f01ba0bd.gif",
  552. )
  553. async def _decline(
  554. self,
  555. call: InlineCall,
  556. channel: EntityLike,
  557. event: asyncio.Event,
  558. ):
  559. self._db.set(
  560. "hikka.main",
  561. "declined_joins",
  562. list(set(self._db.get("hikka.main", "declined_joins", []) + [channel.id])),
  563. )
  564. event.status = False
  565. event.set()
  566. await call.edit(
  567. "✖️ <b>Declined joining <a"
  568. f' href="https://t.me/{channel.username}">{utils.escape_html(channel.title)}</a></b>',
  569. gif="https://static.hikari.gay/0d32cbaa959e755ac8eef610f01ba0bd.gif",
  570. )
  571. async def _request_join(
  572. self,
  573. peer: EntityLike,
  574. reason: str,
  575. assure_joined: Optional[bool] = False,
  576. _module: Module = None,
  577. ) -> bool:
  578. """
  579. Request to join a channel.
  580. :param peer: The channel to join.
  581. :param reason: The reason for joining.
  582. :param assure_joined: If set, module will not be loaded unless the required channel is joined.
  583. ⚠️ Works only in `client_ready`!
  584. ⚠️ If user declines to join channel, he will not be asked to
  585. join again, so unless he joins it manually, module will not be loaded
  586. ever.
  587. :return: Status of the request.
  588. :rtype: bool
  589. :notice: This method will block module loading until the request is approved or declined.
  590. """
  591. event = asyncio.Event()
  592. await self.client(
  593. UpdateNotifySettingsRequest(
  594. peer=self.inline.bot_username,
  595. settings=InputPeerNotifySettings(show_previews=False, silent=False),
  596. )
  597. )
  598. channel = await self.client.get_entity(peer)
  599. if channel.id in self._db.get("hikka.main", "declined_joins", []):
  600. if assure_joined:
  601. raise LoadError(
  602. f"You need to join @{channel.username} in order to use this module"
  603. )
  604. return False
  605. if not isinstance(channel, Channel):
  606. raise TypeError("`peer` field must be a channel")
  607. if getattr(channel, "left", True):
  608. channel = await self.client.force_get_entity(peer)
  609. if not getattr(channel, "left", True):
  610. return True
  611. _module.strings._base_strings["_hikka_internal_request_join"] = (
  612. f"💫 <b>Module </b><code>{_module.__class__.__name__}</code><b> requested to"
  613. " join channel <a"
  614. f" href='https://t.me/{channel.username}'>{utils.escape_html(channel.title)}</a></b>\n\n<b>❓"
  615. f" Reason: </b><i>{utils.escape_html(reason)}</i>"
  616. )
  617. if not hasattr(_module, "strings_ru"):
  618. _module.strings_ru = {}
  619. _module.strings_ru["_hikka_internal_request_join"] = (
  620. f"💫 <b>Модуль </b><code>{_module.__class__.__name__}</code><b> запросил"
  621. " разрешение на вступление в канал <a"
  622. f" href='https://t.me/{channel.username}'>{utils.escape_html(channel.title)}</a></b>\n\n<b>❓"
  623. f" Причина: </b><i>{utils.escape_html(reason)}</i>"
  624. )
  625. await self.inline.bot.send_animation(
  626. self.client.tg_id,
  627. "https://static.hikari.gay/ab3adf144c94a0883bfe489f4eebc520.gif",
  628. caption=_module.strings("_hikka_internal_request_join"),
  629. reply_markup=self.inline.generate_markup(
  630. [
  631. {
  632. "text": "💫 Approve",
  633. "callback": self._approve,
  634. "args": (channel, event),
  635. },
  636. {
  637. "text": "✖️ Decline",
  638. "callback": self._decline,
  639. "args": (channel, event),
  640. },
  641. ]
  642. ),
  643. )
  644. _module.hikka_wait_channel_approve = (
  645. _module.__class__.__name__,
  646. channel,
  647. reason,
  648. )
  649. await event.wait()
  650. with contextlib.suppress(AttributeError):
  651. delattr(_module, "hikka_wait_channel_approve")
  652. if assure_joined and not event.status:
  653. raise LoadError(
  654. f"You need to join @{channel.username} in order to use this module"
  655. )
  656. return event.status
  657. def complete_registration(self, instance: Module):
  658. """Complete registration of instance"""
  659. with contextlib.suppress(AttributeError):
  660. _hikka_client_id_logging_tag = copy.copy(self.client.tg_id)
  661. instance.allclients = self.allclients
  662. instance.allmodules = self
  663. instance.hikka = True
  664. instance.get = partial(self._mod_get, _module=instance)
  665. instance.set = partial(self._mod_set, _modname=instance.__class__.__name__)
  666. instance.get_prefix = partial(self._db.get, "hikka.main", "command_prefix", ".")
  667. instance.client = self.client
  668. instance._client = self.client
  669. instance.db = self._db
  670. instance._db = self._db
  671. instance.lookup = self._lookup
  672. instance.import_lib = self._mod_import_lib
  673. instance.tg_id = self.client.tg_id
  674. instance._tg_id = self.client.tg_id
  675. instance.request_join = partial(self._request_join, _module=instance)
  676. instance.animate = self._animate
  677. for module in self.modules:
  678. if module.__class__.__name__ == instance.__class__.__name__:
  679. if getattr(module, "__origin__", "") == "<core>":
  680. raise CoreOverwriteError(
  681. module=module.__class__.__name__[:-3]
  682. if module.__class__.__name__.endswith("Mod")
  683. else module.__class__.__name__
  684. )
  685. logger.debug(f"Removing module for update {module}")
  686. asyncio.ensure_future(module.on_unload())
  687. self.modules.remove(module)
  688. for method in dir(module):
  689. if isinstance(getattr(module, method), InfiniteLoop):
  690. getattr(module, method).stop()
  691. logger.debug(f"Stopped loop in {module=}, {method=}")
  692. self.modules += [instance]
  693. def _mod_get(
  694. self,
  695. key: str,
  696. default: Optional[Hashable] = None,
  697. _module: Module = None,
  698. ) -> Hashable:
  699. mod, legacy = _module.__class__.__name__, _module.strings["name"]
  700. if self._db.get(legacy, key, Placeholder) is not Placeholder:
  701. for iterkey, value in self._db[legacy].items():
  702. if iterkey == "__config__":
  703. # Config already uses classname as key
  704. # No need to migrate
  705. continue
  706. if isinstance(value, dict) and isinstance(
  707. self._db.get(mod, iterkey), dict
  708. ):
  709. self._db[mod][iterkey].update(value)
  710. else:
  711. self._db.set(mod, iterkey, value)
  712. logger.debug(f"Migrated {legacy} -> {mod}")
  713. del self._db[legacy]
  714. return self._db.get(mod, key, default)
  715. def _mod_set(self, key: str, value: Hashable, _modname: str = None) -> bool:
  716. return self._db.set(_modname, key, value)
  717. def _lib_get(
  718. self,
  719. key: str,
  720. default: Optional[Hashable] = None,
  721. _lib: Library = None,
  722. ) -> Hashable:
  723. return self._db.get(_lib.__class__.__name__, key, default)
  724. def _lib_set(self, key: str, value: Hashable, _lib: Library = None) -> bool:
  725. return self._db.set(_lib.__class__.__name__, key, value)
  726. async def _mod_import_lib(
  727. self,
  728. url: str,
  729. *,
  730. suspend_on_error: Optional[bool] = False,
  731. ) -> object:
  732. """
  733. Import library from url and register it in :obj:`Modules`
  734. :param url: Url to import
  735. :param suspend_on_error: Will raise :obj:`loader.SelfSuspend` if library can't be loaded
  736. :return: :obj:`Library`
  737. :raise: SelfUnload if :attr:`suspend_on_error` is True and error occurred
  738. :raise: HTTPError if library is not found
  739. :raise: ImportError if library doesn't have any class which is a subclass of :obj:`loader.Library`
  740. :raise: ImportError if library name doesn't end with `Lib`
  741. :raise: RuntimeError if library throws in :method:`init`
  742. :raise: RuntimeError if library classname exists in :obj:`Modules`.libraries
  743. """
  744. def _raise(e: Exception):
  745. if suspend_on_error:
  746. raise SelfSuspend("Required library is not available or is corrupted.")
  747. else:
  748. raise e
  749. if not utils.check_url(url):
  750. _raise(ValueError("Invalid url for library"))
  751. code = await utils.run_sync(requests.get, url)
  752. code.raise_for_status()
  753. code = code.text
  754. module = f"hikka.libraries.{url.replace('%', '%%').replace('.', '%d')}"
  755. origin = f"<library {url}>"
  756. spec = ModuleSpec(module, StringLoader(code, origin), origin=origin)
  757. instance = importlib.util.module_from_spec(spec)
  758. sys.modules[module] = instance
  759. spec.loader.exec_module(instance)
  760. lib_obj = next(
  761. (
  762. value()
  763. for value in vars(instance).values()
  764. if inspect.isclass(value) and issubclass(value, Library)
  765. ),
  766. None,
  767. )
  768. if not lib_obj:
  769. _raise(ImportError("Invalid library. No class found"))
  770. if not lib_obj.__class__.__name__.endswith("Lib"):
  771. _raise(ImportError("Invalid library. Class name must end with 'Lib'"))
  772. lib_obj.client = self.client
  773. lib_obj._client = self.client # skipcq
  774. lib_obj.db = self._db
  775. lib_obj._db = self._db # skipcq
  776. lib_obj.name = lib_obj.__class__.__name__
  777. lib_obj.source_url = url.strip("/")
  778. lib_obj.lookup = self._lookup
  779. lib_obj.tg_id = self.client.tg_id
  780. lib_obj.allmodules = self
  781. lib_obj._lib_get = partial(self._lib_get, _lib=lib_obj) # skipcq
  782. lib_obj._lib_set = partial(self._lib_set, _lib=lib_obj) # skipcq
  783. lib_obj.get_prefix = partial(self._db.get, "hikka.main", "command_prefix", ".")
  784. for old_lib in self.libraries:
  785. if old_lib.source_url == lib_obj.source_url and (
  786. not isinstance(getattr(old_lib, "version", None), tuple)
  787. and not isinstance(getattr(lib_obj, "version", None), tuple)
  788. or old_lib.version == lib_obj.version
  789. ):
  790. logging.debug(
  791. f"Using existing instance of library {old_lib.source_url}"
  792. )
  793. return old_lib
  794. new = True
  795. for old_lib in self.libraries:
  796. if old_lib.source_url == lib_obj.source_url:
  797. if hasattr(old_lib, "on_lib_update") and callable(
  798. old_lib.on_lib_update
  799. ):
  800. await old_lib.on_lib_update(lib_obj)
  801. replace_all_refs(old_lib, lib_obj)
  802. new = False
  803. logging.debug(
  804. "Replacing existing instance of library"
  805. f" {lib_obj.source_url} with updated object"
  806. )
  807. if hasattr(lib_obj, "init") and callable(lib_obj.init):
  808. try:
  809. await lib_obj.init()
  810. except Exception:
  811. _raise(RuntimeError("Library init() failed"))
  812. if hasattr(lib_obj, "config"):
  813. if not isinstance(lib_obj.config, LibraryConfig):
  814. _raise(
  815. RuntimeError("Library config must be a `LibraryConfig` instance")
  816. )
  817. libcfg = lib_obj.db.get(
  818. lib_obj.__class__.__name__,
  819. "__config__",
  820. {},
  821. )
  822. for conf in lib_obj.config.keys():
  823. with contextlib.suppress(Exception):
  824. lib_obj.config.set_no_raise(
  825. conf,
  826. (
  827. libcfg[conf]
  828. if conf in libcfg.keys()
  829. else os.environ.get(f"{lib_obj.__class__.__name__}.{conf}")
  830. or lib_obj.config.getdef(conf)
  831. ),
  832. )
  833. if hasattr(lib_obj, "strings"):
  834. lib_obj.strings = Strings(lib_obj, self._translator)
  835. lib_obj.translator = self._translator
  836. if new:
  837. self.libraries += [lib_obj]
  838. return lib_obj
  839. def dispatch(self, command: str) -> tuple:
  840. """Dispatch command to appropriate module"""
  841. change = str.maketrans(ru_keys + en_keys, en_keys + ru_keys)
  842. try:
  843. return command, self.commands[command.lower()]
  844. except KeyError:
  845. try:
  846. cmd = self.aliases[command.lower()]
  847. return cmd, self.commands[cmd.lower()]
  848. except KeyError:
  849. try:
  850. cmd = self.aliases[str.translate(command, change).lower()]
  851. return cmd, self.commands[cmd.lower()]
  852. except KeyError:
  853. try:
  854. cmd = str.translate(command, change).lower()
  855. return cmd, self.commands[cmd.lower()]
  856. except KeyError:
  857. return command, None
  858. def send_config(self, skip_hook: bool = False):
  859. """Configure modules"""
  860. for mod in self.modules:
  861. self.send_config_one(mod, skip_hook)
  862. def send_config_one(self, mod: "Module", skip_hook: bool = False):
  863. """Send config to single instance"""
  864. with contextlib.suppress(AttributeError):
  865. _hikka_client_id_logging_tag = copy.copy(self.client.tg_id)
  866. if hasattr(mod, "config"):
  867. modcfg = self._db.get(
  868. mod.__class__.__name__,
  869. "__config__",
  870. {},
  871. )
  872. try:
  873. for conf in mod.config.keys():
  874. with contextlib.suppress(validators.ValidationError):
  875. mod.config.set_no_raise(
  876. conf,
  877. (
  878. modcfg[conf]
  879. if conf in modcfg.keys()
  880. else os.environ.get(f"{mod.__class__.__name__}.{conf}")
  881. or mod.config.getdef(conf)
  882. ),
  883. )
  884. except AttributeError:
  885. logger.warning(
  886. "Got invalid config instance. Expected `ModuleConfig`, got"
  887. f" {type(mod.config)=}, {mod.config=}"
  888. )
  889. if skip_hook:
  890. return
  891. if not hasattr(mod, "name"):
  892. mod.name = mod.strings["name"]
  893. if hasattr(mod, "strings"):
  894. mod.strings = Strings(mod, self._translator)
  895. mod.translator = self._translator
  896. try:
  897. mod.config_complete()
  898. except Exception as e:
  899. logger.exception(f"Failed to send mod config complete signal due to {e}")
  900. raise
  901. async def send_ready(self):
  902. """Send all data to all modules"""
  903. # Init inline manager anyway, so the modules
  904. # can access its `init_complete`
  905. inline_manager = InlineManager(self.client, self._db, self)
  906. await inline_manager._register_manager()
  907. # We save it to `Modules` attribute, so not to re-init
  908. # it everytime module is loaded. Then we can just
  909. # re-assign it to all modules
  910. self.inline = inline_manager
  911. try:
  912. await asyncio.gather(*[self.send_ready_one(mod) for mod in self.modules])
  913. except Exception as e:
  914. logger.exception(f"Failed to send mod init complete signal due to {e}")
  915. async def _animate(
  916. self,
  917. message: Union[Message, InlineMessage],
  918. frames: List[str],
  919. interval: Union[float, int],
  920. *,
  921. inline: bool = False,
  922. ) -> None:
  923. """
  924. Animate message
  925. :param message: Message to animate
  926. :param frames: A List of strings which are the frames of animation
  927. :param interval: Animation delay
  928. :param inline: Whether to use inline bot for animation
  929. :returns message:
  930. Please, note that if you set `inline=True`, first frame will be shown with an empty
  931. button due to the limitations of Telegram API
  932. """
  933. with contextlib.suppress(AttributeError):
  934. _hikka_client_id_logging_tag = copy.copy(self.client.tg_id)
  935. if interval < 0.1:
  936. logger.warning(
  937. "Resetting animation interval to 0.1s, because it may get you in"
  938. " floodwaits bro"
  939. )
  940. interval = 0.1
  941. for frame in frames:
  942. if isinstance(message, Message):
  943. if inline:
  944. message = await self.inline.form(
  945. message=message,
  946. text=frame,
  947. reply_markup={"text": "\u0020\u2800", "data": "empty"},
  948. )
  949. else:
  950. message = await utils.answer(message, frame)
  951. elif isinstance(message, InlineMessage) and inline:
  952. await message.edit(frame)
  953. await asyncio.sleep(interval)
  954. return message
  955. async def send_ready_one(
  956. self,
  957. mod: Module,
  958. no_self_unload: bool = False,
  959. from_dlmod: bool = False,
  960. ):
  961. with contextlib.suppress(AttributeError):
  962. _hikka_client_id_logging_tag = copy.copy(self.client.tg_id)
  963. mod.inline = self.inline
  964. for method in dir(mod):
  965. if isinstance(getattr(mod, method), InfiniteLoop):
  966. setattr(getattr(mod, method), "module_instance", mod)
  967. if getattr(mod, method).autostart:
  968. getattr(mod, method).start()
  969. logger.debug(f"Added {mod=} to {method=}")
  970. if from_dlmod:
  971. try:
  972. await mod.on_dlmod(self.client, self._db)
  973. except Exception:
  974. logger.info("Can't process `on_dlmod` hook", exc_info=True)
  975. try:
  976. if len(inspect.signature(mod.client_ready).parameters) == 2:
  977. await mod.client_ready(self.client, self._db)
  978. else:
  979. await mod.client_ready()
  980. except SelfUnload as e:
  981. if no_self_unload:
  982. raise e
  983. logger.debug(f"Unloading {mod}, because it raised SelfUnload")
  984. self.modules.remove(mod)
  985. except SelfSuspend as e:
  986. if no_self_unload:
  987. raise e
  988. logger.debug(f"Suspending {mod}, because it raised SelfSuspend")
  989. return
  990. except Exception as e:
  991. logger.exception(
  992. f"Failed to send mod init complete signal for {mod} due to {e},"
  993. " attempting unload"
  994. )
  995. self.modules.remove(mod)
  996. raise
  997. if not hasattr(mod, "commands"):
  998. mod.commands = get_commands(mod)
  999. if not hasattr(mod, "inline_handlers"):
  1000. mod.inline_handlers = get_inline_handlers(mod)
  1001. if not hasattr(mod, "callback_handlers"):
  1002. mod.callback_handlers = get_callback_handlers(mod)
  1003. self.register_commands(mod)
  1004. self.register_watcher(mod)
  1005. def get_classname(self, name: str) -> str:
  1006. return next(
  1007. (
  1008. module.__class__.__module__
  1009. for module in reversed(self.modules)
  1010. if name in (module.name, module.__class__.__module__)
  1011. ),
  1012. name,
  1013. )
  1014. def unload_module(self, classname: str) -> bool:
  1015. """Remove module and all stuff from it"""
  1016. worked = []
  1017. to_remove = []
  1018. with contextlib.suppress(AttributeError):
  1019. _hikka_client_id_logging_tag = copy.copy(self.client.tg_id)
  1020. for module in self.modules:
  1021. if classname.lower() in (
  1022. module.name.lower(),
  1023. module.__class__.__name__.lower(),
  1024. ):
  1025. if getattr(module, "__origin__", "") == "<core>":
  1026. raise RuntimeError("You can't unload core module")
  1027. worked += [module.__class__.__name__]
  1028. name = module.__class__.__name__
  1029. if "DYNO" not in os.environ:
  1030. path = os.path.join(
  1031. LOADED_MODULES_DIR,
  1032. f"{name}_{self.client.tg_id}.py",
  1033. )
  1034. if os.path.isfile(path):
  1035. os.remove(path)
  1036. logger.debug(f"Removed {name} file at {path=}")
  1037. logger.debug(f"Removing module for unload {module}")
  1038. self.modules.remove(module)
  1039. asyncio.ensure_future(module.on_unload())
  1040. for method in dir(module):
  1041. if isinstance(getattr(module, method), InfiniteLoop):
  1042. getattr(module, method).stop()
  1043. logger.debug(f"Stopped loop in {module=}, {method=}")
  1044. to_remove += (
  1045. module.commands
  1046. if isinstance(getattr(module, "commands", None), dict)
  1047. else {}
  1048. ).values()
  1049. if hasattr(module, "watcher"):
  1050. to_remove += [module.watcher]
  1051. logger.debug(f"{to_remove=}, {worked=}")
  1052. for watcher in self.watchers.copy():
  1053. if watcher in to_remove:
  1054. logger.debug(f"Removing {watcher=} for unload")
  1055. self.watchers.remove(watcher)
  1056. aliases_to_remove = []
  1057. for name, command in self.commands.copy().items():
  1058. if command in to_remove:
  1059. logger.debug(f"Removing {command=} for unload")
  1060. del self.commands[name]
  1061. aliases_to_remove.append(name)
  1062. for alias, command in self.aliases.copy().items():
  1063. if command in aliases_to_remove:
  1064. del self.aliases[alias]
  1065. return worked
  1066. def add_alias(self, alias, cmd):
  1067. """Make an alias"""
  1068. if cmd not in self.commands:
  1069. return False
  1070. self.aliases[alias.lower().strip()] = cmd
  1071. return True
  1072. def remove_alias(self, alias):
  1073. """Remove an alias"""
  1074. try:
  1075. del self.aliases[alias.lower().strip()]
  1076. except KeyError:
  1077. return False
  1078. return True
  1079. async def log(
  1080. self,
  1081. type_,
  1082. *,
  1083. group=None,
  1084. affected_uids=None,
  1085. data=None,
  1086. ):
  1087. return await asyncio.gather(
  1088. *[fun(type_, group, affected_uids, data) for fun in self._log_handlers]
  1089. )
  1090. def register_logger(self, _logger):
  1091. self._log_handlers += [_logger]