spluspy 2.0.0__tar.gz → 2.0.1__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 (165) hide show
  1. {spluspy-2.0.0 → spluspy-2.0.1}/PKG-INFO +1 -1
  2. {spluspy-2.0.0 → spluspy-2.0.1}/spluspy/client/updates.py +16 -23
  3. {spluspy-2.0.0 → spluspy-2.0.1}/spluspy/crypto/rsa.py +10 -0
  4. {spluspy-2.0.0 → spluspy-2.0.1}/spluspy/network/connection/connection.py +19 -4
  5. spluspy-2.0.1/spluspy/network/connection/websocket.py +313 -0
  6. {spluspy-2.0.0 → spluspy-2.0.1}/spluspy/network/mtprotosender.py +27 -8
  7. {spluspy-2.0.0 → spluspy-2.0.1}/spluspy/version.py +1 -1
  8. {spluspy-2.0.0 → spluspy-2.0.1}/spluspy.egg-info/PKG-INFO +1 -1
  9. spluspy-2.0.0/spluspy/network/connection/websocket.py +0 -186
  10. {spluspy-2.0.0 → spluspy-2.0.1}/README.rst +0 -0
  11. {spluspy-2.0.0 → spluspy-2.0.1}/setup.cfg +0 -0
  12. {spluspy-2.0.0 → spluspy-2.0.1}/setup.py +0 -0
  13. {spluspy-2.0.0 → spluspy-2.0.1}/spluspy/__init__.py +0 -0
  14. {spluspy-2.0.0 → spluspy-2.0.1}/spluspy/_updates/__init__.py +0 -0
  15. {spluspy-2.0.0 → spluspy-2.0.1}/spluspy/_updates/entitycache.py +0 -0
  16. {spluspy-2.0.0 → spluspy-2.0.1}/spluspy/_updates/messagebox.py +0 -0
  17. {spluspy-2.0.0 → spluspy-2.0.1}/spluspy/_updates/session.py +0 -0
  18. {spluspy-2.0.0 → spluspy-2.0.1}/spluspy/client/__init__.py +0 -0
  19. {spluspy-2.0.0 → spluspy-2.0.1}/spluspy/client/account.py +0 -0
  20. {spluspy-2.0.0 → spluspy-2.0.1}/spluspy/client/auth.py +0 -0
  21. {spluspy-2.0.0 → spluspy-2.0.1}/spluspy/client/bots.py +0 -0
  22. {spluspy-2.0.0 → spluspy-2.0.1}/spluspy/client/buttons.py +0 -0
  23. {spluspy-2.0.0 → spluspy-2.0.1}/spluspy/client/chats.py +0 -0
  24. {spluspy-2.0.0 → spluspy-2.0.1}/spluspy/client/dialogs.py +0 -0
  25. {spluspy-2.0.0 → spluspy-2.0.1}/spluspy/client/downloads.py +0 -0
  26. {spluspy-2.0.0 → spluspy-2.0.1}/spluspy/client/messageparse.py +0 -0
  27. {spluspy-2.0.0 → spluspy-2.0.1}/spluspy/client/messages.py +0 -0
  28. {spluspy-2.0.0 → spluspy-2.0.1}/spluspy/client/ratelimiter.py +0 -0
  29. {spluspy-2.0.0 → spluspy-2.0.1}/spluspy/client/soroushclient.py +0 -0
  30. {spluspy-2.0.0 → spluspy-2.0.1}/spluspy/client/telegrambaseclient.py +0 -0
  31. {spluspy-2.0.0 → spluspy-2.0.1}/spluspy/client/uploads.py +0 -0
  32. {spluspy-2.0.0 → spluspy-2.0.1}/spluspy/client/users.py +0 -0
  33. {spluspy-2.0.0 → spluspy-2.0.1}/spluspy/crypto/__init__.py +0 -0
  34. {spluspy-2.0.0 → spluspy-2.0.1}/spluspy/crypto/aes.py +0 -0
  35. {spluspy-2.0.0 → spluspy-2.0.1}/spluspy/crypto/aesctr.py +0 -0
  36. {spluspy-2.0.0 → spluspy-2.0.1}/spluspy/crypto/authkey.py +0 -0
  37. {spluspy-2.0.0 → spluspy-2.0.1}/spluspy/crypto/cdndecrypter.py +0 -0
  38. {spluspy-2.0.0 → spluspy-2.0.1}/spluspy/crypto/factorization.py +0 -0
  39. {spluspy-2.0.0 → spluspy-2.0.1}/spluspy/crypto/libssl.py +0 -0
  40. {spluspy-2.0.0 → spluspy-2.0.1}/spluspy/custom.py +0 -0
  41. {spluspy-2.0.0 → spluspy-2.0.1}/spluspy/errors/__init__.py +0 -0
  42. {spluspy-2.0.0 → spluspy-2.0.1}/spluspy/errors/common.py +0 -0
  43. {spluspy-2.0.0 → spluspy-2.0.1}/spluspy/errors/rpcbaseerrors.py +0 -0
  44. {spluspy-2.0.0 → spluspy-2.0.1}/spluspy/errors/rpcerrorlist.py +0 -0
  45. {spluspy-2.0.0 → spluspy-2.0.1}/spluspy/events/__init__.py +0 -0
  46. {spluspy-2.0.0 → spluspy-2.0.1}/spluspy/events/album.py +0 -0
  47. {spluspy-2.0.0 → spluspy-2.0.1}/spluspy/events/callbackquery.py +0 -0
  48. {spluspy-2.0.0 → spluspy-2.0.1}/spluspy/events/chataction.py +0 -0
  49. {spluspy-2.0.0 → spluspy-2.0.1}/spluspy/events/common.py +0 -0
  50. {spluspy-2.0.0 → spluspy-2.0.1}/spluspy/events/decorators.py +0 -0
  51. {spluspy-2.0.0 → spluspy-2.0.1}/spluspy/events/inlinequery.py +0 -0
  52. {spluspy-2.0.0 → spluspy-2.0.1}/spluspy/events/messagedeleted.py +0 -0
  53. {spluspy-2.0.0 → spluspy-2.0.1}/spluspy/events/messageedited.py +0 -0
  54. {spluspy-2.0.0 → spluspy-2.0.1}/spluspy/events/messageread.py +0 -0
  55. {spluspy-2.0.0 → spluspy-2.0.1}/spluspy/events/newmessage.py +0 -0
  56. {spluspy-2.0.0 → spluspy-2.0.1}/spluspy/events/raw.py +0 -0
  57. {spluspy-2.0.0 → spluspy-2.0.1}/spluspy/events/userupdate.py +0 -0
  58. {spluspy-2.0.0 → spluspy-2.0.1}/spluspy/extensions/__init__.py +0 -0
  59. {spluspy-2.0.0 → spluspy-2.0.1}/spluspy/extensions/binaryreader.py +0 -0
  60. {spluspy-2.0.0 → spluspy-2.0.1}/spluspy/extensions/html.py +0 -0
  61. {spluspy-2.0.0 → spluspy-2.0.1}/spluspy/extensions/markdown.py +0 -0
  62. {spluspy-2.0.0 → spluspy-2.0.1}/spluspy/extensions/messagepacker.py +0 -0
  63. {spluspy-2.0.0 → spluspy-2.0.1}/spluspy/functions.py +0 -0
  64. {spluspy-2.0.0 → spluspy-2.0.1}/spluspy/helpers.py +0 -0
  65. {spluspy-2.0.0 → spluspy-2.0.1}/spluspy/hints.py +0 -0
  66. {spluspy-2.0.0 → spluspy-2.0.1}/spluspy/network/__init__.py +0 -0
  67. {spluspy-2.0.0 → spluspy-2.0.1}/spluspy/network/authenticator.py +0 -0
  68. {spluspy-2.0.0 → spluspy-2.0.1}/spluspy/network/connection/__init__.py +0 -0
  69. {spluspy-2.0.0 → spluspy-2.0.1}/spluspy/network/connection/http.py +0 -0
  70. {spluspy-2.0.0 → spluspy-2.0.1}/spluspy/network/connection/tcpabridged.py +0 -0
  71. {spluspy-2.0.0 → spluspy-2.0.1}/spluspy/network/connection/tcpfull.py +0 -0
  72. {spluspy-2.0.0 → spluspy-2.0.1}/spluspy/network/connection/tcpintermediate.py +0 -0
  73. {spluspy-2.0.0 → spluspy-2.0.1}/spluspy/network/connection/tcpmtproxy.py +0 -0
  74. {spluspy-2.0.0 → spluspy-2.0.1}/spluspy/network/connection/tcpobfuscated.py +0 -0
  75. {spluspy-2.0.0 → spluspy-2.0.1}/spluspy/network/mtprotoplainsender.py +0 -0
  76. {spluspy-2.0.0 → spluspy-2.0.1}/spluspy/network/mtprotostate.py +0 -0
  77. {spluspy-2.0.0 → spluspy-2.0.1}/spluspy/network/requeststate.py +0 -0
  78. {spluspy-2.0.0 → spluspy-2.0.1}/spluspy/password.py +0 -0
  79. {spluspy-2.0.0 → spluspy-2.0.1}/spluspy/requestiter.py +0 -0
  80. {spluspy-2.0.0 → spluspy-2.0.1}/spluspy/sessions/__init__.py +0 -0
  81. {spluspy-2.0.0 → spluspy-2.0.1}/spluspy/sessions/abstract.py +0 -0
  82. {spluspy-2.0.0 → spluspy-2.0.1}/spluspy/sessions/memory.py +0 -0
  83. {spluspy-2.0.0 → spluspy-2.0.1}/spluspy/sessions/sqlite.py +0 -0
  84. {spluspy-2.0.0 → spluspy-2.0.1}/spluspy/sessions/string.py +0 -0
  85. {spluspy-2.0.0 → spluspy-2.0.1}/spluspy/sync.py +0 -0
  86. {spluspy-2.0.0 → spluspy-2.0.1}/spluspy/tl/__init__.py +0 -0
  87. {spluspy-2.0.0 → spluspy-2.0.1}/spluspy/tl/alltlobjects.py +0 -0
  88. {spluspy-2.0.0 → spluspy-2.0.1}/spluspy/tl/core/__init__.py +0 -0
  89. {spluspy-2.0.0 → spluspy-2.0.1}/spluspy/tl/core/gzippacked.py +0 -0
  90. {spluspy-2.0.0 → spluspy-2.0.1}/spluspy/tl/core/messagecontainer.py +0 -0
  91. {spluspy-2.0.0 → spluspy-2.0.1}/spluspy/tl/core/rpcresult.py +0 -0
  92. {spluspy-2.0.0 → spluspy-2.0.1}/spluspy/tl/core/tlmessage.py +0 -0
  93. {spluspy-2.0.0 → spluspy-2.0.1}/spluspy/tl/custom/__init__.py +0 -0
  94. {spluspy-2.0.0 → spluspy-2.0.1}/spluspy/tl/custom/adminlogevent.py +0 -0
  95. {spluspy-2.0.0 → spluspy-2.0.1}/spluspy/tl/custom/button.py +0 -0
  96. {spluspy-2.0.0 → spluspy-2.0.1}/spluspy/tl/custom/chatgetter.py +0 -0
  97. {spluspy-2.0.0 → spluspy-2.0.1}/spluspy/tl/custom/conversation.py +0 -0
  98. {spluspy-2.0.0 → spluspy-2.0.1}/spluspy/tl/custom/dialog.py +0 -0
  99. {spluspy-2.0.0 → spluspy-2.0.1}/spluspy/tl/custom/draft.py +0 -0
  100. {spluspy-2.0.0 → spluspy-2.0.1}/spluspy/tl/custom/file.py +0 -0
  101. {spluspy-2.0.0 → spluspy-2.0.1}/spluspy/tl/custom/forward.py +0 -0
  102. {spluspy-2.0.0 → spluspy-2.0.1}/spluspy/tl/custom/inlinebuilder.py +0 -0
  103. {spluspy-2.0.0 → spluspy-2.0.1}/spluspy/tl/custom/inlineresult.py +0 -0
  104. {spluspy-2.0.0 → spluspy-2.0.1}/spluspy/tl/custom/inlineresults.py +0 -0
  105. {spluspy-2.0.0 → spluspy-2.0.1}/spluspy/tl/custom/inputsizedfile.py +0 -0
  106. {spluspy-2.0.0 → spluspy-2.0.1}/spluspy/tl/custom/message.py +0 -0
  107. {spluspy-2.0.0 → spluspy-2.0.1}/spluspy/tl/custom/messagebutton.py +0 -0
  108. {spluspy-2.0.0 → spluspy-2.0.1}/spluspy/tl/custom/participantpermissions.py +0 -0
  109. {spluspy-2.0.0 → spluspy-2.0.1}/spluspy/tl/custom/qrlogin.py +0 -0
  110. {spluspy-2.0.0 → spluspy-2.0.1}/spluspy/tl/custom/sendergetter.py +0 -0
  111. {spluspy-2.0.0 → spluspy-2.0.1}/spluspy/tl/custom/types.py +0 -0
  112. {spluspy-2.0.0 → spluspy-2.0.1}/spluspy/tl/functions/__init__.py +0 -0
  113. {spluspy-2.0.0 → spluspy-2.0.1}/spluspy/tl/functions/account.py +0 -0
  114. {spluspy-2.0.0 → spluspy-2.0.1}/spluspy/tl/functions/auth.py +0 -0
  115. {spluspy-2.0.0 → spluspy-2.0.1}/spluspy/tl/functions/bots.py +0 -0
  116. {spluspy-2.0.0 → spluspy-2.0.1}/spluspy/tl/functions/channels.py +0 -0
  117. {spluspy-2.0.0 → spluspy-2.0.1}/spluspy/tl/functions/chatlists.py +0 -0
  118. {spluspy-2.0.0 → spluspy-2.0.1}/spluspy/tl/functions/conference.py +0 -0
  119. {spluspy-2.0.0 → spluspy-2.0.1}/spluspy/tl/functions/contacts.py +0 -0
  120. {spluspy-2.0.0 → spluspy-2.0.1}/spluspy/tl/functions/folders.py +0 -0
  121. {spluspy-2.0.0 → spluspy-2.0.1}/spluspy/tl/functions/help.py +0 -0
  122. {spluspy-2.0.0 → spluspy-2.0.1}/spluspy/tl/functions/langpack.py +0 -0
  123. {spluspy-2.0.0 → spluspy-2.0.1}/spluspy/tl/functions/messages.py +0 -0
  124. {spluspy-2.0.0 → spluspy-2.0.1}/spluspy/tl/functions/payments.py +0 -0
  125. {spluspy-2.0.0 → spluspy-2.0.1}/spluspy/tl/functions/phone.py +0 -0
  126. {spluspy-2.0.0 → spluspy-2.0.1}/spluspy/tl/functions/photos.py +0 -0
  127. {spluspy-2.0.0 → spluspy-2.0.1}/spluspy/tl/functions/premium.py +0 -0
  128. {spluspy-2.0.0 → spluspy-2.0.1}/spluspy/tl/functions/stats.py +0 -0
  129. {spluspy-2.0.0 → spluspy-2.0.1}/spluspy/tl/functions/stories.py +0 -0
  130. {spluspy-2.0.0 → spluspy-2.0.1}/spluspy/tl/functions/thirdParty.py +0 -0
  131. {spluspy-2.0.0 → spluspy-2.0.1}/spluspy/tl/functions/updates.py +0 -0
  132. {spluspy-2.0.0 → spluspy-2.0.1}/spluspy/tl/functions/upload.py +0 -0
  133. {spluspy-2.0.0 → spluspy-2.0.1}/spluspy/tl/functions/users.py +0 -0
  134. {spluspy-2.0.0 → spluspy-2.0.1}/spluspy/tl/patched/__init__.py +0 -0
  135. {spluspy-2.0.0 → spluspy-2.0.1}/spluspy/tl/tlobject.py +0 -0
  136. {spluspy-2.0.0 → spluspy-2.0.1}/spluspy/tl/types/__init__.py +0 -0
  137. {spluspy-2.0.0 → spluspy-2.0.1}/spluspy/tl/types/account.py +0 -0
  138. {spluspy-2.0.0 → spluspy-2.0.1}/spluspy/tl/types/auth.py +0 -0
  139. {spluspy-2.0.0 → spluspy-2.0.1}/spluspy/tl/types/bots.py +0 -0
  140. {spluspy-2.0.0 → spluspy-2.0.1}/spluspy/tl/types/channels.py +0 -0
  141. {spluspy-2.0.0 → spluspy-2.0.1}/spluspy/tl/types/chatlists.py +0 -0
  142. {spluspy-2.0.0 → spluspy-2.0.1}/spluspy/tl/types/conference.py +0 -0
  143. {spluspy-2.0.0 → spluspy-2.0.1}/spluspy/tl/types/contacts.py +0 -0
  144. {spluspy-2.0.0 → spluspy-2.0.1}/spluspy/tl/types/help.py +0 -0
  145. {spluspy-2.0.0 → spluspy-2.0.1}/spluspy/tl/types/messages.py +0 -0
  146. {spluspy-2.0.0 → spluspy-2.0.1}/spluspy/tl/types/payments.py +0 -0
  147. {spluspy-2.0.0 → spluspy-2.0.1}/spluspy/tl/types/phone.py +0 -0
  148. {spluspy-2.0.0 → spluspy-2.0.1}/spluspy/tl/types/photos.py +0 -0
  149. {spluspy-2.0.0 → spluspy-2.0.1}/spluspy/tl/types/premium.py +0 -0
  150. {spluspy-2.0.0 → spluspy-2.0.1}/spluspy/tl/types/stats.py +0 -0
  151. {spluspy-2.0.0 → spluspy-2.0.1}/spluspy/tl/types/stickers.py +0 -0
  152. {spluspy-2.0.0 → spluspy-2.0.1}/spluspy/tl/types/storage.py +0 -0
  153. {spluspy-2.0.0 → spluspy-2.0.1}/spluspy/tl/types/stories.py +0 -0
  154. {spluspy-2.0.0 → spluspy-2.0.1}/spluspy/tl/types/thirdParty.py +0 -0
  155. {spluspy-2.0.0 → spluspy-2.0.1}/spluspy/tl/types/update.py +0 -0
  156. {spluspy-2.0.0 → spluspy-2.0.1}/spluspy/tl/types/updates.py +0 -0
  157. {spluspy-2.0.0 → spluspy-2.0.1}/spluspy/tl/types/upload.py +0 -0
  158. {spluspy-2.0.0 → spluspy-2.0.1}/spluspy/tl/types/users.py +0 -0
  159. {spluspy-2.0.0 → spluspy-2.0.1}/spluspy/types.py +0 -0
  160. {spluspy-2.0.0 → spluspy-2.0.1}/spluspy/updater.py +0 -0
  161. {spluspy-2.0.0 → spluspy-2.0.1}/spluspy/utils.py +0 -0
  162. {spluspy-2.0.0 → spluspy-2.0.1}/spluspy.egg-info/SOURCES.txt +0 -0
  163. {spluspy-2.0.0 → spluspy-2.0.1}/spluspy.egg-info/dependency_links.txt +0 -0
  164. {spluspy-2.0.0 → spluspy-2.0.1}/spluspy.egg-info/requires.txt +0 -0
  165. {spluspy-2.0.0 → spluspy-2.0.1}/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.0.0
