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.
- {spluspy-2.0.0 → spluspy-2.0.1}/PKG-INFO +1 -1
- {spluspy-2.0.0 → spluspy-2.0.1}/spluspy/client/updates.py +16 -23
- {spluspy-2.0.0 → spluspy-2.0.1}/spluspy/crypto/rsa.py +10 -0
- {spluspy-2.0.0 → spluspy-2.0.1}/spluspy/network/connection/connection.py +19 -4
- spluspy-2.0.1/spluspy/network/connection/websocket.py +313 -0
- {spluspy-2.0.0 → spluspy-2.0.1}/spluspy/network/mtprotosender.py +27 -8
- {spluspy-2.0.0 → spluspy-2.0.1}/spluspy/version.py +1 -1
- {spluspy-2.0.0 → spluspy-2.0.1}/spluspy.egg-info/PKG-INFO +1 -1
- spluspy-2.0.0/spluspy/network/connection/websocket.py +0 -186
- {spluspy-2.0.0 → spluspy-2.0.1}/README.rst +0 -0
- {spluspy-2.0.0 → spluspy-2.0.1}/setup.cfg +0 -0
- {spluspy-2.0.0 → spluspy-2.0.1}/setup.py +0 -0
- {spluspy-2.0.0 → spluspy-2.0.1}/spluspy/__init__.py +0 -0
- {spluspy-2.0.0 → spluspy-2.0.1}/spluspy/_updates/__init__.py +0 -0
- {spluspy-2.0.0 → spluspy-2.0.1}/spluspy/_updates/entitycache.py +0 -0
- {spluspy-2.0.0 → spluspy-2.0.1}/spluspy/_updates/messagebox.py +0 -0
- {spluspy-2.0.0 → spluspy-2.0.1}/spluspy/_updates/session.py +0 -0
- {spluspy-2.0.0 → spluspy-2.0.1}/spluspy/client/__init__.py +0 -0
- {spluspy-2.0.0 → spluspy-2.0.1}/spluspy/client/account.py +0 -0
- {spluspy-2.0.0 → spluspy-2.0.1}/spluspy/client/auth.py +0 -0
- {spluspy-2.0.0 → spluspy-2.0.1}/spluspy/client/bots.py +0 -0
- {spluspy-2.0.0 → spluspy-2.0.1}/spluspy/client/buttons.py +0 -0
- {spluspy-2.0.0 → spluspy-2.0.1}/spluspy/client/chats.py +0 -0
- {spluspy-2.0.0 → spluspy-2.0.1}/spluspy/client/dialogs.py +0 -0
- {spluspy-2.0.0 → spluspy-2.0.1}/spluspy/client/downloads.py +0 -0
- {spluspy-2.0.0 → spluspy-2.0.1}/spluspy/client/messageparse.py +0 -0
- {spluspy-2.0.0 → spluspy-2.0.1}/spluspy/client/messages.py +0 -0
- {spluspy-2.0.0 → spluspy-2.0.1}/spluspy/client/ratelimiter.py +0 -0
- {spluspy-2.0.0 → spluspy-2.0.1}/spluspy/client/soroushclient.py +0 -0
- {spluspy-2.0.0 → spluspy-2.0.1}/spluspy/client/telegrambaseclient.py +0 -0
- {spluspy-2.0.0 → spluspy-2.0.1}/spluspy/client/uploads.py +0 -0
- {spluspy-2.0.0 → spluspy-2.0.1}/spluspy/client/users.py +0 -0
- {spluspy-2.0.0 → spluspy-2.0.1}/spluspy/crypto/__init__.py +0 -0
- {spluspy-2.0.0 → spluspy-2.0.1}/spluspy/crypto/aes.py +0 -0
- {spluspy-2.0.0 → spluspy-2.0.1}/spluspy/crypto/aesctr.py +0 -0
- {spluspy-2.0.0 → spluspy-2.0.1}/spluspy/crypto/authkey.py +0 -0
- {spluspy-2.0.0 → spluspy-2.0.1}/spluspy/crypto/cdndecrypter.py +0 -0
- {spluspy-2.0.0 → spluspy-2.0.1}/spluspy/crypto/factorization.py +0 -0
- {spluspy-2.0.0 → spluspy-2.0.1}/spluspy/crypto/libssl.py +0 -0
- {spluspy-2.0.0 → spluspy-2.0.1}/spluspy/custom.py +0 -0
- {spluspy-2.0.0 → spluspy-2.0.1}/spluspy/errors/__init__.py +0 -0
- {spluspy-2.0.0 → spluspy-2.0.1}/spluspy/errors/common.py +0 -0
- {spluspy-2.0.0 → spluspy-2.0.1}/spluspy/errors/rpcbaseerrors.py +0 -0
- {spluspy-2.0.0 → spluspy-2.0.1}/spluspy/errors/rpcerrorlist.py +0 -0
- {spluspy-2.0.0 → spluspy-2.0.1}/spluspy/events/__init__.py +0 -0
- {spluspy-2.0.0 → spluspy-2.0.1}/spluspy/events/album.py +0 -0
- {spluspy-2.0.0 → spluspy-2.0.1}/spluspy/events/callbackquery.py +0 -0
- {spluspy-2.0.0 → spluspy-2.0.1}/spluspy/events/chataction.py +0 -0
- {spluspy-2.0.0 → spluspy-2.0.1}/spluspy/events/common.py +0 -0
- {spluspy-2.0.0 → spluspy-2.0.1}/spluspy/events/decorators.py +0 -0
- {spluspy-2.0.0 → spluspy-2.0.1}/spluspy/events/inlinequery.py +0 -0
- {spluspy-2.0.0 → spluspy-2.0.1}/spluspy/events/messagedeleted.py +0 -0
- {spluspy-2.0.0 → spluspy-2.0.1}/spluspy/events/messageedited.py +0 -0
- {spluspy-2.0.0 → spluspy-2.0.1}/spluspy/events/messageread.py +0 -0
- {spluspy-2.0.0 → spluspy-2.0.1}/spluspy/events/newmessage.py +0 -0
- {spluspy-2.0.0 → spluspy-2.0.1}/spluspy/events/raw.py +0 -0
- {spluspy-2.0.0 → spluspy-2.0.1}/spluspy/events/userupdate.py +0 -0
- {spluspy-2.0.0 → spluspy-2.0.1}/spluspy/extensions/__init__.py +0 -0
- {spluspy-2.0.0 → spluspy-2.0.1}/spluspy/extensions/binaryreader.py +0 -0
- {spluspy-2.0.0 → spluspy-2.0.1}/spluspy/extensions/html.py +0 -0
- {spluspy-2.0.0 → spluspy-2.0.1}/spluspy/extensions/markdown.py +0 -0
- {spluspy-2.0.0 → spluspy-2.0.1}/spluspy/extensions/messagepacker.py +0 -0
- {spluspy-2.0.0 → spluspy-2.0.1}/spluspy/functions.py +0 -0
- {spluspy-2.0.0 → spluspy-2.0.1}/spluspy/helpers.py +0 -0
- {spluspy-2.0.0 → spluspy-2.0.1}/spluspy/hints.py +0 -0
- {spluspy-2.0.0 → spluspy-2.0.1}/spluspy/network/__init__.py +0 -0
- {spluspy-2.0.0 → spluspy-2.0.1}/spluspy/network/authenticator.py +0 -0
- {spluspy-2.0.0 → spluspy-2.0.1}/spluspy/network/connection/__init__.py +0 -0
- {spluspy-2.0.0 → spluspy-2.0.1}/spluspy/network/connection/http.py +0 -0
- {spluspy-2.0.0 → spluspy-2.0.1}/spluspy/network/connection/tcpabridged.py +0 -0
- {spluspy-2.0.0 → spluspy-2.0.1}/spluspy/network/connection/tcpfull.py +0 -0
- {spluspy-2.0.0 → spluspy-2.0.1}/spluspy/network/connection/tcpintermediate.py +0 -0
- {spluspy-2.0.0 → spluspy-2.0.1}/spluspy/network/connection/tcpmtproxy.py +0 -0
- {spluspy-2.0.0 → spluspy-2.0.1}/spluspy/network/connection/tcpobfuscated.py +0 -0
- {spluspy-2.0.0 → spluspy-2.0.1}/spluspy/network/mtprotoplainsender.py +0 -0
- {spluspy-2.0.0 → spluspy-2.0.1}/spluspy/network/mtprotostate.py +0 -0
- {spluspy-2.0.0 → spluspy-2.0.1}/spluspy/network/requeststate.py +0 -0
- {spluspy-2.0.0 → spluspy-2.0.1}/spluspy/password.py +0 -0
- {spluspy-2.0.0 → spluspy-2.0.1}/spluspy/requestiter.py +0 -0
- {spluspy-2.0.0 → spluspy-2.0.1}/spluspy/sessions/__init__.py +0 -0
- {spluspy-2.0.0 → spluspy-2.0.1}/spluspy/sessions/abstract.py +0 -0
- {spluspy-2.0.0 → spluspy-2.0.1}/spluspy/sessions/memory.py +0 -0
- {spluspy-2.0.0 → spluspy-2.0.1}/spluspy/sessions/sqlite.py +0 -0
- {spluspy-2.0.0 → spluspy-2.0.1}/spluspy/sessions/string.py +0 -0
- {spluspy-2.0.0 → spluspy-2.0.1}/spluspy/sync.py +0 -0
- {spluspy-2.0.0 → spluspy-2.0.1}/spluspy/tl/__init__.py +0 -0
- {spluspy-2.0.0 → spluspy-2.0.1}/spluspy/tl/alltlobjects.py +0 -0
- {spluspy-2.0.0 → spluspy-2.0.1}/spluspy/tl/core/__init__.py +0 -0
- {spluspy-2.0.0 → spluspy-2.0.1}/spluspy/tl/core/gzippacked.py +0 -0
- {spluspy-2.0.0 → spluspy-2.0.1}/spluspy/tl/core/messagecontainer.py +0 -0
- {spluspy-2.0.0 → spluspy-2.0.1}/spluspy/tl/core/rpcresult.py +0 -0
- {spluspy-2.0.0 → spluspy-2.0.1}/spluspy/tl/core/tlmessage.py +0 -0
- {spluspy-2.0.0 → spluspy-2.0.1}/spluspy/tl/custom/__init__.py +0 -0
- {spluspy-2.0.0 → spluspy-2.0.1}/spluspy/tl/custom/adminlogevent.py +0 -0
- {spluspy-2.0.0 → spluspy-2.0.1}/spluspy/tl/custom/button.py +0 -0
- {spluspy-2.0.0 → spluspy-2.0.1}/spluspy/tl/custom/chatgetter.py +0 -0
- {spluspy-2.0.0 → spluspy-2.0.1}/spluspy/tl/custom/conversation.py +0 -0
- {spluspy-2.0.0 → spluspy-2.0.1}/spluspy/tl/custom/dialog.py +0 -0
- {spluspy-2.0.0 → spluspy-2.0.1}/spluspy/tl/custom/draft.py +0 -0
- {spluspy-2.0.0 → spluspy-2.0.1}/spluspy/tl/custom/file.py +0 -0
- {spluspy-2.0.0 → spluspy-2.0.1}/spluspy/tl/custom/forward.py +0 -0
- {spluspy-2.0.0 → spluspy-2.0.1}/spluspy/tl/custom/inlinebuilder.py +0 -0
- {spluspy-2.0.0 → spluspy-2.0.1}/spluspy/tl/custom/inlineresult.py +0 -0
- {spluspy-2.0.0 → spluspy-2.0.1}/spluspy/tl/custom/inlineresults.py +0 -0
- {spluspy-2.0.0 → spluspy-2.0.1}/spluspy/tl/custom/inputsizedfile.py +0 -0
- {spluspy-2.0.0 → spluspy-2.0.1}/spluspy/tl/custom/message.py +0 -0
- {spluspy-2.0.0 → spluspy-2.0.1}/spluspy/tl/custom/messagebutton.py +0 -0
- {spluspy-2.0.0 → spluspy-2.0.1}/spluspy/tl/custom/participantpermissions.py +0 -0
- {spluspy-2.0.0 → spluspy-2.0.1}/spluspy/tl/custom/qrlogin.py +0 -0
- {spluspy-2.0.0 → spluspy-2.0.1}/spluspy/tl/custom/sendergetter.py +0 -0
- {spluspy-2.0.0 → spluspy-2.0.1}/spluspy/tl/custom/types.py +0 -0
- {spluspy-2.0.0 → spluspy-2.0.1}/spluspy/tl/functions/__init__.py +0 -0
- {spluspy-2.0.0 → spluspy-2.0.1}/spluspy/tl/functions/account.py +0 -0
- {spluspy-2.0.0 → spluspy-2.0.1}/spluspy/tl/functions/auth.py +0 -0
- {spluspy-2.0.0 → spluspy-2.0.1}/spluspy/tl/functions/bots.py +0 -0
- {spluspy-2.0.0 → spluspy-2.0.1}/spluspy/tl/functions/channels.py +0 -0
- {spluspy-2.0.0 → spluspy-2.0.1}/spluspy/tl/functions/chatlists.py +0 -0
- {spluspy-2.0.0 → spluspy-2.0.1}/spluspy/tl/functions/conference.py +0 -0
- {spluspy-2.0.0 → spluspy-2.0.1}/spluspy/tl/functions/contacts.py +0 -0
- {spluspy-2.0.0 → spluspy-2.0.1}/spluspy/tl/functions/folders.py +0 -0
- {spluspy-2.0.0 → spluspy-2.0.1}/spluspy/tl/functions/help.py +0 -0
- {spluspy-2.0.0 → spluspy-2.0.1}/spluspy/tl/functions/langpack.py +0 -0
- {spluspy-2.0.0 → spluspy-2.0.1}/spluspy/tl/functions/messages.py +0 -0
- {spluspy-2.0.0 → spluspy-2.0.1}/spluspy/tl/functions/payments.py +0 -0
- {spluspy-2.0.0 → spluspy-2.0.1}/spluspy/tl/functions/phone.py +0 -0
- {spluspy-2.0.0 → spluspy-2.0.1}/spluspy/tl/functions/photos.py +0 -0
- {spluspy-2.0.0 → spluspy-2.0.1}/spluspy/tl/functions/premium.py +0 -0
- {spluspy-2.0.0 → spluspy-2.0.1}/spluspy/tl/functions/stats.py +0 -0
- {spluspy-2.0.0 → spluspy-2.0.1}/spluspy/tl/functions/stories.py +0 -0
- {spluspy-2.0.0 → spluspy-2.0.1}/spluspy/tl/functions/thirdParty.py +0 -0
- {spluspy-2.0.0 → spluspy-2.0.1}/spluspy/tl/functions/updates.py +0 -0
- {spluspy-2.0.0 → spluspy-2.0.1}/spluspy/tl/functions/upload.py +0 -0
- {spluspy-2.0.0 → spluspy-2.0.1}/spluspy/tl/functions/users.py +0 -0
- {spluspy-2.0.0 → spluspy-2.0.1}/spluspy/tl/patched/__init__.py +0 -0
- {spluspy-2.0.0 → spluspy-2.0.1}/spluspy/tl/tlobject.py +0 -0
- {spluspy-2.0.0 → spluspy-2.0.1}/spluspy/tl/types/__init__.py +0 -0
- {spluspy-2.0.0 → spluspy-2.0.1}/spluspy/tl/types/account.py +0 -0
- {spluspy-2.0.0 → spluspy-2.0.1}/spluspy/tl/types/auth.py +0 -0
- {spluspy-2.0.0 → spluspy-2.0.1}/spluspy/tl/types/bots.py +0 -0
- {spluspy-2.0.0 → spluspy-2.0.1}/spluspy/tl/types/channels.py +0 -0
- {spluspy-2.0.0 → spluspy-2.0.1}/spluspy/tl/types/chatlists.py +0 -0
- {spluspy-2.0.0 → spluspy-2.0.1}/spluspy/tl/types/conference.py +0 -0
- {spluspy-2.0.0 → spluspy-2.0.1}/spluspy/tl/types/contacts.py +0 -0
- {spluspy-2.0.0 → spluspy-2.0.1}/spluspy/tl/types/help.py +0 -0
- {spluspy-2.0.0 → spluspy-2.0.1}/spluspy/tl/types/messages.py +0 -0
- {spluspy-2.0.0 → spluspy-2.0.1}/spluspy/tl/types/payments.py +0 -0
- {spluspy-2.0.0 → spluspy-2.0.1}/spluspy/tl/types/phone.py +0 -0
- {spluspy-2.0.0 → spluspy-2.0.1}/spluspy/tl/types/photos.py +0 -0
- {spluspy-2.0.0 → spluspy-2.0.1}/spluspy/tl/types/premium.py +0 -0
- {spluspy-2.0.0 → spluspy-2.0.1}/spluspy/tl/types/stats.py +0 -0
- {spluspy-2.0.0 → spluspy-2.0.1}/spluspy/tl/types/stickers.py +0 -0
- {spluspy-2.0.0 → spluspy-2.0.1}/spluspy/tl/types/storage.py +0 -0
- {spluspy-2.0.0 → spluspy-2.0.1}/spluspy/tl/types/stories.py +0 -0
- {spluspy-2.0.0 → spluspy-2.0.1}/spluspy/tl/types/thirdParty.py +0 -0
- {spluspy-2.0.0 → spluspy-2.0.1}/spluspy/tl/types/update.py +0 -0
- {spluspy-2.0.0 → spluspy-2.0.1}/spluspy/tl/types/updates.py +0 -0
- {spluspy-2.0.0 → spluspy-2.0.1}/spluspy/tl/types/upload.py +0 -0
- {spluspy-2.0.0 → spluspy-2.0.1}/spluspy/tl/types/users.py +0 -0
- {spluspy-2.0.0 → spluspy-2.0.1}/spluspy/types.py +0 -0
- {spluspy-2.0.0 → spluspy-2.0.1}/spluspy/updater.py +0 -0
- {spluspy-2.0.0 → spluspy-2.0.1}/spluspy/utils.py +0 -0
- {spluspy-2.0.0 → spluspy-2.0.1}/spluspy.egg-info/SOURCES.txt +0 -0
- {spluspy-2.0.0 → spluspy-2.0.1}/spluspy.egg-info/dependency_links.txt +0 -0
- {spluspy-2.0.0 → spluspy-2.0.1}/spluspy.egg-info/requires.txt +0 -0
- {spluspy-2.0.0 → spluspy-2.0.1}/spluspy.egg-info/top_level.txt +0 -0
|
@@ -477,27 +477,23 @@ class UpdateMethods:
|
|
|
477
477
|
updates_to_dispatch.extend(_preprocess_updates)
|
|
478
478
|
continue
|
|
479
479
|
|
|
480
|
-
# Drain
|
|
481
|
-
|
|
482
|
-
pending_updates = []
|
|
480
|
+
# Drain the update queue aggressively before sleeping
|
|
481
|
+
got_update = False
|
|
483
482
|
try:
|
|
484
|
-
|
|
485
|
-
|
|
483
|
+
updates = self._updates_queue.get_nowait()
|
|
484
|
+
got_update = True
|
|
486
485
|
except asyncio.QueueEmpty:
|
|
487
486
|
pass
|
|
488
487
|
|
|
489
|
-
if
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
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(
|
|
51
|
-
self._recv_queue = asyncio.Queue(
|
|
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='
|
|
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
|
-
|
|
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(
|
|
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
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
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
|
|