spluspy 2.2.7__tar.gz → 2.2.8__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (162) hide show
  1. {spluspy-2.2.7/spluspy.egg-info → spluspy-2.2.8}/PKG-INFO +3 -2
  2. {spluspy-2.2.7 → spluspy-2.2.8}/README.md +2 -1
  3. {spluspy-2.2.7 → spluspy-2.2.8}/pyproject.toml +1 -1
  4. {spluspy-2.2.7 → spluspy-2.2.8}/spluspy/__init__.py +2 -1
  5. {spluspy-2.2.7 → spluspy-2.2.8}/spluspy/client/downloads.py +13 -2
  6. {spluspy-2.2.7 → spluspy-2.2.8}/spluspy/client/extra.py +16 -3
  7. {spluspy-2.2.7 → spluspy-2.2.8}/spluspy/client/telegrambaseclient.py +1 -1
  8. {spluspy-2.2.7 → spluspy-2.2.8}/spluspy/client/uploads.py +111 -90
  9. spluspy-2.2.8/spluspy/enums.py +144 -0
  10. {spluspy-2.2.7 → spluspy-2.2.8}/spluspy/network/mtprotosender.py +51 -2
  11. {spluspy-2.2.7 → spluspy-2.2.8}/spluspy/tl/custom/message.py +22 -0
  12. {spluspy-2.2.7 → spluspy-2.2.8}/spluspy/utils.py +1 -9
  13. {spluspy-2.2.7 → spluspy-2.2.8}/spluspy/version.py +1 -1
  14. {spluspy-2.2.7 → spluspy-2.2.8/spluspy.egg-info}/PKG-INFO +3 -2
  15. spluspy-2.2.7/spluspy/enums.py +0 -17
  16. {spluspy-2.2.7 → spluspy-2.2.8}/LICENSE +0 -0
  17. {spluspy-2.2.7 → spluspy-2.2.8}/setup.cfg +0 -0
  18. {spluspy-2.2.7 → spluspy-2.2.8}/spluspy/_updates/__init__.py +0 -0
  19. {spluspy-2.2.7 → spluspy-2.2.8}/spluspy/_updates/entitycache.py +0 -0
  20. {spluspy-2.2.7 → spluspy-2.2.8}/spluspy/_updates/messagebox.py +0 -0
  21. {spluspy-2.2.7 → spluspy-2.2.8}/spluspy/_updates/session.py +0 -0
  22. {spluspy-2.2.7 → spluspy-2.2.8}/spluspy/admin.py +0 -0
  23. {spluspy-2.2.7 → spluspy-2.2.8}/spluspy/client/__init__.py +0 -0
  24. {spluspy-2.2.7 → spluspy-2.2.8}/spluspy/client/account.py +0 -0
  25. {spluspy-2.2.7 → spluspy-2.2.8}/spluspy/client/auth.py +0 -0
  26. {spluspy-2.2.7 → spluspy-2.2.8}/spluspy/client/buttons.py +0 -0
  27. {spluspy-2.2.7 → spluspy-2.2.8}/spluspy/client/chats.py +0 -0
  28. {spluspy-2.2.7 → spluspy-2.2.8}/spluspy/client/dialogs.py +0 -0
  29. {spluspy-2.2.7 → spluspy-2.2.8}/spluspy/client/messageparse.py +0 -0
  30. {spluspy-2.2.7 → spluspy-2.2.8}/spluspy/client/messages.py +0 -0
  31. {spluspy-2.2.7 → spluspy-2.2.8}/spluspy/client/soroushclient.py +0 -0
  32. {spluspy-2.2.7 → spluspy-2.2.8}/spluspy/client/updates.py +0 -0
  33. {spluspy-2.2.7 → spluspy-2.2.8}/spluspy/client/users.py +0 -0
  34. {spluspy-2.2.7 → spluspy-2.2.8}/spluspy/crypto/__init__.py +0 -0
  35. {spluspy-2.2.7 → spluspy-2.2.8}/spluspy/crypto/aes.py +0 -0
  36. {spluspy-2.2.7 → spluspy-2.2.8}/spluspy/crypto/aesctr.py +0 -0
  37. {spluspy-2.2.7 → spluspy-2.2.8}/spluspy/crypto/authkey.py +0 -0
  38. {spluspy-2.2.7 → spluspy-2.2.8}/spluspy/crypto/cdndecrypter.py +0 -0
  39. {spluspy-2.2.7 → spluspy-2.2.8}/spluspy/crypto/factorization.py +0 -0
  40. {spluspy-2.2.7 → spluspy-2.2.8}/spluspy/crypto/libssl.py +0 -0
  41. {spluspy-2.2.7 → spluspy-2.2.8}/spluspy/crypto/rsa.py +0 -0
  42. {spluspy-2.2.7 → spluspy-2.2.8}/spluspy/custom.py +0 -0
  43. {spluspy-2.2.7 → spluspy-2.2.8}/spluspy/errors/__init__.py +0 -0
  44. {spluspy-2.2.7 → spluspy-2.2.8}/spluspy/errors/common.py +0 -0
  45. {spluspy-2.2.7 → spluspy-2.2.8}/spluspy/errors/exceptions.py +0 -0
  46. {spluspy-2.2.7 → spluspy-2.2.8}/spluspy/errors/rpcbaseerrors.py +0 -0
  47. {spluspy-2.2.7 → spluspy-2.2.8}/spluspy/errors/rpcerrorlist.py +0 -0
  48. {spluspy-2.2.7 → spluspy-2.2.8}/spluspy/events/__init__.py +0 -0
  49. {spluspy-2.2.7 → spluspy-2.2.8}/spluspy/events/album.py +0 -0
  50. {spluspy-2.2.7 → spluspy-2.2.8}/spluspy/events/chataction.py +0 -0
  51. {spluspy-2.2.7 → spluspy-2.2.8}/spluspy/events/common.py +0 -0
  52. {spluspy-2.2.7 → spluspy-2.2.8}/spluspy/events/messagedeleted.py +0 -0
  53. {spluspy-2.2.7 → spluspy-2.2.8}/spluspy/events/messageedited.py +0 -0
  54. {spluspy-2.2.7 → spluspy-2.2.8}/spluspy/events/messageread.py +0 -0
  55. {spluspy-2.2.7 → spluspy-2.2.8}/spluspy/events/newmessage.py +0 -0
  56. {spluspy-2.2.7 → spluspy-2.2.8}/spluspy/events/raw.py +0 -0
  57. {spluspy-2.2.7 → spluspy-2.2.8}/spluspy/events/userupdate.py +0 -0
  58. {spluspy-2.2.7 → spluspy-2.2.8}/spluspy/extensions/__init__.py +0 -0
  59. {spluspy-2.2.7 → spluspy-2.2.8}/spluspy/extensions/binaryreader.py +0 -0
  60. {spluspy-2.2.7 → spluspy-2.2.8}/spluspy/extensions/html.py +0 -0
  61. {spluspy-2.2.7 → spluspy-2.2.8}/spluspy/extensions/markdown.py +0 -0
  62. {spluspy-2.2.7 → spluspy-2.2.8}/spluspy/extensions/messagepacker.py +0 -0
  63. {spluspy-2.2.7 → spluspy-2.2.8}/spluspy/filters.py +0 -0
  64. {spluspy-2.2.7 → spluspy-2.2.8}/spluspy/functions.py +0 -0
  65. {spluspy-2.2.7 → spluspy-2.2.8}/spluspy/helpers.py +0 -0
  66. {spluspy-2.2.7 → spluspy-2.2.8}/spluspy/hints.py +0 -0
  67. {spluspy-2.2.7 → spluspy-2.2.8}/spluspy/network/__init__.py +0 -0
  68. {spluspy-2.2.7 → spluspy-2.2.8}/spluspy/network/authenticator.py +0 -0
  69. {spluspy-2.2.7 → spluspy-2.2.8}/spluspy/network/connection/__init__.py +0 -0
  70. {spluspy-2.2.7 → spluspy-2.2.8}/spluspy/network/connection/connection.py +0 -0
  71. {spluspy-2.2.7 → spluspy-2.2.8}/spluspy/network/connection/http.py +0 -0
  72. {spluspy-2.2.7 → spluspy-2.2.8}/spluspy/network/connection/tcpabridged.py +0 -0
  73. {spluspy-2.2.7 → spluspy-2.2.8}/spluspy/network/connection/tcpfull.py +0 -0
  74. {spluspy-2.2.7 → spluspy-2.2.8}/spluspy/network/connection/tcpintermediate.py +0 -0
  75. {spluspy-2.2.7 → spluspy-2.2.8}/spluspy/network/connection/tcpmtproxy.py +0 -0
  76. {spluspy-2.2.7 → spluspy-2.2.8}/spluspy/network/connection/tcpobfuscated.py +0 -0
  77. {spluspy-2.2.7 → spluspy-2.2.8}/spluspy/network/connection/websocket.py +0 -0
  78. {spluspy-2.2.7 → spluspy-2.2.8}/spluspy/network/mtprotoplainsender.py +0 -0
  79. {spluspy-2.2.7 → spluspy-2.2.8}/spluspy/network/mtprotostate.py +0 -0
  80. {spluspy-2.2.7 → spluspy-2.2.8}/spluspy/network/requeststate.py +0 -0
  81. {spluspy-2.2.7 → spluspy-2.2.8}/spluspy/password.py +0 -0
  82. {spluspy-2.2.7 → spluspy-2.2.8}/spluspy/requestiter.py +0 -0
  83. {spluspy-2.2.7 → spluspy-2.2.8}/spluspy/resolvers.py +0 -0
  84. {spluspy-2.2.7 → spluspy-2.2.8}/spluspy/sessions/__init__.py +0 -0
  85. {spluspy-2.2.7 → spluspy-2.2.8}/spluspy/sessions/abstract.py +0 -0
  86. {spluspy-2.2.7 → spluspy-2.2.8}/spluspy/sessions/memory.py +0 -0
  87. {spluspy-2.2.7 → spluspy-2.2.8}/spluspy/sessions/sqlite.py +0 -0
  88. {spluspy-2.2.7 → spluspy-2.2.8}/spluspy/sessions/string.py +0 -0
  89. {spluspy-2.2.7 → spluspy-2.2.8}/spluspy/sync.py +0 -0
  90. {spluspy-2.2.7 → spluspy-2.2.8}/spluspy/tl/__init__.py +0 -0
  91. {spluspy-2.2.7 → spluspy-2.2.8}/spluspy/tl/alltlobjects.py +0 -0
  92. {spluspy-2.2.7 → spluspy-2.2.8}/spluspy/tl/core/__init__.py +0 -0
  93. {spluspy-2.2.7 → spluspy-2.2.8}/spluspy/tl/core/gzippacked.py +0 -0
  94. {spluspy-2.2.7 → spluspy-2.2.8}/spluspy/tl/core/messagecontainer.py +0 -0
  95. {spluspy-2.2.7 → spluspy-2.2.8}/spluspy/tl/core/rpcresult.py +0 -0
  96. {spluspy-2.2.7 → spluspy-2.2.8}/spluspy/tl/core/tlmessage.py +0 -0
  97. {spluspy-2.2.7 → spluspy-2.2.8}/spluspy/tl/custom/__init__.py +0 -0
  98. {spluspy-2.2.7 → spluspy-2.2.8}/spluspy/tl/custom/adminlogevent.py +0 -0
  99. {spluspy-2.2.7 → spluspy-2.2.8}/spluspy/tl/custom/button.py +0 -0
  100. {spluspy-2.2.7 → spluspy-2.2.8}/spluspy/tl/custom/chatgetter.py +0 -0
  101. {spluspy-2.2.7 → spluspy-2.2.8}/spluspy/tl/custom/conversation.py +0 -0
  102. {spluspy-2.2.7 → spluspy-2.2.8}/spluspy/tl/custom/dialog.py +0 -0
  103. {spluspy-2.2.7 → spluspy-2.2.8}/spluspy/tl/custom/draft.py +0 -0
  104. {spluspy-2.2.7 → spluspy-2.2.8}/spluspy/tl/custom/file.py +0 -0
  105. {spluspy-2.2.7 → spluspy-2.2.8}/spluspy/tl/custom/forward.py +0 -0
  106. {spluspy-2.2.7 → spluspy-2.2.8}/spluspy/tl/custom/inputsizedfile.py +0 -0
  107. {spluspy-2.2.7 → spluspy-2.2.8}/spluspy/tl/custom/messagebutton.py +0 -0
  108. {spluspy-2.2.7 → spluspy-2.2.8}/spluspy/tl/custom/participantpermissions.py +0 -0
  109. {spluspy-2.2.7 → spluspy-2.2.8}/spluspy/tl/custom/qrlogin.py +0 -0
  110. {spluspy-2.2.7 → spluspy-2.2.8}/spluspy/tl/custom/sendergetter.py +0 -0
  111. {spluspy-2.2.7 → spluspy-2.2.8}/spluspy/tl/custom/types.py +0 -0
  112. {spluspy-2.2.7 → spluspy-2.2.8}/spluspy/tl/functions/__init__.py +0 -0
  113. {spluspy-2.2.7 → spluspy-2.2.8}/spluspy/tl/functions/account.py +0 -0
  114. {spluspy-2.2.7 → spluspy-2.2.8}/spluspy/tl/functions/auth.py +0 -0
  115. {spluspy-2.2.7 → spluspy-2.2.8}/spluspy/tl/functions/bots.py +0 -0
  116. {spluspy-2.2.7 → spluspy-2.2.8}/spluspy/tl/functions/channels.py +0 -0
  117. {spluspy-2.2.7 → spluspy-2.2.8}/spluspy/tl/functions/chatlists.py +0 -0
  118. {spluspy-2.2.7 → spluspy-2.2.8}/spluspy/tl/functions/conference.py +0 -0
  119. {spluspy-2.2.7 → spluspy-2.2.8}/spluspy/tl/functions/contacts.py +0 -0
  120. {spluspy-2.2.7 → spluspy-2.2.8}/spluspy/tl/functions/folders.py +0 -0
  121. {spluspy-2.2.7 → spluspy-2.2.8}/spluspy/tl/functions/help.py +0 -0
  122. {spluspy-2.2.7 → spluspy-2.2.8}/spluspy/tl/functions/langpack.py +0 -0
  123. {spluspy-2.2.7 → spluspy-2.2.8}/spluspy/tl/functions/messages.py +0 -0
  124. {spluspy-2.2.7 → spluspy-2.2.8}/spluspy/tl/functions/payments.py +0 -0
  125. {spluspy-2.2.7 → spluspy-2.2.8}/spluspy/tl/functions/phone.py +0 -0
  126. {spluspy-2.2.7 → spluspy-2.2.8}/spluspy/tl/functions/photos.py +0 -0
  127. {spluspy-2.2.7 → spluspy-2.2.8}/spluspy/tl/functions/premium.py +0 -0
  128. {spluspy-2.2.7 → spluspy-2.2.8}/spluspy/tl/functions/stats.py +0 -0
  129. {spluspy-2.2.7 → spluspy-2.2.8}/spluspy/tl/functions/stories.py +0 -0
  130. {spluspy-2.2.7 → spluspy-2.2.8}/spluspy/tl/functions/thirdParty.py +0 -0
  131. {spluspy-2.2.7 → spluspy-2.2.8}/spluspy/tl/functions/updates.py +0 -0
  132. {spluspy-2.2.7 → spluspy-2.2.8}/spluspy/tl/functions/upload.py +0 -0
  133. {spluspy-2.2.7 → spluspy-2.2.8}/spluspy/tl/functions/users.py +0 -0
  134. {spluspy-2.2.7 → spluspy-2.2.8}/spluspy/tl/patched/__init__.py +0 -0
  135. {spluspy-2.2.7 → spluspy-2.2.8}/spluspy/tl/tlobject.py +0 -0
  136. {spluspy-2.2.7 → spluspy-2.2.8}/spluspy/tl/types/__init__.py +0 -0
  137. {spluspy-2.2.7 → spluspy-2.2.8}/spluspy/tl/types/account.py +0 -0
  138. {spluspy-2.2.7 → spluspy-2.2.8}/spluspy/tl/types/auth.py +0 -0
  139. {spluspy-2.2.7 → spluspy-2.2.8}/spluspy/tl/types/bots.py +0 -0
  140. {spluspy-2.2.7 → spluspy-2.2.8}/spluspy/tl/types/channels.py +0 -0
  141. {spluspy-2.2.7 → spluspy-2.2.8}/spluspy/tl/types/chatlists.py +0 -0
  142. {spluspy-2.2.7 → spluspy-2.2.8}/spluspy/tl/types/conference.py +0 -0
  143. {spluspy-2.2.7 → spluspy-2.2.8}/spluspy/tl/types/contacts.py +0 -0
  144. {spluspy-2.2.7 → spluspy-2.2.8}/spluspy/tl/types/help.py +0 -0
  145. {spluspy-2.2.7 → spluspy-2.2.8}/spluspy/tl/types/messages.py +0 -0
  146. {spluspy-2.2.7 → spluspy-2.2.8}/spluspy/tl/types/payments.py +0 -0
  147. {spluspy-2.2.7 → spluspy-2.2.8}/spluspy/tl/types/phone.py +0 -0
  148. {spluspy-2.2.7 → spluspy-2.2.8}/spluspy/tl/types/photos.py +0 -0
  149. {spluspy-2.2.7 → spluspy-2.2.8}/spluspy/tl/types/premium.py +0 -0
  150. {spluspy-2.2.7 → spluspy-2.2.8}/spluspy/tl/types/stats.py +0 -0
  151. {spluspy-2.2.7 → spluspy-2.2.8}/spluspy/tl/types/stickers.py +0 -0
  152. {spluspy-2.2.7 → spluspy-2.2.8}/spluspy/tl/types/storage.py +0 -0
  153. {spluspy-2.2.7 → spluspy-2.2.8}/spluspy/tl/types/stories.py +0 -0
  154. {spluspy-2.2.7 → spluspy-2.2.8}/spluspy/tl/types/thirdParty.py +0 -0
  155. {spluspy-2.2.7 → spluspy-2.2.8}/spluspy/tl/types/update.py +0 -0
  156. {spluspy-2.2.7 → spluspy-2.2.8}/spluspy/tl/types/updates.py +0 -0
  157. {spluspy-2.2.7 → spluspy-2.2.8}/spluspy/tl/types/upload.py +0 -0
  158. {spluspy-2.2.7 → spluspy-2.2.8}/spluspy/tl/types/users.py +0 -0
  159. {spluspy-2.2.7 → spluspy-2.2.8}/spluspy.egg-info/SOURCES.txt +0 -0
  160. {spluspy-2.2.7 → spluspy-2.2.8}/spluspy.egg-info/dependency_links.txt +0 -0
  161. {spluspy-2.2.7 → spluspy-2.2.8}/spluspy.egg-info/requires.txt +0 -0
  162. {spluspy-2.2.7 → spluspy-2.2.8}/spluspy.egg-info/top_level.txt +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: spluspy
