spluspy 2.2.7__tar.gz → 2.2.9__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.9}/PKG-INFO +4 -3
  2. {spluspy-2.2.7 → spluspy-2.2.9}/README.md +3 -2
  3. {spluspy-2.2.7 → spluspy-2.2.9}/pyproject.toml +1 -1
  4. {spluspy-2.2.7 → spluspy-2.2.9}/spluspy/__init__.py +2 -1
  5. {spluspy-2.2.7 → spluspy-2.2.9}/spluspy/client/downloads.py +13 -2
  6. {spluspy-2.2.7 → spluspy-2.2.9}/spluspy/client/extra.py +16 -3
  7. {spluspy-2.2.7 → spluspy-2.2.9}/spluspy/client/telegrambaseclient.py +1 -1
  8. {spluspy-2.2.7 → spluspy-2.2.9}/spluspy/client/updates.py +2 -18
  9. {spluspy-2.2.7 → spluspy-2.2.9}/spluspy/client/uploads.py +111 -90
  10. {spluspy-2.2.7 → spluspy-2.2.9}/spluspy/client/users.py +16 -2
  11. spluspy-2.2.9/spluspy/enums.py +144 -0
  12. {spluspy-2.2.7 → spluspy-2.2.9}/spluspy/events/common.py +9 -3
  13. {spluspy-2.2.7 → spluspy-2.2.9}/spluspy/network/mtprotosender.py +51 -2
  14. {spluspy-2.2.7 → spluspy-2.2.9}/spluspy/sessions/memory.py +11 -3
  15. {spluspy-2.2.7 → spluspy-2.2.9}/spluspy/tl/custom/message.py +22 -0
  16. {spluspy-2.2.7 → spluspy-2.2.9}/spluspy/utils.py +4 -14
  17. {spluspy-2.2.7 → spluspy-2.2.9}/spluspy/version.py +1 -1
  18. {spluspy-2.2.7 → spluspy-2.2.9/spluspy.egg-info}/PKG-INFO +4 -3
  19. spluspy-2.2.7/spluspy/enums.py +0 -17
  20. {spluspy-2.2.7 → spluspy-2.2.9}/LICENSE +0 -0
  21. {spluspy-2.2.7 → spluspy-2.2.9}/setup.cfg +0 -0
  22. {spluspy-2.2.7 → spluspy-2.2.9}/spluspy/_updates/__init__.py +0 -0
  23. {spluspy-2.2.7 → spluspy-2.2.9}/spluspy/_updates/entitycache.py +0 -0
  24. {spluspy-2.2.7 → spluspy-2.2.9}/spluspy/_updates/messagebox.py +0 -0
  25. {spluspy-2.2.7 → spluspy-2.2.9}/spluspy/_updates/session.py +0 -0
  26. {spluspy-2.2.7 → spluspy-2.2.9}/spluspy/admin.py +0 -0
  27. {spluspy-2.2.7 → spluspy-2.2.9}/spluspy/client/__init__.py +0 -0
  28. {spluspy-2.2.7 → spluspy-2.2.9}/spluspy/client/account.py +0 -0
  29. {spluspy-2.2.7 → spluspy-2.2.9}/spluspy/client/auth.py +0 -0
  30. {spluspy-2.2.7 → spluspy-2.2.9}/spluspy/client/buttons.py +0 -0
  31. {spluspy-2.2.7 → spluspy-2.2.9}/spluspy/client/chats.py +0 -0
  32. {spluspy-2.2.7 → spluspy-2.2.9}/spluspy/client/dialogs.py +0 -0
  33. {spluspy-2.2.7 → spluspy-2.2.9}/spluspy/client/messageparse.py +0 -0
  34. {spluspy-2.2.7 → spluspy-2.2.9}/spluspy/client/messages.py +0 -0
  35. {spluspy-2.2.7 → spluspy-2.2.9}/spluspy/client/soroushclient.py +0 -0
  36. {spluspy-2.2.7 → spluspy-2.2.9}/spluspy/crypto/__init__.py +0 -0
  37. {spluspy-2.2.7 → spluspy-2.2.9}/spluspy/crypto/aes.py +0 -0
  38. {spluspy-2.2.7 → spluspy-2.2.9}/spluspy/crypto/aesctr.py +0 -0
  39. {spluspy-2.2.7 → spluspy-2.2.9}/spluspy/crypto/authkey.py +0 -0
  40. {spluspy-2.2.7 → spluspy-2.2.9}/spluspy/crypto/cdndecrypter.py +0 -0
  41. {spluspy-2.2.7 → spluspy-2.2.9}/spluspy/crypto/factorization.py +0 -0
  42. {spluspy-2.2.7 → spluspy-2.2.9}/spluspy/crypto/libssl.py +0 -0
  43. {spluspy-2.2.7 → spluspy-2.2.9}/spluspy/crypto/rsa.py +0 -0
  44. {spluspy-2.2.7 → spluspy-2.2.9}/spluspy/custom.py +0 -0
  45. {spluspy-2.2.7 → spluspy-2.2.9}/spluspy/errors/__init__.py +0 -0
  46. {spluspy-2.2.7 → spluspy-2.2.9}/spluspy/errors/common.py +0 -0
  47. {spluspy-2.2.7 → spluspy-2.2.9}/spluspy/errors/exceptions.py +0 -0
  48. {spluspy-2.2.7 → spluspy-2.2.9}/spluspy/errors/rpcbaseerrors.py +0 -0
  49. {spluspy-2.2.7 → spluspy-2.2.9}/spluspy/errors/rpcerrorlist.py +0 -0
  50. {spluspy-2.2.7 → spluspy-2.2.9}/spluspy/events/__init__.py +0 -0
  51. {spluspy-2.2.7 → spluspy-2.2.9}/spluspy/events/album.py +0 -0
  52. {spluspy-2.2.7 → spluspy-2.2.9}/spluspy/events/chataction.py +0 -0
  53. {spluspy-2.2.7 → spluspy-2.2.9}/spluspy/events/messagedeleted.py +0 -0
  54. {spluspy-2.2.7 → spluspy-2.2.9}/spluspy/events/messageedited.py +0 -0
  55. {spluspy-2.2.7 → spluspy-2.2.9}/spluspy/events/messageread.py +0 -0
  56. {spluspy-2.2.7 → spluspy-2.2.9}/spluspy/events/newmessage.py +0 -0
  57. {spluspy-2.2.7 → spluspy-2.2.9}/spluspy/events/raw.py +0 -0
  58. {spluspy-2.2.7 → spluspy-2.2.9}/spluspy/events/userupdate.py +0 -0
  59. {spluspy-2.2.7 → spluspy-2.2.9}/spluspy/extensions/__init__.py +0 -0
  60. {spluspy-2.2.7 → spluspy-2.2.9}/spluspy/extensions/binaryreader.py +0 -0
  61. {spluspy-2.2.7 → spluspy-2.2.9}/spluspy/extensions/html.py +0 -0
  62. {spluspy-2.2.7 → spluspy-2.2.9}/spluspy/extensions/markdown.py +0 -0
  63. {spluspy-2.2.7 → spluspy-2.2.9}/spluspy/extensions/messagepacker.py +0 -0
  64. {spluspy-2.2.7 → spluspy-2.2.9}/spluspy/filters.py +0 -0
  65. {spluspy-2.2.7 → spluspy-2.2.9}/spluspy/functions.py +0 -0
  66. {spluspy-2.2.7 → spluspy-2.2.9}/spluspy/helpers.py +0 -0
  67. {spluspy-2.2.7 → spluspy-2.2.9}/spluspy/hints.py +0 -0
  68. {spluspy-2.2.7 → spluspy-2.2.9}/spluspy/network/__init__.py +0 -0
  69. {spluspy-2.2.7 → spluspy-2.2.9}/spluspy/network/authenticator.py +0 -0
  70. {spluspy-2.2.7 → spluspy-2.2.9}/spluspy/network/connection/__init__.py +0 -0
  71. {spluspy-2.2.7 → spluspy-2.2.9}/spluspy/network/connection/connection.py +0 -0
  72. {spluspy-2.2.7 → spluspy-2.2.9}/spluspy/network/connection/http.py +0 -0
  73. {spluspy-2.2.7 → spluspy-2.2.9}/spluspy/network/connection/tcpabridged.py +0 -0
  74. {spluspy-2.2.7 → spluspy-2.2.9}/spluspy/network/connection/tcpfull.py +0 -0
  75. {spluspy-2.2.7 → spluspy-2.2.9}/spluspy/network/connection/tcpintermediate.py +0 -0
  76. {spluspy-2.2.7 → spluspy-2.2.9}/spluspy/network/connection/tcpmtproxy.py +0 -0
  77. {spluspy-2.2.7 → spluspy-2.2.9}/spluspy/network/connection/tcpobfuscated.py +0 -0
  78. {spluspy-2.2.7 → spluspy-2.2.9}/spluspy/network/connection/websocket.py +0 -0
  79. {spluspy-2.2.7 → spluspy-2.2.9}/spluspy/network/mtprotoplainsender.py +0 -0
  80. {spluspy-2.2.7 → spluspy-2.2.9}/spluspy/network/mtprotostate.py +0 -0
  81. {spluspy-2.2.7 → spluspy-2.2.9}/spluspy/network/requeststate.py +0 -0
  82. {spluspy-2.2.7 → spluspy-2.2.9}/spluspy/password.py +0 -0
  83. {spluspy-2.2.7 → spluspy-2.2.9}/spluspy/requestiter.py +0 -0
  84. {spluspy-2.2.7 → spluspy-2.2.9}/spluspy/resolvers.py +0 -0
  85. {spluspy-2.2.7 → spluspy-2.2.9}/spluspy/sessions/__init__.py +0 -0
  86. {spluspy-2.2.7 → spluspy-2.2.9}/spluspy/sessions/abstract.py +0 -0
  87. {spluspy-2.2.7 → spluspy-2.2.9}/spluspy/sessions/sqlite.py +0 -0
  88. {spluspy-2.2.7 → spluspy-2.2.9}/spluspy/sessions/string.py +0 -0
  89. {spluspy-2.2.7 → spluspy-2.2.9}/spluspy/sync.py +0 -0
  90. {spluspy-2.2.7 → spluspy-2.2.9}/spluspy/tl/__init__.py +0 -0
  91. {spluspy-2.2.7 → spluspy-2.2.9}/spluspy/tl/alltlobjects.py +0 -0
  92. {spluspy-2.2.7 → spluspy-2.2.9}/spluspy/tl/core/__init__.py +0 -0
  93. {spluspy-2.2.7 → spluspy-2.2.9}/spluspy/tl/core/gzippacked.py +0 -0
  94. {spluspy-2.2.7 → spluspy-2.2.9}/spluspy/tl/core/messagecontainer.py +0 -0
  95. {spluspy-2.2.7 → spluspy-2.2.9}/spluspy/tl/core/rpcresult.py +0 -0
  96. {spluspy-2.2.7 → spluspy-2.2.9}/spluspy/tl/core/tlmessage.py +0 -0
  97. {spluspy-2.2.7 → spluspy-2.2.9}/spluspy/tl/custom/__init__.py +0 -0
  98. {spluspy-2.2.7 → spluspy-2.2.9}/spluspy/tl/custom/adminlogevent.py +0 -0
  99. {spluspy-2.2.7 → spluspy-2.2.9}/spluspy/tl/custom/button.py +0 -0
  100. {spluspy-2.2.7 → spluspy-2.2.9}/spluspy/tl/custom/chatgetter.py +0 -0
  101. {spluspy-2.2.7 → spluspy-2.2.9}/spluspy/tl/custom/conversation.py +0 -0
  102. {spluspy-2.2.7 → spluspy-2.2.9}/spluspy/tl/custom/dialog.py +0 -0
  103. {spluspy-2.2.7 → spluspy-2.2.9}/spluspy/tl/custom/draft.py +0 -0
  104. {spluspy-2.2.7 → spluspy-2.2.9}/spluspy/tl/custom/file.py +0 -0
  105. {spluspy-2.2.7 → spluspy-2.2.9}/spluspy/tl/custom/forward.py +0 -0
  106. {spluspy-2.2.7 → spluspy-2.2.9}/spluspy/tl/custom/inputsizedfile.py +0 -0
  107. {spluspy-2.2.7 → spluspy-2.2.9}/spluspy/tl/custom/messagebutton.py +0 -0
  108. {spluspy-2.2.7 → spluspy-2.2.9}/spluspy/tl/custom/participantpermissions.py +0 -0
  109. {spluspy-2.2.7 → spluspy-2.2.9}/spluspy/tl/custom/qrlogin.py +0 -0
  110. {spluspy-2.2.7 → spluspy-2.2.9}/spluspy/tl/custom/sendergetter.py +0 -0
  111. {spluspy-2.2.7 → spluspy-2.2.9}/spluspy/tl/custom/types.py +0 -0
  112. {spluspy-2.2.7 → spluspy-2.2.9}/spluspy/tl/functions/__init__.py +0 -0
  113. {spluspy-2.2.7 → spluspy-2.2.9}/spluspy/tl/functions/account.py +0 -0
  114. {spluspy-2.2.7 → spluspy-2.2.9}/spluspy/tl/functions/auth.py +0 -0
  115. {spluspy-2.2.7 → spluspy-2.2.9}/spluspy/tl/functions/bots.py +0 -0
  116. {spluspy-2.2.7 → spluspy-2.2.9}/spluspy/tl/functions/channels.py +0 -0
  117. {spluspy-2.2.7 → spluspy-2.2.9}/spluspy/tl/functions/chatlists.py +0 -0
  118. {spluspy-2.2.7 → spluspy-2.2.9}/spluspy/tl/functions/conference.py +0 -0
  119. {spluspy-2.2.7 → spluspy-2.2.9}/spluspy/tl/functions/contacts.py +0 -0
  120. {spluspy-2.2.7 → spluspy-2.2.9}/spluspy/tl/functions/folders.py +0 -0
  121. {spluspy-2.2.7 → spluspy-2.2.9}/spluspy/tl/functions/help.py +0 -0
  122. {spluspy-2.2.7 → spluspy-2.2.9}/spluspy/tl/functions/langpack.py +0 -0
  123. {spluspy-2.2.7 → spluspy-2.2.9}/spluspy/tl/functions/messages.py +0 -0
  124. {spluspy-2.2.7 → spluspy-2.2.9}/spluspy/tl/functions/payments.py +0 -0
  125. {spluspy-2.2.7 → spluspy-2.2.9}/spluspy/tl/functions/phone.py +0 -0
  126. {spluspy-2.2.7 → spluspy-2.2.9}/spluspy/tl/functions/photos.py +0 -0
  127. {spluspy-2.2.7 → spluspy-2.2.9}/spluspy/tl/functions/premium.py +0 -0
  128. {spluspy-2.2.7 → spluspy-2.2.9}/spluspy/tl/functions/stats.py +0 -0
  129. {spluspy-2.2.7 → spluspy-2.2.9}/spluspy/tl/functions/stories.py +0 -0
  130. {spluspy-2.2.7 → spluspy-2.2.9}/spluspy/tl/functions/thirdParty.py +0 -0
  131. {spluspy-2.2.7 → spluspy-2.2.9}/spluspy/tl/functions/updates.py +0 -0
  132. {spluspy-2.2.7 → spluspy-2.2.9}/spluspy/tl/functions/upload.py +0 -0
  133. {spluspy-2.2.7 → spluspy-2.2.9}/spluspy/tl/functions/users.py +0 -0
  134. {spluspy-2.2.7 → spluspy-2.2.9}/spluspy/tl/patched/__init__.py +0 -0
  135. {spluspy-2.2.7 → spluspy-2.2.9}/spluspy/tl/tlobject.py +0 -0
  136. {spluspy-2.2.7 → spluspy-2.2.9}/spluspy/tl/types/__init__.py +0 -0
  137. {spluspy-2.2.7 → spluspy-2.2.9}/spluspy/tl/types/account.py +0 -0
  138. {spluspy-2.2.7 → spluspy-2.2.9}/spluspy/tl/types/auth.py +0 -0
  139. {spluspy-2.2.7 → spluspy-2.2.9}/spluspy/tl/types/bots.py +0 -0
  140. {spluspy-2.2.7 → spluspy-2.2.9}/spluspy/tl/types/channels.py +0 -0
  141. {spluspy-2.2.7 → spluspy-2.2.9}/spluspy/tl/types/chatlists.py +0 -0
  142. {spluspy-2.2.7 → spluspy-2.2.9}/spluspy/tl/types/conference.py +0 -0
  143. {spluspy-2.2.7 → spluspy-2.2.9}/spluspy/tl/types/contacts.py +0 -0
  144. {spluspy-2.2.7 → spluspy-2.2.9}/spluspy/tl/types/help.py +0 -0
  145. {spluspy-2.2.7 → spluspy-2.2.9}/spluspy/tl/types/messages.py +0 -0
  146. {spluspy-2.2.7 → spluspy-2.2.9}/spluspy/tl/types/payments.py +0 -0
  147. {spluspy-2.2.7 → spluspy-2.2.9}/spluspy/tl/types/phone.py +0 -0
  148. {spluspy-2.2.7 → spluspy-2.2.9}/spluspy/tl/types/photos.py +0 -0
  149. {spluspy-2.2.7 → spluspy-2.2.9}/spluspy/tl/types/premium.py +0 -0
  150. {spluspy-2.2.7 → spluspy-2.2.9}/spluspy/tl/types/stats.py +0 -0
  151. {spluspy-2.2.7 → spluspy-2.2.9}/spluspy/tl/types/stickers.py +0 -0
  152. {spluspy-2.2.7 → spluspy-2.2.9}/spluspy/tl/types/storage.py +0 -0
  153. {spluspy-2.2.7 → spluspy-2.2.9}/spluspy/tl/types/stories.py +0 -0
  154. {spluspy-2.2.7 → spluspy-2.2.9}/spluspy/tl/types/thirdParty.py +0 -0
  155. {spluspy-2.2.7 → spluspy-2.2.9}/spluspy/tl/types/update.py +0 -0
  156. {spluspy-2.2.7 → spluspy-2.2.9}/spluspy/tl/types/updates.py +0 -0
  157. {spluspy-2.2.7 → spluspy-2.2.9}/spluspy/tl/types/upload.py +0 -0
  158. {spluspy-2.2.7 → spluspy-2.2.9}/spluspy/tl/types/users.py +0 -0
  159. {spluspy-2.2.7 → spluspy-2.2.9}/spluspy.egg-info/SOURCES.txt +0 -0
  160. {spluspy-2.2.7 → spluspy-2.2.9}/spluspy.egg-info/dependency_links.txt +0 -0
  161. {spluspy-2.2.7 → spluspy-2.2.9}/spluspy.egg-info/requires.txt +0 -0
  162. {spluspy-2.2.7 → spluspy-2.2.9}/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.9
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.9
30
+
30
31
 
