loader.py 54 KB

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