3
- Version: 2.2.7
3
+ Version: 2.2.8
4
4
  Summary: High-performance Python Self-Bot Framework for Soroush Plus
5
5
  Author: Ali Mirshekari
6
6
  License: MIT
@@ -26,7 +26,8 @@ Requires-Dist: rsa
26
26
  Requires-Dist: pyaes
27
27
  Dynamic: license-file
28
28
 
29
- # SplusPy v2.2.7
29
+ # SplusPy v2.2.8
30
+
30
31
 
31
32
  **A fast, minimal, and fully-featured self-bot framework**
32
33
 
@@ -1,4 +1,5 @@
1
- # SplusPy v2.2.7
1
+ # SplusPy v2.2.8
2
+
2
3
 
3
4
  **A fast, minimal, and fully-featured self-bot framework**
4
5
 
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "spluspy"
7
- version = "2.2.7"
7
+ version = "2.2.8"
8
8
  description = "High-performance Python Self-Bot Framework for Soroush Plus"
9
9
  readme = "README.md"
10
10
  license = {text = "MIT"}
@@ -4,11 +4,12 @@ from .tl.custom import Button
4
4
  from .tl import patched as _ # import for its side-effects
5
5
  from . import version, events, utils, errors, functions, custom
6
6
  from .filters import filters
