types.py 36 KB

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697989910010110210310410510610710810911011111211311411511611711811912012112212312412512612712812913013113213313413513613713813914014114214314414514614714814915015115215315415515615715815916016116216316416516616716816917017117217317417517617717817918018118218318418518618718818919019119219319419519619719819920020120220320420520620720820921021121221321421521621721821922022122222322422522622722822923023123223323423523623723823924024124224324424524624724824925025125225325425525625725825926026126226326426526626726826927027127227327427527627727827928028128228328428528628728828929029129229329429529629729829930030130230330430530630730830931031131231331431531631731831932032132232332432532632732832933033133233333433533633733833934034134234334434534634734834935035135235335435535635735835936036136236336436536636736836937037137237337437537637737837938038138238338438538638738838939039139239339439539639739839940040140240340440540640740840941041141241341441541641741841942042142242342442542642742842943043143243343443543643743843944044144244344444544644744844945045145245345445545645745845946046146246346446546646746846947047147247347447547647747847948048148248348448548648748848949049149249349449549649749849950050150250350450550650750850951051151251351451551651751851952052152252352452552652752852953053153253353453553653753853954054154254354454554654754854955055155255355455555655755855956056156256356456556656756856957057157257357457557657757857958058158258358458558658758858959059159259359459559659759859960060160260360460560660760860961061161261361461561661761861962062162262362462562662762862963063163263363463563663763863964064164264364464564664764864965065165265365465565665765865966066166266366466566666766866967067167267367467567667767867968068168268368468568668768868969069169269369469569669769869970070170270370470570670770870971071171271371471571671771871972072172272372472572672772872973073173273373473573673773873974074174274374474574674774874975075175275375475575675775875976076176276376476576676776876977077177277377477577677777877978078178278378478578678778878979079179279379479579679779879980080180280380480580680780880981081181281381481581681781881982082182282382482582682782882983083183283383483583683783883984084184284384484584684784884985085185285385485585685785885986086186286386486586686786886987087187287387487587687787887988088188288388488588688788888989089189289389489589689789889990090190290390490590690790890991091191291391491591691791891992092192292392492592692792892993093193293393493593693793893994094194294394494594694794894995095195295395495595695795895996096196296396496596696796896997097197297397497597697797897998098198298398498598698798898999099199299399499599699799899910001001100210031004100510061007100810091010101110121013101410151016101710181019102010211022102310241025102610271028102910301031103210331034103510361037103810391040104110421043104410451046104710481049105010511052105310541055105610571058105910601061106210631064106510661067106810691070107110721073107410751076107710781079108010811082108310841085108610871088108910901091109210931094109510961097109810991100110111021103110411051106110711081109111011111112111311141115111611171118111911201121112211231124112511261127112811291130113111321133113411351136113711381139114011411142114311441145114611471148114911501151115211531154115511561157115811591160116111621163116411651166116711681169
  1. # ©️ Dan Gazizullin, 2021-2023
  2. # This file is a part of Hikka Userbot
  3. # 🌐 https://github.com/hikariatama/Hikka
  4. # You can redistribute it and/or modify it under the terms of the GNU AGPLv3
  5. # 🔑 https://www.gnu.org/licenses/agpl-3.0.html
  6. import ast
  7. import asyncio
  8. import contextlib
  9. import copy
  10. import importlib
  11. import importlib.machinery
  12. import importlib.util
  13. import inspect
  14. import logging
  15. import os
  16. import re
  17. import sys
  18. import time
  19. import typing
  20. from dataclasses import dataclass, field
  21. from importlib.abc import SourceLoader
  22. import requests
  23. from hikkatl.hints import EntityLike
  24. from hikkatl.tl.functions.account import UpdateNotifySettingsRequest
  25. from hikkatl.tl.types import (
  26. Channel,
  27. ChannelFull,
  28. InputPeerNotifySettings,
  29. Message,
  30. UserFull,
  31. )
  32. from . import version
  33. from ._reference_finder import replace_all_refs
  34. from .inline.types import (
  35. BotInlineCall,
  36. BotInlineMessage,
  37. BotMessage,
  38. InlineCall,
  39. InlineMessage,
  40. InlineQuery,
  41. InlineUnit,
  42. )
  43. from .pointers import PointerDict, PointerList
  44. __all__ = [
  45. "JSONSerializable",
  46. "HikkaReplyMarkup",
  47. "ListLike",
  48. "Command",
  49. "StringLoader",
  50. "Module",
  51. "get_commands",
  52. "get_inline_handlers",
  53. "get_callback_handlers",
  54. "BotInlineCall",
  55. "BotMessage",
  56. "InlineCall",
  57. "InlineMessage",
  58. "InlineQuery",
  59. "InlineUnit",
  60. "BotInlineMessage",
  61. "PointerDict",
  62. "PointerList",
  63. ]
  64. logger = logging.getLogger(__name__)
  65. JSONSerializable = typing.Union[str, int, float, bool, list, dict, None]
  66. HikkaReplyMarkup = typing.Union[typing.List[typing.List[dict]], typing.List[dict], dict]
  67. ListLike = typing.Union[list, set, tuple]
  68. Command = typing.Callable[..., typing.Awaitable[typing.Any]]
  69. class StringLoader(SourceLoader):
  70. """Load a python module/file from a string"""
  71. def __init__(self, data: str, origin: str):
  72. self.data = data.encode("utf-8") if isinstance(data, str) else data
  73. self.origin = origin
  74. def get_source(self, _=None) -> str:
  75. return self.data.decode("utf-8")
  76. def get_code(self, fullname: str) -> bytes:
  77. return (
  78. compile(source, self.origin, "exec", dont_inherit=True)
  79. if (source := self.get_data(fullname))
  80. else None
  81. )
  82. def get_filename(self, *args, **kwargs) -> str:
  83. return self.origin
  84. def get_data(self, *args, **kwargs) -> bytes:
  85. return self.data
  86. class Module:
  87. strings = {"name": "Unknown"}
  88. """There is no help for this module"""
  89. def config_complete(self):
  90. """Called when module.config is populated"""
  91. async def client_ready(self):
  92. """Called after client is ready (after config_loaded)"""
  93. def internal_init(self):
  94. """Called after the class is initialized in order to pass the client and db. Do not call it yourself"""
  95. self.db = self.allmodules.db
  96. self._db = self.allmodules.db
  97. self.client = self.allmodules.client
  98. self._client = self.allmodules.client
  99. self.lookup = self.allmodules.lookup
  100. self.get_prefix = self.allmodules.get_prefix
  101. self.inline = self.allmodules.inline
  102. self.allclients = self.allmodules.allclients
  103. self.tg_id = self._client.tg_id
  104. self._tg_id = self._client.tg_id
  105. async def on_unload(self):
  106. """Called after unloading / reloading module"""
  107. async def on_dlmod(self):
  108. """
  109. Called after the module is first time loaded with .dlmod or .loadmod
  110. Possible use-cases:
  111. - Send reaction to author's channel message
  112. - Create asset folder
  113. - ...
  114. ⚠️ Note, that any error there will not interrupt module load, and will just
  115. send a message to logs with verbosity INFO and exception traceback
  116. """
  117. async def invoke(
  118. self,
  119. command: str,
  120. args: typing.Optional[str] = None,
  121. peer: typing.Optional[EntityLike] = None,
  122. message: typing.Optional[Message] = None,
  123. edit: bool = False,
  124. ) -> Message:
  125. """
  126. Invoke another command
  127. :param command: Command to invoke
  128. :param args: Arguments to pass to command
  129. :param peer: Peer to send the command to. If not specified, will send to the current chat
  130. :param edit: Whether to edit the message
  131. :returns Message:
  132. """
  133. if command not in self.allmodules.commands:
  134. raise ValueError(f"Command {command} not found")
  135. if not message and not peer:
  136. raise ValueError("Either peer or message must be specified")
  137. cmd = f"{self.get_prefix()}{command} {args or ''}".strip()
  138. message = (
  139. (await self._client.send_message(peer, cmd))
  140. if peer
  141. else (await (message.edit if edit else message.respond)(cmd))
  142. )
  143. await self.allmodules.commands[command](message)
  144. return message
  145. @property
  146. def commands(self) -> typing.Dict[str, Command]:
  147. """List of commands that module supports"""
  148. return get_commands(self)
  149. @property
  150. def hikka_commands(self) -> typing.Dict[str, Command]:
  151. """List of commands that module supports"""
  152. return get_commands(self)
  153. @property
  154. def inline_handlers(self) -> typing.Dict[str, Command]:
  155. """List of inline handlers that module supports"""
  156. return get_inline_handlers(self)
  157. @property
  158. def hikka_inline_handlers(self) -> typing.Dict[str, Command]:
  159. """List of inline handlers that module supports"""
  160. return get_inline_handlers(self)
  161. @property
  162. def callback_handlers(self) -> typing.Dict[str, Command]:
  163. """List of callback handlers that module supports"""
  164. return get_callback_handlers(self)
  165. @property
  166. def hikka_callback_handlers(self) -> typing.Dict[str, Command]:
  167. """List of callback handlers that module supports"""
  168. return get_callback_handlers(self)
  169. @property
  170. def watchers(self) -> typing.Dict[str, Command]:
  171. """List of watchers that module supports"""
  172. return get_watchers(self)
  173. @property
  174. def hikka_watchers(self) -> typing.Dict[str, Command]:
  175. """List of watchers that module supports"""
  176. return get_watchers(self)
  177. @commands.setter
  178. def commands(self, _):
  179. pass
  180. @hikka_commands.setter
  181. def hikka_commands(self, _):
  182. pass
  183. @inline_handlers.setter
  184. def inline_handlers(self, _):
  185. pass
  186. @hikka_inline_handlers.setter
  187. def hikka_inline_handlers(self, _):
  188. pass
  189. @callback_handlers.setter
  190. def callback_handlers(self, _):
  191. pass
  192. @hikka_callback_handlers.setter
  193. def hikka_callback_handlers(self, _):
  194. pass
  195. @watchers.setter
  196. def watchers(self, _):
  197. pass
  198. @hikka_watchers.setter
  199. def hikka_watchers(self, _):
  200. pass
  201. async def animate(
  202. self,
  203. message: typing.Union[Message, InlineMessage],
  204. frames: typing.List[str],
  205. interval: typing.Union[float, int],
  206. *,
  207. inline: bool = False,
  208. ) -> None:
  209. """
  210. Animate message
  211. :param message: Message to animate
  212. :param frames: A List of strings which are the frames of animation
  213. :param interval: Animation delay
  214. :param inline: Whether to use inline bot for animation
  215. :returns message:
  216. Please, note that if you set `inline=True`, first frame will be shown with an empty
  217. button due to the limitations of Telegram API
  218. """
  219. from . import utils
  220. with contextlib.suppress(AttributeError):
  221. _hikka_client_id_logging_tag = copy.copy(self.client.tg_id) # noqa: F841
  222. if interval < 0.1:
  223. logger.warning(
  224. "Resetting animation interval to 0.1s, because it may get you in"
  225. " floodwaits"
  226. )
  227. interval = 0.1
  228. for frame in frames:
  229. if isinstance(message, Message):
  230. if inline:
  231. message = await self.inline.form(
  232. message=message,
  233. text=frame,
  234. reply_markup={"text": "\u0020\u2800", "data": "empty"},
  235. )
  236. else:
  237. message = await utils.answer(message, frame)
  238. elif isinstance(message, InlineMessage) and inline:
  239. await message.edit(frame)
  240. await asyncio.sleep(interval)
  241. return message
  242. def get(
  243. self,
  244. key: str,
  245. default: typing.Optional[JSONSerializable] = None,
  246. ) -> JSONSerializable:
  247. return self._db.get(self.__class__.__name__, key, default)
  248. def set(self, key: str, value: JSONSerializable) -> bool:
  249. self._db.set(self.__class__.__name__, key, value)
  250. def pointer(
  251. self,
  252. key: str,
  253. default: typing.Optional[JSONSerializable] = None,
  254. item_type: typing.Optional[typing.Any] = None,
  255. ) -> typing.Union[JSONSerializable, PointerList, PointerDict]:
  256. return self._db.pointer(self.__class__.__name__, key, default, item_type)
  257. async def _approve(
  258. self,
  259. call: InlineCall,
  260. channel: EntityLike,
  261. event: asyncio.Event,
  262. ):
  263. from . import utils
  264. local_event = asyncio.Event()
  265. self.__approve += [(channel, local_event)] # skipcq: PTC-W0037
  266. await local_event.wait()
  267. event.status = local_event.status
  268. event.set()
  269. await call.edit(
  270. (
  271. "💫 <b>Joined <a"
  272. f' href="https://t.me/{channel.username}">{utils.escape_html(channel.title)}</a></b>'
  273. ),
  274. gif="https://static.hikari.gay/0d32cbaa959e755ac8eef610f01ba0bd.gif",
  275. )
  276. async def _decline(
  277. self,
  278. call: InlineCall,
  279. channel: EntityLike,
  280. event: asyncio.Event,
  281. ):
  282. from . import utils
  283. self._db.set(
  284. "hikka.main",
  285. "declined_joins",
  286. list(set(self._db.get("hikka.main", "declined_joins", []) + [channel.id])),
  287. )
  288. event.status = False
  289. event.set()
  290. await call.edit(
  291. (
  292. "✖️ <b>Declined joining <a"
  293. f' href="https://t.me/{channel.username}">{utils.escape_html(channel.title)}</a></b>'
  294. ),
  295. gif="https://static.hikari.gay/0d32cbaa959e755ac8eef610f01ba0bd.gif",
  296. )
  297. async def request_join(
  298. self,
  299. peer: EntityLike,
  300. reason: str,
  301. assure_joined: typing.Optional[bool] = False,
  302. ) -> bool:
  303. """
  304. Request to join a channel.
  305. :param peer: The channel to join.
  306. :param reason: The reason for joining.
  307. :param assure_joined: If set, module will not be loaded unless the required channel is joined.
  308. ⚠️ Works only in `client_ready`!
  309. ⚠️ If user declines to join channel, he will not be asked to
  310. join again, so unless he joins it manually, module will not be loaded
  311. ever.
  312. :return: Status of the request.
  313. :rtype: bool
  314. :notice: This method will block module loading until the request is approved or declined.
  315. """
  316. from . import utils
  317. event = asyncio.Event()
  318. await self.client(
  319. UpdateNotifySettingsRequest(
  320. peer=self.inline.bot_username,
  321. settings=InputPeerNotifySettings(show_previews=False, silent=False),
  322. )
  323. )
  324. channel = await self.client.get_entity(peer)
  325. if channel.id in self._db.get("hikka.main", "declined_joins", []):
  326. if assure_joined:
  327. raise LoadError(
  328. f"You need to join @{channel.username} in order to use this module"
  329. )
  330. return False
  331. if not isinstance(channel, Channel):
  332. raise TypeError("`peer` field must be a channel")
  333. if getattr(channel, "left", True):
  334. channel = await self.client.force_get_entity(peer)
  335. if not getattr(channel, "left", True):
  336. return True
  337. await self.inline.bot.send_animation(
  338. self.tg_id,
  339. "https://static.hikari.gay/ab3adf144c94a0883bfe489f4eebc520.gif",
  340. caption=(
  341. self._client.loader.lookup("translations")
  342. .strings("requested_join")
  343. .format(
  344. self.__class__.__name__,
  345. channel.username,
  346. utils.escape_html(channel.title),
  347. utils.escape_html(reason),
  348. )
  349. ),
  350. reply_markup=self.inline.generate_markup(
  351. [
  352. {
  353. "text": "💫 Approve",
  354. "callback": self._approve,
  355. "args": (channel, event),
  356. },
  357. {
  358. "text": "✖️ Decline",
  359. "callback": self._decline,
  360. "args": (channel, event),
  361. },
  362. ]
  363. ),
  364. )
  365. self.hikka_wait_channel_approve = (
  366. self.__class__.__name__,
  367. channel,
  368. reason,
  369. )
  370. await event.wait()
  371. with contextlib.suppress(AttributeError):
  372. delattr(self, "hikka_wait_channel_approve")
  373. if assure_joined and not event.status:
  374. raise LoadError(
  375. f"You need to join @{channel.username} in order to use this module"
  376. )
  377. return event.status
  378. async def import_lib(
  379. self,
  380. url: str,
  381. *,
  382. suspend_on_error: typing.Optional[bool] = False,
  383. _did_requirements: bool = False,
  384. ) -> "Library":
  385. """
  386. Import library from url and register it in :obj:`Modules`
  387. :param url: Url to import
  388. :param suspend_on_error: Will raise :obj:`loader.SelfSuspend` if library can't be loaded
  389. :return: :obj:`Library`
  390. :raise: SelfUnload if :attr:`suspend_on_error` is True and error occurred
  391. :raise: HTTPError if library is not found
  392. :raise: ImportError if library doesn't have any class which is a subclass of :obj:`loader.Library`
  393. :raise: ImportError if library name doesn't end with `Lib`
  394. :raise: RuntimeError if library throws in :method:`init`
  395. :raise: RuntimeError if library classname exists in :obj:`Modules`.libraries
  396. """
  397. from . import utils # Avoiding circular import
  398. from .loader import USER_INSTALL, VALID_PIP_PACKAGES
  399. from .translations import Strings
  400. def _raise(e: Exception):
  401. if suspend_on_error:
  402. raise SelfSuspend("Required library is not available or is corrupted.")
  403. raise e
  404. if not utils.check_url(url):
  405. _raise(ValueError("Invalid url for library"))
  406. code = await utils.run_sync(requests.get, url)
  407. code.raise_for_status()
  408. code = code.text
  409. if re.search(r"# ?scope: ?hikka_min", code):
  410. ver = tuple(
  411. map(
  412. int,
  413. re.search(r"# ?scope: ?hikka_min ((\d+\.){2}\d+)", code)[1].split(
  414. "."
  415. ),
  416. )
  417. )
  418. if version.__version__ < ver:
  419. _raise(
  420. RuntimeError(
  421. f"Library requires Hikka version {'{}.{}.{}'.format(*ver)}+"
  422. )
  423. )
  424. module = f"hikka.libraries.{url.replace('%', '%%').replace('.', '%d')}"
  425. origin = f"<library {url}>"
  426. spec = importlib.machinery.ModuleSpec(
  427. module,
  428. StringLoader(code, origin),
  429. origin=origin,
  430. )
  431. try:
  432. instance = importlib.util.module_from_spec(spec)
  433. sys.modules[module] = instance
  434. spec.loader.exec_module(instance)
  435. except ImportError as e:
  436. logger.info(
  437. "Library loading failed, attemping dependency installation (%s)",
  438. e.name,
  439. )
  440. # Let's try to reinstall dependencies
  441. try:
  442. requirements = list(
  443. filter(
  444. lambda x: not x.startswith(("-", "_", ".")),
  445. map(
  446. str.strip,
  447. VALID_PIP_PACKAGES.search(code)[1].split(),
  448. ),
  449. )
  450. )
  451. except TypeError:
  452. logger.warning(
  453. "No valid pip packages specified in code, attemping"
  454. " installation from error"
  455. )
  456. requirements = [e.name]
  457. logger.debug("Installing requirements: %s", requirements)
  458. if not requirements or _did_requirements:
  459. _raise(e)
  460. pip = await asyncio.create_subprocess_exec(
  461. sys.executable,
  462. "-m",
  463. "pip",
  464. "install",
  465. "--upgrade",
  466. "-q",
  467. "--disable-pip-version-check",
  468. "--no-warn-script-location",
  469. *["--user"] if USER_INSTALL else [],
  470. *requirements,
  471. )
  472. rc = await pip.wait()
  473. if rc != 0:
  474. _raise(e)
  475. importlib.invalidate_caches()
  476. kwargs = utils.get_kwargs()
  477. kwargs["_did_requirements"] = True
  478. return await self._mod_import_lib(**kwargs) # Try again
  479. lib_obj = next(
  480. (
  481. value()
  482. for value in vars(instance).values()
  483. if inspect.isclass(value) and issubclass(value, Library)
  484. ),
  485. None,
  486. )
  487. if not lib_obj:
  488. _raise(ImportError("Invalid library. No class found"))
  489. if not lib_obj.__class__.__name__.endswith("Lib"):
  490. _raise(
  491. ImportError(
  492. "Invalid library. Classname {} does not end with 'Lib'".format(
  493. lib_obj.__class__.__name__
  494. )
  495. )
  496. )
  497. if (
  498. all(
  499. line.replace(" ", "") != "#scope:no_stats" for line in code.splitlines()
  500. )
  501. and self._db.get("hikka.main", "stats", True)
  502. and url is not None
  503. and utils.check_url(url)
  504. ):
  505. with contextlib.suppress(Exception):
  506. await self.lookup("loader")._send_stats(url)
  507. lib_obj.source_url = url.strip("/")
  508. lib_obj.allmodules = self.allmodules
  509. lib_obj.internal_init()
  510. for old_lib in self.allmodules.libraries:
  511. if old_lib.name == lib_obj.name and (
  512. not isinstance(getattr(old_lib, "version", None), tuple)
  513. and not isinstance(getattr(lib_obj, "version", None), tuple)
  514. or old_lib.version >= lib_obj.version
  515. ):
  516. logger.debug("Using existing instance of library %s", old_lib.name)
  517. return old_lib
  518. if hasattr(lib_obj, "init"):
  519. if not callable(lib_obj.init):
  520. _raise(ValueError("Library init() must be callable"))
  521. try:
  522. await lib_obj.init()
  523. except Exception:
  524. _raise(RuntimeError("Library init() failed"))
  525. if hasattr(lib_obj, "config"):
  526. if not isinstance(lib_obj.config, LibraryConfig):
  527. _raise(
  528. RuntimeError("Library config must be a `LibraryConfig` instance")
  529. )
  530. libcfg = lib_obj.db.get(
  531. lib_obj.__class__.__name__,
  532. "__config__",
  533. {},
  534. )
  535. for conf in lib_obj.config:
  536. with contextlib.suppress(Exception):
  537. lib_obj.config.set_no_raise(
  538. conf,
  539. (
  540. libcfg[conf]
  541. if conf in libcfg
  542. else os.environ.get(f"{lib_obj.__class__.__name__}.{conf}")
  543. or lib_obj.config.getdef(conf)
  544. ),
  545. )
  546. if hasattr(lib_obj, "strings"):
  547. lib_obj.strings = Strings(lib_obj, self.translator)
  548. lib_obj.translator = self.translator
  549. for old_lib in self.allmodules.libraries:
  550. if old_lib.name == lib_obj.name:
  551. if hasattr(old_lib, "on_lib_update") and callable(
  552. old_lib.on_lib_update
  553. ):
  554. await old_lib.on_lib_update(lib_obj)
  555. replace_all_refs(old_lib, lib_obj)
  556. logger.debug(
  557. "Replacing existing instance of library %s with updated object",
  558. lib_obj.name,
  559. )
  560. return lib_obj
  561. self.allmodules.libraries += [lib_obj]
  562. return lib_obj
  563. class DragonModule:
  564. """Module is running in compatibility mode with Dragon, so it might be unstable"""
  565. # fmt: off
  566. strings_ru = {"_cls_doc": "Модуль запущен в режиме совместимости с Dragon, поэтому он может быть нестабильным"}
  567. strings_de = {"_cls_doc": "Das Modul wird im Dragon-Kompatibilitäts modus ausgeführt, daher kann es instabil sein"}
  568. strings_tr = {"_cls_doc": "Modül Dragon uyumluluğu modunda çalıştığı için istikrarsız olabilir"}
  569. strings_uz = {"_cls_doc": "Modul Dragon muvofiqligi rejimida ishlamoqda, shuning uchun u beqaror bo'lishi mumkin"}
  570. strings_es = {"_cls_doc": "El módulo se ejecuta en modo de compatibilidad con Dragon, por lo que puede ser inestable"}
  571. strings_kk = {"_cls_doc": "Модуль Dragon қамтамасыз ету режимінде іске қосылған, сондықтан белсенді емес болуы мүмкін"}
  572. strings_tt = {"_clc_doc": "Модуль Dragon белән ярашучанлык режимда эшли башлады, шуңа күрә ул тотрыксыз була ала"}
  573. # fmt: on
  574. def __init__(self):
  575. self.name = "Unknown"
  576. self.url = None
  577. self.commands = {}
  578. self.watchers = {}
  579. self.hikka_watchers = {}
  580. self.inline_handlers = {}
  581. self.hikka_inline_handlers = {}
  582. self.callback_handlers = {}
  583. self.hikka_callback_handlers = {}
  584. @property
  585. def hikka_commands(
  586. self,
  587. ) -> typing.Dict[str, Command]:
  588. return self.commands
  589. @property
  590. def __origin__(self) -> str:
  591. return f"<dragon {self.name}>"
  592. def config_complete(self):
  593. pass
  594. async def client_ready(self):
  595. pass
  596. async def on_unload(self):
  597. pass
  598. async def on_dlmod(self):
  599. pass
  600. class Library:
  601. """All external libraries must have a class-inheritant from this class"""
  602. def internal_init(self):
  603. self.name = self.__class__.__name__
  604. self.db = self.allmodules.db
  605. self._db = self.allmodules.db
  606. self.client = self.allmodules.client
  607. self._client = self.allmodules.client
  608. self.tg_id = self._client.tg_id
  609. self._tg_id = self._client.tg_id
  610. self.lookup = self.allmodules.lookup
  611. self.get_prefix = self.allmodules.get_prefix
  612. self.inline = self.allmodules.inline
  613. self.allclients = self.allmodules.allclients
  614. def _lib_get(
  615. self,
  616. key: str,
  617. default: typing.Optional[JSONSerializable] = None,
  618. ) -> JSONSerializable:
  619. return self._db.get(self.__class__.__name__, key, default)
  620. def _lib_set(self, key: str, value: JSONSerializable) -> bool:
  621. self._db.set(self.__class__.__name__, key, value)
  622. def _lib_pointer(
  623. self,
  624. key: str,
  625. default: typing.Optional[JSONSerializable] = None,
  626. ) -> typing.Union[JSONSerializable, PointerDict, PointerList]:
  627. return self._db.pointer(self.__class__.__name__, key, default)
  628. class LoadError(Exception):
  629. """Tells user, why your module can't be loaded, if raised in `client_ready`"""
  630. def __init__(self, error_message: str): # skipcq: PYL-W0231
  631. self._error = error_message
  632. def __str__(self) -> str:
  633. return self._error
  634. class CoreOverwriteError(LoadError):
  635. """Is being raised when core module or command is overwritten"""
  636. def __init__(
  637. self,
  638. module: typing.Optional[str] = None,
  639. command: typing.Optional[str] = None,
  640. ):
  641. self.type = "module" if module else "command"
  642. self.target = module or command
  643. super().__init__(str(self))
  644. def __str__(self) -> str:
  645. return (
  646. f"{'Module' if self.type == 'module' else 'command'} {self.target} will not"
  647. " be overwritten, because it's core"
  648. )
  649. class CoreUnloadError(Exception):
  650. """Is being raised when user tries to unload core module"""
  651. def __init__(self, module: str):
  652. self.module = module
  653. super().__init__()
  654. def __str__(self) -> str:
  655. return f"Module {self.module} will not be unloaded, because it's core"
  656. class SelfUnload(Exception):
  657. """Silently unloads module, if raised in `client_ready`"""
  658. def __init__(self, error_message: str = ""):
  659. super().__init__()
  660. self._error = error_message
  661. def __str__(self) -> str:
  662. return self._error
  663. class SelfSuspend(Exception):
  664. """
  665. Silently suspends module, if raised in `client_ready`
  666. Commands and watcher will not be registered if raised
  667. Module won't be unloaded from db and will be unfreezed after restart, unless
  668. the exception is raised again
  669. """
  670. def __init__(self, error_message: str = ""):
  671. super().__init__()
  672. self._error = error_message
  673. def __str__(self) -> str:
  674. return self._error
  675. class StopLoop(Exception):
  676. """Stops the loop, in which is raised"""
  677. class ModuleConfig(dict):
  678. """Stores config for modules and apparently libraries"""
  679. def __init__(self, *entries: typing.Union[str, "ConfigValue"]):
  680. if all(isinstance(entry, ConfigValue) for entry in entries):
  681. # New config format processing
  682. self._config = {config.option: config for config in entries}
  683. else:
  684. # Legacy config processing
  685. keys = []
  686. values = []
  687. defaults = []
  688. docstrings = []
  689. for i, entry in enumerate(entries):
  690. if i % 3 == 0:
  691. keys += [entry]
  692. elif i % 3 == 1:
  693. values += [entry]
  694. defaults += [entry]
  695. else:
  696. docstrings += [entry]
  697. self._config = {
  698. key: ConfigValue(option=key, default=default, doc=doc)
  699. for key, default, doc in zip(keys, defaults, docstrings)
  700. }
  701. super().__init__(
  702. {option: config.value for option, config in self._config.items()}
  703. )
  704. def getdoc(self, key: str, message: typing.Optional[Message] = None) -> str:
  705. """Get the documentation by key"""
  706. ret = self._config[key].doc
  707. if callable(ret):
  708. try:
  709. # Compatibility tweak
  710. # does nothing in Hikka
  711. ret = ret(message)
  712. except Exception:
  713. ret = ret()
  714. return ret
  715. def getdef(self, key: str) -> str:
  716. """Get the default value by key"""
  717. return self._config[key].default
  718. def __setitem__(self, key: str, value: typing.Any):
  719. self._config[key].value = value
  720. super().__setitem__(key, value)
  721. def set_no_raise(self, key: str, value: typing.Any):
  722. self._config[key].set_no_raise(value)
  723. super().__setitem__(key, value)
  724. def __getitem__(self, key: str) -> typing.Any:
  725. try:
  726. return self._config[key].value
  727. except KeyError:
  728. return None
  729. def reload(self):
  730. for key in self._config:
  731. super().__setitem__(key, self._config[key].value)
  732. def change_validator(
  733. self,
  734. key: str,
  735. validator: typing.Callable[[JSONSerializable], JSONSerializable],
  736. ):
  737. self._config[key].validator = validator
  738. LibraryConfig = ModuleConfig
  739. class _Placeholder:
  740. """Placeholder to determine if the default value is going to be set"""
  741. async def wrap(func: typing.Callable[[], typing.Awaitable]) -> typing.Any:
  742. with contextlib.suppress(Exception):
  743. return await func()
  744. def syncwrap(func: typing.Callable[[], typing.Any]) -> typing.Any:
  745. with contextlib.suppress(Exception):
  746. return func()
  747. @dataclass(repr=True)
  748. class ConfigValue:
  749. option: str
  750. default: typing.Any = None
  751. doc: typing.Union[typing.Callable[[], str], str] = "No description"
  752. value: typing.Any = field(default_factory=_Placeholder)
  753. validator: typing.Optional[
  754. typing.Callable[[JSONSerializable], JSONSerializable]
  755. ] = None
  756. on_change: typing.Optional[
  757. typing.Union[typing.Callable[[], typing.Awaitable], typing.Callable]
  758. ] = None
  759. def __post_init__(self):
  760. if isinstance(self.value, _Placeholder):
  761. self.value = self.default
  762. def set_no_raise(self, value: typing.Any) -> bool:
  763. """
  764. Sets the config value w/o ValidationError being raised
  765. Should not be used uninternally
  766. """
  767. return self.__setattr__("value", value, ignore_validation=True)
  768. def __setattr__(
  769. self,
  770. key: str,
  771. value: typing.Any,
  772. *,
  773. ignore_validation: bool = False,
  774. ):
  775. if key == "value":
  776. try:
  777. value = ast.literal_eval(value)
  778. except Exception:
  779. pass
  780. # Convert value to list if it's tuple just not to mess up
  781. # with json convertations
  782. if isinstance(value, (set, tuple)):
  783. value = list(value)
  784. if isinstance(value, list):
  785. value = [
  786. item.strip() if isinstance(item, str) else item for item in value
  787. ]
  788. if self.validator is not None:
  789. if value is not None:
  790. from . import validators
  791. try:
  792. value = self.validator.validate(value)
  793. except validators.ValidationError as e:
  794. if not ignore_validation:
  795. raise e
  796. logger.debug(
  797. "Config value was broken (%s), so it was reset to %s",
  798. value,
  799. self.default,
  800. )
  801. value = self.default
  802. else:
  803. defaults = {
  804. "String": "",
  805. "Integer": 0,
  806. "Boolean": False,
  807. "Series": [],
  808. "Float": 0.0,
  809. }
  810. if self.validator.internal_id in defaults:
  811. logger.debug(
  812. "Config value was None, so it was reset to %s",
  813. defaults[self.validator.internal_id],
  814. )
  815. value = defaults[self.validator.internal_id]
  816. # This attribute will tell the `Loader` to save this value in db
  817. self._save_marker = True
  818. object.__setattr__(self, key, value)
  819. if key == "value" and not ignore_validation and callable(self.on_change):
  820. if inspect.iscoroutinefunction(self.on_change):
  821. asyncio.ensure_future(wrap(self.on_change))
  822. else:
  823. syncwrap(self.on_change)
  824. def _get_members(
  825. mod: Module,
  826. ending: str,
  827. attribute: typing.Optional[str] = None,
  828. strict: bool = False,
  829. ) -> dict:
  830. """Get method of module, which end with ending"""
  831. return {
  832. (
  833. method_name.rsplit(ending, maxsplit=1)[0]
  834. if (method_name == ending if strict else method_name.endswith(ending))
  835. else method_name
  836. ).lower(): getattr(mod, method_name)
  837. for method_name in dir(mod)
  838. if not isinstance(getattr(type(mod), method_name, None), property)
  839. and callable(getattr(mod, method_name))
  840. and (
  841. (method_name == ending if strict else method_name.endswith(ending))
  842. or attribute
  843. and getattr(getattr(mod, method_name), attribute, False)
  844. )
  845. }
  846. class CacheRecordEntity:
  847. def __init__(
  848. self,
  849. hashable_entity: "Hashable", # type: ignore # noqa: F821
  850. resolved_entity: EntityLike,
  851. exp: int,
  852. ):
  853. self.entity = copy.deepcopy(resolved_entity)
  854. self._hashable_entity = copy.deepcopy(hashable_entity)
  855. self._exp = round(time.time() + exp)
  856. self.ts = time.time()
  857. @property
  858. def expired(self) -> bool:
  859. return self._exp < time.time()
  860. def __eq__(self, record: "CacheRecordEntity") -> bool:
  861. return hash(record) == hash(self)
  862. def __hash__(self) -> int:
  863. return hash(self._hashable_entity)
  864. def __str__(self) -> str:
  865. return f"CacheRecordEntity of {self.entity}"
  866. def __repr__(self) -> str:
  867. return (
  868. f"CacheRecordEntity(entity={type(self.entity).__name__}(...),"
  869. f" exp={self._exp})"
  870. )
  871. class CacheRecordPerms:
  872. def __init__(
  873. self,
  874. hashable_entity: "Hashable", # type: ignore # noqa: F821
  875. hashable_user: "Hashable", # type: ignore # noqa: F821
  876. resolved_perms: EntityLike,
  877. exp: int,
  878. ):
  879. self.perms = copy.deepcopy(resolved_perms)
  880. self._hashable_entity = copy.deepcopy(hashable_entity)
  881. self._hashable_user = copy.deepcopy(hashable_user)
  882. self._exp = round(time.time() + exp)
  883. self.ts = time.time()
  884. @property
  885. def expired(self) -> bool:
  886. return self._exp < time.time()
  887. def __eq__(self, record: "CacheRecordPerms") -> bool:
  888. return hash(record) == hash(self)
  889. def __hash__(self) -> int:
  890. return hash((self._hashable_entity, self._hashable_user))
  891. def __str__(self) -> str:
  892. return f"CacheRecordPerms of {self.perms}"
  893. def __repr__(self) -> str:
  894. return (
  895. f"CacheRecordPerms(perms={type(self.perms).__name__}(...), exp={self._exp})"
  896. )
  897. class CacheRecordFullChannel:
  898. def __init__(self, channel_id: int, full_channel: ChannelFull, exp: int):
  899. self.channel_id = channel_id
  900. self.full_channel = full_channel
  901. self._exp = round(time.time() + exp)
  902. self.ts = time.time()
  903. @property
  904. def expired(self) -> bool:
  905. return self._exp < time.time()
  906. def __eq__(self, record: "CacheRecordFullChannel") -> bool:
  907. return hash(record) == hash(self)
  908. def __hash__(self) -> int:
  909. return hash((self._hashable_entity, self._hashable_user))
  910. def __str__(self) -> str:
  911. return f"CacheRecordFullChannel of {self.channel_id}"
  912. def __repr__(self) -> str:
  913. return (
  914. f"CacheRecordFullChannel(channel_id={self.channel_id}(...),"
  915. f" exp={self._exp})"
  916. )
  917. class CacheRecordFullUser:
  918. def __init__(self, user_id: int, full_user: UserFull, exp: int):
  919. self.user_id = user_id
  920. self.full_user = full_user
  921. self._exp = round(time.time() + exp)
  922. self.ts = time.time()
  923. @property
  924. def expired(self) -> bool:
  925. return self._exp < time.time()
  926. def __eq__(self, record: "CacheRecordFullUser") -> bool:
  927. return hash(record) == hash(self)
  928. def __hash__(self) -> int:
  929. return hash((self._hashable_entity, self._hashable_user))
  930. def __str__(self) -> str:
  931. return f"CacheRecordFullUser of {self.user_id}"
  932. def __repr__(self) -> str:
  933. return f"CacheRecordFullUser(channel_id={self.user_id}(...), exp={self._exp})"
  934. def get_commands(mod: Module) -> dict:
  935. """Introspect the module to get its commands"""
  936. return _get_members(mod, "cmd", "is_command")
  937. def get_inline_handlers(mod: Module) -> dict:
  938. """Introspect the module to get its inline handlers"""
  939. return _get_members(mod, "_inline_handler", "is_inline_handler")
  940. def get_callback_handlers(mod: Module) -> dict:
  941. """Introspect the module to get its callback handlers"""
  942. return _get_members(mod, "_callback_handler", "is_callback_handler")
  943. def get_watchers(mod: Module) -> dict:
  944. """Introspect the module to get its watchers"""
  945. return _get_members(
  946. mod,
  947. "watcher",
  948. "is_watcher",
  949. strict=True,
  950. )