events.py 18 KB

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