dispatcher.py 19 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539
  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 logging
  25. import re
  26. import traceback
  27. from typing import Tuple, Union
  28. from telethon import types
  29. from telethon.tl.types import Message
  30. from . import main, security, utils
  31. from .database import Database
  32. from .loader import Modules
  33. # Keys for layout switch
  34. ru_keys = 'ёйцукенгшщзхъфывапролджэячсмитьбю.Ё"№;%:?ЙЦУКЕНГШЩЗХЪФЫВАПРОЛДЖЭ/ЯЧСМИТЬБЮ,'
  35. en_keys = "`qwertyuiop[]asdfghjkl;'zxcvbnm,./~@#$%^&QWERTYUIOP{}ASDFGHJKL:\"|ZXCVBNM<>?"
  36. def _decrement_ratelimit(delay, data, key, severity):
  37. def inner():
  38. data[key] = max(0, data[key] - severity)
  39. asyncio.get_event_loop().call_later(delay, inner)
  40. class CommandDispatcher:
  41. def __init__(self, modules: Modules, db: Database, no_nickname: bool = False):
  42. self._modules = modules
  43. self._db = db
  44. self.security = security.SecurityManager(db)
  45. self.no_nickname = no_nickname
  46. self._ratelimit_storage_user = collections.defaultdict(int)
  47. self._ratelimit_storage_chat = collections.defaultdict(int)
  48. self._ratelimit_max_user = db.get(__name__, "ratelimit_max_user", 30)
  49. self._ratelimit_max_chat = db.get(__name__, "ratelimit_max_chat", 100)
  50. self.check_security = self.security.check
  51. async def init(self, client: "TelegramClient"): # type: ignore
  52. await self.security.init(client)
  53. me = await client.get_me()
  54. self.client = client # Intended to be used to track user in logging
  55. self._me = me.id
  56. self._cached_username = me.username.lower() if me.username else str(me.id)
  57. async def _handle_ratelimit(self, message: Message, func: callable) -> bool:
  58. if await self.security.check(
  59. message,
  60. security.OWNER | security.SUDO | security.SUPPORT,
  61. ):
  62. return True
  63. func = getattr(func, "__func__", func)
  64. ret = True
  65. chat = self._ratelimit_storage_chat[message.chat_id]
  66. if message.sender_id:
  67. user = self._ratelimit_storage_user[message.sender_id]
  68. severity = (5 if getattr(func, "ratelimit", False) else 2) * (
  69. (user + chat) // 30 + 1
  70. )
  71. user += severity
  72. self._ratelimit_storage_user[message.sender_id] = user
  73. if user > self._ratelimit_max_user:
  74. ret = False
  75. else:
  76. self._ratelimit_storage_chat[message.chat_id] = chat
  77. _decrement_ratelimit(
  78. self._ratelimit_max_user * severity,
  79. self._ratelimit_storage_user,
  80. message.sender_id,
  81. severity,
  82. )
  83. else:
  84. severity = (5 if getattr(func, "ratelimit", False) else 2) * (
  85. chat // 15 + 1
  86. )
  87. chat += severity
  88. if chat > self._ratelimit_max_chat:
  89. ret = False
  90. _decrement_ratelimit(
  91. self._ratelimit_max_chat * severity,
  92. self._ratelimit_storage_chat,
  93. message.chat_id,
  94. severity,
  95. )
  96. return ret
  97. def _handle_grep(self, message: Message) -> Message:
  98. # Allow escaping grep with double stick
  99. if "||grep" in message.text or "|| grep" in message.text:
  100. message.raw_text = re.sub(r"\|\| ?grep", "| grep", message.raw_text)
  101. message.text = re.sub(r"\|\| ?grep", "| grep", message.text)
  102. message.message = re.sub(r"\|\| ?grep", "| grep", message.message)
  103. return message
  104. grep = False
  105. if not re.search(r".+\| ?grep (.+)", message.raw_text):
  106. return message
  107. grep = re.search(r".+\| ?grep (.+)", message.raw_text).group(1)
  108. message.text = re.sub(r"\| ?grep.+", "", message.text)
  109. message.raw_text = re.sub(r"\| ?grep.+", "", message.raw_text)
  110. message.message = re.sub(r"\| ?grep.+", "", message.message)
  111. ungrep = False
  112. if re.search(r"-v (.+)", grep):
  113. ungrep = re.search(r"-v (.+)", grep).group(1)
  114. grep = re.sub(r"(.+) -v .+", r"\g<1>", grep)
  115. grep = utils.escape_html(grep).strip() if grep else False
  116. ungrep = utils.escape_html(ungrep).strip() if ungrep else False
  117. old_edit = message.edit
  118. old_reply = message.reply
  119. old_respond = message.respond
  120. def process_text(text: str) -> str:
  121. nonlocal grep, ungrep
  122. res = []
  123. for line in text.split("\n"):
  124. if (
  125. grep
  126. and grep in utils.remove_html(line)
  127. and (not ungrep or ungrep not in utils.remove_html(line))
  128. ):
  129. res.append(
  130. utils.remove_html(line, escape=True).replace(
  131. grep, f"<u>{grep}</u>"
  132. )
  133. )
  134. if not grep and ungrep and ungrep not in utils.remove_html(line):
  135. res.append(utils.remove_html(line, escape=True))
  136. cont = (
  137. (f"contain <b>{grep}</b>" if grep else "")
  138. + (" and" if grep and ungrep else "")
  139. + ((" do not contain <b>" + ungrep + "</b>") if ungrep else "")
  140. )
  141. if res:
  142. text = f"<i>💬 Lines that {cont}:</i>\n" + "\n".join(res)
  143. else:
  144. text = f"💬 <i>No lines that {cont}</i>"
  145. return text
  146. async def my_edit(text, *args, **kwargs):
  147. text = process_text(text)
  148. kwargs["parse_mode"] = "HTML"
  149. return await old_edit(text, *args, **kwargs)
  150. async def my_reply(text, *args, **kwargs):
  151. text = process_text(text)
  152. kwargs["parse_mode"] = "HTML"
  153. return await old_reply(text, *args, **kwargs)
  154. async def my_respond(text, *args, **kwargs):
  155. text = process_text(text)
  156. kwargs["parse_mode"] = "HTML"
  157. return await old_respond(text, *args, **kwargs)
  158. message.edit = my_edit
  159. message.reply = my_reply
  160. message.respond = my_respond
  161. return message
  162. async def _handle_command(
  163. self,
  164. event,
  165. watcher: bool = False,
  166. ) -> Union[bool, Tuple[Message, str, str, callable]]:
  167. if not hasattr(event, "message") or not hasattr(event.message, "message"):
  168. return False
  169. prefix = self._db.get(main.__name__, "command_prefix", False) or "."
  170. change = str.maketrans(ru_keys + en_keys, en_keys + ru_keys)
  171. message = utils.censor(event.message)
  172. if not event.message.message:
  173. return False
  174. if (
  175. event.message.message.startswith(str.translate(prefix, change))
  176. and str.translate(prefix, change) != prefix
  177. ):
  178. message.message = str.translate(message.message, change)
  179. message.text = str.translate(message.text, change)
  180. elif not event.message.message.startswith(prefix):
  181. return False
  182. if (
  183. event.sticker
  184. or event.dice
  185. or event.audio
  186. or event.via_bot_id
  187. or getattr(event, "reactions", False)
  188. ):
  189. return False
  190. blacklist_chats = self._db.get(main.__name__, "blacklist_chats", [])
  191. whitelist_chats = self._db.get(main.__name__, "whitelist_chats", [])
  192. whitelist_modules = self._db.get(main.__name__, "whitelist_modules", [])
  193. if utils.get_chat_id(message) in blacklist_chats or (
  194. whitelist_chats and utils.get_chat_id(message) not in whitelist_chats
  195. ):
  196. return False
  197. if (
  198. message.out
  199. and len(message.message) > 2
  200. and message.message.startswith(prefix * 2)
  201. and any(s != prefix for s in message.message)
  202. ):
  203. # Allow escaping commands using .'s
  204. if not watcher:
  205. await message.edit(
  206. message.message[1:],
  207. parse_mode=lambda s: (
  208. s,
  209. utils.relocate_entities(message.entities, -1, message.message)
  210. or (),
  211. ),
  212. )
  213. return False
  214. if not message.message or len(message.message) == 1:
  215. return False # Message is just the prefix
  216. initiator = getattr(event, "sender_id", 0)
  217. command = message.message.split(maxsplit=1)[0][1:]
  218. tag = command.split("@", maxsplit=1)
  219. if len(tag) == 2:
  220. if tag[1] == "me":
  221. if not message.out:
  222. return False
  223. elif tag[1].lower() != self._cached_username:
  224. return False
  225. elif (
  226. event.out
  227. or event.mentioned
  228. and event.message is not None
  229. and event.message.message is not None
  230. and f"@{self._cached_username}" not in event.message.message
  231. ):
  232. pass
  233. elif (
  234. not event.is_private
  235. and not self.no_nickname
  236. and not self._db.get(main.__name__, "no_nickname", False)
  237. and command not in self._db.get(main.__name__, "nonickcmds", [])
  238. and initiator not in self._db.get(main.__name__, "nonickusers", [])
  239. and utils.get_chat_id(event)
  240. not in self._db.get(main.__name__, "nonickchats", [])
  241. ):
  242. return False
  243. txt, func = self._modules.dispatch(tag[0])
  244. if (
  245. not func
  246. or not await self._handle_ratelimit(message, func)
  247. or not await self.security.check(message, func)
  248. ):
  249. return False
  250. if (
  251. message.is_channel
  252. and message.is_group
  253. and message.chat.title.startswith("hikka-")
  254. and message.chat.title != "hikka-logs"
  255. ):
  256. if not watcher:
  257. logging.warning("Ignoring message in datachat \\ logging chat")
  258. return False
  259. message.message = prefix + txt + message.message[len(prefix + command) :]
  260. if (
  261. f"{str(utils.get_chat_id(message))}.{func.__self__.__module__}"
  262. in blacklist_chats
  263. or whitelist_modules
  264. and f"{utils.get_chat_id(message)}.{func.__self__.__module__}"
  265. not in whitelist_modules
  266. ):
  267. return False
  268. if await self._handle_tags(event, func):
  269. return False
  270. if self._db.get(main.__name__, "grep", False) and not watcher:
  271. message = self._handle_grep(message)
  272. return message, prefix, txt, func
  273. async def handle_command(self, event: Message):
  274. """Handle all commands"""
  275. message = await self._handle_command(event)
  276. if not message:
  277. return
  278. message, prefix, _, func = message
  279. asyncio.ensure_future(
  280. self.future_dispatcher(
  281. func,
  282. message,
  283. self.command_exc,
  284. prefix,
  285. )
  286. )
  287. async def command_exc(self, e, message: Message, prefix: str):
  288. logging.exception("Command failed")
  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>{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")
  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. )
  392. async def handle_incoming(self, event):
  393. """Handle all incoming messages"""
  394. message = utils.censor(getattr(event, "message", event))
  395. blacklist_chats = self._db.get(main.__name__, "blacklist_chats", [])
  396. whitelist_chats = self._db.get(main.__name__, "whitelist_chats", [])
  397. whitelist_modules = self._db.get(main.__name__, "whitelist_modules", [])
  398. if utils.get_chat_id(message) in blacklist_chats or (
  399. whitelist_chats and utils.get_chat_id(message) not in whitelist_chats
  400. ):
  401. logging.debug("Message is blacklisted")
  402. return
  403. for func in self._modules.watchers:
  404. bl = self._db.get(main.__name__, "disabled_watchers", {})
  405. modname = str(func.__self__.__class__.strings["name"])
  406. if (
  407. modname in bl
  408. and isinstance(message, types.Message)
  409. and (
  410. "*" in bl[modname]
  411. or utils.get_chat_id(message) in bl[modname]
  412. or "only_chats" in bl[modname]
  413. and message.is_private
  414. or "only_pm" in bl[modname]
  415. and not message.is_private
  416. or "out" in bl[modname]
  417. and not message.out
  418. or "in" in bl[modname]
  419. and message.out
  420. )
  421. or f"{str(utils.get_chat_id(message))}.{func.__self__.__module__}"
  422. in blacklist_chats
  423. or whitelist_modules
  424. and f"{str(utils.get_chat_id(message))}.{func.__self__.__module__}"
  425. not in whitelist_modules
  426. or await self._handle_tags(event, func)
  427. ):
  428. logging.debug(f"Ignored watcher of module {modname}")
  429. continue
  430. # Avoid weird AttributeErrors in weird dochub modules by settings placeholder
  431. # of attributes
  432. for placeholder in {"text", "raw_text"}:
  433. try:
  434. if not hasattr(message, placeholder):
  435. setattr(message, placeholder, "")
  436. except UnicodeDecodeError:
  437. pass
  438. # Run watcher via ensure_future so in case user has a lot
  439. # of watchers with long actions, they can run simultaneously
  440. asyncio.ensure_future(
  441. self.future_dispatcher(
  442. func,
  443. message,
  444. self.watcher_exc,
  445. )
  446. )
  447. async def future_dispatcher(
  448. self,
  449. func: callable,
  450. message: Message,
  451. exception_handler: callable,
  452. *args,
  453. ):
  454. # Will be used to determine, which client caused logging messages
  455. # parsed via inspect.stack()
  456. _hikka_client_id_logging_tag = copy.copy(self.client.tg_id) # skipcq
  457. try:
  458. await func(message)
  459. except BaseException as e:
  460. await exception_handler(e, message, *args)