dispatcher.py 22 KB

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