31
32
  **A fast, minimal, and fully-featured self-bot framework**
32
33
 
@@ -36,7 +37,7 @@ Dynamic: license-file
36
37
 
37
38
  SplusPy is a Python library designed specifically for building **Soroush Plus self-bots** (userbots). It provides a clean, intuitive API for automating your personal Soroush Plus account with ease.
38
39
 
39
- > ⚠️ **Note:** This is a **self-bot framework** — it operates on your personal account, not a bot account. Please adhere to Telegram's Terms of Service.
40
+ > ⚠️ **Note:** This is a **self-bot framework** — it operates on your personal account, not a bot account. Please adhere to Soroush's Terms of Service.
40
41
 
41
42
  ---
42
43
 
@@ -1,4 +1,5 @@
1
- # SplusPy v2.2.7
1
+ # SplusPy v2.2.9
2
+
2
3
 
3
4
  **A fast, minimal, and fully-featured self-bot framework**
4
5
 
@@ -8,7 +9,7 @@
8
9
 
9
10
  SplusPy is a Python library designed specifically for building **Soroush Plus self-bots** (userbots). It provides a clean, intuitive API for automating your personal Soroush Plus account with ease.
10
11
 
11
- > ⚠️ **Note:** This is a **self-bot framework** — it operates on your personal account, not a bot account. Please adhere to Telegram's Terms of Service.
12
+ > ⚠️ **Note:** This is a **self-bot framework** — it operates on your personal account, not a bot account. Please adhere to Soroush's Terms of Service.
12
13
 