7
+ from .enums import MessageMediaType
7
8
 
8
9
  __version__ = version.__version__
9
10
 
10
11
  __all__ = [
11
- 'Client', 'Button', 'filters',
12
+ 'Client', 'Button', 'filters', 'MessageMediaType',
12
13
  'functions', 'custom', 'errors',
13
14
  'events', 'utils', 'connection'
14
15
  ]
@@ -118,6 +118,17 @@ class _DirectDownloadIter(RequestIter):
118
118
  self._exported = True
119
119
  return await self._request()
120
120
 
121
+ except errors.RPCError as e:
122
+ err_msg = str(e).upper()
123
+ if "FILE_REQUEST_RECEIVED_ON_CONNECTION_SERVER" in err_msg or "422" in err_msg:
124
+ self.client._log[__name__].info('File download must be on Media DC; switching to DC 5')
125
+ if self._exported and getattr(self, '_sender', None):
126
+ await self.client._return_exported_sender(self._sender)
127
+ self._sender = await self.client._borrow_exported_sender(5)
128
+ self._exported = True
129
+ return await self._request()
130
+ raise
131
+
121
132
  except (errors.FilerefUpgradeNeededError, errors.FileReferenceExpiredError) as e:
122
133
  # Only implemented for documents which are the ones that may take that long to download
