form.py 23 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571
  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 contextlib
  9. import copy
  10. import logging
  11. import os
  12. import time
  13. from asyncio import Event
  14. import typing
  15. import random
  16. from urllib.parse import urlparse
  17. import grapheme
  18. import traceback
  19. from aiogram.types import (
  20. InlineQuery,
  21. InlineQueryResultArticle,
  22. InlineQueryResultPhoto,
  23. InputTextMessageContent,
  24. InlineQueryResultDocument,
  25. InlineQueryResultGif,
  26. InlineQueryResultVideo,
  27. InlineQueryResultLocation,
  28. InlineQueryResultAudio,
  29. )
  30. from telethon.tl.types import Message
  31. from telethon.errors.rpcerrorlist import ChatSendInlineForbiddenError
  32. from telethon.extensions.html import CUSTOM_EMOJIS
  33. from .. import utils, main
  34. from ..types import HikkaReplyMarkup
  35. from .types import InlineMessage, InlineUnit
  36. logger = logging.getLogger(__name__)
  37. VERIFICATION_EMOJIES = list(
  38. grapheme.graphemes(
  39. "👨‍🏫👩‍🏫👨‍🎤🧑‍🎤👩‍🎤👨‍🎓👩‍🎓👩‍🍳👩‍🌾👩‍⚕️🕵️‍♀️💂‍♀️👷‍♂️👮‍♂️👴🧑‍🦳👩‍🦳👱‍♀️👩‍🦰👨‍🦱👩‍⚖️🧙‍♂️🧝‍♀️🧛‍♀️"
  40. "🎅🧚‍♂️🙆‍♀️🙍‍♂️👩‍👦🧶🪢🪡🧵🩲👖👕👚🦺👗👙🩱👘🥻🩴🥿🧦🥾👟👞"
  41. "👢👡👠🪖👑💍👝👛👜💼🌂🥽🕶👓🧳🎒🐶🐱🐭🐹🐰🦊🐻🐷🐮"
  42. "🦁🐯🐨🐻‍❄️🐼🐽🐸🐵🙈🙉🙊🐒🦆🐥🐣🐤🐦🐧🐔🦅🦉🦇🐺🐗🐴"
  43. "🦄🐜🐞🐌🦋🐛🪱🐝🪰🪲🪳🦟🦗🕷🕸🐙🦕🦖🦎🐍🐢🦂🦑🦐🦞"
  44. "🦀🐡🐠🐟🐅🐊🦭🦈🐋🐳🐬🐆🦓🦍🦧🦣🐘🦛🐃🦬🦘🦒🐫🐪🦏"
  45. "🐂🐄🐎🐖🐏🐑🦙🐈🐕‍🦺🦮🐩🐕🦌🐐🐈‍⬛🪶🐓🦃🦤🦚🦜🦡🦨🦝🐇"
  46. "🕊🦩🦢🦫🦦🦥🐁🐀🐿🦔🌳🌲🌵🐲🐉🐾🎋🍂🍁🍄🐚🌾🪨💐🌷"
  47. "🥀🌺🌸🌻🌞🌜🌘🌗🌎🪐💫⭐️✨⚡️☄️💥☀️🌪🔥🌈🌤⛅️❄️⛄️🌊"
  48. "☂️🍏🍎🍐🍊🍋🍌🍉🥭🍑🍒🍈🫐🍓🍇🍍🥥🥝🍅🥑🥦🧔‍♂️"
  49. )
  50. )
  51. class Placeholder:
  52. """Placeholder"""
  53. class Form(InlineUnit):
  54. async def form(
  55. self,
  56. text: str,
  57. message: typing.Union[Message, int],
  58. reply_markup: typing.Optional[HikkaReplyMarkup] = None,
  59. *,
  60. force_me: bool = False,
  61. always_allow: typing.Optional[typing.List[int]] = None,
  62. manual_security: bool = False,
  63. disable_security: bool = False,
  64. ttl: typing.Optional[int] = None,
  65. on_unload: typing.Optional[callable] = None,
  66. photo: typing.Optional[str] = None,
  67. gif: typing.Optional[str] = None,
  68. file: typing.Optional[str] = None,
  69. mime_type: typing.Optional[str] = None,
  70. video: typing.Optional[str] = None,
  71. location: typing.Optional[str] = None,
  72. audio: typing.Optional[typing.Union[dict, str]] = None,
  73. silent: bool = False,
  74. ) -> typing.Union[InlineMessage, bool]:
  75. """
  76. Send inline form to chat
  77. :param text: Content of inline form. HTML markdown supported
  78. :param message: Where to send inline. Can be either `Message` or `int`
  79. :param reply_markup: List of buttons to insert in markup. List of dicts with keys: text, callback
  80. :param force_me: Either this form buttons must be pressed only by owner scope or no
  81. :param always_allow: Users, that are allowed to press buttons in addition to previous rules
  82. :param ttl: Time, when the form is going to be unloaded. Unload means, that the form
  83. buttons with inline queries and callback queries will become unusable, but
  84. buttons with type url will still work as usual. Pay attention, that ttl can't
  85. be bigger, than default one (1 day) and must be either `int` or `False`
  86. :param on_unload: Callback, called when form is unloaded and/or closed. You can clean up trash
  87. or perform another needed action
  88. :param manual_security: By default, Hikka will try to inherit inline buttons security from the caller (command)
  89. If you want to avoid this, pass `manual_security=True`
  90. :param disable_security: By default, Hikka will try to inherit inline buttons security from the caller (command)
  91. If you want to disable all security checks on this form in particular, pass `disable_security=True`
  92. :param photo: Attach a photo to the form. URL must be supplied
  93. :param gif: Attach a gif to the form. URL must be supplied
  94. :param file: Attach a file to the form. URL must be supplied
  95. :param mime_type: Only needed, if `file` field is not empty. Must be either 'application/pdf' or 'application/zip'
  96. :param video: Attach a video to the form. URL must be supplied
  97. :param location: Attach a map point to the form. List/tuple must be supplied (latitude, longitude)
  98. Example: (55.749931, 48.742371)
  99. ⚠️ If you pass this parameter, you'll need to pass empty string to `text` ⚠️
  100. :param audio: Attach a audio to the form. Dict or URL must be supplied
  101. :param silent: Whether the form must be sent silently (w/o "Opening form..." message)
  102. :return: If form is sent, returns :obj:`InlineMessage`, otherwise returns `False`
  103. """
  104. with contextlib.suppress(AttributeError):
  105. _hikka_client_id_logging_tag = copy.copy(self._client.tg_id)
  106. if reply_markup is None:
  107. reply_markup = []
  108. if always_allow is None:
  109. always_allow = []
  110. if not isinstance(text, str):
  111. logger.error(
  112. "Invalid type for `text`. Expected `str`, got `%s`",
  113. type(text),
  114. )
  115. return False
  116. text = self.sanitise_text(text)
  117. if not isinstance(silent, bool):
  118. logger.error(
  119. "Invalid type for `silent`. Expected `bool`, got `%s`",
  120. type(silent),
  121. )
  122. return False
  123. if not isinstance(manual_security, bool):
  124. logger.error(
  125. "Invalid type for `manual_security`. Expected `bool`, got `%s`",
  126. type(manual_security),
  127. )
  128. return False
  129. if not isinstance(disable_security, bool):
  130. logger.error(
  131. "Invalid type for `disable_security`. Expected `bool`, got `%s`",
  132. type(disable_security),
  133. )
  134. return False
  135. if not isinstance(message, (Message, int)):
  136. logger.error(
  137. "Invalid type for `message`. Expected `Message` or `int`, got `%s`",
  138. type(message),
  139. )
  140. return False
  141. if not isinstance(reply_markup, (list, dict)):
  142. logger.error(
  143. "Invalid type for `reply_markup`. Expected `list` or `dict`, got `%s`",
  144. type(reply_markup),
  145. )
  146. return False
  147. if photo and (not isinstance(photo, str) or not utils.check_url(photo)):
  148. logger.error(
  149. "Invalid type for `photo`. Expected `str` with URL, got `%s`",
  150. type(photo),
  151. )
  152. return False
  153. try:
  154. path = urlparse(photo).path
  155. ext = os.path.splitext(path)[1]
  156. except Exception:
  157. ext = None
  158. if photo is not None and ext in {".gif", ".mp4"}:
  159. gif = copy.copy(photo)
  160. photo = None
  161. if gif and (not isinstance(gif, str) or not utils.check_url(gif)):
  162. logger.error(
  163. "Invalid type for `gif`. Expected `str` with URL, got `%s`",
  164. type(gif),
  165. )
  166. return False
  167. if file and (not isinstance(file, str) or not utils.check_url(file)):
  168. logger.error(
  169. "Invalid type for `file`. Expected `str` with URL, got `%s`",
  170. type(file),
  171. )
  172. return False
  173. if file and not mime_type:
  174. logger.error(
  175. "You must pass `mime_type` along with `file` field\n"
  176. "It may be either 'application/zip' or 'application/pdf'"
  177. )
  178. return False
  179. if video and (not isinstance(video, str) or not utils.check_url(video)):
  180. logger.error(
  181. "Invalid type for `video`. Expected `str` with URL, got `%s`",
  182. type(video),
  183. )
  184. return False
  185. if isinstance(audio, str):
  186. audio = {"url": audio}
  187. if audio and (
  188. not isinstance(audio, dict)
  189. or "url" not in audio
  190. or not utils.check_url(audio["url"])
  191. ):
  192. logger.error(
  193. "Invalid type for `audio`. Expected `dict` with `url` key, got `%s`",
  194. type(audio),
  195. )
  196. return False
  197. if location and (
  198. not isinstance(location, (list, tuple))
  199. or len(location) != 2
  200. or not all(isinstance(item, float) for item in location)
  201. ):
  202. logger.error(
  203. "Invalid type for `location`. Expected `list` or `tuple` with 2 `float`"
  204. " items, got `%s`",
  205. type(location),
  206. )
  207. return False
  208. if [
  209. photo is not None,
  210. gif is not None,
  211. file is not None,
  212. video is not None,
  213. audio is not None,
  214. location is not None,
  215. ].count(True) > 1:
  216. logger.error("You passed two or more exclusive parameters simultaneously")
  217. return False
  218. reply_markup = self._validate_markup(reply_markup) or []
  219. if not isinstance(force_me, bool):
  220. logger.error(
  221. "Invalid type for `force_me`. Expected `bool`, got `%s`",
  222. type(force_me),
  223. )
  224. return False
  225. if not isinstance(always_allow, list):
  226. logger.error(
  227. "Invalid type for `always_allow`. Expected `list`, got `%s`",
  228. type(always_allow),
  229. )
  230. return False
  231. if not isinstance(ttl, int) and ttl:
  232. logger.error("Invalid type for `ttl`. Expected `int`, got `%s`", type(ttl))
  233. return False
  234. if isinstance(message, Message) and not silent:
  235. try:
  236. status_message = await (
  237. message.edit if message.out else message.respond
  238. )(
  239. (
  240. utils.get_platform_emoji(self._client)
  241. if self._client.hikka_me.premium and CUSTOM_EMOJIS
  242. else "🌘"
  243. )
  244. + self._client.loader._lookup("translations").strings(
  245. "opening_form"
  246. ),
  247. )
  248. except Exception:
  249. status_message = None
  250. else:
  251. status_message = None
  252. unit_id = utils.rand(16)
  253. perms_map = None if manual_security else self._find_caller_sec_map()
  254. if not reply_markup and not ttl:
  255. logger.debug("Patching form reply markup with empty data")
  256. base_reply_markup = copy.deepcopy(reply_markup) or None
  257. reply_markup = self._validate_markup({"text": "­", "data": "­"})
  258. else:
  259. base_reply_markup = Placeholder()
  260. if (
  261. not any(
  262. any("callback" in button or "input" in button for button in row)
  263. for row in reply_markup
  264. )
  265. and not ttl
  266. ):
  267. logger.debug(
  268. "Patching form ttl to 10 minutes, because it doesn't contain any"
  269. " buttons"
  270. )
  271. ttl = 10 * 60
  272. self._units[unit_id] = {
  273. "type": "form",
  274. "text": text,
  275. "buttons": reply_markup,
  276. "chat": None,
  277. "message_id": None,
  278. "uid": unit_id,
  279. "on_unload": on_unload,
  280. "future": Event(),
  281. **({"photo": photo} if photo else {}),
  282. **({"video": video} if video else {}),
  283. **({"gif": gif} if gif else {}),
  284. **({"location": location} if location else {}),
  285. **({"audio": audio} if audio else {}),
  286. **({"location": location} if location else {}),
  287. **({"perms_map": perms_map} if perms_map else {}),
  288. **({"message": message} if isinstance(message, Message) else {}),
  289. **({"force_me": force_me} if force_me else {}),
  290. **({"disable_security": disable_security} if disable_security else {}),
  291. **({"ttl": round(time.time()) + ttl} if ttl else {}),
  292. **({"always_allow": always_allow} if always_allow else {}),
  293. }
  294. async def answer(msg: str):
  295. nonlocal message
  296. if isinstance(message, Message):
  297. await (message.edit if message.out else message.respond)(msg)
  298. else:
  299. await self._client.send_message(message, msg)
  300. try:
  301. q = await self._client.inline_query(self.bot_username, unit_id)
  302. m = await q[0].click(
  303. utils.get_chat_id(message) if isinstance(message, Message) else message,
  304. reply_to=message.reply_to_msg_id
  305. if isinstance(message, Message)
  306. else None,
  307. )
  308. except ChatSendInlineForbiddenError:
  309. await answer(
  310. self._client.loader._lookup("translations").strings("inline403")
  311. )
  312. except Exception:
  313. logger.exception("Can't send form")
  314. if not self._db.get(main.__name__, "inlinelogs", True):
  315. msg = self._client.loader._lookup("translations").strings(
  316. "invoke_failed"
  317. )
  318. else:
  319. exc = traceback.format_exc()
  320. # Remove `Traceback (most recent call last):`
  321. exc = "\n".join(exc.splitlines()[1:])
  322. msg = (
  323. "<b>🚫 Form invoke failed!</b>\n\n"
  324. f"<b>🧾 Logs:</b>\n<code>{utils.escape_html(exc)}</code>"
  325. )
  326. del self._units[unit_id]
  327. await answer(msg)
  328. return False
  329. await self._units[unit_id]["future"].wait()
  330. del self._units[unit_id]["future"]
  331. self._units[unit_id]["chat"] = utils.get_chat_id(m)
  332. self._units[unit_id]["message_id"] = m.id
  333. if isinstance(message, Message) and message.out:
  334. await message.delete()
  335. if status_message and not message.out:
  336. await status_message.delete()
  337. inline_message_id = self._units[unit_id]["inline_message_id"]
  338. msg = InlineMessage(self, unit_id, inline_message_id)
  339. if not isinstance(base_reply_markup, Placeholder):
  340. await msg.edit(text, reply_markup=base_reply_markup)
  341. return msg
  342. async def _form_inline_handler(self, inline_query: InlineQuery):
  343. try:
  344. query = inline_query.query.split()[0]
  345. except IndexError:
  346. return
  347. for unit in self._units.copy().values():
  348. for button in utils.array_sum(unit.get("buttons", [])):
  349. if (
  350. "_switch_query" in button
  351. and "input" in button
  352. and button["_switch_query"] == query
  353. and inline_query.from_user.id
  354. in [self._me]
  355. + self._client.dispatcher.security._owner
  356. + unit.get("always_allow", [])
  357. ):
  358. await inline_query.answer(
  359. [
  360. InlineQueryResultArticle(
  361. id=utils.rand(20),
  362. title=button["input"],
  363. description=(
  364. self._client.loader._lookup("translations")
  365. .strings("keep_id")
  366. .format(random.choice(VERIFICATION_EMOJIES))
  367. ),
  368. input_message_content=InputTextMessageContent(
  369. "🔄 <b>Transferring value to userbot...</b>\n"
  370. "<i>This message will be deleted automatically</i>"
  371. if inline_query.from_user.id == self._me
  372. else "🔄 <b>Transferring value to userbot...</b>",
  373. "HTML",
  374. disable_web_page_preview=True,
  375. ),
  376. )
  377. ],
  378. cache_time=60,
  379. )
  380. return
  381. if (
  382. inline_query.query not in self._units
  383. or self._units[inline_query.query]["type"] != "form"
  384. ):
  385. return
  386. # Otherwise, answer it with templated form
  387. form = self._units[inline_query.query]
  388. if "photo" in form:
  389. await inline_query.answer(
  390. [
  391. InlineQueryResultPhoto(
  392. id=utils.rand(20),
  393. title="Hikka",
  394. description="Hikka",
  395. caption=form.get("text"),
  396. parse_mode="HTML",
  397. photo_url=form["photo"],
  398. thumb_url=(
  399. "https://img.icons8.com/cotton/452/moon-satellite.png"
  400. ),
  401. reply_markup=self.generate_markup(
  402. form["uid"],
  403. ),
  404. )
  405. ],
  406. cache_time=0,
  407. )
  408. elif "gif" in form:
  409. await inline_query.answer(
  410. [
  411. InlineQueryResultGif(
  412. id=utils.rand(20),
  413. title="Hikka",
  414. caption=form.get("text"),
  415. parse_mode="HTML",
  416. gif_url=form["gif"],
  417. thumb_url=(
  418. "https://img.icons8.com/cotton/452/moon-satellite.png"
  419. ),
  420. reply_markup=self.generate_markup(
  421. form["uid"],
  422. ),
  423. )
  424. ],
  425. cache_time=0,
  426. )
  427. elif "video" in form:
  428. await inline_query.answer(
  429. [
  430. InlineQueryResultVideo(
  431. id=utils.rand(20),
  432. title="Hikka",
  433. description="Hikka",
  434. caption=form.get("text"),
  435. parse_mode="HTML",
  436. video_url=form["video"],
  437. thumb_url=(
  438. "https://img.icons8.com/cotton/452/moon-satellite.png"
  439. ),
  440. mime_type="video/mp4",
  441. reply_markup=self.generate_markup(
  442. form["uid"],
  443. ),
  444. )
  445. ],
  446. cache_time=0,
  447. )
  448. elif "file" in form:
  449. await inline_query.answer(
  450. [
  451. InlineQueryResultDocument(
  452. id=utils.rand(20),
  453. title="Hikka",
  454. description="Hikka",
  455. caption=form.get("text"),
  456. parse_mode="HTML",
  457. document_url=form["file"],
  458. mime_type=form["mime_type"],
  459. reply_markup=self.generate_markup(
  460. form["uid"],
  461. ),
  462. )
  463. ],
  464. cache_time=0,
  465. )
  466. elif "location" in form:
  467. await inline_query.answer(
  468. [
  469. InlineQueryResultLocation(
  470. id=utils.rand(20),
  471. latitude=form["location"][0],
  472. longitude=form["location"][1],
  473. title="Hikka",
  474. reply_markup=self.generate_markup(
  475. form["uid"],
  476. ),
  477. )
  478. ],
  479. cache_time=0,
  480. )
  481. elif "audio" in form:
  482. await inline_query.answer(
  483. [
  484. InlineQueryResultAudio(
  485. id=utils.rand(20),
  486. audio_url=form["audio"]["url"],
  487. caption=form.get("text"),
  488. parse_mode="HTML",
  489. title=form["audio"].get("title", "Hikka"),
  490. performer=form["audio"].get("performer"),
  491. audio_duration=form["audio"].get("duration"),
  492. reply_markup=self.generate_markup(
  493. form["uid"],
  494. ),
  495. )
  496. ],
  497. cache_time=0,
  498. )
  499. else:
  500. await inline_query.answer(
  501. [
  502. InlineQueryResultArticle(
  503. id=utils.rand(20),
  504. title="Hikka",
  505. input_message_content=InputTextMessageContent(
  506. form["text"],
  507. "HTML",
  508. disable_web_page_preview=True,
  509. ),
  510. reply_markup=self.generate_markup(inline_query.query),
  511. )
  512. ],
  513. cache_time=0,
  514. )