dispatcher.py 16 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462
  1. """Obviously, dispatches stuff"""
  2. # Friendly Telegram (telegram userbot)
  3. # Copyright (C) 2018-2022 The Authors
  4. # This program is free software: you can redistribute it and/or modify
  5. # it under the terms of the GNU Affero General Public License as published by
  6. # the Free Software Foundation, either version 3 of the License, or
  7. # (at your option) any later version.
  8. # This program is distributed in the hope that it will be useful,
  9. # but WITHOUT ANY WARRANTY; without even the implied warranty of
  10. # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
  11. # GNU Affero General Public License for more details.
  12. # You should have received a copy of the GNU Affero General Public License
  13. # along with this program. If not, see <https://www.gnu.org/licenses/>.
  14. # █ █ ▀ █▄▀ ▄▀█ █▀█ ▀
  15. # █▀█ █ █ █ █▀█ █▀▄ █
  16. # © Copyright 2022
  17. # https://t.me/hikariatama
  18. #
  19. # 🔒 Licensed under the GNU AGPLv3
  20. # 🌐 https://www.gnu.org/licenses/agpl-3.0.html
  21. import asyncio
  22. import collections
  23. import copy
  24. import logging
  25. import re
  26. import traceback
  27. from typing import Tuple, Union
  28. from telethon import types
  29. from telethon.tl.types import Message
  30. from . import main, security, utils
  31. from .database import Database
  32. from .loader import Modules
  33. # Keys for layout switch
  34. ru_keys = 'ёйцукенгшщзхъфывапролджэячсмитьбю.Ё"№;%:?ЙЦУКЕНГШЩЗХЪФЫВАПРОЛДЖЭ/ЯЧСМИТЬБЮ,'
  35. en_keys = "`qwertyuiop[]asdfghjkl;'zxcvbnm,./~@#$%^&QWERTYUIOP{}ASDFGHJKL:\"|ZXCVBNM<>?"
  36. def _decrement_ratelimit(delay, data, key, severity):
  37. def inner():
  38. data[key] = max(0, data[key] - severity)
  39. asyncio.get_event_loop().call_later(delay, inner)
  40. class CommandDispatcher:
  41. def __init__(self, modules: Modules, db: Database, no_nickname: bool = False):
  42. self._modules = modules
  43. self._db = db
  44. self.security = security.SecurityManager(db)
  45. self.no_nickname = no_nickname
  46. self._ratelimit_storage_user = collections.defaultdict(int)
  47. self._ratelimit_storage_chat = collections.defaultdict(int)
  48. self._ratelimit_max_user = db.get(__name__, "ratelimit_max_user", 30)
  49. self._ratelimit_max_chat = db.get(__name__, "ratelimit_max_chat", 100)
  50. self.check_security = self.security.check
  51. async def init(self, client: "TelegramClient"): # type: ignore
  52. await self.security.init(client)
  53. me = await client.get_me()
  54. self.client = client # Intended to be used to track user in logging
  55. self._me = me.id
  56. self._cached_username = me.username.lower() if me.username else str(me.id)
  57. async def _handle_ratelimit(self, message: Message, func: callable) -> bool:
  58. if await self.security.check(
  59. message,
  60. security.OWNER | security.SUDO | security.SUPPORT,
  61. ):
  62. return True
  63. func = getattr(func, "__func__", func)
  64. ret = True
  65. chat = self._ratelimit_storage_chat[message.chat_id]
  66. if message.sender_id:
  67. user = self._ratelimit_storage_user[message.sender_id]
  68. severity = (5 if getattr(func, "ratelimit", False) else 2) * (
  69. (user + chat) // 30 + 1
  70. )
  71. user += severity
  72. self._ratelimit_storage_user[message.sender_id] = user
  73. if user > self._ratelimit_max_user:
  74. ret = False
  75. else:
  76. self._ratelimit_storage_chat[message.chat_id] = chat
  77. _decrement_ratelimit(
  78. self._ratelimit_max_user * severity,
  79. self._ratelimit_storage_user,
  80. message.sender_id,
  81. severity,
  82. )
  83. else:
  84. severity = (5 if getattr(func, "ratelimit", False) else 2) * (
  85. chat // 15 + 1
  86. )
  87. chat += severity
  88. if chat > self._ratelimit_max_chat:
  89. ret = False
  90. _decrement_ratelimit(
  91. self._ratelimit_max_chat * severity,
  92. self._ratelimit_storage_chat,
  93. message.chat_id,
  94. severity,
  95. )
  96. return ret
  97. def _handle_grep(self, message: Message) -> Message:
  98. # Allow escaping grep with double stick
  99. if "||grep" in message.text or "|| grep" in message.text:
  100. message.raw_text = re.sub(r"\|\| ?grep", "| grep", message.raw_text)
  101. message.text = re.sub(r"\|\| ?grep", "| grep", message.text)
  102. message.message = re.sub(r"\|\| ?grep", "| grep", message.message)
  103. return message
  104. grep = False
  105. if not re.search(r".+\| ?grep (.+)", message.raw_text):
  106. return message
  107. grep = re.search(r".+\| ?grep (.+)", message.raw_text).group(1)
  108. message.text = re.sub(r"\| ?grep.+", "", message.text)
  109. message.raw_text = re.sub(r"\| ?grep.+", "", message.raw_text)
  110. message.message = re.sub(r"\| ?grep.+", "", message.message)
  111. ungrep = False
  112. if re.search(r"-v (.+)", grep):
  113. ungrep = re.search(r"-v (.+)", grep).group(1)
  114. grep = re.sub(r"(.+) -v .+", r"\g<1>", grep)
  115. grep = utils.escape_html(grep).strip() if grep else False
  116. ungrep = utils.escape_html(ungrep).strip() if ungrep else False
  117. old_edit = message.edit
  118. old_reply = message.reply
  119. old_respond = message.respond
  120. def process_text(text: str) -> str:
  121. nonlocal grep, ungrep
  122. res = []
  123. for line in text.split("\n"):
  124. if (
  125. grep
  126. and grep in utils.remove_html(line)
  127. and (not ungrep or ungrep not in utils.remove_html(line))
  128. ):
  129. res.append(
  130. utils.remove_html(line, escape=True).replace(
  131. grep, f"<u>{grep}</u>"
  132. )
  133. )
  134. if not grep and ungrep and ungrep not in utils.remove_html(line):
  135. res.append(utils.remove_html(line, escape=True))
  136. cont = (
  137. (f"contain <b>{grep}</b>" if grep else "")
  138. + (" and" if grep and ungrep else "")
  139. + ((" do not contain <b>" + ungrep + "</b>") if ungrep else "")
  140. )
  141. if res:
  142. text = f"<i>💬 Lines that {cont}:</i>\n" + ("\n".join(res))
  143. else:
  144. text = f"💬 <i>No lines that {cont}</i>"
  145. return text
  146. async def my_edit(text, *args, **kwargs):
  147. text = process_text(text)
  148. kwargs["parse_mode"] = "HTML"
  149. return await old_edit(text, *args, **kwargs)
  150. async def my_reply(text, *args, **kwargs):
  151. text = process_text(text)
  152. kwargs["parse_mode"] = "HTML"
  153. return await old_reply(text, *args, **kwargs)
  154. async def my_respond(text, *args, **kwargs):
  155. text = process_text(text)
  156. kwargs["parse_mode"] = "HTML"
  157. return await old_respond(text, *args, **kwargs)
  158. message.edit = my_edit
  159. message.reply = my_reply
  160. message.respond = my_respond
  161. return message
  162. async def _handle_command(
  163. self,
  164. event,
  165. ) -> Union[bool, Tuple[Message, str, str, callable]]:
  166. if not hasattr(event, "message") or not hasattr(event.message, "message"):
  167. return False
  168. if (
  169. len(prefix := self._db.get(main.__name__, "command_prefix", False) or ".")
  170. != 1
  171. ):
  172. prefix = "."
  173. self._db.set(main.__name__, "command_prefix", prefix)
  174. logging.warning("Prefix has been reset to a default one («.»)")
  175. change = str.maketrans(ru_keys + en_keys, en_keys + ru_keys)
  176. message = utils.censor(event.message)
  177. if not event.message.message:
  178. return False
  179. if (
  180. event.message.message.startswith(str.translate(prefix, change))
  181. and str.translate(prefix, change) != prefix
  182. ):
  183. prefix = str.translate(prefix, change)
  184. message.message = str.translate(message.message, change)
  185. elif not event.message.message.startswith(prefix):
  186. return False
  187. if (
  188. event.sticker
  189. or event.dice
  190. or event.audio
  191. or event.via_bot_id
  192. or getattr(event, "reactions", False)
  193. ):
  194. return False
  195. blacklist_chats = self._db.get(main.__name__, "blacklist_chats", [])
  196. whitelist_chats = self._db.get(main.__name__, "whitelist_chats", [])
  197. whitelist_modules = self._db.get(main.__name__, "whitelist_modules", [])
  198. if utils.get_chat_id(message) in blacklist_chats or (
  199. whitelist_chats and utils.get_chat_id(message) not in whitelist_chats
  200. ):
  201. return False
  202. if (
  203. message.out
  204. and len(message.message) > 2
  205. and message.message.startswith(prefix * 2)
  206. ):
  207. # Allow escaping commands using .'s
  208. await message.edit(
  209. message.message[1:],
  210. parse_mode=lambda s: (
  211. s,
  212. utils.relocate_entities(message.entities, -1, message.message)
  213. or (),
  214. ),
  215. )
  216. return False
  217. message.message = message.message[1:]
  218. if not message.message:
  219. return False # Message is just the prefix
  220. utils.relocate_entities(message.entities, -1)
  221. initiator = getattr(event, "sender_id", 0)
  222. command = message.message.split(maxsplit=1)[0]
  223. tag = command.split("@", maxsplit=1)
  224. if len(tag) == 2:
  225. if tag[1] == "me":
  226. if not message.out:
  227. return False
  228. elif tag[1].lower() != self._cached_username:
  229. return False
  230. elif (
  231. event.out
  232. or event.mentioned
  233. and event.message is not None
  234. and event.message.message is not None
  235. and f"@{self._cached_username}" not in event.message.message
  236. ):
  237. pass
  238. elif (
  239. not event.is_private
  240. and not self.no_nickname
  241. and not self._db.get(main.__name__, "no_nickname", False)
  242. and command not in self._db.get(main.__name__, "nonickcmds", [])
  243. and initiator not in self._db.get(main.__name__, "nonickusers", [])
  244. and utils.get_chat_id(event)
  245. not in self._db.get(main.__name__, "nonickchats", [])
  246. ):
  247. return False
  248. txt, func = self._modules.dispatch(tag[0])
  249. if (
  250. not func
  251. or not await self._handle_ratelimit(message, func)
  252. or not await self.security.check(message, func)
  253. ):
  254. return False
  255. if (
  256. message.is_channel
  257. and message.is_group
  258. and message.chat.title.startswith("hikka-")
  259. and message.chat.title != "hikka-logs"
  260. ):
  261. logging.warning("Ignoring message in datachat \\ logging chat")
  262. return False
  263. message.message = txt + message.message[len(command) :]
  264. if (
  265. f"{str(utils.get_chat_id(message))}.{func.__self__.__module__}"
  266. in blacklist_chats
  267. or whitelist_modules
  268. and f"{utils.get_chat_id(message)}.{func.__self__.__module__}"
  269. not in whitelist_modules
  270. ):
  271. return False
  272. if self._db.get(main.__name__, "grep", False):
  273. message = self._handle_grep(message)
  274. return message, prefix, txt, func
  275. async def handle_command(self, event: Message):
  276. """Handle all commands"""
  277. message = await self._handle_command(event)
  278. if not message:
  279. return
  280. message, prefix, _, func = message
  281. asyncio.ensure_future(
  282. self.future_dispatcher(
  283. func,
  284. message,
  285. self.command_exc,
  286. prefix,
  287. )
  288. )
  289. async def command_exc(self, e, message: Message, prefix: str):
  290. logging.exception("Command failed")
  291. if not self._db.get(main.__name__, "inlinelogs", True):
  292. try:
  293. txt = f"<b>🚫 Call</b> <code>{utils.escape_html(prefix)}{utils.escape_html(message.message)}</code><b> failed!</b>"
  294. await (message.edit if message.out else message.reply)(txt)
  295. except Exception:
  296. pass
  297. return
  298. try:
  299. exc = traceback.format_exc()
  300. # Remove `Traceback (most recent call last):`
  301. exc = "\n".join(exc.splitlines()[1:])
  302. txt = (
  303. f"<b>🚫 Call</b> <code>{utils.escape_html(prefix)}{utils.escape_html(message.message)}</code><b> failed!</b>\n\n"
  304. f"<b>🧾 Logs:</b>\n<code>{exc}</code>"
  305. )
  306. await (message.edit if message.out else message.reply)(txt)
  307. except Exception:
  308. pass
  309. async def watcher_exc(self, e, message: Message):
  310. logging.exception("Error running watcher")
  311. async def handle_incoming(self, event):
  312. """Handle all incoming messages"""
  313. message = utils.censor(getattr(event, "message", event))
  314. blacklist_chats = self._db.get(main.__name__, "blacklist_chats", [])
  315. whitelist_chats = self._db.get(main.__name__, "whitelist_chats", [])
  316. whitelist_modules = self._db.get(main.__name__, "whitelist_modules", [])
  317. if utils.get_chat_id(message) in blacklist_chats or (
  318. whitelist_chats and utils.get_chat_id(message) not in whitelist_chats
  319. ):
  320. logging.debug("Message is blacklisted")
  321. return
  322. for func in self._modules.watchers:
  323. bl = self._db.get(main.__name__, "disabled_watchers", {})
  324. modname = str(func.__self__.__class__.strings["name"])
  325. if (
  326. modname in bl
  327. and isinstance(message, types.Message)
  328. and (
  329. "*" in bl[modname]
  330. or utils.get_chat_id(message) in bl[modname]
  331. or "only_chats" in bl[modname]
  332. and message.is_private
  333. or "only_pm" in bl[modname]
  334. and not message.is_private
  335. or "out" in bl[modname]
  336. and not message.out
  337. or "in" in bl[modname]
  338. and message.out
  339. )
  340. or f"{str(utils.get_chat_id(message))}.{func.__self__.__module__}"
  341. in blacklist_chats
  342. or (
  343. whitelist_modules
  344. and (
  345. f"{str(utils.get_chat_id(message))}." + func.__self__.__module__
  346. )
  347. not in whitelist_modules
  348. )
  349. ):
  350. logging.debug(f"Ignored watcher of module {modname}")
  351. continue
  352. # Avoid weird AttributeErrors in weird dochub modules by settings placeholder
  353. # of attributes
  354. for placeholder in {"text", "raw_text"}:
  355. try:
  356. if not hasattr(message, placeholder):
  357. setattr(message, placeholder, "")
  358. except UnicodeDecodeError:
  359. pass
  360. # Run watcher via ensure_future so in case user has a lot
  361. # of watchers with long actions, they can run simultaneously
  362. asyncio.ensure_future(
  363. self.future_dispatcher(
  364. func,
  365. message,
  366. self.watcher_exc,
  367. )
  368. )
  369. async def future_dispatcher(
  370. self,
  371. func: callable,
  372. message: Message,
  373. exception_handler: callable,
  374. *args,
  375. ):
  376. # Will be used to determine, which client caused logging messages
  377. # parsed via inspect.stack()
  378. _hikka_client_id_logging_tag = copy.copy(self.client._tg_id) # skipcq
  379. try:
  380. await func(message)
  381. except BaseException as e:
  382. await exception_handler(e, message, *args)