dispatcher.py 21 KB

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