123
134
  if not self._msg_data \
@@ -142,7 +153,7 @@ class _DirectDownloadIter(RequestIter):
142
153
  return await self._request()
143
154
 
144
155
  async def close(self):
145
- if not self._sender:
156
+ if getattr(self, '_sender', None) is None:
146
157
  return
147
158
 
148
159
  try:
@@ -1093,4 +1104,4 @@ class DownloadMethods:
1093
1104
  return result
1094
1105
  i += 1
1095
1106
 
1096
- # endregion
1107
+ # endregion
@@ -1,6 +1,6 @@
1
1
  import typing
2
2
  from ..tl import functions, types
3
- from .. import helpers, utils, hints
3
+ from .. import helpers, utils, hints, errors
4
4
 
5
5
  if typing.TYPE_CHECKING:
6
6
  from .soroushclient import SoroushClient
@@ -224,9 +224,9 @@ class ExtraMethods:
224
224
  The full chat info (FullChannel or FullChat).
225
225
  """
226
226
  entity = await self.get_input_entity(chat_id)
227
- if helpers._entity_type(entity) == helpers._EntityType.CHANNEL:
227
+ try:
228
228
  return await self(functions.channels.GetFullChannelRequest(entity))
229
- else:
229
+ except errors.BadRequestError:
230
230
  return await self(functions.messages.GetFullChatRequest(entity.chat_id))
231
231
 
232
232
  async def get_chat_info(self: 'SoroushClient', chat_id):
@@ -237,10 +237,23 @@ class ExtraMethods:
237
237
  """Alias for get_entity. Returns the entity for the given user."""
238
238
  return await self.get_entity(user_id)
239
239
 
240
+ async def get_full_user(self: 'SoroushClient', user_id):
241
+ """Fetches full user profile information."""
242
+ try:
243
+ entity = await self.get_input_entity(user_id)
244
+ return await self(functions.users.GetFullUserRequest(id=entity))
245
+ except Exception:
246
+ return None
247
+
240
248
  async def get_full_chat(self: 'SoroushClient', chat_id):
241
249
  """Alias for get_chat. Returns full chat/channel info."""
242
250
  return await self.get_chat(chat_id)
243
251
 
252
+ async def is_public_chat(self: 'SoroushClient', chat_id: 'hints.EntityLike') -> bool:
253
+ """Check if a chat is public (has a username) or not."""
254
+ entity = await self.get_entity(chat_id)
255
+ return bool(getattr(entity, 'username', None))
256
+
244
257
  async def get_dialogs(
245
258
  self: 'SoroushClient',
246
259
  limit: int = 0,
@@ -855,7 +855,7 @@ class SoroushPlusBaseClient(abc.ABC):
855
855
  # Soroush: map all DCs to splus.ir servers
856
856
  if not cdn:
857
857
  domain = '.splus.ir'
858
- if dc_id in (1, 2, 3, 4, 5):
858
+ if dc_id in (1, 2, 3, 4):
859
859
  ip = f'im-server{domain}'
860
860
  elif dc_id == 6:
861
861
  ip = f'im-server{domain}'
@@ -9,7 +9,7 @@ from io import BytesIO
9
9
 
10
10
  from ..crypto import AES
11
11
 
12
- from .. import utils, helpers, hints
12
+ from .. import utils, helpers, hints, errors
13
13
  from ..tl import types, functions, custom
14
14
 
15
15
  try:
@@ -666,96 +666,117 @@ class UploadMethods:
666
666
  if isinstance(file, (types.InputFile, types.InputFileBig)):
667
667
  return file # Already uploaded
668
668
 
669
+ media_sender = None
670
+ media_dc = 5 # دیتاسنتر اختصاصی فایل‌های سروش پلاس
671
+
669
672
  pos = 0
670
- async with helpers._FileStream(file, file_size=file_size) as stream:
671
- # Opening the stream will determine the correct file size
672
- file_size = stream.file_size
673
-
674
- if not part_size_kb:
675
- part_size_kb = utils.get_appropriated_part_size(file_size)
676
-
677
- if part_size_kb > 512:
678
- raise ValueError('The part size must be less or equal to 512KB')
679
-
680
- part_size = int(part_size_kb * 1024)
681
- if part_size % 1024 != 0:
682
- raise ValueError(
683
- 'The part size must be evenly divisible by 1024')
684
-
685
- # Set a default file name if None was specified
686
- file_id = helpers.generate_random_long()
687
- if not file_name:
688
- file_name = stream.name or str(file_id)
689
-
690
- # If the file name lacks extension, add it if possible.
691
- # Else SoroushPlus complains with `PHOTO_EXT_INVALID_ERROR`
692
- # even if the uploaded image is indeed a photo.
693
- if not os.path.splitext(file_name)[-1]:
694
- file_name += utils._get_extension(stream)
695
-
696
- # Determine whether the file is too big (over 10MB) or not
697
- # SoroushPlus does make a distinction between smaller or larger files
698
- is_big = file_size > 10 * 1024 * 1024
699
- hash_md5 = hashlib.md5()
700
-
701
- part_count = (file_size + part_size - 1) // part_size
702
- self._log[__name__].info('Uploading file of %d bytes in %d chunks of %d',
703
- file_size, part_count, part_size)
704
-
705
- pos = 0
706
- for part_index in range(part_count):
707
- # Read the file by in chunks of size part_size
708
- part = await helpers._maybe_await(stream.read(part_size))
709
-
710
- if not isinstance(part, bytes):
711
- raise TypeError(
712
- 'file descriptor returned {}, not bytes (you must '
713
- 'open the file in bytes mode)'.format(type(part)))
714
-
715
- # `file_size` could be wrong in which case `part` may not be
716
- # `part_size` before reaching the end.
717
- if len(part) != part_size and part_index < part_count - 1:
718
- raise ValueError(
719
- 'read less than {} before reaching the end; either '
720
- '`file_size` or `read` are wrong'.format(part_size))
721
-
722
- pos += len(part)
723
-
724
- # Encryption part if needed
725
- if key and iv:
726
- part = AES.encrypt_ige(part, key, iv)
727
-
728
- if not is_big:
729
- # Bit odd that MD5 is only needed for small files and not
730
- # big ones with more chance for corruption, but that's
731
- # what SoroushPlus wants.
732
- hash_md5.update(part)
733
-
734
- # The SavePartRequest is different depending on whether
735
- # the file is too large or not (over or less than 10MB)
736
- if is_big:
737
- request = functions.upload.SaveBigFilePartRequest(
738
- file_id, part_index, part_count, part)
739
- else:
740
- request = functions.upload.SaveFilePartRequest(
741
- file_id, part_index, part)
742
-
743
- result = await self(request)
744
- if result:
745
- self._log[__name__].debug('Uploaded %d/%d',
746
- part_index + 1, part_count)
747
- if progress_callback:
748
- await helpers._maybe_await(progress_callback(pos, file_size))
749
- else:
750
- raise RuntimeError(
751
- 'Failed to upload file part {}.'.format(part_index))
673
+ try:
674
+ async with helpers._FileStream(file, file_size=file_size) as stream:
675
+ # Opening the stream will determine the correct file size
676
+ file_size = stream.file_size
752
677
 
753
- if is_big:
754
- return types.InputFileBig(file_id, part_count, file_name)
755
- else:
756
- return custom.InputSizedFile(
757
- file_id, part_count, file_name, md5=hash_md5, size=file_size
758
- )
678
+ if not part_size_kb:
679
+ part_size_kb = utils.get_appropriated_part_size(file_size)
680
+
681
+ if part_size_kb > 512:
682
+ raise ValueError('The part size must be less or equal to 512KB')
683
+
684
+ part_size = int(part_size_kb * 1024)
685
+ if part_size % 1024 != 0:
686
+ raise ValueError(
687
+ 'The part size must be evenly divisible by 1024')
688
+
689
+ # Set a default file name if None was specified
690
+ file_id = helpers.generate_random_long()
691
+ if not file_name:
692
+ file_name = stream.name or str(file_id)
693
+
694
+ # If the file name lacks extension, add it if possible.
695
+ # Else SoroushPlus complains with `PHOTO_EXT_INVALID_ERROR`
696
+ # even if the uploaded image is indeed a photo.
697
+ if not os.path.splitext(file_name)[-1]:
698
+ file_name += utils._get_extension(stream)
699
+
700
+ # Determine whether the file is too big (over 10MB) or not
701
+ # SoroushPlus does make a distinction between smaller or larger files
702
+ is_big = file_size > 10 * 1024 * 1024
703
+ hash_md5 = hashlib.md5()
704
+
705
+ part_count = (file_size + part_size - 1) // part_size
706
+ self._log[__name__].info('Uploading file of %d bytes in %d chunks of %d',
707
+ file_size, part_count, part_size)
708
+
709
+ pos = 0
710
+ for part_index in range(part_count):
711
+ # Read the file by in chunks of size part_size
712
+ part = await helpers._maybe_await(stream.read(part_size))
713
+
714
+ if not isinstance(part, bytes):
715
+ raise TypeError(
716
+ 'file descriptor returned {}, not bytes (you must '
717
+ 'open the file in bytes mode)'.format(type(part)))
718
+
719
+ # `file_size` could be wrong in which case `part` may not be
720
+ # `part_size` before reaching the end.
721
+ if len(part) != part_size and part_index < part_count - 1:
722
+ raise ValueError(
723
+ 'read less than {} before reaching the end; either '
724
+ '`file_size` or `read` are wrong'.format(part_size))
725
+
726
+ pos += len(part)
727
+
728
+ # Encryption part if needed
729
+ if key and iv:
730
+ part = AES.encrypt_ige(part, key, iv)
731
+
732
+ if not is_big:
733
+ # Bit odd that MD5 is only needed for small files and not
734
+ # big ones with more chance for corruption, but that's
735
+ # what SoroushPlus wants.
736
+ hash_md5.update(part)
737
+
738
+ # The SavePartRequest is different depending on whether
739
+ # the file is too large or not (over or less than 10MB)
740
+ if is_big:
741
+ request = functions.upload.SaveBigFilePartRequest(
742
+ file_id, part_index, part_count, part)
743
+ else:
744
+ request = functions.upload.SaveFilePartRequest(
745
+ file_id, part_index, part)
746
+
747
+ # ارسال ریکوئست آپلود با هندل کردن اتصال سرور مدیا
748
+ if media_sender:
749
+ result = await self._call(media_sender, request)
750
+ else:
751
+ try:
752
+ result = await self(request)
753
+ except errors.RPCError as e:
754
+ err_msg = str(e).upper()
755
+ if "FILE_REQUEST_RECEIVED_ON_CONNECTION_SERVER" in err_msg or "422" in err_msg:
756
+ self._log[__name__].info('Switching to Media DC (DC 5) for upload...')
757
+ media_sender = await self._borrow_exported_sender(media_dc)
758
+ result = await self._call(media_sender, request)
759
+ else:
760
+ raise
761
+
762
+ if result:
763
+ self._log[__name__].debug('Uploaded %d/%d',
764
+ part_index + 1, part_count)
765
+ if progress_callback:
766
+ await helpers._maybe_await(progress_callback(pos, file_size))
767
+ else:
768
+ raise RuntimeError(
769
+ 'Failed to upload file part {}.'.format(part_index))
770
+
771
+ if is_big:
772
+ return types.InputFileBig(file_id, part_count, file_name)
773
+ else:
774
+ return custom.InputSizedFile(
775
+ file_id, part_count, file_name, md5=hash_md5, size=file_size
776
+ )
777
+ finally:
778
+ if media_sender:
779
+ await self._return_exported_sender(media_sender)
759
780
 
760
781
  # endregion
761
782
 
@@ -864,4 +885,4 @@ class UploadMethods:
864
885
  )
865
886
  return file_handle, media, as_image
866
887
 
867
- # endregion
888
+ # endregion
@@ -0,0 +1,144 @@
1
+ from enum import Enum
2
+
3
+
4
+ class MessageMediaType(Enum):
5
+ PHOTO = "photo"
6
+ VIDEO = "video"
7
+ VIDEO_NOTE = "video_note"
8
+ DOCUMENT = "document"
9
+ AUDIO = "audio"
10
+ VOICE = "voice"
11
+ STICKER = "sticker"
12
+ ANIMATION = "animation"
13
+ GIF = "gif"
14
+ WEB_PAGE = "web_page"
15
+ GEO = "geo"
16
+ GEO_LIVE = "geo_live"
17
+ CONTACT = "contact"
18
+ GAME = "game"
19
+ POLL = "poll"
20
+ DICE = "dice"
21
+ VENUE = "venue"
22
+ STORY = "story"
23
+ GIVEAWAY = "giveaway"
24
+ INVOICE = "invoice"
25
+ UNSUPPORTED = "unsupported"
26
+ EMPTY = "empty"
27
+
28
+ @classmethod
29
+ def from_media(cls, media):
30
+ """
31
+ Detect MessageMediaType from a MessageMedia object.
32
+
33
+ Usage:
34
+ from spluspy.enums import MessageMediaType
35
+ media_type = MessageMediaType.from_media(message.media)
36
+ """
37
+ from .tl import types
38
+
39
+ if media is None or isinstance(media, types.MessageMediaEmpty):
40
+ return cls.EMPTY
41
+
42
+ if isinstance(media, types.MessageMediaPhoto):
43
+ return cls.PHOTO
44
+
45
+ if isinstance(media, types.MessageMediaDocument):
46
+ doc = media.document
47
+ if doc is None or isinstance(doc, types.DocumentEmpty):
48
+ return cls.DOCUMENT
49
+
50
+ attrs = {type(a): a for a in doc.attributes}
51
+
52
+ if types.DocumentAttributeSticker in attrs:
53
+ return cls.STICKER
54
+
55
+ if types.DocumentAttributeAnimated in attrs:
56
+ if types.DocumentAttributeVideo in attrs:
57
+ return cls.ANIMATION
58
+ return cls.GIF
59
+
60
+ if types.DocumentAttributeVideo in attrs:
61
+ vid = attrs[types.DocumentAttributeVideo]
62
+ if vid.round_message:
63
+ return cls.VIDEO_NOTE
64
+ return cls.VIDEO
65
+
66
+ if types.DocumentAttributeAudio in attrs:
67
+ aud = attrs[types.DocumentAttributeAudio]
68
+ if aud.voice:
69
+ return cls.VOICE
70
+ return cls.AUDIO
71
+
72
+ return cls.DOCUMENT
73
+
74
+ if isinstance(media, types.MessageMediaWebPage):
75
+ return cls.WEB_PAGE
76
+
77
+ if isinstance(media, types.MessageMediaGeo):
78
+ return cls.GEO
79
+
80
+ if isinstance(media, types.MessageMediaGeoLive):
81
+ return cls.GEO_LIVE
82
+
83
+ if isinstance(media, types.MessageMediaContact):
84
+ return cls.CONTACT
85
+
86
+ if isinstance(media, types.MessageMediaGame):
87
+ return cls.GAME
88
+
89
+ if isinstance(media, types.MessageMediaPoll):
90
+ return cls.POLL
91
+
92
+ if isinstance(media, types.MessageMediaDice):
93
+ return cls.DICE
94
+
95
+ if isinstance(media, types.MessageMediaVenue):
96
+ return cls.VENUE
97
+
98
+ if isinstance(media, types.MessageMediaStory):
99
+ return cls.STORY
100
+
101
+ if isinstance(media, types.MessageMediaGiveaway):
102
+ return cls.GIVEAWAY
103
+
104
+ if isinstance(media, types.MessageMediaInvoice):
105
+ return cls.INVOICE
106
+
107
+ if isinstance(media, types.MessageMediaUnsupported):
108
+ return cls.UNSUPPORTED
109
+
110
+ return cls.EMPTY
111
+
112
+ @classmethod
113
+ def from_message(cls, message):
114
+ """
115
+ Detect MessageMediaType from a Message object.
116
+
117
+ Usage:
118
+ from spluspy.enums import MessageMediaType
119
+ media_type = MessageMediaType.from_message(event.message)
120
+ """
121
+ if hasattr(message, 'media'):
122
+ return cls.from_media(message.media)
123
+ return cls.EMPTY
124
+
125
+ def is_image(self):
126
+ return self == self.PHOTO
127
+
128
+ def is_video(self):
129
+ return self in (self.VIDEO, self.VIDEO_NOTE, self.ANIMATION)
130
+
131
+ def is_audio(self):
132
+ return self in (self.AUDIO, self.VOICE)
133
+
134
+ def is_document(self):
135
+ return self == self.DOCUMENT
136
+
137
+ def is_sticker(self):
138
+ return self == self.STICKER
139
+
140
+ def is_animation(self):
141
+ return self in (self.ANIMATION, self.GIF)
142
+
143
+ def is_media(self):
144
+ return self not in (self.EMPTY, self.UNSUPPORTED)
@@ -30,7 +30,7 @@ from ..crypto import AuthKey
30
30
  from ..helpers import retry_range
31
31
 
32
32
 
33
- _RECV_TIMEOUT = 15 # seconds; triggers reconnection if no data received
33
+ _RECV_TIMEOUT = 60 # seconds; triggers reconnection if no data received
34
34
 
35
35
 
36
36
  class MTProtoSender:
@@ -154,6 +154,49 @@ class MTProtoSender:
154
154
  """