3
+ Version: 2.0.1
4
4
  Summary: Soroush Plus library for Python
5
5
  Home-page: https://github.com/Itskillmaster/spluspy
6
6
  Download-URL: https://github.com/Itskillmaster/spluspy/releases
@@ -477,27 +477,23 @@ class UpdateMethods:
477
477
  updates_to_dispatch.extend(_preprocess_updates)
478
478
  continue
479
479
 
480
- # Drain all available updates from the queue in one batch
481
- # to minimize loop iterations and dispatch latency.
482
- pending_updates = []
480
+ # Drain the update queue aggressively before sleeping
481
+ got_update = False
483
482
  try:
484
- while True:
485
- pending_updates.append(self._updates_queue.get_nowait())
483
+ updates = self._updates_queue.get_nowait()
484
+ got_update = True
486
485
  except asyncio.QueueEmpty:
487
486
  pass
488
487
 
489
- if pending_updates:
490
- for updates in pending_updates:
491
- processed = []
492
- try:
493
- users, chats = self._message_box.process_updates(
494
- updates, self._mb_entity_cache, processed)
495
- except GapError:
496
- continue # get(_channel)_difference will start returning requests
497
-
498
- _preprocess_updates = await self._preprocess_updates(
499
- processed, users, chats)
500
- updates_to_dispatch.extend(_preprocess_updates)
488
+ if got_update:
489
+ processed = []
490
+ try:
491
+ users, chats = self._message_box.process_updates(updates, self._mb_entity_cache, processed)
492
+ except GapError:
493
+ continue # get(_channel)_difference will start returning requests
494
+
495
+ _preprocess_updates = await self._preprocess_updates(processed, users, chats)
496
+ updates_to_dispatch.extend(_preprocess_updates)
501
497
  continue
