utils.py 33 KB

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