types.py 14 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503
  1. # █ █ ▀ █▄▀ ▄▀█ █▀█ ▀
  2. # █▀█ █ █ █ █▀█ █▀▄ █
  3. # © Copyright 2022
  4. # https://t.me/hikariatama
  5. #
  6. # 🔒 Licensed under the GNU AGPLv3
  7. # 🌐 https://www.gnu.org/licenses/agpl-3.0.html
  8. import ast
  9. import asyncio
  10. import contextlib
  11. import copy
  12. import inspect
  13. import logging
  14. from dataclasses import dataclass, field
  15. import time
  16. from typing import Any, Awaitable, Callable, Optional, Union
  17. from importlib.abc import SourceLoader
  18. from telethon.tl.types import Message, ChannelFull, UserFull
  19. from telethon.hints import EntityLike
  20. from .inline.types import ( # skipcq: PY-W2000
  21. InlineMessage,
  22. BotInlineMessage,
  23. InlineCall,
  24. BotInlineCall,
  25. InlineUnit,
  26. BotMessage,
  27. InlineQuery,
  28. )
  29. from . import validators # skipcq: PY-W2000
  30. from .pointers import ( # skipcq: PY-W2000
  31. PointerList,
  32. PointerDict,
  33. )
  34. logger = logging.getLogger(__name__)
  35. JSONSerializable = Union[str, int, float, bool, list, dict, None]
  36. class StringLoader(SourceLoader):
  37. """Load a python module/file from a string"""
  38. def __init__(self, data: str, origin: str):
  39. self.data = data.encode("utf-8") if isinstance(data, str) else data
  40. self.origin = origin
  41. def get_code(self, fullname: str) -> str:
  42. return (
  43. compile(source, self.origin, "exec", dont_inherit=True)
  44. if (source := self.get_source(fullname))
  45. else None
  46. )
  47. def get_filename(self, *args, **kwargs) -> str:
  48. return self.origin
  49. def get_data(self, *args, **kwargs) -> bytes:
  50. return self.data
  51. class Module:
  52. strings = {"name": "Unknown"}
  53. """There is no help for this module"""
  54. def config_complete(self):
  55. """Called when module.config is populated"""
  56. async def client_ready(self, client, db):
  57. """Called after client is ready (after config_loaded)"""
  58. async def on_unload(self):
  59. """Called after unloading / reloading module"""
  60. async def on_dlmod(self, client, db):
  61. """
  62. Called after the module is first time loaded with .dlmod or .loadmod
  63. Possible use-cases:
  64. - Send reaction to author's channel message
  65. - Join author's channel
  66. - Create asset folder
  67. - ...
  68. ⚠️ Note, that any error there will not interrupt module load, and will just
  69. send a message to logs with verbosity INFO and exception traceback
  70. """
  71. def __getattr__(self, name: str):
  72. if name in {"hikka_commands", "commands"}:
  73. return get_commands(self)
  74. if name in {"hikka_inline_handlers", "inline_handlers"}:
  75. return get_inline_handlers(self)
  76. if name in {"hikka_callback_handlers", "callback_handlers"}:
  77. return get_callback_handlers(self)
  78. if name in {"hikka_watchers", "watchers"}:
  79. return get_watchers(self)
  80. raise AttributeError(f"Module has no attribute {name}")
  81. class Library:
  82. """All external libraries must have a class-inheritant from this class"""
  83. class LoadError(Exception):
  84. """Tells user, why your module can't be loaded, if raised in `client_ready`"""
  85. def __init__(self, error_message: str): # skipcq: PYL-W0231
  86. self._error = error_message
  87. def __str__(self) -> str:
  88. return self._error
  89. class CoreOverwriteError(LoadError):
  90. """Is being raised when core module or command is overwritten"""
  91. def __init__(self, module: Optional[str] = None, command: Optional[str] = None):
  92. self.type = "module" if module else "command"
  93. self.target = module or command
  94. super().__init__()
  95. def __str__(self) -> str:
  96. return (
  97. f"Module {self.target} will not be overwritten, because it's core"
  98. if self.type == "module"
  99. else f"Command {self.target} will not be overwritten, because it's core"
  100. )
  101. class CoreUnloadError(Exception):
  102. """Is being raised when user tries to unload core module"""
  103. def __init__(self, module: str):
  104. self.module = module
  105. super().__init__()
  106. def __str__(self) -> str:
  107. return f"Module {self.module} will not be unloaded, because it's core"
  108. class SelfUnload(Exception):
  109. """Silently unloads module, if raised in `client_ready`"""
  110. def __init__(self, error_message: str = ""):
  111. super().__init__()
  112. self._error = error_message
  113. def __str__(self) -> str:
  114. return self._error
  115. class SelfSuspend(Exception):
  116. """
  117. Silently suspends module, if raised in `client_ready`
  118. Commands and watcher will not be registered if raised
  119. Module won't be unloaded from db and will be unfreezed after restart, unless
  120. the exception is raised again
  121. """
  122. def __init__(self, error_message: str = ""):
  123. super().__init__()
  124. self._error = error_message
  125. def __str__(self) -> str:
  126. return self._error
  127. class StopLoop(Exception):
  128. """Stops the loop, in which is raised"""
  129. class ModuleConfig(dict):
  130. """Stores config for modules and apparently libraries"""
  131. def __init__(self, *entries):
  132. if all(isinstance(entry, ConfigValue) for entry in entries):
  133. # New config format processing
  134. self._config = {config.option: config for config in entries}
  135. else:
  136. # Legacy config processing
  137. keys = []
  138. values = []
  139. defaults = []
  140. docstrings = []
  141. for i, entry in enumerate(entries):
  142. if i % 3 == 0:
  143. keys += [entry]
  144. elif i % 3 == 1:
  145. values += [entry]
  146. defaults += [entry]
  147. else:
  148. docstrings += [entry]
  149. self._config = {
  150. key: ConfigValue(option=key, default=default, doc=doc)
  151. for key, default, doc in zip(keys, defaults, docstrings)
  152. }
  153. super().__init__(
  154. {option: config.value for option, config in self._config.items()}
  155. )
  156. def getdoc(self, key: str, message: Message = None) -> str:
  157. """Get the documentation by key"""
  158. ret = self._config[key].doc
  159. if callable(ret):
  160. try:
  161. # Compatibility tweak
  162. # does nothing in Hikka
  163. ret = ret(message)
  164. except Exception:
  165. ret = ret()
  166. return ret
  167. def getdef(self, key: str) -> str:
  168. """Get the default value by key"""
  169. return self._config[key].default
  170. def __setitem__(self, key: str, value: Any):
  171. self._config[key].value = value
  172. self.update({key: value})
  173. def set_no_raise(self, key: str, value: Any):
  174. self._config[key].set_no_raise(value)
  175. self.update({key: value})
  176. def __getitem__(self, key: str) -> Any:
  177. try:
  178. return self._config[key].value
  179. except KeyError:
  180. return None
  181. LibraryConfig = ModuleConfig
  182. class _Placeholder:
  183. """Placeholder to determine if the default value is going to be set"""
  184. async def wrap(func: Awaitable):
  185. with contextlib.suppress(Exception):
  186. return await func()
  187. def syncwrap(func: Callable):
  188. with contextlib.suppress(Exception):
  189. return func()
  190. @dataclass(repr=True)
  191. class ConfigValue:
  192. option: str
  193. default: Any = None
  194. doc: Union[callable, str] = "No description"
  195. value: Any = field(default_factory=_Placeholder)
  196. validator: Optional[callable] = None
  197. on_change: Optional[Union[Awaitable, Callable]] = None
  198. def __post_init__(self):
  199. if isinstance(self.value, _Placeholder):
  200. self.value = self.default
  201. def set_no_raise(self, value: Any) -> bool:
  202. """
  203. Sets the config value w/o ValidationError being raised
  204. Should not be used uninternally
  205. """
  206. return self.__setattr__("value", value, ignore_validation=True)
  207. def __setattr__(
  208. self,
  209. key: str,
  210. value: Any,
  211. *,
  212. ignore_validation: bool = False,
  213. ) -> bool:
  214. if key == "value":
  215. try:
  216. value = ast.literal_eval(value)
  217. except Exception:
  218. pass
  219. # Convert value to list if it's tuple just not to mess up
  220. # with json convertations
  221. if isinstance(value, (set, tuple)):
  222. value = list(value)
  223. if isinstance(value, list):
  224. value = [
  225. item.strip() if isinstance(item, str) else item for item in value
  226. ]
  227. if self.validator is not None:
  228. if value is not None:
  229. try:
  230. value = self.validator.validate(value)
  231. except validators.ValidationError as e:
  232. if not ignore_validation:
  233. raise e
  234. logger.debug(
  235. f"Config value was broken ({value}), so it was reset to"
  236. f" {self.default}"
  237. )
  238. value = self.default
  239. else:
  240. defaults = {
  241. "String": "",
  242. "Integer": 0,
  243. "Boolean": False,
  244. "Series": [],
  245. "Float": 0.0,
  246. }
  247. if self.validator.internal_id in defaults:
  248. logger.debug(
  249. "Config value was None, so it was reset to"
  250. f" {defaults[self.validator.internal_id]}"
  251. )
  252. value = defaults[self.validator.internal_id]
  253. # This attribute will tell the `Loader` to save this value in db
  254. self._save_marker = True
  255. if not ignore_validation and callable(self.on_change):
  256. if inspect.iscoroutinefunction(self.on_change):
  257. asyncio.ensure_future(wrap(self.on_change))
  258. else:
  259. syncwrap(self.on_change)
  260. object.__setattr__(self, key, value)
  261. def _get_members(
  262. mod: Module,
  263. ending: str,
  264. attribute: Optional[str] = None,
  265. strict: bool = False,
  266. ) -> dict:
  267. """Get method of module, which end with ending"""
  268. return {
  269. (
  270. method_name.rsplit(ending, maxsplit=1)[0]
  271. if (method_name == ending if strict else method_name.endswith(ending))
  272. else method_name
  273. ): getattr(mod, method_name)
  274. for method_name in dir(mod)
  275. if callable(getattr(mod, method_name))
  276. and (
  277. (method_name == ending if strict else method_name.endswith(ending))
  278. or attribute
  279. and getattr(getattr(mod, method_name), attribute, False)
  280. )
  281. }
  282. class CacheRecord:
  283. def __init__(
  284. self,
  285. hashable_entity: "Hashable", # type: ignore
  286. resolved_entity: EntityLike,
  287. exp: int,
  288. ):
  289. self.entity = copy.deepcopy(resolved_entity)
  290. self._hashable_entity = copy.deepcopy(hashable_entity)
  291. self._exp = round(time.time() + exp)
  292. self.ts = time.time()
  293. def expired(self):
  294. return self._exp < time.time()
  295. def __eq__(self, record: "CacheRecord"):
  296. return hash(record) == hash(self)
  297. def __hash__(self):
  298. return hash(self._hashable_entity)
  299. def __str__(self):
  300. return f"CacheRecord of {self.entity}"
  301. def __repr__(self):
  302. return f"CacheRecord(entity={type(self.entity).__name__}(...), exp={self._exp})"
  303. class CacheRecordPerms:
  304. def __init__(
  305. self,
  306. hashable_entity: "Hashable", # type: ignore
  307. hashable_user: "Hashable", # type: ignore
  308. resolved_perms: EntityLike,
  309. exp: int,
  310. ):
  311. self.perms = copy.deepcopy(resolved_perms)
  312. self._hashable_entity = copy.deepcopy(hashable_entity)
  313. self._hashable_user = copy.deepcopy(hashable_user)
  314. self._exp = round(time.time() + exp)
  315. self.ts = time.time()
  316. def expired(self):
  317. return self._exp < time.time()
  318. def __eq__(self, record: "CacheRecordPerms"):
  319. return hash(record) == hash(self)
  320. def __hash__(self):
  321. return hash((self._hashable_entity, self._hashable_user))
  322. def __str__(self):
  323. return f"CacheRecordPerms of {self.perms}"
  324. def __repr__(self):
  325. return (
  326. f"CacheRecordPerms(perms={type(self.perms).__name__}(...), exp={self._exp})"
  327. )
  328. class CacheRecordFullChannel:
  329. def __init__(self, channel_id: int, full_channel: ChannelFull, exp: int):
  330. self.channel_id = channel_id
  331. self.full_channel = full_channel
  332. self._exp = round(time.time() + exp)
  333. self.ts = time.time()
  334. def expired(self):
  335. return self._exp < time.time()
  336. def __eq__(self, record: "CacheRecordFullChannel"):
  337. return hash(record) == hash(self)
  338. def __hash__(self):
  339. return hash((self._hashable_entity, self._hashable_user))
  340. def __str__(self):
  341. return f"CacheRecordFullChannel of {self.channel_id}"
  342. def __repr__(self):
  343. return (
  344. f"CacheRecordFullChannel(channel_id={self.channel_id}(...),"
  345. f" exp={self._exp})"
  346. )
  347. class CacheRecordFullUser:
  348. def __init__(self, user_id: int, full_user: UserFull, exp: int):
  349. self.user_id = user_id
  350. self.full_user = full_user
  351. self._exp = round(time.time() + exp)
  352. self.ts = time.time()
  353. def expired(self):
  354. return self._exp < time.time()
  355. def __eq__(self, record: "CacheRecordFullUser"):
  356. return hash(record) == hash(self)
  357. def __hash__(self):
  358. return hash((self._hashable_entity, self._hashable_user))
  359. def __str__(self):
  360. return f"CacheRecordFullUser of {self.user_id}"
  361. def __repr__(self):
  362. return f"CacheRecordFullUser(channel_id={self.user_id}(...), exp={self._exp})"
  363. def get_commands(mod: Module) -> dict:
  364. """Introspect the module to get its commands"""
  365. return _get_members(mod, "cmd", "is_command")
  366. def get_inline_handlers(mod: Module) -> dict:
  367. """Introspect the module to get its inline handlers"""
  368. return _get_members(mod, "_inline_handler", "is_inline_handler")
  369. def get_callback_handlers(mod: Module) -> dict:
  370. """Introspect the module to get its callback handlers"""
  371. return _get_members(mod, "_callback_handler", "is_callback_handler")
  372. def get_watchers(mod: Module) -> dict:
  373. """Introspect the module to get its watchers"""
  374. return _get_members(
  375. mod,
  376. "watcher",
  377. "is_watcher",
  378. strict=True,
  379. )