155
155
  await self._disconnect()
156
156
 
157
+ def _intercept_request(self, request):
158
+ """
159
+ Soroush+ Interceptor Middleware:
160
+ Intercepts and rewrites outgoing requests to bypass Soroush+ server restrictions
161
+ and prevent RPCError 422 (NOT_SUPPORTED).
162
+ """
163
+ req_type = type(request).__name__
164
+
165
+ try:
166
+ # 1. تبدیل متد خروج از گروه (جلوگیری از ارور 422)
167
+ if req_type == 'DeleteChatUserRequest':
168
+ from ..tl.functions.channels import LeaveChannelRequest
169
+ from ..tl.types import InputChannel
170
+ self._log.debug("Soroush+ Interceptor: Converting DeleteChatUserRequest -> LeaveChannelRequest")
171
+ # سروش برای خروج از همه گروه‌ها فقط این متد را می‌پذیرد
172
+ return LeaveChannelRequest(channel=InputChannel(request.chat_id, 0))
173
+
174
+ # 2. تبدیل متد دریافت لیست گروه‌ها (جلوگیری از ارور 422)
175
+ elif req_type == 'GetChatsRequest':
176
+ from ..tl.functions.channels import GetChannelsRequest
177
+ from ..tl.types import InputChannel
178
+ self._log.debug("Soroush+ Interceptor: Converting GetChatsRequest -> GetChannelsRequest")
179
+ channels = []
180
+ for chat_id in request.id:
181
+ if isinstance(chat_id, int):
182
+ channels.append(InputChannel(chat_id, 0))
183
+ else:
184
+ channels.append(chat_id)
185
+ return GetChannelsRequest(id=channels)
186
+
187
+ # 3. تبدیل متد دریافت اطلاعات کامل گروه (جلوگیری از ارور 422)
188
+ elif req_type == 'GetFullChatRequest':
189
+ from ..tl.functions.channels import GetFullChannelRequest
190
+ from ..tl.types import InputChannel
191
+ self._log.debug("Soroush+ Interceptor: Converting GetFullChatRequest -> GetFullChannelRequest")
192
+ return GetFullChannelRequest(channel=InputChannel(request.chat_id, 0))
193
+
194
+ except Exception as e:
195
+ self._log.error('Interceptor failed to convert request %s: %s', req_type, e)
196
+
197
+ # اگر درخواست نیازی به تغییر نداشت، خودش رو برمی‌گردونیم
198
+ return request
199
+
157
200
  def send(self, request, ordered=False):
