dispatcher.py 21 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627
  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. from typing import Tuple, Union
  29. from telethon import TelegramClient
  30. from telethon.tl.types import Message
  31. from . import main, security, utils
  32. from .database import Database
  33. from .loader import Modules
  34. # Keys for layout switch
  35. ru_keys = 'ёйцукенгшщзхъфывапролджэячсмитьбю.Ё"№;%:?ЙЦУКЕНГШЩЗХЪФЫВАПРОЛДЖЭ/ЯЧСМИТЬБЮ,'
  36. en_keys = "`qwertyuiop[]asdfghjkl;'zxcvbnm,./~@#$%^&QWERTYUIOP{}ASDFGHJKL:\"|ZXCVBNM<>?"
  37. ALL_TAGS = [
  38. "no_commands",
  39. "only_commands",
  40. "out",
  41. "in",
  42. "only_messages",
  43. "editable",
  44. "no_media",
  45. "only_media",
  46. "only_photos",
  47. "only_videos",
  48. "only_audios",
  49. "only_stickers",
  50. "only_docs",
  51. "only_inline",
  52. "only_channels",
  53. "only_groups",
  54. "only_pm",
  55. "startswith",
  56. "endswith",
  57. "contains",
  58. "filter",
  59. "from_id",
  60. "chat_id",
  61. "regex",
  62. ]
  63. def _decrement_ratelimit(delay, data, key, severity):
  64. def inner():
  65. data[key] = max(0, data[key] - severity)
  66. asyncio.get_event_loop().call_later(delay, inner)
  67. class CommandDispatcher:
  68. def __init__(
  69. self,
  70. modules: Modules,
  71. client: TelegramClient,
  72. db: Database,
  73. no_nickname: bool = False,
  74. ):
  75. self._modules = modules
  76. self._client = client
  77. self.client = client
  78. self._db = db
  79. self.no_nickname = no_nickname
  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,
  201. watcher: bool = False,
  202. ) -> Union[bool, 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.no_nickname
  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. logging.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_command(self, event: Message):
  310. """Handle all commands"""
  311. message = await self._handle_command(event)
  312. if not message:
  313. return
  314. message, _, _, func = message
  315. asyncio.ensure_future(
  316. self.future_dispatcher(
  317. func,
  318. message,
  319. self.command_exc,
  320. )
  321. )
  322. async def command_exc(self, e, message: Message):
  323. logging.exception("Command failed", extra={"stack": inspect.stack()})
  324. if not self._db.get(main.__name__, "inlinelogs", True):
  325. try:
  326. txt = (
  327. "<b>🚫 Call</b>"
  328. f" <code>{utils.escape_html(message.message)}</code><b>"
  329. " failed!</b>"
  330. )
  331. await (message.edit if message.out else message.reply)(txt)
  332. except Exception:
  333. pass
  334. return
  335. try:
  336. exc = traceback.format_exc()
  337. # Remove `Traceback (most recent call last):`
  338. exc = "\n".join(exc.splitlines()[1:])
  339. txt = (
  340. "<b>🚫 Call</b>"
  341. f" <code>{utils.escape_html(message.message)}</code><b>"
  342. f" failed!</b>\n\n<b>🧾 Logs:</b>\n<code>{utils.escape_html(exc)}</code>"
  343. )
  344. await (message.edit if message.out else message.reply)(txt)
  345. except Exception:
  346. pass
  347. async def watcher_exc(self, e, message: Message):
  348. logging.exception("Error running watcher", extra={"stack": inspect.stack()})
  349. async def _handle_tags(self, event, func: callable) -> bool:
  350. message = getattr(event, "message", event)
  351. return (
  352. (
  353. getattr(func, "no_commands", False)
  354. and await self._handle_command(event, watcher=True)
  355. )
  356. or (
  357. getattr(func, "only_commands", False)
  358. and not await self._handle_command(event, watcher=True)
  359. )
  360. or (getattr(func, "out", False) and not getattr(message, "out", True))
  361. or (getattr(func, "in", False) and getattr(message, "out", True))
  362. or (
  363. getattr(func, "only_messages", False)
  364. and not isinstance(message, Message)
  365. )
  366. or (
  367. getattr(func, "editable", False)
  368. and (
  369. getattr(message, "fwd_from", False)
  370. or not getattr(message, "out", False)
  371. or getattr(message, "sticker", False)
  372. or getattr(message, "via_bot_id", False)
  373. )
  374. )
  375. or (
  376. getattr(func, "no_media", False)
  377. and isinstance(message, Message)
  378. and getattr(message, "media", False)
  379. )
  380. or (
  381. getattr(func, "only_media", False)
  382. and (
  383. not isinstance(message, Message)
  384. or not getattr(message, "media", False)
  385. )
  386. )
  387. or (
  388. getattr(func, "only_photos", False)
  389. and not utils.mime_type(message).startswith("image/")
  390. )
  391. or (
  392. getattr(func, "only_videos", False)
  393. and not utils.mime_type(message).startswith("video/")
  394. )
  395. or (
  396. getattr(func, "only_audios", False)
  397. and not utils.mime_type(message).startswith("audio/")
  398. )
  399. or (
  400. getattr(func, "only_stickers", False)
  401. and not getattr(message, "sticker", False)
  402. )
  403. or (
  404. getattr(func, "only_docs", False)
  405. and not getattr(message, "document", False)
  406. )
  407. or (
  408. getattr(func, "only_inline", False)
  409. and not getattr(message, "via_bot_id", False)
  410. )
  411. or (
  412. getattr(func, "only_channels", False)
  413. and (
  414. not getattr(message, "is_channel", False)
  415. and getattr(message, "is_group", False)
  416. or getattr(message, "is_private", False)
  417. )
  418. )
  419. or (
  420. getattr(func, "only_groups", False)
  421. and not getattr(message, "is_group", False)
  422. )
  423. or (
  424. getattr(func, "only_pm", False)
  425. and not getattr(message, "is_private", False)
  426. )
  427. or (
  428. getattr(func, "startswith", False)
  429. and (
  430. not isinstance(message, Message)
  431. or isinstance(func.startswith, str)
  432. and not message.raw_text.startswith(getattr(func, "startswith"))
  433. )
  434. )
  435. or (
  436. getattr(func, "endswith", False)
  437. and (
  438. not isinstance(message, Message)
  439. or isinstance(func.endswith, str)
  440. and not message.raw_text.endswith(getattr(func, "endswith"))
  441. )
  442. )
  443. or (
  444. getattr(func, "contains", False)
  445. and (
  446. not isinstance(message, Message)
  447. or isinstance(func.contains, str)
  448. and getattr(func, "contains") not in message.raw_text
  449. )
  450. )
  451. or (
  452. getattr(func, "filter", False)
  453. and callable(func.filter)
  454. and not func.filter(message)
  455. )
  456. or (
  457. getattr(func, "from_id", False)
  458. and getattr(message, "sender_id", None) != func.from_id
  459. )
  460. or (
  461. getattr(func, "chat_id", False)
  462. and utils.get_chat_id(message)
  463. != (
  464. func.chat_id
  465. if not str(func.chat_id).startswith("-100")
  466. else int(str(func.chat_id)[4:])
  467. )
  468. )
  469. or (
  470. getattr(func, "regex", False)
  471. and (
  472. not isinstance(message, Message)
  473. or not re.search(func.regex, message.raw_text)
  474. )
  475. )
  476. )
  477. async def handle_incoming(self, event):
  478. """Handle all incoming messages"""
  479. message = utils.censor(getattr(event, "message", event))
  480. blacklist_chats = self._db.get(main.__name__, "blacklist_chats", [])
  481. whitelist_chats = self._db.get(main.__name__, "whitelist_chats", [])
  482. whitelist_modules = self._db.get(main.__name__, "whitelist_modules", [])
  483. if utils.get_chat_id(message) in blacklist_chats or (
  484. whitelist_chats and utils.get_chat_id(message) not in whitelist_chats
  485. ):
  486. logging.debug("Message is blacklisted")
  487. return
  488. for func in self._modules.watchers:
  489. bl = self._db.get(main.__name__, "disabled_watchers", {})
  490. modname = str(func.__self__.__class__.strings["name"])
  491. if (
  492. modname in bl
  493. and isinstance(message, Message)
  494. and (
  495. "*" in bl[modname]
  496. or utils.get_chat_id(message) in bl[modname]
  497. or "only_chats" in bl[modname]
  498. and message.is_private
  499. or "only_pm" in bl[modname]
  500. and not message.is_private
  501. or "out" in bl[modname]
  502. and not message.out
  503. or "in" in bl[modname]
  504. and message.out
  505. )
  506. or f"{str(utils.get_chat_id(message))}.{func.__self__.__module__}"
  507. in blacklist_chats
  508. or whitelist_modules
  509. and f"{str(utils.get_chat_id(message))}.{func.__self__.__module__}"
  510. not in whitelist_modules
  511. or await self._handle_tags(event, func)
  512. ):
  513. tags = ", ".join(
  514. f"{tag}={getattr(func, tag, None)}" for tag in ALL_TAGS
  515. )
  516. logging.debug(f"Ignored watcher of module {modname} {tags}")
  517. continue
  518. # Avoid weird AttributeErrors in weird dochub modules by settings placeholder
  519. # of attributes
  520. for placeholder in {"text", "raw_text", "out"}:
  521. try:
  522. if not hasattr(message, placeholder):
  523. setattr(message, placeholder, "")
  524. except UnicodeDecodeError:
  525. pass
  526. # Run watcher via ensure_future so in case user has a lot
  527. # of watchers with long actions, they can run simultaneously
  528. asyncio.ensure_future(
  529. self.future_dispatcher(
  530. func,
  531. message,
  532. self.watcher_exc,
  533. )
  534. )
  535. async def future_dispatcher(
  536. self,
  537. func: callable,
  538. message: Message,
  539. exception_handler: callable,
  540. *args,
  541. ):
  542. # Will be used to determine, which client caused logging messages
  543. # parsed via inspect.stack()
  544. _hikka_client_id_logging_tag = copy.copy(self.client.tg_id) # skipcq
  545. try:
  546. await func(message)
  547. except BaseException as e:
  548. await exception_handler(e, message, *args)