13
14
  ---
14
15
 
@@ -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.9"
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}'
@@ -298,6 +298,8 @@ class UpdateMethods:
298
298
  event = None
299
299
  if isinstance(update, (types.UpdateNewMessage, types.UpdateNewChannelMessage)):
300
300
  event = NewMessage.build(update, None, self._mb_entity_cache.self_id)
301
+ if event is None and isinstance(update.message, types.MessageService):
302
+ event = ChatAction.build(update, None, self._mb_entity_cache.self_id)
301
303
  elif isinstance(update, (types.UpdateEditMessage, types.UpdateEditChannelMessage)):
302
304
  event = MessageEdited.build(update, None, self._mb_entity_cache.self_id)
303
305
  elif isinstance(update, (types.UpdateDeleteMessages, types.UpdateDeleteChannelMessages)):
@@ -317,24 +319,6 @@ class UpdateMethods:
317
319
  types.UpdateChannelUserTyping,
318
320
  )):
319
321
  event = UserUpdate.build(update, None, self._mb_entity_cache.self_id)
320
- elif isinstance(update, (types.UpdateNewMessage, types.UpdateNewChannelMessage)):
321
- # Check for service messages that are ChatActions
322
- msg = update.message
323
- if isinstance(msg, types.MessageService) and isinstance(msg.action, (
324
- types.MessageActionChatJoinedByLink,
325
- types.MessageActionChatAddUser,
326
- types.MessageActionChatDeleteUser,
327
- types.MessageActionChatCreate,
328
- types.MessageActionChannelCreate,
329
- types.MessageActionChatEditTitle,
330
- types.MessageActionChatEditPhoto,
331
- types.MessageActionChatDeletePhoto,
332
- types.MessageActionPinMessage,
333
- types.MessageActionGameScore,
334
- )):
335
- event = ChatAction.build(update, None, self._mb_entity_cache.self_id)
336
- else:
337
- event = NewMessage.build(update, None, self._mb_entity_cache.self_id)
338
322
 
