dispatcher.py 24 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689
  1. """Processes incoming events and dispatches them to appropriate handlers"""
  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. # ©️ Dan Gazizullin, 2021-2022
  15. # This file is a part of Hikka Userbot
  16. # 🌐 https://github.com/hikariatama/Hikka
  17. # You can redistribute it and/or modify it under the terms of the GNU AGPLv3
  18. # 🔑 https://www.gnu.org/licenses/agpl-3.0.html
  19. import asyncio
  20. import collections
  21. import contextlib
  22. import copy
  23. import inspect
  24. import logging
  25. import re
  26. import sys
  27. import traceback
  28. import typing
  29. from telethon import events
  30. from telethon.errors import FloodWaitError, RPCError
  31. from telethon.tl.types import Message
  32. from . import main, security, utils
  33. from .database import Database
  34. from .loader import Modules
  35. from .tl_cache import CustomTelegramClient
  36. logger = logging.getLogger(__name__)
  37. # Keys for layout switch
  38. ru_keys = 'ёйцукенгшщзхъфывапролджэячсмитьбю.Ё"№;%:?ЙЦУКЕНГШЩЗХЪФЫВАПРОЛДЖЭ/ЯЧСМИТЬБЮ,'
  39. en_keys = "`qwertyuiop[]asdfghjkl;'zxcvbnm,./~@#$%^&QWERTYUIOP{}ASDFGHJKL:\"|ZXCVBNM<>?"
  40. ALL_TAGS = [
  41. "no_commands",
  42. "only_commands",
  43. "out",
  44. "in",
  45. "only_messages",
  46. "editable",
  47. "no_media",
  48. "only_media",
  49. "only_photos",
  50. "only_videos",
  51. "only_audios",
  52. "only_stickers",
  53. "only_docs",
  54. "only_inline",
  55. "only_channels",
  56. "only_groups",
  57. "only_pm",
  58. "startswith",
  59. "endswith",
  60. "contains",
  61. "filter",
  62. "from_id",
  63. "chat_id",
  64. "regex",
  65. ]
  66. def _decrement_ratelimit(delay, data, key, severity):
  67. def inner():
  68. data[key] = max(0, data[key] - severity)
  69. asyncio.get_event_loop().call_later(delay, inner)
  70. class CommandDispatcher:
  71. def __init__(
  72. self,
  73. modules: Modules,
  74. client: CustomTelegramClient,
  75. db: Database,
  76. ):
  77. self._modules = modules
  78. self._client = client
  79. self.client = client
  80. self._db = db
  81. self._ratelimit_storage_user = collections.defaultdict(int)
  82. self._ratelimit_storage_chat = collections.defaultdict(int)
  83. self._ratelimit_max_user = db.get(__name__, "ratelimit_max_user", 30)
  84. self._ratelimit_max_chat = db.get(__name__, "ratelimit_max_chat", 100)
  85. self.security = security.SecurityManager(client, db)
  86. self.check_security = self.security.check
  87. self._me = self._client.hikka_me.id
  88. self._cached_usernames = [
  89. (
  90. self._client.hikka_me.username.lower()
  91. if self._client.hikka_me.username
  92. else str(self._client.hikka_me.id)
  93. )
  94. ]
  95. self._cached_usernames.extend(
  96. getattr(self._client.hikka_me, "usernames", None) or []
  97. )
  98. self.raw_handlers = []
  99. async def _handle_ratelimit(self, message: Message, func: callable) -> bool:
  100. if await self.security.check(
  101. message,
  102. security.OWNER | security.SUDO | security.SUPPORT,
  103. ):
  104. return True
  105. func = getattr(func, "__func__", func)
  106. ret = True
  107. chat = self._ratelimit_storage_chat[message.chat_id]
  108. if message.sender_id:
  109. user = self._ratelimit_storage_user[message.sender_id]
  110. severity = (5 if getattr(func, "ratelimit", False) else 2) * (
  111. (user + chat) // 30 + 1
  112. )
  113. user += severity
  114. self._ratelimit_storage_user[message.sender_id] = user
  115. if user > self._ratelimit_max_user:
  116. ret = False
  117. else:
  118. self._ratelimit_storage_chat[message.chat_id] = chat
  119. _decrement_ratelimit(
  120. self._ratelimit_max_user * severity,
  121. self._ratelimit_storage_user,
  122. message.sender_id,
  123. severity,
  124. )
  125. else:
  126. severity = (5 if getattr(func, "ratelimit", False) else 2) * (
  127. chat // 15 + 1
  128. )
  129. chat += severity
  130. if chat > self._ratelimit_max_chat:
  131. ret = False
  132. _decrement_ratelimit(
  133. self._ratelimit_max_chat * severity,
  134. self._ratelimit_storage_chat,
  135. message.chat_id,
  136. severity,
  137. )
  138. return ret
  139. def _handle_grep(self, message: Message) -> Message:
  140. # Allow escaping grep with double stick
  141. if "||grep" in message.text or "|| grep" in message.text:
  142. message.raw_text = re.sub(r"\|\| ?grep", "| grep", message.raw_text)
  143. message.text = re.sub(r"\|\| ?grep", "| grep", message.text)
  144. message.message = re.sub(r"\|\| ?grep", "| grep", message.message)
  145. return message
  146. grep = False
  147. if not re.search(r".+\| ?grep (.+)", message.raw_text):
  148. return message
  149. grep = re.search(r".+\| ?grep (.+)", message.raw_text).group(1)
  150. message.text = re.sub(r"\| ?grep.+", "", message.text)
  151. message.raw_text = re.sub(r"\| ?grep.+", "", message.raw_text)
  152. message.message = re.sub(r"\| ?grep.+", "", message.message)
  153. ungrep = False
  154. if re.search(r"-v (.+)", grep):
  155. ungrep = re.search(r"-v (.+)", grep).group(1)
  156. grep = re.sub(r"(.+) -v .+", r"\g<1>", grep)
  157. grep = utils.escape_html(grep).strip() if grep else False
  158. ungrep = utils.escape_html(ungrep).strip() if ungrep else False
  159. old_edit = message.edit
  160. old_reply = message.reply
  161. old_respond = message.respond
  162. def process_text(text: str) -> str:
  163. nonlocal grep, ungrep
  164. res = []
  165. for line in text.split("\n"):
  166. if (
  167. grep
  168. and grep in utils.remove_html(line)
  169. and (not ungrep or ungrep not in utils.remove_html(line))
  170. ):
  171. res.append(
  172. utils.remove_html(line, escape=True).replace(
  173. grep, f"<u>{grep}</u>"
  174. )
  175. )
  176. if not grep and ungrep and ungrep not in utils.remove_html(line):
  177. res.append(utils.remove_html(line, escape=True))
  178. cont = (
  179. (f"contain <b>{grep}</b>" if grep else "")
  180. + (" and" if grep and ungrep else "")
  181. + ((" do not contain <b>" + ungrep + "</b>") if ungrep else "")
  182. )
  183. if res:
  184. text = f"<i>💬 Lines that {cont}:</i>\n" + "\n".join(res)
  185. else:
  186. text = f"💬 <i>No lines that {cont}</i>"
  187. return text
  188. async def my_edit(text, *args, **kwargs):
  189. text = process_text(text)
  190. kwargs["parse_mode"] = "HTML"
  191. return await old_edit(text, *args, **kwargs)
  192. async def my_reply(text, *args, **kwargs):
  193. text = process_text(text)
  194. kwargs["parse_mode"] = "HTML"
  195. return await old_reply(text, *args, **kwargs)
  196. async def my_respond(text, *args, **kwargs):
  197. text = process_text(text)
  198. kwargs["parse_mode"] = "HTML"
  199. kwargs.setdefault("reply_to", utils.get_topic(message))
  200. return await old_respond(text, *args, **kwargs)
  201. message.edit = my_edit
  202. message.reply = my_reply
  203. message.respond = my_respond
  204. message.hikka_grepped = True
  205. return message
  206. async def _handle_command(
  207. self,
  208. event: typing.Union[events.NewMessage, events.MessageDeleted],
  209. watcher: bool = False,
  210. ) -> typing.Union[bool, typing.Tuple[Message, str, str, callable]]:
  211. if not hasattr(event, "message") or not hasattr(event.message, "message"):
  212. return False
  213. prefix = self._db.get(main.__name__, "command_prefix", False) or "."
  214. change = str.maketrans(ru_keys + en_keys, en_keys + ru_keys)
  215. message = utils.censor(event.message)
  216. if not event.message.message:
  217. return False
  218. if (
  219. message.out
  220. and len(message.message) > 2
  221. and (
  222. message.message.startswith(prefix * 2)
  223. and any(s != prefix for s in message.message)
  224. or message.message.startswith(str.translate(prefix * 2, change))
  225. and any(s != str.translate(prefix, change) for s in message.message)
  226. )
  227. ):
  228. # Allow escaping commands using .'s
  229. if not watcher:
  230. await message.edit(
  231. message.message[1:],
  232. parse_mode=lambda s: (
  233. s,
  234. utils.relocate_entities(message.entities, -1, message.message)
  235. or (),
  236. ),
  237. )
  238. return False
  239. if (
  240. event.message.message.startswith(str.translate(prefix, change))
  241. and str.translate(prefix, change) != prefix
  242. ):
  243. message.message = str.translate(message.message, change)
  244. message.text = str.translate(message.text, change)
  245. elif not event.message.message.startswith(prefix):
  246. return False
  247. if (
  248. event.sticker
  249. or event.dice
  250. or event.audio
  251. or event.via_bot_id
  252. or getattr(event, "reactions", False)
  253. ):
  254. return False
  255. blacklist_chats = self._db.get(main.__name__, "blacklist_chats", [])
  256. whitelist_chats = self._db.get(main.__name__, "whitelist_chats", [])
  257. whitelist_modules = self._db.get(main.__name__, "whitelist_modules", [])
  258. if utils.get_chat_id(message) in blacklist_chats or (
  259. whitelist_chats and utils.get_chat_id(message) not in whitelist_chats
  260. ):
  261. return False
  262. if not message.message or len(message.message) == 1:
  263. return False # Message is just the prefix
  264. initiator = getattr(event, "sender_id", 0)
  265. command = message.message[1:].strip().split(maxsplit=1)[0]
  266. tag = command.split("@", maxsplit=1)
  267. if len(tag) == 2:
  268. if tag[1] == "me":
  269. if not message.out:
  270. return False
  271. elif tag[1].lower() not in self._cached_usernames:
  272. return False
  273. elif (
  274. event.out
  275. or event.mentioned
  276. and event.message is not None
  277. and event.message.message is not None
  278. and not any(
  279. f"@{username}" not in command.lower()
  280. for username in self._cached_usernames
  281. )
  282. ):
  283. pass
  284. elif (
  285. not event.is_private
  286. and not self._db.get(main.__name__, "no_nickname", False)
  287. and command not in self._db.get(main.__name__, "nonickcmds", [])
  288. and initiator not in self._db.get(main.__name__, "nonickusers", [])
  289. and utils.get_chat_id(event)
  290. not in self._db.get(main.__name__, "nonickchats", [])
  291. ):
  292. return False
  293. txt, func = self._modules.dispatch(tag[0])
  294. if (
  295. not func
  296. or not await self._handle_ratelimit(message, func)
  297. or not await self.security.check(message, func)
  298. ):
  299. return False
  300. if (
  301. message.is_channel
  302. and message.is_group
  303. and message.chat.title.startswith("hikka-")
  304. and message.chat.title != "hikka-logs"
  305. ):
  306. if not watcher:
  307. logger.warning("Ignoring message in datachat \\ logging chat")
  308. return False
  309. message.message = prefix + txt + message.message[len(prefix + command) :]
  310. if (
  311. f"{str(utils.get_chat_id(message))}.{func.__self__.__module__}"
  312. in blacklist_chats
  313. or whitelist_modules
  314. and f"{utils.get_chat_id(message)}.{func.__self__.__module__}"
  315. not in whitelist_modules
  316. ):
  317. return False
  318. if await self._handle_tags(event, func):
  319. return False
  320. if self._db.get(main.__name__, "grep", False) and not watcher:
  321. message = self._handle_grep(message)
  322. return message, prefix, txt, func
  323. async def handle_raw(self, event: events.Raw):
  324. """Handle raw events."""
  325. for handler in self.raw_handlers:
  326. if isinstance(event, tuple(handler.updates)):
  327. try:
  328. await handler(event)
  329. except Exception as e:
  330. logger.exception("Error in raw handler %s: %s", handler.id, e)
  331. async def handle_command(
  332. self,
  333. event: typing.Union[events.NewMessage, events.MessageDeleted],
  334. ):
  335. """Handle all commands"""
  336. message = await self._handle_command(event)
  337. if not message:
  338. return
  339. message, _, _, func = message
  340. asyncio.ensure_future(
  341. self.future_dispatcher(
  342. func,
  343. message,
  344. self.command_exc,
  345. )
  346. )
  347. async def command_exc(self, _, message: Message):
  348. """Handle command exceptions."""
  349. exc = sys.exc_info()[1]
  350. logger.exception("Command failed", extra={"stack": inspect.stack()})
  351. if isinstance(exc, RPCError):
  352. if isinstance(exc, FloodWaitError):
  353. hours = exc.seconds // 3600
  354. minutes = (exc.seconds % 3600) // 60
  355. seconds = exc.seconds % 60
  356. hours = f"{hours} hours, " if hours else ""
  357. minutes = f"{minutes} minutes, " if minutes else ""
  358. seconds = f"{seconds} seconds" if seconds else ""
  359. fw_time = f"{hours}{minutes}{seconds}"
  360. txt = (
  361. self._client.loader.lookup("translations")
  362. .strings("fw_error")
  363. .format(
  364. utils.escape_html(message.message),
  365. fw_time,
  366. type(exc.request).__name__,
  367. )
  368. )
  369. else:
  370. txt = (
  371. "<emoji document_id=5877477244938489129>🚫</emoji> <b>Call"
  372. f" </b><code>{utils.escape_html(message.message)}</code><b> failed"
  373. " due to RPC (Telegram) error:</b>"
  374. f" <code>{utils.escape_html(str(exc))}</code>"
  375. )
  376. txt = (
  377. self._client.loader.lookup("translations")
  378. .strings("rpc_error")
  379. .format(
  380. utils.escape_html(message.message),
  381. utils.escape_html(str(exc)),
  382. )
  383. )
  384. else:
  385. if not self._db.get(main.__name__, "inlinelogs", True):
  386. txt = (
  387. "<emoji document_id=5877477244938489129>🚫</emoji><b> Call</b>"
  388. f" <code>{utils.escape_html(message.message)}</code><b>"
  389. " failed!</b>"
  390. )
  391. else:
  392. exc = "\n".join(traceback.format_exc().splitlines()[1:])
  393. txt = (
  394. "<emoji document_id=5877477244938489129>🚫</emoji><b> Call</b>"
  395. f" <code>{utils.escape_html(message.message)}</code><b>"
  396. " failed!</b>\n\n<b>🧾"
  397. f" Logs:</b>\n<code>{utils.escape_html(exc)}</code>"
  398. )
  399. with contextlib.suppress(Exception):
  400. await (message.edit if message.out else message.reply)(txt)
  401. async def watcher_exc(self, *_):
  402. logger.exception("Error running watcher", extra={"stack": inspect.stack()})
  403. async def _handle_tags(
  404. self,
  405. event: typing.Union[events.NewMessage, events.MessageDeleted],
  406. func: callable,
  407. ) -> bool:
  408. message = getattr(event, "message", event)
  409. return (
  410. (
  411. getattr(func, "no_commands", False)
  412. and await self._handle_command(event, watcher=True)
  413. )
  414. or (
  415. getattr(func, "only_commands", False)
  416. and not await self._handle_command(event, watcher=True)
  417. )
  418. or (getattr(func, "out", False) and not getattr(message, "out", True))
  419. or (getattr(func, "in", False) and getattr(message, "out", True))
  420. or (
  421. getattr(func, "only_messages", False)
  422. and not isinstance(message, Message)
  423. )
  424. or (
  425. getattr(func, "editable", False)
  426. and (
  427. getattr(message, "fwd_from", False)
  428. or not getattr(message, "out", False)
  429. or getattr(message, "sticker", False)
  430. or getattr(message, "via_bot_id", False)
  431. )
  432. )
  433. or (
  434. getattr(func, "no_media", False)
  435. and isinstance(message, Message)
  436. and getattr(message, "media", False)
  437. )
  438. or (
  439. getattr(func, "only_media", False)
  440. and (
  441. not isinstance(message, Message)
  442. or not getattr(message, "media", False)
  443. )
  444. )
  445. or (
  446. getattr(func, "only_photos", False)
  447. and not utils.mime_type(message).startswith("image/")
  448. )
  449. or (
  450. getattr(func, "only_videos", False)
  451. and not utils.mime_type(message).startswith("video/")
  452. )
  453. or (
  454. getattr(func, "only_audios", False)
  455. and not utils.mime_type(message).startswith("audio/")
  456. )
  457. or (
  458. getattr(func, "only_stickers", False)
  459. and not getattr(message, "sticker", False)
  460. )
  461. or (
  462. getattr(func, "only_docs", False)
  463. and not getattr(message, "document", False)
  464. )
  465. or (
  466. getattr(func, "only_inline", False)
  467. and not getattr(message, "via_bot_id", False)
  468. )
  469. or (
  470. getattr(func, "only_channels", False)
  471. and (
  472. not getattr(message, "is_channel", False)
  473. and getattr(message, "is_group", False)
  474. or getattr(message, "is_private", False)
  475. )
  476. )
  477. or (
  478. getattr(func, "only_groups", False)
  479. and not getattr(message, "is_group", False)
  480. )
  481. or (
  482. getattr(func, "only_pm", False)
  483. and not getattr(message, "is_private", False)
  484. )
  485. or (
  486. getattr(func, "startswith", False)
  487. and (
  488. not isinstance(message, Message)
  489. or isinstance(func.startswith, str)
  490. and not message.raw_text.startswith(func.startswith)
  491. )
  492. )
  493. or (
  494. getattr(func, "endswith", False)
  495. and (
  496. not isinstance(message, Message)
  497. or isinstance(func.endswith, str)
  498. and not message.raw_text.endswith(func.endswith)
  499. )
  500. )
  501. or (
  502. getattr(func, "contains", False)
  503. and (
  504. not isinstance(message, Message)
  505. or isinstance(func.contains, str)
  506. and func.contains not in message.raw_text
  507. )
  508. )
  509. or (
  510. getattr(func, "filter", False)
  511. and callable(func.filter)
  512. and not func.filter(message)
  513. )
  514. or (
  515. getattr(func, "from_id", False)
  516. and getattr(message, "sender_id", None) != func.from_id
  517. )
  518. or (
  519. getattr(func, "chat_id", False)
  520. and utils.get_chat_id(message)
  521. != (
  522. func.chat_id
  523. if not str(func.chat_id).startswith("-100")
  524. else int(str(func.chat_id)[4:])
  525. )
  526. )
  527. or (
  528. getattr(func, "regex", False)
  529. and (
  530. not isinstance(message, Message)
  531. or not re.search(func.regex, message.raw_text)
  532. )
  533. )
  534. )
  535. async def handle_incoming(
  536. self,
  537. event: typing.Union[events.NewMessage, events.MessageDeleted],
  538. ):
  539. """Handle all incoming messages"""
  540. message = utils.censor(getattr(event, "message", event))
  541. blacklist_chats = self._db.get(main.__name__, "blacklist_chats", [])
  542. whitelist_chats = self._db.get(main.__name__, "whitelist_chats", [])
  543. whitelist_modules = self._db.get(main.__name__, "whitelist_modules", [])
  544. if utils.get_chat_id(message) in blacklist_chats or (
  545. whitelist_chats and utils.get_chat_id(message) not in whitelist_chats
  546. ):
  547. logger.debug("Message is blacklisted")
  548. return
  549. for func in self._modules.watchers:
  550. bl = self._db.get(main.__name__, "disabled_watchers", {})
  551. modname = str(func.__self__.__class__.strings["name"])
  552. if (
  553. modname in bl
  554. and isinstance(message, Message)
  555. and (
  556. "*" in bl[modname]
  557. or utils.get_chat_id(message) in bl[modname]
  558. or "only_chats" in bl[modname]
  559. and message.is_private
  560. or "only_pm" in bl[modname]
  561. and not message.is_private
  562. or "out" in bl[modname]
  563. and not message.out
  564. or "in" in bl[modname]
  565. and message.out
  566. )
  567. or f"{str(utils.get_chat_id(message))}.{func.__self__.__module__}"
  568. in blacklist_chats
  569. or whitelist_modules
  570. and f"{str(utils.get_chat_id(message))}.{func.__self__.__module__}"
  571. not in whitelist_modules
  572. or await self._handle_tags(event, func)
  573. ):
  574. logger.debug("Ignored watcher of module %s", modname)
  575. continue
  576. # Avoid weird AttributeErrors in weird dochub modules by settings placeholder
  577. # of attributes
  578. for placeholder in {"text", "raw_text", "out"}:
  579. try:
  580. if not hasattr(message, placeholder):
  581. setattr(message, placeholder, "")
  582. except UnicodeDecodeError:
  583. pass
  584. # Run watcher via ensure_future so in case user has a lot
  585. # of watchers with long actions, they can run simultaneously
  586. asyncio.ensure_future(
  587. self.future_dispatcher(
  588. func,
  589. message,
  590. self.watcher_exc,
  591. )
  592. )
  593. async def future_dispatcher(
  594. self,
  595. func: callable,
  596. message: Message,
  597. exception_handler: callable,
  598. *args,
  599. ):
  600. # Will be used to determine, which client caused logging messages
  601. # parsed via inspect.stack()
  602. _hikka_client_id_logging_tag = copy.copy(self.client.tg_id) # skipcq
  603. try:
  604. await func(message)
  605. except Exception as e:
  606. await exception_handler(e, message, *args)