502
498
 
503
499
  # Wait for next update or deadline
@@ -505,8 +501,7 @@ class UpdateMethods:
505
501
  deadline_delay = deadline - get_running_loop().time()
506
502
  if deadline_delay > 0:
507
503
  try:
508
- updates = await asyncio.wait_for(
509
- self._updates_queue.get(), deadline_delay)
504
+ updates = await asyncio.wait_for(self._updates_queue.get(), deadline_delay)
510
505
  except asyncio.TimeoutError:
511
506
  continue
512
507
  else:
@@ -514,13 +509,11 @@ class UpdateMethods:
514
509
 
515
510
  processed = []
516
511
  try:
517
- users, chats = self._message_box.process_updates(
518
- updates, self._mb_entity_cache, processed)
512
+ users, chats = self._message_box.process_updates(updates, self._mb_entity_cache, processed)
519
513
  except GapError:
520
514
  continue # get(_channel)_difference will start returning requests
521
515
 
522
- _preprocess_updates = await self._preprocess_updates(
523
- processed, users, chats)
516
+ _preprocess_updates = await self._preprocess_updates(processed, users, chats)
524
517
  updates_to_dispatch.extend(_preprocess_updates)
525
518
  except asyncio.CancelledError:
526
519
  pass
@@ -266,3 +266,13 @@ eJzmIQvYNmlq5/zb2XxJpZMUAg3iS0nqTQIDAQAB
266
266
  -----END RSA PUBLIC KEY-----''',
267
267
  ):
268
268
  add_key(pub, old=False)
269
+
270
+ # Add one old key for backward compatibility
271
+ add_key('''-----BEGIN RSA PUBLIC KEY-----
272
+ MIIBCgKCAQEAwACIRK5lXVLU/qPY1VuCssYQjNqh6dUr9UCmkHoOklt7X/Ki1Gqu
273
+ PeVx5VL3NLn64V1heDZqwKZ11NCSS+XBxXh5T2cXRf1Et5USaHtUcU6y0fDFWOBo
274
+ kyehfkJ4KHedI5Z6wkRdSxOVITeBLhIV9OYsa+MuOa7xiD9A8Ogh4Y7k+jA2/zPl
275
+ D41nej3OZEQnDIe762N6J54FzGuKVGtg+NSwkf/h1ULtSg5ifx9bj/vKDNhwdCrm
276
+ Q6IYeDsWwDYm6Rup+9+nUCA/khMpHpzNnEdUwcAa8HQeZCpJX7TZPZRew3T6VhS9
277
+ lv827e/5tL7bohxJrFlyf4BcRzP2I0Nu7QIDAQAB
278
+ -----END RSA PUBLIC KEY-----''', old=True)
@@ -47,8 +47,8 @@ class Connection(abc.ABC):
47
47
  self._recv_task = None
48
48
  self._codec = None
49
49
  self._obfuscation = None # TcpObfuscated and MTProxy
50
- self._send_queue = asyncio.Queue(1)
51
- self._recv_queue = asyncio.Queue(1)
50
+ self._send_queue = asyncio.Queue(500)
51
+ self._recv_queue = asyncio.Queue(500)
52
52
 
53
53
  @staticmethod
54
54
  def _wrap_socket_ssl(sock):
@@ -62,7 +62,7 @@ class Connection(abc.ABC):
62
62
  sock,
63
63
  do_handshake_on_connect=True,
64
64
  ssl_version=ssl_mod.PROTOCOL_SSLv23,
65
- ciphers='ADH-AES256-SHA')
65
+ ciphers='ECDHE-RSA-AES128-GCM-SHA256:ECDHE-RSA-AES256-GCM-SHA384:AES128-GCM-SHA256')
66
66
 
67
67
  @staticmethod
68
68
  def _parse_proxy(proxy_type, addr, port, rdns=True, username=None, password=None):
@@ -316,10 +316,25 @@ class Connection(abc.ABC):
316
316
  async def _send_loop(self):
317
317
  """
318
318
  This loop is constantly popping items off the queue to send them.
319
+
320
+ Optimized to batch multiple sends before draining to reduce
321
+ the number of system calls and improve throughput.
319
322
  """
320
323
  try:
321
324
  while self._connected:
322
- self._send(await self._send_queue.get())
325
+ # Wait for first item
326
+ data = await self._send_queue.get()
327
+ self._send(data)
328
+
329
+ # Try to drain the queue without waiting to batch sends
330
+ while not self._send_queue.empty():
331
+ try:
332
+ data = self._send_queue.get_nowait()
333
+ self._send(data)
334
+ except asyncio.QueueEmpty:
335
+ break
336
+
337
+ # Drain once after batch
323
338
  await self._writer.drain()
324
339
  except asyncio.CancelledError:
325
340
  pass
@@ -0,0 +1,313 @@
1
+ import asyncio
2
+ import collections
3
+ import socket
4
+
5
+ try:
6
+ import aiohttp
7
+ except ImportError:
8
+ aiohttp = None
9
+
10
+ from .tcpabridged import AbridgedPacketCodec
11
+ from .connection import ObfuscatedConnection
12
+
13
+ from ...crypto import AESModeCTR
14
+
15
+ import os
16
+
17
+
18
+ class WebSocketReader:
19
+ """
20
+ Adapter that wraps an aiohttp WebSocket connection to expose
21
+ the ``readexactly(n)`` interface expected by ``PacketCodec``.
22
+
23
+ Uses a deque of received chunks and memoryview for zero-copy slicing
24
+ to minimize memory allocations under high message rates.
25
+ """
26
+ __slots__ = ('_ws', '_chunks', '_chunk_offset', '_total_len')
27
+
28
+ def __init__(self, ws):
29
+ self._ws = ws
30
+ self._chunks = collections.deque()
31
+ self._chunk_offset = 0
32
+ self._total_len = 0
33
+
34
+ async def readexactly(self, n):
35
+ while self._total_len < n:
36
+ try:
37
+ msg = await self._ws.receive()
38
+ except Exception:
39
+ break
40
+ if msg.type in (aiohttp.WSMsgType.CLOSE, aiohttp.WSMsgType.CLOSING,
41
+ aiohttp.WSMsgType.CLOSED):
42
+ break
43
+ if msg.type == aiohttp.WSMsgType.ERROR:
44
+ break
45
+ data = msg.data
46
+ if isinstance(data, str):
47
+ data = data.encode()
48
+ self._chunks.append(data)
49
+ self._total_len += len(data)
50
+
51
+ if self._total_len < n:
52
+ raise IOError('WebSocket closed while reading')
53
+
54
+ # Fast path: entire read is within the first chunk
55
+ first = self._chunks[0]
56
+ first_avail = len(first) - self._chunk_offset
57
+ if first_avail >= n:
58
+ result = first[self._chunk_offset:self._chunk_offset + n]
59
+ self._chunk_offset += n
60
+ self._total_len -= n
61
+ if self._chunk_offset >= len(first):
62
+ self._chunks.popleft()
63
+ self._chunk_offset = 0
64
+ return result
65
+
66
+ # Slow path: read spans multiple chunks
67
+ # Pre-allocate buffer for known size to avoid list growing
68
+ parts = bytearray(n)
69
+ offset = 0
70
+ remaining = n
71
+
72
+ if self._chunk_offset > 0:
73
+ chunk = self._chunks[0]
74
+ avail = len(chunk) - self._chunk_offset
75
+ parts[offset:offset + avail] = memoryview(chunk)[self._chunk_offset:]
76
+ offset += avail
77
+ remaining -= avail
78
+ self._chunks.popleft()
79
+ self._chunk_offset = 0
80
+
81
+ while remaining > 0:
82
+ chunk = self._chunks.popleft()
83
+ chunk_len = len(chunk)
84
+ if chunk_len <= remaining:
85
+ parts[offset:offset + chunk_len] = chunk
86
+ offset += chunk_len
87
+ remaining -= chunk_len
88
+ else:
89
+ parts[offset:offset + remaining] = memoryview(chunk)[:remaining]
90
+ self._chunks.appendleft(chunk)
91
+ self._chunk_offset = remaining
92
+ remaining = 0
93
+
94
+ self._total_len -= n
95
+ if not self._chunks:
96
+ self._chunk_offset = 0
97
+ return bytes(parts)
98
+
99
+
100
+ class WebSocketWriter:
101
+ """
102
+ Adapter that wraps an aiohttp WebSocket connection to expose
103
+ the ``write(data)`` / ``drain()`` / ``close()`` interface
104
+ expected by ``Connection``.
105
+
106
+ Buffers pending data in a bytearray and sends in bulk on drain()
107
+ to reduce syscall overhead.
108
+ """
109
+ __slots__ = ('_ws', '_pending')
110
+
111
+ def __init__(self, ws):
112
+ self._ws = ws
113
+ self._pending = bytearray()
114
+
115
+ def write(self, data):
116
+ self._pending.extend(data)
117
+
118
+ async def drain(self):
119
+ if self._pending:
120
+ data = bytes(self._pending)
121
+ self._pending.clear()
122
+ await self._ws.send_bytes(data)
123
+
124
+ def close(self):
125
+ pass
126
+
127
+ async def wait_closed(self):
128
+ if self._ws is not None:
129
+ try:
130
+ await self._ws.close()
131
+ except Exception:
132
+ pass
133
+
134
+
135
+ class WebSocketObfuscatedIO:
136
+ """
137
+ Same obfuscation logic as ``ObfuscatedIO`` in ``tcpobfuscated.py``,
138
+ but works with ``WebSocketReader`` / ``WebSocketWriter`` adapters.
139
+ """
140
+ header = None
141
+
142
+ def __init__(self, connection):
143
+ self._reader = connection._reader
144
+ self._writer = connection._writer
145
+
146
+ (self.header,
147
+ self._encrypt,
148
+ self._decrypt) = self._init_header(connection.packet_codec)
149
+
150
+ @staticmethod
151
+ def _init_header(packet_codec):
152
+ keywords = (b'PVrG', b'GET ', b'POST', b'\xee\xee\xee\xee')
153
+ while True:
154
+ random = os.urandom(64)
155
+ if (random[0] != 0xef and
156
+ random[:4] not in keywords and
157
+ random[4:8] != b'\0\0\0\0'):
158
+ break
159
+
160
+ random = bytearray(random)
161
+ random_reversed = random[55:7:-1]
162
+
163
+ encrypt_key = bytes(random[8:40])
164
+ encrypt_iv = bytes(random[40:56])
165
+ decrypt_key = bytes(random_reversed[:32])
166
+ decrypt_iv = bytes(random_reversed[32:48])
167
+
168
+ encryptor = AESModeCTR(encrypt_key, encrypt_iv)
169
+ decryptor = AESModeCTR(decrypt_key, decrypt_iv)
170
+
171
+ random[56:60] = packet_codec.obfuscate_tag
172
+ random[56:64] = encryptor.encrypt(bytes(random))[56:64]
173
+ return (random, encryptor, decryptor)
174
+
175
+ async def readexactly(self, n):
176
+ return self._decrypt.encrypt(await self._reader.readexactly(n))
177
+
178
+ def write(self, data):
179
+ self._writer.write(self._encrypt.encrypt(data))
180
+
181
+
182
+ class ConnectionWebSocket(ObfuscatedConnection):
183
+ """
184
+ WebSocket transport for Soroush messenger.
185
+
186
+ Connects to ``wss://{ip}:{port}/apiws`` with Origin header
187
+ set to ``https://web.splus.ir``, then uses obfuscated+abridged
188
+ protocol on top of the WebSocket binary frames.
189
+ """
190
+ obfuscated_io = WebSocketObfuscatedIO
191
+ packet_codec = AbridgedPacketCodec
192
+ # Class-level session cache to reuse across reconnections
193
+ _cached_session = None
194
+ _cached_session_key = None
195
+
196
+ async def _connect(self, timeout=None, ssl=None):
197
+ if aiohttp is None:
198
+ raise ImportError(
199
+ 'ConnectionWebSocket requires the "aiohttp" package. '
200
+ 'Install it with: pip install aiohttp'
201
+ )
202
+
203
+ protocol = 'wss' if self._port == 443 else 'ws'
204
+ url = f'{protocol}://{self._ip}:{self._port}/apiws'
205
+
206
+ extra_headers = {
207
+ 'Origin': 'https://web.splus.ir'
208
+ }
209
+
210
+ connect_timeout = aiohttp.ClientTimeout(total=timeout) if timeout else None
211
+ # Reuse cached session if connection parameters haven't changed
212
+ session_key = (self._ip, self._port, self._proxy, getattr(self, '_local_addr', None))
213
+ if ConnectionWebSocket._cached_session is not None and ConnectionWebSocket._cached_session_key == session_key:
214
+ session = ConnectionWebSocket._cached_session
215
+ else:
216
+ if ConnectionWebSocket._cached_session is not None:
217
+ try:
218
+ await asyncio.wait_for(ConnectionWebSocket._cached_session.close(), timeout=5)
219
+ except Exception:
220
+ pass
221
+ session = aiohttp.ClientSession(timeout=connect_timeout)
222
+ ConnectionWebSocket._cached_session = session
223
+ ConnectionWebSocket._cached_session_key = session_key
224
+
225
+ try:
226
+ self._ws = await asyncio.wait_for(
227
+ session.ws_connect(
228
+ url,
229
+ headers=extra_headers,
230
+ protocols=["binary"],
231
+ max_msg_size=2 ** 24, # 16 MB
232
+ heartbeat=None, # MTProto keepalive is used instead
233
+ ),
234
+ timeout=timeout
235
+ )
236
+ except Exception:
237
+ # Only close session if it's not cached
238
+ if session is not ConnectionWebSocket._cached_session:
239
+ await session.close()
240
+ raise
241
+
242
+ self._session = session
243
+ self._reader = WebSocketReader(self._ws)
244
+ self._writer = WebSocketWriter(self._ws)
245
+
246
+ # Optimize underlying socket for low latency
247
+ try:
248
+ transport = None
249
+ writer_obj = getattr(self._ws, '_writer', None)
250
+ if writer_obj is not None:
251
+ transport = getattr(writer_obj, '_transport', None)
252
+ if transport is not None and hasattr(transport, 'get_extra_info'):
253
+ sock = transport.get_extra_info('socket')
254
+ if sock is not None:
255
+ # Disable Nagle's algorithm for lower latency
256
+ sock.setsockopt(socket.IPPROTO_TCP, socket.TCP_NODELAY, 1)
257
+
258
+ # Enable TCP keepalive to detect dead connections
259
+ sock.setsockopt(socket.SOL_SOCKET, socket.SO_KEEPALIVE, 1)
260
+
261
+ # Reduce send/recv buffer sizes for lower memory usage
262
+ try:
263
+ sock.setsockopt(socket.SOL_SOCKET, socket.SO_SNDBUF, 65536)
264
+ sock.setsockopt(socket.SOL_SOCKET, socket.SO_RCVBUF, 65536)
265
+ except OSError:
266
+ pass
267
+
268
+ # Enable TCP Quick ACK on Linux for faster response
269
+ try:
270
+ TCP_QUICKACK = 12
271
+ sock.setsockopt(socket.IPPROTO_TCP, TCP_QUICKACK, 1)
272
+ except (OSError, AttributeError):
273
+ pass
274
+ except Exception:
275
+ pass
276
+
277
+ self._codec = self.packet_codec(self)
278
+ self._init_conn()
279
+ await self._writer.drain()
280
+
281
+ async def disconnect(self):
282
+ if not self._connected:
283
+ return
284
+
285
+ self._connected = False
286
+
287
+ from ... import helpers
288
+ await helpers._cancel(
289
+ self._log,
290
+ send_task=self._send_task,
291
+ recv_task=self._recv_task
292
+ )
293
+
294
+ if hasattr(self, '_writer') and self._writer:
295
+ self._writer.close()
296
+ try:
297
+ await asyncio.wait_for(self._writer.wait_closed(), timeout=10)
298
+ except asyncio.TimeoutError:
299
+ self._log.warning(
300
+ 'WebSocket disconnection timed out, forcibly ignoring cleanup'
301
+ )
302
+ except Exception as e:
303
+ self._log.info(
304
+ '%s during disconnect: %s', type(e), e
305
+ )
306
+
307
+ if hasattr(self, '_session') and self._session:
308
+ # Don't close session if it's cached for reuse
309
+ if self._session is not ConnectionWebSocket._cached_session:
310
+ try:
311
+ await asyncio.wait_for(self._session.close(), timeout=5)
312
+ except Exception:
313
+ pass
@@ -455,18 +455,18 @@ class MTProtoSender:
455
455
  queue, encrypting them, and sending them over the network.
456
456
 
457
457
  Besides `connect`, only this method ever sends data.
458
+
459
+ Optimized to batch pending acks with outgoing messages
460
+ and reduce encryption overhead.
458
461
  """
459
462
  while self._user_connected and not self._reconnecting:
460
463
  if self._pending_ack:
461
- ack = RequestState(MsgsAck(list(self._pending_ack)))
464
+ ack = RequestState(MsgsAck(self._pending_ack))
462
465
  self._send_queue.append(ack)
463
466
  self._last_acks.append(ack)
464
467
  self._pending_ack.clear()
465
468
 
466
469
  self._log.debug('Waiting for messages to send...')
467
- # TODO Wait for the connection send queue to be empty?
468
- # This means that while it's not empty we can wait for
469
- # more messages to be added to the send queue.
470
470
  batch, data = await self._send_queue.get()
471
471
 
472
472
  if not data:
@@ -563,10 +563,29 @@ class MTProtoSender:
563
563
  self._start_reconnect(e)
564
564
  return
565
565
 
566
- try:
567
- await self._process_message(message)
568
- except Exception:
569
- self._log.exception('Unhandled error while processing msgs')
566
+ # Batch-drain available receives to reduce event-loop overhead
567
+ batch = [message]
568
+ while True:
569
+ try:
570
+ next_data = self._connection._recv_queue.get_nowait()
571
+ if next_data[1] is not None:
572
+ # Error — re-queue and stop batching
573
+ await self._connection._recv_queue.put(next_data)
574
+ break
575
+ next_body = next_data[0]
576
+ next_msg = self._state.decrypt_message_data(next_body)
577
+ if next_msg is not None:
578
+ batch.append(next_msg)
579
+ except asyncio.QueueEmpty:
580
+ break
581
+ except Exception:
582
+ break
583
+
584
+ for msg in batch:
585
+ try:
586
+ await self._process_message(msg)
587
+ except Exception:
588
+ self._log.exception('Unhandled error while processing msgs')
570
589
 
571
590
  # Response Handlers
572
591
 
@@ -1,3 +1,3 @@
1
1
  # Versions should comply with PEP440.
2
2
  # This line is parsed in setup.py:
3
- __version__ = '2.0.0'
3
+ __version__ = '2.0.1'
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: spluspy
3
- Version: 2.0.0
3
+ Version: 2.0.1
4
4
  Summary: Soroush Plus library for Python
5
5
  Home-page: https://github.com/Itskillmaster/spluspy
6
6
  Download-URL: https://github.com/Itskillmaster/spluspy/releases