loader.py 52 KB

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