158
201
  """
159
202
  This method enqueues the given request to be sent. Its send
@@ -181,6 +224,9 @@ class MTProtoSender:
181
224
  raise ConnectionError('Cannot send requests while disconnected')
182
225
 
183
226
  if not utils.is_list_like(request):
227
+ # --- SOROUSH+ INTERCEPTOR HOOK ---
228
+ request = self._intercept_request(request)
229
+
184
230
  try:
185
231
  state = RequestState(request)
186
232
  except struct.error as e:
@@ -196,6 +242,9 @@ class MTProtoSender:
196
242
  futures = []
197
243
  state = None
198
244
  for req in request:
245
+ # --- SOROUSH+ INTERCEPTOR HOOK ---
246
+ req = self._intercept_request(req)
247
+
199
248
  try:
200
249
  state = RequestState(req, after=ordered and state)
201
250
  except struct.error as e:
@@ -976,4 +1025,4 @@ class MTProtoSender:
976
1025
  # Even if the library didn't disconnect, the server would (and then
977
1026
  # the library would reconnect and learn about auth key being invalid).
978
1027
  if isinstance(message.obj, DestroyAuthKeyOk):
979
- await self._disconnect(error=AuthKeyNotFound())
1028
+ await self._disconnect(error=AuthKeyNotFound())
@@ -7,6 +7,7 @@ from .forward import Forward
7
7
  from .file import File
8
8
  from .. import TLObject, types, functions
9
9
  from ... import utils, errors
10
+ from ...enums import MessageMediaType
10
11
 
11
12
  _log = logging.getLogger(__name__)
12
13
 
@@ -150,6 +151,27 @@ class Message(ChatGetter, SenderGetter, TLObject):
150
151
  self._file = File(media)
151
152
  return self._file
152
153
 
154
+ @property
155
+ def media_type(self):
156
+ """
157
+ Returns the MessageMediaType of this message.
158
+
159
+ Usage:
160
+ from spluspy.enums import MessageMediaType
161
+
162
+ @client.on_message()
163
+ async def handler(client, event):
164
+ media_type = event.message.media_type
165
+
166
+ if media_type == MessageMediaType.PHOTO:
167
+ await event.reply("عکس دریافت شد!")
168
+ elif media_type == MessageMediaType.VIDEO:
169
+ await event.reply("ویدیو دریافت شد!")
170
+ elif media_type == MessageMediaType.STICKER:
171
+ await event.reply("استیکر دریافت شد!")
172
+ """
173
+ return MessageMediaType.from_media(self.media)
174
+
153
175
  @property
154
176
  def photo(self):
155
177
  if isinstance(self.media, types.MessageMediaPhoto):
@@ -58,9 +58,6 @@ USERNAME_RE = re.compile(
58
58
  TG_JOIN_RE = re.compile(
59
59
  r'tg://(join)\?invite='
60
60
  )
61
- SOROUSH_JOIN_RE = re.compile(
62
- r'https?://splus\.ir/joingroup/([a-zA-Z0-9_-]+)'
63
- )
64
61
 
65
62
  VALID_USERNAME_RE = re.compile(
66
63
  r'^[a-z](?:(?!__)\w){1,30}[a-z\d]$',
@@ -936,11 +933,6 @@ def parse_username(username):
936
933
  Returns ``(None, False)`` if the ``username`` or link is not valid.
937
934
  """
