utils.py 40 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029103010311032103310341035103610371038103910401041104210431044104510461047104810491050105110521053105410551056105710581059106010611062106310641065106610671068106910701071107210731074107510761077107810791080108110821083108410851086108710881089109010911092109310941095109610971098109911001101110211031104110511061107110811091110111111121113111411151116111711181119112011211122112311241125112611271128112911301131113211331134113511361137113811391140114111421143114411451146114711481149115011511152115311541155115611571158115911601161116211631164116511661167116811691170117111721173117411751176117711781179118011811182118311841185118611871188118911901191119211931194119511961197119811991200120112021203120412051206120712081209121012111212121312141215121612171218121912201221122212231224122512261227122812291230123112321233123412351236123712381239124012411242124312441245124612471248124912501251125212531254125512561257125812591260126112621263126412651266126712681269127012711272127312741275127612771278127912801281128212831284128512861287128812891290129112921293129412951296129712981299130013011302130313041305130613071308130913101311131213131314131513161317131813191320132113221323132413251326132713281329133013311332133313341335133613371338133913401341134213431344134513461347134813491350135113521353135413551356135713581359136013611362136313641365136613671368136913701371137213731374137513761377137813791380138113821383138413851386138713881389139013911392139313941395139613971398139914001401140214031404140514061407140814091410141114121413141414151416141714181419142014211422142314241425142614271428142914301431143214331434143514361437143814391440144114421443
  1. """Utilities"""
  2. # Friendly Telegram (telegram userbot)
  3. # Copyright (C) 2018-2021 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 contextlib
  23. import functools
  24. import io
  25. import json
  26. import logging
  27. import os
  28. import random
  29. import re
  30. import shlex
  31. import string
  32. import time
  33. import inspect
  34. from datetime import timedelta
  35. import typing
  36. from urllib.parse import urlparse
  37. import git
  38. import grapheme
  39. import requests
  40. import telethon
  41. from telethon import hints
  42. from telethon.tl.custom.message import Message
  43. from telethon.tl.functions.account import UpdateNotifySettingsRequest
  44. from telethon.tl.functions.channels import (
  45. CreateChannelRequest,
  46. EditPhotoRequest,
  47. EditAdminRequest,
  48. InviteToChannelRequest,
  49. )
  50. from telethon.tl.functions.messages import (
  51. GetDialogFiltersRequest,
  52. UpdateDialogFilterRequest,
  53. SetHistoryTTLRequest,
  54. )
  55. from telethon.tl.types import (
  56. Channel,
  57. InputPeerNotifySettings,
  58. MessageEntityBankCard,
  59. MessageEntityBlockquote,
  60. MessageEntityBold,
  61. MessageEntityBotCommand,
  62. MessageEntityCashtag,
  63. MessageEntityCode,
  64. MessageEntityEmail,
  65. MessageEntityHashtag,
  66. MessageEntityItalic,
  67. MessageEntityMention,
  68. MessageEntityMentionName,
  69. MessageEntityPhone,
  70. MessageEntityPre,
  71. MessageEntitySpoiler,
  72. MessageEntityStrike,
  73. MessageEntityTextUrl,
  74. MessageEntityUnderline,
  75. MessageEntityUnknown,
  76. MessageEntityUrl,
  77. MessageMediaWebPage,
  78. PeerChannel,
  79. PeerChat,
  80. PeerUser,
  81. User,
  82. Chat,
  83. UpdateNewChannelMessage,
  84. ChatAdminRights,
  85. )
  86. from aiogram.types import Message as AiogramMessage
  87. from .inline.types import InlineCall, InlineMessage
  88. from .types import Module, ListLike
  89. from .tl_cache import CustomTelegramClient
  90. FormattingEntity = typing.Union[
  91. MessageEntityUnknown,
  92. MessageEntityMention,
  93. MessageEntityHashtag,
  94. MessageEntityBotCommand,
  95. MessageEntityUrl,
  96. MessageEntityEmail,
  97. MessageEntityBold,
  98. MessageEntityItalic,
  99. MessageEntityCode,
  100. MessageEntityPre,
  101. MessageEntityTextUrl,
  102. MessageEntityMentionName,
  103. MessageEntityPhone,
  104. MessageEntityCashtag,
  105. MessageEntityUnderline,
  106. MessageEntityStrike,
  107. MessageEntityBlockquote,
  108. MessageEntityBankCard,
  109. MessageEntitySpoiler,
  110. ]
  111. emoji_pattern = re.compile(
  112. "["
  113. "\U0001F600-\U0001F64F" # emoticons
  114. "\U0001F300-\U0001F5FF" # symbols & pictographs
  115. "\U0001F680-\U0001F6FF" # transport & map symbols
  116. "\U0001F1E0-\U0001F1FF" # flags (iOS)
  117. "]+",
  118. flags=re.UNICODE,
  119. )
  120. parser = telethon.utils.sanitize_parse_mode("html")
  121. logger = logging.getLogger(__name__)
  122. def get_args(message: typing.Union[Message, str]) -> typing.List[str]:
  123. """
  124. Get arguments from message
  125. :param message: Message or string to get arguments from
  126. :return: List of arguments
  127. """
  128. if not (message := getattr(message, "message", message)):
  129. return False
  130. if len(message := message.split(maxsplit=1)) <= 1:
  131. return []
  132. message = message[1]
  133. try:
  134. split = shlex.split(message)
  135. except ValueError:
  136. return message # Cannot split, let's assume that it's just one long message
  137. return list(filter(lambda x: len(x) > 0, split))
  138. def get_args_raw(message: typing.Union[Message, str]) -> str:
  139. """
  140. Get the parameters to the command as a raw string (not split)
  141. :param message: Message or string to get arguments from
  142. :return: Raw string of arguments
  143. """
  144. if not (message := getattr(message, "message", message)):
  145. return False
  146. return args[1] if len(args := message.split(maxsplit=1)) > 1 else ""
  147. def get_args_html(message: Message) -> str:
  148. """
  149. Get the parameters to the command as string with HTML (not split)
  150. :param message: Message to get arguments from
  151. :return: String with HTML arguments
  152. """
  153. prefix = message.client.loader.get_prefix()
  154. if not (message := message.text):
  155. return False
  156. if prefix not in message:
  157. return message
  158. raw_text, entities = parser.parse(message)
  159. raw_text = parser._add_surrogate(raw_text)
  160. try:
  161. command = raw_text[
  162. raw_text.index(prefix) : raw_text.index(" ", raw_text.index(prefix) + 1)
  163. ]
  164. except ValueError:
  165. return ""
  166. command_len = len(command) + 1
  167. return parser.unparse(
  168. parser._del_surrogate(raw_text[command_len:]),
  169. relocate_entities(entities, -command_len, raw_text[command_len:]),
  170. )
  171. def get_args_split_by(
  172. message: typing.Union[Message, str],
  173. separator: str,
  174. ) -> typing.List[str]:
  175. """
  176. Split args with a specific separator
  177. :param message: Message or string to get arguments from
  178. :param separator: Separator to split by
  179. :return: List of arguments
  180. """
  181. return [
  182. section.strip() for section in get_args_raw(message).split(separator) if section
  183. ]
  184. def get_chat_id(message: typing.Union[Message, AiogramMessage]) -> int:
  185. """
  186. Get the chat ID, but without -100 if its a channel
  187. :param message: Message to get chat ID from
  188. :return: Chat ID
  189. """
  190. return telethon.utils.resolve_id(
  191. getattr(message, "chat_id", None)
  192. or getattr(getattr(message, "chat", None), "id", None)
  193. )[0]
  194. def get_entity_id(entity: hints.Entity) -> int:
  195. """
  196. Get entity ID
  197. :param entity: Entity to get ID from
  198. :return: Entity ID
  199. """
  200. return telethon.utils.get_peer_id(entity)
  201. def escape_html(text: str, /) -> str: # sourcery skip
  202. """
  203. Pass all untrusted/potentially corrupt input here
  204. :param text: Text to escape
  205. :return: Escaped text
  206. """
  207. return str(text).replace("&", "&amp;").replace("<", "&lt;").replace(">", "&gt;")
  208. def escape_quotes(text: str, /) -> str:
  209. """
  210. Escape quotes to html quotes
  211. :param text: Text to escape
  212. :return: Escaped text
  213. """
  214. return escape_html(text).replace('"', "&quot;")
  215. def get_base_dir() -> str:
  216. """
  217. Get directory of this file
  218. :return: Directory of this file
  219. """
  220. return get_dir(__file__)
  221. def get_dir(mod: str) -> str:
  222. """
  223. Get directory of given module
  224. :param mod: Module's `__file__` to get directory of
  225. :return: Directory of given module
  226. """
  227. return os.path.abspath(os.path.dirname(os.path.abspath(mod)))
  228. async def get_user(message: Message) -> typing.Optional[User]:
  229. """
  230. Get user who sent message, searching if not found easily
  231. :param message: Message to get user from
  232. :return: User who sent message
  233. """
  234. try:
  235. return await message.client.get_entity(message.sender_id)
  236. except ValueError: # Not in database. Lets go looking for them.
  237. logger.debug("User not in session cache. Searching...")
  238. if isinstance(message.peer_id, PeerUser):
  239. await message.client.get_dialogs()
  240. return await message.client.get_entity(message.sender_id)
  241. if isinstance(message.peer_id, (PeerChannel, PeerChat)):
  242. try:
  243. return await message.client.get_entity(message.sender_id)
  244. except Exception:
  245. pass
  246. async for user in message.client.iter_participants(
  247. message.peer_id,
  248. aggressive=True,
  249. ):
  250. if user.id == message.sender_id:
  251. return user
  252. logger.error("User isn't in the group where they sent the message")
  253. return None
  254. logger.error("`peer_id` is not a user, chat or channel")
  255. return None
  256. def run_sync(func, *args, **kwargs):
  257. """
  258. Run a non-async function in a new thread and return an awaitable
  259. :param func: Sync-only function to execute
  260. :return: Awaitable coroutine
  261. """
  262. return asyncio.get_event_loop().run_in_executor(
  263. None,
  264. functools.partial(func, *args, **kwargs),
  265. )
  266. def run_async(loop: asyncio.AbstractEventLoop, coro: typing.Awaitable) -> typing.Any:
  267. """
  268. Run an async function as a non-async function, blocking till it's done
  269. :param loop: Event loop to run the coroutine in
  270. :param coro: Coroutine to run
  271. :return: Result of the coroutine
  272. """
  273. # When we bump minimum support to 3.7, use run()
  274. return asyncio.run_coroutine_threadsafe(coro, loop).result()
  275. def censor(
  276. obj: typing.Any,
  277. to_censor: typing.Optional[typing.Iterable[str]] = None,
  278. replace_with: str = "redacted_{count}_chars",
  279. ):
  280. """
  281. May modify the original object, but don't rely on it
  282. :param obj: Object to censor, preferrably telethon
  283. :param to_censor: Iterable of strings to censor
  284. :param replace_with: String to replace with, {count} will be replaced with the number of characters
  285. :return: Censored object
  286. """
  287. if to_censor is None:
  288. to_censor = ["phone"]
  289. for k, v in vars(obj).items():
  290. if k in to_censor:
  291. setattr(obj, k, replace_with.format(count=len(v)))
  292. elif k[0] != "_" and hasattr(v, "__dict__"):
  293. setattr(obj, k, censor(v, to_censor, replace_with))
  294. return obj
  295. def relocate_entities(
  296. entities: typing.List[FormattingEntity],
  297. offset: int,
  298. text: typing.Optional[str] = None,
  299. ) -> typing.List[FormattingEntity]:
  300. """
  301. Move all entities by offset (truncating at text)
  302. :param entities: List of entities
  303. :param offset: Offset to move by
  304. :param text: Text to truncate at
  305. :return: List of entities
  306. """
  307. length = len(text) if text is not None else 0
  308. for ent in entities.copy() if entities else ():
  309. ent.offset += offset
  310. if ent.offset < 0:
  311. ent.length += ent.offset
  312. ent.offset = 0
  313. if text is not None and ent.offset + ent.length > length:
  314. ent.length = length - ent.offset
  315. if ent.length <= 0:
  316. entities.remove(ent)
  317. return entities
  318. async def answer(
  319. message: typing.Union[Message, InlineCall, InlineMessage],
  320. response: str,
  321. *,
  322. reply_markup: typing.Optional[
  323. typing.Union[typing.List[typing.List[dict]], typing.List[dict], dict]
  324. ] = None,
  325. **kwargs,
  326. ) -> typing.Union[InlineCall, InlineMessage, Message]:
  327. """
  328. Use this to give the response to a command
  329. :param message: Message to answer to. Can be a tl message or hikka inline object
  330. :param response: Response to send
  331. :param reply_markup: Reply markup to send. If specified, inline form will be used
  332. :return: Message or inline object
  333. :example:
  334. >>> await utils.answer(message, "Hello world!")
  335. >>> await utils.answer(
  336. message,
  337. "https://some-url.com/photo.jpg",
  338. caption="Hello, this is your photo!",
  339. asfile=True,
  340. )
  341. >>> await utils.answer(
  342. message,
  343. "Hello world!",
  344. reply_markup={"text": "Hello!", "data": "world"},
  345. silent=True,
  346. disable_security=True,
  347. )
  348. """
  349. # Compatibility with FTG\GeekTG
  350. if isinstance(message, list) and message:
  351. message = message[0]
  352. if reply_markup is not None:
  353. if not isinstance(reply_markup, (list, dict)):
  354. raise ValueError("reply_markup must be a list or dict")
  355. if reply_markup:
  356. kwargs.pop("message", None)
  357. if isinstance(message, (InlineMessage, InlineCall)):
  358. await message.edit(response, reply_markup, **kwargs)
  359. return
  360. reply_markup = message.client.loader.inline._normalize_markup(reply_markup)
  361. result = await message.client.loader.inline.form(
  362. response,
  363. message=message if message.out else get_chat_id(message),
  364. reply_markup=reply_markup,
  365. **kwargs,
  366. )
  367. return result
  368. if isinstance(message, (InlineMessage, InlineCall)):
  369. await message.edit(response)
  370. return message
  371. kwargs.setdefault("link_preview", False)
  372. if not (edit := (message.out and not message.via_bot_id and not message.fwd_from)):
  373. kwargs.setdefault(
  374. "reply_to",
  375. getattr(message, "reply_to_msg_id", None),
  376. )
  377. parse_mode = telethon.utils.sanitize_parse_mode(
  378. kwargs.pop(
  379. "parse_mode",
  380. message.client.parse_mode,
  381. )
  382. )
  383. if isinstance(response, str) and not kwargs.pop("asfile", False):
  384. text, entities = parse_mode.parse(response)
  385. if len(text) >= 4096 and not hasattr(message, "hikka_grepped"):
  386. try:
  387. if not message.client.loader.inline.init_complete:
  388. raise
  389. strings = list(smart_split(text, entities, 4096))
  390. if len(strings) > 10:
  391. raise
  392. list_ = await message.client.loader.inline.list(
  393. message=message,
  394. strings=strings,
  395. )
  396. if not list_:
  397. raise
  398. return list_
  399. except Exception:
  400. file = io.BytesIO(text.encode("utf-8"))
  401. file.name = "command_result.txt"
  402. result = await message.client.send_file(
  403. message.peer_id,
  404. file,
  405. caption=message.client.loader._lookup("translations").strings(
  406. "too_long"
  407. ),
  408. )
  409. if message.out:
  410. await message.delete()
  411. return result
  412. result = await (message.edit if edit else message.respond)(
  413. text,
  414. parse_mode=lambda t: (t, entities),
  415. **kwargs,
  416. )
  417. elif isinstance(response, Message):
  418. if message.media is None and (
  419. response.media is None or isinstance(response.media, MessageMediaWebPage)
  420. ):
  421. result = await message.edit(
  422. response.message,
  423. parse_mode=lambda t: (t, response.entities or []),
  424. link_preview=isinstance(response.media, MessageMediaWebPage),
  425. )
  426. else:
  427. result = await message.respond(response, **kwargs)
  428. else:
  429. if isinstance(response, bytes):
  430. response = io.BytesIO(response)
  431. elif isinstance(response, str):
  432. response = io.BytesIO(response.encode("utf-8"))
  433. if name := kwargs.pop("filename", None):
  434. response.name = name
  435. if message.media is not None and edit:
  436. await message.edit(file=response, **kwargs)
  437. else:
  438. kwargs.setdefault(
  439. "reply_to",
  440. getattr(message, "reply_to_msg_id", None),
  441. )
  442. result = await message.client.send_file(message.peer_id, response, **kwargs)
  443. if message.out:
  444. await message.delete()
  445. return result
  446. async def get_target(message: Message, arg_no: int = 0) -> typing.Optional[int]:
  447. """
  448. Get target from message
  449. :param message: Message to get target from
  450. :param arg_no: Argument number to get target from
  451. :return: Target
  452. """
  453. if any(
  454. isinstance(entity, MessageEntityMentionName)
  455. for entity in (message.entities or [])
  456. ):
  457. e = sorted(
  458. filter(lambda x: isinstance(x, MessageEntityMentionName), message.entities),
  459. key=lambda x: x.offset,
  460. )[0]
  461. return e.user_id
  462. if len(get_args(message)) > arg_no:
  463. user = get_args(message)[arg_no]
  464. elif message.is_reply:
  465. return (await message.get_reply_message()).sender_id
  466. elif hasattr(message.peer_id, "user_id"):
  467. user = message.peer_id.user_id
  468. else:
  469. return None
  470. try:
  471. entity = await message.client.get_entity(user)
  472. except ValueError:
  473. return None
  474. else:
  475. if isinstance(entity, User):
  476. return entity.id
  477. def merge(a: dict, b: dict, /) -> dict:
  478. """
  479. Merge with replace dictionary a to dictionary b
  480. :param a: Dictionary to merge
  481. :param b: Dictionary to merge to
  482. :return: Merged dictionary
  483. """
  484. for key in a:
  485. if key in b:
  486. if isinstance(a[key], dict) and isinstance(b[key], dict):
  487. b[key] = merge(a[key], b[key])
  488. elif isinstance(a[key], list) and isinstance(b[key], list):
  489. b[key] = list(set(b[key] + a[key]))
  490. else:
  491. b[key] = a[key]
  492. b[key] = a[key]
  493. return b
  494. async def set_avatar(
  495. client: CustomTelegramClient,
  496. peer: hints.Entity,
  497. avatar: str,
  498. ) -> bool:
  499. """
  500. Sets an entity avatar
  501. :param client: Client to use
  502. :param peer: Peer to set avatar to
  503. :param avatar: Avatar to set
  504. :return: True if avatar was set, False otherwise
  505. """
  506. if isinstance(avatar, str) and check_url(avatar):
  507. f = (
  508. await run_sync(
  509. requests.get,
  510. avatar,
  511. )
  512. ).content
  513. elif isinstance(avatar, bytes):
  514. f = avatar
  515. else:
  516. return False
  517. res = await client(
  518. EditPhotoRequest(
  519. channel=peer,
  520. photo=await client.upload_file(f, file_name="photo.png"),
  521. )
  522. )
  523. try:
  524. await client.delete_messages(
  525. peer,
  526. message_ids=[
  527. next(
  528. update
  529. for update in res.updates
  530. if isinstance(update, UpdateNewChannelMessage)
  531. ).message.id
  532. ],
  533. )
  534. except Exception:
  535. pass
  536. return True
  537. async def invite_inline_bot(
  538. client: CustomTelegramClient,
  539. peer: hints.EntityLike,
  540. ) -> None:
  541. """
  542. Invites inline bot to a chat
  543. :param client: Client to use
  544. :param peer: Peer to invite bot to
  545. :return: None
  546. :raise RuntimeError: If error occurred while inviting bot
  547. """
  548. try:
  549. await client(InviteToChannelRequest(peer, [client.loader.inline.bot_username]))
  550. except Exception as e:
  551. raise RuntimeError(
  552. "Can't invite inline bot to old asset chat, which is required by module"
  553. ) from e
  554. with contextlib.suppress(Exception):
  555. await client(
  556. EditAdminRequest(
  557. channel=peer,
  558. user_id=client.loader.inline.bot_username,
  559. admin_rights=ChatAdminRights(ban_users=True),
  560. rank="Hikka",
  561. )
  562. )
  563. async def asset_channel(
  564. client: CustomTelegramClient,
  565. title: str,
  566. description: str,
  567. *,
  568. channel: bool = False,
  569. silent: bool = False,
  570. archive: bool = False,
  571. invite_bot: bool = False,
  572. avatar: typing.Optional[str] = None,
  573. ttl: typing.Optional[int] = None,
  574. _folder: typing.Optional[str] = None,
  575. ) -> typing.Tuple[Channel, bool]:
  576. """
  577. Create new channel (if needed) and return its entity
  578. :param client: Telegram client to create channel by
  579. :param title: Channel title
  580. :param description: Description
  581. :param channel: Whether to create a channel or supergroup
  582. :param silent: Automatically mute channel
  583. :param archive: Automatically archive channel
  584. :param invite_bot: Add inline bot and assure it's in chat
  585. :param avatar: Url to an avatar to set as pfp of created peer
  586. :param ttl: Time to live for messages in channel
  587. :return: Peer and bool: is channel new or pre-existent
  588. """
  589. if not hasattr(client, "_channels_cache"):
  590. client._channels_cache = {}
  591. if (
  592. title in client._channels_cache
  593. and client._channels_cache[title]["exp"] > time.time()
  594. ):
  595. return client._channels_cache[title]["peer"], False
  596. async for d in client.iter_dialogs():
  597. if d.title == title:
  598. client._channels_cache[title] = {"peer": d.entity, "exp": int(time.time())}
  599. if invite_bot:
  600. if all(
  601. participant.id != client.loader.inline.bot_id
  602. for participant in (
  603. await client.get_participants(d.entity, limit=100)
  604. )
  605. ):
  606. await invite_inline_bot(client, d.entity)
  607. return d.entity, False
  608. peer = (
  609. await client(
  610. CreateChannelRequest(
  611. title,
  612. description,
  613. megagroup=not channel,
  614. )
  615. )
  616. ).chats[0]
  617. if invite_bot:
  618. await invite_inline_bot(client, peer)
  619. if silent:
  620. await dnd(client, peer, archive)
  621. elif archive:
  622. await client.edit_folder(peer, 1)
  623. if avatar:
  624. await set_avatar(client, peer, avatar)
  625. if ttl:
  626. await client(SetHistoryTTLRequest(peer=peer, period=ttl))
  627. if _folder:
  628. if _folder != "hikka":
  629. raise NotImplementedError
  630. folders = await client(GetDialogFiltersRequest())
  631. try:
  632. folder = next(folder for folder in folders if folder.title == "hikka")
  633. except Exception:
  634. folder = None
  635. if folder is not None and not any(
  636. peer.id == getattr(folder_peer, "channel_id", None)
  637. for folder_peer in folder.include_peers
  638. ):
  639. folder.include_peers += [await client.get_input_entity(peer)]
  640. await client(
  641. UpdateDialogFilterRequest(
  642. folder.id,
  643. folder,
  644. )
  645. )
  646. client._channels_cache[title] = {"peer": peer, "exp": int(time.time())}
  647. return peer, True
  648. async def dnd(
  649. client: CustomTelegramClient,
  650. peer: hints.Entity,
  651. archive: bool = True,
  652. ) -> bool:
  653. """
  654. Mutes and optionally archives peer
  655. :param peer: Anything entity-link
  656. :param archive: Archive peer, or just mute?
  657. :return: `True` on success, otherwise `False`
  658. """
  659. try:
  660. await client(
  661. UpdateNotifySettingsRequest(
  662. peer=peer,
  663. settings=InputPeerNotifySettings(
  664. show_previews=False,
  665. silent=True,
  666. mute_until=2**31 - 1,
  667. ),
  668. )
  669. )
  670. if archive:
  671. await client.edit_folder(peer, 1)
  672. except Exception:
  673. logger.exception("utils.dnd error")
  674. return False
  675. return True
  676. def get_link(user: typing.Union[User, Channel], /) -> str:
  677. """
  678. Get telegram permalink to entity
  679. :param user: User or channel
  680. :return: Link to entity
  681. """
  682. return (
  683. f"tg://user?id={user.id}"
  684. if isinstance(user, User)
  685. else (
  686. f"tg://resolve?domain={user.username}"
  687. if getattr(user, "username", None)
  688. else ""
  689. )
  690. )
  691. def chunks(_list: ListLike, n: int, /) -> typing.List[typing.List[typing.Any]]:
  692. """
  693. Split provided `_list` into chunks of `n`
  694. :param _list: List to split
  695. :param n: Chunk size
  696. :return: List of chunks
  697. """
  698. return [_list[i : i + n] for i in range(0, len(_list), n)]
  699. def get_named_platform() -> str:
  700. """
  701. Returns formatted platform name
  702. :return: Platform name
  703. """
  704. try:
  705. if os.path.isfile("/proc/device-tree/model"):
  706. with open("/proc/device-tree/model") as f:
  707. model = f.read()
  708. if "Orange" in model:
  709. return f"🍊 {model}"
  710. return f"🍇 {model}" if "Raspberry" in model else f"❓ {model}"
  711. except Exception:
  712. # In case of weird fs, aka Termux
  713. pass
  714. try:
  715. from platform import uname
  716. if "microsoft-standard" in uname().release:
  717. return "🍁 WSL"
  718. except Exception:
  719. pass
  720. if "GOORM" in os.environ:
  721. return "🦾 GoormIDE"
  722. if "RAILWAY" in os.environ:
  723. return "🚂 Railway"
  724. if "DOCKER" in os.environ:
  725. return "🐳 Docker"
  726. if "com.termux" in os.environ.get("PREFIX", ""):
  727. return "🕶 Termux"
  728. if "OKTETO" in os.environ:
  729. return "☁️ Okteto"
  730. if "CODESPACES" in os.environ:
  731. return "🐈‍⬛ Codespaces"
  732. return f"✌️ lavHost {os.environ['LAVHOST']}" if "LAVHOST" in os.environ else "📻 VDS"
  733. def get_platform_emoji(client: typing.Optional[CustomTelegramClient] = None) -> str:
  734. """
  735. Returns custom emoji for current platform
  736. :client: It is recommended to pass the client in here in order
  737. to provide correct emojis for rtl languages. Make sure, that other strings
  738. you use, are adapted to rtl languages as well. Otherwise, the string
  739. will be broken.
  740. :return: Emoji entity in string
  741. """
  742. BASE = (
  743. "<emoji document_id={}>🌘</emoji>",
  744. "<emoji document_id=5195311729663286630>🌘</emoji>",
  745. "<emoji document_id=5195045669324201904>🌘</emoji>",
  746. )
  747. if client and (
  748. client.loader._db.get("hikka.translations", "lang", False) or ""
  749. ).startswith("ar"):
  750. BASE = tuple(reversed(BASE))
  751. BASE = "".join(BASE)
  752. if "GOORM" in os.environ:
  753. return BASE.format(5298947740032573902)
  754. if "OKTETO" in os.environ:
  755. return BASE.format(5192767786174128165)
  756. if "CODESPACES" in os.environ:
  757. return BASE.format(5194976881127989720)
  758. if "com.termux" in os.environ.get("PREFIX", ""):
  759. return BASE.format(5193051778001673828)
  760. if "RAILWAY" in os.environ:
  761. return BASE.format(5199607521593007466)
  762. return BASE.format(5192765204898783881)
  763. def uptime() -> int:
  764. """
  765. Returns userbot uptime in seconds
  766. :return: Uptime in seconds
  767. """
  768. return round(time.perf_counter() - init_ts)
  769. def formatted_uptime() -> str:
  770. """
  771. Returnes formmated uptime
  772. :return: Formatted uptime
  773. """
  774. return f"{str(timedelta(seconds=uptime()))}"
  775. def ascii_face() -> str:
  776. """
  777. Returnes cute ASCII-art face
  778. :return: ASCII-art face
  779. """
  780. return escape_html(
  781. random.choice(
  782. [
  783. "ヽ(๑◠ܫ◠๑)ノ",
  784. "(◕ᴥ◕ʋ)",
  785. "ᕙ(`▽´)ᕗ",
  786. "(✿◠‿◠)",
  787. "(▰˘◡˘▰)",
  788. "(˵ ͡° ͜ʖ ͡°˵)",
  789. "ʕっ•ᴥ•ʔっ",
  790. "( ͡° ᴥ ͡°)",
  791. "(๑•́ ヮ •̀๑)",
  792. "٩(^‿^)۶",
  793. "(っˆڡˆς)",
  794. "ψ(`∇´)ψ",
  795. "⊙ω⊙",
  796. "٩(^ᴗ^)۶",
  797. "(´・ω・)っ由",
  798. "( ͡~ ͜ʖ ͡°)",
  799. "✧♡(◕‿◕✿)",
  800. "โ๏௰๏ใ ื",
  801. "∩。• ᵕ •。∩ ♡",
  802. "(♡´౪`♡)",
  803. "(◍>◡<◍)⋈。✧♡",
  804. "╰(✿´⌣`✿)╯♡",
  805. "ʕ•ᴥ•ʔ",
  806. "ᶘ ◕ᴥ◕ᶅ",
  807. "▼・ᴥ・▼",
  808. "ฅ^•ﻌ•^ฅ",
  809. "(΄◞ิ౪◟ิ‵)",
  810. "٩(^ᴗ^)۶",
  811. "ᕴーᴥーᕵ",
  812. "ʕ→ᴥ←ʔ",
  813. "ʕᵕᴥᵕʔ",
  814. "ʕᵒᴥᵒʔ",
  815. "ᵔᴥᵔ",
  816. "(✿╹◡╹)",
  817. "(๑→ܫ←)",
  818. "ʕ·ᴥ· ʔ",
  819. "(ノ≧ڡ≦)",
  820. "(≖ᴗ≖✿)",
  821. "(〜^∇^ )〜",
  822. "( ノ・ェ・ )ノ",
  823. "~( ˘▾˘~)",
  824. "(〜^∇^)〜",
  825. "ヽ(^ᴗ^ヽ)",
  826. "(´・ω・`)",
  827. "₍ᐢ•ﻌ•ᐢ₎*・゚。",
  828. "(。・・)_且",
  829. "(=`ω´=)",
  830. "(*•‿•*)",
  831. "(*゚∀゚*)",
  832. "(☉⋆‿⋆☉)",
  833. "ɷ◡ɷ",
  834. "ʘ‿ʘ",
  835. "(。-ω-)ノ",
  836. "( ・ω・)ノ",
  837. "(=゚ω゚)ノ",
  838. "(・ε・`*) …",
  839. "ʕっ•ᴥ•ʔっ",
  840. "(*˘︶˘*)",
  841. ]
  842. )
  843. )
  844. def array_sum(
  845. array: typing.List[typing.List[typing.Any]], /
  846. ) -> typing.List[typing.Any]:
  847. """
  848. Performs basic sum operation on array
  849. :param array: Array to sum
  850. :return: Sum of array
  851. """
  852. result = []
  853. for item in array:
  854. result += item
  855. return result
  856. def rand(size: int, /) -> str:
  857. """
  858. Return random string of len `size`
  859. :param size: Length of string
  860. :return: Random string
  861. """
  862. return "".join(
  863. [random.choice("abcdefghijklmnopqrstuvwxyz1234567890") for _ in range(size)]
  864. )
  865. def smart_split(
  866. text: str,
  867. entities: typing.List[FormattingEntity],
  868. length: int = 4096,
  869. split_on: ListLike = ("\n", " "),
  870. min_length: int = 1,
  871. ) -> typing.Iterator[str]:
  872. """
  873. Split the message into smaller messages.
  874. A grapheme will never be broken. Entities will be displaced to match the right location. No inputs will be mutated.
  875. The end of each message except the last one is stripped of characters from [split_on]
  876. :param text: the plain text input
  877. :param entities: the entities
  878. :param length: the maximum length of a single message
  879. :param split_on: characters (or strings) which are preferred for a message break
  880. :param min_length: ignore any matches on [split_on] strings before this number of characters into each message
  881. :return: iterator, which returns strings
  882. :example:
  883. >>> utils.smart_split(
  884. *telethon.extensions.html.parse(
  885. "<b>Hello, world!</b>"
  886. )
  887. )
  888. <<< ["<b>Hello, world!</b>"]
  889. """
  890. # Authored by @bsolute
  891. # https://t.me/LonamiWebs/27777
  892. encoded = text.encode("utf-16le")
  893. pending_entities = entities
  894. text_offset = 0
  895. bytes_offset = 0
  896. text_length = len(text)
  897. bytes_length = len(encoded)
  898. while text_offset < text_length:
  899. if bytes_offset + length * 2 >= bytes_length:
  900. yield parser.unparse(
  901. text[text_offset:],
  902. list(sorted(pending_entities, key=lambda x: x.offset)),
  903. )
  904. break
  905. codepoint_count = len(
  906. encoded[bytes_offset : bytes_offset + length * 2].decode(
  907. "utf-16le",
  908. errors="ignore",
  909. )
  910. )
  911. for search in split_on:
  912. search_index = text.rfind(
  913. search,
  914. text_offset + min_length,
  915. text_offset + codepoint_count,
  916. )
  917. if search_index != -1:
  918. break
  919. else:
  920. search_index = text_offset + codepoint_count
  921. split_index = grapheme.safe_split_index(text, search_index)
  922. split_offset_utf16 = (
  923. len(text[text_offset:split_index].encode("utf-16le"))
  924. ) // 2
  925. exclude = 0
  926. while (
  927. split_index + exclude < text_length
  928. and text[split_index + exclude] in split_on
  929. ):
  930. exclude += 1
  931. current_entities = []
  932. entities = pending_entities.copy()
  933. pending_entities = []
  934. for entity in entities:
  935. if (
  936. entity.offset < split_offset_utf16
  937. and entity.offset + entity.length > split_offset_utf16 + exclude
  938. ):
  939. # spans boundary
  940. current_entities.append(
  941. _copy_tl(
  942. entity,
  943. length=split_offset_utf16 - entity.offset,
  944. )
  945. )
  946. pending_entities.append(
  947. _copy_tl(
  948. entity,
  949. offset=0,
  950. length=entity.offset
  951. + entity.length
  952. - split_offset_utf16
  953. - exclude,
  954. )
  955. )
  956. elif entity.offset < split_offset_utf16 < entity.offset + entity.length:
  957. # overlaps boundary
  958. current_entities.append(
  959. _copy_tl(
  960. entity,
  961. length=split_offset_utf16 - entity.offset,
  962. )
  963. )
  964. elif entity.offset < split_offset_utf16:
  965. # wholly left
  966. current_entities.append(entity)
  967. elif (
  968. entity.offset + entity.length
  969. > split_offset_utf16 + exclude
  970. > entity.offset
  971. ):
  972. # overlaps right boundary
  973. pending_entities.append(
  974. _copy_tl(
  975. entity,
  976. offset=0,
  977. length=entity.offset
  978. + entity.length
  979. - split_offset_utf16
  980. - exclude,
  981. )
  982. )
  983. elif entity.offset + entity.length > split_offset_utf16 + exclude:
  984. # wholly right
  985. pending_entities.append(
  986. _copy_tl(
  987. entity,
  988. offset=entity.offset - split_offset_utf16 - exclude,
  989. )
  990. )
  991. current_text = text[text_offset:split_index]
  992. yield parser.unparse(
  993. current_text,
  994. list(sorted(current_entities, key=lambda x: x.offset)),
  995. )
  996. text_offset = split_index + exclude
  997. bytes_offset += len(current_text.encode("utf-16le"))
  998. def _copy_tl(o, **kwargs):
  999. d = o.to_dict()
  1000. del d["_"]
  1001. d.update(kwargs)
  1002. return o.__class__(**d)
  1003. def check_url(url: str) -> bool:
  1004. """
  1005. Statically checks url for validity
  1006. :param url: URL to check
  1007. :return: True if valid, False otherwise
  1008. """
  1009. try:
  1010. return bool(urlparse(url).netloc)
  1011. except Exception:
  1012. return False
  1013. def get_git_hash() -> typing.Union[str, bool]:
  1014. """
  1015. Get current Hikka git hash
  1016. :return: Git commit hash
  1017. """
  1018. try:
  1019. return git.Repo().head.commit.hexsha
  1020. except Exception:
  1021. return False
  1022. def get_commit_url() -> str:
  1023. """
  1024. Get current Hikka git commit url
  1025. :return: Git commit url
  1026. """
  1027. try:
  1028. hash_ = get_git_hash()
  1029. return (
  1030. f'<a href="https://github.com/hikariatama/Hikka/commit/{hash_}">#{hash_[:7]}</a>'
  1031. )
  1032. except Exception:
  1033. return "Unknown"
  1034. def is_serializable(x: typing.Any, /) -> bool:
  1035. """
  1036. Checks if object is JSON-serializable
  1037. :param x: Object to check
  1038. :return: True if object is JSON-serializable, False otherwise
  1039. """
  1040. try:
  1041. json.dumps(x)
  1042. return True
  1043. except Exception:
  1044. return False
  1045. def get_lang_flag(countrycode: str) -> str:
  1046. """
  1047. Gets an emoji of specified countrycode
  1048. :param countrycode: 2-letter countrycode
  1049. :return: Emoji flag
  1050. """
  1051. if (
  1052. len(
  1053. code := [
  1054. c
  1055. for c in countrycode.lower()
  1056. if c in string.ascii_letters + string.digits
  1057. ]
  1058. )
  1059. == 2
  1060. ):
  1061. return "".join([chr(ord(c.upper()) + (ord("🇦") - ord("A"))) for c in code])
  1062. return countrycode
  1063. def get_entity_url(
  1064. entity: typing.Union[User, Channel],
  1065. openmessage: bool = False,
  1066. ) -> str:
  1067. """
  1068. Get link to object, if available
  1069. :param entity: Entity to get url of
  1070. :param openmessage: Use tg://openmessage link for users
  1071. :return: Link to object or empty string
  1072. """
  1073. return (
  1074. (
  1075. f"tg://openmessage?id={entity.id}"
  1076. if openmessage
  1077. else f"tg://user?id={entity.id}"
  1078. )
  1079. if isinstance(entity, User)
  1080. else (
  1081. f"tg://resolve?domain={entity.username}"
  1082. if getattr(entity, "username", None)
  1083. else ""
  1084. )
  1085. )
  1086. async def get_message_link(
  1087. message: Message,
  1088. chat: typing.Optional[typing.Union[Chat, Channel]] = None,
  1089. ) -> str:
  1090. """
  1091. Get link to message
  1092. :param message: Message to get link of
  1093. :param chat: Chat, where message was sent
  1094. :return: Link to message
  1095. """
  1096. if message.is_private:
  1097. return (
  1098. f"tg://openmessage?user_id={get_chat_id(message)}&message_id={message.id}"
  1099. )
  1100. if not chat:
  1101. chat = await message.get_chat()
  1102. return (
  1103. f"https://t.me/{chat.username}/{message.id}"
  1104. if getattr(chat, "username", False)
  1105. else f"https://t.me/c/{chat.id}/{message.id}"
  1106. )
  1107. def remove_html(text: str, escape: bool = False, keep_emojis: bool = False) -> str:
  1108. """
  1109. Removes HTML tags from text
  1110. :param text: Text to remove HTML from
  1111. :param escape: Escape HTML
  1112. :param keep_emojis: Keep custom emojis
  1113. :return: Text without HTML
  1114. """
  1115. return (escape_html if escape else str)(
  1116. re.sub(
  1117. r"(<\/?a.*?>|<\/?b>|<\/?i>|<\/?u>|<\/?strong>|<\/?em>|<\/?code>|<\/?strike>|<\/?del>|<\/?pre.*?>)"
  1118. if keep_emojis
  1119. else r"(<\/?a.*?>|<\/?b>|<\/?i>|<\/?u>|<\/?strong>|<\/?em>|<\/?code>|<\/?strike>|<\/?del>|<\/?pre.*?>|<\/?emoji.*?>)",
  1120. "",
  1121. text,
  1122. )
  1123. )
  1124. def get_kwargs() -> typing.Dict[str, typing.Any]:
  1125. """
  1126. Get kwargs of function, in which is called
  1127. :return: kwargs
  1128. """
  1129. # https://stackoverflow.com/a/65927265/19170642
  1130. keys, _, _, values = inspect.getargvalues(inspect.currentframe().f_back)
  1131. return {key: values[key] for key in keys if key != "self"}
  1132. def mime_type(message: Message) -> str:
  1133. """
  1134. Get mime type of document in message
  1135. :param message: Message with document
  1136. :return: Mime type or empty string if not present
  1137. """
  1138. return (
  1139. ""
  1140. if not isinstance(message, Message) or not getattr(message, "media", False)
  1141. else getattr(getattr(message, "media", False), "mime_type", False) or ""
  1142. )
  1143. def find_caller(
  1144. stack: typing.Optional[typing.List[inspect.FrameInfo]] = None,
  1145. ) -> typing.Any:
  1146. """
  1147. Attempts to find command in stack
  1148. :param stack: Stack to search in
  1149. :return: Command-caller or None
  1150. """
  1151. caller = next(
  1152. (
  1153. frame_info
  1154. for frame_info in stack or inspect.stack()
  1155. if hasattr(frame_info, "function")
  1156. and any(
  1157. inspect.isclass(cls_)
  1158. and issubclass(cls_, Module)
  1159. and cls_ is not Module
  1160. for cls_ in frame_info.frame.f_globals.values()
  1161. )
  1162. ),
  1163. None,
  1164. )
  1165. if not caller:
  1166. return next(
  1167. (
  1168. frame_info.frame.f_locals["func"]
  1169. for frame_info in stack or inspect.stack()
  1170. if hasattr(frame_info, "function")
  1171. and frame_info.function == "future_dispatcher"
  1172. and (
  1173. "CommandDispatcher"
  1174. in getattr(getattr(frame_info, "frame", None), "f_globals", {})
  1175. )
  1176. ),
  1177. None,
  1178. )
  1179. return next(
  1180. (
  1181. getattr(cls_, caller.function, None)
  1182. for cls_ in caller.frame.f_globals.values()
  1183. if inspect.isclass(cls_) and issubclass(cls_, Module)
  1184. ),
  1185. None,
  1186. )
  1187. def validate_html(html: str) -> str:
  1188. """
  1189. Removes broken tags from html
  1190. :param html: HTML to validate
  1191. :return: Valid HTML
  1192. """
  1193. text, entities = telethon.extensions.html.parse(html)
  1194. return telethon.extensions.html.unparse(escape_html(text), entities)
  1195. def iter_attrs(obj: typing.Any, /) -> typing.Iterator[typing.Tuple[str, typing.Any]]:
  1196. """
  1197. Iterates over attributes of object
  1198. :param obj: Object to iterate over
  1199. :return: Iterator of attributes and their values
  1200. """
  1201. return ((attr, getattr(obj, attr)) for attr in dir(obj))
  1202. init_ts = time.perf_counter()
  1203. # GeekTG Compatibility
  1204. def get_git_info() -> typing.Tuple[str, str]:
  1205. """
  1206. Get git info
  1207. :return: Git info
  1208. """
  1209. hash_ = get_git_hash()
  1210. return (
  1211. hash_,
  1212. f"https://github.com/hikariatama/Hikka/commit/{hash_}" if hash_ else "",
  1213. )
  1214. def get_version_raw() -> str:
  1215. """
  1216. Get the version of the userbot
  1217. :return: Version in format %s.%s.%s
  1218. """
  1219. from . import version
  1220. return ".".join(map(str, list(version.__version__)))
  1221. get_platform_name = get_named_platform