dispatcher.py 22 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634
  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. async def _handle_ratelimit(self, message: Message, func: callable) -> bool:
  93. if await self.security.check(
  94. message,
  95. security.OWNER | security.SUDO | security.SUPPORT,
  96. ):
  97. return True
  98. func = getattr(func, "__func__", func)
  99. ret = True
  100. chat = self._ratelimit_storage_chat[message.chat_id]
  101. if message.sender_id:
  102. user = self._ratelimit_storage_user[message.sender_id]
  103. severity = (5 if getattr(func, "ratelimit", False) else 2) * (
  104. (user + chat) // 30 + 1
  105. )
  106. user += severity
  107. self._ratelimit_storage_user[message.sender_id] = user
  108. if user > self._ratelimit_max_user:
  109. ret = False
  110. else:
  111. self._ratelimit_storage_chat[message.chat_id] = chat
  112. _decrement_ratelimit(
  113. self._ratelimit_max_user * severity,
  114. self._ratelimit_storage_user,
  115. message.sender_id,
  116. severity,
  117. )
  118. else:
  119. severity = (5 if getattr(func, "ratelimit", False) else 2) * (
  120. chat // 15 + 1
  121. )
  122. chat += severity
  123. if chat > self._ratelimit_max_chat:
  124. ret = False
  125. _decrement_ratelimit(
  126. self._ratelimit_max_chat * severity,
  127. self._ratelimit_storage_chat,
  128. message.chat_id,
  129. severity,
  130. )
  131. return ret
  132. def _handle_grep(self, message: Message) -> Message:
  133. # Allow escaping grep with double stick
  134. if "||grep" in message.text or "|| grep" in message.text:
  135. message.raw_text = re.sub(r"\|\| ?grep", "| grep", message.raw_text)
  136. message.text = re.sub(r"\|\| ?grep", "| grep", message.text)
  137. message.message = re.sub(r"\|\| ?grep", "| grep", message.message)
  138. return message
  139. grep = False
  140. if not re.search(r".+\| ?grep (.+)", message.raw_text):
  141. return message
  142. grep = re.search(r".+\| ?grep (.+)", message.raw_text).group(1)
  143. message.text = re.sub(r"\| ?grep.+", "", message.text)
  144. message.raw_text = re.sub(r"\| ?grep.+", "", message.raw_text)
  145. message.message = re.sub(r"\| ?grep.+", "", message.message)
  146. ungrep = False
  147. if re.search(r"-v (.+)", grep):
  148. ungrep = re.search(r"-v (.+)", grep).group(1)
  149. grep = re.sub(r"(.+) -v .+", r"\g<1>", grep)
  150. grep = utils.escape_html(grep).strip() if grep else False
  151. ungrep = utils.escape_html(ungrep).strip() if ungrep else False
  152. old_edit = message.edit
  153. old_reply = message.reply
  154. old_respond = message.respond
  155. def process_text(text: str) -> str:
  156. nonlocal grep, ungrep
  157. res = []
  158. for line in text.split("\n"):
  159. if (
  160. grep
  161. and grep in utils.remove_html(line)
  162. and (not ungrep or ungrep not in utils.remove_html(line))
  163. ):
  164. res.append(
  165. utils.remove_html(line, escape=True).replace(
  166. grep, f"<u>{grep}</u>"
  167. )
  168. )
  169. if not grep and ungrep and ungrep not in utils.remove_html(line):
  170. res.append(utils.remove_html(line, escape=True))
  171. cont = (
  172. (f"contain <b>{grep}</b>" if grep else "")
  173. + (" and" if grep and ungrep else "")
  174. + ((" do not contain <b>" + ungrep + "</b>") if ungrep else "")
  175. )
  176. if res:
  177. text = f"<i>💬 Lines that {cont}:</i>\n" + "\n".join(res)
  178. else:
  179. text = f"💬 <i>No lines that {cont}</i>"
  180. return text
  181. async def my_edit(text, *args, **kwargs):
  182. text = process_text(text)
  183. kwargs["parse_mode"] = "HTML"
  184. return await old_edit(text, *args, **kwargs)
  185. async def my_reply(text, *args, **kwargs):
  186. text = process_text(text)
  187. kwargs["parse_mode"] = "HTML"
  188. return await old_reply(text, *args, **kwargs)
  189. async def my_respond(text, *args, **kwargs):
  190. text = process_text(text)
  191. kwargs["parse_mode"] = "HTML"
  192. return await old_respond(text, *args, **kwargs)
  193. message.edit = my_edit
  194. message.reply = my_reply
  195. message.respond = my_respond
  196. message.hikka_grepped = True
  197. return message
  198. async def _handle_command(
  199. self,
  200. event: typing.Union[events.NewMessage, events.MessageDeleted],
  201. watcher: bool = False,
  202. ) -> typing.Union[bool, typing.Tuple[Message, str, str, callable]]:
  203. if not hasattr(event, "message") or not hasattr(event.message, "message"):
  204. return False
  205. prefix = self._db.get(main.__name__, "command_prefix", False) or "."
  206. change = str.maketrans(ru_keys + en_keys, en_keys + ru_keys)
  207. message = utils.censor(event.message)
  208. if not event.message.message:
  209. return False
  210. if (
  211. event.message.message.startswith(str.translate(prefix, change))
  212. and str.translate(prefix, change) != prefix
  213. ):
  214. message.message = str.translate(message.message, change)
  215. message.text = str.translate(message.text, change)
  216. elif not event.message.message.startswith(prefix):
  217. return False
  218. if (
  219. event.sticker
  220. or event.dice
  221. or event.audio
  222. or event.via_bot_id
  223. or getattr(event, "reactions", False)
  224. ):
  225. return False
  226. blacklist_chats = self._db.get(main.__name__, "blacklist_chats", [])
  227. whitelist_chats = self._db.get(main.__name__, "whitelist_chats", [])
  228. whitelist_modules = self._db.get(main.__name__, "whitelist_modules", [])
  229. if utils.get_chat_id(message) in blacklist_chats or (
  230. whitelist_chats and utils.get_chat_id(message) not in whitelist_chats
  231. ):
  232. return False
  233. if (
  234. message.out
  235. and len(message.message) > 2
  236. and message.message.startswith(prefix * 2)
  237. and any(s != prefix for s in message.message)
  238. ):
  239. # Allow escaping commands using .'s
  240. if not watcher:
  241. await message.edit(
  242. message.message[1:],
  243. parse_mode=lambda s: (
  244. s,
  245. utils.relocate_entities(message.entities, -1, message.message)
  246. or (),
  247. ),
  248. )
  249. return False
  250. if not message.message or len(message.message) == 1:
  251. return False # Message is just the prefix
  252. initiator = getattr(event, "sender_id", 0)
  253. command = message.message[1:].strip().split(maxsplit=1)[0]
  254. tag = command.split("@", maxsplit=1)
  255. if len(tag) == 2:
  256. if tag[1] == "me":
  257. if not message.out:
  258. return False
  259. elif tag[1].lower() != self._cached_username:
  260. return False
  261. elif (
  262. event.out
  263. or event.mentioned
  264. and event.message is not None
  265. and event.message.message is not None
  266. and f"@{self._cached_username}" not in command.lower()
  267. ):
  268. pass
  269. elif (
  270. not event.is_private
  271. and not self._db.get(main.__name__, "no_nickname", False)
  272. and command not in self._db.get(main.__name__, "nonickcmds", [])
  273. and initiator not in self._db.get(main.__name__, "nonickusers", [])
  274. and utils.get_chat_id(event)
  275. not in self._db.get(main.__name__, "nonickchats", [])
  276. ):
  277. return False
  278. txt, func = self._modules.dispatch(tag[0])
  279. if (
  280. not func
  281. or not await self._handle_ratelimit(message, func)
  282. or not await self.security.check(message, func)
  283. ):
  284. return False
  285. if (
  286. message.is_channel
  287. and message.is_group
  288. and message.chat.title.startswith("hikka-")
  289. and message.chat.title != "hikka-logs"
  290. ):
  291. if not watcher:
  292. logger.warning("Ignoring message in datachat \\ logging chat")
  293. return False
  294. message.message = prefix + txt + message.message[len(prefix + command) :]
  295. if (
  296. f"{str(utils.get_chat_id(message))}.{func.__self__.__module__}"
  297. in blacklist_chats
  298. or whitelist_modules
  299. and f"{utils.get_chat_id(message)}.{func.__self__.__module__}"
  300. not in whitelist_modules
  301. ):
  302. return False
  303. if await self._handle_tags(event, func):
  304. return False
  305. if self._db.get(main.__name__, "grep", False) and not watcher:
  306. message = self._handle_grep(message)
  307. return message, prefix, txt, func
  308. async def handle_command(
  309. self,
  310. event: typing.Union[events.NewMessage, events.MessageDeleted],
  311. ):
  312. """Handle all commands"""
  313. message = await self._handle_command(event)
  314. if not message:
  315. return
  316. message, _, _, func = message
  317. asyncio.ensure_future(
  318. self.future_dispatcher(
  319. func,
  320. message,
  321. self.command_exc,
  322. )
  323. )
  324. async def command_exc(self, _, message: Message):
  325. logger.exception("Command failed", extra={"stack": inspect.stack()})
  326. if not self._db.get(main.__name__, "inlinelogs", True):
  327. try:
  328. txt = (
  329. "<b>🚫 Call</b>"
  330. f" <code>{utils.escape_html(message.message)}</code><b>"
  331. " failed!</b>"
  332. )
  333. await (message.edit if message.out else message.reply)(txt)
  334. except Exception:
  335. pass
  336. return
  337. try:
  338. exc = traceback.format_exc()
  339. # Remove `Traceback (most recent call last):`
  340. exc = "\n".join(exc.splitlines()[1:])
  341. txt = (
  342. "<b>🚫 Call</b>"
  343. f" <code>{utils.escape_html(message.message)}</code><b>"
  344. f" failed!</b>\n\n<b>🧾 Logs:</b>\n<code>{utils.escape_html(exc)}</code>"
  345. )
  346. await (message.edit if message.out else message.reply)(txt)
  347. except Exception:
  348. pass
  349. async def watcher_exc(self, *_):
  350. logger.exception("Error running watcher", extra={"stack": inspect.stack()})
  351. async def _handle_tags(
  352. self,
  353. event: typing.Union[events.NewMessage, events.MessageDeleted],
  354. func: callable,
  355. ) -> bool:
  356. message = getattr(event, "message", event)
  357. return (
  358. (
  359. getattr(func, "no_commands", False)
  360. and await self._handle_command(event, watcher=True)
  361. )
  362. or (
  363. getattr(func, "only_commands", False)
  364. and not await self._handle_command(event, watcher=True)
  365. )
  366. or (getattr(func, "out", False) and not getattr(message, "out", True))
  367. or (getattr(func, "in", False) and getattr(message, "out", True))
  368. or (
  369. getattr(func, "only_messages", False)
  370. and not isinstance(message, Message)
  371. )
  372. or (
  373. getattr(func, "editable", False)
  374. and (
  375. getattr(message, "fwd_from", False)
  376. or not getattr(message, "out", False)
  377. or getattr(message, "sticker", False)
  378. or getattr(message, "via_bot_id", False)
  379. )
  380. )
  381. or (
  382. getattr(func, "no_media", False)
  383. and isinstance(message, Message)
  384. and getattr(message, "media", False)
  385. )
  386. or (
  387. getattr(func, "only_media", False)
  388. and (
  389. not isinstance(message, Message)
  390. or not getattr(message, "media", False)
  391. )
  392. )
  393. or (
  394. getattr(func, "only_photos", False)
  395. and not utils.mime_type(message).startswith("image/")
  396. )
  397. or (
  398. getattr(func, "only_videos", False)
  399. and not utils.mime_type(message).startswith("video/")
  400. )
  401. or (
  402. getattr(func, "only_audios", False)
  403. and not utils.mime_type(message).startswith("audio/")
  404. )
  405. or (
  406. getattr(func, "only_stickers", False)
  407. and not getattr(message, "sticker", False)
  408. )
  409. or (
  410. getattr(func, "only_docs", False)
  411. and not getattr(message, "document", False)
  412. )
  413. or (
  414. getattr(func, "only_inline", False)
  415. and not getattr(message, "via_bot_id", False)
  416. )
  417. or (
  418. getattr(func, "only_channels", False)
  419. and (
  420. not getattr(message, "is_channel", False)
  421. and getattr(message, "is_group", False)
  422. or getattr(message, "is_private", False)
  423. )
  424. )
  425. or (
  426. getattr(func, "only_groups", False)
  427. and not getattr(message, "is_group", False)
  428. )
  429. or (
  430. getattr(func, "only_pm", False)
  431. and not getattr(message, "is_private", False)
  432. )
  433. or (
  434. getattr(func, "startswith", False)
  435. and (
  436. not isinstance(message, Message)
  437. or isinstance(func.startswith, str)
  438. and not message.raw_text.startswith(func.startswith)
  439. )
  440. )
  441. or (
  442. getattr(func, "endswith", False)
  443. and (
  444. not isinstance(message, Message)
  445. or isinstance(func.endswith, str)
  446. and not message.raw_text.endswith(func.endswith)
  447. )
  448. )
  449. or (
  450. getattr(func, "contains", False)
  451. and (
  452. not isinstance(message, Message)
  453. or isinstance(func.contains, str)
  454. and func.contains not in message.raw_text
  455. )
  456. )
  457. or (
  458. getattr(func, "filter", False)
  459. and callable(func.filter)
  460. and not func.filter(message)
  461. )
  462. or (
  463. getattr(func, "from_id", False)
  464. and getattr(message, "sender_id", None) != func.from_id
  465. )
  466. or (
  467. getattr(func, "chat_id", False)
  468. and utils.get_chat_id(message)
  469. != (
  470. func.chat_id
  471. if not str(func.chat_id).startswith("-100")
  472. else int(str(func.chat_id)[4:])
  473. )
  474. )
  475. or (
  476. getattr(func, "regex", False)
  477. and (
  478. not isinstance(message, Message)
  479. or not re.search(func.regex, message.raw_text)
  480. )
  481. )
  482. )
  483. async def handle_incoming(
  484. self,
  485. event: typing.Union[events.NewMessage, events.MessageDeleted],
  486. ):
  487. """Handle all incoming messages"""
  488. message = utils.censor(getattr(event, "message", event))
  489. blacklist_chats = self._db.get(main.__name__, "blacklist_chats", [])
  490. whitelist_chats = self._db.get(main.__name__, "whitelist_chats", [])
  491. whitelist_modules = self._db.get(main.__name__, "whitelist_modules", [])
  492. if utils.get_chat_id(message) in blacklist_chats or (
  493. whitelist_chats and utils.get_chat_id(message) not in whitelist_chats
  494. ):
  495. logger.debug("Message is blacklisted")
  496. return
  497. for func in self._modules.watchers:
  498. bl = self._db.get(main.__name__, "disabled_watchers", {})
  499. modname = str(func.__self__.__class__.strings["name"])
  500. if (
  501. modname in bl
  502. and isinstance(message, Message)
  503. and (
  504. "*" in bl[modname]
  505. or utils.get_chat_id(message) in bl[modname]
  506. or "only_chats" in bl[modname]
  507. and message.is_private
  508. or "only_pm" in bl[modname]
  509. and not message.is_private
  510. or "out" in bl[modname]
  511. and not message.out
  512. or "in" in bl[modname]
  513. and message.out
  514. )
  515. or f"{str(utils.get_chat_id(message))}.{func.__self__.__module__}"
  516. in blacklist_chats
  517. or whitelist_modules
  518. and f"{str(utils.get_chat_id(message))}.{func.__self__.__module__}"
  519. not in whitelist_modules
  520. or await self._handle_tags(event, func)
  521. ):
  522. logger.debug("Ignored watcher of module %s", modname)
  523. continue
  524. # Avoid weird AttributeErrors in weird dochub modules by settings placeholder
  525. # of attributes
  526. for placeholder in {"text", "raw_text", "out"}:
  527. try:
  528. if not hasattr(message, placeholder):
  529. setattr(message, placeholder, "")
  530. except UnicodeDecodeError:
  531. pass
  532. # Run watcher via ensure_future so in case user has a lot
  533. # of watchers with long actions, they can run simultaneously
  534. asyncio.ensure_future(
  535. self.future_dispatcher(
  536. func,
  537. message,
  538. self.watcher_exc,
  539. )
  540. )
  541. async def future_dispatcher(
  542. self,
  543. func: callable,
  544. message: Message,
  545. exception_handler: callable,
  546. *args,
  547. ):
  548. # Will be used to determine, which client caused logging messages
  549. # parsed via inspect.stack()
  550. _hikka_client_id_logging_tag = copy.copy(self.client.tg_id) # skipcq
  551. try:
  552. await func(message)
  553. except BaseException as e:
  554. await exception_handler(e, message, *args)