_types.py 8.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281
  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 logging
  10. from dataclasses import dataclass, field
  11. from typing import Any, Optional, Union
  12. from .inline.types import * # skipcq: PYL-W0614
  13. from . import validators # skipcq: PY-W2000
  14. from importlib.abc import SourceLoader
  15. from telethon.tl.types import Message
  16. logger = logging.getLogger(__name__)
  17. class StringLoader(SourceLoader):
  18. """Load a python module/file from a string"""
  19. def __init__(self, data: str, origin: str):
  20. self.data = data.encode("utf-8") if isinstance(data, str) else data
  21. self.origin = origin
  22. def get_code(self, fullname: str) -> str:
  23. return (
  24. compile(source, self.origin, "exec", dont_inherit=True)
  25. if (source := self.get_source(fullname))
  26. else None
  27. )
  28. def get_filename(self, *args, **kwargs) -> str:
  29. return self.origin
  30. def get_data(self, *args, **kwargs) -> bytes:
  31. return self.data
  32. class Module:
  33. strings = {"name": "Unknown"}
  34. """There is no help for this module"""
  35. def config_complete(self):
  36. """Called when module.config is populated"""
  37. async def client_ready(self, client, db):
  38. """Called after client is ready (after config_loaded)"""
  39. async def on_unload(self):
  40. """Called after unloading / reloading module"""
  41. async def on_dlmod(self, client, db):
  42. """
  43. Called after the module is first time loaded with .dlmod or .loadmod
  44. Possible use-cases:
  45. - Send reaction to author's channel message
  46. - Join author's channel
  47. - Create asset folder
  48. - ...
  49. ⚠️ Note, that any error there will not interrupt module load, and will just
  50. send a message to logs with verbosity INFO and exception traceback
  51. """
  52. class Library:
  53. """All external libraries must have a class-inheritant from this class"""
  54. class LoadError(Exception):
  55. """Tells user, why your module can't be loaded, if raised in `client_ready`"""
  56. def __init__(self, error_message: str): # skipcq: PYL-W0231
  57. self._error = error_message
  58. def __str__(self) -> str:
  59. return self._error
  60. class CoreOverwriteError(Exception):
  61. """Is being raised when core module or command is overwritten"""
  62. def __init__(self, module: Optional[str] = None, command: Optional[str] = None):
  63. self.type = "module" if module else "command"
  64. self.target = module or command
  65. super().__init__()
  66. def __str__(self) -> str:
  67. return (
  68. f"Module {self.target} will not be overwritten, because it's core"
  69. if self.type == "module"
  70. else f"Command {self.target} will not be overwritten, because it's core"
  71. )
  72. class SelfUnload(Exception):
  73. """Silently unloads module, if raised in `client_ready`"""
  74. def __init__(self, error_message: Optional[str] = ""):
  75. super().__init__()
  76. self._error = error_message
  77. def __str__(self) -> str:
  78. return self._error
  79. class SelfSuspend(Exception):
  80. """
  81. Silently suspends module, if raised in `client_ready`
  82. Commands and watcher will not be registered if raised
  83. Module won't be unloaded from db and will be unfreezed after restart, unless
  84. the exception is raised again
  85. """
  86. def __init__(self, error_message: Optional[str] = ""):
  87. super().__init__()
  88. self._error = error_message
  89. def __str__(self) -> str:
  90. return self._error
  91. class StopLoop(Exception):
  92. """Stops the loop, in which is raised"""
  93. class ModuleConfig(dict):
  94. """Stores config for modules and apparently libraries"""
  95. def __init__(self, *entries):
  96. if all(isinstance(entry, ConfigValue) for entry in entries):
  97. # New config format processing
  98. self._config = {config.option: config for config in entries}
  99. else:
  100. # Legacy config processing
  101. keys = []
  102. values = []
  103. defaults = []
  104. docstrings = []
  105. for i, entry in enumerate(entries):
  106. if i % 3 == 0:
  107. keys += [entry]
  108. elif i % 3 == 1:
  109. values += [entry]
  110. defaults += [entry]
  111. else:
  112. docstrings += [entry]
  113. self._config = {
  114. key: ConfigValue(option=key, default=default, doc=doc)
  115. for key, default, doc in zip(keys, defaults, docstrings)
  116. }
  117. super().__init__(
  118. {option: config.value for option, config in self._config.items()}
  119. )
  120. def getdoc(self, key: str, message: Message = None) -> str:
  121. """Get the documentation by key"""
  122. ret = self._config[key].doc
  123. if callable(ret):
  124. try:
  125. # Compatibility tweak
  126. # does nothing in Hikka
  127. ret = ret(message)
  128. except Exception:
  129. ret = ret()
  130. return ret
  131. def getdef(self, key: str) -> str:
  132. """Get the default value by key"""
  133. return self._config[key].default
  134. def __setitem__(self, key: str, value: Any):
  135. self._config[key].value = value
  136. self.update({key: value})
  137. def set_no_raise(self, key: str, value: Any):
  138. self._config[key].set_no_raise(value)
  139. self.update({key: value})
  140. def __getitem__(self, key: str) -> Any:
  141. try:
  142. return self._config[key].value
  143. except KeyError:
  144. return None
  145. LibraryConfig = ModuleConfig
  146. class _Placeholder:
  147. """Placeholder to determine if the default value is going to be set"""
  148. @dataclass(repr=True)
  149. class ConfigValue:
  150. option: str
  151. default: Any = None
  152. doc: Union[callable, str] = "No description"
  153. value: Any = field(default_factory=_Placeholder)
  154. validator: Optional[callable] = None
  155. def __post_init__(self):
  156. if isinstance(self.value, _Placeholder):
  157. self.value = self.default
  158. def set_no_raise(self, value: Any) -> bool:
  159. """
  160. Sets the config value w/o ValidationError being raised
  161. Should not be used uninternally
  162. """
  163. return self.__setattr__("value", value, ignore_validation=True)
  164. def __setattr__(
  165. self,
  166. key: str,
  167. value: Any,
  168. *,
  169. ignore_validation: Optional[bool] = False,
  170. ) -> bool:
  171. if key == "value":
  172. try:
  173. value = ast.literal_eval(value)
  174. except Exception:
  175. pass
  176. # Convert value to list if it's tuple just not to mess up
  177. # with json convertations
  178. if isinstance(value, (set, tuple)):
  179. value = list(value)
  180. if isinstance(value, list):
  181. value = [
  182. item.strip() if isinstance(item, str) else item for item in value
  183. ]
  184. if self.validator is not None:
  185. if value is not None:
  186. try:
  187. value = self.validator.validate(value)
  188. except validators.ValidationError as e:
  189. if not ignore_validation:
  190. raise e
  191. logger.debug(
  192. f"Config value was broken ({value}), so it was reset to"
  193. f" {self.default}"
  194. )
  195. value = self.default
  196. else:
  197. defaults = {
  198. "String": "",
  199. "Integer": 0,
  200. "Boolean": False,
  201. "Series": [],
  202. "Float": 0.0,
  203. }
  204. if self.validator.internal_id in defaults:
  205. logger.debug(
  206. "Config value was None, so it was reset to"
  207. f" {defaults[self.validator.internal_id]}"
  208. )
  209. value = defaults[self.validator.internal_id]
  210. # This attribute will tell the `Loader` to save this value in db
  211. self._save_marker = True
  212. object.__setattr__(self, key, value)