339
323
  # For Raw handlers, always dispatch with the raw update
340
324
  raw_event = update
@@ -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
@@ -483,6 +483,18 @@ class UserMethods:
483
483
  'GetUsersRequest with access_hash=0 failed for user %s, '
484
484
  'trying contacts fallback', peer.user_id)
485
485
 
486
+ # SoroushPlus groups are channels. A bare positive ID may actually
487
+ # be a group, so try resolving it as a channel before giving up.
488
+ try:
489
+ channels = await self(functions.channels.GetChannelsRequest([
490
+ types.InputChannel(peer.user_id, access_hash=0)]))
491
+ if channels.chats:
492
+ return utils.get_input_peer(channels.chats[0])
493
+ except errors.RPCError:
494
+ _log.debug(
495
+ 'GetChannelsRequest with access_hash=0 failed for %s',
496
+ peer.user_id)
497
+
486
498
  # Fallback: search contacts for the user by ID
487
499
  try:
488
500
  contacts = (await self(
@@ -510,8 +522,10 @@ class UserMethods:
510
522
  )
511
523
 
512
524
  async def _get_peer(self: 'SoroushClient', peer: 'hints.EntityLike'):
513
- i, cls = utils.resolve_id(await self.get_peer_id(peer))
514
- return cls(i)
525
+ if isinstance(peer, int):
526
+ i, cls = utils.resolve_id(await self.get_peer_id(peer))
527
+ return cls(i)
528
+ return utils.get_peer(peer)
515
529
 
516
530
  async def get_peer_id(
517
531
  self: 'SoroushClient',
@@ -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)
@@ -19,9 +19,15 @@ async def _into_id_set(client, chats):
19
19
  for chat in chats:
20
20
  if isinstance(chat, int):
21
21
  if chat < 0:
22
- result.add(chat) # Explicitly marked IDs are negative
22
+ # Explicitly marked IDs (e.g. legacy "-100...") are kept, and
23
+ # their unmarked form is also added so both styles match.
24
+ result.add(chat)
25
+ result.add(utils.resolve_id(chat)[0])
23
26
  else:
24
- result.update({ # Support all valid types of peers
27
+ # A bare positive ID in SoroushPlus can be a user or a group
28
+ # (channels), so add every possible representation to match
29
+ # whichever type the chat actually is.
30
+ result.update({
25
31
  utils.get_peer_id(types.PeerUser(chat)),
26
32
  utils.get_peer_id(types.PeerChat(chat)),
27
33
  utils.get_peer_id(types.PeerChannel(chat)),
@@ -122,7 +128,7 @@ class EventBuilder(abc.ABC):
122
128
  return
123
129
 
124
130
  if self.filters:
125
- if not await self.filters(event.client, event.message if hasattr(event, 'message') else event):
131
+ if not await self.filters(event.client, event):
126
132
  return
127
133
 
128
134
  if not self.func: