dispatcher.py 21 KB

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