938
935
  username = username.strip()
939
-
940
- splus_match = SOROUSH_JOIN_RE.match(username)
941
- if splus_match:
942
- return splus_match.group(1), True
943
-
944
936
  m = USERNAME_RE.match(username) or TG_JOIN_RE.match(username)
945
937
  if m:
946
938
  username = username[m.end():]
@@ -1575,4 +1567,4 @@ async def maybe_async(coro):
1575
1567
  if inspect.isawaitable(result):
1576
1568
  warnings.warn('Using async sessions support is an experimental feature')
1577
1569
  result = await result
1578
- return result
1570
+ return result
@@ -1,3 +1,3 @@
1
1
  # Versions should comply with PEP440.
2
2
  # This line is parsed in setup.py:
3
- __version__ = '2.2.7'
3
+ __version__ = '2.2.8'
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: spluspy
3
- Version: 2.2.7
3
+ Version: 2.2.8
4
4
  Summary: High-performance Python Self-Bot Framework for Soroush Plus
5
5
  Author: Ali Mirshekari
6
6
  License: MIT
@@ -26,7 +26,8 @@ Requires-Dist: rsa
26
26
  Requires-Dist: pyaes
27
27
  Dynamic: license-file
28
28
 
29
- # SplusPy v2.2.7
29
+ # SplusPy v2.2.8
30
+
30
31
 
31
32
  **A fast, minimal, and fully-featured self-bot framework**
32
33