pyroproxy.py 10 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325
  1. # ©️ Dan Gazizullin, 2021-2023
  2. # This file is a part of Hikka Userbot
  3. # 🌐 https://github.com/hikariatama/Hikka
  4. # You can redistribute it and/or modify it under the terms of the GNU AGPLv3
  5. # 🔑 https://www.gnu.org/licenses/agpl-3.0.html
  6. import asyncio
  7. import copy
  8. import datetime
  9. import functools
  10. import logging
  11. import re
  12. import typing
  13. import hikkatl
  14. from hikkapyro import Client as PyroClient
  15. from hikkapyro import errors as pyro_errors
  16. from hikkapyro import raw
  17. from .. import utils
  18. from ..tl_cache import CustomTelegramClient
  19. from ..version import __version__
  20. PROXY = {
  21. pyro_object: hikkatl.tl.alltlobjects.tlobjects[constructor_id]
  22. for constructor_id, pyro_object in raw.all.objects.items()
  23. if constructor_id in hikkatl.tl.alltlobjects.tlobjects
  24. }
  25. REVERSED_PROXY = {
  26. **{tl_object: pyro_object for pyro_object, tl_object in PROXY.items()},
  27. **{
  28. tl_object: raw.all.objects[tl_object.CONSTRUCTOR_ID]
  29. for _, tl_object in utils.iter_attrs(hikkatl.tl.custom)
  30. if getattr(tl_object, "CONSTRUCTOR_ID", None) in raw.all.objects
  31. },
  32. }
  33. PYRO_ERRORS = {
  34. cls.ID: cls
  35. for _, cls in utils.iter_attrs(pyro_errors)
  36. if hasattr(cls, "ID") and issubclass(cls, pyro_errors.RPCError)
  37. }
  38. logger = logging.getLogger(__name__)
  39. class PyroProxyClient(PyroClient):
  40. """
  41. Pyrogram client that redirects all requests to telethon's handler
  42. :param tl_client: telethon client
  43. :type tl_client: CustomTelegramClient
  44. """
  45. def __init__(self, tl_client: CustomTelegramClient):
  46. self.tl_client = tl_client
  47. super().__init__(
  48. name="proxied_pyrogram_client",
  49. api_id=tl_client.api_id,
  50. api_hash=tl_client.api_hash,
  51. app_version=".".join(map(str, __version__)) + " x64",
  52. lang_code="en",
  53. in_memory=True,
  54. phone_number=tl_client.hikka_me.phone,
  55. )
  56. # We need to set this to True so pyro thinks he's connected
  57. # even tho it's not. We don't need to connect to Telegram as
  58. # we redirect all requests to telethon's handler
  59. self.is_connected = True
  60. self.conn = tl_client.session._conn
  61. async def start(self):
  62. """
  63. Starts the client
  64. :return: None
  65. """
  66. self.me = await self.get_me()
  67. self.tl_client.raw_updates_processor = self._on_event
  68. def _on_event(
  69. self,
  70. event: typing.Union[
  71. hikkatl.tl.types.Updates,
  72. hikkatl.tl.types.UpdatesCombined,
  73. hikkatl.tl.types.UpdateShort,
  74. ],
  75. ):
  76. asyncio.ensure_future(self.handle_updates(self._tl2pyro(event)))
  77. async def invoke(
  78. self,
  79. query: raw.core.TLObject,
  80. *args,
  81. **kwargs,
  82. ) -> typing.Union[typing.List[raw.core.TLObject], raw.core.TLObject]:
  83. """
  84. Invokes a Pyrogram request through telethon
  85. :param query: Pyrogram request
  86. :return: Pyrogram response
  87. :raises RPCError: if Telegram returns an error
  88. :rtype: typing.Union[typing.List[raw.core.TLObject], raw.core.TLObject]
  89. """
  90. logger.debug(
  91. "Running Pyrogram's invoke of %s with Telethon proxying",
  92. query.__class__.__name__,
  93. )
  94. if self.tl_client.session.takeout_id:
  95. query = raw.functions.InvokeWithTakeout(
  96. takeout_id=self.tl_client.session.takeout_id,
  97. query=query,
  98. )
  99. try:
  100. r = await self.tl_client(self._pyro2tl(query))
  101. except hikkatl.errors.rpcerrorlist.RPCError as e:
  102. raise self._tl_error2pyro(e)
  103. return self._tl2pyro(r)
  104. @staticmethod
  105. def _tl_error2pyro(
  106. error: hikkatl.errors.rpcerrorlist.RPCError,
  107. ) -> pyro_errors.RPCError:
  108. rpc = (
  109. re.sub(r"([A-Z])", r"_\1", error.__class__.__name__)
  110. .upper()
  111. .strip("_")
  112. .rsplit("ERROR", maxsplit=1)[0]
  113. .strip("_")
  114. )
  115. if rpc in PYRO_ERRORS:
  116. return PYRO_ERRORS[rpc]()
  117. return PYRO_ERRORS.get(
  118. f"{rpc}_X",
  119. PYRO_ERRORS.get(
  120. f"{rpc}_0",
  121. pyro_errors.RPCError,
  122. ),
  123. )()
  124. def _pyro2tl(self, pyro_obj: raw.core.TLObject) -> hikkatl.tl.TLObject:
  125. """
  126. Recursively converts Pyrogram TLObjects to Telethon TLObjects (methods,
  127. types and everything else, which is in tl schema)
  128. :param pyro_obj: Pyrogram TLObject
  129. :return: Telethon TLObject
  130. :raises TypeError: if it's not possible to convert Pyrogram TLObject to
  131. Telethon TLObject
  132. """
  133. pyro_obj = self._convert(pyro_obj)
  134. if isinstance(pyro_obj, list):
  135. return [self._pyro2tl(i) for i in pyro_obj]
  136. if isinstance(pyro_obj, dict):
  137. return {k: self._pyro2tl(v) for k, v in pyro_obj.items()}
  138. if not isinstance(pyro_obj, raw.core.TLObject):
  139. return pyro_obj
  140. if type(pyro_obj) not in PROXY:
  141. raise TypeError(
  142. f"Cannot convert Pyrogram's {type(pyro_obj)} to Telethon TLObject"
  143. )
  144. return PROXY[type(pyro_obj)](
  145. **{
  146. attr: self._pyro2tl(getattr(pyro_obj, attr))
  147. for attr in pyro_obj.__slots__
  148. }
  149. )
  150. def _tl2pyro(self, tl_obj: hikkatl.tl.TLObject) -> raw.core.TLObject:
  151. """
  152. Recursively converts Telethon TLObjects to Pyrogram TLObjects (methods,
  153. types and everything else, which is in tl schema)
  154. :param tl_obj: Telethon TLObject
  155. :return: Pyrogram TLObject
  156. :raises TypeError: if it's not possible to convert Telethon TLObject to
  157. Pyrogram TLObject
  158. """
  159. tl_obj = self._convert(tl_obj)
  160. if (
  161. isinstance(getattr(tl_obj, "from_id", None), int)
  162. and tl_obj.from_id
  163. and hasattr(tl_obj, "sender_id")
  164. ):
  165. tl_obj = copy.copy(tl_obj)
  166. tl_obj.from_id = hikkatl.tl.types.PeerUser(tl_obj.sender_id)
  167. if isinstance(tl_obj, list):
  168. return [self._tl2pyro(i) for i in tl_obj]
  169. if isinstance(tl_obj, dict):
  170. return {k: self._tl2pyro(v) for k, v in tl_obj.items()}
  171. if isinstance(tl_obj, int) and str(tl_obj).startswith("-100"):
  172. return int(str(tl_obj)[4:])
  173. if not isinstance(tl_obj, hikkatl.tl.TLObject):
  174. return tl_obj
  175. if type(tl_obj) not in REVERSED_PROXY:
  176. raise TypeError(
  177. f"Cannot convert Telethon's {type(tl_obj)} to Pyrogram TLObject"
  178. )
  179. hints = typing.get_type_hints(REVERSED_PROXY[type(tl_obj)].__init__) or {}
  180. return REVERSED_PROXY[type(tl_obj)](
  181. **{
  182. attr: self._convert_types(
  183. hints.get(attr),
  184. self._tl2pyro(getattr(tl_obj, attr)),
  185. )
  186. for attr in REVERSED_PROXY[type(tl_obj)].__slots__
  187. }
  188. )
  189. @staticmethod
  190. def _get_origin(hint: typing.Any) -> typing.Any:
  191. try:
  192. return typing.get_origin(hint)
  193. except Exception:
  194. return None
  195. def _convert_types(self, hint: typing.Any, value: typing.Any) -> typing.Any:
  196. if not value and (
  197. self._get_origin(hint) in {typing.List, list}
  198. or (
  199. self._get_origin(hint) is typing.Union
  200. and any(
  201. self._get_origin(i) in {typing.List, list} for i in hint.__args__
  202. )
  203. )
  204. ):
  205. return []
  206. return value
  207. def _convert(self, obj: typing.Any) -> typing.Any:
  208. if isinstance(obj, datetime.datetime):
  209. return int(obj.timestamp())
  210. return obj
  211. async def resolve_peer(
  212. self,
  213. *args,
  214. **kwargs,
  215. ) -> "typing.Union[hikkapyro.raw.types.PeerChat, hikkapyro.raw.types.PeerChannel, hikkapyro.raw.types.PeerUser]": # type: ignore # noqa: E501, F821
  216. """
  217. Resolve a peer (user, chat or channel) from the given input.
  218. :param args: Arguments to pass to the Telethon client's
  219. :return: The resolved peer
  220. :rtype: typing.Union[hikkapyro.raw.types.PeerChat, hikkapyro.raw.types.PeerChannel, hikkapyro.raw.types.PeerUser]
  221. """
  222. return self._tl2pyro(await self.tl_client.get_entity(*args, **kwargs))
  223. async def fetch_peers(
  224. self,
  225. peers: typing.List[
  226. typing.Union[raw.types.User, raw.types.Chat, raw.types.Channel]
  227. ],
  228. ) -> bool:
  229. """
  230. Fetches the given peers (users, chats or channels) from the server.
  231. :param peers: List of peers to fetch
  232. :return: True if the peers were fetched successfully
  233. :rtype: bool
  234. """
  235. return any(getattr(peer, "min", False) for peer in peers)
  236. @property
  237. def iter_chat_members(self):
  238. """Alias for :obj:`get_chat_members <pyrogram.Client.get_chat_members>`"""
  239. return self.get_chat_members
  240. @property
  241. def iter_dialogs(self):
  242. """Alias for :obj:`get_dialogs <pyrogram.Client.get_dialogs>`"""
  243. return self.get_dialogs
  244. @property
  245. def iter_history(self):
  246. """Alias for :obj:`get_history <pyrogram.Client.get_history>`"""
  247. return self.get_chat_history
  248. @property
  249. def iter_profile_photos(self):
  250. """Alias for :obj:`get_profile_photos <pyrogram.Client.get_profile_photos>`"""
  251. return self.get_chat_photos
  252. async def save_file(
  253. self,
  254. path: typing.Union[str, typing.BinaryIO],
  255. file_id: int = None,
  256. file_part: int = 0,
  257. progress: typing.Callable = None,
  258. progress_args: tuple = (),
  259. ) -> raw.types.InputFileLocation:
  260. """
  261. Save a file to the given path.
  262. :param path: The path to save the file to
  263. :param file_id: The file ID to save
  264. :param file_part: The file part to save
  265. :param progress: A callback function to track the progress
  266. :param progress_args: Arguments to pass to the progress callback
  267. :return: The file location
  268. :rtype: :obj:`InputFileLocation <pyrogram.api.types.InputFileLocation>`
  269. """
  270. return self._tl2pyro(
  271. await self.tl_client.upload_file(
  272. path,
  273. part_size_kb=file_part,
  274. progress_callback=(
  275. functools.partial(progress, *progress_args)
  276. if progress and callable(progress)
  277. else None
  278. ),
  279. )
  280. )