events.py 20 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494
  1. # ©️ Dan Gazizullin, 2021-2023
  2. # This file is a part of Hikka Userbot
  3. # 🌐 https://github.com/hikariatama/Hikka
  4. # You can redistribute it and/or modify it under the terms of the GNU AGPLv3
  5. # 🔑 https://www.gnu.org/licenses/agpl-3.0.html
  6. import inspect
  7. import logging
  8. import re
  9. import typing
  10. from asyncio import Event
  11. from aiogram.types import CallbackQuery, ChosenInlineResult
  12. from aiogram.types import InlineQuery as AiogramInlineQuery
  13. from aiogram.types import (
  14. InlineQueryResultArticle,
  15. InlineQueryResultDocument,
  16. InlineQueryResultGif,
  17. InlineQueryResultPhoto,
  18. InlineQueryResultVideo,
  19. InputTextMessageContent,
  20. )
  21. from aiogram.types import Message as AiogramMessage
  22. from .. import utils
  23. from .types import BotInlineCall, InlineCall, InlineQuery, InlineUnit
  24. logger = logging.getLogger(__name__)
  25. class Events(InlineUnit):
  26. async def _message_handler(self, message: AiogramMessage):
  27. """Processes incoming messages"""
  28. if message.chat.type != "private" or message.text == "/start hikka init":
  29. return
  30. for mod in self._allmodules.modules:
  31. if (
  32. not hasattr(mod, "aiogram_watcher")
  33. or message.text == "/start"
  34. and mod.__class__.__name__ != "InlineStuff"
  35. ):
  36. continue
  37. try:
  38. await mod.aiogram_watcher(message)
  39. except Exception:
  40. logger.exception("Error on running aiogram watcher!")
  41. async def _inline_handler(self, inline_query: AiogramInlineQuery):
  42. """Inline query handler (forms' calls)"""
  43. if not (query := inline_query.query):
  44. await self._query_help(inline_query)
  45. return
  46. cmd = query.split()[0].lower()
  47. if (
  48. cmd in self._allmodules.inline_handlers
  49. and await self.check_inline_security(
  50. func=self._allmodules.inline_handlers[cmd],
  51. user=inline_query.from_user.id,
  52. )
  53. ):
  54. instance = InlineQuery(inline_query)
  55. try:
  56. if not (
  57. result := await self._allmodules.inline_handlers[cmd](instance)
  58. ):
  59. return
  60. except Exception:
  61. logger.exception("Error on running inline watcher!")
  62. return
  63. if isinstance(result, dict):
  64. result = [result]
  65. if not isinstance(result, list):
  66. logger.error(
  67. "Got invalid type from inline handler. It must be `dict`, got `%s`",
  68. type(result),
  69. )
  70. await instance.e500()
  71. return
  72. for res in result:
  73. mandatory = ["message", "photo", "gif", "video", "file"]
  74. if all(item not in res for item in mandatory):
  75. logger.error(
  76. (
  77. "Got invalid type from inline handler. It must contain one"
  78. " of `%s`"
  79. ),
  80. mandatory,
  81. )
  82. await instance.e500()
  83. return
  84. if "file" in res and "mime_type" not in res:
  85. logger.error(
  86. "Got invalid type from inline handler. It contains field"
  87. " `file`, so it must contain `mime_type` as well"
  88. )
  89. try:
  90. await inline_query.answer(
  91. [
  92. (
  93. InlineQueryResultArticle(
  94. id=utils.rand(20),
  95. title=self.sanitise_text(res["title"]),
  96. description=self.sanitise_text(res.get("description")),
  97. input_message_content=InputTextMessageContent(
  98. self.sanitise_text(res["message"]),
  99. "HTML",
  100. disable_web_page_preview=True,
  101. ),
  102. thumb_url=res.get("thumb"),
  103. thumb_width=128,
  104. thumb_height=128,
  105. reply_markup=self.generate_markup(
  106. res.get("reply_markup")
  107. ),
  108. )
  109. if "message" in res
  110. else (
  111. InlineQueryResultPhoto(
  112. id=utils.rand(20),
  113. title=self.sanitise_text(res.get("title")),
  114. description=self.sanitise_text(
  115. res.get("description")
  116. ),
  117. caption=self.sanitise_text(res.get("caption")),
  118. parse_mode="HTML",
  119. thumb_url=res.get("thumb", res["photo"]),
  120. photo_url=res["photo"],
  121. reply_markup=self.generate_markup(
  122. res.get("reply_markup")
  123. ),
  124. )
  125. if "photo" in res
  126. else (
  127. InlineQueryResultGif(
  128. id=utils.rand(20),
  129. title=self.sanitise_text(res.get("title")),
  130. caption=self.sanitise_text(res.get("caption")),
  131. parse_mode="HTML",
  132. thumb_url=res.get("thumb", res["gif"]),
  133. gif_url=res["gif"],
  134. reply_markup=self.generate_markup(
  135. res.get("reply_markup")
  136. ),
  137. )
  138. if "gif" in res
  139. else (
  140. InlineQueryResultVideo(
  141. id=utils.rand(20),
  142. title=self.sanitise_text(res.get("title")),
  143. description=self.sanitise_text(
  144. res.get("description")
  145. ),
  146. caption=self.sanitise_text(
  147. res.get("caption")
  148. ),
  149. parse_mode="HTML",
  150. thumb_url=res.get("thumb", res["video"]),
  151. video_url=res["video"],
  152. mime_type="video/mp4",
  153. reply_markup=self.generate_markup(
  154. res.get("reply_markup")
  155. ),
  156. )
  157. if "video" in res
  158. else InlineQueryResultDocument(
  159. id=utils.rand(20),
  160. title=self.sanitise_text(res.get("title")),
  161. description=self.sanitise_text(
  162. res.get("description")
  163. ),
  164. caption=self.sanitise_text(
  165. res.get("caption")
  166. ),
  167. parse_mode="HTML",
  168. thumb_url=res.get("thumb", res["file"]),
  169. document_url=res["file"],
  170. mime_type=res["mime_type"],
  171. reply_markup=self.generate_markup(
  172. res.get("reply_markup")
  173. ),
  174. )
  175. )
  176. )
  177. )
  178. )
  179. for res in result
  180. ],
  181. cache_time=0,
  182. )
  183. except Exception:
  184. logger.exception(
  185. "Exception when answering inline query with result from %s",
  186. cmd,
  187. )
  188. return
  189. await self._form_inline_handler(inline_query)
  190. await self._gallery_inline_handler(inline_query)
  191. await self._list_inline_handler(inline_query)
  192. async def _callback_query_handler(
  193. self,
  194. call: CallbackQuery,
  195. reply_markup: typing.Optional[
  196. typing.List[typing.List[typing.Dict[str, typing.Any]]]
  197. ] = None,
  198. ):
  199. """Callback query handler (buttons' presses)"""
  200. if reply_markup is None:
  201. reply_markup = []
  202. if re.search(r"authorize_web_(.{8})", call.data):
  203. self._web_auth_tokens += [re.search(r"authorize_web_(.{8})", call.data)[1]]
  204. return
  205. for func in self._allmodules.callback_handlers.values():
  206. if await self.check_inline_security(func=func, user=call.from_user.id):
  207. try:
  208. await func(
  209. (
  210. BotInlineCall
  211. if getattr(getattr(call, "message", None), "chat", None)
  212. else InlineCall
  213. )(call, self, None)
  214. )
  215. except Exception:
  216. logger.exception("Error on running callback watcher!")
  217. await call.answer(
  218. "Error occured while processing request. More info in logs",
  219. show_alert=True,
  220. )
  221. continue
  222. for unit_id, unit in self._units.copy().items():
  223. for button in utils.array_sum(unit.get("buttons", [])):
  224. if not isinstance(button, dict):
  225. logger.warning(
  226. "Can't process update, because of corrupted button: %s",
  227. button,
  228. )
  229. continue
  230. if button.get("_callback_data") == call.data:
  231. if (
  232. button.get("disable_security", False)
  233. or unit.get("disable_security", False)
  234. or (
  235. unit.get("force_me", False)
  236. and call.from_user.id == self._me
  237. )
  238. or not unit.get("force_me", False)
  239. and (
  240. await self.check_inline_security(
  241. func=unit.get(
  242. "perms_map",
  243. lambda: self._client.dispatcher.security._default,
  244. )(), # we call it so we can get reloaded rights in runtime
  245. user=call.from_user.id,
  246. )
  247. if "message" in unit
  248. else False
  249. )
  250. ):
  251. pass
  252. elif call.from_user.id not in (
  253. self._client.dispatcher.security._owner
  254. + unit.get("always_allow", [])
  255. + button.get("always_allow", [])
  256. ):
  257. await call.answer(self.translator.getkey("inline.button403"))
  258. return
  259. try:
  260. result = await button["callback"](
  261. (
  262. BotInlineCall
  263. if getattr(getattr(call, "message", None), "chat", None)
  264. else InlineCall
  265. )(call, self, unit_id),
  266. *button.get("args", []),
  267. **button.get("kwargs", {}),
  268. )
  269. except Exception:
  270. logger.exception("Error on running callback watcher!")
  271. await call.answer(
  272. (
  273. "Error occurred while processing request. More info in"
  274. " logs"
  275. ),
  276. show_alert=True,
  277. )
  278. return
  279. return result
  280. if call.data in self._custom_map:
  281. if (
  282. self._custom_map[call.data].get("disable_security", False)
  283. or (
  284. self._custom_map[call.data].get("force_me", False)
  285. and call.from_user.id == self._me
  286. )
  287. or not self._custom_map[call.data].get("force_me", False)
  288. and (
  289. await self.check_inline_security(
  290. func=self._custom_map[call.data].get(
  291. "perms_map",
  292. lambda: self._client.dispatcher.security._default,
  293. )(),
  294. user=call.from_user.id,
  295. )
  296. if "message" in self._custom_map[call.data]
  297. else False
  298. )
  299. ):
  300. pass
  301. elif (
  302. call.from_user.id not in self._client.dispatcher.security._owner
  303. and call.from_user.id
  304. not in self._custom_map[call.data].get("always_allow", [])
  305. ):
  306. await call.answer(self.translator.getkey("inline.button403"))
  307. return
  308. await self._custom_map[call.data]["handler"](
  309. (
  310. BotInlineCall
  311. if getattr(getattr(call, "message", None), "chat", None)
  312. else InlineCall
  313. )(call, self, None),
  314. *self._custom_map[call.data].get("args", []),
  315. **self._custom_map[call.data].get("kwargs", {}),
  316. )
  317. return
  318. async def _chosen_inline_handler(
  319. self,
  320. chosen_inline_query: ChosenInlineResult,
  321. ):
  322. query = chosen_inline_query.query
  323. if not query:
  324. return
  325. for unit_id, unit in self._units.items():
  326. if (
  327. unit_id == query
  328. and "future" in unit
  329. and isinstance(unit["future"], Event)
  330. ):
  331. unit["inline_message_id"] = chosen_inline_query.inline_message_id
  332. unit["future"].set()
  333. return
  334. for unit_id, unit in self._units.copy().items():
  335. for button in utils.array_sum(unit.get("buttons", [])):
  336. if (
  337. "_switch_query" in button
  338. and "input" in button
  339. and button["_switch_query"] == query.split()[0]
  340. and chosen_inline_query.from_user.id
  341. in [self._me]
  342. + self._client.dispatcher.security._owner
  343. + unit.get("always_allow", [])
  344. ):
  345. query = query.split(maxsplit=1)[1] if len(query.split()) > 1 else ""
  346. try:
  347. return await button["handler"](
  348. InlineCall(chosen_inline_query, self, unit_id),
  349. query,
  350. *button.get("args", []),
  351. **button.get("kwargs", {}),
  352. )
  353. except Exception:
  354. logger.exception(
  355. "Exception while running chosen query watcher!"
  356. )
  357. return
  358. async def _query_help(self, inline_query: InlineQuery):
  359. _help = []
  360. for name, fun in self._allmodules.inline_handlers.items():
  361. if not await self.check_inline_security(
  362. func=fun,
  363. user=inline_query.from_user.id,
  364. ):
  365. continue
  366. try:
  367. doc = inspect.getdoc(fun)
  368. except Exception:
  369. doc = "🦥 No docs"
  370. try:
  371. thumb = getattr(fun, "thumb_url", None) or fun.__self__.hikka_meta_pic
  372. except Exception:
  373. thumb = None
  374. thumb = thumb or "https://img.icons8.com/fluency/50/000000/info-squared.png"
  375. _help += [
  376. (
  377. InlineQueryResultArticle(
  378. id=utils.rand(20),
  379. title=self.translator.getkey("inline.command").format(name),
  380. description=doc,
  381. input_message_content=InputTextMessageContent(
  382. (
  383. self.translator.getkey("inline.command_msg").format(
  384. utils.escape_html(name),
  385. utils.escape_html(doc),
  386. )
  387. ),
  388. "HTML",
  389. disable_web_page_preview=True,
  390. ),
  391. thumb_url=thumb,
  392. thumb_width=128,
  393. thumb_height=128,
  394. reply_markup=self.generate_markup(
  395. {
  396. "text": self.translator.getkey("inline.run_command"),
  397. "switch_inline_query_current_chat": f"{name} ",
  398. }
  399. ),
  400. ),
  401. (
  402. f"🎹 <code>@{self.bot_username} {utils.escape_html(name)}</code>"
  403. f" - {utils.escape_html(doc)}\n"
  404. ),
  405. )
  406. ]
  407. if not _help:
  408. await inline_query.answer(
  409. [
  410. InlineQueryResultArticle(
  411. id=utils.rand(20),
  412. title=self.translator.getkey("inline.show_inline_cmds"),
  413. description=self.translator.getkey("inline.no_inline_cmds"),
  414. input_message_content=InputTextMessageContent(
  415. self.translator.getkey("inline.no_inline_cmds_msg"),
  416. "HTML",
  417. disable_web_page_preview=True,
  418. ),
  419. thumb_url=(
  420. "https://img.icons8.com/fluency/50/000000/info-squared.png"
  421. ),
  422. thumb_width=128,
  423. thumb_height=128,
  424. )
  425. ],
  426. cache_time=0,
  427. )
  428. return
  429. await inline_query.answer(
  430. [
  431. InlineQueryResultArticle(
  432. id=utils.rand(20),
  433. title=self.translator.getkey("inline.show_inline_cmds"),
  434. description=(
  435. self.translator.getkey("inline.inline_cmds").format(len(_help))
  436. ),
  437. input_message_content=InputTextMessageContent(
  438. (
  439. self.translator.getkey("inline.inline_cmds_msg").format(
  440. "\n".join(map(lambda x: x[1], _help))
  441. )
  442. ),
  443. "HTML",
  444. disable_web_page_preview=True,
  445. ),
  446. thumb_url=(
  447. "https://img.icons8.com/fluency/50/000000/info-squared.png"
  448. ),
  449. thumb_width=128,
  450. thumb_height=128,
  451. )
  452. ]
  453. + [i[0] for i in _help],
  454. cache_time=0,
  455. )