From 382ffbe0ca0ffd81ca232098f2eb76b37f3f7130 Mon Sep 17 00:00:00 2001 From: VD171 Contributor Date: Sun, 8 Nov 2026 02:21:30 +0100 Subject: [PATCH 01/12] feat(telegram): add Telegram Cloud Storage backup and restore support - MTProto authentication via official client API ID (no bot tokens or custom secrets) - Phone number verification + Telegram OTP (+ 2FA password support) - Automatic private backup channel creation (VD_Backup_) with safety warning - Telegram Cloud upload, listing, download and restore support - Fully vendored pure-Python MTProto client stack (telethon, pyaes, rsa, pyasn1) - Modern WebUI modal for linking and browsing Telegram cloud backups --- README.md | 5 +- docs/USER-GUIDE.md | 11 + module/customize.sh | 4 +- module/scripts/helper.sh | 47 +- module/scripts/lib/pyaes/__init__.py | 53 + .../__pycache__/__init__.cpython-314.pyc | Bin 0 -> 687 bytes .../lib/pyaes/__pycache__/aes.cpython-314.pyc | Bin 0 -> 110140 bytes .../__pycache__/blockfeeder.cpython-314.pyc | Bin 0 -> 8209 bytes .../pyaes/__pycache__/util.cpython-314.pyc | Bin 0 -> 1723 bytes module/scripts/lib/pyaes/aes.py | 589 + module/scripts/lib/pyaes/blockfeeder.py | 227 + module/scripts/lib/pyaes/util.py | 60 + module/scripts/lib/pyasn1/__init__.py | 2 + .../__pycache__/__init__.cpython-314.pyc | Bin 0 -> 231 bytes .../pyasn1/__pycache__/debug.cpython-314.pyc | Bin 0 -> 6647 bytes .../pyasn1/__pycache__/error.cpython-314.pyc | Bin 0 -> 4688 bytes module/scripts/lib/pyasn1/codec/__init__.py | 1 + .../__pycache__/__init__.cpython-314.pyc | Bin 0 -> 211 bytes .../__pycache__/streaming.cpython-314.pyc | Bin 0 -> 8440 bytes .../scripts/lib/pyasn1/codec/ber/__init__.py | 1 + .../ber/__pycache__/__init__.cpython-314.pyc | Bin 0 -> 215 bytes .../ber/__pycache__/decoder.cpython-314.pyc | Bin 0 -> 83025 bytes .../ber/__pycache__/encoder.cpython-314.pyc | Bin 0 -> 35433 bytes .../codec/ber/__pycache__/eoo.cpython-314.pyc | Bin 0 -> 1241 bytes .../scripts/lib/pyasn1/codec/ber/decoder.py | 2262 + .../scripts/lib/pyasn1/codec/ber/encoder.py | 954 + module/scripts/lib/pyasn1/codec/ber/eoo.py | 28 + .../scripts/lib/pyasn1/codec/cer/__init__.py | 1 + .../cer/__pycache__/__init__.cpython-314.pyc | Bin 0 -> 215 bytes .../cer/__pycache__/decoder.cpython-314.pyc | Bin 0 -> 4544 bytes .../cer/__pycache__/encoder.cpython-314.pyc | Bin 0 -> 12522 bytes .../scripts/lib/pyasn1/codec/cer/decoder.py | 149 + .../scripts/lib/pyasn1/codec/cer/encoder.py | 331 + .../scripts/lib/pyasn1/codec/der/__init__.py | 1 + .../der/__pycache__/__init__.cpython-314.pyc | Bin 0 -> 215 bytes .../der/__pycache__/decoder.cpython-314.pyc | Bin 0 -> 3430 bytes .../der/__pycache__/encoder.cpython-314.pyc | Bin 0 -> 3652 bytes .../scripts/lib/pyasn1/codec/der/decoder.py | 120 + .../scripts/lib/pyasn1/codec/der/encoder.py | 126 + .../lib/pyasn1/codec/native/__init__.py | 1 + .../__pycache__/__init__.cpython-314.pyc | Bin 0 -> 190 bytes .../__pycache__/decoder.cpython-314.pyc | Bin 0 -> 12452 bytes .../__pycache__/encoder.cpython-314.pyc | Bin 0 -> 15277 bytes .../lib/pyasn1/codec/native/decoder.py | 244 + .../lib/pyasn1/codec/native/encoder.py | 285 + module/scripts/lib/pyasn1/codec/streaming.py | 234 + module/scripts/lib/pyasn1/compat/__init__.py | 4 + .../__pycache__/__init__.cpython-314.pyc | Bin 0 -> 250 bytes .../__pycache__/integer.cpython-314.pyc | Bin 0 -> 707 bytes module/scripts/lib/pyasn1/compat/integer.py | 13 + module/scripts/lib/pyasn1/debug.py | 146 + module/scripts/lib/pyasn1/error.py | 116 + module/scripts/lib/pyasn1/type/__init__.py | 1 + .../type/__pycache__/__init__.cpython-314.pyc | Bin 0 -> 210 bytes .../type/__pycache__/base.cpython-314.pyc | Bin 0 -> 27610 bytes .../type/__pycache__/char.cpython-314.pyc | Bin 0 -> 9959 bytes .../__pycache__/constraint.cpython-314.pyc | Bin 0 -> 28388 bytes .../type/__pycache__/error.cpython-314.pyc | Bin 0 -> 504 bytes .../__pycache__/namedtype.cpython-314.pyc | Bin 0 -> 23811 bytes .../type/__pycache__/namedval.cpython-314.pyc | Bin 0 -> 8005 bytes .../type/__pycache__/opentype.cpython-314.pyc | Bin 0 -> 3834 bytes .../type/__pycache__/tag.cpython-314.pyc | Bin 0 -> 13310 bytes .../type/__pycache__/tagmap.cpython-314.pyc | Bin 0 -> 4667 bytes .../type/__pycache__/univ.cpython-314.pyc | Bin 0 -> 132977 bytes .../type/__pycache__/useful.cpython-314.pyc | Bin 0 -> 8058 bytes module/scripts/lib/pyasn1/type/base.py | 699 + module/scripts/lib/pyasn1/type/char.py | 288 + module/scripts/lib/pyasn1/type/constraint.py | 751 + module/scripts/lib/pyasn1/type/error.py | 11 + module/scripts/lib/pyasn1/type/namedtype.py | 550 + module/scripts/lib/pyasn1/type/namedval.py | 192 + module/scripts/lib/pyasn1/type/opentype.py | 104 + module/scripts/lib/pyasn1/type/tag.py | 347 + module/scripts/lib/pyasn1/type/tagmap.py | 96 + module/scripts/lib/pyasn1/type/univ.py | 3338 + module/scripts/lib/pyasn1/type/useful.py | 190 + module/scripts/lib/rsa/__init__.py | 60 + .../rsa/__pycache__/__init__.cpython-314.pyc | Bin 0 -> 1188 bytes .../lib/rsa/__pycache__/asn1.cpython-314.pyc | Bin 0 -> 2279 bytes .../lib/rsa/__pycache__/cli.cpython-314.pyc | Bin 0 -> 17314 bytes .../rsa/__pycache__/common.cpython-314.pyc | Bin 0 -> 6411 bytes .../lib/rsa/__pycache__/core.cpython-314.pyc | Bin 0 -> 2263 bytes .../lib/rsa/__pycache__/key.cpython-314.pyc | Bin 0 -> 41767 bytes .../rsa/__pycache__/parallel.cpython-314.pyc | Bin 0 -> 2987 bytes .../lib/rsa/__pycache__/pem.cpython-314.pyc | Bin 0 -> 4776 bytes .../lib/rsa/__pycache__/pkcs1.cpython-314.pyc | Bin 0 -> 18695 bytes .../rsa/__pycache__/pkcs1_v2.cpython-314.pyc | Bin 0 -> 3726 bytes .../lib/rsa/__pycache__/prime.cpython-314.pyc | Bin 0 -> 5680 bytes .../rsa/__pycache__/randnum.cpython-314.pyc | Bin 0 -> 2945 bytes .../rsa/__pycache__/transform.cpython-314.pyc | Bin 0 -> 2499 bytes .../lib/rsa/__pycache__/util.cpython-314.pyc | Bin 0 -> 3777 bytes module/scripts/lib/rsa/asn1.py | 52 + module/scripts/lib/rsa/cli.py | 321 + module/scripts/lib/rsa/common.py | 184 + module/scripts/lib/rsa/core.py | 53 + module/scripts/lib/rsa/key.py | 858 + module/scripts/lib/rsa/parallel.py | 96 + module/scripts/lib/rsa/pem.py | 134 + module/scripts/lib/rsa/pkcs1.py | 485 + module/scripts/lib/rsa/pkcs1_v2.py | 100 + module/scripts/lib/rsa/prime.py | 198 + module/scripts/lib/rsa/py.typed | 1 + module/scripts/lib/rsa/randnum.py | 95 + module/scripts/lib/rsa/transform.py | 72 + module/scripts/lib/rsa/util.py | 97 + module/scripts/lib/telethon/__init__.py | 13 + .../__pycache__/__init__.cpython-314.pyc | Bin 0 -> 653 bytes .../__pycache__/custom.cpython-314.pyc | Bin 0 -> 236 bytes .../__pycache__/functions.cpython-314.pyc | Bin 0 -> 242 bytes .../__pycache__/helpers.cpython-314.pyc | Bin 0 -> 19857 bytes .../__pycache__/hints.cpython-314.pyc | Bin 0 -> 3156 bytes .../__pycache__/password.cpython-314.pyc | Bin 0 -> 12385 bytes .../__pycache__/requestiter.cpython-314.pyc | Bin 0 -> 6505 bytes .../telethon/__pycache__/sync.cpython-314.pyc | Bin 0 -> 3072 bytes .../__pycache__/types.cpython-314.pyc | Bin 0 -> 234 bytes .../__pycache__/utils.cpython-314.pyc | Bin 0 -> 70777 bytes .../__pycache__/version.cpython-314.pyc | Bin 0 -> 235 bytes .../scripts/lib/telethon/_updates/__init__.py | 3 + .../__pycache__/__init__.cpython-314.pyc | Bin 0 -> 472 bytes .../__pycache__/entitycache.cpython-314.pyc | Bin 0 -> 4393 bytes .../__pycache__/messagebox.cpython-314.pyc | Bin 0 -> 35504 bytes .../__pycache__/session.cpython-314.pyc | Bin 0 -> 10463 bytes .../lib/telethon/_updates/entitycache.py | 62 + .../lib/telethon/_updates/messagebox.py | 825 + .../scripts/lib/telethon/_updates/session.py | 198 + .../scripts/lib/telethon/client/__init__.py | 25 + .../__pycache__/__init__.cpython-314.pyc | Bin 0 -> 1407 bytes .../__pycache__/account.cpython-314.pyc | Bin 0 -> 11778 bytes .../client/__pycache__/auth.cpython-314.pyc | Bin 0 -> 30694 bytes .../client/__pycache__/bots.cpython-314.pyc | Bin 0 -> 3399 bytes .../__pycache__/buttons.cpython-314.pyc | Bin 0 -> 4257 bytes .../client/__pycache__/chats.cpython-314.pyc | Bin 0 -> 61573 bytes .../__pycache__/dialogs.cpython-314.pyc | Bin 0 -> 27645 bytes .../__pycache__/downloads.cpython-314.pyc | Bin 0 -> 51769 bytes .../__pycache__/messageparse.cpython-314.pyc | Bin 0 -> 11185 bytes .../__pycache__/messages.cpython-314.pyc | Bin 0 -> 66286 bytes .../telegrambaseclient.cpython-314.pyc | Bin 0 -> 51402 bytes .../telegramclient.cpython-314.pyc | Bin 0 -> 864 bytes .../__pycache__/updates.cpython-314.pyc | Bin 0 -> 35661 bytes .../__pycache__/uploads.cpython-314.pyc | Bin 0 -> 38271 bytes .../client/__pycache__/users.cpython-314.pyc | Bin 0 -> 34135 bytes module/scripts/lib/telethon/client/account.py | 243 + module/scripts/lib/telethon/client/auth.py | 677 + module/scripts/lib/telethon/client/bots.py | 72 + module/scripts/lib/telethon/client/buttons.py | 104 + module/scripts/lib/telethon/client/chats.py | 1336 + module/scripts/lib/telethon/client/dialogs.py | 610 + .../scripts/lib/telethon/client/downloads.py | 1092 + .../lib/telethon/client/messageparse.py | 233 + .../scripts/lib/telethon/client/messages.py | 1526 + .../lib/telethon/client/telegrambaseclient.py | 979 + .../lib/telethon/client/telegramclient.py | 13 + module/scripts/lib/telethon/client/updates.py | 733 + module/scripts/lib/telethon/client/uploads.py | 870 + module/scripts/lib/telethon/client/users.py | 620 + .../scripts/lib/telethon/crypto/__init__.py | 10 + .../__pycache__/__init__.cpython-314.pyc | Bin 0 -> 630 bytes .../crypto/__pycache__/aes.cpython-314.pyc | Bin 0 -> 4634 bytes .../crypto/__pycache__/aesctr.cpython-314.pyc | Bin 0 -> 2024 bytes .../__pycache__/authkey.cpython-314.pyc | Bin 0 -> 3420 bytes .../__pycache__/cdndecrypter.cpython-314.pyc | Bin 0 -> 4788 bytes .../__pycache__/factorization.cpython-314.pyc | Bin 0 -> 2601 bytes .../crypto/__pycache__/libssl.cpython-314.pyc | Bin 0 -> 6399 bytes .../crypto/__pycache__/rsa.cpython-314.pyc | Bin 0 -> 6954 bytes module/scripts/lib/telethon/crypto/aes.py | 111 + module/scripts/lib/telethon/crypto/aesctr.py | 42 + module/scripts/lib/telethon/crypto/authkey.py | 63 + .../lib/telethon/crypto/cdndecrypter.py | 105 + .../lib/telethon/crypto/factorization.py | 67 + module/scripts/lib/telethon/crypto/libssl.py | 140 + module/scripts/lib/telethon/crypto/rsa.py | 165 + module/scripts/lib/telethon/custom.py | 1 + .../scripts/lib/telethon/errors/__init__.py | 46 + .../__pycache__/__init__.cpython-314.pyc | Bin 0 -> 2007 bytes .../errors/__pycache__/common.cpython-314.pyc | Bin 0 -> 9663 bytes .../__pycache__/rpcbaseerrors.cpython-314.pyc | Bin 0 -> 5763 bytes .../__pycache__/rpcerrorlist.cpython-314.pyc | Bin 0 -> 385563 bytes module/scripts/lib/telethon/errors/common.py | 180 + .../lib/telethon/errors/rpcbaseerrors.py | 131 + .../lib/telethon/errors/rpcerrorlist.py | 5356 ++ .../scripts/lib/telethon/events/__init__.py | 140 + .../__pycache__/__init__.cpython-314.pyc | Bin 0 -> 5120 bytes .../events/__pycache__/album.cpython-314.pyc | Bin 0 -> 16409 bytes .../__pycache__/callbackquery.cpython-314.pyc | Bin 0 -> 18539 bytes .../__pycache__/chataction.cpython-314.pyc | Bin 0 -> 22291 bytes .../events/__pycache__/common.cpython-314.pyc | Bin 0 -> 9357 bytes .../__pycache__/inlinequery.cpython-314.pyc | Bin 0 -> 10903 bytes .../messagedeleted.cpython-314.pyc | Bin 0 -> 3213 bytes .../__pycache__/messageedited.cpython-314.pyc | Bin 0 -> 2666 bytes .../__pycache__/messageread.cpython-314.pyc | Bin 0 -> 7816 bytes .../__pycache__/newmessage.cpython-314.pyc | Bin 0 -> 12509 bytes .../events/__pycache__/raw.cpython-314.pyc | Bin 0 -> 3008 bytes .../__pycache__/userupdate.cpython-314.pyc | Bin 0 -> 15197 bytes module/scripts/lib/telethon/events/album.py | 343 + .../lib/telethon/events/callbackquery.py | 346 + .../scripts/lib/telethon/events/chataction.py | 458 + module/scripts/lib/telethon/events/common.py | 186 + .../lib/telethon/events/inlinequery.py | 247 + .../lib/telethon/events/messagedeleted.py | 57 + .../lib/telethon/events/messageedited.py | 52 + .../lib/telethon/events/messageread.py | 143 + .../scripts/lib/telethon/events/newmessage.py | 223 + module/scripts/lib/telethon/events/raw.py | 53 + .../scripts/lib/telethon/events/userupdate.py | 310 + .../lib/telethon/extensions/__init__.py | 6 + .../__pycache__/__init__.cpython-314.pyc | Bin 0 -> 521 bytes .../__pycache__/binaryreader.cpython-314.pyc | Bin 0 -> 9231 bytes .../__pycache__/html.cpython-314.pyc | Bin 0 -> 10655 bytes .../__pycache__/markdown.cpython-314.pyc | Bin 0 -> 7715 bytes .../__pycache__/messagepacker.cpython-314.pyc | Bin 0 -> 5349 bytes .../lib/telethon/extensions/binaryreader.py | 201 + .../scripts/lib/telethon/extensions/html.py | 208 + .../lib/telethon/extensions/markdown.py | 193 + .../lib/telethon/extensions/messagepacker.py | 111 + module/scripts/lib/telethon/functions.py | 1 + module/scripts/lib/telethon/helpers.py | 436 + module/scripts/lib/telethon/hints.py | 72 + .../scripts/lib/telethon/network/__init__.py | 14 + .../__pycache__/__init__.cpython-314.pyc | Bin 0 -> 930 bytes .../__pycache__/authenticator.cpython-314.pyc | Bin 0 -> 9735 bytes .../mtprotoplainsender.cpython-314.pyc | Bin 0 -> 2719 bytes .../__pycache__/mtprotosender.cpython-314.pyc | Bin 0 -> 50377 bytes .../__pycache__/mtprotostate.cpython-314.pyc | Bin 0 -> 13385 bytes .../__pycache__/requeststate.cpython-314.pyc | Bin 0 -> 1282 bytes .../lib/telethon/network/authenticator.py | 212 + .../telethon/network/connection/__init__.py | 12 + .../__pycache__/__init__.cpython-314.pyc | Bin 0 -> 744 bytes .../__pycache__/connection.cpython-314.pyc | Bin 0 -> 20211 bytes .../__pycache__/http.cpython-314.pyc | Bin 0 -> 2703 bytes .../__pycache__/tcpabridged.cpython-314.pyc | Bin 0 -> 2214 bytes .../__pycache__/tcpfull.cpython-314.pyc | Bin 0 -> 3175 bytes .../tcpintermediate.cpython-314.pyc | Bin 0 -> 3041 bytes .../__pycache__/tcpmtproxy.cpython-314.pyc | Bin 0 -> 8471 bytes .../__pycache__/tcpobfuscated.cpython-314.pyc | Bin 0 -> 3437 bytes .../telethon/network/connection/connection.py | 440 + .../lib/telethon/network/connection/http.py | 39 + .../network/connection/tcpabridged.py | 33 + .../telethon/network/connection/tcpfull.py | 62 + .../network/connection/tcpintermediate.py | 46 + .../telethon/network/connection/tcpmtproxy.py | 165 + .../network/connection/tcpobfuscated.py | 62 + .../telethon/network/mtprotoplainsender.py | 56 + .../lib/telethon/network/mtprotosender.py | 942 + .../lib/telethon/network/mtprotostate.py | 285 + .../lib/telethon/network/requeststate.py | 19 + module/scripts/lib/telethon/password.py | 194 + module/scripts/lib/telethon/requestiter.py | 134 + .../scripts/lib/telethon/sessions/__init__.py | 4 + .../__pycache__/__init__.cpython-314.pyc | Bin 0 -> 394 bytes .../__pycache__/abstract.cpython-314.pyc | Bin 0 -> 6523 bytes .../__pycache__/memory.cpython-314.pyc | Bin 0 -> 13995 bytes .../__pycache__/sqlite.cpython-314.pyc | Bin 0 -> 19014 bytes .../__pycache__/string.cpython-314.pyc | Bin 0 -> 4331 bytes .../scripts/lib/telethon/sessions/abstract.py | 172 + .../scripts/lib/telethon/sessions/memory.py | 260 + .../scripts/lib/telethon/sessions/sqlite.py | 385 + .../scripts/lib/telethon/sessions/string.py | 63 + module/scripts/lib/telethon/sync.py | 74 + module/scripts/lib/telethon/tl/__init__.py | 1 + .../tl/__pycache__/__init__.cpython-314.pyc | Bin 0 -> 277 bytes .../__pycache__/alltlobjects.cpython-314.pyc | Bin 0 -> 237208 bytes .../tl/__pycache__/tlobject.cpython-314.pyc | Bin 0 -> 11263 bytes .../scripts/lib/telethon/tl/alltlobjects.py | 2531 + .../scripts/lib/telethon/tl/core/__init__.py | 26 + .../core/__pycache__/__init__.cpython-314.pyc | Bin 0 -> 1449 bytes .../__pycache__/gzippacked.cpython-314.pyc | Bin 0 -> 2868 bytes .../messagecontainer.cpython-314.pyc | Bin 0 -> 2036 bytes .../__pycache__/rpcresult.cpython-314.pyc | Bin 0 -> 1941 bytes .../__pycache__/tlmessage.cpython-314.pyc | Bin 0 -> 1688 bytes .../lib/telethon/tl/core/gzippacked.py | 48 + .../lib/telethon/tl/core/messagecontainer.py | 47 + .../scripts/lib/telethon/tl/core/rpcresult.py | 35 + .../scripts/lib/telethon/tl/core/tlmessage.py | 34 + .../lib/telethon/tl/custom/__init__.py | 14 + .../__pycache__/__init__.cpython-314.pyc | Bin 0 -> 883 bytes .../__pycache__/adminlogevent.cpython-314.pyc | Bin 0 -> 21642 bytes .../custom/__pycache__/button.cpython-314.pyc | Bin 0 -> 15868 bytes .../__pycache__/chatgetter.cpython-314.pyc | Bin 0 -> 7610 bytes .../__pycache__/conversation.cpython-314.pyc | Bin 0 -> 23973 bytes .../custom/__pycache__/dialog.cpython-314.pyc | Bin 0 -> 8164 bytes .../custom/__pycache__/draft.cpython-314.pyc | Bin 0 -> 8642 bytes .../custom/__pycache__/file.cpython-314.pyc | Bin 0 -> 7437 bytes .../__pycache__/forward.cpython-314.pyc | Bin 0 -> 2815 bytes .../__pycache__/inlinebuilder.cpython-314.pyc | Bin 0 -> 17486 bytes .../__pycache__/inlineresult.cpython-314.pyc | Bin 0 -> 8125 bytes .../__pycache__/inlineresults.cpython-314.pyc | Bin 0 -> 4548 bytes .../inputsizedfile.cpython-314.pyc | Bin 0 -> 1037 bytes .../__pycache__/message.cpython-314.pyc | Bin 0 -> 57555 bytes .../__pycache__/messagebutton.cpython-314.pyc | Bin 0 -> 9809 bytes .../participantpermissions.cpython-314.pyc | Bin 0 -> 6249 bytes .../__pycache__/qrlogin.cpython-314.pyc | Bin 0 -> 7209 bytes .../__pycache__/sendergetter.cpython-314.pyc | Bin 0 -> 5060 bytes .../lib/telethon/tl/custom/adminlogevent.py | 475 + .../scripts/lib/telethon/tl/custom/button.py | 347 + .../lib/telethon/tl/custom/chatgetter.py | 150 + .../lib/telethon/tl/custom/conversation.py | 529 + .../scripts/lib/telethon/tl/custom/dialog.py | 172 + .../scripts/lib/telethon/tl/custom/draft.py | 191 + module/scripts/lib/telethon/tl/custom/file.py | 146 + .../scripts/lib/telethon/tl/custom/forward.py | 51 + .../lib/telethon/tl/custom/inlinebuilder.py | 450 + .../lib/telethon/tl/custom/inlineresult.py | 176 + .../lib/telethon/tl/custom/inlineresults.py | 83 + .../lib/telethon/tl/custom/inputsizedfile.py | 9 + .../scripts/lib/telethon/tl/custom/message.py | 1248 + .../lib/telethon/tl/custom/messagebutton.py | 154 + .../tl/custom/participantpermissions.py | 138 + .../scripts/lib/telethon/tl/custom/qrlogin.py | 119 + .../lib/telethon/tl/custom/sendergetter.py | 102 + .../lib/telethon/tl/functions/__init__.py | 717 + .../__pycache__/__init__.cpython-314.pyc | Bin 0 -> 38749 bytes .../__pycache__/account.cpython-314.pyc | Bin 0 -> 214450 bytes .../__pycache__/aicompose.cpython-314.pyc | Bin 0 -> 14101 bytes .../__pycache__/auth.cpython-314.pyc | Bin 0 -> 52891 bytes .../__pycache__/bots.cpython-314.pyc | Bin 0 -> 83604 bytes .../__pycache__/channels.cpython-314.pyc | Bin 0 -> 133225 bytes .../__pycache__/chatlists.cpython-314.pyc | Bin 0 -> 25224 bytes .../__pycache__/communities.cpython-314.pyc | Bin 0 -> 22793 bytes .../__pycache__/contacts.cpython-314.pyc | Bin 0 -> 54495 bytes .../__pycache__/ephemeral.cpython-314.pyc | Bin 0 -> 33530 bytes .../__pycache__/folders.cpython-314.pyc | Bin 0 -> 3196 bytes .../__pycache__/fragment.cpython-314.pyc | Bin 0 -> 2392 bytes .../__pycache__/help.cpython-314.pyc | Bin 0 -> 32161 bytes .../__pycache__/langpack.cpython-314.pyc | Bin 0 -> 9115 bytes .../__pycache__/messages.cpython-314.pyc | Bin 0 -> 698744 bytes .../__pycache__/payments.cpython-314.pyc | Bin 0 -> 147721 bytes .../__pycache__/phone.cpython-314.pyc | Bin 0 -> 112066 bytes .../__pycache__/photos.cpython-314.pyc | Bin 0 -> 20210 bytes .../__pycache__/premium.cpython-314.pyc | Bin 0 -> 11625 bytes .../__pycache__/smsjobs.cpython-314.pyc | Bin 0 -> 8128 bytes .../__pycache__/stats.cpython-314.pyc | Bin 0 -> 19174 bytes .../__pycache__/stickers.cpython-314.pyc | Bin 0 -> 27021 bytes .../__pycache__/stories.cpython-314.pyc | Bin 0 -> 106152 bytes .../__pycache__/updates.cpython-314.pyc | Bin 0 -> 8675 bytes .../__pycache__/upload.cpython-314.pyc | Bin 0 -> 15930 bytes .../__pycache__/users.cpython-314.pyc | Bin 0 -> 18128 bytes .../lib/telethon/tl/functions/account.py | 4034 ++ .../lib/telethon/tl/functions/aicompose.py | 260 + .../scripts/lib/telethon/tl/functions/auth.py | 1014 + .../scripts/lib/telethon/tl/functions/bots.py | 1421 + .../lib/telethon/tl/functions/channels.py | 2250 + .../lib/telethon/tl/functions/chatlists.py | 425 + .../lib/telethon/tl/functions/communities.py | 359 + .../lib/telethon/tl/functions/contacts.py | 966 + .../lib/telethon/tl/functions/ephemeral.py | 456 + .../lib/telethon/tl/functions/folders.py | 44 + .../lib/telethon/tl/functions/fragment.py | 39 + .../scripts/lib/telethon/tl/functions/help.py | 672 + .../lib/telethon/tl/functions/langpack.py | 178 + .../lib/telethon/tl/functions/messages.py | 11202 ++++ .../lib/telethon/tl/functions/payments.py | 2437 + .../lib/telethon/tl/functions/phone.py | 1794 + .../lib/telethon/tl/functions/photos.py | 282 + .../lib/telethon/tl/functions/premium.py | 188 + .../lib/telethon/tl/functions/smsjobs.py | 180 + .../lib/telethon/tl/functions/stats.py | 332 + .../lib/telethon/tl/functions/stickers.py | 440 + .../lib/telethon/tl/functions/stories.py | 1574 + .../lib/telethon/tl/functions/updates.py | 139 + .../lib/telethon/tl/functions/upload.py | 300 + .../lib/telethon/tl/functions/users.py | 285 + .../lib/telethon/tl/patched/__init__.py | 20 + .../__pycache__/__init__.cpython-314.pyc | Bin 0 -> 1273 bytes module/scripts/lib/telethon/tl/tlobject.py | 222 + .../scripts/lib/telethon/tl/types/__init__.py | 53528 ++++++++++++++++ .../__pycache__/__init__.cpython-314.pyc | Bin 0 -> 3200348 bytes .../types/__pycache__/account.cpython-314.pyc | Bin 0 -> 97690 bytes .../__pycache__/aicompose.cpython-314.pyc | Bin 0 -> 4862 bytes .../tl/types/__pycache__/auth.cpython-314.pyc | Bin 0 -> 51868 bytes .../tl/types/__pycache__/bots.cpython-314.pyc | Bin 0 -> 13966 bytes .../__pycache__/channels.cpython-314.pyc | Bin 0 -> 21618 bytes .../__pycache__/chatlists.cpython-314.pyc | Bin 0 -> 21484 bytes .../__pycache__/communities.cpython-314.pyc | Bin 0 -> 10766 bytes .../__pycache__/contacts.cpython-314.pyc | Bin 0 -> 39103 bytes .../__pycache__/ephemeral.cpython-314.pyc | Bin 0 -> 4065 bytes .../__pycache__/fragment.cpython-314.pyc | Bin 0 -> 3360 bytes .../tl/types/__pycache__/help.cpython-314.pyc | Bin 0 -> 78853 bytes .../__pycache__/messages.cpython-314.pyc | Bin 0 -> 257210 bytes .../__pycache__/payments.cpython-314.pyc | Bin 0 -> 136979 bytes .../types/__pycache__/phone.cpython-314.pyc | Bin 0 -> 26291 bytes .../types/__pycache__/photos.cpython-314.pyc | Bin 0 -> 9381 bytes .../types/__pycache__/premium.cpython-314.pyc | Bin 0 -> 16916 bytes .../types/__pycache__/smsjobs.cpython-314.pyc | Bin 0 -> 5963 bytes .../types/__pycache__/stats.cpython-314.pyc | Bin 0 -> 33690 bytes .../__pycache__/stickers.cpython-314.pyc | Bin 0 -> 2071 bytes .../types/__pycache__/storage.cpython-314.pyc | Bin 0 -> 6585 bytes .../types/__pycache__/stories.cpython-314.pyc | Bin 0 -> 39956 bytes .../types/__pycache__/updates.cpython-314.pyc | Bin 0 -> 32692 bytes .../types/__pycache__/upload.cpython-314.pyc | Bin 0 -> 10859 bytes .../types/__pycache__/users.cpython-314.pyc | Bin 0 -> 12144 bytes .../scripts/lib/telethon/tl/types/account.py | 1501 + .../lib/telethon/tl/types/aicompose.py | 75 + module/scripts/lib/telethon/tl/types/auth.py | 972 + module/scripts/lib/telethon/tl/types/bots.py | 229 + .../scripts/lib/telethon/tl/types/channels.py | 307 + .../lib/telethon/tl/types/chatlists.py | 276 + .../lib/telethon/tl/types/communities.py | 135 + .../scripts/lib/telethon/tl/types/contacts.py | 548 + .../lib/telethon/tl/types/ephemeral.py | 66 + .../scripts/lib/telethon/tl/types/fragment.py | 55 + module/scripts/lib/telethon/tl/types/help.py | 1248 + .../scripts/lib/telethon/tl/types/messages.py | 3946 ++ .../scripts/lib/telethon/tl/types/payments.py | 1896 + module/scripts/lib/telethon/tl/types/phone.py | 368 + .../scripts/lib/telethon/tl/types/photos.py | 135 + .../scripts/lib/telethon/tl/types/premium.py | 209 + .../scripts/lib/telethon/tl/types/smsjobs.py | 100 + module/scripts/lib/telethon/tl/types/stats.py | 396 + .../scripts/lib/telethon/tl/types/stickers.py | 35 + .../scripts/lib/telethon/tl/types/storage.py | 197 + .../scripts/lib/telethon/tl/types/stories.py | 563 + .../scripts/lib/telethon/tl/types/updates.py | 447 + .../scripts/lib/telethon/tl/types/upload.py | 196 + module/scripts/lib/telethon/tl/types/users.py | 191 + module/scripts/lib/telethon/types.py | 1 + module/scripts/lib/telethon/utils.py | 1584 + module/scripts/lib/telethon/version.py | 3 + module/scripts/telegram.sh | 84 + module/scripts/telegram_cli.py | 722 + module/scripts/worker.sh | 56 +- module/webroot/index.html | 105 + module/webroot/lang/en.js | 31 +- module/webroot/lang/en.json | 31 +- module/webroot/scripts.js | 315 +- 424 files changed, 149534 insertions(+), 21 deletions(-) mode change 100644 => 100755 module/scripts/helper.sh create mode 100644 module/scripts/lib/pyaes/__init__.py create mode 100644 module/scripts/lib/pyaes/__pycache__/__init__.cpython-314.pyc create mode 100644 module/scripts/lib/pyaes/__pycache__/aes.cpython-314.pyc create mode 100644 module/scripts/lib/pyaes/__pycache__/blockfeeder.cpython-314.pyc create mode 100644 module/scripts/lib/pyaes/__pycache__/util.cpython-314.pyc create mode 100644 module/scripts/lib/pyaes/aes.py create mode 100644 module/scripts/lib/pyaes/blockfeeder.py create mode 100644 module/scripts/lib/pyaes/util.py create mode 100644 module/scripts/lib/pyasn1/__init__.py create mode 100644 module/scripts/lib/pyasn1/__pycache__/__init__.cpython-314.pyc create mode 100644 module/scripts/lib/pyasn1/__pycache__/debug.cpython-314.pyc create mode 100644 module/scripts/lib/pyasn1/__pycache__/error.cpython-314.pyc create mode 100644 module/scripts/lib/pyasn1/codec/__init__.py create mode 100644 module/scripts/lib/pyasn1/codec/__pycache__/__init__.cpython-314.pyc create mode 100644 module/scripts/lib/pyasn1/codec/__pycache__/streaming.cpython-314.pyc create mode 100644 module/scripts/lib/pyasn1/codec/ber/__init__.py create mode 100644 module/scripts/lib/pyasn1/codec/ber/__pycache__/__init__.cpython-314.pyc create mode 100644 module/scripts/lib/pyasn1/codec/ber/__pycache__/decoder.cpython-314.pyc create mode 100644 module/scripts/lib/pyasn1/codec/ber/__pycache__/encoder.cpython-314.pyc create mode 100644 module/scripts/lib/pyasn1/codec/ber/__pycache__/eoo.cpython-314.pyc create mode 100644 module/scripts/lib/pyasn1/codec/ber/decoder.py create mode 100644 module/scripts/lib/pyasn1/codec/ber/encoder.py create mode 100644 module/scripts/lib/pyasn1/codec/ber/eoo.py create mode 100644 module/scripts/lib/pyasn1/codec/cer/__init__.py create mode 100644 module/scripts/lib/pyasn1/codec/cer/__pycache__/__init__.cpython-314.pyc create mode 100644 module/scripts/lib/pyasn1/codec/cer/__pycache__/decoder.cpython-314.pyc create mode 100644 module/scripts/lib/pyasn1/codec/cer/__pycache__/encoder.cpython-314.pyc create mode 100644 module/scripts/lib/pyasn1/codec/cer/decoder.py create mode 100644 module/scripts/lib/pyasn1/codec/cer/encoder.py create mode 100644 module/scripts/lib/pyasn1/codec/der/__init__.py create mode 100644 module/scripts/lib/pyasn1/codec/der/__pycache__/__init__.cpython-314.pyc create mode 100644 module/scripts/lib/pyasn1/codec/der/__pycache__/decoder.cpython-314.pyc create mode 100644 module/scripts/lib/pyasn1/codec/der/__pycache__/encoder.cpython-314.pyc create mode 100644 module/scripts/lib/pyasn1/codec/der/decoder.py create mode 100644 module/scripts/lib/pyasn1/codec/der/encoder.py create mode 100644 module/scripts/lib/pyasn1/codec/native/__init__.py create mode 100644 module/scripts/lib/pyasn1/codec/native/__pycache__/__init__.cpython-314.pyc create mode 100644 module/scripts/lib/pyasn1/codec/native/__pycache__/decoder.cpython-314.pyc create mode 100644 module/scripts/lib/pyasn1/codec/native/__pycache__/encoder.cpython-314.pyc create mode 100644 module/scripts/lib/pyasn1/codec/native/decoder.py create mode 100644 module/scripts/lib/pyasn1/codec/native/encoder.py create mode 100644 module/scripts/lib/pyasn1/codec/streaming.py create mode 100644 module/scripts/lib/pyasn1/compat/__init__.py create mode 100644 module/scripts/lib/pyasn1/compat/__pycache__/__init__.cpython-314.pyc create mode 100644 module/scripts/lib/pyasn1/compat/__pycache__/integer.cpython-314.pyc create mode 100644 module/scripts/lib/pyasn1/compat/integer.py create mode 100644 module/scripts/lib/pyasn1/debug.py create mode 100644 module/scripts/lib/pyasn1/error.py create mode 100644 module/scripts/lib/pyasn1/type/__init__.py create mode 100644 module/scripts/lib/pyasn1/type/__pycache__/__init__.cpython-314.pyc create mode 100644 module/scripts/lib/pyasn1/type/__pycache__/base.cpython-314.pyc create mode 100644 module/scripts/lib/pyasn1/type/__pycache__/char.cpython-314.pyc create mode 100644 module/scripts/lib/pyasn1/type/__pycache__/constraint.cpython-314.pyc create mode 100644 module/scripts/lib/pyasn1/type/__pycache__/error.cpython-314.pyc create mode 100644 module/scripts/lib/pyasn1/type/__pycache__/namedtype.cpython-314.pyc create mode 100644 module/scripts/lib/pyasn1/type/__pycache__/namedval.cpython-314.pyc create mode 100644 module/scripts/lib/pyasn1/type/__pycache__/opentype.cpython-314.pyc create mode 100644 module/scripts/lib/pyasn1/type/__pycache__/tag.cpython-314.pyc create mode 100644 module/scripts/lib/pyasn1/type/__pycache__/tagmap.cpython-314.pyc create mode 100644 module/scripts/lib/pyasn1/type/__pycache__/univ.cpython-314.pyc create mode 100644 module/scripts/lib/pyasn1/type/__pycache__/useful.cpython-314.pyc create mode 100644 module/scripts/lib/pyasn1/type/base.py create mode 100644 module/scripts/lib/pyasn1/type/char.py create mode 100644 module/scripts/lib/pyasn1/type/constraint.py create mode 100644 module/scripts/lib/pyasn1/type/error.py create mode 100644 module/scripts/lib/pyasn1/type/namedtype.py create mode 100644 module/scripts/lib/pyasn1/type/namedval.py create mode 100644 module/scripts/lib/pyasn1/type/opentype.py create mode 100644 module/scripts/lib/pyasn1/type/tag.py create mode 100644 module/scripts/lib/pyasn1/type/tagmap.py create mode 100644 module/scripts/lib/pyasn1/type/univ.py create mode 100644 module/scripts/lib/pyasn1/type/useful.py create mode 100644 module/scripts/lib/rsa/__init__.py create mode 100644 module/scripts/lib/rsa/__pycache__/__init__.cpython-314.pyc create mode 100644 module/scripts/lib/rsa/__pycache__/asn1.cpython-314.pyc create mode 100644 module/scripts/lib/rsa/__pycache__/cli.cpython-314.pyc create mode 100644 module/scripts/lib/rsa/__pycache__/common.cpython-314.pyc create mode 100644 module/scripts/lib/rsa/__pycache__/core.cpython-314.pyc create mode 100644 module/scripts/lib/rsa/__pycache__/key.cpython-314.pyc create mode 100644 module/scripts/lib/rsa/__pycache__/parallel.cpython-314.pyc create mode 100644 module/scripts/lib/rsa/__pycache__/pem.cpython-314.pyc create mode 100644 module/scripts/lib/rsa/__pycache__/pkcs1.cpython-314.pyc create mode 100644 module/scripts/lib/rsa/__pycache__/pkcs1_v2.cpython-314.pyc create mode 100644 module/scripts/lib/rsa/__pycache__/prime.cpython-314.pyc create mode 100644 module/scripts/lib/rsa/__pycache__/randnum.cpython-314.pyc create mode 100644 module/scripts/lib/rsa/__pycache__/transform.cpython-314.pyc create mode 100644 module/scripts/lib/rsa/__pycache__/util.cpython-314.pyc create mode 100644 module/scripts/lib/rsa/asn1.py create mode 100644 module/scripts/lib/rsa/cli.py create mode 100644 module/scripts/lib/rsa/common.py create mode 100644 module/scripts/lib/rsa/core.py create mode 100644 module/scripts/lib/rsa/key.py create mode 100644 module/scripts/lib/rsa/parallel.py create mode 100644 module/scripts/lib/rsa/pem.py create mode 100644 module/scripts/lib/rsa/pkcs1.py create mode 100644 module/scripts/lib/rsa/pkcs1_v2.py create mode 100644 module/scripts/lib/rsa/prime.py create mode 100644 module/scripts/lib/rsa/py.typed create mode 100644 module/scripts/lib/rsa/randnum.py create mode 100644 module/scripts/lib/rsa/transform.py create mode 100644 module/scripts/lib/rsa/util.py create mode 100644 module/scripts/lib/telethon/__init__.py create mode 100644 module/scripts/lib/telethon/__pycache__/__init__.cpython-314.pyc create mode 100644 module/scripts/lib/telethon/__pycache__/custom.cpython-314.pyc create mode 100644 module/scripts/lib/telethon/__pycache__/functions.cpython-314.pyc create mode 100644 module/scripts/lib/telethon/__pycache__/helpers.cpython-314.pyc create mode 100644 module/scripts/lib/telethon/__pycache__/hints.cpython-314.pyc create mode 100644 module/scripts/lib/telethon/__pycache__/password.cpython-314.pyc create mode 100644 module/scripts/lib/telethon/__pycache__/requestiter.cpython-314.pyc create mode 100644 module/scripts/lib/telethon/__pycache__/sync.cpython-314.pyc create mode 100644 module/scripts/lib/telethon/__pycache__/types.cpython-314.pyc create mode 100644 module/scripts/lib/telethon/__pycache__/utils.cpython-314.pyc create mode 100644 module/scripts/lib/telethon/__pycache__/version.cpython-314.pyc create mode 100644 module/scripts/lib/telethon/_updates/__init__.py create mode 100644 module/scripts/lib/telethon/_updates/__pycache__/__init__.cpython-314.pyc create mode 100644 module/scripts/lib/telethon/_updates/__pycache__/entitycache.cpython-314.pyc create mode 100644 module/scripts/lib/telethon/_updates/__pycache__/messagebox.cpython-314.pyc create mode 100644 module/scripts/lib/telethon/_updates/__pycache__/session.cpython-314.pyc create mode 100644 module/scripts/lib/telethon/_updates/entitycache.py create mode 100644 module/scripts/lib/telethon/_updates/messagebox.py create mode 100644 module/scripts/lib/telethon/_updates/session.py create mode 100644 module/scripts/lib/telethon/client/__init__.py create mode 100644 module/scripts/lib/telethon/client/__pycache__/__init__.cpython-314.pyc create mode 100644 module/scripts/lib/telethon/client/__pycache__/account.cpython-314.pyc create mode 100644 module/scripts/lib/telethon/client/__pycache__/auth.cpython-314.pyc create mode 100644 module/scripts/lib/telethon/client/__pycache__/bots.cpython-314.pyc create mode 100644 module/scripts/lib/telethon/client/__pycache__/buttons.cpython-314.pyc create mode 100644 module/scripts/lib/telethon/client/__pycache__/chats.cpython-314.pyc create mode 100644 module/scripts/lib/telethon/client/__pycache__/dialogs.cpython-314.pyc create mode 100644 module/scripts/lib/telethon/client/__pycache__/downloads.cpython-314.pyc create mode 100644 module/scripts/lib/telethon/client/__pycache__/messageparse.cpython-314.pyc create mode 100644 module/scripts/lib/telethon/client/__pycache__/messages.cpython-314.pyc create mode 100644 module/scripts/lib/telethon/client/__pycache__/telegrambaseclient.cpython-314.pyc create mode 100644 module/scripts/lib/telethon/client/__pycache__/telegramclient.cpython-314.pyc create mode 100644 module/scripts/lib/telethon/client/__pycache__/updates.cpython-314.pyc create mode 100644 module/scripts/lib/telethon/client/__pycache__/uploads.cpython-314.pyc create mode 100644 module/scripts/lib/telethon/client/__pycache__/users.cpython-314.pyc create mode 100644 module/scripts/lib/telethon/client/account.py create mode 100644 module/scripts/lib/telethon/client/auth.py create mode 100644 module/scripts/lib/telethon/client/bots.py create mode 100644 module/scripts/lib/telethon/client/buttons.py create mode 100644 module/scripts/lib/telethon/client/chats.py create mode 100644 module/scripts/lib/telethon/client/dialogs.py create mode 100644 module/scripts/lib/telethon/client/downloads.py create mode 100644 module/scripts/lib/telethon/client/messageparse.py create mode 100644 module/scripts/lib/telethon/client/messages.py create mode 100644 module/scripts/lib/telethon/client/telegrambaseclient.py create mode 100644 module/scripts/lib/telethon/client/telegramclient.py create mode 100644 module/scripts/lib/telethon/client/updates.py create mode 100644 module/scripts/lib/telethon/client/uploads.py create mode 100644 module/scripts/lib/telethon/client/users.py create mode 100644 module/scripts/lib/telethon/crypto/__init__.py create mode 100644 module/scripts/lib/telethon/crypto/__pycache__/__init__.cpython-314.pyc create mode 100644 module/scripts/lib/telethon/crypto/__pycache__/aes.cpython-314.pyc create mode 100644 module/scripts/lib/telethon/crypto/__pycache__/aesctr.cpython-314.pyc create mode 100644 module/scripts/lib/telethon/crypto/__pycache__/authkey.cpython-314.pyc create mode 100644 module/scripts/lib/telethon/crypto/__pycache__/cdndecrypter.cpython-314.pyc create mode 100644 module/scripts/lib/telethon/crypto/__pycache__/factorization.cpython-314.pyc create mode 100644 module/scripts/lib/telethon/crypto/__pycache__/libssl.cpython-314.pyc create mode 100644 module/scripts/lib/telethon/crypto/__pycache__/rsa.cpython-314.pyc create mode 100644 module/scripts/lib/telethon/crypto/aes.py create mode 100644 module/scripts/lib/telethon/crypto/aesctr.py create mode 100644 module/scripts/lib/telethon/crypto/authkey.py create mode 100644 module/scripts/lib/telethon/crypto/cdndecrypter.py create mode 100644 module/scripts/lib/telethon/crypto/factorization.py create mode 100644 module/scripts/lib/telethon/crypto/libssl.py create mode 100644 module/scripts/lib/telethon/crypto/rsa.py create mode 100644 module/scripts/lib/telethon/custom.py create mode 100644 module/scripts/lib/telethon/errors/__init__.py create mode 100644 module/scripts/lib/telethon/errors/__pycache__/__init__.cpython-314.pyc create mode 100644 module/scripts/lib/telethon/errors/__pycache__/common.cpython-314.pyc create mode 100644 module/scripts/lib/telethon/errors/__pycache__/rpcbaseerrors.cpython-314.pyc create mode 100644 module/scripts/lib/telethon/errors/__pycache__/rpcerrorlist.cpython-314.pyc create mode 100644 module/scripts/lib/telethon/errors/common.py create mode 100644 module/scripts/lib/telethon/errors/rpcbaseerrors.py create mode 100644 module/scripts/lib/telethon/errors/rpcerrorlist.py create mode 100644 module/scripts/lib/telethon/events/__init__.py create mode 100644 module/scripts/lib/telethon/events/__pycache__/__init__.cpython-314.pyc create mode 100644 module/scripts/lib/telethon/events/__pycache__/album.cpython-314.pyc create mode 100644 module/scripts/lib/telethon/events/__pycache__/callbackquery.cpython-314.pyc create mode 100644 module/scripts/lib/telethon/events/__pycache__/chataction.cpython-314.pyc create mode 100644 module/scripts/lib/telethon/events/__pycache__/common.cpython-314.pyc create mode 100644 module/scripts/lib/telethon/events/__pycache__/inlinequery.cpython-314.pyc create mode 100644 module/scripts/lib/telethon/events/__pycache__/messagedeleted.cpython-314.pyc create mode 100644 module/scripts/lib/telethon/events/__pycache__/messageedited.cpython-314.pyc create mode 100644 module/scripts/lib/telethon/events/__pycache__/messageread.cpython-314.pyc create mode 100644 module/scripts/lib/telethon/events/__pycache__/newmessage.cpython-314.pyc create mode 100644 module/scripts/lib/telethon/events/__pycache__/raw.cpython-314.pyc create mode 100644 module/scripts/lib/telethon/events/__pycache__/userupdate.cpython-314.pyc create mode 100644 module/scripts/lib/telethon/events/album.py create mode 100644 module/scripts/lib/telethon/events/callbackquery.py create mode 100644 module/scripts/lib/telethon/events/chataction.py create mode 100644 module/scripts/lib/telethon/events/common.py create mode 100644 module/scripts/lib/telethon/events/inlinequery.py create mode 100644 module/scripts/lib/telethon/events/messagedeleted.py create mode 100644 module/scripts/lib/telethon/events/messageedited.py create mode 100644 module/scripts/lib/telethon/events/messageread.py create mode 100644 module/scripts/lib/telethon/events/newmessage.py create mode 100644 module/scripts/lib/telethon/events/raw.py create mode 100644 module/scripts/lib/telethon/events/userupdate.py create mode 100644 module/scripts/lib/telethon/extensions/__init__.py create mode 100644 module/scripts/lib/telethon/extensions/__pycache__/__init__.cpython-314.pyc create mode 100644 module/scripts/lib/telethon/extensions/__pycache__/binaryreader.cpython-314.pyc create mode 100644 module/scripts/lib/telethon/extensions/__pycache__/html.cpython-314.pyc create mode 100644 module/scripts/lib/telethon/extensions/__pycache__/markdown.cpython-314.pyc create mode 100644 module/scripts/lib/telethon/extensions/__pycache__/messagepacker.cpython-314.pyc create mode 100644 module/scripts/lib/telethon/extensions/binaryreader.py create mode 100644 module/scripts/lib/telethon/extensions/html.py create mode 100644 module/scripts/lib/telethon/extensions/markdown.py create mode 100644 module/scripts/lib/telethon/extensions/messagepacker.py create mode 100644 module/scripts/lib/telethon/functions.py create mode 100644 module/scripts/lib/telethon/helpers.py create mode 100644 module/scripts/lib/telethon/hints.py create mode 100644 module/scripts/lib/telethon/network/__init__.py create mode 100644 module/scripts/lib/telethon/network/__pycache__/__init__.cpython-314.pyc create mode 100644 module/scripts/lib/telethon/network/__pycache__/authenticator.cpython-314.pyc create mode 100644 module/scripts/lib/telethon/network/__pycache__/mtprotoplainsender.cpython-314.pyc create mode 100644 module/scripts/lib/telethon/network/__pycache__/mtprotosender.cpython-314.pyc create mode 100644 module/scripts/lib/telethon/network/__pycache__/mtprotostate.cpython-314.pyc create mode 100644 module/scripts/lib/telethon/network/__pycache__/requeststate.cpython-314.pyc create mode 100644 module/scripts/lib/telethon/network/authenticator.py create mode 100644 module/scripts/lib/telethon/network/connection/__init__.py create mode 100644 module/scripts/lib/telethon/network/connection/__pycache__/__init__.cpython-314.pyc create mode 100644 module/scripts/lib/telethon/network/connection/__pycache__/connection.cpython-314.pyc create mode 100644 module/scripts/lib/telethon/network/connection/__pycache__/http.cpython-314.pyc create mode 100644 module/scripts/lib/telethon/network/connection/__pycache__/tcpabridged.cpython-314.pyc create mode 100644 module/scripts/lib/telethon/network/connection/__pycache__/tcpfull.cpython-314.pyc create mode 100644 module/scripts/lib/telethon/network/connection/__pycache__/tcpintermediate.cpython-314.pyc create mode 100644 module/scripts/lib/telethon/network/connection/__pycache__/tcpmtproxy.cpython-314.pyc create mode 100644 module/scripts/lib/telethon/network/connection/__pycache__/tcpobfuscated.cpython-314.pyc create mode 100644 module/scripts/lib/telethon/network/connection/connection.py create mode 100644 module/scripts/lib/telethon/network/connection/http.py create mode 100644 module/scripts/lib/telethon/network/connection/tcpabridged.py create mode 100644 module/scripts/lib/telethon/network/connection/tcpfull.py create mode 100644 module/scripts/lib/telethon/network/connection/tcpintermediate.py create mode 100644 module/scripts/lib/telethon/network/connection/tcpmtproxy.py create mode 100644 module/scripts/lib/telethon/network/connection/tcpobfuscated.py create mode 100644 module/scripts/lib/telethon/network/mtprotoplainsender.py create mode 100644 module/scripts/lib/telethon/network/mtprotosender.py create mode 100644 module/scripts/lib/telethon/network/mtprotostate.py create mode 100644 module/scripts/lib/telethon/network/requeststate.py create mode 100644 module/scripts/lib/telethon/password.py create mode 100644 module/scripts/lib/telethon/requestiter.py create mode 100644 module/scripts/lib/telethon/sessions/__init__.py create mode 100644 module/scripts/lib/telethon/sessions/__pycache__/__init__.cpython-314.pyc create mode 100644 module/scripts/lib/telethon/sessions/__pycache__/abstract.cpython-314.pyc create mode 100644 module/scripts/lib/telethon/sessions/__pycache__/memory.cpython-314.pyc create mode 100644 module/scripts/lib/telethon/sessions/__pycache__/sqlite.cpython-314.pyc create mode 100644 module/scripts/lib/telethon/sessions/__pycache__/string.cpython-314.pyc create mode 100644 module/scripts/lib/telethon/sessions/abstract.py create mode 100644 module/scripts/lib/telethon/sessions/memory.py create mode 100644 module/scripts/lib/telethon/sessions/sqlite.py create mode 100644 module/scripts/lib/telethon/sessions/string.py create mode 100644 module/scripts/lib/telethon/sync.py create mode 100644 module/scripts/lib/telethon/tl/__init__.py create mode 100644 module/scripts/lib/telethon/tl/__pycache__/__init__.cpython-314.pyc create mode 100644 module/scripts/lib/telethon/tl/__pycache__/alltlobjects.cpython-314.pyc create mode 100644 module/scripts/lib/telethon/tl/__pycache__/tlobject.cpython-314.pyc create mode 100644 module/scripts/lib/telethon/tl/alltlobjects.py create mode 100644 module/scripts/lib/telethon/tl/core/__init__.py create mode 100644 module/scripts/lib/telethon/tl/core/__pycache__/__init__.cpython-314.pyc create mode 100644 module/scripts/lib/telethon/tl/core/__pycache__/gzippacked.cpython-314.pyc create mode 100644 module/scripts/lib/telethon/tl/core/__pycache__/messagecontainer.cpython-314.pyc create mode 100644 module/scripts/lib/telethon/tl/core/__pycache__/rpcresult.cpython-314.pyc create mode 100644 module/scripts/lib/telethon/tl/core/__pycache__/tlmessage.cpython-314.pyc create mode 100644 module/scripts/lib/telethon/tl/core/gzippacked.py create mode 100644 module/scripts/lib/telethon/tl/core/messagecontainer.py create mode 100644 module/scripts/lib/telethon/tl/core/rpcresult.py create mode 100644 module/scripts/lib/telethon/tl/core/tlmessage.py create mode 100644 module/scripts/lib/telethon/tl/custom/__init__.py create mode 100644 module/scripts/lib/telethon/tl/custom/__pycache__/__init__.cpython-314.pyc create mode 100644 module/scripts/lib/telethon/tl/custom/__pycache__/adminlogevent.cpython-314.pyc create mode 100644 module/scripts/lib/telethon/tl/custom/__pycache__/button.cpython-314.pyc create mode 100644 module/scripts/lib/telethon/tl/custom/__pycache__/chatgetter.cpython-314.pyc create mode 100644 module/scripts/lib/telethon/tl/custom/__pycache__/conversation.cpython-314.pyc create mode 100644 module/scripts/lib/telethon/tl/custom/__pycache__/dialog.cpython-314.pyc create mode 100644 module/scripts/lib/telethon/tl/custom/__pycache__/draft.cpython-314.pyc create mode 100644 module/scripts/lib/telethon/tl/custom/__pycache__/file.cpython-314.pyc create mode 100644 module/scripts/lib/telethon/tl/custom/__pycache__/forward.cpython-314.pyc create mode 100644 module/scripts/lib/telethon/tl/custom/__pycache__/inlinebuilder.cpython-314.pyc create mode 100644 module/scripts/lib/telethon/tl/custom/__pycache__/inlineresult.cpython-314.pyc create mode 100644 module/scripts/lib/telethon/tl/custom/__pycache__/inlineresults.cpython-314.pyc create mode 100644 module/scripts/lib/telethon/tl/custom/__pycache__/inputsizedfile.cpython-314.pyc create mode 100644 module/scripts/lib/telethon/tl/custom/__pycache__/message.cpython-314.pyc create mode 100644 module/scripts/lib/telethon/tl/custom/__pycache__/messagebutton.cpython-314.pyc create mode 100644 module/scripts/lib/telethon/tl/custom/__pycache__/participantpermissions.cpython-314.pyc create mode 100644 module/scripts/lib/telethon/tl/custom/__pycache__/qrlogin.cpython-314.pyc create mode 100644 module/scripts/lib/telethon/tl/custom/__pycache__/sendergetter.cpython-314.pyc create mode 100644 module/scripts/lib/telethon/tl/custom/adminlogevent.py create mode 100644 module/scripts/lib/telethon/tl/custom/button.py create mode 100644 module/scripts/lib/telethon/tl/custom/chatgetter.py create mode 100644 module/scripts/lib/telethon/tl/custom/conversation.py create mode 100644 module/scripts/lib/telethon/tl/custom/dialog.py create mode 100644 module/scripts/lib/telethon/tl/custom/draft.py create mode 100644 module/scripts/lib/telethon/tl/custom/file.py create mode 100644 module/scripts/lib/telethon/tl/custom/forward.py create mode 100644 module/scripts/lib/telethon/tl/custom/inlinebuilder.py create mode 100644 module/scripts/lib/telethon/tl/custom/inlineresult.py create mode 100644 module/scripts/lib/telethon/tl/custom/inlineresults.py create mode 100644 module/scripts/lib/telethon/tl/custom/inputsizedfile.py create mode 100644 module/scripts/lib/telethon/tl/custom/message.py create mode 100644 module/scripts/lib/telethon/tl/custom/messagebutton.py create mode 100644 module/scripts/lib/telethon/tl/custom/participantpermissions.py create mode 100644 module/scripts/lib/telethon/tl/custom/qrlogin.py create mode 100644 module/scripts/lib/telethon/tl/custom/sendergetter.py create mode 100644 module/scripts/lib/telethon/tl/functions/__init__.py create mode 100644 module/scripts/lib/telethon/tl/functions/__pycache__/__init__.cpython-314.pyc create mode 100644 module/scripts/lib/telethon/tl/functions/__pycache__/account.cpython-314.pyc create mode 100644 module/scripts/lib/telethon/tl/functions/__pycache__/aicompose.cpython-314.pyc create mode 100644 module/scripts/lib/telethon/tl/functions/__pycache__/auth.cpython-314.pyc create mode 100644 module/scripts/lib/telethon/tl/functions/__pycache__/bots.cpython-314.pyc create mode 100644 module/scripts/lib/telethon/tl/functions/__pycache__/channels.cpython-314.pyc create mode 100644 module/scripts/lib/telethon/tl/functions/__pycache__/chatlists.cpython-314.pyc create mode 100644 module/scripts/lib/telethon/tl/functions/__pycache__/communities.cpython-314.pyc create mode 100644 module/scripts/lib/telethon/tl/functions/__pycache__/contacts.cpython-314.pyc create mode 100644 module/scripts/lib/telethon/tl/functions/__pycache__/ephemeral.cpython-314.pyc create mode 100644 module/scripts/lib/telethon/tl/functions/__pycache__/folders.cpython-314.pyc create mode 100644 module/scripts/lib/telethon/tl/functions/__pycache__/fragment.cpython-314.pyc create mode 100644 module/scripts/lib/telethon/tl/functions/__pycache__/help.cpython-314.pyc create mode 100644 module/scripts/lib/telethon/tl/functions/__pycache__/langpack.cpython-314.pyc create mode 100644 module/scripts/lib/telethon/tl/functions/__pycache__/messages.cpython-314.pyc create mode 100644 module/scripts/lib/telethon/tl/functions/__pycache__/payments.cpython-314.pyc create mode 100644 module/scripts/lib/telethon/tl/functions/__pycache__/phone.cpython-314.pyc create mode 100644 module/scripts/lib/telethon/tl/functions/__pycache__/photos.cpython-314.pyc create mode 100644 module/scripts/lib/telethon/tl/functions/__pycache__/premium.cpython-314.pyc create mode 100644 module/scripts/lib/telethon/tl/functions/__pycache__/smsjobs.cpython-314.pyc create mode 100644 module/scripts/lib/telethon/tl/functions/__pycache__/stats.cpython-314.pyc create mode 100644 module/scripts/lib/telethon/tl/functions/__pycache__/stickers.cpython-314.pyc create mode 100644 module/scripts/lib/telethon/tl/functions/__pycache__/stories.cpython-314.pyc create mode 100644 module/scripts/lib/telethon/tl/functions/__pycache__/updates.cpython-314.pyc create mode 100644 module/scripts/lib/telethon/tl/functions/__pycache__/upload.cpython-314.pyc create mode 100644 module/scripts/lib/telethon/tl/functions/__pycache__/users.cpython-314.pyc create mode 100644 module/scripts/lib/telethon/tl/functions/account.py create mode 100644 module/scripts/lib/telethon/tl/functions/aicompose.py create mode 100644 module/scripts/lib/telethon/tl/functions/auth.py create mode 100644 module/scripts/lib/telethon/tl/functions/bots.py create mode 100644 module/scripts/lib/telethon/tl/functions/channels.py create mode 100644 module/scripts/lib/telethon/tl/functions/chatlists.py create mode 100644 module/scripts/lib/telethon/tl/functions/communities.py create mode 100644 module/scripts/lib/telethon/tl/functions/contacts.py create mode 100644 module/scripts/lib/telethon/tl/functions/ephemeral.py create mode 100644 module/scripts/lib/telethon/tl/functions/folders.py create mode 100644 module/scripts/lib/telethon/tl/functions/fragment.py create mode 100644 module/scripts/lib/telethon/tl/functions/help.py create mode 100644 module/scripts/lib/telethon/tl/functions/langpack.py create mode 100644 module/scripts/lib/telethon/tl/functions/messages.py create mode 100644 module/scripts/lib/telethon/tl/functions/payments.py create mode 100644 module/scripts/lib/telethon/tl/functions/phone.py create mode 100644 module/scripts/lib/telethon/tl/functions/photos.py create mode 100644 module/scripts/lib/telethon/tl/functions/premium.py create mode 100644 module/scripts/lib/telethon/tl/functions/smsjobs.py create mode 100644 module/scripts/lib/telethon/tl/functions/stats.py create mode 100644 module/scripts/lib/telethon/tl/functions/stickers.py create mode 100644 module/scripts/lib/telethon/tl/functions/stories.py create mode 100644 module/scripts/lib/telethon/tl/functions/updates.py create mode 100644 module/scripts/lib/telethon/tl/functions/upload.py create mode 100644 module/scripts/lib/telethon/tl/functions/users.py create mode 100644 module/scripts/lib/telethon/tl/patched/__init__.py create mode 100644 module/scripts/lib/telethon/tl/patched/__pycache__/__init__.cpython-314.pyc create mode 100644 module/scripts/lib/telethon/tl/tlobject.py create mode 100644 module/scripts/lib/telethon/tl/types/__init__.py create mode 100644 module/scripts/lib/telethon/tl/types/__pycache__/__init__.cpython-314.pyc create mode 100644 module/scripts/lib/telethon/tl/types/__pycache__/account.cpython-314.pyc create mode 100644 module/scripts/lib/telethon/tl/types/__pycache__/aicompose.cpython-314.pyc create mode 100644 module/scripts/lib/telethon/tl/types/__pycache__/auth.cpython-314.pyc create mode 100644 module/scripts/lib/telethon/tl/types/__pycache__/bots.cpython-314.pyc create mode 100644 module/scripts/lib/telethon/tl/types/__pycache__/channels.cpython-314.pyc create mode 100644 module/scripts/lib/telethon/tl/types/__pycache__/chatlists.cpython-314.pyc create mode 100644 module/scripts/lib/telethon/tl/types/__pycache__/communities.cpython-314.pyc create mode 100644 module/scripts/lib/telethon/tl/types/__pycache__/contacts.cpython-314.pyc create mode 100644 module/scripts/lib/telethon/tl/types/__pycache__/ephemeral.cpython-314.pyc create mode 100644 module/scripts/lib/telethon/tl/types/__pycache__/fragment.cpython-314.pyc create mode 100644 module/scripts/lib/telethon/tl/types/__pycache__/help.cpython-314.pyc create mode 100644 module/scripts/lib/telethon/tl/types/__pycache__/messages.cpython-314.pyc create mode 100644 module/scripts/lib/telethon/tl/types/__pycache__/payments.cpython-314.pyc create mode 100644 module/scripts/lib/telethon/tl/types/__pycache__/phone.cpython-314.pyc create mode 100644 module/scripts/lib/telethon/tl/types/__pycache__/photos.cpython-314.pyc create mode 100644 module/scripts/lib/telethon/tl/types/__pycache__/premium.cpython-314.pyc create mode 100644 module/scripts/lib/telethon/tl/types/__pycache__/smsjobs.cpython-314.pyc create mode 100644 module/scripts/lib/telethon/tl/types/__pycache__/stats.cpython-314.pyc create mode 100644 module/scripts/lib/telethon/tl/types/__pycache__/stickers.cpython-314.pyc create mode 100644 module/scripts/lib/telethon/tl/types/__pycache__/storage.cpython-314.pyc create mode 100644 module/scripts/lib/telethon/tl/types/__pycache__/stories.cpython-314.pyc create mode 100644 module/scripts/lib/telethon/tl/types/__pycache__/updates.cpython-314.pyc create mode 100644 module/scripts/lib/telethon/tl/types/__pycache__/upload.cpython-314.pyc create mode 100644 module/scripts/lib/telethon/tl/types/__pycache__/users.cpython-314.pyc create mode 100644 module/scripts/lib/telethon/tl/types/account.py create mode 100644 module/scripts/lib/telethon/tl/types/aicompose.py create mode 100644 module/scripts/lib/telethon/tl/types/auth.py create mode 100644 module/scripts/lib/telethon/tl/types/bots.py create mode 100644 module/scripts/lib/telethon/tl/types/channels.py create mode 100644 module/scripts/lib/telethon/tl/types/chatlists.py create mode 100644 module/scripts/lib/telethon/tl/types/communities.py create mode 100644 module/scripts/lib/telethon/tl/types/contacts.py create mode 100644 module/scripts/lib/telethon/tl/types/ephemeral.py create mode 100644 module/scripts/lib/telethon/tl/types/fragment.py create mode 100644 module/scripts/lib/telethon/tl/types/help.py create mode 100644 module/scripts/lib/telethon/tl/types/messages.py create mode 100644 module/scripts/lib/telethon/tl/types/payments.py create mode 100644 module/scripts/lib/telethon/tl/types/phone.py create mode 100644 module/scripts/lib/telethon/tl/types/photos.py create mode 100644 module/scripts/lib/telethon/tl/types/premium.py create mode 100644 module/scripts/lib/telethon/tl/types/smsjobs.py create mode 100644 module/scripts/lib/telethon/tl/types/stats.py create mode 100644 module/scripts/lib/telethon/tl/types/stickers.py create mode 100644 module/scripts/lib/telethon/tl/types/storage.py create mode 100644 module/scripts/lib/telethon/tl/types/stories.py create mode 100644 module/scripts/lib/telethon/tl/types/updates.py create mode 100644 module/scripts/lib/telethon/tl/types/upload.py create mode 100644 module/scripts/lib/telethon/tl/types/users.py create mode 100644 module/scripts/lib/telethon/types.py create mode 100644 module/scripts/lib/telethon/utils.py create mode 100644 module/scripts/lib/telethon/version.py create mode 100755 module/scripts/telegram.sh create mode 100755 module/scripts/telegram_cli.py mode change 100644 => 100755 module/scripts/worker.sh diff --git a/README.md b/README.md index 5a38e28..0e03fa5 100644 --- a/README.md +++ b/README.md @@ -48,8 +48,9 @@ import existing Neo-Backup and Android-DataBackup backups so you can move over. - **Compression:** none / lz4 / zstd / gzip (gzip = "compatibility mode", opens anywhere). - **Encryption:** optional and portable (`age` + passphrase, or AES-256 zip) - never tied to device hardware, so a backup restores on another device or opens on a PC. -- **Destinations:** local, SFTP, SCP, SMB, WebDAV, FTP, S3 and 70+ more (`rclone`), - plus `rsync` and `restic` repos. Optional Google Drive via a tiny opt-in companion. +- **Destinations:** local, Telegram Cloud Storage (private storage channel), + SFTP, SCP, SMB, WebDAV, FTP, S3 and 70+ more (`rclone`), plus `rsync` and `restic` + repos. Optional Google Drive via a tiny opt-in companion. - **Integrity:** every archive is verified against the manifest; dry-run and restore-test let you check a backup without touching anything. - **Selective & batch restore:** restore any subset of a backup's items, or multi-select diff --git a/docs/USER-GUIDE.md b/docs/USER-GUIDE.md index 3e3688c..d665ce9 100644 --- a/docs/USER-GUIDE.md +++ b/docs/USER-GUIDE.md @@ -401,6 +401,17 @@ automatically, so **no PC is needed** (pasting a token by hand still works as a The remote then appears in the destination's remote list. Passwords are stored obscured, never in clear text. You can list and delete your remotes from the same place. +### Telegram Cloud Storage + +You can back up directly to your personal Telegram account as private cloud storage: + +1. In the **Destinations** tab, tap **Link Telegram**. +2. Enter your phone number with country code (e.g. `+1 234 567 8900`). +3. Enter the login verification code (OTP) sent to your Telegram app (and your 2FA cloud password if enabled). +4. A dedicated private channel starting with `VD_` (e.g. `VD_Backup_4F2A`) is automatically created in your Telegram account to serve as your private storage. +5. Backups targeting the Telegram destination upload directly to this channel. +6. In the **Backups** tab, tap **Telegram Cloud** to browse, download, and restore backups directly from your Telegram channel. + Two rules keep remote use safe: - **Credentials are stored encrypted** at rest in the module's state directory, never diff --git a/module/customize.sh b/module/customize.sh index 801f09b..ef4a32f 100644 --- a/module/customize.sh +++ b/module/customize.sh @@ -95,6 +95,8 @@ set_perm_recursive "$MODPATH" 0 0 0755 0644 set_perm_recursive "$MODPATH/bin" 0 0 0755 0755 set_perm "$MODPATH/scripts/helper.sh" 0 0 0755 set_perm "$MODPATH/scripts/worker.sh" 0 0 0755 +set_perm "$MODPATH/scripts/telegram.sh" 0 0 0755 +set_perm "$MODPATH/scripts/telegram_cli.py" 0 0 0755 set_perm "$MODPATH/service.sh" 0 0 0755 [ -f "$MODPATH/uninstall.sh" ] && set_perm "$MODPATH/uninstall.sh" 0 0 0755 @@ -109,7 +111,7 @@ if [ "$BOOTMODE" = "true" ] && [ "$MODPATH" != "$ACTIVE" ]; then ui_print "- Activating without reboot" mkdir -p "$ACTIVE" cp -afT "$MODPATH" "$ACTIVE" 2>/dev/null || cp -af "$MODPATH"/. "$ACTIVE"/ 2>/dev/null - chmod 0755 "$ACTIVE"/*.sh "$ACTIVE"/scripts/*.sh "$ACTIVE"/bin/*/* 2>/dev/null + chmod 0755 "$ACTIVE"/*.sh "$ACTIVE"/scripts/*.sh "$ACTIVE"/scripts/*.py "$ACTIVE"/bin/*/* 2>/dev/null # the manager writes an "update" marker AFTER customize.sh returns; clear it (and # kick the scheduler) from a detached job so the module shows as active, not pending. setsid sh -c "sleep 4; rm -f '$ACTIVE/update' '$ACTIVE/remove' '$ACTIVE/disable'; '$ACTIVE/scripts/helper.sh' scheduler-tick" >/dev/null 2>&1 & diff --git a/module/scripts/helper.sh b/module/scripts/helper.sh old mode 100644 new mode 100755 index 456a09b..dddb2c2 --- a/module/scripts/helper.sh +++ b/module/scripts/helper.sh @@ -384,7 +384,7 @@ case "$verb" in done printf '}}\n' ;; - backup|restore|import|special-backup|special-restore|cloud-fetch|path-backup|path-restore) + backup|restore|import|special-backup|special-restore|cloud-fetch|path-backup|path-restore|telegram-download) opid="$(new_opid)" detach "$opid" "$verb" "$ARGS_B64" ok "\"opid\":\"$opid\"" ;; @@ -731,9 +731,54 @@ case "$verb" in i=0; while [ ! -f "$res.done" ] && [ "$i" -lt 30 ]; do sleep 1; i=$((i+1)); done if [ -f "$res.done" ] && "$JQ" -e '.ok==true' "$res" >/dev/null 2>&1; then rm -f "$res" "$res.done"; ok '"reachable":true' else c="$("$JQ" -r '.code // "ERR_DRIVE_TIMEOUT"' "$res" 2>/dev/null)"; rm -f "$res" "$res.done"; err "${c:-ERR_DRIVE}" "Drive not reachable - authorize consent"; fi ;; + telegram) + st="$("$MODDIR/scripts/telegram.sh" check-auth 2>/dev/null)" + if printf '%s' "$st" | "$JQ" -e '.authorized == true' >/dev/null 2>&1; then + ok '"reachable":true' + else + err ERR_DEST_AUTH "Telegram not connected. Please link your Telegram account." + fi ;; *) err ERR_DEST_TYPE "unknown type" ;; esac ;; + telegram-status) + "$MODDIR/scripts/telegram.sh" check-auth ;; + + telegram-send-code) + JQ="$(command -v "$BIN/jq" 2>/dev/null || echo jq)" + j="$(printf '%s' "$ARGS_B64" | base64 -d 2>/dev/null)" + phone="$(printf '%s' "$j" | "$JQ" -r '.phone // empty' 2>/dev/null)" + [ -n "$phone" ] || { err ERR_TG_PHONE "Phone number is required"; exit 0; } + "$MODDIR/scripts/telegram.sh" send-code "$phone" ;; + + telegram-sign-in) + JQ="$(command -v "$BIN/jq" 2>/dev/null || echo jq)" + j="$(printf '%s' "$ARGS_B64" | base64 -d 2>/dev/null)" + phone="$(printf '%s' "$j" | "$JQ" -r '.phone // empty' 2>/dev/null)" + code="$(printf '%s' "$j" | "$JQ" -r '.code // empty' 2>/dev/null)" + pw="$(printf '%s' "$j" | "$JQ" -r '.password // empty' 2>/dev/null)" + [ -n "$phone" ] && [ -n "$code" ] || { err ERR_TG_CODE "Phone and verification code are required"; exit 0; } + "$MODDIR/scripts/telegram.sh" sign-in "$phone" "$code" "$pw" ;; + + telegram-init-storage) + JQ="$(command -v "$BIN/jq" 2>/dev/null || echo jq)" + j="$(printf '%s' "$ARGS_B64" | base64 -d 2>/dev/null)" + title="$(printf '%s' "$j" | "$JQ" -r '.title // empty' 2>/dev/null)" + "$MODDIR/scripts/telegram.sh" init-storage "$title" ;; + + telegram-list-remote) + JQ="$(command -v "$BIN/jq" 2>/dev/null || echo jq)" + j="$(printf '%s' "$ARGS_B64" | base64 -d 2>/dev/null)" + cid="$(printf '%s' "$j" | "$JQ" -r '.channel_id // empty' 2>/dev/null)" + if [ -z "$cid" ] && [ -f "$STATE/dests/telegram.json" ]; then + cid="$("$JQ" -r '.channel_id // empty' "$STATE/dests/telegram.json" 2>/dev/null)" + fi + [ -n "$cid" ] || { err ERR_TG_NO_CHANNEL "No storage channel configured"; exit 0; } + "$MODDIR/scripts/telegram.sh" list-remote "$cid" ;; + + telegram-logout) + "$MODDIR/scripts/telegram.sh" logout ;; + schedule-set) JQ="$(command -v "$BIN/jq" 2>/dev/null || echo jq)" j="$(printf '%s' "$ARGS_B64" | base64 -d 2>/dev/null)" diff --git a/module/scripts/lib/pyaes/__init__.py b/module/scripts/lib/pyaes/__init__.py new file mode 100644 index 0000000..5712f79 --- /dev/null +++ b/module/scripts/lib/pyaes/__init__.py @@ -0,0 +1,53 @@ +# The MIT License (MIT) +# +# Copyright (c) 2014 Richard Moore +# +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in +# all copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +# THE SOFTWARE. + +# This is a pure-Python implementation of the AES algorithm and AES common +# modes of operation. + +# See: https://en.wikipedia.org/wiki/Advanced_Encryption_Standard +# See: https://en.wikipedia.org/wiki/Block_cipher_mode_of_operation + + +# Supported key sizes: +# 128-bit +# 192-bit +# 256-bit + + +# Supported modes of operation: +# ECB - Electronic Codebook +# CBC - Cipher-Block Chaining +# CFB - Cipher Feedback +# OFB - Output Feedback +# CTR - Counter + +# See the README.md for API details and general information. + +# Also useful, PyCrypto, a crypto library implemented in C with Python bindings: +# https://www.dlitz.net/software/pycrypto/ + + +VERSION = [1, 3, 0] + +from .aes import AES, AESModeOfOperationCTR, AESModeOfOperationCBC, AESModeOfOperationCFB, AESModeOfOperationECB, AESModeOfOperationOFB, AESModesOfOperation, Counter +from .blockfeeder import decrypt_stream, Decrypter, encrypt_stream, Encrypter +from .blockfeeder import PADDING_NONE, PADDING_DEFAULT diff --git a/module/scripts/lib/pyaes/__pycache__/__init__.cpython-314.pyc b/module/scripts/lib/pyaes/__pycache__/__init__.cpython-314.pyc new file mode 100644 index 0000000000000000000000000000000000000000..30e42eb89e019d6e57a7b5b3935b5a1673204e62 GIT binary patch literal 687 zcmZvZF>ljA6vyvuCwA(@2?B(eIxrxsYL2NBLP}zni$rYXmX5U?`;wX_wsn3%^471z zM`1*4w{(D|6NnC7xg)IvQl4}_z4w3b?mgW*aC&=4+K(?kKfbgO`mTZp)9#(ybLm{6 zA&Ocd26T=iJjGN4jq0FN0}R@N7HvbDnqX23EZTw2zN*uvd(;M-cA-mq(4!7Ge|25z zedyBx4E9lEyg+XI2FoU{CG?ShBA!aj%8Z4Juwp!cyeuZC(N1|h*(o27cgg-_ryR=a z$M-Gke|idYQf>-h+%<%gv6MFz#5Hi1tVPG$#*7Px75}IFZ7lC;qWga2dDGx+9E5={ z`VW!kACEq~I}Kc&t8c#1734wgBVnHT(aAIngr2aPs|+_3S_*9iQ$el^@s5I8p|Izx zGQC_dmNCw4C3h8i3XVcwVIc7t-AW>nlPrORY-+~IE0UJ$VX|zjQ~jI>z|-93%V5m3e0%ywL7sdZEbyC9l!!8=76Cqp#?QKa=%;0QYym A0ssI2 literal 0 HcmV?d00001 diff --git a/module/scripts/lib/pyaes/__pycache__/aes.cpython-314.pyc b/module/scripts/lib/pyaes/__pycache__/aes.cpython-314.pyc new file mode 100644 index 0000000000000000000000000000000000000000..4eeb22b86b32730dbdef525a7c0710a1bcf986a8 GIT binary patch literal 110140 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≻XA}G>{hp{kZ8;~kE4MDIU@UDvrs zQCuZ|oWCJwk={P% zQA%`p%Vw5n6Ar!3O^?1d`RPfy#73q-d2;55<=7he&=_U3VH9fM5T7JAXuXL|4piTv-rPe0ZJXg8HB?AGp!kk!RZYa+Ie&q%xb^iaM6Z z^=x<^i?hB_E?GTX3IC+`SkX54oVegn#^bQaE zt-VgykTu}7?kOq}{{v!c6kkG!vdIdoc#s9`A*1xxuyv_)vP&t2GZdrN!;mFhMAc0> z>~RNiv{XiuGk;w_UGrMQD-Da8J5e$-OXhTQRc~1iO;z4CC%w?{Y{PW!iaC3RUpD71 znzQdGro3?F*(=j6%ZXcO@|F^J{ATb$6lcwys*w-HA@9a>$>k4Hxvkde+Rs+c8h$VS zR{Y}jgUi_mr)oaYAeAlg)3|&iifS8vi{iv*@gshm1<$eIc^3RC3#3|R@`x%kvFeYP8b3rfEOiAowVNB{Z}F3f9&f-ZjQED7Ud7#a zix0)rj=S7G)?M9F%Kz}%w2ojyXzNC4?Q^@t3146@lKPOfSMZCh>O=gj3an^;J3O!iJ4gnQ0|K&Ef>jZ z(X*O>w$2#JMEgyQQtX{5@_s3<-7dj5=obAYq8aIEEQ;DVW26>> z9VOoid(`3=!U0QzpEcIM!u=1!N%;u2bN!p$f<3FvG*(3<7d6`uW)~6 zsJ@??{>w_M%yzDS|NF6LMar>bx!_bDGICAJ?``R1JDlXfK)~mJsy0K zF@l_Qc>5iyQJ+Wo4J%j9Z%4`A^4jXElD&tO8)k`WJlWc2ZEY^!vuF3d@=9wVyC|;d z@{6t#kKhlK40tb=`hvZ~$Pm{2`b*hXZ19)L=QvCKK4q=M*VkWk1h?Q~M_Lb-lud4; zbkK7zL@J8b7YWS5S-%(SNh%+MA&2sv&5>bdQ+v5VzFRfJsPFUf-K;E13(Q4k@UB^! z^6L`><=$>Kk?nkXhp>imX3YOQlj4{b-#eW9<-*7FmNUvaVTb(&fhb zKLa1{BXUqn*0dn(VJRi8jaA>HI1&9;T*B1Eb4RCbOL1H8C1=m)99vATm^W9vow|Ld zYcaKSD&`){G{fg(UXNL^mMvJz=86`rPcJ4uGjDoEYVv+jb3Rut>_27JOP%HKuFZPM zXxJd@6_erY>a3r9?$~tiQk+%F`ijNm%6W6;+i5#yE-a>%O~tOw`kjl`XBHEW&YO

py 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 K_aE3}^8PPtWnB*d literal 0 HcmV?d00001 diff --git a/module/scripts/lib/pyaes/__pycache__/util.cpython-314.pyc b/module/scripts/lib/pyaes/__pycache__/util.cpython-314.pyc new file mode 100644 index 0000000000000000000000000000000000000000..a5fb6f7aaa29bf8d3f03b11ca7731a3bcebc71a0 GIT binary patch literal 1723 zcmah}&2Jk;6o2Ej9edsQD<)0TnpijlIXGUc4G_^NhbADTk_fqIPa)RY-q;Jy+RUzt z-2+^G(zaGr4JlJrcQjp-6y4OyRJp5}tp%lCmy9A+%wk27*A{Lm#rGO@X{T0d zR8_X2)lEaHY6Yp@QqV=i)T)_!%N{pt`9foRTct{&s!{|3;y8>ke!YW0#9BYaDV$4! zCBT!I;#Ql(1Z7xs=1HK;0@D&|w1gj0>?cNc{oDu#6QRR_2zYVK-g)i_4-jd%;582=$3|wkS}Q75VsE z^wHEb!APQJXu4r4dQr7K3~7UU*lAu~JThmSD+ZSFJSv9&Go9_P)n5X$KLutFCa$#C z4@0pZS)^Vp-3z6=zVtP*+OlD2-t^s(LB^*6r0hujFyJr0HdvKqnFogSofhK)kdobk@G` z@7=l|d?e0Ts|SJ5?!vdigVl$=?{9n;>_w$V0jcYjF5<#)<+yysUHv~QTd?#2Kx3+J&{{XMh zIox0^U|dIaED%CYUL_=Y;zf1r6-ne*w|Fw?BH5=tKe_zJaok5DCvL&B@Khw8$X^Bv M+IB_ZbKf=LAIPvp2LJ#7 literal 0 HcmV?d00001 diff --git a/module/scripts/lib/pyaes/aes.py b/module/scripts/lib/pyaes/aes.py new file mode 100644 index 0000000..c6e8bc0 --- /dev/null +++ b/module/scripts/lib/pyaes/aes.py @@ -0,0 +1,589 @@ +# The MIT License (MIT) +# +# Copyright (c) 2014 Richard Moore +# +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in +# all copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +# THE SOFTWARE. + +# This is a pure-Python implementation of the AES algorithm and AES common +# modes of operation. + +# See: https://en.wikipedia.org/wiki/Advanced_Encryption_Standard + +# Honestly, the best description of the modes of operations are the wonderful +# diagrams on Wikipedia. They explain in moments what my words could never +# achieve. Hence the inline documentation here is sparer than I'd prefer. +# See: https://en.wikipedia.org/wiki/Block_cipher_mode_of_operation + +# Also useful, PyCrypto, a crypto library implemented in C with Python bindings: +# https://www.dlitz.net/software/pycrypto/ + + +# Supported key sizes: +# 128-bit +# 192-bit +# 256-bit + + +# Supported modes of operation: +# ECB - Electronic Codebook +# CBC - Cipher-Block Chaining +# CFB - Cipher Feedback +# OFB - Output Feedback +# CTR - Counter + + +# See the README.md for API details and general information. + + +import copy +import struct + +__all__ = ["AES", "AESModeOfOperationCTR", "AESModeOfOperationCBC", "AESModeOfOperationCFB", + "AESModeOfOperationECB", "AESModeOfOperationOFB", "AESModesOfOperation", "Counter"] + + +def _compact_word(word): + return (word[0] << 24) | (word[1] << 16) | (word[2] << 8) | word[3] + +def _string_to_bytes(text): + return list(ord(c) for c in text) + +def _bytes_to_string(binary): + return "".join(chr(b) for b in binary) + +def _concat_list(a, b): + return a + b + + +# Python 3 compatibility +try: + xrange +except Exception: + xrange = range + + # Python 3 supports bytes, which is already an array of integers + def _string_to_bytes(text): + if isinstance(text, bytes): + return text + return [ord(c) for c in text] + + # In Python 3, we return bytes + def _bytes_to_string(binary): + return bytes(binary) + + # Python 3 cannot concatenate a list onto a bytes, so we bytes-ify it first + def _concat_list(a, b): + return a + bytes(b) + + +# Based *largely* on the Rijndael implementation +# See: http://csrc.nist.gov/publications/fips/fips197/fips-197.pdf +class AES(object): + '''Encapsulates the AES block cipher. + + You generally should not need this. Use the AESModeOfOperation classes + below instead.''' + + # Number of rounds by keysize + number_of_rounds = {16: 10, 24: 12, 32: 14} + + # Round constant words + rcon = [ 0x01, 0x02, 0x04, 0x08, 0x10, 0x20, 0x40, 0x80, 0x1b, 0x36, 0x6c, 0xd8, 0xab, 0x4d, 0x9a, 0x2f, 0x5e, 0xbc, 0x63, 0xc6, 0x97, 0x35, 0x6a, 0xd4, 0xb3, 0x7d, 0xfa, 0xef, 0xc5, 0x91 ] + + # S-box and Inverse S-box (S is for Substitution) + S = [ 0x63, 0x7c, 0x77, 0x7b, 0xf2, 0x6b, 0x6f, 0xc5, 0x30, 0x01, 0x67, 0x2b, 0xfe, 0xd7, 0xab, 0x76, 0xca, 0x82, 0xc9, 0x7d, 0xfa, 0x59, 0x47, 0xf0, 0xad, 0xd4, 0xa2, 0xaf, 0x9c, 0xa4, 0x72, 0xc0, 0xb7, 0xfd, 0x93, 0x26, 0x36, 0x3f, 0xf7, 0xcc, 0x34, 0xa5, 0xe5, 0xf1, 0x71, 0xd8, 0x31, 0x15, 0x04, 0xc7, 0x23, 0xc3, 0x18, 0x96, 0x05, 0x9a, 0x07, 0x12, 0x80, 0xe2, 0xeb, 0x27, 0xb2, 0x75, 0x09, 0x83, 0x2c, 0x1a, 0x1b, 0x6e, 0x5a, 0xa0, 0x52, 0x3b, 0xd6, 0xb3, 0x29, 0xe3, 0x2f, 0x84, 0x53, 0xd1, 0x00, 0xed, 0x20, 0xfc, 0xb1, 0x5b, 0x6a, 0xcb, 0xbe, 0x39, 0x4a, 0x4c, 0x58, 0xcf, 0xd0, 0xef, 0xaa, 0xfb, 0x43, 0x4d, 0x33, 0x85, 0x45, 0xf9, 0x02, 0x7f, 0x50, 0x3c, 0x9f, 0xa8, 0x51, 0xa3, 0x40, 0x8f, 0x92, 0x9d, 0x38, 0xf5, 0xbc, 0xb6, 0xda, 0x21, 0x10, 0xff, 0xf3, 0xd2, 0xcd, 0x0c, 0x13, 0xec, 0x5f, 0x97, 0x44, 0x17, 0xc4, 0xa7, 0x7e, 0x3d, 0x64, 0x5d, 0x19, 0x73, 0x60, 0x81, 0x4f, 0xdc, 0x22, 0x2a, 0x90, 0x88, 0x46, 0xee, 0xb8, 0x14, 0xde, 0x5e, 0x0b, 0xdb, 0xe0, 0x32, 0x3a, 0x0a, 0x49, 0x06, 0x24, 0x5c, 0xc2, 0xd3, 0xac, 0x62, 0x91, 0x95, 0xe4, 0x79, 0xe7, 0xc8, 0x37, 0x6d, 0x8d, 0xd5, 0x4e, 0xa9, 0x6c, 0x56, 0xf4, 0xea, 0x65, 0x7a, 0xae, 0x08, 0xba, 0x78, 0x25, 0x2e, 0x1c, 0xa6, 0xb4, 0xc6, 0xe8, 0xdd, 0x74, 0x1f, 0x4b, 0xbd, 0x8b, 0x8a, 0x70, 0x3e, 0xb5, 0x66, 0x48, 0x03, 0xf6, 0x0e, 0x61, 0x35, 0x57, 0xb9, 0x86, 0xc1, 0x1d, 0x9e, 0xe1, 0xf8, 0x98, 0x11, 0x69, 0xd9, 0x8e, 0x94, 0x9b, 0x1e, 0x87, 0xe9, 0xce, 0x55, 0x28, 0xdf, 0x8c, 0xa1, 0x89, 0x0d, 0xbf, 0xe6, 0x42, 0x68, 0x41, 0x99, 0x2d, 0x0f, 0xb0, 0x54, 0xbb, 0x16 ] + Si =[ 0x52, 0x09, 0x6a, 0xd5, 0x30, 0x36, 0xa5, 0x38, 0xbf, 0x40, 0xa3, 0x9e, 0x81, 0xf3, 0xd7, 0xfb, 0x7c, 0xe3, 0x39, 0x82, 0x9b, 0x2f, 0xff, 0x87, 0x34, 0x8e, 0x43, 0x44, 0xc4, 0xde, 0xe9, 0xcb, 0x54, 0x7b, 0x94, 0x32, 0xa6, 0xc2, 0x23, 0x3d, 0xee, 0x4c, 0x95, 0x0b, 0x42, 0xfa, 0xc3, 0x4e, 0x08, 0x2e, 0xa1, 0x66, 0x28, 0xd9, 0x24, 0xb2, 0x76, 0x5b, 0xa2, 0x49, 0x6d, 0x8b, 0xd1, 0x25, 0x72, 0xf8, 0xf6, 0x64, 0x86, 0x68, 0x98, 0x16, 0xd4, 0xa4, 0x5c, 0xcc, 0x5d, 0x65, 0xb6, 0x92, 0x6c, 0x70, 0x48, 0x50, 0xfd, 0xed, 0xb9, 0xda, 0x5e, 0x15, 0x46, 0x57, 0xa7, 0x8d, 0x9d, 0x84, 0x90, 0xd8, 0xab, 0x00, 0x8c, 0xbc, 0xd3, 0x0a, 0xf7, 0xe4, 0x58, 0x05, 0xb8, 0xb3, 0x45, 0x06, 0xd0, 0x2c, 0x1e, 0x8f, 0xca, 0x3f, 0x0f, 0x02, 0xc1, 0xaf, 0xbd, 0x03, 0x01, 0x13, 0x8a, 0x6b, 0x3a, 0x91, 0x11, 0x41, 0x4f, 0x67, 0xdc, 0xea, 0x97, 0xf2, 0xcf, 0xce, 0xf0, 0xb4, 0xe6, 0x73, 0x96, 0xac, 0x74, 0x22, 0xe7, 0xad, 0x35, 0x85, 0xe2, 0xf9, 0x37, 0xe8, 0x1c, 0x75, 0xdf, 0x6e, 0x47, 0xf1, 0x1a, 0x71, 0x1d, 0x29, 0xc5, 0x89, 0x6f, 0xb7, 0x62, 0x0e, 0xaa, 0x18, 0xbe, 0x1b, 0xfc, 0x56, 0x3e, 0x4b, 0xc6, 0xd2, 0x79, 0x20, 0x9a, 0xdb, 0xc0, 0xfe, 0x78, 0xcd, 0x5a, 0xf4, 0x1f, 0xdd, 0xa8, 0x33, 0x88, 0x07, 0xc7, 0x31, 0xb1, 0x12, 0x10, 0x59, 0x27, 0x80, 0xec, 0x5f, 0x60, 0x51, 0x7f, 0xa9, 0x19, 0xb5, 0x4a, 0x0d, 0x2d, 0xe5, 0x7a, 0x9f, 0x93, 0xc9, 0x9c, 0xef, 0xa0, 0xe0, 0x3b, 0x4d, 0xae, 0x2a, 0xf5, 0xb0, 0xc8, 0xeb, 0xbb, 0x3c, 0x83, 0x53, 0x99, 0x61, 0x17, 0x2b, 0x04, 0x7e, 0xba, 0x77, 0xd6, 0x26, 0xe1, 0x69, 0x14, 0x63, 0x55, 0x21, 0x0c, 0x7d ] + + # Transformations for encryption + T1 = [ 0xc66363a5, 0xf87c7c84, 0xee777799, 0xf67b7b8d, 0xfff2f20d, 0xd66b6bbd, 0xde6f6fb1, 0x91c5c554, 0x60303050, 0x02010103, 0xce6767a9, 0x562b2b7d, 0xe7fefe19, 0xb5d7d762, 0x4dababe6, 0xec76769a, 0x8fcaca45, 0x1f82829d, 0x89c9c940, 0xfa7d7d87, 0xeffafa15, 0xb25959eb, 0x8e4747c9, 0xfbf0f00b, 0x41adadec, 0xb3d4d467, 0x5fa2a2fd, 0x45afafea, 0x239c9cbf, 0x53a4a4f7, 0xe4727296, 0x9bc0c05b, 0x75b7b7c2, 0xe1fdfd1c, 0x3d9393ae, 0x4c26266a, 0x6c36365a, 0x7e3f3f41, 0xf5f7f702, 0x83cccc4f, 0x6834345c, 0x51a5a5f4, 0xd1e5e534, 0xf9f1f108, 0xe2717193, 0xabd8d873, 0x62313153, 0x2a15153f, 0x0804040c, 0x95c7c752, 0x46232365, 0x9dc3c35e, 0x30181828, 0x379696a1, 0x0a05050f, 0x2f9a9ab5, 0x0e070709, 0x24121236, 0x1b80809b, 0xdfe2e23d, 0xcdebeb26, 0x4e272769, 0x7fb2b2cd, 0xea75759f, 0x1209091b, 0x1d83839e, 0x582c2c74, 0x341a1a2e, 0x361b1b2d, 0xdc6e6eb2, 0xb45a5aee, 0x5ba0a0fb, 0xa45252f6, 0x763b3b4d, 0xb7d6d661, 0x7db3b3ce, 0x5229297b, 0xdde3e33e, 0x5e2f2f71, 0x13848497, 0xa65353f5, 0xb9d1d168, 0x00000000, 0xc1eded2c, 0x40202060, 0xe3fcfc1f, 0x79b1b1c8, 0xb65b5bed, 0xd46a6abe, 0x8dcbcb46, 0x67bebed9, 0x7239394b, 0x944a4ade, 0x984c4cd4, 0xb05858e8, 0x85cfcf4a, 0xbbd0d06b, 0xc5efef2a, 0x4faaaae5, 0xedfbfb16, 0x864343c5, 0x9a4d4dd7, 0x66333355, 0x11858594, 0x8a4545cf, 0xe9f9f910, 0x04020206, 0xfe7f7f81, 0xa05050f0, 0x783c3c44, 0x259f9fba, 0x4ba8a8e3, 0xa25151f3, 0x5da3a3fe, 0x804040c0, 0x058f8f8a, 0x3f9292ad, 0x219d9dbc, 0x70383848, 0xf1f5f504, 0x63bcbcdf, 0x77b6b6c1, 0xafdada75, 0x42212163, 0x20101030, 0xe5ffff1a, 0xfdf3f30e, 0xbfd2d26d, 0x81cdcd4c, 0x180c0c14, 0x26131335, 0xc3ecec2f, 0xbe5f5fe1, 0x359797a2, 0x884444cc, 0x2e171739, 0x93c4c457, 0x55a7a7f2, 0xfc7e7e82, 0x7a3d3d47, 0xc86464ac, 0xba5d5de7, 0x3219192b, 0xe6737395, 0xc06060a0, 0x19818198, 0x9e4f4fd1, 0xa3dcdc7f, 0x44222266, 0x542a2a7e, 0x3b9090ab, 0x0b888883, 0x8c4646ca, 0xc7eeee29, 0x6bb8b8d3, 0x2814143c, 0xa7dede79, 0xbc5e5ee2, 0x160b0b1d, 0xaddbdb76, 0xdbe0e03b, 0x64323256, 0x743a3a4e, 0x140a0a1e, 0x924949db, 0x0c06060a, 0x4824246c, 0xb85c5ce4, 0x9fc2c25d, 0xbdd3d36e, 0x43acacef, 0xc46262a6, 0x399191a8, 0x319595a4, 0xd3e4e437, 0xf279798b, 0xd5e7e732, 0x8bc8c843, 0x6e373759, 0xda6d6db7, 0x018d8d8c, 0xb1d5d564, 0x9c4e4ed2, 0x49a9a9e0, 0xd86c6cb4, 0xac5656fa, 0xf3f4f407, 0xcfeaea25, 0xca6565af, 0xf47a7a8e, 0x47aeaee9, 0x10080818, 0x6fbabad5, 0xf0787888, 0x4a25256f, 0x5c2e2e72, 0x381c1c24, 0x57a6a6f1, 0x73b4b4c7, 0x97c6c651, 0xcbe8e823, 0xa1dddd7c, 0xe874749c, 0x3e1f1f21, 0x964b4bdd, 0x61bdbddc, 0x0d8b8b86, 0x0f8a8a85, 0xe0707090, 0x7c3e3e42, 0x71b5b5c4, 0xcc6666aa, 0x904848d8, 0x06030305, 0xf7f6f601, 0x1c0e0e12, 0xc26161a3, 0x6a35355f, 0xae5757f9, 0x69b9b9d0, 0x17868691, 0x99c1c158, 0x3a1d1d27, 0x279e9eb9, 0xd9e1e138, 0xebf8f813, 0x2b9898b3, 0x22111133, 0xd26969bb, 0xa9d9d970, 0x078e8e89, 0x339494a7, 0x2d9b9bb6, 0x3c1e1e22, 0x15878792, 0xc9e9e920, 0x87cece49, 0xaa5555ff, 0x50282878, 0xa5dfdf7a, 0x038c8c8f, 0x59a1a1f8, 0x09898980, 0x1a0d0d17, 0x65bfbfda, 0xd7e6e631, 0x844242c6, 0xd06868b8, 0x824141c3, 0x299999b0, 0x5a2d2d77, 0x1e0f0f11, 0x7bb0b0cb, 0xa85454fc, 0x6dbbbbd6, 0x2c16163a ] + T2 = [ 0xa5c66363, 0x84f87c7c, 0x99ee7777, 0x8df67b7b, 0x0dfff2f2, 0xbdd66b6b, 0xb1de6f6f, 0x5491c5c5, 0x50603030, 0x03020101, 0xa9ce6767, 0x7d562b2b, 0x19e7fefe, 0x62b5d7d7, 0xe64dabab, 0x9aec7676, 0x458fcaca, 0x9d1f8282, 0x4089c9c9, 0x87fa7d7d, 0x15effafa, 0xebb25959, 0xc98e4747, 0x0bfbf0f0, 0xec41adad, 0x67b3d4d4, 0xfd5fa2a2, 0xea45afaf, 0xbf239c9c, 0xf753a4a4, 0x96e47272, 0x5b9bc0c0, 0xc275b7b7, 0x1ce1fdfd, 0xae3d9393, 0x6a4c2626, 0x5a6c3636, 0x417e3f3f, 0x02f5f7f7, 0x4f83cccc, 0x5c683434, 0xf451a5a5, 0x34d1e5e5, 0x08f9f1f1, 0x93e27171, 0x73abd8d8, 0x53623131, 0x3f2a1515, 0x0c080404, 0x5295c7c7, 0x65462323, 0x5e9dc3c3, 0x28301818, 0xa1379696, 0x0f0a0505, 0xb52f9a9a, 0x090e0707, 0x36241212, 0x9b1b8080, 0x3ddfe2e2, 0x26cdebeb, 0x694e2727, 0xcd7fb2b2, 0x9fea7575, 0x1b120909, 0x9e1d8383, 0x74582c2c, 0x2e341a1a, 0x2d361b1b, 0xb2dc6e6e, 0xeeb45a5a, 0xfb5ba0a0, 0xf6a45252, 0x4d763b3b, 0x61b7d6d6, 0xce7db3b3, 0x7b522929, 0x3edde3e3, 0x715e2f2f, 0x97138484, 0xf5a65353, 0x68b9d1d1, 0x00000000, 0x2cc1eded, 0x60402020, 0x1fe3fcfc, 0xc879b1b1, 0xedb65b5b, 0xbed46a6a, 0x468dcbcb, 0xd967bebe, 0x4b723939, 0xde944a4a, 0xd4984c4c, 0xe8b05858, 0x4a85cfcf, 0x6bbbd0d0, 0x2ac5efef, 0xe54faaaa, 0x16edfbfb, 0xc5864343, 0xd79a4d4d, 0x55663333, 0x94118585, 0xcf8a4545, 0x10e9f9f9, 0x06040202, 0x81fe7f7f, 0xf0a05050, 0x44783c3c, 0xba259f9f, 0xe34ba8a8, 0xf3a25151, 0xfe5da3a3, 0xc0804040, 0x8a058f8f, 0xad3f9292, 0xbc219d9d, 0x48703838, 0x04f1f5f5, 0xdf63bcbc, 0xc177b6b6, 0x75afdada, 0x63422121, 0x30201010, 0x1ae5ffff, 0x0efdf3f3, 0x6dbfd2d2, 0x4c81cdcd, 0x14180c0c, 0x35261313, 0x2fc3ecec, 0xe1be5f5f, 0xa2359797, 0xcc884444, 0x392e1717, 0x5793c4c4, 0xf255a7a7, 0x82fc7e7e, 0x477a3d3d, 0xacc86464, 0xe7ba5d5d, 0x2b321919, 0x95e67373, 0xa0c06060, 0x98198181, 0xd19e4f4f, 0x7fa3dcdc, 0x66442222, 0x7e542a2a, 0xab3b9090, 0x830b8888, 0xca8c4646, 0x29c7eeee, 0xd36bb8b8, 0x3c281414, 0x79a7dede, 0xe2bc5e5e, 0x1d160b0b, 0x76addbdb, 0x3bdbe0e0, 0x56643232, 0x4e743a3a, 0x1e140a0a, 0xdb924949, 0x0a0c0606, 0x6c482424, 0xe4b85c5c, 0x5d9fc2c2, 0x6ebdd3d3, 0xef43acac, 0xa6c46262, 0xa8399191, 0xa4319595, 0x37d3e4e4, 0x8bf27979, 0x32d5e7e7, 0x438bc8c8, 0x596e3737, 0xb7da6d6d, 0x8c018d8d, 0x64b1d5d5, 0xd29c4e4e, 0xe049a9a9, 0xb4d86c6c, 0xfaac5656, 0x07f3f4f4, 0x25cfeaea, 0xafca6565, 0x8ef47a7a, 0xe947aeae, 0x18100808, 0xd56fbaba, 0x88f07878, 0x6f4a2525, 0x725c2e2e, 0x24381c1c, 0xf157a6a6, 0xc773b4b4, 0x5197c6c6, 0x23cbe8e8, 0x7ca1dddd, 0x9ce87474, 0x213e1f1f, 0xdd964b4b, 0xdc61bdbd, 0x860d8b8b, 0x850f8a8a, 0x90e07070, 0x427c3e3e, 0xc471b5b5, 0xaacc6666, 0xd8904848, 0x05060303, 0x01f7f6f6, 0x121c0e0e, 0xa3c26161, 0x5f6a3535, 0xf9ae5757, 0xd069b9b9, 0x91178686, 0x5899c1c1, 0x273a1d1d, 0xb9279e9e, 0x38d9e1e1, 0x13ebf8f8, 0xb32b9898, 0x33221111, 0xbbd26969, 0x70a9d9d9, 0x89078e8e, 0xa7339494, 0xb62d9b9b, 0x223c1e1e, 0x92158787, 0x20c9e9e9, 0x4987cece, 0xffaa5555, 0x78502828, 0x7aa5dfdf, 0x8f038c8c, 0xf859a1a1, 0x80098989, 0x171a0d0d, 0xda65bfbf, 0x31d7e6e6, 0xc6844242, 0xb8d06868, 0xc3824141, 0xb0299999, 0x775a2d2d, 0x111e0f0f, 0xcb7bb0b0, 0xfca85454, 0xd66dbbbb, 0x3a2c1616 ] + T3 = [ 0x63a5c663, 0x7c84f87c, 0x7799ee77, 0x7b8df67b, 0xf20dfff2, 0x6bbdd66b, 0x6fb1de6f, 0xc55491c5, 0x30506030, 0x01030201, 0x67a9ce67, 0x2b7d562b, 0xfe19e7fe, 0xd762b5d7, 0xabe64dab, 0x769aec76, 0xca458fca, 0x829d1f82, 0xc94089c9, 0x7d87fa7d, 0xfa15effa, 0x59ebb259, 0x47c98e47, 0xf00bfbf0, 0xadec41ad, 0xd467b3d4, 0xa2fd5fa2, 0xafea45af, 0x9cbf239c, 0xa4f753a4, 0x7296e472, 0xc05b9bc0, 0xb7c275b7, 0xfd1ce1fd, 0x93ae3d93, 0x266a4c26, 0x365a6c36, 0x3f417e3f, 0xf702f5f7, 0xcc4f83cc, 0x345c6834, 0xa5f451a5, 0xe534d1e5, 0xf108f9f1, 0x7193e271, 0xd873abd8, 0x31536231, 0x153f2a15, 0x040c0804, 0xc75295c7, 0x23654623, 0xc35e9dc3, 0x18283018, 0x96a13796, 0x050f0a05, 0x9ab52f9a, 0x07090e07, 0x12362412, 0x809b1b80, 0xe23ddfe2, 0xeb26cdeb, 0x27694e27, 0xb2cd7fb2, 0x759fea75, 0x091b1209, 0x839e1d83, 0x2c74582c, 0x1a2e341a, 0x1b2d361b, 0x6eb2dc6e, 0x5aeeb45a, 0xa0fb5ba0, 0x52f6a452, 0x3b4d763b, 0xd661b7d6, 0xb3ce7db3, 0x297b5229, 0xe33edde3, 0x2f715e2f, 0x84971384, 0x53f5a653, 0xd168b9d1, 0x00000000, 0xed2cc1ed, 0x20604020, 0xfc1fe3fc, 0xb1c879b1, 0x5bedb65b, 0x6abed46a, 0xcb468dcb, 0xbed967be, 0x394b7239, 0x4ade944a, 0x4cd4984c, 0x58e8b058, 0xcf4a85cf, 0xd06bbbd0, 0xef2ac5ef, 0xaae54faa, 0xfb16edfb, 0x43c58643, 0x4dd79a4d, 0x33556633, 0x85941185, 0x45cf8a45, 0xf910e9f9, 0x02060402, 0x7f81fe7f, 0x50f0a050, 0x3c44783c, 0x9fba259f, 0xa8e34ba8, 0x51f3a251, 0xa3fe5da3, 0x40c08040, 0x8f8a058f, 0x92ad3f92, 0x9dbc219d, 0x38487038, 0xf504f1f5, 0xbcdf63bc, 0xb6c177b6, 0xda75afda, 0x21634221, 0x10302010, 0xff1ae5ff, 0xf30efdf3, 0xd26dbfd2, 0xcd4c81cd, 0x0c14180c, 0x13352613, 0xec2fc3ec, 0x5fe1be5f, 0x97a23597, 0x44cc8844, 0x17392e17, 0xc45793c4, 0xa7f255a7, 0x7e82fc7e, 0x3d477a3d, 0x64acc864, 0x5de7ba5d, 0x192b3219, 0x7395e673, 0x60a0c060, 0x81981981, 0x4fd19e4f, 0xdc7fa3dc, 0x22664422, 0x2a7e542a, 0x90ab3b90, 0x88830b88, 0x46ca8c46, 0xee29c7ee, 0xb8d36bb8, 0x143c2814, 0xde79a7de, 0x5ee2bc5e, 0x0b1d160b, 0xdb76addb, 0xe03bdbe0, 0x32566432, 0x3a4e743a, 0x0a1e140a, 0x49db9249, 0x060a0c06, 0x246c4824, 0x5ce4b85c, 0xc25d9fc2, 0xd36ebdd3, 0xacef43ac, 0x62a6c462, 0x91a83991, 0x95a43195, 0xe437d3e4, 0x798bf279, 0xe732d5e7, 0xc8438bc8, 0x37596e37, 0x6db7da6d, 0x8d8c018d, 0xd564b1d5, 0x4ed29c4e, 0xa9e049a9, 0x6cb4d86c, 0x56faac56, 0xf407f3f4, 0xea25cfea, 0x65afca65, 0x7a8ef47a, 0xaee947ae, 0x08181008, 0xbad56fba, 0x7888f078, 0x256f4a25, 0x2e725c2e, 0x1c24381c, 0xa6f157a6, 0xb4c773b4, 0xc65197c6, 0xe823cbe8, 0xdd7ca1dd, 0x749ce874, 0x1f213e1f, 0x4bdd964b, 0xbddc61bd, 0x8b860d8b, 0x8a850f8a, 0x7090e070, 0x3e427c3e, 0xb5c471b5, 0x66aacc66, 0x48d89048, 0x03050603, 0xf601f7f6, 0x0e121c0e, 0x61a3c261, 0x355f6a35, 0x57f9ae57, 0xb9d069b9, 0x86911786, 0xc15899c1, 0x1d273a1d, 0x9eb9279e, 0xe138d9e1, 0xf813ebf8, 0x98b32b98, 0x11332211, 0x69bbd269, 0xd970a9d9, 0x8e89078e, 0x94a73394, 0x9bb62d9b, 0x1e223c1e, 0x87921587, 0xe920c9e9, 0xce4987ce, 0x55ffaa55, 0x28785028, 0xdf7aa5df, 0x8c8f038c, 0xa1f859a1, 0x89800989, 0x0d171a0d, 0xbfda65bf, 0xe631d7e6, 0x42c68442, 0x68b8d068, 0x41c38241, 0x99b02999, 0x2d775a2d, 0x0f111e0f, 0xb0cb7bb0, 0x54fca854, 0xbbd66dbb, 0x163a2c16 ] + T4 = [ 0x6363a5c6, 0x7c7c84f8, 0x777799ee, 0x7b7b8df6, 0xf2f20dff, 0x6b6bbdd6, 0x6f6fb1de, 0xc5c55491, 0x30305060, 0x01010302, 0x6767a9ce, 0x2b2b7d56, 0xfefe19e7, 0xd7d762b5, 0xababe64d, 0x76769aec, 0xcaca458f, 0x82829d1f, 0xc9c94089, 0x7d7d87fa, 0xfafa15ef, 0x5959ebb2, 0x4747c98e, 0xf0f00bfb, 0xadadec41, 0xd4d467b3, 0xa2a2fd5f, 0xafafea45, 0x9c9cbf23, 0xa4a4f753, 0x727296e4, 0xc0c05b9b, 0xb7b7c275, 0xfdfd1ce1, 0x9393ae3d, 0x26266a4c, 0x36365a6c, 0x3f3f417e, 0xf7f702f5, 0xcccc4f83, 0x34345c68, 0xa5a5f451, 0xe5e534d1, 0xf1f108f9, 0x717193e2, 0xd8d873ab, 0x31315362, 0x15153f2a, 0x04040c08, 0xc7c75295, 0x23236546, 0xc3c35e9d, 0x18182830, 0x9696a137, 0x05050f0a, 0x9a9ab52f, 0x0707090e, 0x12123624, 0x80809b1b, 0xe2e23ddf, 0xebeb26cd, 0x2727694e, 0xb2b2cd7f, 0x75759fea, 0x09091b12, 0x83839e1d, 0x2c2c7458, 0x1a1a2e34, 0x1b1b2d36, 0x6e6eb2dc, 0x5a5aeeb4, 0xa0a0fb5b, 0x5252f6a4, 0x3b3b4d76, 0xd6d661b7, 0xb3b3ce7d, 0x29297b52, 0xe3e33edd, 0x2f2f715e, 0x84849713, 0x5353f5a6, 0xd1d168b9, 0x00000000, 0xeded2cc1, 0x20206040, 0xfcfc1fe3, 0xb1b1c879, 0x5b5bedb6, 0x6a6abed4, 0xcbcb468d, 0xbebed967, 0x39394b72, 0x4a4ade94, 0x4c4cd498, 0x5858e8b0, 0xcfcf4a85, 0xd0d06bbb, 0xefef2ac5, 0xaaaae54f, 0xfbfb16ed, 0x4343c586, 0x4d4dd79a, 0x33335566, 0x85859411, 0x4545cf8a, 0xf9f910e9, 0x02020604, 0x7f7f81fe, 0x5050f0a0, 0x3c3c4478, 0x9f9fba25, 0xa8a8e34b, 0x5151f3a2, 0xa3a3fe5d, 0x4040c080, 0x8f8f8a05, 0x9292ad3f, 0x9d9dbc21, 0x38384870, 0xf5f504f1, 0xbcbcdf63, 0xb6b6c177, 0xdada75af, 0x21216342, 0x10103020, 0xffff1ae5, 0xf3f30efd, 0xd2d26dbf, 0xcdcd4c81, 0x0c0c1418, 0x13133526, 0xecec2fc3, 0x5f5fe1be, 0x9797a235, 0x4444cc88, 0x1717392e, 0xc4c45793, 0xa7a7f255, 0x7e7e82fc, 0x3d3d477a, 0x6464acc8, 0x5d5de7ba, 0x19192b32, 0x737395e6, 0x6060a0c0, 0x81819819, 0x4f4fd19e, 0xdcdc7fa3, 0x22226644, 0x2a2a7e54, 0x9090ab3b, 0x8888830b, 0x4646ca8c, 0xeeee29c7, 0xb8b8d36b, 0x14143c28, 0xdede79a7, 0x5e5ee2bc, 0x0b0b1d16, 0xdbdb76ad, 0xe0e03bdb, 0x32325664, 0x3a3a4e74, 0x0a0a1e14, 0x4949db92, 0x06060a0c, 0x24246c48, 0x5c5ce4b8, 0xc2c25d9f, 0xd3d36ebd, 0xacacef43, 0x6262a6c4, 0x9191a839, 0x9595a431, 0xe4e437d3, 0x79798bf2, 0xe7e732d5, 0xc8c8438b, 0x3737596e, 0x6d6db7da, 0x8d8d8c01, 0xd5d564b1, 0x4e4ed29c, 0xa9a9e049, 0x6c6cb4d8, 0x5656faac, 0xf4f407f3, 0xeaea25cf, 0x6565afca, 0x7a7a8ef4, 0xaeaee947, 0x08081810, 0xbabad56f, 0x787888f0, 0x25256f4a, 0x2e2e725c, 0x1c1c2438, 0xa6a6f157, 0xb4b4c773, 0xc6c65197, 0xe8e823cb, 0xdddd7ca1, 0x74749ce8, 0x1f1f213e, 0x4b4bdd96, 0xbdbddc61, 0x8b8b860d, 0x8a8a850f, 0x707090e0, 0x3e3e427c, 0xb5b5c471, 0x6666aacc, 0x4848d890, 0x03030506, 0xf6f601f7, 0x0e0e121c, 0x6161a3c2, 0x35355f6a, 0x5757f9ae, 0xb9b9d069, 0x86869117, 0xc1c15899, 0x1d1d273a, 0x9e9eb927, 0xe1e138d9, 0xf8f813eb, 0x9898b32b, 0x11113322, 0x6969bbd2, 0xd9d970a9, 0x8e8e8907, 0x9494a733, 0x9b9bb62d, 0x1e1e223c, 0x87879215, 0xe9e920c9, 0xcece4987, 0x5555ffaa, 0x28287850, 0xdfdf7aa5, 0x8c8c8f03, 0xa1a1f859, 0x89898009, 0x0d0d171a, 0xbfbfda65, 0xe6e631d7, 0x4242c684, 0x6868b8d0, 0x4141c382, 0x9999b029, 0x2d2d775a, 0x0f0f111e, 0xb0b0cb7b, 0x5454fca8, 0xbbbbd66d, 0x16163a2c ] + + # Transformations for decryption + T5 = [ 0x51f4a750, 0x7e416553, 0x1a17a4c3, 0x3a275e96, 0x3bab6bcb, 0x1f9d45f1, 0xacfa58ab, 0x4be30393, 0x2030fa55, 0xad766df6, 0x88cc7691, 0xf5024c25, 0x4fe5d7fc, 0xc52acbd7, 0x26354480, 0xb562a38f, 0xdeb15a49, 0x25ba1b67, 0x45ea0e98, 0x5dfec0e1, 0xc32f7502, 0x814cf012, 0x8d4697a3, 0x6bd3f9c6, 0x038f5fe7, 0x15929c95, 0xbf6d7aeb, 0x955259da, 0xd4be832d, 0x587421d3, 0x49e06929, 0x8ec9c844, 0x75c2896a, 0xf48e7978, 0x99583e6b, 0x27b971dd, 0xbee14fb6, 0xf088ad17, 0xc920ac66, 0x7dce3ab4, 0x63df4a18, 0xe51a3182, 0x97513360, 0x62537f45, 0xb16477e0, 0xbb6bae84, 0xfe81a01c, 0xf9082b94, 0x70486858, 0x8f45fd19, 0x94de6c87, 0x527bf8b7, 0xab73d323, 0x724b02e2, 0xe31f8f57, 0x6655ab2a, 0xb2eb2807, 0x2fb5c203, 0x86c57b9a, 0xd33708a5, 0x302887f2, 0x23bfa5b2, 0x02036aba, 0xed16825c, 0x8acf1c2b, 0xa779b492, 0xf307f2f0, 0x4e69e2a1, 0x65daf4cd, 0x0605bed5, 0xd134621f, 0xc4a6fe8a, 0x342e539d, 0xa2f355a0, 0x058ae132, 0xa4f6eb75, 0x0b83ec39, 0x4060efaa, 0x5e719f06, 0xbd6e1051, 0x3e218af9, 0x96dd063d, 0xdd3e05ae, 0x4de6bd46, 0x91548db5, 0x71c45d05, 0x0406d46f, 0x605015ff, 0x1998fb24, 0xd6bde997, 0x894043cc, 0x67d99e77, 0xb0e842bd, 0x07898b88, 0xe7195b38, 0x79c8eedb, 0xa17c0a47, 0x7c420fe9, 0xf8841ec9, 0x00000000, 0x09808683, 0x322bed48, 0x1e1170ac, 0x6c5a724e, 0xfd0efffb, 0x0f853856, 0x3daed51e, 0x362d3927, 0x0a0fd964, 0x685ca621, 0x9b5b54d1, 0x24362e3a, 0x0c0a67b1, 0x9357e70f, 0xb4ee96d2, 0x1b9b919e, 0x80c0c54f, 0x61dc20a2, 0x5a774b69, 0x1c121a16, 0xe293ba0a, 0xc0a02ae5, 0x3c22e043, 0x121b171d, 0x0e090d0b, 0xf28bc7ad, 0x2db6a8b9, 0x141ea9c8, 0x57f11985, 0xaf75074c, 0xee99ddbb, 0xa37f60fd, 0xf701269f, 0x5c72f5bc, 0x44663bc5, 0x5bfb7e34, 0x8b432976, 0xcb23c6dc, 0xb6edfc68, 0xb8e4f163, 0xd731dcca, 0x42638510, 0x13972240, 0x84c61120, 0x854a247d, 0xd2bb3df8, 0xaef93211, 0xc729a16d, 0x1d9e2f4b, 0xdcb230f3, 0x0d8652ec, 0x77c1e3d0, 0x2bb3166c, 0xa970b999, 0x119448fa, 0x47e96422, 0xa8fc8cc4, 0xa0f03f1a, 0x567d2cd8, 0x223390ef, 0x87494ec7, 0xd938d1c1, 0x8ccaa2fe, 0x98d40b36, 0xa6f581cf, 0xa57ade28, 0xdab78e26, 0x3fadbfa4, 0x2c3a9de4, 0x5078920d, 0x6a5fcc9b, 0x547e4662, 0xf68d13c2, 0x90d8b8e8, 0x2e39f75e, 0x82c3aff5, 0x9f5d80be, 0x69d0937c, 0x6fd52da9, 0xcf2512b3, 0xc8ac993b, 0x10187da7, 0xe89c636e, 0xdb3bbb7b, 0xcd267809, 0x6e5918f4, 0xec9ab701, 0x834f9aa8, 0xe6956e65, 0xaaffe67e, 0x21bccf08, 0xef15e8e6, 0xbae79bd9, 0x4a6f36ce, 0xea9f09d4, 0x29b07cd6, 0x31a4b2af, 0x2a3f2331, 0xc6a59430, 0x35a266c0, 0x744ebc37, 0xfc82caa6, 0xe090d0b0, 0x33a7d815, 0xf104984a, 0x41ecdaf7, 0x7fcd500e, 0x1791f62f, 0x764dd68d, 0x43efb04d, 0xccaa4d54, 0xe49604df, 0x9ed1b5e3, 0x4c6a881b, 0xc12c1fb8, 0x4665517f, 0x9d5eea04, 0x018c355d, 0xfa877473, 0xfb0b412e, 0xb3671d5a, 0x92dbd252, 0xe9105633, 0x6dd64713, 0x9ad7618c, 0x37a10c7a, 0x59f8148e, 0xeb133c89, 0xcea927ee, 0xb761c935, 0xe11ce5ed, 0x7a47b13c, 0x9cd2df59, 0x55f2733f, 0x1814ce79, 0x73c737bf, 0x53f7cdea, 0x5ffdaa5b, 0xdf3d6f14, 0x7844db86, 0xcaaff381, 0xb968c43e, 0x3824342c, 0xc2a3405f, 0x161dc372, 0xbce2250c, 0x283c498b, 0xff0d9541, 0x39a80171, 0x080cb3de, 0xd8b4e49c, 0x6456c190, 0x7bcb8461, 0xd532b670, 0x486c5c74, 0xd0b85742 ] + T6 = [ 0x5051f4a7, 0x537e4165, 0xc31a17a4, 0x963a275e, 0xcb3bab6b, 0xf11f9d45, 0xabacfa58, 0x934be303, 0x552030fa, 0xf6ad766d, 0x9188cc76, 0x25f5024c, 0xfc4fe5d7, 0xd7c52acb, 0x80263544, 0x8fb562a3, 0x49deb15a, 0x6725ba1b, 0x9845ea0e, 0xe15dfec0, 0x02c32f75, 0x12814cf0, 0xa38d4697, 0xc66bd3f9, 0xe7038f5f, 0x9515929c, 0xebbf6d7a, 0xda955259, 0x2dd4be83, 0xd3587421, 0x2949e069, 0x448ec9c8, 0x6a75c289, 0x78f48e79, 0x6b99583e, 0xdd27b971, 0xb6bee14f, 0x17f088ad, 0x66c920ac, 0xb47dce3a, 0x1863df4a, 0x82e51a31, 0x60975133, 0x4562537f, 0xe0b16477, 0x84bb6bae, 0x1cfe81a0, 0x94f9082b, 0x58704868, 0x198f45fd, 0x8794de6c, 0xb7527bf8, 0x23ab73d3, 0xe2724b02, 0x57e31f8f, 0x2a6655ab, 0x07b2eb28, 0x032fb5c2, 0x9a86c57b, 0xa5d33708, 0xf2302887, 0xb223bfa5, 0xba02036a, 0x5ced1682, 0x2b8acf1c, 0x92a779b4, 0xf0f307f2, 0xa14e69e2, 0xcd65daf4, 0xd50605be, 0x1fd13462, 0x8ac4a6fe, 0x9d342e53, 0xa0a2f355, 0x32058ae1, 0x75a4f6eb, 0x390b83ec, 0xaa4060ef, 0x065e719f, 0x51bd6e10, 0xf93e218a, 0x3d96dd06, 0xaedd3e05, 0x464de6bd, 0xb591548d, 0x0571c45d, 0x6f0406d4, 0xff605015, 0x241998fb, 0x97d6bde9, 0xcc894043, 0x7767d99e, 0xbdb0e842, 0x8807898b, 0x38e7195b, 0xdb79c8ee, 0x47a17c0a, 0xe97c420f, 0xc9f8841e, 0x00000000, 0x83098086, 0x48322bed, 0xac1e1170, 0x4e6c5a72, 0xfbfd0eff, 0x560f8538, 0x1e3daed5, 0x27362d39, 0x640a0fd9, 0x21685ca6, 0xd19b5b54, 0x3a24362e, 0xb10c0a67, 0x0f9357e7, 0xd2b4ee96, 0x9e1b9b91, 0x4f80c0c5, 0xa261dc20, 0x695a774b, 0x161c121a, 0x0ae293ba, 0xe5c0a02a, 0x433c22e0, 0x1d121b17, 0x0b0e090d, 0xadf28bc7, 0xb92db6a8, 0xc8141ea9, 0x8557f119, 0x4caf7507, 0xbbee99dd, 0xfda37f60, 0x9ff70126, 0xbc5c72f5, 0xc544663b, 0x345bfb7e, 0x768b4329, 0xdccb23c6, 0x68b6edfc, 0x63b8e4f1, 0xcad731dc, 0x10426385, 0x40139722, 0x2084c611, 0x7d854a24, 0xf8d2bb3d, 0x11aef932, 0x6dc729a1, 0x4b1d9e2f, 0xf3dcb230, 0xec0d8652, 0xd077c1e3, 0x6c2bb316, 0x99a970b9, 0xfa119448, 0x2247e964, 0xc4a8fc8c, 0x1aa0f03f, 0xd8567d2c, 0xef223390, 0xc787494e, 0xc1d938d1, 0xfe8ccaa2, 0x3698d40b, 0xcfa6f581, 0x28a57ade, 0x26dab78e, 0xa43fadbf, 0xe42c3a9d, 0x0d507892, 0x9b6a5fcc, 0x62547e46, 0xc2f68d13, 0xe890d8b8, 0x5e2e39f7, 0xf582c3af, 0xbe9f5d80, 0x7c69d093, 0xa96fd52d, 0xb3cf2512, 0x3bc8ac99, 0xa710187d, 0x6ee89c63, 0x7bdb3bbb, 0x09cd2678, 0xf46e5918, 0x01ec9ab7, 0xa8834f9a, 0x65e6956e, 0x7eaaffe6, 0x0821bccf, 0xe6ef15e8, 0xd9bae79b, 0xce4a6f36, 0xd4ea9f09, 0xd629b07c, 0xaf31a4b2, 0x312a3f23, 0x30c6a594, 0xc035a266, 0x37744ebc, 0xa6fc82ca, 0xb0e090d0, 0x1533a7d8, 0x4af10498, 0xf741ecda, 0x0e7fcd50, 0x2f1791f6, 0x8d764dd6, 0x4d43efb0, 0x54ccaa4d, 0xdfe49604, 0xe39ed1b5, 0x1b4c6a88, 0xb8c12c1f, 0x7f466551, 0x049d5eea, 0x5d018c35, 0x73fa8774, 0x2efb0b41, 0x5ab3671d, 0x5292dbd2, 0x33e91056, 0x136dd647, 0x8c9ad761, 0x7a37a10c, 0x8e59f814, 0x89eb133c, 0xeecea927, 0x35b761c9, 0xede11ce5, 0x3c7a47b1, 0x599cd2df, 0x3f55f273, 0x791814ce, 0xbf73c737, 0xea53f7cd, 0x5b5ffdaa, 0x14df3d6f, 0x867844db, 0x81caaff3, 0x3eb968c4, 0x2c382434, 0x5fc2a340, 0x72161dc3, 0x0cbce225, 0x8b283c49, 0x41ff0d95, 0x7139a801, 0xde080cb3, 0x9cd8b4e4, 0x906456c1, 0x617bcb84, 0x70d532b6, 0x74486c5c, 0x42d0b857 ] + T7 = [ 0xa75051f4, 0x65537e41, 0xa4c31a17, 0x5e963a27, 0x6bcb3bab, 0x45f11f9d, 0x58abacfa, 0x03934be3, 0xfa552030, 0x6df6ad76, 0x769188cc, 0x4c25f502, 0xd7fc4fe5, 0xcbd7c52a, 0x44802635, 0xa38fb562, 0x5a49deb1, 0x1b6725ba, 0x0e9845ea, 0xc0e15dfe, 0x7502c32f, 0xf012814c, 0x97a38d46, 0xf9c66bd3, 0x5fe7038f, 0x9c951592, 0x7aebbf6d, 0x59da9552, 0x832dd4be, 0x21d35874, 0x692949e0, 0xc8448ec9, 0x896a75c2, 0x7978f48e, 0x3e6b9958, 0x71dd27b9, 0x4fb6bee1, 0xad17f088, 0xac66c920, 0x3ab47dce, 0x4a1863df, 0x3182e51a, 0x33609751, 0x7f456253, 0x77e0b164, 0xae84bb6b, 0xa01cfe81, 0x2b94f908, 0x68587048, 0xfd198f45, 0x6c8794de, 0xf8b7527b, 0xd323ab73, 0x02e2724b, 0x8f57e31f, 0xab2a6655, 0x2807b2eb, 0xc2032fb5, 0x7b9a86c5, 0x08a5d337, 0x87f23028, 0xa5b223bf, 0x6aba0203, 0x825ced16, 0x1c2b8acf, 0xb492a779, 0xf2f0f307, 0xe2a14e69, 0xf4cd65da, 0xbed50605, 0x621fd134, 0xfe8ac4a6, 0x539d342e, 0x55a0a2f3, 0xe132058a, 0xeb75a4f6, 0xec390b83, 0xefaa4060, 0x9f065e71, 0x1051bd6e, 0x8af93e21, 0x063d96dd, 0x05aedd3e, 0xbd464de6, 0x8db59154, 0x5d0571c4, 0xd46f0406, 0x15ff6050, 0xfb241998, 0xe997d6bd, 0x43cc8940, 0x9e7767d9, 0x42bdb0e8, 0x8b880789, 0x5b38e719, 0xeedb79c8, 0x0a47a17c, 0x0fe97c42, 0x1ec9f884, 0x00000000, 0x86830980, 0xed48322b, 0x70ac1e11, 0x724e6c5a, 0xfffbfd0e, 0x38560f85, 0xd51e3dae, 0x3927362d, 0xd9640a0f, 0xa621685c, 0x54d19b5b, 0x2e3a2436, 0x67b10c0a, 0xe70f9357, 0x96d2b4ee, 0x919e1b9b, 0xc54f80c0, 0x20a261dc, 0x4b695a77, 0x1a161c12, 0xba0ae293, 0x2ae5c0a0, 0xe0433c22, 0x171d121b, 0x0d0b0e09, 0xc7adf28b, 0xa8b92db6, 0xa9c8141e, 0x198557f1, 0x074caf75, 0xddbbee99, 0x60fda37f, 0x269ff701, 0xf5bc5c72, 0x3bc54466, 0x7e345bfb, 0x29768b43, 0xc6dccb23, 0xfc68b6ed, 0xf163b8e4, 0xdccad731, 0x85104263, 0x22401397, 0x112084c6, 0x247d854a, 0x3df8d2bb, 0x3211aef9, 0xa16dc729, 0x2f4b1d9e, 0x30f3dcb2, 0x52ec0d86, 0xe3d077c1, 0x166c2bb3, 0xb999a970, 0x48fa1194, 0x642247e9, 0x8cc4a8fc, 0x3f1aa0f0, 0x2cd8567d, 0x90ef2233, 0x4ec78749, 0xd1c1d938, 0xa2fe8cca, 0x0b3698d4, 0x81cfa6f5, 0xde28a57a, 0x8e26dab7, 0xbfa43fad, 0x9de42c3a, 0x920d5078, 0xcc9b6a5f, 0x4662547e, 0x13c2f68d, 0xb8e890d8, 0xf75e2e39, 0xaff582c3, 0x80be9f5d, 0x937c69d0, 0x2da96fd5, 0x12b3cf25, 0x993bc8ac, 0x7da71018, 0x636ee89c, 0xbb7bdb3b, 0x7809cd26, 0x18f46e59, 0xb701ec9a, 0x9aa8834f, 0x6e65e695, 0xe67eaaff, 0xcf0821bc, 0xe8e6ef15, 0x9bd9bae7, 0x36ce4a6f, 0x09d4ea9f, 0x7cd629b0, 0xb2af31a4, 0x23312a3f, 0x9430c6a5, 0x66c035a2, 0xbc37744e, 0xcaa6fc82, 0xd0b0e090, 0xd81533a7, 0x984af104, 0xdaf741ec, 0x500e7fcd, 0xf62f1791, 0xd68d764d, 0xb04d43ef, 0x4d54ccaa, 0x04dfe496, 0xb5e39ed1, 0x881b4c6a, 0x1fb8c12c, 0x517f4665, 0xea049d5e, 0x355d018c, 0x7473fa87, 0x412efb0b, 0x1d5ab367, 0xd25292db, 0x5633e910, 0x47136dd6, 0x618c9ad7, 0x0c7a37a1, 0x148e59f8, 0x3c89eb13, 0x27eecea9, 0xc935b761, 0xe5ede11c, 0xb13c7a47, 0xdf599cd2, 0x733f55f2, 0xce791814, 0x37bf73c7, 0xcdea53f7, 0xaa5b5ffd, 0x6f14df3d, 0xdb867844, 0xf381caaf, 0xc43eb968, 0x342c3824, 0x405fc2a3, 0xc372161d, 0x250cbce2, 0x498b283c, 0x9541ff0d, 0x017139a8, 0xb3de080c, 0xe49cd8b4, 0xc1906456, 0x84617bcb, 0xb670d532, 0x5c74486c, 0x5742d0b8 ] + T8 = [ 0xf4a75051, 0x4165537e, 0x17a4c31a, 0x275e963a, 0xab6bcb3b, 0x9d45f11f, 0xfa58abac, 0xe303934b, 0x30fa5520, 0x766df6ad, 0xcc769188, 0x024c25f5, 0xe5d7fc4f, 0x2acbd7c5, 0x35448026, 0x62a38fb5, 0xb15a49de, 0xba1b6725, 0xea0e9845, 0xfec0e15d, 0x2f7502c3, 0x4cf01281, 0x4697a38d, 0xd3f9c66b, 0x8f5fe703, 0x929c9515, 0x6d7aebbf, 0x5259da95, 0xbe832dd4, 0x7421d358, 0xe0692949, 0xc9c8448e, 0xc2896a75, 0x8e7978f4, 0x583e6b99, 0xb971dd27, 0xe14fb6be, 0x88ad17f0, 0x20ac66c9, 0xce3ab47d, 0xdf4a1863, 0x1a3182e5, 0x51336097, 0x537f4562, 0x6477e0b1, 0x6bae84bb, 0x81a01cfe, 0x082b94f9, 0x48685870, 0x45fd198f, 0xde6c8794, 0x7bf8b752, 0x73d323ab, 0x4b02e272, 0x1f8f57e3, 0x55ab2a66, 0xeb2807b2, 0xb5c2032f, 0xc57b9a86, 0x3708a5d3, 0x2887f230, 0xbfa5b223, 0x036aba02, 0x16825ced, 0xcf1c2b8a, 0x79b492a7, 0x07f2f0f3, 0x69e2a14e, 0xdaf4cd65, 0x05bed506, 0x34621fd1, 0xa6fe8ac4, 0x2e539d34, 0xf355a0a2, 0x8ae13205, 0xf6eb75a4, 0x83ec390b, 0x60efaa40, 0x719f065e, 0x6e1051bd, 0x218af93e, 0xdd063d96, 0x3e05aedd, 0xe6bd464d, 0x548db591, 0xc45d0571, 0x06d46f04, 0x5015ff60, 0x98fb2419, 0xbde997d6, 0x4043cc89, 0xd99e7767, 0xe842bdb0, 0x898b8807, 0x195b38e7, 0xc8eedb79, 0x7c0a47a1, 0x420fe97c, 0x841ec9f8, 0x00000000, 0x80868309, 0x2bed4832, 0x1170ac1e, 0x5a724e6c, 0x0efffbfd, 0x8538560f, 0xaed51e3d, 0x2d392736, 0x0fd9640a, 0x5ca62168, 0x5b54d19b, 0x362e3a24, 0x0a67b10c, 0x57e70f93, 0xee96d2b4, 0x9b919e1b, 0xc0c54f80, 0xdc20a261, 0x774b695a, 0x121a161c, 0x93ba0ae2, 0xa02ae5c0, 0x22e0433c, 0x1b171d12, 0x090d0b0e, 0x8bc7adf2, 0xb6a8b92d, 0x1ea9c814, 0xf1198557, 0x75074caf, 0x99ddbbee, 0x7f60fda3, 0x01269ff7, 0x72f5bc5c, 0x663bc544, 0xfb7e345b, 0x4329768b, 0x23c6dccb, 0xedfc68b6, 0xe4f163b8, 0x31dccad7, 0x63851042, 0x97224013, 0xc6112084, 0x4a247d85, 0xbb3df8d2, 0xf93211ae, 0x29a16dc7, 0x9e2f4b1d, 0xb230f3dc, 0x8652ec0d, 0xc1e3d077, 0xb3166c2b, 0x70b999a9, 0x9448fa11, 0xe9642247, 0xfc8cc4a8, 0xf03f1aa0, 0x7d2cd856, 0x3390ef22, 0x494ec787, 0x38d1c1d9, 0xcaa2fe8c, 0xd40b3698, 0xf581cfa6, 0x7ade28a5, 0xb78e26da, 0xadbfa43f, 0x3a9de42c, 0x78920d50, 0x5fcc9b6a, 0x7e466254, 0x8d13c2f6, 0xd8b8e890, 0x39f75e2e, 0xc3aff582, 0x5d80be9f, 0xd0937c69, 0xd52da96f, 0x2512b3cf, 0xac993bc8, 0x187da710, 0x9c636ee8, 0x3bbb7bdb, 0x267809cd, 0x5918f46e, 0x9ab701ec, 0x4f9aa883, 0x956e65e6, 0xffe67eaa, 0xbccf0821, 0x15e8e6ef, 0xe79bd9ba, 0x6f36ce4a, 0x9f09d4ea, 0xb07cd629, 0xa4b2af31, 0x3f23312a, 0xa59430c6, 0xa266c035, 0x4ebc3774, 0x82caa6fc, 0x90d0b0e0, 0xa7d81533, 0x04984af1, 0xecdaf741, 0xcd500e7f, 0x91f62f17, 0x4dd68d76, 0xefb04d43, 0xaa4d54cc, 0x9604dfe4, 0xd1b5e39e, 0x6a881b4c, 0x2c1fb8c1, 0x65517f46, 0x5eea049d, 0x8c355d01, 0x877473fa, 0x0b412efb, 0x671d5ab3, 0xdbd25292, 0x105633e9, 0xd647136d, 0xd7618c9a, 0xa10c7a37, 0xf8148e59, 0x133c89eb, 0xa927eece, 0x61c935b7, 0x1ce5ede1, 0x47b13c7a, 0xd2df599c, 0xf2733f55, 0x14ce7918, 0xc737bf73, 0xf7cdea53, 0xfdaa5b5f, 0x3d6f14df, 0x44db8678, 0xaff381ca, 0x68c43eb9, 0x24342c38, 0xa3405fc2, 0x1dc37216, 0xe2250cbc, 0x3c498b28, 0x0d9541ff, 0xa8017139, 0x0cb3de08, 0xb4e49cd8, 0x56c19064, 0xcb84617b, 0x32b670d5, 0x6c5c7448, 0xb85742d0 ] + + # Transformations for decryption key expansion + U1 = [ 0x00000000, 0x0e090d0b, 0x1c121a16, 0x121b171d, 0x3824342c, 0x362d3927, 0x24362e3a, 0x2a3f2331, 0x70486858, 0x7e416553, 0x6c5a724e, 0x62537f45, 0x486c5c74, 0x4665517f, 0x547e4662, 0x5a774b69, 0xe090d0b0, 0xee99ddbb, 0xfc82caa6, 0xf28bc7ad, 0xd8b4e49c, 0xd6bde997, 0xc4a6fe8a, 0xcaaff381, 0x90d8b8e8, 0x9ed1b5e3, 0x8ccaa2fe, 0x82c3aff5, 0xa8fc8cc4, 0xa6f581cf, 0xb4ee96d2, 0xbae79bd9, 0xdb3bbb7b, 0xd532b670, 0xc729a16d, 0xc920ac66, 0xe31f8f57, 0xed16825c, 0xff0d9541, 0xf104984a, 0xab73d323, 0xa57ade28, 0xb761c935, 0xb968c43e, 0x9357e70f, 0x9d5eea04, 0x8f45fd19, 0x814cf012, 0x3bab6bcb, 0x35a266c0, 0x27b971dd, 0x29b07cd6, 0x038f5fe7, 0x0d8652ec, 0x1f9d45f1, 0x119448fa, 0x4be30393, 0x45ea0e98, 0x57f11985, 0x59f8148e, 0x73c737bf, 0x7dce3ab4, 0x6fd52da9, 0x61dc20a2, 0xad766df6, 0xa37f60fd, 0xb16477e0, 0xbf6d7aeb, 0x955259da, 0x9b5b54d1, 0x894043cc, 0x87494ec7, 0xdd3e05ae, 0xd33708a5, 0xc12c1fb8, 0xcf2512b3, 0xe51a3182, 0xeb133c89, 0xf9082b94, 0xf701269f, 0x4de6bd46, 0x43efb04d, 0x51f4a750, 0x5ffdaa5b, 0x75c2896a, 0x7bcb8461, 0x69d0937c, 0x67d99e77, 0x3daed51e, 0x33a7d815, 0x21bccf08, 0x2fb5c203, 0x058ae132, 0x0b83ec39, 0x1998fb24, 0x1791f62f, 0x764dd68d, 0x7844db86, 0x6a5fcc9b, 0x6456c190, 0x4e69e2a1, 0x4060efaa, 0x527bf8b7, 0x5c72f5bc, 0x0605bed5, 0x080cb3de, 0x1a17a4c3, 0x141ea9c8, 0x3e218af9, 0x302887f2, 0x223390ef, 0x2c3a9de4, 0x96dd063d, 0x98d40b36, 0x8acf1c2b, 0x84c61120, 0xaef93211, 0xa0f03f1a, 0xb2eb2807, 0xbce2250c, 0xe6956e65, 0xe89c636e, 0xfa877473, 0xf48e7978, 0xdeb15a49, 0xd0b85742, 0xc2a3405f, 0xccaa4d54, 0x41ecdaf7, 0x4fe5d7fc, 0x5dfec0e1, 0x53f7cdea, 0x79c8eedb, 0x77c1e3d0, 0x65daf4cd, 0x6bd3f9c6, 0x31a4b2af, 0x3fadbfa4, 0x2db6a8b9, 0x23bfa5b2, 0x09808683, 0x07898b88, 0x15929c95, 0x1b9b919e, 0xa17c0a47, 0xaf75074c, 0xbd6e1051, 0xb3671d5a, 0x99583e6b, 0x97513360, 0x854a247d, 0x8b432976, 0xd134621f, 0xdf3d6f14, 0xcd267809, 0xc32f7502, 0xe9105633, 0xe7195b38, 0xf5024c25, 0xfb0b412e, 0x9ad7618c, 0x94de6c87, 0x86c57b9a, 0x88cc7691, 0xa2f355a0, 0xacfa58ab, 0xbee14fb6, 0xb0e842bd, 0xea9f09d4, 0xe49604df, 0xf68d13c2, 0xf8841ec9, 0xd2bb3df8, 0xdcb230f3, 0xcea927ee, 0xc0a02ae5, 0x7a47b13c, 0x744ebc37, 0x6655ab2a, 0x685ca621, 0x42638510, 0x4c6a881b, 0x5e719f06, 0x5078920d, 0x0a0fd964, 0x0406d46f, 0x161dc372, 0x1814ce79, 0x322bed48, 0x3c22e043, 0x2e39f75e, 0x2030fa55, 0xec9ab701, 0xe293ba0a, 0xf088ad17, 0xfe81a01c, 0xd4be832d, 0xdab78e26, 0xc8ac993b, 0xc6a59430, 0x9cd2df59, 0x92dbd252, 0x80c0c54f, 0x8ec9c844, 0xa4f6eb75, 0xaaffe67e, 0xb8e4f163, 0xb6edfc68, 0x0c0a67b1, 0x02036aba, 0x10187da7, 0x1e1170ac, 0x342e539d, 0x3a275e96, 0x283c498b, 0x26354480, 0x7c420fe9, 0x724b02e2, 0x605015ff, 0x6e5918f4, 0x44663bc5, 0x4a6f36ce, 0x587421d3, 0x567d2cd8, 0x37a10c7a, 0x39a80171, 0x2bb3166c, 0x25ba1b67, 0x0f853856, 0x018c355d, 0x13972240, 0x1d9e2f4b, 0x47e96422, 0x49e06929, 0x5bfb7e34, 0x55f2733f, 0x7fcd500e, 0x71c45d05, 0x63df4a18, 0x6dd64713, 0xd731dcca, 0xd938d1c1, 0xcb23c6dc, 0xc52acbd7, 0xef15e8e6, 0xe11ce5ed, 0xf307f2f0, 0xfd0efffb, 0xa779b492, 0xa970b999, 0xbb6bae84, 0xb562a38f, 0x9f5d80be, 0x91548db5, 0x834f9aa8, 0x8d4697a3 ] + U2 = [ 0x00000000, 0x0b0e090d, 0x161c121a, 0x1d121b17, 0x2c382434, 0x27362d39, 0x3a24362e, 0x312a3f23, 0x58704868, 0x537e4165, 0x4e6c5a72, 0x4562537f, 0x74486c5c, 0x7f466551, 0x62547e46, 0x695a774b, 0xb0e090d0, 0xbbee99dd, 0xa6fc82ca, 0xadf28bc7, 0x9cd8b4e4, 0x97d6bde9, 0x8ac4a6fe, 0x81caaff3, 0xe890d8b8, 0xe39ed1b5, 0xfe8ccaa2, 0xf582c3af, 0xc4a8fc8c, 0xcfa6f581, 0xd2b4ee96, 0xd9bae79b, 0x7bdb3bbb, 0x70d532b6, 0x6dc729a1, 0x66c920ac, 0x57e31f8f, 0x5ced1682, 0x41ff0d95, 0x4af10498, 0x23ab73d3, 0x28a57ade, 0x35b761c9, 0x3eb968c4, 0x0f9357e7, 0x049d5eea, 0x198f45fd, 0x12814cf0, 0xcb3bab6b, 0xc035a266, 0xdd27b971, 0xd629b07c, 0xe7038f5f, 0xec0d8652, 0xf11f9d45, 0xfa119448, 0x934be303, 0x9845ea0e, 0x8557f119, 0x8e59f814, 0xbf73c737, 0xb47dce3a, 0xa96fd52d, 0xa261dc20, 0xf6ad766d, 0xfda37f60, 0xe0b16477, 0xebbf6d7a, 0xda955259, 0xd19b5b54, 0xcc894043, 0xc787494e, 0xaedd3e05, 0xa5d33708, 0xb8c12c1f, 0xb3cf2512, 0x82e51a31, 0x89eb133c, 0x94f9082b, 0x9ff70126, 0x464de6bd, 0x4d43efb0, 0x5051f4a7, 0x5b5ffdaa, 0x6a75c289, 0x617bcb84, 0x7c69d093, 0x7767d99e, 0x1e3daed5, 0x1533a7d8, 0x0821bccf, 0x032fb5c2, 0x32058ae1, 0x390b83ec, 0x241998fb, 0x2f1791f6, 0x8d764dd6, 0x867844db, 0x9b6a5fcc, 0x906456c1, 0xa14e69e2, 0xaa4060ef, 0xb7527bf8, 0xbc5c72f5, 0xd50605be, 0xde080cb3, 0xc31a17a4, 0xc8141ea9, 0xf93e218a, 0xf2302887, 0xef223390, 0xe42c3a9d, 0x3d96dd06, 0x3698d40b, 0x2b8acf1c, 0x2084c611, 0x11aef932, 0x1aa0f03f, 0x07b2eb28, 0x0cbce225, 0x65e6956e, 0x6ee89c63, 0x73fa8774, 0x78f48e79, 0x49deb15a, 0x42d0b857, 0x5fc2a340, 0x54ccaa4d, 0xf741ecda, 0xfc4fe5d7, 0xe15dfec0, 0xea53f7cd, 0xdb79c8ee, 0xd077c1e3, 0xcd65daf4, 0xc66bd3f9, 0xaf31a4b2, 0xa43fadbf, 0xb92db6a8, 0xb223bfa5, 0x83098086, 0x8807898b, 0x9515929c, 0x9e1b9b91, 0x47a17c0a, 0x4caf7507, 0x51bd6e10, 0x5ab3671d, 0x6b99583e, 0x60975133, 0x7d854a24, 0x768b4329, 0x1fd13462, 0x14df3d6f, 0x09cd2678, 0x02c32f75, 0x33e91056, 0x38e7195b, 0x25f5024c, 0x2efb0b41, 0x8c9ad761, 0x8794de6c, 0x9a86c57b, 0x9188cc76, 0xa0a2f355, 0xabacfa58, 0xb6bee14f, 0xbdb0e842, 0xd4ea9f09, 0xdfe49604, 0xc2f68d13, 0xc9f8841e, 0xf8d2bb3d, 0xf3dcb230, 0xeecea927, 0xe5c0a02a, 0x3c7a47b1, 0x37744ebc, 0x2a6655ab, 0x21685ca6, 0x10426385, 0x1b4c6a88, 0x065e719f, 0x0d507892, 0x640a0fd9, 0x6f0406d4, 0x72161dc3, 0x791814ce, 0x48322bed, 0x433c22e0, 0x5e2e39f7, 0x552030fa, 0x01ec9ab7, 0x0ae293ba, 0x17f088ad, 0x1cfe81a0, 0x2dd4be83, 0x26dab78e, 0x3bc8ac99, 0x30c6a594, 0x599cd2df, 0x5292dbd2, 0x4f80c0c5, 0x448ec9c8, 0x75a4f6eb, 0x7eaaffe6, 0x63b8e4f1, 0x68b6edfc, 0xb10c0a67, 0xba02036a, 0xa710187d, 0xac1e1170, 0x9d342e53, 0x963a275e, 0x8b283c49, 0x80263544, 0xe97c420f, 0xe2724b02, 0xff605015, 0xf46e5918, 0xc544663b, 0xce4a6f36, 0xd3587421, 0xd8567d2c, 0x7a37a10c, 0x7139a801, 0x6c2bb316, 0x6725ba1b, 0x560f8538, 0x5d018c35, 0x40139722, 0x4b1d9e2f, 0x2247e964, 0x2949e069, 0x345bfb7e, 0x3f55f273, 0x0e7fcd50, 0x0571c45d, 0x1863df4a, 0x136dd647, 0xcad731dc, 0xc1d938d1, 0xdccb23c6, 0xd7c52acb, 0xe6ef15e8, 0xede11ce5, 0xf0f307f2, 0xfbfd0eff, 0x92a779b4, 0x99a970b9, 0x84bb6bae, 0x8fb562a3, 0xbe9f5d80, 0xb591548d, 0xa8834f9a, 0xa38d4697 ] + U3 = [ 0x00000000, 0x0d0b0e09, 0x1a161c12, 0x171d121b, 0x342c3824, 0x3927362d, 0x2e3a2436, 0x23312a3f, 0x68587048, 0x65537e41, 0x724e6c5a, 0x7f456253, 0x5c74486c, 0x517f4665, 0x4662547e, 0x4b695a77, 0xd0b0e090, 0xddbbee99, 0xcaa6fc82, 0xc7adf28b, 0xe49cd8b4, 0xe997d6bd, 0xfe8ac4a6, 0xf381caaf, 0xb8e890d8, 0xb5e39ed1, 0xa2fe8cca, 0xaff582c3, 0x8cc4a8fc, 0x81cfa6f5, 0x96d2b4ee, 0x9bd9bae7, 0xbb7bdb3b, 0xb670d532, 0xa16dc729, 0xac66c920, 0x8f57e31f, 0x825ced16, 0x9541ff0d, 0x984af104, 0xd323ab73, 0xde28a57a, 0xc935b761, 0xc43eb968, 0xe70f9357, 0xea049d5e, 0xfd198f45, 0xf012814c, 0x6bcb3bab, 0x66c035a2, 0x71dd27b9, 0x7cd629b0, 0x5fe7038f, 0x52ec0d86, 0x45f11f9d, 0x48fa1194, 0x03934be3, 0x0e9845ea, 0x198557f1, 0x148e59f8, 0x37bf73c7, 0x3ab47dce, 0x2da96fd5, 0x20a261dc, 0x6df6ad76, 0x60fda37f, 0x77e0b164, 0x7aebbf6d, 0x59da9552, 0x54d19b5b, 0x43cc8940, 0x4ec78749, 0x05aedd3e, 0x08a5d337, 0x1fb8c12c, 0x12b3cf25, 0x3182e51a, 0x3c89eb13, 0x2b94f908, 0x269ff701, 0xbd464de6, 0xb04d43ef, 0xa75051f4, 0xaa5b5ffd, 0x896a75c2, 0x84617bcb, 0x937c69d0, 0x9e7767d9, 0xd51e3dae, 0xd81533a7, 0xcf0821bc, 0xc2032fb5, 0xe132058a, 0xec390b83, 0xfb241998, 0xf62f1791, 0xd68d764d, 0xdb867844, 0xcc9b6a5f, 0xc1906456, 0xe2a14e69, 0xefaa4060, 0xf8b7527b, 0xf5bc5c72, 0xbed50605, 0xb3de080c, 0xa4c31a17, 0xa9c8141e, 0x8af93e21, 0x87f23028, 0x90ef2233, 0x9de42c3a, 0x063d96dd, 0x0b3698d4, 0x1c2b8acf, 0x112084c6, 0x3211aef9, 0x3f1aa0f0, 0x2807b2eb, 0x250cbce2, 0x6e65e695, 0x636ee89c, 0x7473fa87, 0x7978f48e, 0x5a49deb1, 0x5742d0b8, 0x405fc2a3, 0x4d54ccaa, 0xdaf741ec, 0xd7fc4fe5, 0xc0e15dfe, 0xcdea53f7, 0xeedb79c8, 0xe3d077c1, 0xf4cd65da, 0xf9c66bd3, 0xb2af31a4, 0xbfa43fad, 0xa8b92db6, 0xa5b223bf, 0x86830980, 0x8b880789, 0x9c951592, 0x919e1b9b, 0x0a47a17c, 0x074caf75, 0x1051bd6e, 0x1d5ab367, 0x3e6b9958, 0x33609751, 0x247d854a, 0x29768b43, 0x621fd134, 0x6f14df3d, 0x7809cd26, 0x7502c32f, 0x5633e910, 0x5b38e719, 0x4c25f502, 0x412efb0b, 0x618c9ad7, 0x6c8794de, 0x7b9a86c5, 0x769188cc, 0x55a0a2f3, 0x58abacfa, 0x4fb6bee1, 0x42bdb0e8, 0x09d4ea9f, 0x04dfe496, 0x13c2f68d, 0x1ec9f884, 0x3df8d2bb, 0x30f3dcb2, 0x27eecea9, 0x2ae5c0a0, 0xb13c7a47, 0xbc37744e, 0xab2a6655, 0xa621685c, 0x85104263, 0x881b4c6a, 0x9f065e71, 0x920d5078, 0xd9640a0f, 0xd46f0406, 0xc372161d, 0xce791814, 0xed48322b, 0xe0433c22, 0xf75e2e39, 0xfa552030, 0xb701ec9a, 0xba0ae293, 0xad17f088, 0xa01cfe81, 0x832dd4be, 0x8e26dab7, 0x993bc8ac, 0x9430c6a5, 0xdf599cd2, 0xd25292db, 0xc54f80c0, 0xc8448ec9, 0xeb75a4f6, 0xe67eaaff, 0xf163b8e4, 0xfc68b6ed, 0x67b10c0a, 0x6aba0203, 0x7da71018, 0x70ac1e11, 0x539d342e, 0x5e963a27, 0x498b283c, 0x44802635, 0x0fe97c42, 0x02e2724b, 0x15ff6050, 0x18f46e59, 0x3bc54466, 0x36ce4a6f, 0x21d35874, 0x2cd8567d, 0x0c7a37a1, 0x017139a8, 0x166c2bb3, 0x1b6725ba, 0x38560f85, 0x355d018c, 0x22401397, 0x2f4b1d9e, 0x642247e9, 0x692949e0, 0x7e345bfb, 0x733f55f2, 0x500e7fcd, 0x5d0571c4, 0x4a1863df, 0x47136dd6, 0xdccad731, 0xd1c1d938, 0xc6dccb23, 0xcbd7c52a, 0xe8e6ef15, 0xe5ede11c, 0xf2f0f307, 0xfffbfd0e, 0xb492a779, 0xb999a970, 0xae84bb6b, 0xa38fb562, 0x80be9f5d, 0x8db59154, 0x9aa8834f, 0x97a38d46 ] + U4 = [ 0x00000000, 0x090d0b0e, 0x121a161c, 0x1b171d12, 0x24342c38, 0x2d392736, 0x362e3a24, 0x3f23312a, 0x48685870, 0x4165537e, 0x5a724e6c, 0x537f4562, 0x6c5c7448, 0x65517f46, 0x7e466254, 0x774b695a, 0x90d0b0e0, 0x99ddbbee, 0x82caa6fc, 0x8bc7adf2, 0xb4e49cd8, 0xbde997d6, 0xa6fe8ac4, 0xaff381ca, 0xd8b8e890, 0xd1b5e39e, 0xcaa2fe8c, 0xc3aff582, 0xfc8cc4a8, 0xf581cfa6, 0xee96d2b4, 0xe79bd9ba, 0x3bbb7bdb, 0x32b670d5, 0x29a16dc7, 0x20ac66c9, 0x1f8f57e3, 0x16825ced, 0x0d9541ff, 0x04984af1, 0x73d323ab, 0x7ade28a5, 0x61c935b7, 0x68c43eb9, 0x57e70f93, 0x5eea049d, 0x45fd198f, 0x4cf01281, 0xab6bcb3b, 0xa266c035, 0xb971dd27, 0xb07cd629, 0x8f5fe703, 0x8652ec0d, 0x9d45f11f, 0x9448fa11, 0xe303934b, 0xea0e9845, 0xf1198557, 0xf8148e59, 0xc737bf73, 0xce3ab47d, 0xd52da96f, 0xdc20a261, 0x766df6ad, 0x7f60fda3, 0x6477e0b1, 0x6d7aebbf, 0x5259da95, 0x5b54d19b, 0x4043cc89, 0x494ec787, 0x3e05aedd, 0x3708a5d3, 0x2c1fb8c1, 0x2512b3cf, 0x1a3182e5, 0x133c89eb, 0x082b94f9, 0x01269ff7, 0xe6bd464d, 0xefb04d43, 0xf4a75051, 0xfdaa5b5f, 0xc2896a75, 0xcb84617b, 0xd0937c69, 0xd99e7767, 0xaed51e3d, 0xa7d81533, 0xbccf0821, 0xb5c2032f, 0x8ae13205, 0x83ec390b, 0x98fb2419, 0x91f62f17, 0x4dd68d76, 0x44db8678, 0x5fcc9b6a, 0x56c19064, 0x69e2a14e, 0x60efaa40, 0x7bf8b752, 0x72f5bc5c, 0x05bed506, 0x0cb3de08, 0x17a4c31a, 0x1ea9c814, 0x218af93e, 0x2887f230, 0x3390ef22, 0x3a9de42c, 0xdd063d96, 0xd40b3698, 0xcf1c2b8a, 0xc6112084, 0xf93211ae, 0xf03f1aa0, 0xeb2807b2, 0xe2250cbc, 0x956e65e6, 0x9c636ee8, 0x877473fa, 0x8e7978f4, 0xb15a49de, 0xb85742d0, 0xa3405fc2, 0xaa4d54cc, 0xecdaf741, 0xe5d7fc4f, 0xfec0e15d, 0xf7cdea53, 0xc8eedb79, 0xc1e3d077, 0xdaf4cd65, 0xd3f9c66b, 0xa4b2af31, 0xadbfa43f, 0xb6a8b92d, 0xbfa5b223, 0x80868309, 0x898b8807, 0x929c9515, 0x9b919e1b, 0x7c0a47a1, 0x75074caf, 0x6e1051bd, 0x671d5ab3, 0x583e6b99, 0x51336097, 0x4a247d85, 0x4329768b, 0x34621fd1, 0x3d6f14df, 0x267809cd, 0x2f7502c3, 0x105633e9, 0x195b38e7, 0x024c25f5, 0x0b412efb, 0xd7618c9a, 0xde6c8794, 0xc57b9a86, 0xcc769188, 0xf355a0a2, 0xfa58abac, 0xe14fb6be, 0xe842bdb0, 0x9f09d4ea, 0x9604dfe4, 0x8d13c2f6, 0x841ec9f8, 0xbb3df8d2, 0xb230f3dc, 0xa927eece, 0xa02ae5c0, 0x47b13c7a, 0x4ebc3774, 0x55ab2a66, 0x5ca62168, 0x63851042, 0x6a881b4c, 0x719f065e, 0x78920d50, 0x0fd9640a, 0x06d46f04, 0x1dc37216, 0x14ce7918, 0x2bed4832, 0x22e0433c, 0x39f75e2e, 0x30fa5520, 0x9ab701ec, 0x93ba0ae2, 0x88ad17f0, 0x81a01cfe, 0xbe832dd4, 0xb78e26da, 0xac993bc8, 0xa59430c6, 0xd2df599c, 0xdbd25292, 0xc0c54f80, 0xc9c8448e, 0xf6eb75a4, 0xffe67eaa, 0xe4f163b8, 0xedfc68b6, 0x0a67b10c, 0x036aba02, 0x187da710, 0x1170ac1e, 0x2e539d34, 0x275e963a, 0x3c498b28, 0x35448026, 0x420fe97c, 0x4b02e272, 0x5015ff60, 0x5918f46e, 0x663bc544, 0x6f36ce4a, 0x7421d358, 0x7d2cd856, 0xa10c7a37, 0xa8017139, 0xb3166c2b, 0xba1b6725, 0x8538560f, 0x8c355d01, 0x97224013, 0x9e2f4b1d, 0xe9642247, 0xe0692949, 0xfb7e345b, 0xf2733f55, 0xcd500e7f, 0xc45d0571, 0xdf4a1863, 0xd647136d, 0x31dccad7, 0x38d1c1d9, 0x23c6dccb, 0x2acbd7c5, 0x15e8e6ef, 0x1ce5ede1, 0x07f2f0f3, 0x0efffbfd, 0x79b492a7, 0x70b999a9, 0x6bae84bb, 0x62a38fb5, 0x5d80be9f, 0x548db591, 0x4f9aa883, 0x4697a38d ] + + def __init__(self, key): + + if len(key) not in (16, 24, 32): + raise ValueError('Invalid key size') + + rounds = self.number_of_rounds[len(key)] + + # Encryption round keys + self._Ke = [[0] * 4 for i in xrange(rounds + 1)] + + # Decryption round keys + self._Kd = [[0] * 4 for i in xrange(rounds + 1)] + + round_key_count = (rounds + 1) * 4 + KC = len(key) // 4 + + # Convert the key into ints + tk = [ struct.unpack('>i', key[i:i + 4])[0] for i in xrange(0, len(key), 4) ] + + # Copy values into round key arrays + for i in xrange(0, KC): + self._Ke[i // 4][i % 4] = tk[i] + self._Kd[rounds - (i // 4)][i % 4] = tk[i] + + # Key expansion (fips-197 section 5.2) + rconpointer = 0 + t = KC + while t < round_key_count: + + tt = tk[KC - 1] + tk[0] ^= ((self.S[(tt >> 16) & 0xFF] << 24) ^ + (self.S[(tt >> 8) & 0xFF] << 16) ^ + (self.S[ tt & 0xFF] << 8) ^ + self.S[(tt >> 24) & 0xFF] ^ + (self.rcon[rconpointer] << 24)) + rconpointer += 1 + + if KC != 8: + for i in xrange(1, KC): + tk[i] ^= tk[i - 1] + + # Key expansion for 256-bit keys is "slightly different" (fips-197) + else: + for i in xrange(1, KC // 2): + tk[i] ^= tk[i - 1] + tt = tk[KC // 2 - 1] + + tk[KC // 2] ^= (self.S[ tt & 0xFF] ^ + (self.S[(tt >> 8) & 0xFF] << 8) ^ + (self.S[(tt >> 16) & 0xFF] << 16) ^ + (self.S[(tt >> 24) & 0xFF] << 24)) + + for i in xrange(KC // 2 + 1, KC): + tk[i] ^= tk[i - 1] + + # Copy values into round key arrays + j = 0 + while j < KC and t < round_key_count: + self._Ke[t // 4][t % 4] = tk[j] + self._Kd[rounds - (t // 4)][t % 4] = tk[j] + j += 1 + t += 1 + + # Inverse-Cipher-ify the decryption round key (fips-197 section 5.3) + for r in xrange(1, rounds): + for j in xrange(0, 4): + tt = self._Kd[r][j] + self._Kd[r][j] = (self.U1[(tt >> 24) & 0xFF] ^ + self.U2[(tt >> 16) & 0xFF] ^ + self.U3[(tt >> 8) & 0xFF] ^ + self.U4[ tt & 0xFF]) + + def encrypt(self, plaintext): + 'Encrypt a block of plain text using the AES block cipher.' + + if len(plaintext) != 16: + raise ValueError('wrong block length') + + rounds = len(self._Ke) - 1 + (s1, s2, s3) = [1, 2, 3] + a = [0, 0, 0, 0] + + # Convert plaintext to (ints ^ key) + t = [(_compact_word(plaintext[4 * i:4 * i + 4]) ^ self._Ke[0][i]) for i in xrange(0, 4)] + + # Apply round transforms + for r in xrange(1, rounds): + for i in xrange(0, 4): + a[i] = (self.T1[(t[ i ] >> 24) & 0xFF] ^ + self.T2[(t[(i + s1) % 4] >> 16) & 0xFF] ^ + self.T3[(t[(i + s2) % 4] >> 8) & 0xFF] ^ + self.T4[ t[(i + s3) % 4] & 0xFF] ^ + self._Ke[r][i]) + t = copy.copy(a) + + # The last round is special + result = [ ] + for i in xrange(0, 4): + tt = self._Ke[rounds][i] + result.append((self.S[(t[ i ] >> 24) & 0xFF] ^ (tt >> 24)) & 0xFF) + result.append((self.S[(t[(i + s1) % 4] >> 16) & 0xFF] ^ (tt >> 16)) & 0xFF) + result.append((self.S[(t[(i + s2) % 4] >> 8) & 0xFF] ^ (tt >> 8)) & 0xFF) + result.append((self.S[ t[(i + s3) % 4] & 0xFF] ^ tt ) & 0xFF) + + return result + + def decrypt(self, ciphertext): + 'Decrypt a block of cipher text using the AES block cipher.' + + if len(ciphertext) != 16: + raise ValueError('wrong block length') + + rounds = len(self._Kd) - 1 + (s1, s2, s3) = [3, 2, 1] + a = [0, 0, 0, 0] + + # Convert ciphertext to (ints ^ key) + t = [(_compact_word(ciphertext[4 * i:4 * i + 4]) ^ self._Kd[0][i]) for i in xrange(0, 4)] + + # Apply round transforms + for r in xrange(1, rounds): + for i in xrange(0, 4): + a[i] = (self.T5[(t[ i ] >> 24) & 0xFF] ^ + self.T6[(t[(i + s1) % 4] >> 16) & 0xFF] ^ + self.T7[(t[(i + s2) % 4] >> 8) & 0xFF] ^ + self.T8[ t[(i + s3) % 4] & 0xFF] ^ + self._Kd[r][i]) + t = copy.copy(a) + + # The last round is special + result = [ ] + for i in xrange(0, 4): + tt = self._Kd[rounds][i] + result.append((self.Si[(t[ i ] >> 24) & 0xFF] ^ (tt >> 24)) & 0xFF) + result.append((self.Si[(t[(i + s1) % 4] >> 16) & 0xFF] ^ (tt >> 16)) & 0xFF) + result.append((self.Si[(t[(i + s2) % 4] >> 8) & 0xFF] ^ (tt >> 8)) & 0xFF) + result.append((self.Si[ t[(i + s3) % 4] & 0xFF] ^ tt ) & 0xFF) + + return result + + +class Counter(object): + '''A counter object for the Counter (CTR) mode of operation. + + To create a custom counter, you can usually just override the + increment method.''' + + def __init__(self, initial_value = 1): + + # Convert the value into an array of bytes long + self._counter = [ ((initial_value >> i) % 256) for i in xrange(128 - 8, -1, -8) ] + + value = property(lambda s: s._counter) + + def increment(self): + '''Increment the counter (overflow rolls back to 0).''' + + for i in xrange(len(self._counter) - 1, -1, -1): + self._counter[i] += 1 + + if self._counter[i] < 256: break + + # Carry the one + self._counter[i] = 0 + + # Overflow + else: + self._counter = [ 0 ] * len(self._counter) + + +class AESBlockModeOfOperation(object): + '''Super-class for AES modes of operation that require blocks.''' + def __init__(self, key): + self._aes = AES(key) + + def decrypt(self, ciphertext): + raise Exception('not implemented') + + def encrypt(self, plaintext): + raise Exception('not implemented') + + +class AESStreamModeOfOperation(AESBlockModeOfOperation): + '''Super-class for AES modes of operation that are stream-ciphers.''' + +class AESSegmentModeOfOperation(AESStreamModeOfOperation): + '''Super-class for AES modes of operation that segment data.''' + + segment_bytes = 16 + + + +class AESModeOfOperationECB(AESBlockModeOfOperation): + '''AES Electronic Codebook Mode of Operation. + + o Block-cipher, so data must be padded to 16 byte boundaries + + Security Notes: + o This mode is not recommended + o Any two identical blocks produce identical encrypted values, + exposing data patterns. (See the image of Tux on wikipedia) + + Also see: + o https://en.wikipedia.org/wiki/Block_cipher_mode_of_operation#Electronic_codebook_.28ECB.29 + o See NIST SP800-38A (http://csrc.nist.gov/publications/nistpubs/800-38a/sp800-38a.pdf); section 6.1''' + + + name = "Electronic Codebook (ECB)" + + def encrypt(self, plaintext): + if len(plaintext) != 16: + raise ValueError('plaintext block must be 16 bytes') + + plaintext = _string_to_bytes(plaintext) + return _bytes_to_string(self._aes.encrypt(plaintext)) + + def decrypt(self, ciphertext): + if len(ciphertext) != 16: + raise ValueError('ciphertext block must be 16 bytes') + + ciphertext = _string_to_bytes(ciphertext) + return _bytes_to_string(self._aes.decrypt(ciphertext)) + + + +class AESModeOfOperationCBC(AESBlockModeOfOperation): + '''AES Cipher-Block Chaining Mode of Operation. + + o The Initialization Vector (IV) + o Block-cipher, so data must be padded to 16 byte boundaries + o An incorrect initialization vector will only cause the first + block to be corrupt; all other blocks will be intact + o A corrupt bit in the cipher text will cause a block to be + corrupted, and the next block to be inverted, but all other + blocks will be intact. + + Security Notes: + o This method (and CTR) ARE recommended. + + Also see: + o https://en.wikipedia.org/wiki/Block_cipher_mode_of_operation#Cipher-block_chaining_.28CBC.29 + o See NIST SP800-38A (http://csrc.nist.gov/publications/nistpubs/800-38a/sp800-38a.pdf); section 6.2''' + + + name = "Cipher-Block Chaining (CBC)" + + def __init__(self, key, iv = None): + if iv is None: + self._last_cipherblock = [ 0 ] * 16 + elif len(iv) != 16: + raise ValueError('initialization vector must be 16 bytes') + else: + self._last_cipherblock = _string_to_bytes(iv) + + AESBlockModeOfOperation.__init__(self, key) + + def encrypt(self, plaintext): + if len(plaintext) != 16: + raise ValueError('plaintext block must be 16 bytes') + + plaintext = _string_to_bytes(plaintext) + precipherblock = [ (p ^ l) for (p, l) in zip(plaintext, self._last_cipherblock) ] + self._last_cipherblock = self._aes.encrypt(precipherblock) + + return _bytes_to_string(self._last_cipherblock) + + def decrypt(self, ciphertext): + if len(ciphertext) != 16: + raise ValueError('ciphertext block must be 16 bytes') + + cipherblock = _string_to_bytes(ciphertext) + plaintext = [ (p ^ l) for (p, l) in zip(self._aes.decrypt(cipherblock), self._last_cipherblock) ] + self._last_cipherblock = cipherblock + + return _bytes_to_string(plaintext) + + + +class AESModeOfOperationCFB(AESSegmentModeOfOperation): + '''AES Cipher Feedback Mode of Operation. + + o A stream-cipher, so input does not need to be padded to blocks, + but does need to be padded to segment_size + + Also see: + o https://en.wikipedia.org/wiki/Block_cipher_mode_of_operation#Cipher_feedback_.28CFB.29 + o See NIST SP800-38A (http://csrc.nist.gov/publications/nistpubs/800-38a/sp800-38a.pdf); section 6.3''' + + + name = "Cipher Feedback (CFB)" + + def __init__(self, key, iv, segment_size = 1): + if segment_size == 0: segment_size = 1 + + if iv is None: + self._shift_register = [ 0 ] * 16 + elif len(iv) != 16: + raise ValueError('initialization vector must be 16 bytes') + else: + self._shift_register = _string_to_bytes(iv) + + self._segment_bytes = segment_size + + AESBlockModeOfOperation.__init__(self, key) + + segment_bytes = property(lambda s: s._segment_bytes) + + def encrypt(self, plaintext): + if len(plaintext) % self._segment_bytes != 0: + raise ValueError('plaintext block must be a multiple of segment_size') + + plaintext = _string_to_bytes(plaintext) + + # Break block into segments + encrypted = [ ] + for i in xrange(0, len(plaintext), self._segment_bytes): + plaintext_segment = plaintext[i: i + self._segment_bytes] + xor_segment = self._aes.encrypt(self._shift_register)[:len(plaintext_segment)] + cipher_segment = [ (p ^ x) for (p, x) in zip(plaintext_segment, xor_segment) ] + + # Shift the top bits out and the ciphertext in + self._shift_register = _concat_list(self._shift_register[len(cipher_segment):], cipher_segment) + + encrypted.extend(cipher_segment) + + return _bytes_to_string(encrypted) + + def decrypt(self, ciphertext): + if len(ciphertext) % self._segment_bytes != 0: + raise ValueError('ciphertext block must be a multiple of segment_size') + + ciphertext = _string_to_bytes(ciphertext) + + # Break block into segments + decrypted = [ ] + for i in xrange(0, len(ciphertext), self._segment_bytes): + cipher_segment = ciphertext[i: i + self._segment_bytes] + xor_segment = self._aes.encrypt(self._shift_register)[:len(cipher_segment)] + plaintext_segment = [ (p ^ x) for (p, x) in zip(cipher_segment, xor_segment) ] + + # Shift the top bits out and the ciphertext in + self._shift_register = _concat_list(self._shift_register[len(cipher_segment):], cipher_segment) + + decrypted.extend(plaintext_segment) + + return _bytes_to_string(decrypted) + + + +class AESModeOfOperationOFB(AESStreamModeOfOperation): + '''AES Output Feedback Mode of Operation. + + o A stream-cipher, so input does not need to be padded to blocks, + allowing arbitrary length data. + o A bit twiddled in the cipher text, twiddles the same bit in the + same bit in the plain text, which can be useful for error + correction techniques. + + Also see: + o https://en.wikipedia.org/wiki/Block_cipher_mode_of_operation#Output_feedback_.28OFB.29 + o See NIST SP800-38A (http://csrc.nist.gov/publications/nistpubs/800-38a/sp800-38a.pdf); section 6.4''' + + + name = "Output Feedback (OFB)" + + def __init__(self, key, iv = None): + if iv is None: + self._last_precipherblock = [ 0 ] * 16 + elif len(iv) != 16: + raise ValueError('initialization vector must be 16 bytes') + else: + self._last_precipherblock = _string_to_bytes(iv) + + self._remaining_block = [ ] + + AESBlockModeOfOperation.__init__(self, key) + + def encrypt(self, plaintext): + encrypted = [ ] + for p in _string_to_bytes(plaintext): + if len(self._remaining_block) == 0: + self._remaining_block = self._aes.encrypt(self._last_precipherblock) + self._last_precipherblock = [ ] + precipherbyte = self._remaining_block.pop(0) + self._last_precipherblock.append(precipherbyte) + cipherbyte = p ^ precipherbyte + encrypted.append(cipherbyte) + + return _bytes_to_string(encrypted) + + def decrypt(self, ciphertext): + # AES-OFB is symetric + return self.encrypt(ciphertext) + + + +class AESModeOfOperationCTR(AESStreamModeOfOperation): + '''AES Counter Mode of Operation. + + o A stream-cipher, so input does not need to be padded to blocks, + allowing arbitrary length data. + o The counter must be the same size as the key size (ie. len(key)) + o Each block independant of the other, so a corrupt byte will not + damage future blocks. + o Each block has a uniue counter value associated with it, which + contributes to the encrypted value, so no data patterns are + leaked. + o Also known as: Counter Mode (CM), Integer Counter Mode (ICM) and + Segmented Integer Counter (SIC + + Security Notes: + o This method (and CBC) ARE recommended. + o Each message block is associated with a counter value which must be + unique for ALL messages with the same key. Otherwise security may be + compromised. + + Also see: + + o https://en.wikipedia.org/wiki/Block_cipher_mode_of_operation#Counter_.28CTR.29 + o See NIST SP800-38A (http://csrc.nist.gov/publications/nistpubs/800-38a/sp800-38a.pdf); section 6.5 + and Appendix B for managing the initial counter''' + + + name = "Counter (CTR)" + + def __init__(self, key, counter = None): + AESBlockModeOfOperation.__init__(self, key) + + if counter is None: + counter = Counter() + + self._counter = counter + self._remaining_counter = [ ] + + def encrypt(self, plaintext): + while len(self._remaining_counter) < len(plaintext): + self._remaining_counter += self._aes.encrypt(self._counter.value) + self._counter.increment() + + plaintext = _string_to_bytes(plaintext) + + encrypted = [ (p ^ c) for (p, c) in zip(plaintext, self._remaining_counter) ] + self._remaining_counter = self._remaining_counter[len(encrypted):] + + return _bytes_to_string(encrypted) + + def decrypt(self, crypttext): + # AES-CTR is symetric + return self.encrypt(crypttext) + + +# Simple lookup table for each mode +AESModesOfOperation = dict( + ctr = AESModeOfOperationCTR, + cbc = AESModeOfOperationCBC, + cfb = AESModeOfOperationCFB, + ecb = AESModeOfOperationECB, + ofb = AESModeOfOperationOFB, +) diff --git a/module/scripts/lib/pyaes/blockfeeder.py b/module/scripts/lib/pyaes/blockfeeder.py new file mode 100644 index 0000000..b9a904d --- /dev/null +++ b/module/scripts/lib/pyaes/blockfeeder.py @@ -0,0 +1,227 @@ +# The MIT License (MIT) +# +# Copyright (c) 2014 Richard Moore +# +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in +# all copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +# THE SOFTWARE. + + +from .aes import AESBlockModeOfOperation, AESSegmentModeOfOperation, AESStreamModeOfOperation +from .util import append_PKCS7_padding, strip_PKCS7_padding, to_bufferable + + +# First we inject three functions to each of the modes of operations +# +# _can_consume(size) +# - Given a size, determine how many bytes could be consumed in +# a single call to either the decrypt or encrypt method +# +# _final_encrypt(data, padding = PADDING_DEFAULT) +# - call and return encrypt on this (last) chunk of data, +# padding as necessary; this will always be at least 16 +# bytes unless the total incoming input was less than 16 +# bytes +# +# _final_decrypt(data, padding = PADDING_DEFAULT) +# - same as _final_encrypt except for decrypt, for +# stripping off padding +# + +PADDING_NONE = 'none' +PADDING_DEFAULT = 'default' + +# @TODO: Ciphertext stealing and explicit PKCS#7 +# PADDING_CIPHERTEXT_STEALING +# PADDING_PKCS7 + +# ECB and CBC are block-only ciphers + +def _block_can_consume(self, size): + if size >= 16: return 16 + return 0 + +# After padding, we may have more than one block +def _block_final_encrypt(self, data, padding = PADDING_DEFAULT): + if padding == PADDING_DEFAULT: + data = append_PKCS7_padding(data) + + elif padding == PADDING_NONE: + if len(data) != 16: + raise Exception('invalid data length for final block') + else: + raise Exception('invalid padding option') + + if len(data) == 32: + return self.encrypt(data[:16]) + self.encrypt(data[16:]) + + return self.encrypt(data) + + +def _block_final_decrypt(self, data, padding = PADDING_DEFAULT): + if padding == PADDING_DEFAULT: + return strip_PKCS7_padding(self.decrypt(data)) + + if padding == PADDING_NONE: + if len(data) != 16: + raise Exception('invalid data length for final block') + return self.decrypt(data) + + raise Exception('invalid padding option') + +AESBlockModeOfOperation._can_consume = _block_can_consume +AESBlockModeOfOperation._final_encrypt = _block_final_encrypt +AESBlockModeOfOperation._final_decrypt = _block_final_decrypt + + + +# CFB is a segment cipher + +def _segment_can_consume(self, size): + return self.segment_bytes * int(size // self.segment_bytes) + +# CFB can handle a non-segment-sized block at the end using the remaining cipherblock +def _segment_final_encrypt(self, data, padding = PADDING_DEFAULT): + if padding != PADDING_DEFAULT: + raise Exception('invalid padding option') + + faux_padding = (chr(0) * (self.segment_bytes - (len(data) % self.segment_bytes))) + padded = data + to_bufferable(faux_padding) + return self.encrypt(padded)[:len(data)] + +# CFB can handle a non-segment-sized block at the end using the remaining cipherblock +def _segment_final_decrypt(self, data, padding = PADDING_DEFAULT): + if padding != PADDING_DEFAULT: + raise Exception('invalid padding option') + + faux_padding = (chr(0) * (self.segment_bytes - (len(data) % self.segment_bytes))) + padded = data + to_bufferable(faux_padding) + return self.decrypt(padded)[:len(data)] + +AESSegmentModeOfOperation._can_consume = _segment_can_consume +AESSegmentModeOfOperation._final_encrypt = _segment_final_encrypt +AESSegmentModeOfOperation._final_decrypt = _segment_final_decrypt + + + +# OFB and CTR are stream ciphers + +def _stream_can_consume(self, size): + return size + +def _stream_final_encrypt(self, data, padding = PADDING_DEFAULT): + if padding not in [PADDING_NONE, PADDING_DEFAULT]: + raise Exception('invalid padding option') + + return self.encrypt(data) + +def _stream_final_decrypt(self, data, padding = PADDING_DEFAULT): + if padding not in [PADDING_NONE, PADDING_DEFAULT]: + raise Exception('invalid padding option') + + return self.decrypt(data) + +AESStreamModeOfOperation._can_consume = _stream_can_consume +AESStreamModeOfOperation._final_encrypt = _stream_final_encrypt +AESStreamModeOfOperation._final_decrypt = _stream_final_decrypt + + + +class BlockFeeder(object): + '''The super-class for objects to handle chunking a stream of bytes + into the appropriate block size for the underlying mode of operation + and applying (or stripping) padding, as necessary.''' + + def __init__(self, mode, feed, final, padding = PADDING_DEFAULT): + self._mode = mode + self._feed = feed + self._final = final + self._buffer = to_bufferable("") + self._padding = padding + + def feed(self, data = None): + '''Provide bytes to encrypt (or decrypt), returning any bytes + possible from this or any previous calls to feed. + + Call with None or an empty string to flush the mode of + operation and return any final bytes; no further calls to + feed may be made.''' + + if self._buffer is None: + raise ValueError('already finished feeder') + + # Finalize; process the spare bytes we were keeping + if data is None: + result = self._final(self._buffer, self._padding) + self._buffer = None + return result + + self._buffer += to_bufferable(data) + + # We keep 16 bytes around so we can determine padding + result = to_bufferable('') + while len(self._buffer) > 16: + can_consume = self._mode._can_consume(len(self._buffer) - 16) + if can_consume == 0: break + result += self._feed(self._buffer[:can_consume]) + self._buffer = self._buffer[can_consume:] + + return result + + +class Encrypter(BlockFeeder): + 'Accepts bytes of plaintext and returns encrypted ciphertext.' + + def __init__(self, mode, padding = PADDING_DEFAULT): + BlockFeeder.__init__(self, mode, mode.encrypt, mode._final_encrypt, padding) + + +class Decrypter(BlockFeeder): + 'Accepts bytes of ciphertext and returns decrypted plaintext.' + + def __init__(self, mode, padding = PADDING_DEFAULT): + BlockFeeder.__init__(self, mode, mode.decrypt, mode._final_decrypt, padding) + + +# 8kb blocks +BLOCK_SIZE = (1 << 13) + +def _feed_stream(feeder, in_stream, out_stream, block_size = BLOCK_SIZE): + 'Uses feeder to read and convert from in_stream and write to out_stream.' + + while True: + chunk = in_stream.read(block_size) + if not chunk: + break + converted = feeder.feed(chunk) + out_stream.write(converted) + converted = feeder.feed() + out_stream.write(converted) + + +def encrypt_stream(mode, in_stream, out_stream, block_size = BLOCK_SIZE, padding = PADDING_DEFAULT): + 'Encrypts a stream of bytes from in_stream to out_stream using mode.' + + encrypter = Encrypter(mode, padding = padding) + _feed_stream(encrypter, in_stream, out_stream, block_size) + + +def decrypt_stream(mode, in_stream, out_stream, block_size = BLOCK_SIZE, padding = PADDING_DEFAULT): + 'Decrypts a stream of bytes from in_stream to out_stream using mode.' + + decrypter = Decrypter(mode, padding = padding) + _feed_stream(decrypter, in_stream, out_stream, block_size) diff --git a/module/scripts/lib/pyaes/util.py b/module/scripts/lib/pyaes/util.py new file mode 100644 index 0000000..081a375 --- /dev/null +++ b/module/scripts/lib/pyaes/util.py @@ -0,0 +1,60 @@ +# The MIT License (MIT) +# +# Copyright (c) 2014 Richard Moore +# +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in +# all copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +# THE SOFTWARE. + +# Why to_bufferable? +# Python 3 is very different from Python 2.x when it comes to strings of text +# and strings of bytes; in Python 3, strings of bytes do not exist, instead to +# represent arbitrary binary data, we must use the "bytes" object. This method +# ensures the object behaves as we need it to. + +def to_bufferable(binary): + return binary + +def _get_byte(c): + return ord(c) + +try: + xrange +except: + + def to_bufferable(binary): + if isinstance(binary, bytes): + return binary + return bytes(ord(b) for b in binary) + + def _get_byte(c): + return c + +def append_PKCS7_padding(data): + pad = 16 - (len(data) % 16) + return data + to_bufferable(chr(pad) * pad) + +def strip_PKCS7_padding(data): + if len(data) % 16 != 0: + raise ValueError("invalid length") + + pad = _get_byte(data[-1]) + + if pad > 16: + raise ValueError("invalid padding byte") + + return data[:-pad] diff --git a/module/scripts/lib/pyasn1/__init__.py b/module/scripts/lib/pyasn1/__init__.py new file mode 100644 index 0000000..14c960c --- /dev/null +++ b/module/scripts/lib/pyasn1/__init__.py @@ -0,0 +1,2 @@ +# https://www.python.org/dev/peps/pep-0396/ +__version__ = '0.6.5' diff --git a/module/scripts/lib/pyasn1/__pycache__/__init__.cpython-314.pyc b/module/scripts/lib/pyasn1/__pycache__/__init__.cpython-314.pyc new file mode 100644 index 0000000000000000000000000000000000000000..af249c05047a700cd38023cdcc6a9a5c0db96f26 GIT binary patch literal 231 zcmdPq#-C$(5#FFiFkGcQv= zF|Q;uy(qCPv!qftv7kV|IJqdXBsoJr%thBJF*&=mKtDG>r8FlMB#~KAQmmhonWSG( ynOK}>s2?8>G@&FuUaz3?7KaT)om~+d&}xv2i$Sjaz|6?V_<&EeiMxme$OQl*Z$Nqg literal 0 HcmV?d00001 diff --git a/module/scripts/lib/pyasn1/__pycache__/debug.cpython-314.pyc b/module/scripts/lib/pyasn1/__pycache__/debug.cpython-314.pyc new file mode 100644 index 0000000000000000000000000000000000000000..0b5a2a0dc801ea5d9e89befad246a50eea632a15 GIT binary patch literal 6647 zcmbtYYit|WmA=Cnl0#B_SQe>=X^E07$&6(&wiU;Ty?#lulgOf#iNn;1B!(tOG;NAx zW=5$+?`DZb+Dd5}JG*GBU|qxuG_8%cXo?nS{uM=ApaqJSvTReCs;JPWKMEA6Rua(e z{^_1`=RryqiWYkz9p1;e_ntZTJidGPdE8C{<&&TN>B`sKg!~R0M&b3!`p=VHLIw`&g)R3fD&Zh_LfKqgW@1Vw+$T`-DZ|1*_r^Y~wb;K5iHI zab9qYJ0hIoj1a};C+$RVHWI-#Uf0iRdn#_)TL-;dO|NI%CVK^UBT;+;)DKw%ZzIVv z$_~K?ef4zC4wzG4GiT@c&UVtTe~Qu|Fr6ga@CR&1!;I>R$6psEIg`)D!5gPI-U9JUkOpUTs|kNb}^UCr$kA$r$nv7CbHS2zAXK*=#BNmP)UHo#)-lRM6pb? zDr|t{iC_~fR9sGAL0qfA0kwf@>@fl=Lc#2+eN@Wi6j6#MxthgME%48;XBj+-7&F;O zNRW(~EThKmW)DHNGX21hrNG3PQL9$3r8O(qYWuaiF}sb#ER%sPI~wLuGp(xVf+fa2 zA|o}m8g{6Hj22^;0vof8f)Q>lG1lNfIsRW?!Hjyhu1;Xh>iRGFa^`ml1WtsFs%U_P~=E9b19-JCz3*s z=_|~rd^`?oDe<`K(*^D`dO{$K{3ehRS#s@o>-e%iSn+q?^LO9Aw&;I+_RI?Jx#6Aj z-m<^%e$RcM?_P1$zjb0cu)7l2cQ3GSF|dF3?24=5o-4TA)K+N<-)jn&9~)V0iq1Z} z!h0)xaDflbU%b!vEMco@fp5Ci`2OzqcGDhDg>PQqo9BD(^BpTqt?#(kK@O6vbsN*k zj!_X~WE5Gplu_i`Qevi?wGlL$yG%4}k-%SwYNdL)YmvZTiONpNX&JZd?v{382Pi8E zab&Zyw4Q@7E;j%HW$|@4+;i>4@s>skkow}s)S&jMhJA{5)~uN6n)1E zGs@+BE~H!K@w*@#l|{x0x)k^6|J7gk_~YNWwr=Kgy6$GRZhN zf|R*5t%x!#vI7*9O~`U8lT?x-YU(bdx@$UN8h*)1s!z#<>mfn?$jw0h@;MNGgY#M2 z*7h*g_CMPgYsV@9LcfOcCV|Z?hC!^KgRV_c{3$Jpk8{;12Qi$XF&yH$Vy97^r%~M@ zSfR%$*c2DU_Bz21k%X5}lba3=Lk$EZemw|BSE>o;F_JS6SAYQZ6fmYv(*SG+@i8-C zkD4J1*!0p4vjo!HVzdb{f?UnKZ3D8AD+T6BT^S&axz|WUu;}tMp*#?fw+X4$puTDD z4)7^oRU)HS%L5$SqyT{(t2elxX6gVH2-Z=*(Nq1cdLnWi76fEjqmQD(bzJca>*mKa;R z#$M9vF-8cPwp34kr0XADs}}=?UTRm`MJ{ZwS3bs4eL601HCh1=LahkSYF`>Gl=iu{ zYIgWrggC(Agt|@=B4c0`TCtH-n>J_6w8ByJ`Yi<@fc*m7Yg$;-^k zXX7KMMpb@ToEbt`DsgnZ(V0_#Mrcpis^t|c-C4Eg3ko1vS>>>;+A}%Cwjzz--H-K# zLRyf-Gf+K)G+`U58B?Cz3(6^x&Ay<85wa-SdjT|@Ki;8LKa66h}n`sbC~ zr@r#(JC7~2_0OLDjW@8=xNH9O7q8vqRxLL7AoH7@ElUkeUmpJV;jcb(=fVfk+b140 z9Q@Moir#nR&fo{fA2b~J((o!z+&gZJyfyOO)JJD4$At1R;it~Zv3ME&V(|r_Ew4VT zORP5&f6HGs>PbUzgD|kca$r{_&~q=)Q{H>@qtt`FrxybQW#7Omi-Z3wqZR%rXdmjZ z{Af=X&>uVZ0R2fv_>9ZCDbVA=!au)$5-OOTw+9!{Yvdo`Gs+=Lt#d#oVTwC7X8^P9 z)vdeh8hFJ?m?9lVvKL5Aiq~?i+cVjzkEs^+MHpExq151HWS zX7VB5@~Av08&#|+4|HrTF$OK$D4m4L12_mZr9(66S*X{Fr2;4dI1LcQ)EaGGLcSe9 zP-{DHT%Wr>f8l|*z07OyXcM3`h3lsLFZs3ul-r0VJrDenfIHz5HDFQFL^7RMYm3N< z!G_B8<*V;rt&s@Kf?C8$7>|bCQZM{U`;qh^i6Eh{=`_|bHc(^HpxF!9j3Id($pIu3 zn4&&)%!Po2LK{q(GOkQ9*T??ge_Sj@*SJ>eC1&j;!&nEIH9%jxkaTRpVVj)A4oje& zt^WcD+L)PxIW51+WW+XxnxRD|W6-aaQJlM_7!EV%p>Hac)>GxyQ{O8-GNjxH>-~rGhy8@LGd8z)8rK+atQH`1h zWlV(I_a@+lo-$@0;+P3+t3a;WXR1?r8D^=hJOirJbR<&+NEe|$%t{mRBVkCTo16(>sQOV{9E?gRq<)cx4)t8YH{woE{_P|?;Ucl90oRMGD2>U$#fbV!AMX#fOJ*!7`4+AN%{~F3di^$`WV&&FAOqzZjc;?cG-@{wK=*CvIQ)@W2PTivMWY zfApjJkDf0#oTxZXlpQBv(i%=)XW2U2Dq*d*|5>*{n+E!CwVZm~`9t1^wAl+Z338al zIQjJ>(6#Bl_!<4x4{wMZzWl*$5#Rl6^oD4McRwEPhVU#38Qrd0Ur6Q);-)x=2M#LX z*R@xGK5!LOfmZOHLC<5VHJ$|%ekA8(ELS~rUGoX%v(P7_g@PjejkDZ4&PNRxkAd!s zpM+p#+Z?|wR58pIL5j-sLGBrVH6#TIX6Q#Di6I_^By-53eeHbm{mbuNzO8({_+Q1v z)`R87gO4VEQ_y2DkAe~S-CF*h2OBLcTd16>4W7QlTnc2TZ#GsXe^tzFO2k}YTE470 zHPNyDdDyh(t;RX$)L)yrwtcHH5B4z3`$yz9QkfC?bcxwQzVL{8{}t9&9lDJ9!aFG9 zPP!lBmf~FT#v5~QEPC6@d>hrQB*S1d?A#V0pebpv;GeOEYyS;I@EQ&y`3(Hatw1Ou zShFzJrZpR}9$Mq{rAwFW`#EC~w> zKr^$lSLFjwP9>E}eAJPL9L&Fv@*l`ea#EoSrJUlsWpiWanC`)2!G{>>lBq&t(9`qk z{`!HpPK+1?+Sh;m=EJ{K2>BO&B&`&8_P&7578xf~q(LS~Lu-;N`DjdPikky&zNw|$ zoZ=RMTg-6tidzD1xtUL83yP})cPJxUR9pkN!%eMOx>AVuD=U5l_#@47Q&0Irihlz5 zqrf*({%~V#np98P@m16-7k}(6K*xfcHHp+3MCLvilKC^lha1X51MVb?%~B&jL9WCT zWVun0`U;UljUudC3Wt|_*M)a^k#nD0r!+!tSc1`@X91J4k8Ku6$M+0lEhD!^9YN(L zqw6S}3Q^m&grJv@+oCXOK`|EA7L#SxsxZxxl(T>{!8|Fb<+?QRg>cqgMz7yqs$GUr z*OIJdFg%l}8OE~3tqzln3u7*AhQ&9;fbeXEoIr}Y>#Wx!i`JD+Ye6s6O%v9V zrWu~jShkjM&%=2HcoSUltMOO=V0<<4+8-->c-4K{Kup8YD+-5OWQ8oF$bq&Tds>#= z(CXw$jAFoH?%@v&CcE6rqRICp`&b&r>L$zb{q+x-EfIn?X9$#8g+&{X#5lDrkBV*( zxIHTT4x_%phG9pG(YDW(G)RE3YxMwBRQhzCNx&EFdM#i`%WK7Jf+48i#t$RKIUq7R zYzil1fi48S1nw=zmTL-_H=##2eYfR!8#FnB2$=1(9h;)1jj$Z^t2wTNhQj;?lU0om zL0=%8W;mLGpTx0OvZjjXO`;&0Yl}QPN5)S7>671mvO7BYWOQkM7hRdee@@>=PBd|)S2iyk* z%3gG?anl5)Nyj!VDY>)Wl}x~#lcuR604l_0u4|e*S~!wn!Zf_%FQO|VOgIvC_i5<- z=eKaHZ*yZs{kzw5#f#5$QabT%u2QV;<)QaI_M)rdEE}G*C62gv4p^D+SeN-IG|FNl z55Oygr&lbu%YNy5LUPOTq>6+OzllPT>;()B7mo@0!0{nCL68{$)iBUf|AfGdPNFAj7MaA$;Oy!>EwfY`4j?Km5ofFJO2Z~)j+$hOp%kWb0KbLFBQ$>7KfnPlORNzUBvuA@pMyMzA7yIy3J?Z_zTHx$O<23^i* z3o=Rejw-~(Mmc(+qs`)YqT2?)bRaL$cu7cPhT0Ca?|4C1Vlc2^%enn~+J1}KHAAUr ziBG}R4{B=~c}Os0?x(sVp0Z#Yim%m;<&4tO^LFfL7b}L3hV$WCrfak=($}8%Pwmko7{Uc=wPhUcCc0 z3<946v{TbZI+@Q5L4t_!rSOmOAM!m!dN}<+b+u!xc?nO8rhgLOT@&{X3q6C{wqw&H+4P2E`;vAs*Wd zswl`aS^OWKy3eu@i=rAXs@yP!sM#W_!EiO{8{KHaZRl)~yNPF!mE7ILGHEUHqj*+pnve@*Fushd9B5Ne@6@!F+%hKC1MQaf zPHBo1$RxRoSzWbd#L0oSswGv>B>b676OJ;~VrV#EOAPQf3oBWTsQkDTf5-Y`w42g=EnTcx%mAcO##zb26t%9`p36L%Fba?!;&Y$n@4$pr%JpZ(E zcDFM1Wo2snjmMR1pTD+Sz51kj_3xEyPxV)J^$Sn*3y5s1V z3B3AeiqL<*PvF%b1M+xcF8%`TOH}Y)xX+x literal 0 HcmV?d00001 diff --git a/module/scripts/lib/pyasn1/codec/__init__.py b/module/scripts/lib/pyasn1/codec/__init__.py new file mode 100644 index 0000000..8c3066b --- /dev/null +++ b/module/scripts/lib/pyasn1/codec/__init__.py @@ -0,0 +1 @@ +# This file is necessary to make this directory a package. diff --git a/module/scripts/lib/pyasn1/codec/__pycache__/__init__.cpython-314.pyc b/module/scripts/lib/pyasn1/codec/__pycache__/__init__.cpython-314.pyc new file mode 100644 index 0000000000000000000000000000000000000000..82aad7df29c11ac1399278752d380a19b179153e GIT binary patch literal 211 zcmXwzJqiLb6oeBOMFhQsm5BSaRxHHY!ooH@vvG~?Pe`&tIP^Wp{ zFppuzgMN>AJ@2p6+}}qqf6*T_9S>KsB||czn;X$B@0$?T3KFykl@S`Iz{@h9wEz0_?#AWSp?-Vvo#N1i?*N;$Fs literal 0 HcmV?d00001 diff --git a/module/scripts/lib/pyasn1/codec/__pycache__/streaming.cpython-314.pyc b/module/scripts/lib/pyasn1/codec/__pycache__/streaming.cpython-314.pyc new file mode 100644 index 0000000000000000000000000000000000000000..77a330f91602e9c7c06b7336494d062a588ff376 GIT binary patch literal 8440 zcmcIpYit`=cD}=hG<=AZL|G3?vPPCAi+Yf;llYl9m24SSot0TymbY2i^oSfuV^c%= z&d{=KpfJ(}D(@Pw(hXp_D?kdcKsLYv-5-nn)Bafl{n3AVC1kl?tbw{%82wS%a@%CF zKzq)e8Iq!D$3f8x>CTnhi#|rn!SzeBJCt%YakK(MG|(KlNFxtSoOA68L4O> z8Xu{I+7WR^_y&^TG-t#Waj=#v!gY{<`zex-1h|}CqBKdNVM{CW^;ls>LM+D1_7XHi zVD=)>xCqfKm#Q>tJxK!Vnk{01h3pY4)D_4eFyOw(hii)`(%WxaB6fqBDr6Gaa$R&I zqwg5RTGhp>aLdahaT5g^+=%9lIJN3AqSZuPupLjX?t~bdmX)b<8kNME3sg*{Br4iC zLX5T`(qvkR3rbSyS0(AHIFXP9l~Jofr*kB!NrE;lYEVgnJd;XDGm@f-nw(SwSrs%{ ziVIqj^~R-GGA_|Rr;^l!*t8V83JVL9Nh-ueWlDl|R9I~)jT5NsJI<+!S0!h1QUJby zbVgKS60AE4J-~5Xn3&Tf93}}@rIaSfiXg(miY8G-Ob9X9og}0cO-?vfNz-t_=!{6O zO7U<~l^K__({gNDm=Wh-LZD%+vT;FY%sfhzEwn&K9NOY|bJlbCrX*!Qr#HiYQ|$=PltsV9D83 zDKVL|!VncF=<3Q{3&llE45d|xhTaOrk~2YZYA!S>CnPl#oRVf_MGlD|l_@G-m$kWm zIQEblqoNj@4vn4c9~5I()2Yx*GM-LI*deDhHI$GiLa8}XRrbS-aF`+e8h}iLsks~< zjl!&2G@5HIcc+48cLQ*!;$^)}vULr29gpf8?_c@bD?drh+wQuuZNj|mE52nL`pFLI zqy97LFR-CwBn)@7w2X1@7++Hspva#}fi7&~NZ}J1uQXrAEavzC8RyNfVTMZPoBn*q zn7fUPS>WWjkw8Vxh8L0|$l!@cS98{cq~t6~HOHSD8afj_aXu2TQV;fXO0EKKTRNes zcp>~lHD5)2Ff3LqG;A}GRp1Q3B6{Q`Ewm{hU9OXrI>RsR)- zYj(H85EVsyoBWH{r!Sem@$eb%J6rNFqk%JmXK4u+k1#qf)S43SAaoae3|N??DPAW- zsv`{Z9Kq| zQcj3PYg=Ezv7=-fcpbBvO3_9xbo3Pwz2D%eU%H=tl*MXI5N9i0X<3V(p*ab^@J0G#_Z3N>0YFnH(SnUIDxY38JE zVF7DnVj>ZZIz_PD-LevwCO?#vz?hgwO9l;?$+)0SC({YE?V2zFX3|_C3CvL;m8Pkr zDgh5@Y-0%-SVcyVT1}_0kC}GqIuN54!5mcZBo zkR~M(6dB3+P7a+KIRD{jbnyJCQ$vyHx#5q7s0(_SdI6X+vt+-HH^guv81mB)(!2%L z2k@`{1ymqTUx6zqx9>r>ik1d&rPwBHc-vR`cE&T<9h9>!aRj>&8h{$?Cq4qT=o2a8 zj08TBGaA*sDQLT+(Q9cjVf1*T(Mg%AT0&MNB?;Y?(P%swi$-Y;&JCu4&^=f+L1lOn z=Y~d^Nz<{B2lN2%Ses*qm8(EU7orokzg^KNAc-c&qN1izc_IzYB&<;b2$2v~H7>`r z7;H8gg-E8V=pzin>-J~*CBWNHvBHb0PD1td-+)}6S$I4Dyw+-Of9|x}>z_M0d)-zA zsc&4deD3N0l!n%)1*;<9&QPxBVG z{@c&l_iPcUn3UPVI~*6o7qo5v33$9s#`B6XMqclUn?8hTMB};<1;=N{9C>Abi5`qG zD0yCm%X|A{#8ldndGpZmI*&5XTj!gt6@CNo=pfAqJzhAtl42@Me$|3R@cOY*ydQy# z*P7oeNc<@B*ha?d%x__|)+GfZaBh@@(IqG;VKkd1rP#rUSc>0FsZMh;Ic9AoG8eFl z@5AArNGf21G$A>0MT$WHmt^LMMXDGc_Umy9*wU~_K^;LkRA+y_IMwt7P>7mzR5+TN zJIWN}=w%jIUKSuaE&3=YgTrSBMO7;8z$8HMY)q51bD(C*)MckLB57%=#KUE$0A+ZR`4MQ1!90UKH+KSouVPsBm@XXM zR*MD6m_oVfJ)zSYgu+cfO0bA>LI#7+BK{y`44kqmD}X8BV>8pjtVqtz#6@jzm^oLG z9CEy$K3IRwVE_|gBE}5>RgU~li=&ik zQ9TEK2p&9~0^~F3hd_=tOZ_LPZj%=R;cFKHOOfS|9(|U?z&UzbH`Nk&>-@@=x zI^*bEcXU4T*4(@KqnnF?4e#Dney`rAW!ge(ZJ~_gt#!v+d5Vm`Z_VGg;SX+jL#upf z%jU56WLRqHREbqceQ1GEvxR9C%%0@cWn3$thx_;{lwMuf>`Z6k37DNr+?kk zpLMscI5NW9Yr@+Z;k`BCy)WJGW!>HXY2~T{+g6LWXNy=IJzG{7^G6lUz{9#-gNMk& z9uDh1C)S4sY$uLccfcebMhC=P+rI)G*#m0&e}c*@sOkVBl`v~bF+l|46B0%{SW+&L z@w_)wHVJ0{Dg@K7oqvowe3}pz+Lu`c;-|tY=+2ZB6U;GWzd>$8au7WySTPTDg#nie zi3IDIbrK9t3sCE$#;kd;Y&T?5hMS0`h?_iQ7FHN@Il4kR>i?5Cy$P+V}uXV58^CMN+KWf%*} zjd2qT0AiT&5u_V2$elsPPVHmPyK^$F0ButFZis0hBSR)#|8Omq==&BloJb}Umz}!z z6mZg$Af^JgFh@prcZyt~p^=jmTyEWerD#VW#5@C!oF$f`m?z2cREkNktU~yL9K$Mpf&)CCk^`^W5<)TwB_M(dH)K zm96pL-~Gw%jhgnA$Xd-lOvmUXbm_qIu?NRC`F&YW-CZ~MMs= zeCw{}-@0r5;PPeNyY7wL8Cg8AM3-+pxV7o-eO&9mo7m+2->B$hemvN7qLF*p-wX9i zi7}?-hs$btgfnki{?>w1%`_Qdx1f@>pZfW|>~qrPI;Qc{X@6cJccAmDJ6p&KpA zWtdKcu4$qV#23aW7Lv!{K$yDpU0jCp3c!>NlO5oIsPfI7UIr|toAT4iS;#>vb4EfN zSipRr(A~dZcwc~EUJ$Q~aso|T;MWQajq5H=(i}6j*p5M7^9((HVdO-bJI+u?>IKI)`3pRhtVt4dE zY683bRny@Gd$zv$Q`a*q@gLdZ38*_L<7#v7SMI*A=ZB4#FWr5AQg;I%9}hGR`nlgU z*ALprZ}$73{m|wcY_L9TutGaltXLSUF!fC3)sy++@K#!iln;##VAvbP8kUIVkTI8T zJkIRLl%1*UQ*BJy;rB6?XoO?iF=sx~DewYGilRBExq@;Ql*~~3Q*7?CidJqEt7IvaYn7B22}ht_!bc~cc_87xBUO3+GnJ>*(8k%sd6!f8Ekwf z0ARpT9ss~s*Q6WTj*tRld0ofPro?M$NZA2wKx)pQ6=r2X5lHia663F8dhX7&{6_4g z3Ncs}JaA^jC+3(tAeim|0N7v3QX*bHRw)}WA?}pKP8fPR z&N%3GGZ--H;>+m ze2$_W4=bs(8D62YCf%48(<+_-{>ofw9A1EDsUAI2sxQC)Ty^ORdKMl5nI(^ky(D@b zzkY-j!n2WbH-3hqqxc=Gf55&UL50Y>b(FZgkIUfgquPc{ZE(Fd_{-VN+7C9}AN--F zX)(ScF3GH?d%d>%mkpb>M^@cOo>WyYH2(PF;^<~oyW#RIomk<2`n|>Dzc}#s$G$kW z=|1$Lg7_MDP6*IlRr}ak`@@lMR5a@!9~nHz{o}wK&h6%!Ukq~XT>pQa=0cqQ H3)%kyOWHD- literal 0 HcmV?d00001 diff --git a/module/scripts/lib/pyasn1/codec/ber/__init__.py b/module/scripts/lib/pyasn1/codec/ber/__init__.py new file mode 100644 index 0000000..8c3066b --- /dev/null +++ b/module/scripts/lib/pyasn1/codec/ber/__init__.py @@ -0,0 +1 @@ +# This file is necessary to make this directory a package. diff --git a/module/scripts/lib/pyasn1/codec/ber/__pycache__/__init__.cpython-314.pyc b/module/scripts/lib/pyasn1/codec/ber/__pycache__/__init__.cpython-314.pyc new file mode 100644 index 0000000000000000000000000000000000000000..f725d8640b9c08489daf572df2a1ae12f334bb9c GIT binary patch literal 215 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 EAM1o}v;Y7A literal 0 HcmV?d00001 diff --git a/module/scripts/lib/pyasn1/codec/ber/__pycache__/eoo.cpython-314.pyc b/module/scripts/lib/pyasn1/codec/ber/__pycache__/eoo.cpython-314.pyc new file mode 100644 index 0000000000000000000000000000000000000000..2f79dc0c96d2e9d7cfcb5f7f3dc010bba0f2c28f GIT binary patch literal 1241 zcmZ`&&2Jk;6o2Dge>5B0N!qjs9NJJqWuV?7!lj}p{S=kJiMGY3#;fsqY_?tRnw{rX zK2%s-XbVz$<^UYw#Pkou3^cW1U5Ewum9Ci^tX2=d}oFbgU)M0Rp3PWQa7QrzOfskp`3lB?`5XU2A6`gfxN9O5jIy_s~!rJ-Z)^Svg6C}djgzN zTL)wgV5*)+;~+$o9-8avwduV<30?%U7hYnYdEQR1=bpFLVN%XAOT0McFtnW1A;4+J zzvao)oct!uFz0g}3{5%MXOKD{ay7L*M2h`t6us_pyJRce}Rq zVvgSDcJ18pGu*q@>)M^D*$aer_+6BC;5Y1UpUdz)Vr&Y}ZVidAy1GPz_wM`ICcBf1MA4|r8j=4epmhJ{rjbrUk*Gdt=v1fSz5Vu_S;V%7Uwo@ z-oAN1R~k|b{0!xy$`KXs2-j6puAAgUc-nQp=<#6eDY|aUk0l0vD8fj)o1(>g0oHiX z6S1C*nxd-lGH+iJs0`9-mUj7Uz@_{w^skFp@<0{IbRmj6954BuZa{q}0|mtUh#{$R z-gRjv=zA_ljQvIrg`^M8x~`|Wn!bmg2m%@rQ~5nc3ezjL64PQqtpAje$PYi!LdZO$zHD7TKC+&p&j-nrj$r-w$WkQrRq$-(UW z;NmmWOfT#JiFkIxTP +# License: https://pyasn1.readthedocs.io/en/latest/license.html +# +import io +import os +import sys +import warnings + +from pyasn1 import debug +from pyasn1 import error +from pyasn1.codec.ber import eoo +from pyasn1.codec.streaming import asSeekableStream +from pyasn1.codec.streaming import isEndOfStream +from pyasn1.codec.streaming import peekIntoStream +from pyasn1.codec.streaming import readFromStream +from pyasn1.compat import _MISSING +from pyasn1.error import PyAsn1Error +from pyasn1.type import base +from pyasn1.type import char +from pyasn1.type import tag +from pyasn1.type import tagmap +from pyasn1.type import univ +from pyasn1.type import useful + +__all__ = ['StreamingDecoder', 'Decoder', 'decode'] + +LOG = debug.registerLoggee(__name__, flags=debug.DEBUG_DECODER) + +noValue = base.noValue + +SubstrateUnderrunError = error.SubstrateUnderrunError + +# Maximum number of continuation octets (high-bit set) allowed per OID arc. +# 20 octets allows up to 140-bit integers, supporting UUID-based OIDs +MAX_OID_ARC_CONTINUATION_OCTETS = 20 + +# Maximum number of octets in a long-form tag ID (20 octets = up to +# 140-bit tag IDs, matching the OID arc limit) +MAX_TAG_OCTETS = 20 +MAX_NESTING_DEPTH = 100 + +# Maximum number of bytes in a BER length field (8 bytes = up to 2^64-1) +MAX_LENGTH_OCTETS = 8 + + +class AbstractPayloadDecoder(object): + protoComponent = None + + def valueDecoder(self, substrate, asn1Spec, + tagSet=None, length=None, state=None, + decodeFun=None, substrateFun=None, + **options): + """Decode value with fixed byte length. + + The decoder is allowed to consume as many bytes as necessary. + """ + raise error.PyAsn1Error('SingleItemDecoder not implemented for %s' % (tagSet,)) # TODO: Seems more like an NotImplementedError? + + def indefLenValueDecoder(self, substrate, asn1Spec, + tagSet=None, length=None, state=None, + decodeFun=None, substrateFun=None, + **options): + """Decode value with undefined length. + + The decoder is allowed to consume as many bytes as necessary. + """ + raise error.PyAsn1Error('Indefinite length mode decoder not implemented for %s' % (tagSet,)) # TODO: Seems more like an NotImplementedError? + + @staticmethod + def _passAsn1Object(asn1Object, options): + if 'asn1Object' not in options: + options['asn1Object'] = asn1Object + + return options + + +class AbstractSimplePayloadDecoder(AbstractPayloadDecoder): + @staticmethod + def substrateCollector(asn1Object, substrate, length, options): + for chunk in readFromStream(substrate, length, options): + yield chunk + + def _createComponent(self, asn1Spec, tagSet, value, **options): + if options.get('native'): + return value + elif asn1Spec is None: + return self.protoComponent.clone(value, tagSet=tagSet) + elif value is noValue: + return asn1Spec + else: + return asn1Spec.clone(value) + + +class RawPayloadDecoder(AbstractSimplePayloadDecoder): + protoComponent = univ.Any('') + + def valueDecoder(self, substrate, asn1Spec, + tagSet=None, length=None, state=None, + decodeFun=None, substrateFun=None, + **options): + if substrateFun: + asn1Object = self._createComponent(asn1Spec, tagSet, '', **options) + + for chunk in substrateFun(asn1Object, substrate, length, options): + yield chunk + + return + + for value in decodeFun(substrate, asn1Spec, tagSet, length, **options): + yield value + + def indefLenValueDecoder(self, substrate, asn1Spec, + tagSet=None, length=None, state=None, + decodeFun=None, substrateFun=None, + **options): + if substrateFun: + asn1Object = self._createComponent(asn1Spec, tagSet, '', **options) + + for chunk in substrateFun(asn1Object, substrate, length, options): + yield chunk + + return + + while True: + for value in decodeFun( + substrate, asn1Spec, tagSet, length, + allowEoo=True, **options): + + if value is eoo.endOfOctets: + return + + yield value + + +rawPayloadDecoder = RawPayloadDecoder() + + +class IntegerPayloadDecoder(AbstractSimplePayloadDecoder): + protoComponent = univ.Integer(0) + + def valueDecoder(self, substrate, asn1Spec, + tagSet=None, length=None, state=None, + decodeFun=None, substrateFun=None, + **options): + + if tagSet[0].tagFormat != tag.tagFormatSimple: + raise error.PyAsn1Error('Simple tag format expected') + + for chunk in readFromStream(substrate, length, options): + if isinstance(chunk, SubstrateUnderrunError): + yield chunk + + if chunk: + value = int.from_bytes(bytes(chunk), 'big', signed=True) + + else: + value = 0 + + yield self._createComponent(asn1Spec, tagSet, value, **options) + + +class BooleanPayloadDecoder(IntegerPayloadDecoder): + protoComponent = univ.Boolean(0) + + def _createComponent(self, asn1Spec, tagSet, value, **options): + return IntegerPayloadDecoder._createComponent( + self, asn1Spec, tagSet, value and 1 or 0, **options) + + +class BitStringPayloadDecoder(AbstractSimplePayloadDecoder): + protoComponent = univ.BitString(()) + supportConstructedForm = True + + @staticmethod + def _appendFragment(assembled, component, bitLength): + trailingBits = component[0] + if trailingBits > 7: + raise error.PyAsn1Error( + 'Trailing bits overflow %s' % trailingBits + ) + + payload = component[1:] + if not payload: + if trailingBits: + raise error.PyAsn1Error('Trailing bits overflow %s' % trailingBits) + return bitLength + + if not trailingBits and not bitLength % 8: + assembled.extend(payload) + return bitLength + len(payload) * 8 + + for idx, byte in enumerate(payload): + significantBits = 8 + if idx == len(payload) - 1: + significantBits -= trailingBits + byte >>= trailingBits + + usedBits = bitLength % 8 + if not usedBits: + assembled.append(byte << (8 - significantBits)) + elif usedBits + significantBits <= 8: + assembled[-1] |= byte << (8 - usedBits - significantBits) + else: + remainingBits = usedBits + significantBits - 8 + assembled[-1] |= byte >> remainingBits + assembled.append((byte << (8 - remainingBits)) & 0xff) + + bitLength += significantBits + + return bitLength + + def valueDecoder(self, substrate, asn1Spec, + tagSet=None, length=None, state=None, + decodeFun=None, substrateFun=None, + **options): + + if substrateFun: + asn1Object = self._createComponent(asn1Spec, tagSet, noValue, **options) + + for chunk in substrateFun(asn1Object, substrate, length, options): + yield chunk + + return + + if not length: + raise error.PyAsn1Error('Empty BIT STRING substrate') + + for chunk in isEndOfStream(substrate): + if isinstance(chunk, SubstrateUnderrunError): + yield chunk + + if chunk: + raise error.PyAsn1Error('Empty BIT STRING substrate') + + if tagSet[0].tagFormat == tag.tagFormatSimple: # XXX what tag to check? + + for trailingBits in readFromStream(substrate, 1, options): + if isinstance(trailingBits, SubstrateUnderrunError): + yield trailingBits + + trailingBits = ord(trailingBits) + if trailingBits > 7: + raise error.PyAsn1Error( + 'Trailing bits overflow %s' % trailingBits + ) + + for chunk in readFromStream(substrate, length - 1, options): + if isinstance(chunk, SubstrateUnderrunError): + yield chunk + + value = self.protoComponent.fromOctetString( + chunk, internalFormat=True, padding=trailingBits) + + yield self._createComponent(asn1Spec, tagSet, value, **options) + + return + + if not self.supportConstructedForm: + raise error.PyAsn1Error('Constructed encoding form prohibited ' + 'at %s' % self.__class__.__name__) + + if LOG: + LOG('assembling constructed serialization') + + # All inner fragments are of the same type, treat them as octet string + substrateFun = self.substrateCollector + + assembled = bytearray() + bitLength = 0 + + current_position = substrate.tell() + + while substrate.tell() - current_position < length: + for component in decodeFun( + substrate, self.protoComponent, substrateFun=substrateFun, + **options): + if isinstance(component, SubstrateUnderrunError): + yield component + + bitLength = self._appendFragment(assembled, component, bitLength) + + bitString = self.protoComponent.fromOctetString( + assembled, internalFormat=True, padding=(-bitLength) % 8 + ) + + yield self._createComponent(asn1Spec, tagSet, bitString, **options) + + def indefLenValueDecoder(self, substrate, asn1Spec, + tagSet=None, length=None, state=None, + decodeFun=None, substrateFun=None, + **options): + + if substrateFun: + asn1Object = self._createComponent(asn1Spec, tagSet, noValue, **options) + + for chunk in substrateFun(asn1Object, substrate, length, options): + yield chunk + + return + + # All inner fragments are of the same type, treat them as octet string + substrateFun = self.substrateCollector + + assembled = bytearray() + bitLength = 0 + + while True: # loop over fragments + + for component in decodeFun( + substrate, self.protoComponent, substrateFun=substrateFun, + allowEoo=True, **options): + + if component is eoo.endOfOctets: + break + + if isinstance(component, SubstrateUnderrunError): + yield component + + if component is eoo.endOfOctets: + break + + bitLength = self._appendFragment(assembled, component, bitLength) + + bitString = self.protoComponent.fromOctetString( + assembled, internalFormat=True, padding=(-bitLength) % 8 + ) + + yield self._createComponent(asn1Spec, tagSet, bitString, **options) + + +class OctetStringPayloadDecoder(AbstractSimplePayloadDecoder): + protoComponent = univ.OctetString('') + supportConstructedForm = True + + def valueDecoder(self, substrate, asn1Spec, + tagSet=None, length=None, state=None, + decodeFun=None, substrateFun=None, + **options): + if substrateFun: + asn1Object = self._createComponent(asn1Spec, tagSet, noValue, **options) + + for chunk in substrateFun(asn1Object, substrate, length, options): + yield chunk + + return + + if tagSet[0].tagFormat == tag.tagFormatSimple: # XXX what tag to check? + for chunk in readFromStream(substrate, length, options): + if isinstance(chunk, SubstrateUnderrunError): + yield chunk + + yield self._createComponent(asn1Spec, tagSet, chunk, **options) + + return + + if not self.supportConstructedForm: + raise error.PyAsn1Error('Constructed encoding form prohibited at %s' % self.__class__.__name__) + + if LOG: + LOG('assembling constructed serialization') + + # Assemble the logical value of each fragment, including nested ones. + fragments = [] + + original_position = substrate.tell() + # head = popSubstream(substrate, length) + while substrate.tell() - original_position < length: + for component in decodeFun( + substrate, self.protoComponent, **options): + if isinstance(component, SubstrateUnderrunError): + yield component + + fragments.append(bytes(component)) + + yield self._createComponent(asn1Spec, tagSet, b''.join(fragments), **options) + + def indefLenValueDecoder(self, substrate, asn1Spec, + tagSet=None, length=None, state=None, + decodeFun=None, substrateFun=None, + **options): + if substrateFun and substrateFun is not self.substrateCollector: + asn1Object = self._createComponent(asn1Spec, tagSet, noValue, **options) + + for chunk in substrateFun(asn1Object, substrate, length, options): + yield chunk + + return + + # Assemble the logical value of each fragment, including nested ones. + fragments = [] + + while True: # loop over fragments + + for component in decodeFun( + substrate, self.protoComponent, allowEoo=True, **options): + + if isinstance(component, SubstrateUnderrunError): + yield component + + if component is eoo.endOfOctets: + break + + if component is eoo.endOfOctets: + break + + fragments.append(bytes(component)) + + yield self._createComponent(asn1Spec, tagSet, b''.join(fragments), **options) + + +class NullPayloadDecoder(AbstractSimplePayloadDecoder): + protoComponent = univ.Null('') + + def valueDecoder(self, substrate, asn1Spec, + tagSet=None, length=None, state=None, + decodeFun=None, substrateFun=None, + **options): + + if tagSet[0].tagFormat != tag.tagFormatSimple: + raise error.PyAsn1Error('Simple tag format expected') + + for chunk in readFromStream(substrate, length, options): + if isinstance(chunk, SubstrateUnderrunError): + yield chunk + + component = self._createComponent(asn1Spec, tagSet, '', **options) + + if chunk: + raise error.PyAsn1Error('Unexpected %d-octet substrate for Null' % length) + + yield component + + +class ObjectIdentifierPayloadDecoder(AbstractSimplePayloadDecoder): + protoComponent = univ.ObjectIdentifier(()) + + def valueDecoder(self, substrate, asn1Spec, + tagSet=None, length=None, state=None, + decodeFun=None, substrateFun=None, + **options): + if tagSet[0].tagFormat != tag.tagFormatSimple: + raise error.PyAsn1Error('Simple tag format expected') + + for chunk in readFromStream(substrate, length, options): + if isinstance(chunk, SubstrateUnderrunError): + yield chunk + + if not chunk: + raise error.PyAsn1Error('Empty substrate') + + oid = [] + index = 0 + substrateLen = len(chunk) + while index < substrateLen: + subId = chunk[index] + index += 1 + if subId < 128: + oid.append(subId) + elif subId > 128: + # Construct subid from a number of octets + nextSubId = subId + subId = 0 + continuationOctetCount = 0 + while nextSubId >= 128: + continuationOctetCount += 1 + if continuationOctetCount > MAX_OID_ARC_CONTINUATION_OCTETS: + raise error.PyAsn1Error( + 'OID arc exceeds maximum continuation octets limit (%d) ' + 'at position %d' % (MAX_OID_ARC_CONTINUATION_OCTETS, index) + ) + subId = (subId << 7) + (nextSubId & 0x7F) + if index >= substrateLen: + raise error.SubstrateUnderrunError( + 'Short substrate for sub-OID past %s' % (tuple(oid),) + ) + nextSubId = chunk[index] + index += 1 + oid.append((subId << 7) + nextSubId) + elif subId == 128: + # ASN.1 spec forbids leading zeros (0x80) in OID + # encoding, tolerating it opens a vulnerability. See + # https://www.esat.kuleuven.be/cosic/publications/article-1432.pdf + # page 7 + raise error.PyAsn1Error('Invalid octet 0x80 in OID encoding') + + # Decode two leading arcs + if 0 <= oid[0] <= 39: + oid.insert(0, 0) + elif 40 <= oid[0] <= 79: + oid[0] -= 40 + oid.insert(0, 1) + elif oid[0] >= 80: + oid[0] -= 80 + oid.insert(0, 2) + else: + raise error.PyAsn1Error('Malformed first OID octet: %s' % chunk[0]) + + yield self._createComponent(asn1Spec, tagSet, tuple(oid), **options) + + +class RelativeOIDPayloadDecoder(AbstractSimplePayloadDecoder): + protoComponent = univ.RelativeOID(()) + + def valueDecoder(self, substrate, asn1Spec, + tagSet=None, length=None, state=None, + decodeFun=None, substrateFun=None, + **options): + if tagSet[0].tagFormat != tag.tagFormatSimple: + raise error.PyAsn1Error('Simple tag format expected') + + for chunk in readFromStream(substrate, length, options): + if isinstance(chunk, SubstrateUnderrunError): + yield chunk + + if not chunk: + raise error.PyAsn1Error('Empty substrate') + + reloid = [] + index = 0 + substrateLen = len(chunk) + while index < substrateLen: + subId = chunk[index] + index += 1 + if subId < 128: + reloid.append(subId) + elif subId > 128: + # Construct subid from a number of octets + nextSubId = subId + subId = 0 + continuationOctetCount = 0 + while nextSubId >= 128: + continuationOctetCount += 1 + if continuationOctetCount > MAX_OID_ARC_CONTINUATION_OCTETS: + raise error.PyAsn1Error( + 'RELATIVE-OID arc exceeds maximum continuation octets limit (%d) ' + 'at position %d' % (MAX_OID_ARC_CONTINUATION_OCTETS, index) + ) + subId = (subId << 7) + (nextSubId & 0x7F) + if index >= substrateLen: + raise error.SubstrateUnderrunError( + 'Short substrate for sub-OID past %s' % (tuple(reloid),) + ) + nextSubId = chunk[index] + index += 1 + reloid.append((subId << 7) + nextSubId) + elif subId == 128: + # ASN.1 spec forbids leading zeros (0x80) in OID + # encoding, tolerating it opens a vulnerability. See + # https://www.esat.kuleuven.be/cosic/publications/article-1432.pdf + # page 7 + raise error.PyAsn1Error('Invalid octet 0x80 in RELATIVE-OID encoding') + + yield self._createComponent(asn1Spec, tagSet, tuple(reloid), **options) + + +class RealPayloadDecoder(AbstractSimplePayloadDecoder): + protoComponent = univ.Real() + + def valueDecoder(self, substrate, asn1Spec, + tagSet=None, length=None, state=None, + decodeFun=None, substrateFun=None, + **options): + if tagSet[0].tagFormat != tag.tagFormatSimple: + raise error.PyAsn1Error('Simple tag format expected') + + for chunk in readFromStream(substrate, length, options): + if isinstance(chunk, SubstrateUnderrunError): + yield chunk + + if not chunk: + yield self._createComponent(asn1Spec, tagSet, 0.0, **options) + return + + fo = chunk[0] + chunk = chunk[1:] + if fo & 0x80: # binary encoding + if not chunk: + raise error.PyAsn1Error("Incomplete floating-point value") + + if LOG: + LOG('decoding binary encoded REAL') + + n = (fo & 0x03) + 1 + + if n == 4: + n = chunk[0] + chunk = chunk[1:] + + eo, chunk = chunk[:n], chunk[n:] + + if not eo or not chunk: + raise error.PyAsn1Error('Real exponent screwed') + + e = eo[0] & 0x80 and -1 or 0 + + while eo: # exponent + e <<= 8 + e |= eo[0] + eo = eo[1:] + + b = fo >> 4 & 0x03 # base bits + + if b > 2: + raise error.PyAsn1Error('Illegal Real base') + + if b == 1: # encbase = 8 + e *= 3 + + elif b == 2: # encbase = 16 + e *= 4 + p = int.from_bytes(bytes(chunk), 'big') + + if fo & 0x40: # sign bit + p = -p + + sf = fo >> 2 & 0x03 # scale bits + p *= 2 ** sf + value = (p, 2, e) + + elif fo & 0x40: # infinite value + if LOG: + LOG('decoding infinite REAL') + + value = fo & 0x01 and '-inf' or 'inf' + + elif fo & 0xc0 == 0: # character encoding + if not chunk: + raise error.PyAsn1Error("Incomplete floating-point value") + + if LOG: + LOG('decoding character encoded REAL') + + try: + if fo & 0x3 == 0x1: # NR1 + value = (int(chunk), 10, 0) + + elif fo & 0x3 == 0x2: # NR2 + value = float(chunk) + + elif fo & 0x3 == 0x3: # NR3 + value = float(chunk) + + else: + raise error.SubstrateUnderrunError( + 'Unknown NR (tag %s)' % fo + ) + + except ValueError: + raise error.SubstrateUnderrunError( + 'Bad character Real syntax' + ) + + else: + raise error.SubstrateUnderrunError( + 'Unknown encoding (tag %s)' % fo + ) + + yield self._createComponent(asn1Spec, tagSet, value, **options) + + +class AbstractConstructedPayloadDecoder(AbstractPayloadDecoder): + protoComponent = None + + +class ConstructedPayloadDecoderBase(AbstractConstructedPayloadDecoder): + protoRecordComponent = None + protoSequenceComponent = None + + def _getComponentTagMap(self, asn1Object, idx): + raise NotImplementedError + + def _getComponentPositionByType(self, asn1Object, tagSet, idx): + raise NotImplementedError + + def _decodeComponentsSchemaless( + self, substrate, tagSet=None, decodeFun=None, + length=None, **options): + + asn1Object = None + + components = [] + componentTypes = set() + + original_position = substrate.tell() + + while length == -1 or substrate.tell() < original_position + length: + for component in decodeFun(substrate, **options): + if isinstance(component, SubstrateUnderrunError): + yield component + + if length == -1 and component is eoo.endOfOctets: + break + + components.append(component) + # Two distinct types settle the choice; keep the set bounded + # to avoid quadratic comparisons for colliding tag hashes. + if len(componentTypes) < 2: + componentTypes.add(component.tagSet) + + # Now we have to guess is it SEQUENCE/SET or SEQUENCE OF/SET OF + # The heuristics is: + # * 1+ components of different types -> likely SEQUENCE/SET + # * otherwise -> likely SEQUENCE OF/SET OF + if len(componentTypes) > 1: + protoComponent = self.protoRecordComponent + + else: + protoComponent = self.protoSequenceComponent + + asn1Object = protoComponent.clone( + # construct tagSet from base tag from prototype ASN.1 object + # and additional tags recovered from the substrate + tagSet=tag.TagSet(protoComponent.tagSet.baseTag, *tagSet.superTags) + ) + + if LOG: + LOG('guessed %r container type (pass `asn1Spec` to guide the ' + 'decoder)' % asn1Object) + + for idx, component in enumerate(components): + asn1Object.setComponentByPosition( + idx, component, + verifyConstraints=False, + matchTags=False, matchConstraints=False + ) + + yield asn1Object + + def valueDecoder(self, substrate, asn1Spec, + tagSet=None, length=None, state=None, + decodeFun=None, substrateFun=None, + **options): + if tagSet[0].tagFormat != tag.tagFormatConstructed: + raise error.PyAsn1Error('Constructed tag format expected') + + original_position = substrate.tell() + + if substrateFun: + if asn1Spec is not None: + asn1Object = asn1Spec.clone() + + elif self.protoComponent is not None: + asn1Object = self.protoComponent.clone(tagSet=tagSet) + + else: + asn1Object = self.protoRecordComponent, self.protoSequenceComponent + + for chunk in substrateFun(asn1Object, substrate, length, options): + yield chunk + + return + + if asn1Spec is None: + for asn1Object in self._decodeComponentsSchemaless( + substrate, tagSet=tagSet, decodeFun=decodeFun, + length=length, **options): + if isinstance(asn1Object, SubstrateUnderrunError): + yield asn1Object + + if substrate.tell() < original_position + length: + if LOG: + for trailing in readFromStream(substrate, context=options): + if isinstance(trailing, SubstrateUnderrunError): + yield trailing + + LOG('Unused trailing %d octets encountered: %s' % ( + len(trailing), debug.hexdump(trailing))) + + yield asn1Object + + return + + asn1Object = asn1Spec.clone() + asn1Object.clear() + + options = self._passAsn1Object(asn1Object, options) + + if asn1Spec.typeId in (univ.Sequence.typeId, univ.Set.typeId): + + namedTypes = asn1Spec.componentType + + isSetType = asn1Spec.typeId == univ.Set.typeId + isDeterministic = not isSetType and not namedTypes.hasOptionalOrDefault + + if LOG: + LOG('decoding %sdeterministic %s type %r chosen by type ID' % ( + not isDeterministic and 'non-' or '', isSetType and 'SET' or '', + asn1Spec)) + + seenIndices = set() + idx = 0 + while substrate.tell() - original_position < length: + if not namedTypes: + componentType = None + + elif isSetType: + componentType = namedTypes.tagMapUnique + + else: + try: + if isDeterministic: + componentType = namedTypes[idx].asn1Object + + elif namedTypes[idx].isOptional or namedTypes[idx].isDefaulted: + componentType = namedTypes.getTagMapNearPosition(idx) + + else: + componentType = namedTypes[idx].asn1Object + + except IndexError: + raise error.PyAsn1Error( + 'Excessive components decoded at %r' % (asn1Spec,) + ) + + for component in decodeFun(substrate, componentType, **options): + if isinstance(component, SubstrateUnderrunError): + yield component + + if not isDeterministic and namedTypes: + if isSetType: + idx = namedTypes.getPositionByType(component.effectiveTagSet) + + elif namedTypes[idx].isOptional or namedTypes[idx].isDefaulted: + idx = namedTypes.getPositionNearType(component.effectiveTagSet, idx) + + asn1Object.setComponentByPosition( + idx, component, + verifyConstraints=False, + matchTags=False, matchConstraints=False + ) + + seenIndices.add(idx) + idx += 1 + + if LOG: + LOG('seen component indices %s' % seenIndices) + + if namedTypes: + if not namedTypes.requiredComponents.issubset(seenIndices): + raise error.PyAsn1Error( + 'ASN.1 object %s has uninitialized ' + 'components' % asn1Object.__class__.__name__) + + if namedTypes.hasOpenTypes: + + openTypes = options.get('openTypes', {}) + + if LOG: + LOG('user-specified open types map:') + + for k, v in openTypes.items(): + LOG('%s -> %r' % (k, v)) + + if openTypes or options.get('decodeOpenTypes', False): + + for idx, namedType in enumerate(namedTypes.namedTypes): + if not namedType.openType: + continue + + if namedType.isOptional and not asn1Object.getComponentByPosition(idx).isValue: + continue + + governingValue = asn1Object.getComponentByName( + namedType.openType.name + ) + + try: + openType = openTypes[governingValue] + + except KeyError: + + if LOG: + LOG('default open types map of component ' + '"%s.%s" governed by component "%s.%s"' + ':' % (asn1Object.__class__.__name__, + namedType.name, + asn1Object.__class__.__name__, + namedType.openType.name)) + + for k, v in namedType.openType.items(): + LOG('%s -> %r' % (k, v)) + + try: + openType = namedType.openType[governingValue] + + except KeyError: + if LOG: + LOG('failed to resolve open type by governing ' + 'value %r' % (governingValue,)) + continue + + if LOG: + LOG('resolved open type %r by governing ' + 'value %r' % (openType, governingValue)) + + containerValue = asn1Object.getComponentByPosition(idx) + + if containerValue.typeId in ( + univ.SetOf.typeId, univ.SequenceOf.typeId): + + for pos, containerElement in enumerate( + containerValue): + + stream = asSeekableStream(containerValue[pos].asOctets()) + + for component in decodeFun(stream, asn1Spec=openType, **options): + if isinstance(component, SubstrateUnderrunError): + yield component + + containerValue[pos] = component + + else: + stream = asSeekableStream(asn1Object.getComponentByPosition(idx).asOctets()) + + for component in decodeFun(stream, asn1Spec=openType, **options): + if isinstance(component, SubstrateUnderrunError): + yield component + + asn1Object.setComponentByPosition(idx, component) + + else: + inconsistency = asn1Object.isInconsistent + if inconsistency: + raise error.PyAsn1Error( + f"ASN.1 object {asn1Object.__class__.__name__} is inconsistent") + + else: + componentType = asn1Spec.componentType + + if LOG: + LOG('decoding type %r chosen by given `asn1Spec`' % componentType) + + idx = 0 + + while substrate.tell() - original_position < length: + for component in decodeFun(substrate, componentType, **options): + if isinstance(component, SubstrateUnderrunError): + yield component + + asn1Object.setComponentByPosition( + idx, component, + verifyConstraints=False, + matchTags=False, matchConstraints=False + ) + + idx += 1 + + yield asn1Object + + def indefLenValueDecoder(self, substrate, asn1Spec, + tagSet=None, length=None, state=None, + decodeFun=None, substrateFun=None, + **options): + if tagSet[0].tagFormat != tag.tagFormatConstructed: + raise error.PyAsn1Error('Constructed tag format expected') + + if substrateFun is not None: + if asn1Spec is not None: + asn1Object = asn1Spec.clone() + + elif self.protoComponent is not None: + asn1Object = self.protoComponent.clone(tagSet=tagSet) + + else: + asn1Object = self.protoRecordComponent, self.protoSequenceComponent + + for chunk in substrateFun(asn1Object, substrate, length, options): + yield chunk + + return + + if asn1Spec is None: + for asn1Object in self._decodeComponentsSchemaless( + substrate, tagSet=tagSet, decodeFun=decodeFun, + length=length, **dict(options, allowEoo=True)): + if isinstance(asn1Object, SubstrateUnderrunError): + yield asn1Object + + yield asn1Object + + return + + asn1Object = asn1Spec.clone() + asn1Object.clear() + + options = self._passAsn1Object(asn1Object, options) + + if asn1Spec.typeId in (univ.Sequence.typeId, univ.Set.typeId): + + namedTypes = asn1Object.componentType + + isSetType = asn1Object.typeId == univ.Set.typeId + isDeterministic = not isSetType and not namedTypes.hasOptionalOrDefault + + if LOG: + LOG('decoding %sdeterministic %s type %r chosen by type ID' % ( + not isDeterministic and 'non-' or '', isSetType and 'SET' or '', + asn1Spec)) + + seenIndices = set() + + idx = 0 + + while True: # loop over components + if len(namedTypes) <= idx: + asn1Spec = None + + elif isSetType: + asn1Spec = namedTypes.tagMapUnique + + else: + try: + if isDeterministic: + asn1Spec = namedTypes[idx].asn1Object + + elif namedTypes[idx].isOptional or namedTypes[idx].isDefaulted: + asn1Spec = namedTypes.getTagMapNearPosition(idx) + + else: + asn1Spec = namedTypes[idx].asn1Object + + except IndexError: + raise error.PyAsn1Error( + 'Excessive components decoded at %r' % (asn1Object,) + ) + + for component in decodeFun(substrate, asn1Spec, allowEoo=True, **options): + + if isinstance(component, SubstrateUnderrunError): + yield component + + if component is eoo.endOfOctets: + break + + if component is eoo.endOfOctets: + break + + if not isDeterministic and namedTypes: + if isSetType: + idx = namedTypes.getPositionByType(component.effectiveTagSet) + + elif namedTypes[idx].isOptional or namedTypes[idx].isDefaulted: + idx = namedTypes.getPositionNearType(component.effectiveTagSet, idx) + + asn1Object.setComponentByPosition( + idx, component, + verifyConstraints=False, + matchTags=False, matchConstraints=False + ) + + seenIndices.add(idx) + idx += 1 + + if LOG: + LOG('seen component indices %s' % seenIndices) + + if namedTypes: + if not namedTypes.requiredComponents.issubset(seenIndices): + raise error.PyAsn1Error( + 'ASN.1 object %s has uninitialized ' + 'components' % asn1Object.__class__.__name__) + + if namedTypes.hasOpenTypes: + + openTypes = options.get('openTypes', {}) + + if LOG: + LOG('user-specified open types map:') + + for k, v in openTypes.items(): + LOG('%s -> %r' % (k, v)) + + if openTypes or options.get('decodeOpenTypes', False): + + for idx, namedType in enumerate(namedTypes.namedTypes): + if not namedType.openType: + continue + + if namedType.isOptional and not asn1Object.getComponentByPosition(idx).isValue: + continue + + governingValue = asn1Object.getComponentByName( + namedType.openType.name + ) + + try: + openType = openTypes[governingValue] + + except KeyError: + + if LOG: + LOG('default open types map of component ' + '"%s.%s" governed by component "%s.%s"' + ':' % (asn1Object.__class__.__name__, + namedType.name, + asn1Object.__class__.__name__, + namedType.openType.name)) + + for k, v in namedType.openType.items(): + LOG('%s -> %r' % (k, v)) + + try: + openType = namedType.openType[governingValue] + + except KeyError: + if LOG: + LOG('failed to resolve open type by governing ' + 'value %r' % (governingValue,)) + continue + + if LOG: + LOG('resolved open type %r by governing ' + 'value %r' % (openType, governingValue)) + + containerValue = asn1Object.getComponentByPosition(idx) + + if containerValue.typeId in ( + univ.SetOf.typeId, univ.SequenceOf.typeId): + + for pos, containerElement in enumerate( + containerValue): + + stream = asSeekableStream(containerValue[pos].asOctets()) + + for component in decodeFun(stream, asn1Spec=openType, + **dict(options, allowEoo=True)): + if isinstance(component, SubstrateUnderrunError): + yield component + + if component is eoo.endOfOctets: + break + + containerValue[pos] = component + + else: + stream = asSeekableStream(asn1Object.getComponentByPosition(idx).asOctets()) + for component in decodeFun(stream, asn1Spec=openType, + **dict(options, allowEoo=True)): + if isinstance(component, SubstrateUnderrunError): + yield component + + if component is eoo.endOfOctets: + break + + asn1Object.setComponentByPosition(idx, component) + + else: + inconsistency = asn1Object.isInconsistent + if inconsistency: + raise error.PyAsn1Error( + f"ASN.1 object {asn1Object.__class__.__name__} is inconsistent") + + else: + componentType = asn1Spec.componentType + + if LOG: + LOG('decoding type %r chosen by given `asn1Spec`' % componentType) + + idx = 0 + + while True: + + for component in decodeFun( + substrate, componentType, allowEoo=True, **options): + + if isinstance(component, SubstrateUnderrunError): + yield component + + if component is eoo.endOfOctets: + break + + if component is eoo.endOfOctets: + break + + asn1Object.setComponentByPosition( + idx, component, + verifyConstraints=False, + matchTags=False, matchConstraints=False + ) + + idx += 1 + + yield asn1Object + + +class SequenceOrSequenceOfPayloadDecoder(ConstructedPayloadDecoderBase): + protoRecordComponent = univ.Sequence() + protoSequenceComponent = univ.SequenceOf() + + +class SequencePayloadDecoder(SequenceOrSequenceOfPayloadDecoder): + protoComponent = univ.Sequence() + + +class SequenceOfPayloadDecoder(SequenceOrSequenceOfPayloadDecoder): + protoComponent = univ.SequenceOf() + + +class SetOrSetOfPayloadDecoder(ConstructedPayloadDecoderBase): + protoRecordComponent = univ.Set() + protoSequenceComponent = univ.SetOf() + + +class SetPayloadDecoder(SetOrSetOfPayloadDecoder): + protoComponent = univ.Set() + + +class SetOfPayloadDecoder(SetOrSetOfPayloadDecoder): + protoComponent = univ.SetOf() + + +class ChoicePayloadDecoder(ConstructedPayloadDecoderBase): + protoComponent = univ.Choice() + + def valueDecoder(self, substrate, asn1Spec, + tagSet=None, length=None, state=None, + decodeFun=None, substrateFun=None, + **options): + if asn1Spec is None: + asn1Object = self.protoComponent.clone(tagSet=tagSet) + + else: + asn1Object = asn1Spec.clone() + + if substrateFun: + for chunk in substrateFun(asn1Object, substrate, length, options): + yield chunk + + return + + options = self._passAsn1Object(asn1Object, options) + + if asn1Object.tagSet == tagSet: + if LOG: + LOG('decoding %s as explicitly tagged CHOICE' % (tagSet,)) + + for component in decodeFun( + substrate, asn1Object.componentTagMap, **options): + if isinstance(component, SubstrateUnderrunError): + yield component + + else: + if LOG: + LOG('decoding %s as untagged CHOICE' % (tagSet,)) + + for component in decodeFun( + substrate, asn1Object.componentTagMap, tagSet, length, + state, **options): + if isinstance(component, SubstrateUnderrunError): + yield component + + effectiveTagSet = component.effectiveTagSet + + if LOG: + LOG('decoded component %s, effective tag set %s' % (component, effectiveTagSet)) + + asn1Object.setComponentByType( + effectiveTagSet, component, + verifyConstraints=False, + matchTags=False, matchConstraints=False, + innerFlag=False + ) + + yield asn1Object + + def indefLenValueDecoder(self, substrate, asn1Spec, + tagSet=None, length=None, state=None, + decodeFun=None, substrateFun=None, + **options): + if asn1Spec is None: + asn1Object = self.protoComponent.clone(tagSet=tagSet) + + else: + asn1Object = asn1Spec.clone() + + if substrateFun: + for chunk in substrateFun(asn1Object, substrate, length, options): + yield chunk + + return + + options = self._passAsn1Object(asn1Object, options) + + isTagged = asn1Object.tagSet == tagSet + + if LOG: + LOG('decoding %s as %stagged CHOICE' % ( + tagSet, isTagged and 'explicitly ' or 'un')) + + while True: + + if isTagged: + iterator = decodeFun( + substrate, asn1Object.componentType.tagMapUnique, + **dict(options, allowEoo=True)) + + else: + iterator = decodeFun( + substrate, asn1Object.componentType.tagMapUnique, + tagSet, length, state, **dict(options, allowEoo=True)) + + for component in iterator: + + if isinstance(component, SubstrateUnderrunError): + yield component + + if component is eoo.endOfOctets: + break + + effectiveTagSet = component.effectiveTagSet + + if LOG: + LOG('decoded component %s, effective tag set ' + '%s' % (component, effectiveTagSet)) + + asn1Object.setComponentByType( + effectiveTagSet, component, + verifyConstraints=False, + matchTags=False, matchConstraints=False, + innerFlag=False + ) + + if not isTagged: + break + + if not isTagged or component is eoo.endOfOctets: + break + + yield asn1Object + + +class AnyPayloadDecoder(AbstractSimplePayloadDecoder): + protoComponent = univ.Any() + + def valueDecoder(self, substrate, asn1Spec, + tagSet=None, length=None, state=None, + decodeFun=None, substrateFun=None, + **options): + if asn1Spec is None: + isUntagged = True + + elif asn1Spec.__class__ is tagmap.TagMap: + isUntagged = tagSet not in asn1Spec.tagMap + + else: + isUntagged = tagSet != asn1Spec.tagSet + + if isUntagged: + fullPosition = substrate.markedPosition + currentPosition = substrate.tell() + + substrate.seek(fullPosition, os.SEEK_SET) + length += currentPosition - fullPosition + + if LOG: + for chunk in peekIntoStream(substrate, length): + if isinstance(chunk, SubstrateUnderrunError): + yield chunk + LOG('decoding as untagged ANY, substrate ' + '%s' % debug.hexdump(chunk)) + + if substrateFun: + for chunk in substrateFun( + self._createComponent(asn1Spec, tagSet, noValue, **options), + substrate, length, options): + yield chunk + + return + + for chunk in readFromStream(substrate, length, options): + if isinstance(chunk, SubstrateUnderrunError): + yield chunk + + yield self._createComponent(asn1Spec, tagSet, chunk, **options) + + def indefLenValueDecoder(self, substrate, asn1Spec, + tagSet=None, length=None, state=None, + decodeFun=None, substrateFun=None, + **options): + if asn1Spec is None: + isTagged = False + + elif asn1Spec.__class__ is tagmap.TagMap: + isTagged = tagSet in asn1Spec.tagMap + + else: + isTagged = tagSet == asn1Spec.tagSet + + if isTagged: + # tagged Any type -- consume header substrate + chunk = b'' + + if LOG: + LOG('decoding as tagged ANY') + + else: + # TODO: Seems not to be tested + fullPosition = substrate.markedPosition + currentPosition = substrate.tell() + + substrate.seek(fullPosition, os.SEEK_SET) + for chunk in readFromStream(substrate, currentPosition - fullPosition, options): + if isinstance(chunk, SubstrateUnderrunError): + yield chunk + + if LOG: + LOG('decoding as untagged ANY, header substrate %s' % debug.hexdump(chunk)) + + # Any components do not inherit initial tag + asn1Spec = self.protoComponent + + if substrateFun and substrateFun is not self.substrateCollector: + asn1Object = self._createComponent( + asn1Spec, tagSet, noValue, **options) + + for chunk in substrateFun( + asn1Object, chunk + substrate, length + len(chunk), options): + yield chunk + + return + + if LOG: + LOG('assembling constructed serialization') + + # All inner fragments are of the same type, treat them as octet string + substrateFun = self.substrateCollector + + fragments = [chunk] + + while True: # loop over fragments + + for component in decodeFun( + substrate, asn1Spec, substrateFun=substrateFun, + allowEoo=True, **options): + + if isinstance(component, SubstrateUnderrunError): + yield component + + if component is eoo.endOfOctets: + break + + if component is eoo.endOfOctets: + break + + fragments.append(component) + + chunk = b''.join(fragments) + + if substrateFun: + yield chunk # TODO: Weird + + else: + yield self._createComponent(asn1Spec, tagSet, chunk, **options) + + +# character string types +class UTF8StringPayloadDecoder(OctetStringPayloadDecoder): + protoComponent = char.UTF8String() + + +class NumericStringPayloadDecoder(OctetStringPayloadDecoder): + protoComponent = char.NumericString() + + +class PrintableStringPayloadDecoder(OctetStringPayloadDecoder): + protoComponent = char.PrintableString() + + +class TeletexStringPayloadDecoder(OctetStringPayloadDecoder): + protoComponent = char.TeletexString() + + +class VideotexStringPayloadDecoder(OctetStringPayloadDecoder): + protoComponent = char.VideotexString() + + +class IA5StringPayloadDecoder(OctetStringPayloadDecoder): + protoComponent = char.IA5String() + + +class GraphicStringPayloadDecoder(OctetStringPayloadDecoder): + protoComponent = char.GraphicString() + + +class VisibleStringPayloadDecoder(OctetStringPayloadDecoder): + protoComponent = char.VisibleString() + + +class GeneralStringPayloadDecoder(OctetStringPayloadDecoder): + protoComponent = char.GeneralString() + + +class UniversalStringPayloadDecoder(OctetStringPayloadDecoder): + protoComponent = char.UniversalString() + + +class BMPStringPayloadDecoder(OctetStringPayloadDecoder): + protoComponent = char.BMPString() + + +# "useful" types +class ObjectDescriptorPayloadDecoder(OctetStringPayloadDecoder): + protoComponent = useful.ObjectDescriptor() + + +class GeneralizedTimePayloadDecoder(OctetStringPayloadDecoder): + protoComponent = useful.GeneralizedTime() + + +class UTCTimePayloadDecoder(OctetStringPayloadDecoder): + protoComponent = useful.UTCTime() + + +TAG_MAP = { + univ.Integer.tagSet: IntegerPayloadDecoder(), + univ.Boolean.tagSet: BooleanPayloadDecoder(), + univ.BitString.tagSet: BitStringPayloadDecoder(), + univ.OctetString.tagSet: OctetStringPayloadDecoder(), + univ.Null.tagSet: NullPayloadDecoder(), + univ.ObjectIdentifier.tagSet: ObjectIdentifierPayloadDecoder(), + univ.RelativeOID.tagSet: RelativeOIDPayloadDecoder(), + univ.Enumerated.tagSet: IntegerPayloadDecoder(), + univ.Real.tagSet: RealPayloadDecoder(), + univ.Sequence.tagSet: SequenceOrSequenceOfPayloadDecoder(), # conflicts with SequenceOf + univ.Set.tagSet: SetOrSetOfPayloadDecoder(), # conflicts with SetOf + univ.Choice.tagSet: ChoicePayloadDecoder(), # conflicts with Any + # character string types + char.UTF8String.tagSet: UTF8StringPayloadDecoder(), + char.NumericString.tagSet: NumericStringPayloadDecoder(), + char.PrintableString.tagSet: PrintableStringPayloadDecoder(), + char.TeletexString.tagSet: TeletexStringPayloadDecoder(), + char.VideotexString.tagSet: VideotexStringPayloadDecoder(), + char.IA5String.tagSet: IA5StringPayloadDecoder(), + char.GraphicString.tagSet: GraphicStringPayloadDecoder(), + char.VisibleString.tagSet: VisibleStringPayloadDecoder(), + char.GeneralString.tagSet: GeneralStringPayloadDecoder(), + char.UniversalString.tagSet: UniversalStringPayloadDecoder(), + char.BMPString.tagSet: BMPStringPayloadDecoder(), + # useful types + useful.ObjectDescriptor.tagSet: ObjectDescriptorPayloadDecoder(), + useful.GeneralizedTime.tagSet: GeneralizedTimePayloadDecoder(), + useful.UTCTime.tagSet: UTCTimePayloadDecoder() +} + +# Type-to-codec map for ambiguous ASN.1 types +TYPE_MAP = { + univ.Set.typeId: SetPayloadDecoder(), + univ.SetOf.typeId: SetOfPayloadDecoder(), + univ.Sequence.typeId: SequencePayloadDecoder(), + univ.SequenceOf.typeId: SequenceOfPayloadDecoder(), + univ.Choice.typeId: ChoicePayloadDecoder(), + univ.Any.typeId: AnyPayloadDecoder() +} + +# Put in non-ambiguous types for faster codec lookup +for typeDecoder in TAG_MAP.values(): + if typeDecoder.protoComponent is not None: + typeId = typeDecoder.protoComponent.__class__.typeId + if typeId is not None and typeId not in TYPE_MAP: + TYPE_MAP[typeId] = typeDecoder + + +(stDecodeTag, + stDecodeLength, + stGetValueDecoder, + stGetValueDecoderByAsn1Spec, + stGetValueDecoderByTag, + stTryAsExplicitTag, + stDecodeValue, + stDumpRawValue, + stErrorCondition, + stStop) = [x for x in range(10)] + + +EOO_SENTINEL = bytes((0, 0)) + + +class SingleItemDecoder(object): + defaultErrorState = stErrorCondition + #defaultErrorState = stDumpRawValue + defaultRawDecoder = AnyPayloadDecoder() + + supportIndefLength = True + + TAG_MAP = TAG_MAP + TYPE_MAP = TYPE_MAP + + def __init__(self, tagMap=_MISSING, typeMap=_MISSING, **ignored): + self._tagMap = tagMap if tagMap is not _MISSING else self.TAG_MAP + self._typeMap = typeMap if typeMap is not _MISSING else self.TYPE_MAP + + # Tag & TagSet objects caches + self._tagCache = {} + self._tagSetCache = {} + + def __call__(self, substrate, asn1Spec=None, + tagSet=None, length=None, state=stDecodeTag, + decodeFun=None, substrateFun=None, + **options): + + _nestingLevel = options.get('_nestingLevel', 0) + + if _nestingLevel > MAX_NESTING_DEPTH: + raise error.PyAsn1Error( + 'ASN.1 structure nesting depth exceeds limit (%d)' % MAX_NESTING_DEPTH + ) + + options['_nestingLevel'] = _nestingLevel + 1 + + allowEoo = options.pop('allowEoo', False) + + if LOG: + LOG('decoder called at scope %s with state %d, working with up ' + 'to %s octets of substrate: ' + '%s' % (debug.scope, state, length, substrate)) + + # Look for end-of-octets sentinel + if allowEoo and self.supportIndefLength: + + for eoo_candidate in readFromStream(substrate, 2, options): + if isinstance(eoo_candidate, SubstrateUnderrunError): + yield eoo_candidate + + if eoo_candidate == EOO_SENTINEL: + if LOG: + LOG('end-of-octets sentinel found') + yield eoo.endOfOctets + return + + else: + substrate.seek(-2, os.SEEK_CUR) + + tagMap = self._tagMap + typeMap = self._typeMap + tagCache = self._tagCache + tagSetCache = self._tagSetCache + + value = noValue + + substrate.markedPosition = substrate.tell() + + while state is not stStop: + + if state is stDecodeTag: + # Decode tag + isShortTag = True + + for firstByte in readFromStream(substrate, 1, options): + if isinstance(firstByte, SubstrateUnderrunError): + yield firstByte + + firstOctet = ord(firstByte) + + try: + lastTag = tagCache[firstOctet] + + except KeyError: + integerTag = firstOctet + tagClass = integerTag & 0xC0 + tagFormat = integerTag & 0x20 + tagId = integerTag & 0x1F + + if tagId == 0x1F: + isShortTag = False + tagOctetCount = 0 + tagId = 0 + + while True: + for integerByte in readFromStream(substrate, 1, options): + if isinstance(integerByte, SubstrateUnderrunError): + yield integerByte + + if not integerByte: + raise error.SubstrateUnderrunError( + 'Short octet stream on long tag decoding' + ) + + integerTag = ord(integerByte) + tagOctetCount += 1 + if tagOctetCount > MAX_TAG_OCTETS: + raise error.PyAsn1Error( + 'Tag ID octet count exceeds limit (%d)' % ( + MAX_TAG_OCTETS,) + ) + tagId <<= 7 + tagId |= (integerTag & 0x7F) + + if not integerTag & 0x80: + break + + lastTag = tag.Tag( + tagClass=tagClass, tagFormat=tagFormat, tagId=tagId + ) + + if isShortTag: + # cache short tags + tagCache[firstOctet] = lastTag + + if tagSet is None: + if isShortTag: + try: + tagSet = tagSetCache[firstOctet] + + except KeyError: + # base tag not recovered + tagSet = tag.TagSet((), lastTag) + tagSetCache[firstOctet] = tagSet + else: + tagSet = tag.TagSet((), lastTag) + + else: + tagSet = lastTag + tagSet + + state = stDecodeLength + + if LOG: + LOG('tag decoded into %s, decoding length' % tagSet) + + if state is stDecodeLength: + # Decode length + for firstOctet in readFromStream(substrate, 1, options): + if isinstance(firstOctet, SubstrateUnderrunError): + yield firstOctet + + firstOctet = ord(firstOctet) + + if firstOctet < 128: + length = firstOctet + + elif firstOctet > 128: + size = firstOctet & 0x7F + + if size > MAX_LENGTH_OCTETS: + raise error.PyAsn1Error( + 'BER length field size %d exceeds limit (%d)' % ( + size, MAX_LENGTH_OCTETS) + ) + + # encoded in size bytes + for encodedLength in readFromStream(substrate, size, options): + if isinstance(encodedLength, SubstrateUnderrunError): + yield encodedLength + encodedLength = list(encodedLength) + if len(encodedLength) != size: + raise error.SubstrateUnderrunError( + '%s<%s at %s' % (size, len(encodedLength), tagSet) + ) + + length = 0 + for lengthOctet in encodedLength: + length <<= 8 + length |= lengthOctet + size += 1 + + else: # 128 means indefinite + length = -1 + + if length == -1 and not self.supportIndefLength: + raise error.PyAsn1Error('Indefinite length encoding not supported by this codec') + + state = stGetValueDecoder + + if LOG: + LOG('value length decoded into %d' % length) + + if state is stGetValueDecoder: + if asn1Spec is None: + state = stGetValueDecoderByTag + + else: + state = stGetValueDecoderByAsn1Spec + # + # There're two ways of creating subtypes in ASN.1 what influences + # decoder operation. These methods are: + # 1) Either base types used in or no IMPLICIT tagging has been + # applied on subtyping. + # 2) Subtype syntax drops base type information (by means of + # IMPLICIT tagging. + # The first case allows for complete tag recovery from substrate + # while the second one requires original ASN.1 type spec for + # decoding. + # + # In either case a set of tags (tagSet) is coming from substrate + # in an incremental, tag-by-tag fashion (this is the case of + # EXPLICIT tag which is most basic). Outermost tag comes first + # from the wire. + # + if state is stGetValueDecoderByTag: + try: + concreteDecoder = tagMap[tagSet] + + except KeyError: + concreteDecoder = None + + if concreteDecoder: + state = stDecodeValue + + else: + try: + concreteDecoder = tagMap[tagSet[:1]] + + except KeyError: + concreteDecoder = None + + if concreteDecoder: + state = stDecodeValue + else: + state = stTryAsExplicitTag + + if LOG: + LOG('codec %s chosen by a built-in type, decoding %s' % (concreteDecoder and concreteDecoder.__class__.__name__ or "", state is stDecodeValue and 'value' or 'as explicit tag')) + debug.scope.push(concreteDecoder is None and '?' or concreteDecoder.protoComponent.__class__.__name__) + + if state is stGetValueDecoderByAsn1Spec: + + if asn1Spec.__class__ is tagmap.TagMap: + try: + chosenSpec = asn1Spec[tagSet] + + except KeyError: + chosenSpec = None + + if LOG: + LOG('candidate ASN.1 spec is a map of:') + + for firstOctet, v in asn1Spec.presentTypes.items(): + LOG(' %s -> %s' % (firstOctet, v.__class__.__name__)) + + if asn1Spec.skipTypes: + LOG('but neither of: ') + for firstOctet, v in asn1Spec.skipTypes.items(): + LOG(' %s -> %s' % (firstOctet, v.__class__.__name__)) + LOG('new candidate ASN.1 spec is %s, chosen by %s' % (chosenSpec is None and '' or chosenSpec.prettyPrintType(), tagSet)) + + elif tagSet == asn1Spec.tagSet or tagSet in asn1Spec.tagMap: + chosenSpec = asn1Spec + if LOG: + LOG('candidate ASN.1 spec is %s' % asn1Spec.__class__.__name__) + + else: + chosenSpec = None + + if chosenSpec is not None: + try: + # ambiguous type or just faster codec lookup + concreteDecoder = typeMap[chosenSpec.typeId] + + if LOG: + LOG('value decoder chosen for an ambiguous type by type ID %s' % (chosenSpec.typeId,)) + + except KeyError: + # use base type for codec lookup to recover untagged types + baseTagSet = tag.TagSet(chosenSpec.tagSet.baseTag, chosenSpec.tagSet.baseTag) + try: + # base type or tagged subtype + concreteDecoder = tagMap[baseTagSet] + + if LOG: + LOG('value decoder chosen by base %s' % (baseTagSet,)) + + except KeyError: + concreteDecoder = None + + if concreteDecoder: + asn1Spec = chosenSpec + state = stDecodeValue + + else: + state = stTryAsExplicitTag + + else: + concreteDecoder = None + state = stTryAsExplicitTag + + if LOG: + LOG('codec %s chosen by ASN.1 spec, decoding %s' % (state is stDecodeValue and concreteDecoder.__class__.__name__ or "", state is stDecodeValue and 'value' or 'as explicit tag')) + debug.scope.push(chosenSpec is None and '?' or chosenSpec.__class__.__name__) + + if state is stDecodeValue: + if not options.get('recursiveFlag', True) and not substrateFun: # deprecate this + def substrateFun(asn1Object, _substrate, _length, _options): + """Legacy hack to keep the recursiveFlag=False option supported. + + The decode(..., substrateFun=userCallback) option was introduced in 0.1.4 as a generalization + of the old recursiveFlag=False option. Users should pass their callback instead of using + recursiveFlag. + """ + yield asn1Object + + original_position = substrate.tell() + + if length == -1: # indef length + for value in concreteDecoder.indefLenValueDecoder( + substrate, asn1Spec, + tagSet, length, stGetValueDecoder, + self, substrateFun, **options): + if isinstance(value, SubstrateUnderrunError): + yield value + + else: + for value in concreteDecoder.valueDecoder( + substrate, asn1Spec, + tagSet, length, stGetValueDecoder, + self, substrateFun, **options): + if isinstance(value, SubstrateUnderrunError): + yield value + + bytesRead = substrate.tell() - original_position + if not substrateFun and bytesRead != length: + raise PyAsn1Error( + "Read %s bytes instead of expected %s." % (bytesRead, length)) + elif substrateFun and bytesRead > length: + # custom substrateFun may be used for partial decoding, reading less is expected there + raise PyAsn1Error( + "Read %s bytes are more than expected %s." % (bytesRead, length)) + + if LOG: + LOG('codec %s yields type %s, value:\n%s\n...' % ( + concreteDecoder.__class__.__name__, value.__class__.__name__, + isinstance(value, base.Asn1Item) and value.prettyPrint() or value)) + + state = stStop + break + + if state is stTryAsExplicitTag: + if (tagSet and + tagSet[0].tagFormat == tag.tagFormatConstructed and + tagSet[0].tagClass != tag.tagClassUniversal): + # Assume explicit tagging + concreteDecoder = rawPayloadDecoder + state = stDecodeValue + + else: + concreteDecoder = None + state = self.defaultErrorState + + if LOG: + LOG('codec %s chosen, decoding %s' % (concreteDecoder and concreteDecoder.__class__.__name__ or "", state is stDecodeValue and 'value' or 'as failure')) + + if state is stDumpRawValue: + concreteDecoder = self.defaultRawDecoder + + if LOG: + LOG('codec %s chosen, decoding value' % concreteDecoder.__class__.__name__) + + state = stDecodeValue + + if state is stErrorCondition: + raise error.PyAsn1Error( + '%s not in asn1Spec: %r' % (tagSet, asn1Spec) + ) + + if LOG: + debug.scope.pop() + LOG('decoder left scope %s, call completed' % debug.scope) + + yield value + + +class StreamingDecoder(object): + """Create an iterator that turns BER/CER/DER byte stream into ASN.1 objects. + + On each iteration, consume whatever BER/CER/DER serialization is + available in the `substrate` stream-like object and turns it into + one or more, possibly nested, ASN.1 objects. + + Parameters + ---------- + substrate: :py:class:`file`, :py:class:`io.BytesIO` + BER/CER/DER serialization in form of a byte stream + + Keyword Args + ------------ + asn1Spec: :py:class:`~pyasn1.type.base.PyAsn1Item` + A pyasn1 type object to act as a template guiding the decoder. + Depending on the ASN.1 structure being decoded, `asn1Spec` may + or may not be required. One of the reasons why `asn1Spec` may + me required is that ASN.1 structure is encoded in the *IMPLICIT* + tagging mode. + + Yields + ------ + : :py:class:`~pyasn1.type.base.PyAsn1Item`, :py:class:`~pyasn1.error.SubstrateUnderrunError` + Decoded ASN.1 object (possibly, nested) or + :py:class:`~pyasn1.error.SubstrateUnderrunError` object indicating + insufficient BER/CER/DER serialization on input to fully recover ASN.1 + objects from it. + + In the latter case the caller is advised to ensure some more data in + the input stream, then call the iterator again. The decoder will resume + the decoding process using the newly arrived data. + + The `context` property of :py:class:`~pyasn1.error.SubstrateUnderrunError` + object might hold a reference to the partially populated ASN.1 object + being reconstructed. + + Raises + ------ + ~pyasn1.error.PyAsn1Error, ~pyasn1.error.EndOfStreamError + `PyAsn1Error` on deserialization error, `EndOfStreamError` on + premature stream closure. + + Examples + -------- + Decode BER serialisation without ASN.1 schema + + .. code-block:: pycon + + >>> stream = io.BytesIO( + ... b'0\t\x02\x01\x01\x02\x01\x02\x02\x01\x03') + >>> + >>> for asn1Object in StreamingDecoder(stream): + ... print(asn1Object) + >>> + SequenceOf: + 1 2 3 + + Decode BER serialisation with ASN.1 schema + + .. code-block:: pycon + + >>> stream = io.BytesIO( + ... b'0\t\x02\x01\x01\x02\x01\x02\x02\x01\x03') + >>> + >>> schema = SequenceOf(componentType=Integer()) + >>> + >>> decoder = StreamingDecoder(stream, asn1Spec=schema) + >>> for asn1Object in decoder: + ... print(asn1Object) + >>> + SequenceOf: + 1 2 3 + """ + + SINGLE_ITEM_DECODER = SingleItemDecoder + + def __init__(self, substrate, asn1Spec=None, **options): + self._singleItemDecoder = self.SINGLE_ITEM_DECODER(**options) + self._substrate = asSeekableStream(substrate) + self._asn1Spec = asn1Spec + self._options = options + + def __iter__(self): + while True: + for asn1Object in self._singleItemDecoder( + self._substrate, self._asn1Spec, **self._options): + yield asn1Object + + for chunk in isEndOfStream(self._substrate): + if isinstance(chunk, SubstrateUnderrunError): + yield + + break + + if chunk: + break + + +class Decoder(object): + """Create a BER decoder object. + + Parse BER/CER/DER octet-stream into one, possibly nested, ASN.1 object. + """ + STREAMING_DECODER = StreamingDecoder + + @classmethod + def __call__(cls, substrate, asn1Spec=None, **options): + """Turns BER/CER/DER octet stream into an ASN.1 object. + + Takes BER/CER/DER octet-stream in form of :py:class:`bytes` + and decode it into an ASN.1 object + (e.g. :py:class:`~pyasn1.type.base.PyAsn1Item` derivative) which + may be a scalar or an arbitrary nested structure. + + Parameters + ---------- + substrate: :py:class:`bytes` + BER/CER/DER octet-stream to parse + + Keyword Args + ------------ + asn1Spec: :py:class:`~pyasn1.type.base.PyAsn1Item` + A pyasn1 type object (:py:class:`~pyasn1.type.base.PyAsn1Item` + derivative) to act as a template guiding the decoder. + Depending on the ASN.1 structure being decoded, `asn1Spec` may or + may not be required. Most common reason for it to require is that + ASN.1 structure is encoded in *IMPLICIT* tagging mode. + + substrateFun: :py:class:`Union[ + Callable[[pyasn1.type.base.PyAsn1Item, bytes, int], + Tuple[pyasn1.type.base.PyAsn1Item, bytes]], + Callable[[pyasn1.type.base.PyAsn1Item, io.BytesIO, int, dict], + Generator[Union[pyasn1.type.base.PyAsn1Item, + pyasn1.error.SubstrateUnderrunError], + None, None]] + ]` + User callback meant to generalize special use cases like non-recursive or + partial decoding. A 3-arg non-streaming variant is supported for backwards + compatiblilty in addition to the newer 4-arg streaming variant. + The callback will receive the uninitialized object recovered from substrate + as 1st argument, the uninterpreted payload as 2nd argument, and the length + of the uninterpreted payload as 3rd argument. The streaming variant will + additionally receive the decode(..., **options) kwargs as 4th argument. + The non-streaming variant shall return an object that will be propagated + as decode() return value as 1st item, and the remainig payload for further + decode passes as 2nd item. + The streaming variant shall yield an object that will be propagated as + decode() return value, and leave the remaining payload in the stream. + + Returns + ------- + : :py:class:`tuple` + A tuple of :py:class:`~pyasn1.type.base.PyAsn1Item` object + recovered from BER/CER/DER substrate and the unprocessed trailing + portion of the `substrate` (may be empty) + + Raises + ------ + : :py:class:`~pyasn1.error.PyAsn1Error` + :py:class:`~pyasn1.error.SubstrateUnderrunError` on insufficient + input or :py:class:`~pyasn1.error.PyAsn1Error` on decoding error. + + Examples + -------- + Decode BER/CER/DER serialisation without ASN.1 schema + + .. code-block:: pycon + + >>> s, unprocessed = decode(b'0\t\x02\x01\x01\x02\x01\x02\x02\x01\x03') + >>> str(s) + SequenceOf: + 1 2 3 + + Decode BER/CER/DER serialisation with ASN.1 schema + + .. code-block:: pycon + + >>> seq = SequenceOf(componentType=Integer()) + >>> s, unprocessed = decode( + b'0\t\x02\x01\x01\x02\x01\x02\x02\x01\x03', asn1Spec=seq) + >>> str(s) + SequenceOf: + 1 2 3 + + """ + substrate = asSeekableStream(substrate) + + if "substrateFun" in options: + origSubstrateFun = options["substrateFun"] + + def substrateFunWrapper(asn1Object, substrate, length, options=None): + """Support both 0.4 and 0.5 style APIs. + + substrateFun API has changed in 0.5 for use with streaming decoders. To stay backwards compatible, + we first try if we received a streaming user callback. If that fails,we assume we've received a + non-streaming v0.4 user callback and convert it for streaming on the fly + """ + try: + substrate_gen = origSubstrateFun(asn1Object, substrate, length, options) + except TypeError as _value: + if _value.__traceback__.tb_next: + # Traceback depth > 1 means TypeError from inside user provided function + raise + # invariant maintained at Decoder.__call__ entry + assert isinstance(substrate, io.BytesIO) # nosec assert_used + substrate_gen = Decoder._callSubstrateFunV4asV5(origSubstrateFun, asn1Object, substrate, length) + for value in substrate_gen: + yield value + + options["substrateFun"] = substrateFunWrapper + + streamingDecoder = cls.STREAMING_DECODER( + substrate, asn1Spec, **options) + + for asn1Object in streamingDecoder: + if isinstance(asn1Object, SubstrateUnderrunError): + raise error.SubstrateUnderrunError('Short substrate on input') + + try: + tail = next(readFromStream(substrate)) + + except error.EndOfStreamError: + tail = b'' + + return asn1Object, tail + + @staticmethod + def _callSubstrateFunV4asV5(substrateFunV4, asn1Object, substrate, length): + substrate_bytes = substrate.read() + if length == -1: + length = len(substrate_bytes) + value, nextSubstrate = substrateFunV4(asn1Object, substrate_bytes, length) + nbytes = substrate.write(nextSubstrate) + substrate.truncate() + substrate.seek(-nbytes, os.SEEK_CUR) + yield value + +#: Turns BER octet stream into an ASN.1 object. +#: +#: Takes BER octet-stream and decode it into an ASN.1 object +#: (e.g. :py:class:`~pyasn1.type.base.PyAsn1Item` derivative) which +#: may be a scalar or an arbitrary nested structure. +#: +#: Parameters +#: ---------- +#: substrate: :py:class:`bytes` +#: BER octet-stream +#: +#: Keyword Args +#: ------------ +#: asn1Spec: any pyasn1 type object e.g. :py:class:`~pyasn1.type.base.PyAsn1Item` derivative +#: A pyasn1 type object to act as a template guiding the decoder. Depending on the ASN.1 structure +#: being decoded, *asn1Spec* may or may not be required. Most common reason for +#: it to require is that ASN.1 structure is encoded in *IMPLICIT* tagging mode. +#: +#: Returns +#: ------- +#: : :py:class:`tuple` +#: A tuple of pyasn1 object recovered from BER substrate (:py:class:`~pyasn1.type.base.PyAsn1Item` derivative) +#: and the unprocessed trailing portion of the *substrate* (may be empty) +#: +#: Raises +#: ------ +#: ~pyasn1.error.PyAsn1Error, ~pyasn1.error.SubstrateUnderrunError +#: On decoding errors +#: +#: Notes +#: ----- +#: This function is deprecated. Please use :py:class:`Decoder` or +#: :py:class:`StreamingDecoder` class instance. +#: +#: Examples +#: -------- +#: Decode BER serialisation without ASN.1 schema +#: +#: .. code-block:: pycon +#: +#: >>> s, _ = decode(b'0\t\x02\x01\x01\x02\x01\x02\x02\x01\x03') +#: >>> str(s) +#: SequenceOf: +#: 1 2 3 +#: +#: Decode BER serialisation with ASN.1 schema +#: +#: .. code-block:: pycon +#: +#: >>> seq = SequenceOf(componentType=Integer()) +#: >>> s, _ = decode(b'0\t\x02\x01\x01\x02\x01\x02\x02\x01\x03', asn1Spec=seq) +#: >>> str(s) +#: SequenceOf: +#: 1 2 3 +#: +decode = Decoder() + +def __getattr__(attr: str): + if newAttr := {"tagMap": "TAG_MAP", "typeMap": "TYPE_MAP"}.get(attr): + warnings.warn(f"{attr} is deprecated. Please use {newAttr} instead.", DeprecationWarning, stacklevel=2) + return globals()[newAttr] + raise AttributeError(attr) diff --git a/module/scripts/lib/pyasn1/codec/ber/encoder.py b/module/scripts/lib/pyasn1/codec/ber/encoder.py new file mode 100644 index 0000000..f06b19e --- /dev/null +++ b/module/scripts/lib/pyasn1/codec/ber/encoder.py @@ -0,0 +1,954 @@ +# +# This file is part of pyasn1 software. +# +# Copyright (c) 2005-2020, Ilya Etingof +# License: https://pyasn1.readthedocs.io/en/latest/license.html +# +import sys +import warnings + +from pyasn1 import debug +from pyasn1 import error +from pyasn1.codec.ber import eoo +from pyasn1.compat import _MISSING +from pyasn1.compat.integer import to_bytes +from pyasn1.type import char +from pyasn1.type import tag +from pyasn1.type import univ +from pyasn1.type import useful + +__all__ = ['Encoder', 'encode'] + +LOG = debug.registerLoggee(__name__, flags=debug.DEBUG_ENCODER) + + +class AbstractItemEncoder(object): + supportIndefLenMode = True + + # An outcome of otherwise legit call `encodeFun(eoo.endOfOctets)` + eooIntegerSubstrate = (0, 0) + eooOctetsSubstrate = bytes(eooIntegerSubstrate) + + # noinspection PyMethodMayBeStatic + def encodeTag(self, singleTag, isConstructed): + tagClass, tagFormat, tagId = singleTag + encodedTag = tagClass | tagFormat + if isConstructed: + encodedTag |= tag.tagFormatConstructed + + if tagId < 31: + return encodedTag | tagId, + + else: + substrate = tagId & 0x7f, + + tagId >>= 7 + + while tagId: + substrate = (0x80 | (tagId & 0x7f),) + substrate + tagId >>= 7 + + return (encodedTag | 0x1F,) + substrate + + def encodeLength(self, length, defMode): + if not defMode and self.supportIndefLenMode: + return (0x80,) + + if length < 0x80: + return length, + + else: + substrate = () + while length: + substrate = (length & 0xff,) + substrate + length >>= 8 + + substrateLen = len(substrate) + + if substrateLen > 126: + raise error.PyAsn1Error('Length octets overflow (%d)' % substrateLen) + + return (0x80 | substrateLen,) + substrate + + def encodeValue(self, value, asn1Spec, encodeFun, **options): + raise error.PyAsn1Error('Not implemented') + + def encode(self, value, asn1Spec=None, encodeFun=None, **options): + + if asn1Spec is None: + tagSet = value.tagSet + else: + tagSet = asn1Spec.tagSet + + # untagged item? + if not tagSet: + substrate, isConstructed, isOctets = self.encodeValue( + value, asn1Spec, encodeFun, **options + ) + return substrate + + defMode = options.get('defMode', True) + + substrate = b'' + + for idx, singleTag in enumerate(tagSet.superTags): + + defModeOverride = defMode + + # base tag? + if not idx: + try: + substrate, isConstructed, isOctets = self.encodeValue( + value, asn1Spec, encodeFun, **options + ) + + except error.PyAsn1Error as exc: + raise error.PyAsn1Error( + 'Error encoding %r: %s' % (value, exc)) + + if LOG: + LOG('encoded %svalue %s into %s' % ( + isConstructed and 'constructed ' or '', value, substrate + )) + + if not substrate and isConstructed and options.get('ifNotEmpty', False): + return substrate + + if not isConstructed: + defModeOverride = True + + if LOG: + LOG('overridden encoding mode into definitive for primitive type') + + header = self.encodeTag(singleTag, isConstructed) + + if LOG: + LOG('encoded %stag %s into %s' % ( + isConstructed and 'constructed ' or '', + singleTag, debug.hexdump(bytes(header)))) + + header += self.encodeLength(len(substrate), defModeOverride) + + if LOG: + LOG('encoded %s octets (tag + payload) into %s' % ( + len(substrate), debug.hexdump(bytes(header)))) + + if isOctets: + substrate = bytes(header) + substrate + + if not defModeOverride: + substrate += self.eooOctetsSubstrate + + else: + substrate = header + substrate + + if not defModeOverride: + substrate += self.eooIntegerSubstrate + + if not isOctets: + substrate = bytes(substrate) + + return substrate + + +class EndOfOctetsEncoder(AbstractItemEncoder): + def encodeValue(self, value, asn1Spec, encodeFun, **options): + return b'', False, True + + +class BooleanEncoder(AbstractItemEncoder): + supportIndefLenMode = False + + def encodeValue(self, value, asn1Spec, encodeFun, **options): + return value and (1,) or (0,), False, False + + +class IntegerEncoder(AbstractItemEncoder): + supportIndefLenMode = False + supportCompactZero = False + + def encodeValue(self, value, asn1Spec, encodeFun, **options): + if value == 0: + if LOG: + LOG('encoding %spayload for zero INTEGER' % ( + self.supportCompactZero and 'no ' or '' + )) + + # de-facto way to encode zero + if self.supportCompactZero: + return (), False, False + else: + return (0,), False, False + + return to_bytes(int(value), signed=True), False, True + + +class BitStringEncoder(AbstractItemEncoder): + def encodeValue(self, value, asn1Spec, encodeFun, **options): + if asn1Spec is not None: + # TODO: try to avoid ASN.1 schema instantiation + value = asn1Spec.clone(value) + + valueLength = len(value) + if valueLength % 8: + alignedValue = value << (8 - valueLength % 8) + else: + alignedValue = value + + maxChunkSize = options.get('maxChunkSize', 0) + if not maxChunkSize or len(alignedValue) <= maxChunkSize * 8: + substrate = alignedValue.asOctets() + return bytes((len(substrate) * 8 - valueLength,)) + substrate, False, True + + if LOG: + LOG('encoding into up to %s-octet chunks' % maxChunkSize) + + baseTag = value.tagSet.baseTag + + # strip off explicit tags + if baseTag: + tagSet = tag.TagSet(baseTag, baseTag) + + else: + tagSet = tag.TagSet() + + alignedValue = alignedValue.clone(tagSet=tagSet) + + stop = 0 + substrate = b'' + while stop < valueLength: + start = stop + stop = min(start + maxChunkSize * 8, valueLength) + substrate += encodeFun(alignedValue[start:stop], asn1Spec, **options) + + return substrate, True, True + + +class OctetStringEncoder(AbstractItemEncoder): + + def encodeValue(self, value, asn1Spec, encodeFun, **options): + + if asn1Spec is None: + substrate = value.asOctets() + + elif not isinstance(value, bytes): + substrate = asn1Spec.clone(value).asOctets() + + else: + substrate = value + + maxChunkSize = options.get('maxChunkSize', 0) + + if not maxChunkSize or len(substrate) <= maxChunkSize: + return substrate, False, True + + if LOG: + LOG('encoding into up to %s-octet chunks' % maxChunkSize) + + # strip off explicit tags for inner chunks + + if asn1Spec is None: + baseTag = value.tagSet.baseTag + + # strip off explicit tags + if baseTag: + tagSet = tag.TagSet(baseTag, baseTag) + + else: + tagSet = tag.TagSet() + + asn1Spec = value.clone(tagSet=tagSet) + + elif not isinstance(value, bytes): + baseTag = asn1Spec.tagSet.baseTag + + # strip off explicit tags + if baseTag: + tagSet = tag.TagSet(baseTag, baseTag) + + else: + tagSet = tag.TagSet() + + asn1Spec = asn1Spec.clone(tagSet=tagSet) + + pos = 0 + substrate = b'' + + while True: + chunk = value[pos:pos + maxChunkSize] + if not chunk: + break + + substrate += encodeFun(chunk, asn1Spec, **options) + pos += maxChunkSize + + return substrate, True, True + + +class NullEncoder(AbstractItemEncoder): + supportIndefLenMode = False + + def encodeValue(self, value, asn1Spec, encodeFun, **options): + return b'', False, True + + +class ObjectIdentifierEncoder(AbstractItemEncoder): + supportIndefLenMode = False + + def encodeValue(self, value, asn1Spec, encodeFun, **options): + if asn1Spec is not None: + value = asn1Spec.clone(value) + + oid = value.asTuple() + + # Build the first pair + try: + first = oid[0] + second = oid[1] + + except IndexError: + raise error.PyAsn1Error('Short OID %s' % (value,)) + + if 0 <= second <= 39: + if first == 1: + oid = (second + 40,) + oid[2:] + elif first == 0: + oid = (second,) + oid[2:] + elif first == 2: + oid = (second + 80,) + oid[2:] + else: + raise error.PyAsn1Error('Impossible first/second arcs at %s' % (value,)) + + elif first == 2: + oid = (second + 80,) + oid[2:] + + else: + raise error.PyAsn1Error('Impossible first/second arcs at %s' % (value,)) + + octets = [] + + # Cycle through subIds + for subOid in oid: + if 0 <= subOid <= 127: + # Optimize for the common case + octets.append(subOid) + + elif subOid > 127: + # Pack large Sub-Object IDs + res = [subOid & 0x7f] + subOid >>= 7 + + while subOid: + res.append(0x80 | (subOid & 0x7f)) + subOid >>= 7 + + # Add packed Sub-Object ID to resulted Object ID + octets.extend(reversed(res)) + + else: + raise error.PyAsn1Error('Negative OID arc %s at %s' % (subOid, value)) + + return tuple(octets), False, False + + +class RelativeOIDEncoder(AbstractItemEncoder): + supportIndefLenMode = False + + def encodeValue(self, value, asn1Spec, encodeFun, **options): + if asn1Spec is not None: + value = asn1Spec.clone(value) + + octets = [] + + # Cycle through subIds + for subOid in value.asTuple(): + if 0 <= subOid <= 127: + # Optimize for the common case + octets.append(subOid) + + elif subOid > 127: + # Pack large Sub-Object IDs + res = [subOid & 0x7f] + subOid >>= 7 + + while subOid: + res.append(0x80 | (subOid & 0x7f)) + subOid >>= 7 + + # Add packed Sub-Object ID to resulted RELATIVE-OID + octets.extend(reversed(res)) + + else: + raise error.PyAsn1Error('Negative RELATIVE-OID arc %s at %s' % (subOid, value)) + + return tuple(octets), False, False + + +class RealEncoder(AbstractItemEncoder): + supportIndefLenMode = False + binEncBase = 2 # set to None to choose encoding base automatically + + @staticmethod + def _dropFloatingPoint(m, encbase, e): + ms, es = 1, 1 + if m < 0: + ms = -1 # mantissa sign + + if e < 0: + es = -1 # exponent sign + + m *= ms + + if encbase == 8: + m *= 2 ** (abs(e) % 3 * es) + e = abs(e) // 3 * es + + elif encbase == 16: + m *= 2 ** (abs(e) % 4 * es) + e = abs(e) // 4 * es + + while True: + if int(m) != m: + m *= encbase + e -= 1 + continue + break + + return ms, int(m), encbase, e + + def _chooseEncBase(self, value): + m, b, e = value + encBase = [2, 8, 16] + if value.binEncBase in encBase: + return self._dropFloatingPoint(m, value.binEncBase, e) + + elif self.binEncBase in encBase: + return self._dropFloatingPoint(m, self.binEncBase, e) + + # auto choosing base 2/8/16 + mantissa = [m, m, m] + exponent = [e, e, e] + sign = 1 + encbase = 2 + e = float('inf') + + for i in range(3): + (sign, + mantissa[i], + encBase[i], + exponent[i]) = self._dropFloatingPoint(mantissa[i], encBase[i], exponent[i]) + + if abs(exponent[i]) < abs(e) or (abs(exponent[i]) == abs(e) and mantissa[i] < m): + e = exponent[i] + m = int(mantissa[i]) + encbase = encBase[i] + + if LOG: + LOG('automatically chosen REAL encoding base %s, sign %s, mantissa %s, ' + 'exponent %s' % (encbase, sign, m, e)) + + return sign, m, encbase, e + + def encodeValue(self, value, asn1Spec, encodeFun, **options): + if asn1Spec is not None: + value = asn1Spec.clone(value) + + if value.isPlusInf: + return (0x40,), False, False + + if value.isMinusInf: + return (0x41,), False, False + + m, b, e = value + + if not m: + return b'', False, True + + if b == 10: + if LOG: + LOG('encoding REAL into character form') + + return b'\x03%dE%s%d' % (m, e == 0 and b'+' or b'', e), False, True + + elif b == 2: + fo = 0x80 # binary encoding + ms, m, encbase, e = self._chooseEncBase(value) + + if ms < 0: # mantissa sign + fo |= 0x40 # sign bit + + # exponent & mantissa normalization + if encbase == 2: + while m & 0x1 == 0: + m >>= 1 + e += 1 + + elif encbase == 8: + while m & 0x7 == 0: + m >>= 3 + e += 1 + fo |= 0x10 + + else: # encbase = 16 + while m & 0xf == 0: + m >>= 4 + e += 1 + fo |= 0x20 + + sf = 0 # scale factor + + while m & 0x1 == 0: + m >>= 1 + sf += 1 + + if sf > 3: + raise error.PyAsn1Error('Scale factor overflow') # bug if raised + + fo |= sf << 2 + eo = b'' + if e == 0 or e == -1: + eo = bytes((e & 0xff,)) + + else: + while e not in (0, -1): + eo = bytes((e & 0xff,)) + eo + e >>= 8 + + if e == 0 and eo and eo[0] & 0x80: + eo = bytes((0,)) + eo + + if e == -1 and eo and not (eo[0] & 0x80): + eo = bytes((0xff,)) + eo + + n = len(eo) + if n > 0xff: + raise error.PyAsn1Error('Real exponent overflow') + + if n == 1: + pass + + elif n == 2: + fo |= 1 + + elif n == 3: + fo |= 2 + + else: + fo |= 3 + eo = bytes((n & 0xff,)) + eo + + po = b'' + + while m: + po = bytes((m & 0xff,)) + po + m >>= 8 + + substrate = bytes((fo,)) + eo + po + + return substrate, False, True + + else: + raise error.PyAsn1Error('Prohibited Real base %s' % b) + + +class SequenceEncoder(AbstractItemEncoder): + omitEmptyOptionals = False + + # TODO: handling three flavors of input is too much -- split over codecs + + def encodeValue(self, value, asn1Spec, encodeFun, **options): + + substrate = b'' + + omitEmptyOptionals = options.get( + 'omitEmptyOptionals', self.omitEmptyOptionals) + + if LOG: + LOG('%sencoding empty OPTIONAL components' % ( + omitEmptyOptionals and 'not ' or '')) + + if asn1Spec is None: + # instance of ASN.1 schema + inconsistency = value.isInconsistent + if inconsistency: + raise error.PyAsn1Error( + f"ASN.1 object {value.__class__.__name__} is inconsistent") + + namedTypes = value.componentType + + for idx, component in enumerate(value.values()): + if namedTypes: + namedType = namedTypes[idx] + + if namedType.isOptional and not component.isValue: + if LOG: + LOG('not encoding OPTIONAL component %r' % (namedType,)) + continue + + if namedType.isDefaulted and component == namedType.asn1Object: + if LOG: + LOG('not encoding DEFAULT component %r' % (namedType,)) + continue + + if omitEmptyOptionals: + options.update(ifNotEmpty=namedType.isOptional) + + # wrap open type blob if needed + if namedTypes and namedType.openType: + + wrapType = namedType.asn1Object + + if wrapType.typeId in ( + univ.SetOf.typeId, univ.SequenceOf.typeId): + + substrate += encodeFun( + component, asn1Spec, + **dict(options, wrapType=wrapType.componentType)) + + else: + chunk = encodeFun(component, asn1Spec, **options) + + if wrapType.isSameTypeWith(component): + substrate += chunk + + else: + substrate += encodeFun(chunk, wrapType, **options) + + if LOG: + LOG('wrapped with wrap type %r' % (wrapType,)) + + else: + substrate += encodeFun(component, asn1Spec, **options) + + else: + # bare Python value + ASN.1 schema + for idx, namedType in enumerate(asn1Spec.componentType.namedTypes): + + try: + component = value[namedType.name] + + except KeyError: + raise error.PyAsn1Error('Component name "%s" not found in %r' % ( + namedType.name, value)) + + if namedType.isOptional and namedType.name not in value: + if LOG: + LOG('not encoding OPTIONAL component %r' % (namedType,)) + continue + + if namedType.isDefaulted and component == namedType.asn1Object: + if LOG: + LOG('not encoding DEFAULT component %r' % (namedType,)) + continue + + if omitEmptyOptionals: + options.update(ifNotEmpty=namedType.isOptional) + + componentSpec = namedType.asn1Object + + # wrap open type blob if needed + if namedType.openType: + + if componentSpec.typeId in ( + univ.SetOf.typeId, univ.SequenceOf.typeId): + + substrate += encodeFun( + component, componentSpec, + **dict(options, wrapType=componentSpec.componentType)) + + else: + chunk = encodeFun(component, componentSpec, **options) + + if componentSpec.isSameTypeWith(component): + substrate += chunk + + else: + substrate += encodeFun(chunk, componentSpec, **options) + + if LOG: + LOG('wrapped with wrap type %r' % (componentSpec,)) + + else: + substrate += encodeFun(component, componentSpec, **options) + + return substrate, True, True + + +class SequenceOfEncoder(AbstractItemEncoder): + def _encodeComponents(self, value, asn1Spec, encodeFun, **options): + + if asn1Spec is None: + inconsistency = value.isInconsistent + if inconsistency: + raise error.PyAsn1Error( + f"ASN.1 object {value.__class__.__name__} is inconsistent") + + else: + asn1Spec = asn1Spec.componentType + + chunks = [] + + wrapType = options.pop('wrapType', None) + + for idx, component in enumerate(value): + chunk = encodeFun(component, asn1Spec, **options) + + if (wrapType is not None and + not wrapType.isSameTypeWith(component)): + # wrap encoded value with wrapper container (e.g. ANY) + chunk = encodeFun(chunk, wrapType, **options) + + if LOG: + LOG('wrapped with wrap type %r' % (wrapType,)) + + chunks.append(chunk) + + return chunks + + def encodeValue(self, value, asn1Spec, encodeFun, **options): + chunks = self._encodeComponents( + value, asn1Spec, encodeFun, **options) + + return b''.join(chunks), True, True + + +class ChoiceEncoder(AbstractItemEncoder): + def encodeValue(self, value, asn1Spec, encodeFun, **options): + if asn1Spec is None: + component = value.getComponent() + else: + names = [namedType.name for namedType in asn1Spec.componentType.namedTypes + if namedType.name in value] + if len(names) != 1: + raise error.PyAsn1Error('%s components for Choice at %r' % (len(names) and 'Multiple ' or 'None ', value)) + + name = names[0] + + component = value[name] + asn1Spec = asn1Spec[name] + + return encodeFun(component, asn1Spec, **options), True, True + + +class AnyEncoder(OctetStringEncoder): + def encodeValue(self, value, asn1Spec, encodeFun, **options): + if asn1Spec is None: + value = value.asOctets() + elif not isinstance(value, bytes): + value = asn1Spec.clone(value).asOctets() + + return value, not options.get('defMode', True), True + + +TAG_MAP = { + eoo.endOfOctets.tagSet: EndOfOctetsEncoder(), + univ.Boolean.tagSet: BooleanEncoder(), + univ.Integer.tagSet: IntegerEncoder(), + univ.BitString.tagSet: BitStringEncoder(), + univ.OctetString.tagSet: OctetStringEncoder(), + univ.Null.tagSet: NullEncoder(), + univ.ObjectIdentifier.tagSet: ObjectIdentifierEncoder(), + univ.RelativeOID.tagSet: RelativeOIDEncoder(), + univ.Enumerated.tagSet: IntegerEncoder(), + univ.Real.tagSet: RealEncoder(), + # Sequence & Set have same tags as SequenceOf & SetOf + univ.SequenceOf.tagSet: SequenceOfEncoder(), + univ.SetOf.tagSet: SequenceOfEncoder(), + univ.Choice.tagSet: ChoiceEncoder(), + # character string types + char.UTF8String.tagSet: OctetStringEncoder(), + char.NumericString.tagSet: OctetStringEncoder(), + char.PrintableString.tagSet: OctetStringEncoder(), + char.TeletexString.tagSet: OctetStringEncoder(), + char.VideotexString.tagSet: OctetStringEncoder(), + char.IA5String.tagSet: OctetStringEncoder(), + char.GraphicString.tagSet: OctetStringEncoder(), + char.VisibleString.tagSet: OctetStringEncoder(), + char.GeneralString.tagSet: OctetStringEncoder(), + char.UniversalString.tagSet: OctetStringEncoder(), + char.BMPString.tagSet: OctetStringEncoder(), + # useful types + useful.ObjectDescriptor.tagSet: OctetStringEncoder(), + useful.GeneralizedTime.tagSet: OctetStringEncoder(), + useful.UTCTime.tagSet: OctetStringEncoder() +} + +# Put in ambiguous & non-ambiguous types for faster codec lookup +TYPE_MAP = { + univ.Boolean.typeId: BooleanEncoder(), + univ.Integer.typeId: IntegerEncoder(), + univ.BitString.typeId: BitStringEncoder(), + univ.OctetString.typeId: OctetStringEncoder(), + univ.Null.typeId: NullEncoder(), + univ.ObjectIdentifier.typeId: ObjectIdentifierEncoder(), + univ.RelativeOID.typeId: RelativeOIDEncoder(), + univ.Enumerated.typeId: IntegerEncoder(), + univ.Real.typeId: RealEncoder(), + # Sequence & Set have same tags as SequenceOf & SetOf + univ.Set.typeId: SequenceEncoder(), + univ.SetOf.typeId: SequenceOfEncoder(), + univ.Sequence.typeId: SequenceEncoder(), + univ.SequenceOf.typeId: SequenceOfEncoder(), + univ.Choice.typeId: ChoiceEncoder(), + univ.Any.typeId: AnyEncoder(), + # character string types + char.UTF8String.typeId: OctetStringEncoder(), + char.NumericString.typeId: OctetStringEncoder(), + char.PrintableString.typeId: OctetStringEncoder(), + char.TeletexString.typeId: OctetStringEncoder(), + char.VideotexString.typeId: OctetStringEncoder(), + char.IA5String.typeId: OctetStringEncoder(), + char.GraphicString.typeId: OctetStringEncoder(), + char.VisibleString.typeId: OctetStringEncoder(), + char.GeneralString.typeId: OctetStringEncoder(), + char.UniversalString.typeId: OctetStringEncoder(), + char.BMPString.typeId: OctetStringEncoder(), + # useful types + useful.ObjectDescriptor.typeId: OctetStringEncoder(), + useful.GeneralizedTime.typeId: OctetStringEncoder(), + useful.UTCTime.typeId: OctetStringEncoder() +} + + +class SingleItemEncoder(object): + fixedDefLengthMode = None + fixedChunkSize = None + + TAG_MAP = TAG_MAP + TYPE_MAP = TYPE_MAP + + def __init__(self, tagMap=_MISSING, typeMap=_MISSING, **ignored): + self._tagMap = tagMap if tagMap is not _MISSING else self.TAG_MAP + self._typeMap = typeMap if typeMap is not _MISSING else self.TYPE_MAP + + def __call__(self, value, asn1Spec=None, **options): + try: + if asn1Spec is None: + typeId = value.typeId + else: + typeId = asn1Spec.typeId + + except AttributeError: + raise error.PyAsn1Error('Value %r is not ASN.1 type instance ' + 'and "asn1Spec" not given' % (value,)) + + if LOG: + LOG('encoder called in %sdef mode, chunk size %s for type %s, ' + 'value:\n%s' % (not options.get('defMode', True) and 'in' or '', + options.get('maxChunkSize', 0), + asn1Spec is None and value.prettyPrintType() or + asn1Spec.prettyPrintType(), value)) + + if self.fixedDefLengthMode is not None: + options.update(defMode=self.fixedDefLengthMode) + + if self.fixedChunkSize is not None: + options.update(maxChunkSize=self.fixedChunkSize) + + try: + concreteEncoder = self._typeMap[typeId] + + if LOG: + LOG('using value codec %s chosen by type ID ' + '%s' % (concreteEncoder.__class__.__name__, typeId)) + + except KeyError: + if asn1Spec is None: + tagSet = value.tagSet + else: + tagSet = asn1Spec.tagSet + + # use base type for codec lookup to recover untagged types + baseTagSet = tag.TagSet(tagSet.baseTag, tagSet.baseTag) + + try: + concreteEncoder = self._tagMap[baseTagSet] + + except KeyError: + raise error.PyAsn1Error('No encoder for %r (%s)' % (value, tagSet)) + + if LOG: + LOG('using value codec %s chosen by tagSet ' + '%s' % (concreteEncoder.__class__.__name__, tagSet)) + + substrate = concreteEncoder.encode(value, asn1Spec, self, **options) + + if LOG: + LOG('codec %s built %s octets of substrate: %s\nencoder ' + 'completed' % (concreteEncoder, len(substrate), + debug.hexdump(substrate))) + + return substrate + + +class Encoder(object): + SINGLE_ITEM_ENCODER = SingleItemEncoder + + def __init__(self, tagMap=_MISSING, typeMap=_MISSING, **options): + self._singleItemEncoder = self.SINGLE_ITEM_ENCODER( + tagMap=tagMap, typeMap=typeMap, **options + ) + + def __call__(self, pyObject, asn1Spec=None, **options): + return self._singleItemEncoder( + pyObject, asn1Spec=asn1Spec, **options) + + +#: Turns ASN.1 object into BER octet stream. +#: +#: Takes any ASN.1 object (e.g. :py:class:`~pyasn1.type.base.PyAsn1Item` derivative) +#: walks all its components recursively and produces a BER octet stream. +#: +#: Parameters +#: ---------- +#: value: either a Python or pyasn1 object (e.g. :py:class:`~pyasn1.type.base.PyAsn1Item` derivative) +#: A Python or pyasn1 object to encode. If Python object is given, `asnSpec` +#: parameter is required to guide the encoding process. +#: +#: Keyword Args +#: ------------ +#: asn1Spec: +#: Optional ASN.1 schema or value object e.g. :py:class:`~pyasn1.type.base.PyAsn1Item` derivative +#: +#: defMode: :py:class:`bool` +#: If :obj:`False`, produces indefinite length encoding +#: +#: maxChunkSize: :py:class:`int` +#: Maximum chunk size in chunked encoding mode (0 denotes unlimited chunk size) +#: +#: Returns +#: ------- +#: : :py:class:`bytes` +#: Given ASN.1 object encoded into BER octetstream +#: +#: Raises +#: ------ +#: ~pyasn1.error.PyAsn1Error +#: On encoding errors +#: +#: Examples +#: -------- +#: Encode Python value into BER with ASN.1 schema +#: +#: .. code-block:: pycon +#: +#: >>> seq = SequenceOf(componentType=Integer()) +#: >>> encode([1, 2, 3], asn1Spec=seq) +#: b'0\t\x02\x01\x01\x02\x01\x02\x02\x01\x03' +#: +#: Encode ASN.1 value object into BER +#: +#: .. code-block:: pycon +#: +#: >>> seq = SequenceOf(componentType=Integer()) +#: >>> seq.extend([1, 2, 3]) +#: >>> encode(seq) +#: b'0\t\x02\x01\x01\x02\x01\x02\x02\x01\x03' +#: +encode = Encoder() + +def __getattr__(attr: str): + if newAttr := {"tagMap": "TAG_MAP", "typeMap": "TYPE_MAP"}.get(attr): + warnings.warn(f"{attr} is deprecated. Please use {newAttr} instead.", DeprecationWarning, stacklevel=2) + return globals()[newAttr] + raise AttributeError(attr) diff --git a/module/scripts/lib/pyasn1/codec/ber/eoo.py b/module/scripts/lib/pyasn1/codec/ber/eoo.py new file mode 100644 index 0000000..8c91a3d --- /dev/null +++ b/module/scripts/lib/pyasn1/codec/ber/eoo.py @@ -0,0 +1,28 @@ +# +# This file is part of pyasn1 software. +# +# Copyright (c) 2005-2020, Ilya Etingof +# License: https://pyasn1.readthedocs.io/en/latest/license.html +# +from pyasn1.type import base +from pyasn1.type import tag + +__all__ = ['endOfOctets'] + + +class EndOfOctets(base.SimpleAsn1Type): + defaultValue = 0 + tagSet = tag.initTagSet( + tag.Tag(tag.tagClassUniversal, tag.tagFormatSimple, 0x00) + ) + + _instance = None + + def __new__(cls, *args, **kwargs): + if cls._instance is None: + cls._instance = object.__new__(cls, *args, **kwargs) + + return cls._instance + + +endOfOctets = EndOfOctets() diff --git a/module/scripts/lib/pyasn1/codec/cer/__init__.py b/module/scripts/lib/pyasn1/codec/cer/__init__.py new file mode 100644 index 0000000..8c3066b --- /dev/null +++ b/module/scripts/lib/pyasn1/codec/cer/__init__.py @@ -0,0 +1 @@ +# This file is necessary to make this directory a package. diff --git a/module/scripts/lib/pyasn1/codec/cer/__pycache__/__init__.cpython-314.pyc b/module/scripts/lib/pyasn1/codec/cer/__pycache__/__init__.cpython-314.pyc new file mode 100644 index 0000000000000000000000000000000000000000..d8f559fd47126cbadb2da49c778ce4a6e7a1a281 GIT binary patch literal 215 zcmXwzJqiLb5QP&LMFhQsm54i9D;8pHVPU(B*|3M&h=5!z4{KbFJbO^3&%Z6;kH#cTl4q|i&rIiMzAc`!+q=JJ= zrG+q&naUMl?o?v%qFgmWt-%U|P8?yko~%%uOAT2Om#OrD(#}Gv_Mj`Y`HUK4DL_zr d6j7IpqEJ_Lck%sLnl_E_Gp&brz&SSo(FY@_J0Ac5 literal 0 HcmV?d00001 diff --git a/module/scripts/lib/pyasn1/codec/cer/__pycache__/decoder.cpython-314.pyc b/module/scripts/lib/pyasn1/codec/cer/__pycache__/decoder.cpython-314.pyc new file mode 100644 index 0000000000000000000000000000000000000000..c0337315e5ff61f78c611fba8d2d0ac58bf6fe97 GIT binary patch literal 4544 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 TV~DQ&jp`Zo7g}J$l*IlQq9jf& literal 0 HcmV?d00001 diff --git a/module/scripts/lib/pyasn1/codec/cer/decoder.py b/module/scripts/lib/pyasn1/codec/cer/decoder.py new file mode 100644 index 0000000..4610446 --- /dev/null +++ b/module/scripts/lib/pyasn1/codec/cer/decoder.py @@ -0,0 +1,149 @@ +# +# This file is part of pyasn1 software. +# +# Copyright (c) 2005-2020, Ilya Etingof +# License: https://pyasn1.readthedocs.io/en/latest/license.html +# +import warnings + +from pyasn1 import error +from pyasn1.codec.streaming import readFromStream +from pyasn1.codec.ber import decoder +from pyasn1.type import univ + +__all__ = ['decode', 'StreamingDecoder'] + +SubstrateUnderrunError = error.SubstrateUnderrunError + + +class BooleanPayloadDecoder(decoder.AbstractSimplePayloadDecoder): + protoComponent = univ.Boolean(0) + + def valueDecoder(self, substrate, asn1Spec, + tagSet=None, length=None, state=None, + decodeFun=None, substrateFun=None, + **options): + + if length != 1: + raise error.PyAsn1Error('Not single-octet Boolean payload') + + for chunk in readFromStream(substrate, length, options): + if isinstance(chunk, SubstrateUnderrunError): + yield chunk + + byte = chunk[0] + + # CER/DER specifies encoding of TRUE as 0xFF and FALSE as 0x0, while + # BER allows any non-zero value as TRUE; cf. sections 8.2.2. and 11.1 + # in https://www.itu.int/ITU-T/studygroups/com17/languages/X.690-0207.pdf + if byte == 0xff: + value = 1 + + elif byte == 0x00: + value = 0 + + else: + raise error.PyAsn1Error('Unexpected Boolean payload: %s' % byte) + + yield self._createComponent(asn1Spec, tagSet, value, **options) + + +# TODO: prohibit non-canonical encoding +BitStringPayloadDecoder = decoder.BitStringPayloadDecoder +OctetStringPayloadDecoder = decoder.OctetStringPayloadDecoder +RealPayloadDecoder = decoder.RealPayloadDecoder + +TAG_MAP = decoder.TAG_MAP.copy() +TAG_MAP.update( + {univ.Boolean.tagSet: BooleanPayloadDecoder(), + univ.BitString.tagSet: BitStringPayloadDecoder(), + univ.OctetString.tagSet: OctetStringPayloadDecoder(), + univ.Real.tagSet: RealPayloadDecoder()} +) + +TYPE_MAP = decoder.TYPE_MAP.copy() + +# Put in non-ambiguous types for faster codec lookup +for typeDecoder in TAG_MAP.values(): + if typeDecoder.protoComponent is not None: + typeId = typeDecoder.protoComponent.__class__.typeId + if typeId is not None and typeId not in TYPE_MAP: + TYPE_MAP[typeId] = typeDecoder + + +class SingleItemDecoder(decoder.SingleItemDecoder): + __doc__ = decoder.SingleItemDecoder.__doc__ + + TAG_MAP = TAG_MAP + TYPE_MAP = TYPE_MAP + + +class StreamingDecoder(decoder.StreamingDecoder): + __doc__ = decoder.StreamingDecoder.__doc__ + + SINGLE_ITEM_DECODER = SingleItemDecoder + + +class Decoder(decoder.Decoder): + __doc__ = decoder.Decoder.__doc__ + + STREAMING_DECODER = StreamingDecoder + + +#: Turns CER octet stream into an ASN.1 object. +#: +#: Takes CER octet-stream and decode it into an ASN.1 object +#: (e.g. :py:class:`~pyasn1.type.base.PyAsn1Item` derivative) which +#: may be a scalar or an arbitrary nested structure. +#: +#: Parameters +#: ---------- +#: substrate: :py:class:`bytes` +#: CER octet-stream +#: +#: Keyword Args +#: ------------ +#: asn1Spec: any pyasn1 type object e.g. :py:class:`~pyasn1.type.base.PyAsn1Item` derivative +#: A pyasn1 type object to act as a template guiding the decoder. Depending on the ASN.1 structure +#: being decoded, *asn1Spec* may or may not be required. Most common reason for +#: it to require is that ASN.1 structure is encoded in *IMPLICIT* tagging mode. +#: +#: Returns +#: ------- +#: : :py:class:`tuple` +#: A tuple of pyasn1 object recovered from CER substrate (:py:class:`~pyasn1.type.base.PyAsn1Item` derivative) +#: and the unprocessed trailing portion of the *substrate* (may be empty) +#: +#: Raises +#: ------ +#: ~pyasn1.error.PyAsn1Error, ~pyasn1.error.SubstrateUnderrunError +#: On decoding errors +#: +#: Examples +#: -------- +#: Decode CER serialisation without ASN.1 schema +#: +#: .. code-block:: pycon +#: +#: >>> s, _ = decode(b'0\x80\x02\x01\x01\x02\x01\x02\x02\x01\x03\x00\x00') +#: >>> str(s) +#: SequenceOf: +#: 1 2 3 +#: +#: Decode CER serialisation with ASN.1 schema +#: +#: .. code-block:: pycon +#: +#: >>> seq = SequenceOf(componentType=Integer()) +#: >>> s, _ = decode(b'0\x80\x02\x01\x01\x02\x01\x02\x02\x01\x03\x00\x00', asn1Spec=seq) +#: >>> str(s) +#: SequenceOf: +#: 1 2 3 +#: +decode = Decoder() + +def __getattr__(attr: str): + if newAttr := {"tagMap": "TAG_MAP", "typeMap": "TYPE_MAP"}.get(attr): + warnings.warn(f"{attr} is deprecated. Please use {newAttr} instead.", DeprecationWarning, stacklevel=2) + return globals()[newAttr] + raise AttributeError(attr) diff --git a/module/scripts/lib/pyasn1/codec/cer/encoder.py b/module/scripts/lib/pyasn1/codec/cer/encoder.py new file mode 100644 index 0000000..15a8064 --- /dev/null +++ b/module/scripts/lib/pyasn1/codec/cer/encoder.py @@ -0,0 +1,331 @@ +# +# This file is part of pyasn1 software. +# +# Copyright (c) 2005-2020, Ilya Etingof +# License: https://pyasn1.readthedocs.io/en/latest/license.html +# +import warnings + +from pyasn1 import error +from pyasn1.codec.ber import encoder +from pyasn1.type import univ +from pyasn1.type import useful + +__all__ = ['Encoder', 'encode'] + + +class BooleanEncoder(encoder.IntegerEncoder): + def encodeValue(self, value, asn1Spec, encodeFun, **options): + if value == 0: + substrate = (0,) + else: + substrate = (255,) + return substrate, False, False + + +class RealEncoder(encoder.RealEncoder): + def _chooseEncBase(self, value): + m, b, e = value + return self._dropFloatingPoint(m, b, e) + + +# specialized GeneralStringEncoder here + +class TimeEncoderMixIn(object): + Z_CHAR = ord('Z') + PLUS_CHAR = ord('+') + MINUS_CHAR = ord('-') + COMMA_CHAR = ord(',') + DOT_CHAR = ord('.') + ZERO_CHAR = ord('0') + + MIN_LENGTH = 12 + MAX_LENGTH = 19 + + def encodeValue(self, value, asn1Spec, encodeFun, **options): + # CER encoding constraints: + # - minutes are mandatory, seconds are optional + # - sub-seconds must NOT be zero / no meaningless zeros + # - no hanging fraction dot + # - time in UTC (Z) + # - only dot is allowed for fractions + + if asn1Spec is not None: + value = asn1Spec.clone(value) + + numbers = value.asNumbers() + + if self.PLUS_CHAR in numbers or self.MINUS_CHAR in numbers: + raise error.PyAsn1Error('Must be UTC time: %r' % value) + + if numbers[-1] != self.Z_CHAR: + raise error.PyAsn1Error('Missing "Z" time zone specifier: %r' % value) + + if self.COMMA_CHAR in numbers: + raise error.PyAsn1Error('Comma in fractions disallowed: %r' % value) + + if self.DOT_CHAR in numbers: + + isModified = False + + numbers = list(numbers) + + searchIndex = min(numbers.index(self.DOT_CHAR) + 4, len(numbers) - 1) + + while numbers[searchIndex] != self.DOT_CHAR: + if numbers[searchIndex] == self.ZERO_CHAR: + del numbers[searchIndex] + isModified = True + + searchIndex -= 1 + + searchIndex += 1 + + if searchIndex < len(numbers): + if numbers[searchIndex] == self.Z_CHAR: + # drop hanging comma + del numbers[searchIndex - 1] + isModified = True + + if isModified: + value = value.clone(numbers) + + if not self.MIN_LENGTH < len(numbers) < self.MAX_LENGTH: + raise error.PyAsn1Error('Length constraint violated: %r' % value) + + options.update(maxChunkSize=1000) + + return encoder.OctetStringEncoder.encodeValue( + self, value, asn1Spec, encodeFun, **options + ) + + +class GeneralizedTimeEncoder(TimeEncoderMixIn, encoder.OctetStringEncoder): + MIN_LENGTH = 12 + MAX_LENGTH = 20 + + +class UTCTimeEncoder(TimeEncoderMixIn, encoder.OctetStringEncoder): + MIN_LENGTH = 10 + MAX_LENGTH = 14 + + +class SetOfEncoder(encoder.SequenceOfEncoder): + def encodeValue(self, value, asn1Spec, encodeFun, **options): + chunks = self._encodeComponents( + value, asn1Spec, encodeFun, **options) + + # sort by serialised and padded components + if len(chunks) > 1: + zero = b'\x00' + maxLen = max(map(len, chunks)) + paddedChunks = [ + (x.ljust(maxLen, zero), x) for x in chunks + ] + paddedChunks.sort(key=lambda x: x[0]) + + chunks = [x[1] for x in paddedChunks] + + return b''.join(chunks), True, True + + +class SequenceOfEncoder(encoder.SequenceOfEncoder): + def encodeValue(self, value, asn1Spec, encodeFun, **options): + + if options.get('ifNotEmpty', False) and not len(value): + return b'', True, True + + chunks = self._encodeComponents( + value, asn1Spec, encodeFun, **options) + + return b''.join(chunks), True, True + + +class SetEncoder(encoder.SequenceEncoder): + @staticmethod + def _componentSortKey(componentAndType): + """Sort SET components by tag + + Sort regardless of the Choice value (static sort) + """ + component, asn1Spec = componentAndType + + if asn1Spec is None: + asn1Spec = component + + if asn1Spec.typeId == univ.Choice.typeId and not asn1Spec.tagSet: + if asn1Spec.tagSet: + return asn1Spec.tagSet + else: + return asn1Spec.componentType.minTagSet + else: + return asn1Spec.tagSet + + def encodeValue(self, value, asn1Spec, encodeFun, **options): + + substrate = b'' + + comps = [] + compsMap = {} + + if asn1Spec is None: + # instance of ASN.1 schema + inconsistency = value.isInconsistent + if inconsistency: + raise error.PyAsn1Error( + f"ASN.1 object {value.__class__.__name__} is inconsistent") + + namedTypes = value.componentType + + for idx, component in enumerate(value.values()): + if namedTypes: + namedType = namedTypes[idx] + + if namedType.isOptional and not component.isValue: + continue + + if namedType.isDefaulted and component == namedType.asn1Object: + continue + + compsMap[id(component)] = namedType + + else: + compsMap[id(component)] = None + + comps.append((component, asn1Spec)) + + else: + # bare Python value + ASN.1 schema + for idx, namedType in enumerate(asn1Spec.componentType.namedTypes): + + try: + component = value[namedType.name] + + except KeyError: + raise error.PyAsn1Error('Component name "%s" not found in %r' % (namedType.name, value)) + + if namedType.isOptional and namedType.name not in value: + continue + + if namedType.isDefaulted and component == namedType.asn1Object: + continue + + compsMap[id(component)] = namedType + comps.append((component, asn1Spec[idx])) + + for comp, compType in sorted(comps, key=self._componentSortKey): + namedType = compsMap[id(comp)] + + if namedType: + options.update(ifNotEmpty=namedType.isOptional) + + chunk = encodeFun(comp, compType, **options) + + # wrap open type blob if needed + if namedType and namedType.openType: + wrapType = namedType.asn1Object + if wrapType.tagSet and not wrapType.isSameTypeWith(comp): + chunk = encodeFun(chunk, wrapType, **options) + + substrate += chunk + + return substrate, True, True + + +class SequenceEncoder(encoder.SequenceEncoder): + omitEmptyOptionals = True + + +TAG_MAP = encoder.TAG_MAP.copy() + +TAG_MAP.update({ + univ.Boolean.tagSet: BooleanEncoder(), + univ.Real.tagSet: RealEncoder(), + useful.GeneralizedTime.tagSet: GeneralizedTimeEncoder(), + useful.UTCTime.tagSet: UTCTimeEncoder(), + # Sequence & Set have same tags as SequenceOf & SetOf + univ.SetOf.tagSet: SetOfEncoder(), + univ.Sequence.typeId: SequenceEncoder() +}) + +TYPE_MAP = encoder.TYPE_MAP.copy() + +TYPE_MAP.update({ + univ.Boolean.typeId: BooleanEncoder(), + univ.Real.typeId: RealEncoder(), + useful.GeneralizedTime.typeId: GeneralizedTimeEncoder(), + useful.UTCTime.typeId: UTCTimeEncoder(), + # Sequence & Set have same tags as SequenceOf & SetOf + univ.Set.typeId: SetEncoder(), + univ.SetOf.typeId: SetOfEncoder(), + univ.Sequence.typeId: SequenceEncoder(), + univ.SequenceOf.typeId: SequenceOfEncoder() +}) + + +class SingleItemEncoder(encoder.SingleItemEncoder): + fixedDefLengthMode = False + fixedChunkSize = 1000 + + TAG_MAP = TAG_MAP + TYPE_MAP = TYPE_MAP + + +class Encoder(encoder.Encoder): + SINGLE_ITEM_ENCODER = SingleItemEncoder + + +#: Turns ASN.1 object into CER octet stream. +#: +#: Takes any ASN.1 object (e.g. :py:class:`~pyasn1.type.base.PyAsn1Item` derivative) +#: walks all its components recursively and produces a CER octet stream. +#: +#: Parameters +#: ---------- +#: value: either a Python or pyasn1 object (e.g. :py:class:`~pyasn1.type.base.PyAsn1Item` derivative) +#: A Python or pyasn1 object to encode. If Python object is given, `asnSpec` +#: parameter is required to guide the encoding process. +#: +#: Keyword Args +#: ------------ +#: asn1Spec: +#: Optional ASN.1 schema or value object e.g. :py:class:`~pyasn1.type.base.PyAsn1Item` derivative +#: +#: Returns +#: ------- +#: : :py:class:`bytes` +#: Given ASN.1 object encoded into BER octet-stream +#: +#: Raises +#: ------ +#: ~pyasn1.error.PyAsn1Error +#: On encoding errors +#: +#: Examples +#: -------- +#: Encode Python value into CER with ASN.1 schema +#: +#: .. code-block:: pycon +#: +#: >>> seq = SequenceOf(componentType=Integer()) +#: >>> encode([1, 2, 3], asn1Spec=seq) +#: b'0\x80\x02\x01\x01\x02\x01\x02\x02\x01\x03\x00\x00' +#: +#: Encode ASN.1 value object into CER +#: +#: .. code-block:: pycon +#: +#: >>> seq = SequenceOf(componentType=Integer()) +#: >>> seq.extend([1, 2, 3]) +#: >>> encode(seq) +#: b'0\x80\x02\x01\x01\x02\x01\x02\x02\x01\x03\x00\x00' +#: +encode = Encoder() + +# EncoderFactory queries class instance and builds a map of tags -> encoders + +def __getattr__(attr: str): + if newAttr := {"tagMap": "TAG_MAP", "typeMap": "TYPE_MAP"}.get(attr): + warnings.warn(f"{attr} is deprecated. Please use {newAttr} instead.", DeprecationWarning, stacklevel=2) + return globals()[newAttr] + raise AttributeError(attr) diff --git a/module/scripts/lib/pyasn1/codec/der/__init__.py b/module/scripts/lib/pyasn1/codec/der/__init__.py new file mode 100644 index 0000000..8c3066b --- /dev/null +++ b/module/scripts/lib/pyasn1/codec/der/__init__.py @@ -0,0 +1 @@ +# This file is necessary to make this directory a package. diff --git a/module/scripts/lib/pyasn1/codec/der/__pycache__/__init__.cpython-314.pyc b/module/scripts/lib/pyasn1/codec/der/__pycache__/__init__.cpython-314.pyc new file mode 100644 index 0000000000000000000000000000000000000000..037c7e7e049d9fd028af2990f5472799306edc88 GIT binary patch literal 215 zcmXwzJqiLb5QP&LMFhQsm54i9D;8pHVPU(B*|3M&h=5!z4{KbFJbO^3&%Z6;kH#cTl4q|i&rIiMzAc`!+q=JJ= zrG+q&naUMl?o?v%qFgmWt-%U|P8?yko~%%uOAT2Om#OrD(#}Gv_Mj`Y`HUK4DIliy dD55SGMWL?h?&ABgG;JE;XIc;MfOBpFq7Nh4J0Ji6 literal 0 HcmV?d00001 diff --git a/module/scripts/lib/pyasn1/codec/der/__pycache__/decoder.cpython-314.pyc b/module/scripts/lib/pyasn1/codec/der/__pycache__/decoder.cpython-314.pyc new file mode 100644 index 0000000000000000000000000000000000000000..61b7b156de1f2eb6418d0eaaf1087e4b6370a2fd GIT binary patch literal 3430 zcmbtWOKcm*8J;DVyL?D1k`hI|Oi`92nUT$nQyGa}$ADzX5@B0mR)z`zqPA;tMYEx} z?Cj8na-xBvahiiupg?rYvAN|K^ptCFTuAW}3l}iZ_K;I$h(NCWXO^^>P;ne&K+ewo zubKZb-@kKGe-Od=&p-e72i%X)zv)Y71lo(^AumF8w2c%lg9v?kGTf?%cqZxEoAIvl zgr7tTulUj);)|gvqzG}Oh&fNTuVd`wIS#~Nr&^h%tnCH_Dw@tRux6D`?0&$(KY z2>ec-V&PSOhr#dZUY(97VRUl^auUr&vopO?JCL&pbZZcy)A`x+y*k-9dub$=i?wZC z0b8+L_zL(`bgeH^>$z|)Mh|c+gHQk^5UY_O)JOl5r;4 z50Vl{zV!;pk=)2h{K=>?06O6~Du>CK!cC)8EZOTdEY7T#0=5e(i*!V_Ii*ltYA55bV2nRVS#byGEf_rt3ac{bNllw>blLd?DVHjG#Q^rOS*unJ zOl}wzli*sO=!KgG-gRQCY7&i<@~TE)%X>AVo2rT_9vsutJEmOFh$h!e9m{XYd1E)N z75C(wQdu|UbWsOyOR`oWr6SfIl*ryity-1MJl075zPzz?aZ$^ESgXqJF)5=|C8k_1 z-IJ?(npwF779n~vgcJTOtm*0=_QTF>8ldN(Z+}M3GiML^F&yczUMskRJ;5D)AACPO zyv*x0Oi_|8-MuL2V0zow1lIy#HnD?H`soK*15Q_#=ZF^ogaI%xy2fd+Nraq8#GIe_ zp)vwc8NqgCMAo4w%6f+AyPcYF51k6(5E#W2gDJQpOfeS2WkZ-Epd+ek!N@})h<6uO z)H@4V$A5P-yUbS6c5P%T1%2nXUMZ6Mm_qQ6gFp>2YjdzLr(uGuNHJTQwWL{Fk}XND zuRNDx^_Av8yuSK;G;zoeyFsL?r^o3gaJu3&|BK_)juG4(FBZU9J0QWI4hWN?9l#CE z;Cg0l<@U0gxx0KvU0S~J{?f8?s{c%nP83MZQJC8P&sfroEu}3f4gQbK9rAJJ^=mnN z_18G;i0v}?d8a3-zszasffHH3t1K_v0ncCQGSi~dRrs4Ui_GOG#wUg?yQZFN4uUVdjz2R(}oaR245*>kP#XDLR z(~5LNvRBpV8`b@2JXtc61-**(JjAJxPG-xxX6i|R^W;Z?WC@yqt`*V{;UKhu{D)=z zfnH7tj<={2$Nx~n6#yL5;pxhWF0~DnjLPp^mLnF+#yzcUI?_U?e=K8c;FJeXv*T3s zhm>Q|Ak8G*)P+Zmz5zQ6hRz*kor(d%|}(TXm9Gk*|WZHTM& zhfl@TXa4Y${=f9^5A6$$#FejSzaIT2)fm2gxc2+TZsl9Q@oZrD)4l!pw*ylR|J1YK z$o|klFwx)>EdQx-Og}4}u-kjmtoNkzI%YuWgxX{pK}=IgQBy0I;gGB$QLuNSimsJ^ z*q7bF^LeAX=Loeb^h@0dyzJpDJ?jw8P*>-1cxr~_$PpfB<(h6fA-Zq-K08ts8^pL_ z>{bov;KT{2YQC(Q(3`;?$Ylx)yj;X%x6}uxW5N6pY&-sIZuh;87eRy;xA_6gM_!KO zjwQr}|BKH33r#!^4K`x4jo|sZ*b+EydcSBV-m?<#wGh0t{4jkX+Q|hgxxgq#K{R~E zj>%R`ZlLJ%VC0GXiEIZGRxoi8OxDFF5QKDcj7->(IV&<}M=o2D%XZ|N6}k3UXoez(p)+61 zeC2<7_TA>>n~zsN+c@Z(co9HD=US4$P40{K$R%s!QVYRLE9&JgeD=_e%T`=&A$V!^ ddAZc%A_YJCo_bN`P%!f8XkGZ82c%&o_J2$jnu-7b literal 0 HcmV?d00001 diff --git a/module/scripts/lib/pyasn1/codec/der/__pycache__/encoder.cpython-314.pyc b/module/scripts/lib/pyasn1/codec/der/__pycache__/encoder.cpython-314.pyc new file mode 100644 index 0000000000000000000000000000000000000000..9cdd24e1dabe8145b1a1503f4820205cfacb89de GIT binary patch literal 3652 zcmb7HO>7&-6`t8$E`LRl)DQJ%awR#A1>2&Ox{f6ak^fXvwI#8XLf3|*?TTDctf(cs zJG3gDXdtNU=3oa2Pz4C!Tmrba9*y=8_vg@}7gDxEVc`G*+8)wNw((5#l>1I zuGXKfvcx&(j0J1+7B|Po+AO}-$2w}f#mzaok!3A{Ps1r`3nTEQ%jE0nYkl#~dH)Yz zLiKh{{@Z*pBItw&y1*;pRlS1<`V2`oNZLTM=QWc4Ie+ckBoOz2PD2o7eWWRldr_bF zE2=!%hjp8*syP)HyR0O0DVefdLCM_G?PKPI&YLUA`u&*dU~d2z2~uYc5gbQ^nfDR4 z73CnnaU47dJ+Z|Z!Y2^1$645((_PatsoGCgFU7D(6A}`}vo{Po6j1`LLazoA%p!+S z7n;F&c7mB<@@#^QRb8m>W-Y%^4CKGr4%0aMLxfUaP}^XU|E2~)Z&s($e=ua2#ZIHg zG3TqYR-zqdrxzudnDG!_){A*w9@CKzy~`DtepFz-*Mm@=OK>sot5OL(1e0lqRu{V3 ziqPRwlg!nl2su#*3cBx025(vO+XrVND=X+T}9VQgU8aQW+&Jo&@28cqFU0&V*BNJ=77q@c-aWFP`M{Ze`@ zvY5%r8ceE@Y-S;nUn5I71)Kz@yG2U;mP8G&*Vsg{-g{F?3; z54@rma8+K@yJ~fy|CLpmtlEEp6w$5+g(GE0=N;#cyJ^eaxg8YlMt6eYJ5d@ zwW;-f;ltri{U41!?+8~qPHc6Ycs?@v*!G+6S4I-0k;EU*RYs%|{N0qcMx?D|w$!$= z$KuW$2K^SG4%8-mDt+d77VN782g<>Ll5YT9YVP{Q=m(=EPtSk;*#i4$v^@NFwCQpi z_q(7E=r5saJ9LLA(rXh%-6e`Exl|!bd5+ujv>886wOIdB1FwB6o{;Wq+b-1SxM7UnIuWd*{mo6Dm2thf*P&b=T-V{Ray!S zLg8Pdc>V7UxU+4W{{_=&>)m4!ANZHS!ICNY5|~&3uy+iW>!4(muyIVdc|>e;@Xax2 z2l!x!qY5GZX?VI>d7Q{ARV0~FkAQ*6KBr32R}E7lP|^)V4=iSWDyJ^Xi`Qf&O_nAg zLv^n)cWJ4hEKg_FW!*k=;fgqMA*MTKZp6kYr}k0<)UHasGY@>6a+Hk1&p7bU(02Qg z64$!j(psF{@wODNK<2#{HFc=u;RmMSt2^)&!p?Dkm6-5x25^#9>9ee!%6{0X3@`Mi z>G8=c*T%&0nXw6RZ1U37<+1pojvMAExdK1U0R$ZHIPsJV8lD5FpEo2qWDG zjApBOvxNKX1`UN41baWC%>zzVI69A)BISFw}^$W4AENQX;by-+<3K>Ns zvXttFK)c`#CzrGGEjioA>nwnZ?pT#n1>Ofh2j%p@<*K1fPPuL_>GpIsw;*LT-Fv}$ z{3f6<>Y(9{BCpaFjRxtMU|yYD6u~~dVYs>T)KBlP`sa9=-Xf{c&HZ_1e?PAC*>=XO7(S#@6@O zHiOR^yGxGl=dQNR<|nRDi3=I`-`A`T!zMa!0*ncRK6B7-piO@gK4m#79~ zt2FtvKLgoiF~)mdgnfTQZC|3JU!txZck}(Fk}F)a@AF4+@BLuK-&^+g?jsoNp2x@? zxEHw_sklOASLlgLDB5?BeZza(yTds)qPL?PQ@5u+xcTAiu8s9E`-sKtt_yjZD(=p* zyR+gJ%5LF_yQk>bL5_{a+l{Yk`Aw~Tj}5nN+AD2?<+j0n1cQAa#wYKj@8$01_7Siz T7BF)8-)}GSUvuyVjo|+eQ1ki6 literal 0 HcmV?d00001 diff --git a/module/scripts/lib/pyasn1/codec/der/decoder.py b/module/scripts/lib/pyasn1/codec/der/decoder.py new file mode 100644 index 0000000..2bef454 --- /dev/null +++ b/module/scripts/lib/pyasn1/codec/der/decoder.py @@ -0,0 +1,120 @@ +# +# This file is part of pyasn1 software. +# +# Copyright (c) 2005-2020, Ilya Etingof +# License: https://pyasn1.readthedocs.io/en/latest/license.html +# +import warnings + +from pyasn1.codec.cer import decoder +from pyasn1.type import univ + +__all__ = ['decode', 'StreamingDecoder'] + + +class BitStringPayloadDecoder(decoder.BitStringPayloadDecoder): + supportConstructedForm = False + + +class OctetStringPayloadDecoder(decoder.OctetStringPayloadDecoder): + supportConstructedForm = False + + +# TODO: prohibit non-canonical encoding +RealPayloadDecoder = decoder.RealPayloadDecoder + +TAG_MAP = decoder.TAG_MAP.copy() +TAG_MAP.update( + {univ.BitString.tagSet: BitStringPayloadDecoder(), + univ.OctetString.tagSet: OctetStringPayloadDecoder(), + univ.Real.tagSet: RealPayloadDecoder()} +) + +TYPE_MAP = decoder.TYPE_MAP.copy() + +# Put in non-ambiguous types for faster codec lookup +for typeDecoder in TAG_MAP.values(): + if typeDecoder.protoComponent is not None: + typeId = typeDecoder.protoComponent.__class__.typeId + if typeId is not None and typeId not in TYPE_MAP: + TYPE_MAP[typeId] = typeDecoder + + +class SingleItemDecoder(decoder.SingleItemDecoder): + __doc__ = decoder.SingleItemDecoder.__doc__ + + TAG_MAP = TAG_MAP + TYPE_MAP = TYPE_MAP + + supportIndefLength = False + + +class StreamingDecoder(decoder.StreamingDecoder): + __doc__ = decoder.StreamingDecoder.__doc__ + + SINGLE_ITEM_DECODER = SingleItemDecoder + + +class Decoder(decoder.Decoder): + __doc__ = decoder.Decoder.__doc__ + + STREAMING_DECODER = StreamingDecoder + + +#: Turns DER octet stream into an ASN.1 object. +#: +#: Takes DER octet-stream and decode it into an ASN.1 object +#: (e.g. :py:class:`~pyasn1.type.base.PyAsn1Item` derivative) which +#: may be a scalar or an arbitrary nested structure. +#: +#: Parameters +#: ---------- +#: substrate: :py:class:`bytes` +#: DER octet-stream +#: +#: Keyword Args +#: ------------ +#: asn1Spec: any pyasn1 type object e.g. :py:class:`~pyasn1.type.base.PyAsn1Item` derivative +#: A pyasn1 type object to act as a template guiding the decoder. Depending on the ASN.1 structure +#: being decoded, *asn1Spec* may or may not be required. Most common reason for +#: it to require is that ASN.1 structure is encoded in *IMPLICIT* tagging mode. +#: +#: Returns +#: ------- +#: : :py:class:`tuple` +#: A tuple of pyasn1 object recovered from DER substrate (:py:class:`~pyasn1.type.base.PyAsn1Item` derivative) +#: and the unprocessed trailing portion of the *substrate* (may be empty) +#: +#: Raises +#: ------ +#: ~pyasn1.error.PyAsn1Error, ~pyasn1.error.SubstrateUnderrunError +#: On decoding errors +#: +#: Examples +#: -------- +#: Decode DER serialisation without ASN.1 schema +#: +#: .. code-block:: pycon +#: +#: >>> s, _ = decode(b'0\t\x02\x01\x01\x02\x01\x02\x02\x01\x03') +#: >>> str(s) +#: SequenceOf: +#: 1 2 3 +#: +#: Decode DER serialisation with ASN.1 schema +#: +#: .. code-block:: pycon +#: +#: >>> seq = SequenceOf(componentType=Integer()) +#: >>> s, _ = decode(b'0\t\x02\x01\x01\x02\x01\x02\x02\x01\x03', asn1Spec=seq) +#: >>> str(s) +#: SequenceOf: +#: 1 2 3 +#: +decode = Decoder() + +def __getattr__(attr: str): + if newAttr := {"tagMap": "TAG_MAP", "typeMap": "TYPE_MAP"}.get(attr): + warnings.warn(f"{attr} is deprecated. Please use {newAttr} instead.", DeprecationWarning, stacklevel=2) + return globals()[newAttr] + raise AttributeError(attr) diff --git a/module/scripts/lib/pyasn1/codec/der/encoder.py b/module/scripts/lib/pyasn1/codec/der/encoder.py new file mode 100644 index 0000000..8e00138 --- /dev/null +++ b/module/scripts/lib/pyasn1/codec/der/encoder.py @@ -0,0 +1,126 @@ +# +# This file is part of pyasn1 software. +# +# Copyright (c) 2005-2020, Ilya Etingof +# License: https://pyasn1.readthedocs.io/en/latest/license.html +# +import warnings + +from pyasn1 import error +from pyasn1.codec.cer import encoder +from pyasn1.type import univ + +__all__ = ['Encoder', 'encode'] + + +class SetEncoder(encoder.SetEncoder): + @staticmethod + def _componentSortKey(componentAndType): + """Sort SET components by tag + + Sort depending on the actual Choice value (dynamic sort) + """ + component, asn1Spec = componentAndType + + if asn1Spec is None: + compType = component + else: + compType = asn1Spec + + if compType.typeId == univ.Choice.typeId and not compType.tagSet: + if asn1Spec is None: + return component.getComponent().tagSet + else: + # TODO: move out of sorting key function + names = [namedType.name for namedType in asn1Spec.componentType.namedTypes + if namedType.name in component] + if len(names) != 1: + raise error.PyAsn1Error( + '%s components for Choice at %r' % (len(names) and 'Multiple ' or 'None ', component)) + + # TODO: support nested CHOICE ordering + return asn1Spec[names[0]].tagSet + + else: + return compType.tagSet + + +TAG_MAP = encoder.TAG_MAP.copy() + +TAG_MAP.update({ + # Set & SetOf have same tags + univ.Set.tagSet: SetEncoder() +}) + +TYPE_MAP = encoder.TYPE_MAP.copy() + +TYPE_MAP.update({ + # Set & SetOf have same tags + univ.Set.typeId: SetEncoder() +}) + + +class SingleItemEncoder(encoder.SingleItemEncoder): + fixedDefLengthMode = True + fixedChunkSize = 0 + + TAG_MAP = TAG_MAP + TYPE_MAP = TYPE_MAP + + +class Encoder(encoder.Encoder): + SINGLE_ITEM_ENCODER = SingleItemEncoder + + +#: Turns ASN.1 object into DER octet stream. +#: +#: Takes any ASN.1 object (e.g. :py:class:`~pyasn1.type.base.PyAsn1Item` derivative) +#: walks all its components recursively and produces a DER octet stream. +#: +#: Parameters +#: ---------- +#: value: either a Python or pyasn1 object (e.g. :py:class:`~pyasn1.type.base.PyAsn1Item` derivative) +#: A Python or pyasn1 object to encode. If Python object is given, `asnSpec` +#: parameter is required to guide the encoding process. +#: +#: Keyword Args +#: ------------ +#: asn1Spec: +#: Optional ASN.1 schema or value object e.g. :py:class:`~pyasn1.type.base.PyAsn1Item` derivative +#: +#: Returns +#: ------- +#: : :py:class:`bytes` +#: Given ASN.1 object encoded into BER octet-stream +#: +#: Raises +#: ------ +#: ~pyasn1.error.PyAsn1Error +#: On encoding errors +#: +#: Examples +#: -------- +#: Encode Python value into DER with ASN.1 schema +#: +#: .. code-block:: pycon +#: +#: >>> seq = SequenceOf(componentType=Integer()) +#: >>> encode([1, 2, 3], asn1Spec=seq) +#: b'0\t\x02\x01\x01\x02\x01\x02\x02\x01\x03' +#: +#: Encode ASN.1 value object into DER +#: +#: .. code-block:: pycon +#: +#: >>> seq = SequenceOf(componentType=Integer()) +#: >>> seq.extend([1, 2, 3]) +#: >>> encode(seq) +#: b'0\t\x02\x01\x01\x02\x01\x02\x02\x01\x03' +#: +encode = Encoder() + +def __getattr__(attr: str): + if newAttr := {"tagMap": "TAG_MAP", "typeMap": "TYPE_MAP"}.get(attr): + warnings.warn(f"{attr} is deprecated. Please use {newAttr} instead.", DeprecationWarning, stacklevel=2) + return globals()[newAttr] + raise AttributeError(attr) diff --git a/module/scripts/lib/pyasn1/codec/native/__init__.py b/module/scripts/lib/pyasn1/codec/native/__init__.py new file mode 100644 index 0000000..8c3066b --- /dev/null +++ b/module/scripts/lib/pyasn1/codec/native/__init__.py @@ -0,0 +1 @@ +# This file is necessary to make this directory a package. diff --git a/module/scripts/lib/pyasn1/codec/native/__pycache__/__init__.cpython-314.pyc b/module/scripts/lib/pyasn1/codec/native/__pycache__/__init__.cpython-314.pyc new file mode 100644 index 0000000000000000000000000000000000000000..e8c3224e3856e3535263df6f33e91303806dd99e GIT binary patch literal 190 zcmdPqlZtN%-~a})MX|q1$0`!+uby*< zLy^?TqSX$DcP`Jl=iYPg{pOr=&+IL8SP5Le{r!Kx_DMS--{Bwia%C$|d=5gE$wlI0 zJVe5$&clr9B%K@UdZ_CqJ*~4IcFZ6dXx-p3j+rDAts9|kmdvzng1SYr(7GAwR>?~1 z7O2}Ko16Ho-Hc?P(RtPJRej*uN@mJ{(~?KZR(-Z3I;AR=*FZa;y^i=CGZnr1EVYtS z%Yf>fsmxKUD774@6?tu|DYX))Re97JO05QJO&+zjhs;>L-J3P??%zzCsca>^S@J9c zatAlJDM?Q4Bu`vZuBvFf=ko-zRZY2KJ&*2C?)VPnUf7{r{|@C&?NDy8M7ipseHhxI z+(?OXwFTpuy<;WH74Ey)dwPd*XLl$!u|qlU4&{8$E;nQL`gbHXy(6iYcO>=G9Z6l- zft1aAaYs_G?nr9(*`<0cFEY}>Ev@w2<{nG3(xu30S!pe~QmnL=T;bbl^=et^QslI( zbSZLLR=N~9Eh}A$+^NlbeXk*@Qlr?JwdzYzXtSi}P6F1tBg##T*#-Oig(vMxoBm z21Op9p@ksw^imMpvWv0E6?hsKMSgBEDjUOmC?3X!hPh~PUJT_gj4u1<$sVW#;WRfx zB*sT1-NiadUrXY|$NF@j8zZqky=0gsl9B3Ykedc(MM(&Tq^VFa8Wg<2rD!}D9-?Cj z6CpNl6g+C#2r)nDfXXtN)_IvcZniqj^k%8;(^HJ|tU|xs5EwWK%pLMdC40G(d;D;Mynuv?m;tf<3SfLeKu5Q2cUtaDJ(0 zE)wNMAPPOv$ZStyNm_`<4s{>s?GYmq|C2;8bSXFw4H8Q@3P`~aJ+Yt^xx)7-qY2%K zCD@1n%sUzg$j?1(A-dIO^`Mnl0mU+T;H>-Lt#{s9T}U~1ubFp0!3DTYgmQeCu;W

z79}F7X&WTf4MwCXP&hXKG!WsoQV?NSxfO^msCXt`7Mc}ZUQ-bP{%*i-1AcTXx>y0X zORzv8TXHJO<#R&(@?azu6qXdFg-U3#fjVUs>4tDQsltinK+%kn927ZFd>pEU+ELT5jh&3~rr=QTo9)CGK78 zHhb4N#lODD$3pz1FvUxS(=0RsMi`Xt->;yu)&8l0al!ybBDa?yki8PdX+t>~`3WHj z)A`qr0d*dXOA|Q*Jx}Xf$@#i{F10No=jZePX(8f}u7^ysgA7Z~pWg+7XL<;+kQOp+ zXwSZodWr(s0oEe{)+i?VmlC{eMcbfQquXYo3M7SUENZZ*#RA_R*@DfcgV9A^cIG+| zu?G>1afFYCMKiRc){5h}Jm_>hWnFXdX#-jDB`5$s)lGjr@r#L6b;pY3k*h83+Mjgo zUvYj{)3P%Bz*+r`llzbMeYalz=fIu#{eFMCKahmG{nA?KH7Nhmk}BsO8%V7?VqMEG%Fiu*O&`8;^J;qkNOJ#3s(CbN zpZfa9J^R#SJ*nq1#%BNq9ybLnF#DMg3?l*q3I!P0qC{XE-3A!dFD}F*A^vG-$l0I2 zhha-V!&deOQ3CS<`*Y?0-~PC%wThXjF`P{~%k|Kudg&SXBB$~5E zV)s*xM6#TFP+s%MSq}!{v5nNWWUS8s3dEj(1#WvjD69wyC=^g&3vEzfQlTJ1eOCd+ z3@foS@O5FvIAc7d^D#79HDS~$nIUGiK+J0Ou@F}qGNQak;xA_d)eyLk+=VYO zH5h*m6@Un{ab7vigrQ{<@iXJLX`*Ewrb#U4u%>kdk`O{Pk@H-PRY&rXb^^U1B#SQS zrJ$7(iWmfpeq@t>U^Fm3;FV1Q2=&K<3EAv_#XC$%=70ol1!xerpg)q0*(RneT{cDL zV{w5Gi#Pzr$JC5qycvl_BnT8;n^p%vr0_lf5pf?B0HW62pS+Q3J@Shycl=*o`24~O z`xo{H?R!4!Otl|hHNEfniv6AAH;xtdeFxppf(5oh4|IY54s=*18;gVy30~oOT&ib} z;x1s6Mz4){&WocJU22TyCKqx?5Z^ZA9cS$Dg{#yNu=PQ9W1MLn5AG4>K&q%`W;|$>)Xi;fiPOWV`TZv!i#SpXT@^%yv;BV=7~>)_IH$vz ziHpzS-+`%#?gJb;rF~T7IkD?n^|{!kSp2HW1Y6EMCtfQ@EpS9tt83+4JjQux4kN39 zj(b67`mdRDO=`G=0BM8}Xs?V!HSMaE^=BtX1@wQifzC;06N}=4%pzYvXvo%xsOl=9 z&5+FlaG*g=1u)=5wt1J3FpSA5*^*5uKssU+nMUl#6PC?q_$Asx!1zTr`h)XuRHXh1 zpC-&=0ZkZ2E-UZ}LDnbYiOrEQJyO|WK^OU~_m-_$VeH62Icl7|ukbvj>D!b9&M#qH z@gKoZEtCJUmA!Z9p}pdRGw+;%h&X9)`q+Q7^P`LD=H6s;?>+nBZ|%;9&22Y+_VLd? z>A3aAKXs?~45ao9Wb{nMi_AJ<977BQ%J%BJw#Hu_zqRlM`=#S^N4kCFZu`i?%DQxA z``ya+jEmGeqY&nK9ceKITEA`$pe~eYfiW z#eLg-zwy}0*&phO-TB@jRKnG=`ew?tFYP*f&vh98z41|Z+I8Tb>%eEdcg(-;&lq%6 zB^{{r*i0&#S6}|LI$1t&rw)o01D&X5KZ_sjO}o19xw=1NKRx$r8}`$;4odZ}8;PS5 z7v_V>cP8)In;$u=SL{Eo_mJ}Xbz%m6|IpBM!}T|=n`8el`L~nljuXj_6L((zGVpmI z-RDpC`BM$&R!(PF)&Vnfx8Hd4!#8i8zVANrOZ$(1sA|Nu^Dz%=8`HJB@7C^K>+Juk z{&()*xbJs*Q?)OxmA?c_3=RI2apFjS65X(8zqK@vJWu}p`2nk^O#d}gHCCtly6%w2 zYWTgC1xlBWB8p;hqRU1BEufshVF6B=2t!zmV1eHt63g}#NP7Qqn2HtnqkEZ6U-0hU51(rfkKIGQ7i<%+@;Ibo}jfZh;6^f z#kK~FXd&xr@aeOWEi0KJrnUHV5YrlEQ}$g5F zlP;@Vo>em!Qho_7L<~0|q-@)jZaaRr?Rc_n_|ADKR@iUMZn|a@1@U8UT1qIo)wHIW zLy*Is*7Y#c)XT$k7@C^7%?RM!Nj5~@NZH87OIL%!yjU0-D_PY%#T9$;Z>TsX;6QO_ zRoc1hu5;JTHmZbYpNO!4 z3^HLE_hT&nDnuX3`9M_*+7R?HRJDJl5|>U_o{r~zCMALULJYBV*?o0i z3!>kwdGTNkZOpYsj0wWv-}n$LP`I1(MtSgk9DJYPu32bQzz4(Ku(Vc)4?>rs{1rY5 zZ@7LQyqg&S8iQ9(lv%9Fl|$K%;G4&Jg(aKjqw(2bRFumG)DHmE@xq%K0|jJLjK7K; zangRMUB_zms&TF5*qz;X8ougUt37*f;?=dwvHRxuLuc)KORIJFovmx;)`!;m z)#{YBWsPm28{hSUu!u?-l@Bd65K*EEheGMbjA>9(j=r`mjm9RpU4jVH<>blpO1(VFPx3f&-2-6#xXQJcy3fRVZtOz7~}MllJeC9F+>xZGFa=DTe_Jko*9Dxk4AlrO=lzu&$ z^bEpB0(QRVxB?3h5ANGSW)>4i@vQgAlP zt9(Bn zgX7*TFFVa4UJMD5gapx_GbVXtPijG1yzo2{%>lbdoUo;%z_uvvBHei@1WrQjJPg4Ia-fiEK zCVDs0vgMJjYGu!|>5^%{C;9C)2De33t=c zRMYWH1#&92#;QV2HFCzUkEhxCB;2mUDc6xq4RUIcGj@Fpdra2sPt_dE)FHhd>1VE= zNwc*{xa;?&>bo-y$Z0}O@%YWiX#tMXKFu~K;dY-$x%)D1(4xgyj{S{-;pD!#$&Lpn^ky5VzHJ8mAs9|VIHwo zWv|n0TN3V$Q>l)@2S~(CNwcj!;-9LlsJ9Z|Lk)6XFXF}4{{ncc|iq&y9@Y!;vh zEJm>guAUL8_-^LEYgAlAp<)F7-{3odxLr1{k1;(=&8l&oz+*kkn3xG>g<9JQ=5d%I R){6HUmW@BK5I|C&@V^?yrE~xQ literal 0 HcmV?d00001 diff --git a/module/scripts/lib/pyasn1/codec/native/__pycache__/encoder.cpython-314.pyc b/module/scripts/lib/pyasn1/codec/native/__pycache__/encoder.cpython-314.pyc new file mode 100644 index 0000000000000000000000000000000000000000..71a0214b5c836da53a996956d5ec7e9f2940981e GIT binary patch literal 15277 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 literal 0 HcmV?d00001 diff --git a/module/scripts/lib/pyasn1/codec/native/decoder.py b/module/scripts/lib/pyasn1/codec/native/decoder.py new file mode 100644 index 0000000..881ce19 --- /dev/null +++ b/module/scripts/lib/pyasn1/codec/native/decoder.py @@ -0,0 +1,244 @@ +# +# This file is part of pyasn1 software. +# +# Copyright (c) 2005-2020, Ilya Etingof +# License: https://pyasn1.readthedocs.io/en/latest/license.html +# +import warnings + +from pyasn1 import debug +from pyasn1 import error +from pyasn1.compat import _MISSING +from pyasn1.type import base +from pyasn1.type import char +from pyasn1.type import tag +from pyasn1.type import univ +from pyasn1.type import useful + +__all__ = ['decode'] + +LOG = debug.registerLoggee(__name__, flags=debug.DEBUG_DECODER) + + +class AbstractScalarPayloadDecoder(object): + def __call__(self, pyObject, asn1Spec, decodeFun=None, **options): + return asn1Spec.clone(pyObject) + + +class BitStringPayloadDecoder(AbstractScalarPayloadDecoder): + def __call__(self, pyObject, asn1Spec, decodeFun=None, **options): + return asn1Spec.clone(univ.BitString.fromBinaryString(pyObject)) + + +class SequenceOrSetPayloadDecoder(object): + def __call__(self, pyObject, asn1Spec, decodeFun=None, **options): + asn1Value = asn1Spec.clone() + + componentsTypes = asn1Spec.componentType + + for field in asn1Value: + if field in pyObject: + asn1Value[field] = decodeFun(pyObject[field], componentsTypes[field].asn1Object, **options) + + return asn1Value + + +class SequenceOfOrSetOfPayloadDecoder(object): + def __call__(self, pyObject, asn1Spec, decodeFun=None, **options): + asn1Value = asn1Spec.clone() + + for pyValue in pyObject: + asn1Value.append(decodeFun(pyValue, asn1Spec.componentType), **options) + + return asn1Value + + +class ChoicePayloadDecoder(object): + def __call__(self, pyObject, asn1Spec, decodeFun=None, **options): + asn1Value = asn1Spec.clone() + + componentsTypes = asn1Spec.componentType + + for field in pyObject: + if field in componentsTypes: + asn1Value[field] = decodeFun(pyObject[field], componentsTypes[field].asn1Object, **options) + break + + return asn1Value + + +TAG_MAP = { + univ.Integer.tagSet: AbstractScalarPayloadDecoder(), + univ.Boolean.tagSet: AbstractScalarPayloadDecoder(), + univ.BitString.tagSet: BitStringPayloadDecoder(), + univ.OctetString.tagSet: AbstractScalarPayloadDecoder(), + univ.Null.tagSet: AbstractScalarPayloadDecoder(), + univ.ObjectIdentifier.tagSet: AbstractScalarPayloadDecoder(), + univ.RelativeOID.tagSet: AbstractScalarPayloadDecoder(), + univ.Enumerated.tagSet: AbstractScalarPayloadDecoder(), + univ.Real.tagSet: AbstractScalarPayloadDecoder(), + univ.Sequence.tagSet: SequenceOrSetPayloadDecoder(), # conflicts with SequenceOf + univ.Set.tagSet: SequenceOrSetPayloadDecoder(), # conflicts with SetOf + univ.Choice.tagSet: ChoicePayloadDecoder(), # conflicts with Any + # character string types + char.UTF8String.tagSet: AbstractScalarPayloadDecoder(), + char.NumericString.tagSet: AbstractScalarPayloadDecoder(), + char.PrintableString.tagSet: AbstractScalarPayloadDecoder(), + char.TeletexString.tagSet: AbstractScalarPayloadDecoder(), + char.VideotexString.tagSet: AbstractScalarPayloadDecoder(), + char.IA5String.tagSet: AbstractScalarPayloadDecoder(), + char.GraphicString.tagSet: AbstractScalarPayloadDecoder(), + char.VisibleString.tagSet: AbstractScalarPayloadDecoder(), + char.GeneralString.tagSet: AbstractScalarPayloadDecoder(), + char.UniversalString.tagSet: AbstractScalarPayloadDecoder(), + char.BMPString.tagSet: AbstractScalarPayloadDecoder(), + # useful types + useful.ObjectDescriptor.tagSet: AbstractScalarPayloadDecoder(), + useful.GeneralizedTime.tagSet: AbstractScalarPayloadDecoder(), + useful.UTCTime.tagSet: AbstractScalarPayloadDecoder() +} + +# Put in ambiguous & non-ambiguous types for faster codec lookup +TYPE_MAP = { + univ.Integer.typeId: AbstractScalarPayloadDecoder(), + univ.Boolean.typeId: AbstractScalarPayloadDecoder(), + univ.BitString.typeId: BitStringPayloadDecoder(), + univ.OctetString.typeId: AbstractScalarPayloadDecoder(), + univ.Null.typeId: AbstractScalarPayloadDecoder(), + univ.ObjectIdentifier.typeId: AbstractScalarPayloadDecoder(), + univ.RelativeOID.typeId: AbstractScalarPayloadDecoder(), + univ.Enumerated.typeId: AbstractScalarPayloadDecoder(), + univ.Real.typeId: AbstractScalarPayloadDecoder(), + # ambiguous base types + univ.Set.typeId: SequenceOrSetPayloadDecoder(), + univ.SetOf.typeId: SequenceOfOrSetOfPayloadDecoder(), + univ.Sequence.typeId: SequenceOrSetPayloadDecoder(), + univ.SequenceOf.typeId: SequenceOfOrSetOfPayloadDecoder(), + univ.Choice.typeId: ChoicePayloadDecoder(), + univ.Any.typeId: AbstractScalarPayloadDecoder(), + # character string types + char.UTF8String.typeId: AbstractScalarPayloadDecoder(), + char.NumericString.typeId: AbstractScalarPayloadDecoder(), + char.PrintableString.typeId: AbstractScalarPayloadDecoder(), + char.TeletexString.typeId: AbstractScalarPayloadDecoder(), + char.VideotexString.typeId: AbstractScalarPayloadDecoder(), + char.IA5String.typeId: AbstractScalarPayloadDecoder(), + char.GraphicString.typeId: AbstractScalarPayloadDecoder(), + char.VisibleString.typeId: AbstractScalarPayloadDecoder(), + char.GeneralString.typeId: AbstractScalarPayloadDecoder(), + char.UniversalString.typeId: AbstractScalarPayloadDecoder(), + char.BMPString.typeId: AbstractScalarPayloadDecoder(), + # useful types + useful.ObjectDescriptor.typeId: AbstractScalarPayloadDecoder(), + useful.GeneralizedTime.typeId: AbstractScalarPayloadDecoder(), + useful.UTCTime.typeId: AbstractScalarPayloadDecoder() +} + + +class SingleItemDecoder(object): + + TAG_MAP = TAG_MAP + TYPE_MAP = TYPE_MAP + + def __init__(self, tagMap=_MISSING, typeMap=_MISSING, **ignored): + self._tagMap = tagMap if tagMap is not _MISSING else self.TAG_MAP + self._typeMap = typeMap if typeMap is not _MISSING else self.TYPE_MAP + + def __call__(self, pyObject, asn1Spec, **options): + + if LOG: + debug.scope.push(type(pyObject).__name__) + LOG('decoder called at scope %s, working with ' + 'type %s' % (debug.scope, type(pyObject).__name__)) + + if asn1Spec is None or not isinstance(asn1Spec, base.Asn1Item): + raise error.PyAsn1Error( + 'asn1Spec is not valid (should be an instance of an ASN.1 ' + 'Item, not %s)' % asn1Spec.__class__.__name__) + + try: + valueDecoder = self._typeMap[asn1Spec.typeId] + + except KeyError: + # use base type for codec lookup to recover untagged types + baseTagSet = tag.TagSet(asn1Spec.tagSet.baseTag, asn1Spec.tagSet.baseTag) + + try: + valueDecoder = self._tagMap[baseTagSet] + + except KeyError: + raise error.PyAsn1Error('Unknown ASN.1 tag %s' % asn1Spec.tagSet) + + if LOG: + LOG('calling decoder %s on Python type %s ' + '<%s>' % (type(valueDecoder).__name__, + type(pyObject).__name__, repr(pyObject))) + + value = valueDecoder(pyObject, asn1Spec, self, **options) + + if LOG: + LOG('decoder %s produced ASN.1 type %s ' + '<%s>' % (type(valueDecoder).__name__, + type(value).__name__, repr(value))) + debug.scope.pop() + + return value + + +class Decoder(object): + SINGLE_ITEM_DECODER = SingleItemDecoder + + def __init__(self, **options): + self._singleItemDecoder = self.SINGLE_ITEM_DECODER(**options) + + def __call__(self, pyObject, asn1Spec=None, **kwargs): + return self._singleItemDecoder(pyObject, asn1Spec=asn1Spec, **kwargs) + + +#: Turns Python objects of built-in types into ASN.1 objects. +#: +#: Takes Python objects of built-in types and turns them into a tree of +#: ASN.1 objects (e.g. :py:class:`~pyasn1.type.base.PyAsn1Item` derivative) which +#: may be a scalar or an arbitrary nested structure. +#: +#: Parameters +#: ---------- +#: pyObject: :py:class:`object` +#: A scalar or nested Python objects +#: +#: Keyword Args +#: ------------ +#: asn1Spec: any pyasn1 type object e.g. :py:class:`~pyasn1.type.base.PyAsn1Item` derivative +#: A pyasn1 type object to act as a template guiding the decoder. It is required +#: for successful interpretation of Python objects mapping into their ASN.1 +#: representations. +#: +#: Returns +#: ------- +#: : :py:class:`~pyasn1.type.base.PyAsn1Item` derivative +#: A scalar or constructed pyasn1 object +#: +#: Raises +#: ------ +#: ~pyasn1.error.PyAsn1Error +#: On decoding errors +#: +#: Examples +#: -------- +#: Decode native Python object into ASN.1 objects with ASN.1 schema +#: +#: .. code-block:: pycon +#: +#: >>> seq = SequenceOf(componentType=Integer()) +#: >>> s, _ = decode([1, 2, 3], asn1Spec=seq) +#: >>> str(s) +#: SequenceOf: +#: 1 2 3 +#: +decode = Decoder() + +def __getattr__(attr: str): + if newAttr := {"tagMap": "TAG_MAP", "typeMap": "TYPE_MAP"}.get(attr): + warnings.warn(f"{attr} is deprecated. Please use {newAttr} instead.", DeprecationWarning, stacklevel=2) + return globals()[newAttr] + raise AttributeError(attr) diff --git a/module/scripts/lib/pyasn1/codec/native/encoder.py b/module/scripts/lib/pyasn1/codec/native/encoder.py new file mode 100644 index 0000000..ed5446b --- /dev/null +++ b/module/scripts/lib/pyasn1/codec/native/encoder.py @@ -0,0 +1,285 @@ +# +# This file is part of pyasn1 software. +# +# Copyright (c) 2005-2020, Ilya Etingof +# License: https://pyasn1.readthedocs.io/en/latest/license.html +# +from collections import OrderedDict +import warnings + +from pyasn1 import debug +from pyasn1 import error +from pyasn1.compat import _MISSING +from pyasn1.type import base +from pyasn1.type import char +from pyasn1.type import tag +from pyasn1.type import univ +from pyasn1.type import useful + +__all__ = ['encode'] + +LOG = debug.registerLoggee(__name__, flags=debug.DEBUG_ENCODER) + + +class AbstractItemEncoder(object): + def encode(self, value, encodeFun, **options): + raise error.PyAsn1Error('Not implemented') + + +class BooleanEncoder(AbstractItemEncoder): + def encode(self, value, encodeFun, **options): + return bool(value) + + +class IntegerEncoder(AbstractItemEncoder): + def encode(self, value, encodeFun, **options): + return int(value) + + +class BitStringEncoder(AbstractItemEncoder): + def encode(self, value, encodeFun, **options): + return str(value) + + +class OctetStringEncoder(AbstractItemEncoder): + def encode(self, value, encodeFun, **options): + return value.asOctets() + + +class TextStringEncoder(AbstractItemEncoder): + def encode(self, value, encodeFun, **options): + return str(value) + + +class NullEncoder(AbstractItemEncoder): + def encode(self, value, encodeFun, **options): + return None + + +class ObjectIdentifierEncoder(AbstractItemEncoder): + def encode(self, value, encodeFun, **options): + return str(value) + + +class RelativeOIDEncoder(AbstractItemEncoder): + def encode(self, value, encodeFun, **options): + return str(value) + + +class RealEncoder(AbstractItemEncoder): + def encode(self, value, encodeFun, **options): + return float(value) + + +class SetEncoder(AbstractItemEncoder): + protoDict = dict + + def encode(self, value, encodeFun, **options): + inconsistency = value.isInconsistent + if inconsistency: + raise error.PyAsn1Error( + f"ASN.1 object {value.__class__.__name__} is inconsistent") + + namedTypes = value.componentType + substrate = self.protoDict() + + for idx, (key, subValue) in enumerate(value.items()): + if namedTypes and namedTypes[idx].isOptional and not value[idx].isValue: + continue + substrate[key] = encodeFun(subValue, **options) + return substrate + + +class SequenceEncoder(SetEncoder): + protoDict = OrderedDict + + +class SequenceOfEncoder(AbstractItemEncoder): + def encode(self, value, encodeFun, **options): + inconsistency = value.isInconsistent + if inconsistency: + raise error.PyAsn1Error( + f"ASN.1 object {value.__class__.__name__} is inconsistent") + return [encodeFun(x, **options) for x in value] + + +class ChoiceEncoder(SequenceEncoder): + pass + + +class AnyEncoder(AbstractItemEncoder): + def encode(self, value, encodeFun, **options): + return value.asOctets() + + +TAG_MAP = { + univ.Boolean.tagSet: BooleanEncoder(), + univ.Integer.tagSet: IntegerEncoder(), + univ.BitString.tagSet: BitStringEncoder(), + univ.OctetString.tagSet: OctetStringEncoder(), + univ.Null.tagSet: NullEncoder(), + univ.ObjectIdentifier.tagSet: ObjectIdentifierEncoder(), + univ.RelativeOID.tagSet: RelativeOIDEncoder(), + univ.Enumerated.tagSet: IntegerEncoder(), + univ.Real.tagSet: RealEncoder(), + # Sequence & Set have same tags as SequenceOf & SetOf + univ.SequenceOf.tagSet: SequenceOfEncoder(), + univ.SetOf.tagSet: SequenceOfEncoder(), + univ.Choice.tagSet: ChoiceEncoder(), + # character string types + char.UTF8String.tagSet: TextStringEncoder(), + char.NumericString.tagSet: TextStringEncoder(), + char.PrintableString.tagSet: TextStringEncoder(), + char.TeletexString.tagSet: TextStringEncoder(), + char.VideotexString.tagSet: TextStringEncoder(), + char.IA5String.tagSet: TextStringEncoder(), + char.GraphicString.tagSet: TextStringEncoder(), + char.VisibleString.tagSet: TextStringEncoder(), + char.GeneralString.tagSet: TextStringEncoder(), + char.UniversalString.tagSet: TextStringEncoder(), + char.BMPString.tagSet: TextStringEncoder(), + # useful types + useful.ObjectDescriptor.tagSet: OctetStringEncoder(), + useful.GeneralizedTime.tagSet: OctetStringEncoder(), + useful.UTCTime.tagSet: OctetStringEncoder() +} + +# Put in ambiguous & non-ambiguous types for faster codec lookup +TYPE_MAP = { + univ.Boolean.typeId: BooleanEncoder(), + univ.Integer.typeId: IntegerEncoder(), + univ.BitString.typeId: BitStringEncoder(), + univ.OctetString.typeId: OctetStringEncoder(), + univ.Null.typeId: NullEncoder(), + univ.ObjectIdentifier.typeId: ObjectIdentifierEncoder(), + univ.RelativeOID.typeId: RelativeOIDEncoder(), + univ.Enumerated.typeId: IntegerEncoder(), + univ.Real.typeId: RealEncoder(), + # Sequence & Set have same tags as SequenceOf & SetOf + univ.Set.typeId: SetEncoder(), + univ.SetOf.typeId: SequenceOfEncoder(), + univ.Sequence.typeId: SequenceEncoder(), + univ.SequenceOf.typeId: SequenceOfEncoder(), + univ.Choice.typeId: ChoiceEncoder(), + univ.Any.typeId: AnyEncoder(), + # character string types + char.UTF8String.typeId: OctetStringEncoder(), + char.NumericString.typeId: OctetStringEncoder(), + char.PrintableString.typeId: OctetStringEncoder(), + char.TeletexString.typeId: OctetStringEncoder(), + char.VideotexString.typeId: OctetStringEncoder(), + char.IA5String.typeId: OctetStringEncoder(), + char.GraphicString.typeId: OctetStringEncoder(), + char.VisibleString.typeId: OctetStringEncoder(), + char.GeneralString.typeId: OctetStringEncoder(), + char.UniversalString.typeId: OctetStringEncoder(), + char.BMPString.typeId: OctetStringEncoder(), + # useful types + useful.ObjectDescriptor.typeId: OctetStringEncoder(), + useful.GeneralizedTime.typeId: OctetStringEncoder(), + useful.UTCTime.typeId: OctetStringEncoder() +} + + +class SingleItemEncoder(object): + + TAG_MAP = TAG_MAP + TYPE_MAP = TYPE_MAP + + def __init__(self, tagMap=_MISSING, typeMap=_MISSING, **ignored): + self._tagMap = tagMap if tagMap is not _MISSING else self.TAG_MAP + self._typeMap = typeMap if typeMap is not _MISSING else self.TYPE_MAP + + def __call__(self, value, **options): + if not isinstance(value, base.Asn1Item): + raise error.PyAsn1Error( + 'value is not valid (should be an instance of an ASN.1 Item)') + + if LOG: + debug.scope.push(type(value).__name__) + LOG('encoder called for type %s ' + '<%s>' % (type(value).__name__, value.prettyPrint())) + + tagSet = value.tagSet + + try: + concreteEncoder = self._typeMap[value.typeId] + + except KeyError: + # use base type for codec lookup to recover untagged types + baseTagSet = tag.TagSet( + value.tagSet.baseTag, value.tagSet.baseTag) + + try: + concreteEncoder = self._tagMap[baseTagSet] + + except KeyError: + raise error.PyAsn1Error('No encoder for %s' % (value,)) + + if LOG: + LOG('using value codec %s chosen by ' + '%s' % (concreteEncoder.__class__.__name__, tagSet)) + + pyObject = concreteEncoder.encode(value, self, **options) + + if LOG: + LOG('encoder %s produced: ' + '%s' % (type(concreteEncoder).__name__, repr(pyObject))) + debug.scope.pop() + + return pyObject + + +class Encoder(object): + SINGLE_ITEM_ENCODER = SingleItemEncoder + + def __init__(self, **options): + self._singleItemEncoder = self.SINGLE_ITEM_ENCODER(**options) + + def __call__(self, pyObject, asn1Spec=None, **options): + return self._singleItemEncoder( + pyObject, asn1Spec=asn1Spec, **options) + + +#: Turns ASN.1 object into a Python built-in type object(s). +#: +#: Takes any ASN.1 object (e.g. :py:class:`~pyasn1.type.base.PyAsn1Item` derivative) +#: walks all its components recursively and produces a Python built-in type or a tree +#: of those. +#: +#: One exception is that instead of :py:class:`dict`, the :py:class:`OrderedDict` +#: is used to preserve ordering of the components in ASN.1 SEQUENCE. +#: +#: Parameters +#: ---------- +# asn1Value: any pyasn1 object (e.g. :py:class:`~pyasn1.type.base.PyAsn1Item` derivative) +#: pyasn1 object to encode (or a tree of them) +#: +#: Returns +#: ------- +#: : :py:class:`object` +#: Python built-in type instance (or a tree of them) +#: +#: Raises +#: ------ +#: ~pyasn1.error.PyAsn1Error +#: On encoding errors +#: +#: Examples +#: -------- +#: Encode ASN.1 value object into native Python types +#: +#: .. code-block:: pycon +#: +#: >>> seq = SequenceOf(componentType=Integer()) +#: >>> seq.extend([1, 2, 3]) +#: >>> encode(seq) +#: [1, 2, 3] +#: +encode = SingleItemEncoder() + +def __getattr__(attr: str): + if newAttr := {"tagMap": "TAG_MAP", "typeMap": "TYPE_MAP"}.get(attr): + warnings.warn(f"{attr} is deprecated. Please use {newAttr} instead.", DeprecationWarning, stacklevel=2) + return globals()[newAttr] + raise AttributeError(attr) diff --git a/module/scripts/lib/pyasn1/codec/streaming.py b/module/scripts/lib/pyasn1/codec/streaming.py new file mode 100644 index 0000000..c35f248 --- /dev/null +++ b/module/scripts/lib/pyasn1/codec/streaming.py @@ -0,0 +1,234 @@ +# +# This file is part of pyasn1 software. +# +# Copyright (c) 2005-2019, Ilya Etingof +# License: https://pyasn1.readthedocs.io/en/latest/license.html +# +import io +import os + +from pyasn1 import error +from pyasn1.type import univ + +class CachingStreamWrapper(io.IOBase): + """Wrapper around non-seekable streams. + + Note that the implementation is tied to the decoder, + not checking for dangerous arguments for the sake + of performance. + + The read bytes are kept in an internal cache until + setting _markedPosition which may reset the cache. + """ + def __init__(self, raw): + self._raw = raw + self._cache = io.BytesIO() + self._markedPosition = 0 + + def peek(self, n): + result = self.read(n) + self._cache.seek(-len(result), os.SEEK_CUR) + return result + + def seekable(self): + return True + + def seek(self, n=-1, whence=os.SEEK_SET): + # Note that this not safe for seeking forward. + return self._cache.seek(n, whence) + + def read(self, n=-1): + read_from_cache = self._cache.read(n) + if n != -1: + n -= len(read_from_cache) + if not n: # 0 bytes left to read + return read_from_cache + + read_from_raw = self._raw.read(n) + + self._cache.write(read_from_raw) + + return read_from_cache + read_from_raw + + @property + def markedPosition(self): + """Position where the currently processed element starts. + + This is used for back-tracking in SingleItemDecoder.__call__ + and (indefLen)ValueDecoder and should not be used for other purposes. + The client is not supposed to ever seek before this position. + """ + return self._markedPosition + + @markedPosition.setter + def markedPosition(self, value): + # By setting the value, we ensure we won't seek back before it. + # `value` should be the same as the current position + # We don't check for this for performance reasons. + self._markedPosition = value + + # Whenever we set _marked_position, we know for sure + # that we will not return back, and thus it is + # safe to drop all cached data. + if self._cache.tell() > io.DEFAULT_BUFFER_SIZE: + self._cache = io.BytesIO(self._cache.read()) + self._markedPosition = 0 + + def tell(self): + return self._cache.tell() + + +def asSeekableStream(substrate): + """Convert object to seekable byte-stream. + + Parameters + ---------- + substrate: :py:class:`bytes` or :py:class:`io.IOBase` or :py:class:`univ.OctetString` + + Returns + ------- + : :py:class:`io.IOBase` + + Raises + ------ + : :py:class:`~pyasn1.error.PyAsn1Error` + If the supplied substrate cannot be converted to a seekable stream. + """ + if isinstance(substrate, io.BytesIO): + return substrate + + elif isinstance(substrate, bytes): + return io.BytesIO(substrate) + + elif isinstance(substrate, univ.OctetString): + return io.BytesIO(substrate.asOctets()) + + try: + if substrate.seekable(): # Will fail for most invalid types + return substrate + else: + return CachingStreamWrapper(substrate) + + except AttributeError: + raise error.UnsupportedSubstrateError( + "Cannot convert " + substrate.__class__.__name__ + + " to a seekable bit stream.") + + +def isEndOfStream(substrate): + """Check whether we have reached the end of a stream. + + Although it is more effective to read and catch exceptions, this + function + + Parameters + ---------- + substrate: :py:class:`IOBase` + Stream to check + + Returns + ------- + : :py:class:`bool` + """ + if isinstance(substrate, io.BytesIO): + cp = substrate.tell() + substrate.seek(0, os.SEEK_END) + result = substrate.tell() == cp + substrate.seek(cp, os.SEEK_SET) + yield result + + else: + received = substrate.read(1) + if received is None: + yield + + if received: + substrate.seek(-1, os.SEEK_CUR) + + yield not received + + +def peekIntoStream(substrate, size=-1): + """Peek into stream. + + Parameters + ---------- + substrate: :py:class:`IOBase` + Stream to read from. + + size: :py:class:`int` + How many bytes to peek (-1 = all available) + + Returns + ------- + : :py:class:`bytes` or :py:class:`str` + The return type depends on Python major version + """ + if hasattr(substrate, "peek"): + received = substrate.peek(size) + if received is None: + yield + + while len(received) < size: + yield + + yield received + + else: + current_position = substrate.tell() + try: + for chunk in readFromStream(substrate, size): + yield chunk + + finally: + substrate.seek(current_position) + + +def readFromStream(substrate, size=-1, context=None): + """Read from the stream. + + Parameters + ---------- + substrate: :py:class:`IOBase` + Stream to read from. + + Keyword parameters + ------------------ + size: :py:class:`int` + How many bytes to read (-1 = all available) + + context: :py:class:`dict` + Opaque caller context will be attached to exception objects created + by this function. + + Yields + ------ + : :py:class:`bytes` or :py:class:`str` or :py:class:`SubstrateUnderrunError` + Read data or :py:class:`~pyasn1.error.SubstrateUnderrunError` + object if no `size` bytes is readily available in the stream. The + data type depends on Python major version + + Raises + ------ + : :py:class:`~pyasn1.error.EndOfStreamError` + Input stream is exhausted + """ + while True: + # this will block unless stream is non-blocking + received = substrate.read(size) + if received is None: # non-blocking stream can do this + yield error.SubstrateUnderrunError(context=context) + + elif not received and size != 0: # end-of-stream + raise error.EndOfStreamError(context=context) + + elif len(received) < size: + substrate.seek(-len(received), os.SEEK_CUR) + + # behave like a non-blocking stream + yield error.SubstrateUnderrunError(context=context) + + else: + break + + yield received diff --git a/module/scripts/lib/pyasn1/compat/__init__.py b/module/scripts/lib/pyasn1/compat/__init__.py new file mode 100644 index 0000000..d3e676a --- /dev/null +++ b/module/scripts/lib/pyasn1/compat/__init__.py @@ -0,0 +1,4 @@ +# This file is necessary to make this directory a package. + +# sentinal for missing argument +_MISSING = object() diff --git a/module/scripts/lib/pyasn1/compat/__pycache__/__init__.cpython-314.pyc b/module/scripts/lib/pyasn1/compat/__pycache__/__init__.cpython-314.pyc new file mode 100644 index 0000000000000000000000000000000000000000..d4e0b0ad66480da63b1ed24aabfca9715f23caec GIT binary patch literal 250 zcmdPq?kgESgOuGW)K5t)Nz^YbPA$?m&`-|K)k{pT)KAOINiEjbOHa+s z%*)hI%qz)EFG?)SEUDB@EGW<~PA*C;NzTv@bJ2B5OwKMX(9g|JDa}a*Nn{q36zk_? zCg~ScCKl%zf=nz(EYXjT2O3loAFo$Xd5gmaqTH^C9q0l^ATH(u5+9fu85uuvFz^U< LTQqSLu>yqv&^JVU literal 0 HcmV?d00001 diff --git a/module/scripts/lib/pyasn1/compat/__pycache__/integer.cpython-314.pyc b/module/scripts/lib/pyasn1/compat/__pycache__/integer.cpython-314.pyc new file mode 100644 index 0000000000000000000000000000000000000000..16912836084601b5d4817ea25878433a06636e44 GIT binary patch literal 707 zcmZ8fL5R~p6#bLzX3N&r_OL38AY~yEx7*o;g>_LPiu5RJkmx~LrfJNMrfEo~#ol^c z4`L51>_zBN4}!-%demE`NF{g>6l5Nu1nDEl$ExV zHojz{%L&@N0B{jZsg@}V-Nb5e^#MSW4;*u-oJ$!1U%Cf-i}O5PNN!L^Ly;0v$FrN79LX*6G+J&@R(EaB zVf#^rwd;)@vqMfUc8S}u12J=?%=l19s;V0KxJeknozM>OCbrtG3UPY)5q0elR~)-V zJ&K9Ps1uMbWxWmJ`#7`$!mNF4-rl%HtS22Gx7y8)YYT$H@aC)a*XQ1q-mXrx-I2C?j3$~s()8oIpI2|ZU-{5I(8pPGv|vto v3%H|6)e{kDC~p1+f-^h|_)|cXxjtJ!56<#b{7<34|HufTzj+1SM{)5#Y&MX; literal 0 HcmV?d00001 diff --git a/module/scripts/lib/pyasn1/compat/integer.py b/module/scripts/lib/pyasn1/compat/integer.py new file mode 100644 index 0000000..306210e --- /dev/null +++ b/module/scripts/lib/pyasn1/compat/integer.py @@ -0,0 +1,13 @@ +# +# This file is part of pyasn1 software. +# +# Copyright (c) 2005-2020, Ilya Etingof +# License: https://pyasn1.readthedocs.io/en/latest/license.html +# +def to_bytes(value, signed=False, length=0): + length = max(value.bit_length(), length) + + if signed and length % 8 == 0: + length += 1 + + return value.to_bytes(length // 8 + (length % 8 and 1 or 0), 'big', signed=signed) diff --git a/module/scripts/lib/pyasn1/debug.py b/module/scripts/lib/pyasn1/debug.py new file mode 100644 index 0000000..0719423 --- /dev/null +++ b/module/scripts/lib/pyasn1/debug.py @@ -0,0 +1,146 @@ +# +# This file is part of pyasn1 software. +# +# Copyright (c) 2005-2020, Ilya Etingof +# License: https://pyasn1.readthedocs.io/en/latest/license.html +# +import logging +import sys + +from pyasn1 import __version__ +from pyasn1 import error + +__all__ = ['Debug', 'setLogger', 'hexdump'] + +DEBUG_NONE = 0x0000 +DEBUG_ENCODER = 0x0001 +DEBUG_DECODER = 0x0002 +DEBUG_ALL = 0xffff + +FLAG_MAP = { + 'none': DEBUG_NONE, + 'encoder': DEBUG_ENCODER, + 'decoder': DEBUG_DECODER, + 'all': DEBUG_ALL +} + +LOGGEE_MAP = {} + + +class Printer(object): + # noinspection PyShadowingNames + def __init__(self, logger=None, handler=None, formatter=None): + if logger is None: + logger = logging.getLogger('pyasn1') + + logger.setLevel(logging.DEBUG) + + if handler is None: + handler = logging.StreamHandler() + + if formatter is None: + formatter = logging.Formatter('%(asctime)s %(name)s: %(message)s') + + handler.setFormatter(formatter) + handler.setLevel(logging.DEBUG) + logger.addHandler(handler) + + self.__logger = logger + + def __call__(self, msg): + self.__logger.debug(msg) + + def __str__(self): + return '' + + +class Debug(object): + defaultPrinter = Printer() + + def __init__(self, *flags, **options): + self._flags = DEBUG_NONE + + if 'loggerName' in options: + # route our logs to parent logger + self._printer = Printer( + logger=logging.getLogger(options['loggerName']), + handler=logging.NullHandler() + ) + + elif 'printer' in options: + self._printer = options.get('printer') + + else: + self._printer = self.defaultPrinter + + self._printer('running pyasn1 %s, debug flags %s' % (__version__, ', '.join(flags))) + + for flag in flags: + inverse = flag and flag[0] in ('!', '~') + if inverse: + flag = flag[1:] + try: + if inverse: + self._flags &= ~FLAG_MAP[flag] + else: + self._flags |= FLAG_MAP[flag] + except KeyError: + raise error.PyAsn1Error('bad debug flag %s' % flag) + + self._printer("debug category '%s' %s" % (flag, inverse and 'disabled' or 'enabled')) + + def __str__(self): + return 'logger %s, flags %x' % (self._printer, self._flags) + + def __call__(self, msg): + self._printer(msg) + + def __and__(self, flag): + return self._flags & flag + + def __rand__(self, flag): + return flag & self._flags + +_LOG = DEBUG_NONE + + +def setLogger(userLogger): + global _LOG + + if userLogger: + _LOG = userLogger + else: + _LOG = DEBUG_NONE + + # Update registered logging clients + for module, (name, flags) in LOGGEE_MAP.items(): + setattr(module, name, _LOG & flags and _LOG or DEBUG_NONE) + + +def registerLoggee(module, name='LOG', flags=DEBUG_NONE): + LOGGEE_MAP[sys.modules[module]] = name, flags + setLogger(_LOG) + return _LOG + + +def hexdump(octets): + return ' '.join( + ['%s%.2X' % (n % 16 == 0 and ('\n%.5d: ' % n) or '', x) + for n, x in zip(range(len(octets)), octets)] + ) + + +class Scope(object): + def __init__(self): + self._list = [] + + def __str__(self): return '.'.join(self._list) + + def push(self, token): + self._list.append(token) + + def pop(self): + return self._list.pop() + + +scope = Scope() diff --git a/module/scripts/lib/pyasn1/error.py b/module/scripts/lib/pyasn1/error.py new file mode 100644 index 0000000..75c9a3f --- /dev/null +++ b/module/scripts/lib/pyasn1/error.py @@ -0,0 +1,116 @@ +# +# This file is part of pyasn1 software. +# +# Copyright (c) 2005-2020, Ilya Etingof +# License: https://pyasn1.readthedocs.io/en/latest/license.html +# + + +class PyAsn1Error(Exception): + """Base pyasn1 exception + + `PyAsn1Error` is the base exception class (based on + :class:`Exception`) that represents all possible ASN.1 related + errors. + + Parameters + ---------- + args: + Opaque positional parameters + + Keyword Args + ------------ + kwargs: + Opaque keyword parameters + + """ + def __init__(self, *args, **kwargs): + self._args = args + self._kwargs = kwargs + + @property + def context(self): + """Return exception context + + When exception object is created, the caller can supply some opaque + context for the upper layers to better understand the cause of the + exception. + + Returns + ------- + : :py:class:`dict` + Dict holding context specific data + """ + return self._kwargs.get('context', {}) + + +class ValueConstraintError(PyAsn1Error): + """ASN.1 type constraints violation exception + + The `ValueConstraintError` exception indicates an ASN.1 value + constraint violation. + + It might happen on value object instantiation (for scalar types) or on + serialization (for constructed types). + """ + + +class SubstrateUnderrunError(PyAsn1Error): + """ASN.1 data structure deserialization error + + The `SubstrateUnderrunError` exception indicates insufficient serialised + data on input of a de-serialization codec. + """ + + +class EndOfStreamError(SubstrateUnderrunError): + """ASN.1 data structure deserialization error + + The `EndOfStreamError` exception indicates the condition of the input + stream has been closed. + """ + + +class UnsupportedSubstrateError(PyAsn1Error): + """Unsupported substrate type to parse as ASN.1 data.""" + + +class PyAsn1UnicodeError(PyAsn1Error, UnicodeError): + """Unicode text processing error + + The `PyAsn1UnicodeError` exception is a base class for errors relating to + unicode text de/serialization. + + Apart from inheriting from :class:`PyAsn1Error`, it also inherits from + :class:`UnicodeError` to help the caller catching unicode-related errors. + """ + def __init__(self, message, unicode_error=None): + if isinstance(unicode_error, UnicodeError): + UnicodeError.__init__(self, *unicode_error.args) + PyAsn1Error.__init__(self, message) + + +class PyAsn1UnicodeDecodeError(PyAsn1UnicodeError, UnicodeDecodeError): + """Unicode text decoding error + + The `PyAsn1UnicodeDecodeError` exception represents a failure to + deserialize unicode text. + + Apart from inheriting from :class:`PyAsn1UnicodeError`, it also inherits + from :class:`UnicodeDecodeError` to help the caller catching unicode-related + errors. + """ + + +class PyAsn1UnicodeEncodeError(PyAsn1UnicodeError, UnicodeEncodeError): + """Unicode text encoding error + + The `PyAsn1UnicodeEncodeError` exception represents a failure to + serialize unicode text. + + Apart from inheriting from :class:`PyAsn1UnicodeError`, it also inherits + from :class:`UnicodeEncodeError` to help the caller catching + unicode-related errors. + """ + + diff --git a/module/scripts/lib/pyasn1/type/__init__.py b/module/scripts/lib/pyasn1/type/__init__.py new file mode 100644 index 0000000..8c3066b --- /dev/null +++ b/module/scripts/lib/pyasn1/type/__init__.py @@ -0,0 +1 @@ +# This file is necessary to make this directory a package. diff --git a/module/scripts/lib/pyasn1/type/__pycache__/__init__.cpython-314.pyc b/module/scripts/lib/pyasn1/type/__pycache__/__init__.cpython-314.pyc new file mode 100644 index 0000000000000000000000000000000000000000..2b302619538344ccf19be5bc61dbc6513a0580ae GIT binary patch literal 210 zcmXwzJqiLb5QP&LMFhQsmB{XBtyqY)g@tVxV_c*A6Oycu&g*y-uduE5f_niI)Yr`S zc*DFI5Bfdg^}N4MGd~}}{6&AzbUa+ih78Gwu5U!Qyl+BSm#{~RP#K|N0=&!WBTpaaUO9CD literal 0 HcmV?d00001 diff --git a/module/scripts/lib/pyasn1/type/__pycache__/base.cpython-314.pyc b/module/scripts/lib/pyasn1/type/__pycache__/base.cpython-314.pyc new file mode 100644 index 0000000000000000000000000000000000000000..c3c6beda9a00611b86093a2c5cd96ce650888447 GIT binary patch literal 27610 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 literal 0 HcmV?d00001 diff --git a/module/scripts/lib/pyasn1/type/__pycache__/constraint.cpython-314.pyc b/module/scripts/lib/pyasn1/type/__pycache__/constraint.cpython-314.pyc new file mode 100644 index 0000000000000000000000000000000000000000..22c3a05a30b971c872049aaf3048b976e6e981cc GIT binary patch literal 28388 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 UJ}tm!zZhQF)bYOr{Id4{UodL*jsO4v literal 0 HcmV?d00001 diff --git a/module/scripts/lib/pyasn1/type/__pycache__/error.cpython-314.pyc b/module/scripts/lib/pyasn1/type/__pycache__/error.cpython-314.pyc new file mode 100644 index 0000000000000000000000000000000000000000..011847cd4e497150ef0ae936dd245047628b84a4 GIT binary patch literal 504 zcmXv~xlRKy5FPI(Q3yF2juIh+M1&H9LX_2ntF5))E+OQ%4P^bNM#$Xr K?jM+^;Qa%+oq;L< literal 0 HcmV?d00001 diff --git a/module/scripts/lib/pyasn1/type/__pycache__/namedtype.cpython-314.pyc b/module/scripts/lib/pyasn1/type/__pycache__/namedtype.cpython-314.pyc new file mode 100644 index 0000000000000000000000000000000000000000..3d14ff48b35045a795a7ab2c2ccc68be36156f96 GIT binary patch literal 23811 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 literal 0 HcmV?d00001 diff --git a/module/scripts/lib/pyasn1/type/__pycache__/namedval.cpython-314.pyc b/module/scripts/lib/pyasn1/type/__pycache__/namedval.cpython-314.pyc new file mode 100644 index 0000000000000000000000000000000000000000..a9f4a01e29179c44356049039cc50c3a948ff701 GIT binary patch literal 8005 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 PqABM+|NjvDYPj#Ao>uXKcGsKHhIiGsbLQ-s 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%0A3dn?yms~SdIQj;;RO>|Rs?Zl|#H`S_}dz6An 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 literal 0 HcmV?d00001 diff --git a/module/scripts/lib/pyasn1/type/base.py b/module/scripts/lib/pyasn1/type/base.py new file mode 100644 index 0000000..aa86e52 --- /dev/null +++ b/module/scripts/lib/pyasn1/type/base.py @@ -0,0 +1,699 @@ +# +# This file is part of pyasn1 software. +# +# Copyright (c) 2005-2020, Ilya Etingof +# License: https://pyasn1.readthedocs.io/en/latest/license.html +# +import sys + +from pyasn1 import error +from pyasn1.type import constraint +from pyasn1.type import tag +from pyasn1.type import tagmap + +__all__ = ['Asn1Item', 'Asn1Type', 'SimpleAsn1Type', + 'ConstructedAsn1Type'] + + +class Asn1Item(object): + @classmethod + def getTypeId(cls, increment=1): + try: + Asn1Item._typeCounter += increment + except AttributeError: + Asn1Item._typeCounter = increment + return Asn1Item._typeCounter + + +class Asn1Type(Asn1Item): + """Base class for all classes representing ASN.1 types. + + In the user code, |ASN.1| class is normally used only for telling + ASN.1 objects from others. + + Note + ---- + For as long as ASN.1 is concerned, a way to compare ASN.1 types + is to use :meth:`isSameTypeWith` and :meth:`isSuperTypeOf` methods. + """ + #: Set or return a :py:class:`~pyasn1.type.tag.TagSet` object representing + #: ASN.1 tag(s) associated with |ASN.1| type. + tagSet = tag.TagSet() + + #: Default :py:class:`~pyasn1.type.constraint.ConstraintsIntersection` + #: object imposing constraints on initialization values. + subtypeSpec = constraint.ConstraintsIntersection() + + # Disambiguation ASN.1 types identification + typeId = None + + def __init__(self, **kwargs): + readOnly = { + 'tagSet': self.tagSet, + 'subtypeSpec': self.subtypeSpec + } + + readOnly.update(kwargs) + + self.__dict__.update(readOnly) + + self._readOnly = readOnly + + def __setattr__(self, name, value): + if name[0] != '_' and name in self._readOnly: + raise error.PyAsn1Error('read-only instance attribute "%s"' % name) + + self.__dict__[name] = value + + def __str__(self): + return self.prettyPrint() + + @property + def readOnly(self): + return self._readOnly + + @property + def effectiveTagSet(self): + """For |ASN.1| type is equivalent to *tagSet* + """ + return self.tagSet # used by untagged types + + @property + def tagMap(self): + """Return a :class:`~pyasn1.type.tagmap.TagMap` object mapping ASN.1 tags to ASN.1 objects within callee object. + """ + return tagmap.TagMap({self.tagSet: self}) + + def isSameTypeWith(self, other, matchTags=True, matchConstraints=True): + """Examine |ASN.1| type for equality with other ASN.1 type. + + ASN.1 tags (:py:mod:`~pyasn1.type.tag`) and constraints + (:py:mod:`~pyasn1.type.constraint`) are examined when carrying + out ASN.1 types comparison. + + Python class inheritance relationship is NOT considered. + + Parameters + ---------- + other: a pyasn1 type object + Class instance representing ASN.1 type. + + Returns + ------- + : :class:`bool` + :obj:`True` if *other* is |ASN.1| type, + :obj:`False` otherwise. + """ + return (self is other or + (not matchTags or self.tagSet == other.tagSet) and + (not matchConstraints or self.subtypeSpec == other.subtypeSpec)) + + def isSuperTypeOf(self, other, matchTags=True, matchConstraints=True): + """Examine |ASN.1| type for subtype relationship with other ASN.1 type. + + ASN.1 tags (:py:mod:`~pyasn1.type.tag`) and constraints + (:py:mod:`~pyasn1.type.constraint`) are examined when carrying + out ASN.1 types comparison. + + Python class inheritance relationship is NOT considered. + + Parameters + ---------- + other: a pyasn1 type object + Class instance representing ASN.1 type. + + Returns + ------- + : :class:`bool` + :obj:`True` if *other* is a subtype of |ASN.1| type, + :obj:`False` otherwise. + """ + return (not matchTags or + (self.tagSet.isSuperTagSetOf(other.tagSet)) and + (not matchConstraints or self.subtypeSpec.isSuperTypeOf(other.subtypeSpec))) + + @staticmethod + def isNoValue(*values): + for value in values: + if value is not noValue: + return False + return True + + def prettyPrint(self, scope=0): + raise NotImplementedError + + # backward compatibility + + def getTagSet(self): + return self.tagSet + + def getEffectiveTagSet(self): + return self.effectiveTagSet + + def getTagMap(self): + return self.tagMap + + def getSubtypeSpec(self): + return self.subtypeSpec + + # backward compatibility + def hasValue(self): + return self.isValue + +# Backward compatibility +Asn1ItemBase = Asn1Type + + +class NoValue(object): + """Create a singleton instance of NoValue class. + + The *NoValue* sentinel object represents an instance of ASN.1 schema + object as opposed to ASN.1 value object. + + Only ASN.1 schema-related operations can be performed on ASN.1 + schema objects. + + Warning + ------- + Any operation attempted on the *noValue* object will raise the + *PyAsn1Error* exception. + """ + skipMethods = { + '__slots__', + # attributes + '__getattribute__', + '__getattr__', + '__setattr__', + '__delattr__', + # class instance + '__class__', + '__init__', + '__del__', + '__new__', + '__repr__', + '__qualname__', + '__objclass__', + 'im_class', + '__sizeof__', + # pickle protocol + '__reduce__', + '__reduce_ex__', + '__getnewargs__', + '__getinitargs__', + '__getstate__', + '__setstate__', + } + + _instance = None + + def __new__(cls): + if cls._instance is None: + def getPlug(name): + def plug(self, *args, **kw): + raise error.PyAsn1Error('Attempted "%s" operation on ASN.1 schema object' % name) + return plug + + op_names = [name + for typ in (str, int, list, dict) + for name in dir(typ) + if (name not in cls.skipMethods and + name.startswith('__') and + name.endswith('__') and + callable(getattr(typ, name)))] + + for name in set(op_names): + setattr(cls, name, getPlug(name)) + + cls._instance = object.__new__(cls) + + return cls._instance + + def __getattr__(self, attr): + if attr in self.skipMethods: + raise AttributeError('Attribute %s not present' % attr) + + raise error.PyAsn1Error('Attempted "%s" operation on ASN.1 schema object' % attr) + + def __repr__(self): + return '<%s object>' % self.__class__.__name__ + + +noValue = NoValue() + + +class SimpleAsn1Type(Asn1Type): + """Base class for all simple classes representing ASN.1 types. + + ASN.1 distinguishes types by their ability to hold other objects. + Scalar types are known as *simple* in ASN.1. + + In the user code, |ASN.1| class is normally used only for telling + ASN.1 objects from others. + + Note + ---- + For as long as ASN.1 is concerned, a way to compare ASN.1 types + is to use :meth:`isSameTypeWith` and :meth:`isSuperTypeOf` methods. + """ + #: Default payload value + defaultValue = noValue + + def __init__(self, value=noValue, **kwargs): + Asn1Type.__init__(self, **kwargs) + if value is noValue: + value = self.defaultValue + else: + value = self.prettyIn(value) + try: + self.subtypeSpec(value) + + except error.PyAsn1Error as exValue: + raise type(exValue)('%s at %s' % (exValue, self.__class__.__name__)) + + self._value = value + + def __repr__(self): + representation = '%s %s object' % ( + self.__class__.__name__, self.isValue and 'value' or 'schema') + + for attr, value in self.readOnly.items(): + if value: + representation += ', %s %s' % (attr, value) + + if self.isValue: + value = self.prettyPrint() + if len(value) > 32: + value = value[:16] + '...' + value[-16:] + representation += ', payload [%s]' % value + + return '<%s>' % representation + + def __eq__(self, other): + if self is other: + return True + return self._value == other + + def __ne__(self, other): + return self._value != other + + def __lt__(self, other): + return self._value < other + + def __le__(self, other): + return self._value <= other + + def __gt__(self, other): + return self._value > other + + def __ge__(self, other): + return self._value >= other + + def __bool__(self): + return bool(self._value) + + def __hash__(self): + return hash(self._value) + + @property + def isValue(self): + """Indicate that |ASN.1| object represents ASN.1 value. + + If *isValue* is :obj:`False` then this object represents just + ASN.1 schema. + + If *isValue* is :obj:`True` then, in addition to its ASN.1 schema + features, this object can also be used like a Python built-in object + (e.g. :class:`int`, :class:`str`, :class:`dict` etc.). + + Returns + ------- + : :class:`bool` + :obj:`False` if object represents just ASN.1 schema. + :obj:`True` if object represents ASN.1 schema and can be used as a normal value. + + Note + ---- + There is an important distinction between PyASN1 schema and value objects. + The PyASN1 schema objects can only participate in ASN.1 schema-related + operations (e.g. defining or testing the structure of the data). Most + obvious uses of ASN.1 schema is to guide serialisation codecs whilst + encoding/decoding serialised ASN.1 contents. + + The PyASN1 value objects can **additionally** participate in many operations + involving regular Python objects (e.g. arithmetic, comprehension etc). + """ + return self._value is not noValue + + def clone(self, value=noValue, **kwargs): + """Create a modified version of |ASN.1| schema or value object. + + The `clone()` method accepts the same set arguments as |ASN.1| + class takes on instantiation except that all arguments + of the `clone()` method are optional. + + Whatever arguments are supplied, they are used to create a copy + of `self` taking precedence over the ones used to instantiate `self`. + + Note + ---- + Due to the immutable nature of the |ASN.1| object, if no arguments + are supplied, no new |ASN.1| object will be created and `self` will + be returned instead. + """ + if value is noValue: + if not kwargs: + return self + + value = self._value + + initializers = self.readOnly.copy() + initializers.update(kwargs) + + return self.__class__(value, **initializers) + + def subtype(self, value=noValue, **kwargs): + """Create a specialization of |ASN.1| schema or value object. + + The subtype relationship between ASN.1 types has no correlation with + subtype relationship between Python types. ASN.1 type is mainly identified + by its tag(s) (:py:class:`~pyasn1.type.tag.TagSet`) and value range + constraints (:py:class:`~pyasn1.type.constraint.ConstraintsIntersection`). + These ASN.1 type properties are implemented as |ASN.1| attributes. + + The `subtype()` method accepts the same set arguments as |ASN.1| + class takes on instantiation except that all parameters + of the `subtype()` method are optional. + + With the exception of the arguments described below, the rest of + supplied arguments they are used to create a copy of `self` taking + precedence over the ones used to instantiate `self`. + + The following arguments to `subtype()` create a ASN.1 subtype out of + |ASN.1| type: + + Other Parameters + ---------------- + implicitTag: :py:class:`~pyasn1.type.tag.Tag` + Implicitly apply given ASN.1 tag object to `self`'s + :py:class:`~pyasn1.type.tag.TagSet`, then use the result as + new object's ASN.1 tag(s). + + explicitTag: :py:class:`~pyasn1.type.tag.Tag` + Explicitly apply given ASN.1 tag object to `self`'s + :py:class:`~pyasn1.type.tag.TagSet`, then use the result as + new object's ASN.1 tag(s). + + subtypeSpec: :py:class:`~pyasn1.type.constraint.ConstraintsIntersection` + Add ASN.1 constraints object to one of the `self`'s, then + use the result as new object's ASN.1 constraints. + + Returns + ------- + : + new instance of |ASN.1| schema or value object + + Note + ---- + Due to the immutable nature of the |ASN.1| object, if no arguments + are supplied, no new |ASN.1| object will be created and `self` will + be returned instead. + """ + if value is noValue: + if not kwargs: + return self + + value = self._value + + initializers = self.readOnly.copy() + + implicitTag = kwargs.pop('implicitTag', None) + if implicitTag is not None: + initializers['tagSet'] = self.tagSet.tagImplicitly(implicitTag) + + explicitTag = kwargs.pop('explicitTag', None) + if explicitTag is not None: + initializers['tagSet'] = self.tagSet.tagExplicitly(explicitTag) + + for arg, option in kwargs.items(): + initializers[arg] += option + + return self.__class__(value, **initializers) + + def prettyIn(self, value): + return value + + def prettyOut(self, value): + return str(value) + + def prettyPrint(self, scope=0): + return self.prettyOut(self._value) + + def prettyPrintType(self, scope=0): + return '%s -> %s' % (self.tagSet, self.__class__.__name__) + +# Backward compatibility +AbstractSimpleAsn1Item = SimpleAsn1Type + +# +# Constructed types: +# * There are five of them: Sequence, SequenceOf/SetOf, Set and Choice +# * ASN1 types and values are represened by Python class instances +# * Value initialization is made for defaulted components only +# * Primary method of component addressing is by-position. Data model for base +# type is Python sequence. Additional type-specific addressing methods +# may be implemented for particular types. +# * SequenceOf and SetOf types do not implement any additional methods +# * Sequence, Set and Choice types also implement by-identifier addressing +# * Sequence, Set and Choice types also implement by-asn1-type (tag) addressing +# * Sequence and Set types may include optional and defaulted +# components +# * Constructed types hold a reference to component types used for value +# verification and ordering. +# * Component type is a scalar type for SequenceOf/SetOf types and a list +# of types for Sequence/Set/Choice. +# + + +class ConstructedAsn1Type(Asn1Type): + """Base class for all constructed classes representing ASN.1 types. + + ASN.1 distinguishes types by their ability to hold other objects. + Those "nesting" types are known as *constructed* in ASN.1. + + In the user code, |ASN.1| class is normally used only for telling + ASN.1 objects from others. + + Note + ---- + For as long as ASN.1 is concerned, a way to compare ASN.1 types + is to use :meth:`isSameTypeWith` and :meth:`isSuperTypeOf` methods. + """ + + #: If :obj:`True`, requires exact component type matching, + #: otherwise subtype relation is only enforced + strictConstraints = False + + componentType = None + + # backward compatibility, unused + sizeSpec = constraint.ConstraintsIntersection() + + def __init__(self, **kwargs): + readOnly = { + 'componentType': self.componentType, + # backward compatibility, unused + 'sizeSpec': self.sizeSpec + } + + # backward compatibility: preserve legacy sizeSpec support + kwargs = self._moveSizeSpec(**kwargs) + + readOnly.update(kwargs) + + Asn1Type.__init__(self, **readOnly) + + def _moveSizeSpec(self, **kwargs): + # backward compatibility, unused + sizeSpec = kwargs.pop('sizeSpec', self.sizeSpec) + if sizeSpec: + subtypeSpec = kwargs.pop('subtypeSpec', self.subtypeSpec) + if subtypeSpec: + subtypeSpec = sizeSpec + + else: + subtypeSpec += sizeSpec + + kwargs['subtypeSpec'] = subtypeSpec + + return kwargs + + def __repr__(self): + representation = '%s %s object' % ( + self.__class__.__name__, self.isValue and 'value' or 'schema' + ) + + for attr, value in self.readOnly.items(): + if value is not noValue: + representation += ', %s=%r' % (attr, value) + + if self.isValue and self.components: + representation += ', payload [%s]' % ', '.join( + [repr(x) for x in self.components]) + + return '<%s>' % representation + + def __eq__(self, other): + return self is other or self.components == other + + def __ne__(self, other): + return self.components != other + + def __lt__(self, other): + return self.components < other + + def __le__(self, other): + return self.components <= other + + def __gt__(self, other): + return self.components > other + + def __ge__(self, other): + return self.components >= other + + def __bool__(self): + return bool(self.components) + + @property + def components(self): + raise error.PyAsn1Error('Method not implemented') + + def _cloneComponentValues(self, myClone, cloneValueFlag): + pass + + def clone(self, **kwargs): + """Create a modified version of |ASN.1| schema object. + + The `clone()` method accepts the same set arguments as |ASN.1| + class takes on instantiation except that all arguments + of the `clone()` method are optional. + + Whatever arguments are supplied, they are used to create a copy + of `self` taking precedence over the ones used to instantiate `self`. + + Possible values of `self` are never copied over thus `clone()` can + only create a new schema object. + + Returns + ------- + : + new instance of |ASN.1| type/value + + Note + ---- + Due to the mutable nature of the |ASN.1| object, even if no arguments + are supplied, a new |ASN.1| object will be created and returned. + """ + cloneValueFlag = kwargs.pop('cloneValueFlag', False) + + initializers = self.readOnly.copy() + initializers.update(kwargs) + + clone = self.__class__(**initializers) + + if cloneValueFlag: + self._cloneComponentValues(clone, cloneValueFlag) + + return clone + + def subtype(self, **kwargs): + """Create a specialization of |ASN.1| schema object. + + The `subtype()` method accepts the same set arguments as |ASN.1| + class takes on instantiation except that all parameters + of the `subtype()` method are optional. + + With the exception of the arguments described below, the rest of + supplied arguments they are used to create a copy of `self` taking + precedence over the ones used to instantiate `self`. + + The following arguments to `subtype()` create a ASN.1 subtype out of + |ASN.1| type. + + Other Parameters + ---------------- + implicitTag: :py:class:`~pyasn1.type.tag.Tag` + Implicitly apply given ASN.1 tag object to `self`'s + :py:class:`~pyasn1.type.tag.TagSet`, then use the result as + new object's ASN.1 tag(s). + + explicitTag: :py:class:`~pyasn1.type.tag.Tag` + Explicitly apply given ASN.1 tag object to `self`'s + :py:class:`~pyasn1.type.tag.TagSet`, then use the result as + new object's ASN.1 tag(s). + + subtypeSpec: :py:class:`~pyasn1.type.constraint.ConstraintsIntersection` + Add ASN.1 constraints object to one of the `self`'s, then + use the result as new object's ASN.1 constraints. + + + Returns + ------- + : + new instance of |ASN.1| type/value + + Note + ---- + Due to the mutable nature of the |ASN.1| object, even if no arguments + are supplied, a new |ASN.1| object will be created and returned. + """ + + initializers = self.readOnly.copy() + + cloneValueFlag = kwargs.pop('cloneValueFlag', False) + + implicitTag = kwargs.pop('implicitTag', None) + if implicitTag is not None: + initializers['tagSet'] = self.tagSet.tagImplicitly(implicitTag) + + explicitTag = kwargs.pop('explicitTag', None) + if explicitTag is not None: + initializers['tagSet'] = self.tagSet.tagExplicitly(explicitTag) + + for arg, option in kwargs.items(): + initializers[arg] += option + + clone = self.__class__(**initializers) + + if cloneValueFlag: + self._cloneComponentValues(clone, cloneValueFlag) + + return clone + + def getComponentByPosition(self, idx): + raise error.PyAsn1Error('Method not implemented') + + def setComponentByPosition(self, idx, value, verifyConstraints=True): + raise error.PyAsn1Error('Method not implemented') + + def setComponents(self, *args, **kwargs): + for idx, value in enumerate(args): + self[idx] = value + for k in kwargs: + self[k] = kwargs[k] + return self + + # backward compatibility + + def setDefaultComponents(self): + pass + + def getComponentType(self): + return self.componentType + + # backward compatibility, unused + def verifySizeSpec(self): + self.subtypeSpec(self) + + + # Backward compatibility +AbstractConstructedAsn1Item = ConstructedAsn1Type diff --git a/module/scripts/lib/pyasn1/type/char.py b/module/scripts/lib/pyasn1/type/char.py new file mode 100644 index 0000000..ec65f00 --- /dev/null +++ b/module/scripts/lib/pyasn1/type/char.py @@ -0,0 +1,288 @@ +# +# This file is part of pyasn1 software. +# +# Copyright (c) 2005-2020, Ilya Etingof +# License: https://pyasn1.readthedocs.io/en/latest/license.html +# +import sys + +from pyasn1 import error +from pyasn1.type import tag +from pyasn1.type import univ + +__all__ = ['NumericString', 'PrintableString', 'TeletexString', 'T61String', 'VideotexString', + 'IA5String', 'GraphicString', 'VisibleString', 'ISO646String', + 'GeneralString', 'UniversalString', 'BMPString', 'UTF8String'] + +NoValue = univ.NoValue +noValue = univ.noValue + + +class AbstractCharacterString(univ.OctetString): + """Creates |ASN.1| schema or value object. + + |ASN.1| class is based on :class:`~pyasn1.type.base.SimpleAsn1Type`, + its objects are immutable and duck-type :class:`bytes`. + When used in octet-stream context, |ASN.1| type assumes + "|encoding|" encoding. + + Keyword Args + ------------ + value: :class:`str`, :class:`bytes` or |ASN.1| object + :class:`str`, alternatively :class:`bytes` + representing octet-stream of serialised unicode string + (note `encoding` parameter) or |ASN.1| class instance. + If `value` is not given, schema object will be created. + + tagSet: :py:class:`~pyasn1.type.tag.TagSet` + Object representing non-default ASN.1 tag(s) + + subtypeSpec: :py:class:`~pyasn1.type.constraint.ConstraintsIntersection` + Object representing non-default ASN.1 subtype constraint(s). Constraints + verification for |ASN.1| type occurs automatically on object + instantiation. + + encoding: :py:class:`str` + Unicode codec ID to encode/decode + :class:`str` the payload when |ASN.1| object is used + in octet-stream context. + + Raises + ------ + ~pyasn1.error.ValueConstraintError, ~pyasn1.error.PyAsn1Error + On constraint violation or bad initializer. + """ + + def __str__(self): + return str(self._value) + + def __bytes__(self): + try: + return self._value.encode(self.encoding) + except UnicodeEncodeError as exc: + raise error.PyAsn1UnicodeEncodeError( + "Can't encode string '%s' with codec " + "%s" % (self._value, self.encoding), exc + ) + + def prettyIn(self, value): + try: + if isinstance(value, str): + return value + elif isinstance(value, bytes): + return value.decode(self.encoding) + elif isinstance(value, (tuple, list)): + return self.prettyIn(bytes(value)) + elif isinstance(value, univ.OctetString): + return value.asOctets().decode(self.encoding) + else: + return str(value) + + except (UnicodeDecodeError, LookupError) as exc: + raise error.PyAsn1UnicodeDecodeError( + "Can't decode string '%s' with codec " + "%s" % (value, self.encoding), exc + ) + + def asOctets(self, padding=True): + return bytes(self) + + def asNumbers(self, padding=True): + return tuple(bytes(self)) + + # + # See OctetString.prettyPrint() for the explanation + # + + def prettyOut(self, value): + return value + + def prettyPrint(self, scope=0): + # first see if subclass has its own .prettyOut() + value = self.prettyOut(self._value) + + if value is not self._value: + return value + + return AbstractCharacterString.__str__(self) + + def __reversed__(self): + return reversed(self._value) + + +class NumericString(AbstractCharacterString): + __doc__ = AbstractCharacterString.__doc__ + + #: Set (on class, not on instance) or return a + #: :py:class:`~pyasn1.type.tag.TagSet` object representing ASN.1 tag(s) + #: associated with |ASN.1| type. + tagSet = AbstractCharacterString.tagSet.tagImplicitly( + tag.Tag(tag.tagClassUniversal, tag.tagFormatSimple, 18) + ) + encoding = 'us-ascii' + + # Optimization for faster codec lookup + typeId = AbstractCharacterString.getTypeId() + + +class PrintableString(AbstractCharacterString): + __doc__ = AbstractCharacterString.__doc__ + + #: Set (on class, not on instance) or return a + #: :py:class:`~pyasn1.type.tag.TagSet` object representing ASN.1 tag(s) + #: associated with |ASN.1| type. + tagSet = AbstractCharacterString.tagSet.tagImplicitly( + tag.Tag(tag.tagClassUniversal, tag.tagFormatSimple, 19) + ) + encoding = 'us-ascii' + + # Optimization for faster codec lookup + typeId = AbstractCharacterString.getTypeId() + + +class TeletexString(AbstractCharacterString): + __doc__ = AbstractCharacterString.__doc__ + + #: Set (on class, not on instance) or return a + #: :py:class:`~pyasn1.type.tag.TagSet` object representing ASN.1 tag(s) + #: associated with |ASN.1| type. + tagSet = AbstractCharacterString.tagSet.tagImplicitly( + tag.Tag(tag.tagClassUniversal, tag.tagFormatSimple, 20) + ) + encoding = 'iso-8859-1' + + # Optimization for faster codec lookup + typeId = AbstractCharacterString.getTypeId() + + +class T61String(TeletexString): + __doc__ = TeletexString.__doc__ + + # Optimization for faster codec lookup + typeId = AbstractCharacterString.getTypeId() + + +class VideotexString(AbstractCharacterString): + __doc__ = AbstractCharacterString.__doc__ + + #: Set (on class, not on instance) or return a + #: :py:class:`~pyasn1.type.tag.TagSet` object representing ASN.1 tag(s) + #: associated with |ASN.1| type. + tagSet = AbstractCharacterString.tagSet.tagImplicitly( + tag.Tag(tag.tagClassUniversal, tag.tagFormatSimple, 21) + ) + encoding = 'iso-8859-1' + + # Optimization for faster codec lookup + typeId = AbstractCharacterString.getTypeId() + + +class IA5String(AbstractCharacterString): + __doc__ = AbstractCharacterString.__doc__ + + #: Set (on class, not on instance) or return a + #: :py:class:`~pyasn1.type.tag.TagSet` object representing ASN.1 tag(s) + #: associated with |ASN.1| type. + tagSet = AbstractCharacterString.tagSet.tagImplicitly( + tag.Tag(tag.tagClassUniversal, tag.tagFormatSimple, 22) + ) + encoding = 'us-ascii' + + # Optimization for faster codec lookup + typeId = AbstractCharacterString.getTypeId() + + +class GraphicString(AbstractCharacterString): + __doc__ = AbstractCharacterString.__doc__ + + #: Set (on class, not on instance) or return a + #: :py:class:`~pyasn1.type.tag.TagSet` object representing ASN.1 tag(s) + #: associated with |ASN.1| type. + tagSet = AbstractCharacterString.tagSet.tagImplicitly( + tag.Tag(tag.tagClassUniversal, tag.tagFormatSimple, 25) + ) + encoding = 'iso-8859-1' + + # Optimization for faster codec lookup + typeId = AbstractCharacterString.getTypeId() + + +class VisibleString(AbstractCharacterString): + __doc__ = AbstractCharacterString.__doc__ + + #: Set (on class, not on instance) or return a + #: :py:class:`~pyasn1.type.tag.TagSet` object representing ASN.1 tag(s) + #: associated with |ASN.1| type. + tagSet = AbstractCharacterString.tagSet.tagImplicitly( + tag.Tag(tag.tagClassUniversal, tag.tagFormatSimple, 26) + ) + encoding = 'us-ascii' + + # Optimization for faster codec lookup + typeId = AbstractCharacterString.getTypeId() + + +class ISO646String(VisibleString): + __doc__ = VisibleString.__doc__ + + # Optimization for faster codec lookup + typeId = AbstractCharacterString.getTypeId() + +class GeneralString(AbstractCharacterString): + __doc__ = AbstractCharacterString.__doc__ + + #: Set (on class, not on instance) or return a + #: :py:class:`~pyasn1.type.tag.TagSet` object representing ASN.1 tag(s) + #: associated with |ASN.1| type. + tagSet = AbstractCharacterString.tagSet.tagImplicitly( + tag.Tag(tag.tagClassUniversal, tag.tagFormatSimple, 27) + ) + encoding = 'iso-8859-1' + + # Optimization for faster codec lookup + typeId = AbstractCharacterString.getTypeId() + + +class UniversalString(AbstractCharacterString): + __doc__ = AbstractCharacterString.__doc__ + + #: Set (on class, not on instance) or return a + #: :py:class:`~pyasn1.type.tag.TagSet` object representing ASN.1 tag(s) + #: associated with |ASN.1| type. + tagSet = AbstractCharacterString.tagSet.tagImplicitly( + tag.Tag(tag.tagClassUniversal, tag.tagFormatSimple, 28) + ) + encoding = "utf-32-be" + + # Optimization for faster codec lookup + typeId = AbstractCharacterString.getTypeId() + + +class BMPString(AbstractCharacterString): + __doc__ = AbstractCharacterString.__doc__ + + #: Set (on class, not on instance) or return a + #: :py:class:`~pyasn1.type.tag.TagSet` object representing ASN.1 tag(s) + #: associated with |ASN.1| type. + tagSet = AbstractCharacterString.tagSet.tagImplicitly( + tag.Tag(tag.tagClassUniversal, tag.tagFormatSimple, 30) + ) + encoding = "utf-16-be" + + # Optimization for faster codec lookup + typeId = AbstractCharacterString.getTypeId() + + +class UTF8String(AbstractCharacterString): + __doc__ = AbstractCharacterString.__doc__ + + #: Set (on class, not on instance) or return a + #: :py:class:`~pyasn1.type.tag.TagSet` object representing ASN.1 tag(s) + #: associated with |ASN.1| type. + tagSet = AbstractCharacterString.tagSet.tagImplicitly( + tag.Tag(tag.tagClassUniversal, tag.tagFormatSimple, 12) + ) + encoding = "utf-8" + + # Optimization for faster codec lookup + typeId = AbstractCharacterString.getTypeId() diff --git a/module/scripts/lib/pyasn1/type/constraint.py b/module/scripts/lib/pyasn1/type/constraint.py new file mode 100644 index 0000000..02368d0 --- /dev/null +++ b/module/scripts/lib/pyasn1/type/constraint.py @@ -0,0 +1,751 @@ +# +# This file is part of pyasn1 software. +# +# Copyright (c) 2005-2020, Ilya Etingof +# License: https://pyasn1.readthedocs.io/en/latest/license.html +# +# Original concept and code by Mike C. Fletcher. +# +import sys + +from pyasn1.type import error + +__all__ = ['SingleValueConstraint', 'ContainedSubtypeConstraint', + 'ValueRangeConstraint', 'ValueSizeConstraint', + 'PermittedAlphabetConstraint', 'InnerTypeConstraint', + 'ConstraintsExclusion', 'ConstraintsIntersection', + 'ConstraintsUnion'] + + +class AbstractConstraint(object): + + def __init__(self, *values): + self._valueMap = set() + self._setValues(values) + self.__hash = hash((self.__class__.__name__, self._values)) + + def __call__(self, value, idx=None): + if not self._values: + return + + try: + self._testValue(value, idx) + + except error.ValueConstraintError as exc: + raise error.ValueConstraintError( + '%s failed at: %r' % (self, exc) + ) + + def __repr__(self): + representation = '%s object' % (self.__class__.__name__) + + if self._values: + representation += ', consts %s' % ', '.join( + [repr(x) for x in self._values]) + + return '<%s>' % representation + + def __eq__(self, other): + if self is other: + return True + return self._values == other + + def __ne__(self, other): + return self._values != other + + def __lt__(self, other): + return self._values < other + + def __le__(self, other): + return self._values <= other + + def __gt__(self, other): + return self._values > other + + def __ge__(self, other): + return self._values >= other + + def __bool__(self): + return bool(self._values) + + def __hash__(self): + return self.__hash + + def _setValues(self, values): + self._values = values + + def _testValue(self, value, idx): + raise error.ValueConstraintError(value) + + # Constraints derivation logic + def getValueMap(self): + return self._valueMap + + def isSuperTypeOf(self, otherConstraint): + # TODO: fix possible comparison of set vs scalars here + return (otherConstraint is self or + not self._values or + otherConstraint == self or + self in otherConstraint.getValueMap()) + + def isSubTypeOf(self, otherConstraint): + return (otherConstraint is self or + not self or + otherConstraint == self or + otherConstraint in self._valueMap) + + +class SingleValueConstraint(AbstractConstraint): + """Create a SingleValueConstraint object. + + The SingleValueConstraint satisfies any value that + is present in the set of permitted values. + + Objects of this type are iterable (emitting constraint values) and + can act as operands for some arithmetic operations e.g. addition + and subtraction. The latter can be used for combining multiple + SingleValueConstraint objects into one. + + The SingleValueConstraint object can be applied to + any ASN.1 type. + + Parameters + ---------- + *values: :class:`int` + Full set of values permitted by this constraint object. + + Examples + -------- + .. code-block:: python + + class DivisorOfSix(Integer): + ''' + ASN.1 specification: + + Divisor-Of-6 ::= INTEGER (1 | 2 | 3 | 6) + ''' + subtypeSpec = SingleValueConstraint(1, 2, 3, 6) + + # this will succeed + divisor_of_six = DivisorOfSix(1) + + # this will raise ValueConstraintError + divisor_of_six = DivisorOfSix(7) + """ + def _setValues(self, values): + self._values = values + self._set = set(values) + + def _testValue(self, value, idx): + if value not in self._set: + raise error.ValueConstraintError(value) + + # Constrains can be merged or reduced + + def __contains__(self, item): + return item in self._set + + def __iter__(self): + return iter(self._set) + + def __add__(self, constraint): + return self.__class__(*(self._set.union(constraint))) + + def __sub__(self, constraint): + return self.__class__(*(self._set.difference(constraint))) + + +class ContainedSubtypeConstraint(AbstractConstraint): + """Create a ContainedSubtypeConstraint object. + + The ContainedSubtypeConstraint satisfies any value that + is present in the set of permitted values and also + satisfies included constraints. + + The ContainedSubtypeConstraint object can be applied to + any ASN.1 type. + + Parameters + ---------- + *values: + Full set of values and constraint objects permitted + by this constraint object. + + Examples + -------- + .. code-block:: python + + class DivisorOfEighteen(Integer): + ''' + ASN.1 specification: + + Divisors-of-18 ::= INTEGER (INCLUDES Divisors-of-6 | 9 | 18) + ''' + subtypeSpec = ContainedSubtypeConstraint( + SingleValueConstraint(1, 2, 3, 6), 9, 18 + ) + + # this will succeed + divisor_of_eighteen = DivisorOfEighteen(9) + + # this will raise ValueConstraintError + divisor_of_eighteen = DivisorOfEighteen(10) + """ + def _testValue(self, value, idx): + for constraint in self._values: + if isinstance(constraint, AbstractConstraint): + constraint(value, idx) + elif value not in self._set: + raise error.ValueConstraintError(value) + + +class ValueRangeConstraint(AbstractConstraint): + """Create a ValueRangeConstraint object. + + The ValueRangeConstraint satisfies any value that + falls in the range of permitted values. + + The ValueRangeConstraint object can only be applied + to :class:`~pyasn1.type.univ.Integer` and + :class:`~pyasn1.type.univ.Real` types. + + Parameters + ---------- + start: :class:`int` + Minimum permitted value in the range (inclusive) + + end: :class:`int` + Maximum permitted value in the range (inclusive) + + Examples + -------- + .. code-block:: python + + class TeenAgeYears(Integer): + ''' + ASN.1 specification: + + TeenAgeYears ::= INTEGER (13 .. 19) + ''' + subtypeSpec = ValueRangeConstraint(13, 19) + + # this will succeed + teen_year = TeenAgeYears(18) + + # this will raise ValueConstraintError + teen_year = TeenAgeYears(20) + """ + def _testValue(self, value, idx): + if value < self.start or value > self.stop: + raise error.ValueConstraintError(value) + + def _setValues(self, values): + if len(values) != 2: + raise error.PyAsn1Error( + '%s: bad constraint values' % (self.__class__.__name__,) + ) + self.start, self.stop = values + if self.start > self.stop: + raise error.PyAsn1Error( + '%s: screwed constraint values (start > stop): %s > %s' % ( + self.__class__.__name__, + self.start, self.stop + ) + ) + AbstractConstraint._setValues(self, values) + + +class ValueSizeConstraint(ValueRangeConstraint): + """Create a ValueSizeConstraint object. + + The ValueSizeConstraint satisfies any value for + as long as its size falls within the range of + permitted sizes. + + The ValueSizeConstraint object can be applied + to :class:`~pyasn1.type.univ.BitString`, + :class:`~pyasn1.type.univ.OctetString` (including + all :ref:`character ASN.1 types `), + :class:`~pyasn1.type.univ.SequenceOf` + and :class:`~pyasn1.type.univ.SetOf` types. + + Parameters + ---------- + minimum: :class:`int` + Minimum permitted size of the value (inclusive) + + maximum: :class:`int` + Maximum permitted size of the value (inclusive) + + Examples + -------- + .. code-block:: python + + class BaseballTeamRoster(SetOf): + ''' + ASN.1 specification: + + BaseballTeamRoster ::= SET SIZE (1..25) OF PlayerNames + ''' + componentType = PlayerNames() + subtypeSpec = ValueSizeConstraint(1, 25) + + # this will succeed + team = BaseballTeamRoster() + team.extend(['Jan', 'Matej']) + encode(team) + + # this will raise ValueConstraintError + team = BaseballTeamRoster() + team.extend(['Jan'] * 26) + encode(team) + + Note + ---- + Whenever ValueSizeConstraint is applied to mutable types + (e.g. :class:`~pyasn1.type.univ.SequenceOf`, + :class:`~pyasn1.type.univ.SetOf`), constraint + validation only happens at the serialisation phase rather + than schema instantiation phase (as it is with immutable + types). + """ + def _testValue(self, value, idx): + valueSize = len(value) + if valueSize < self.start or valueSize > self.stop: + raise error.ValueConstraintError(value) + + +class PermittedAlphabetConstraint(SingleValueConstraint): + """Create a PermittedAlphabetConstraint object. + + The PermittedAlphabetConstraint satisfies any character + string for as long as all its characters are present in + the set of permitted characters. + + Objects of this type are iterable (emitting constraint values) and + can act as operands for some arithmetic operations e.g. addition + and subtraction. + + The PermittedAlphabetConstraint object can only be applied + to the :ref:`character ASN.1 types ` such as + :class:`~pyasn1.type.char.IA5String`. + + Parameters + ---------- + *alphabet: :class:`str` + Full set of characters permitted by this constraint object. + + Example + ------- + .. code-block:: python + + class BooleanValue(IA5String): + ''' + ASN.1 specification: + + BooleanValue ::= IA5String (FROM ('T' | 'F')) + ''' + subtypeSpec = PermittedAlphabetConstraint('T', 'F') + + # this will succeed + truth = BooleanValue('T') + truth = BooleanValue('TF') + + # this will raise ValueConstraintError + garbage = BooleanValue('TAF') + + ASN.1 `FROM ... EXCEPT ...` clause can be modelled by combining multiple + PermittedAlphabetConstraint objects into one: + + Example + ------- + .. code-block:: python + + class Lipogramme(IA5String): + ''' + ASN.1 specification: + + Lipogramme ::= + IA5String (FROM (ALL EXCEPT ("e"|"E"))) + ''' + subtypeSpec = ( + PermittedAlphabetConstraint(*string.printable) - + PermittedAlphabetConstraint('e', 'E') + ) + + # this will succeed + lipogramme = Lipogramme('A work of fiction?') + + # this will raise ValueConstraintError + lipogramme = Lipogramme('Eel') + + Note + ---- + Although `ConstraintsExclusion` object could seemingly be used for this + purpose, practically, for it to work, it needs to represent its operand + constraints as sets and intersect one with the other. That would require + the insight into the constraint values (and their types) that are otherwise + hidden inside the constraint object. + + Therefore it's more practical to model `EXCEPT` clause at + `PermittedAlphabetConstraint` level instead. + """ + def _setValues(self, values): + self._values = values + self._set = set(values) + + def _testValue(self, value, idx): + if not self._set.issuperset(value): + raise error.ValueConstraintError(value) + + +class ComponentPresentConstraint(AbstractConstraint): + """Create a ComponentPresentConstraint object. + + The ComponentPresentConstraint is only satisfied when the value + is not `None`. + + The ComponentPresentConstraint object is typically used with + `WithComponentsConstraint`. + + Examples + -------- + .. code-block:: python + + present = ComponentPresentConstraint() + + # this will succeed + present('whatever') + + # this will raise ValueConstraintError + present(None) + """ + def _setValues(self, values): + self._values = ('',) + + if values: + raise error.PyAsn1Error('No arguments expected') + + def _testValue(self, value, idx): + if value is None: + raise error.ValueConstraintError( + 'Component is not present:') + + +class ComponentAbsentConstraint(AbstractConstraint): + """Create a ComponentAbsentConstraint object. + + The ComponentAbsentConstraint is only satisfied when the value + is `None`. + + The ComponentAbsentConstraint object is typically used with + `WithComponentsConstraint`. + + Examples + -------- + .. code-block:: python + + absent = ComponentAbsentConstraint() + + # this will succeed + absent(None) + + # this will raise ValueConstraintError + absent('whatever') + """ + def _setValues(self, values): + self._values = ('',) + + if values: + raise error.PyAsn1Error('No arguments expected') + + def _testValue(self, value, idx): + if value is not None: + raise error.ValueConstraintError( + 'Component is not absent: %r' % value) + + +class WithComponentsConstraint(AbstractConstraint): + """Create a WithComponentsConstraint object. + + The `WithComponentsConstraint` satisfies any mapping object that has + constrained fields present or absent, what is indicated by + `ComponentPresentConstraint` and `ComponentAbsentConstraint` + objects respectively. + + The `WithComponentsConstraint` object is typically applied + to :class:`~pyasn1.type.univ.Set` or + :class:`~pyasn1.type.univ.Sequence` types. + + Parameters + ---------- + *fields: :class:`tuple` + Zero or more tuples of (`field`, `constraint`) indicating constrained + fields. + + Notes + ----- + On top of the primary use of `WithComponentsConstraint` (ensuring presence + or absence of particular components of a :class:`~pyasn1.type.univ.Set` or + :class:`~pyasn1.type.univ.Sequence`), it is also possible to pass any other + constraint objects or their combinations. In case of scalar fields, these + constraints will be verified in addition to the constraints belonging to + scalar components themselves. However, formally, these additional + constraints do not change the type of these ASN.1 objects. + + Examples + -------- + + .. code-block:: python + + class Item(Sequence): # Set is similar + ''' + ASN.1 specification: + + Item ::= SEQUENCE { + id INTEGER OPTIONAL, + name OCTET STRING OPTIONAL + } WITH COMPONENTS id PRESENT, name ABSENT | id ABSENT, name PRESENT + ''' + componentType = NamedTypes( + OptionalNamedType('id', Integer()), + OptionalNamedType('name', OctetString()) + ) + withComponents = ConstraintsUnion( + WithComponentsConstraint( + ('id', ComponentPresentConstraint()), + ('name', ComponentAbsentConstraint()) + ), + WithComponentsConstraint( + ('id', ComponentAbsentConstraint()), + ('name', ComponentPresentConstraint()) + ) + ) + + item = Item() + + # This will succeed + item['id'] = 1 + + # This will succeed + item.reset() + item['name'] = 'John' + + # This will fail (on encoding) + item.reset() + descr['id'] = 1 + descr['name'] = 'John' + """ + def _testValue(self, value, idx): + for field, constraint in self._values: + constraint(value.get(field)) + + def _setValues(self, values): + AbstractConstraint._setValues(self, values) + + +# This is a bit kludgy, meaning two op modes within a single constraint +class InnerTypeConstraint(AbstractConstraint): + """Value must satisfy the type and presence constraints""" + + def _testValue(self, value, idx): + if self.__singleTypeConstraint: + self.__singleTypeConstraint(value) + elif self.__multipleTypeConstraint: + if idx not in self.__multipleTypeConstraint: + raise error.ValueConstraintError(value) + constraint, status = self.__multipleTypeConstraint[idx] + if status == 'ABSENT': # XXX presence is not checked! + raise error.ValueConstraintError(value) + constraint(value) + + def _setValues(self, values): + self.__multipleTypeConstraint = {} + self.__singleTypeConstraint = None + for v in values: + if isinstance(v, tuple): + self.__multipleTypeConstraint[v[0]] = v[1], v[2] + else: + self.__singleTypeConstraint = v + AbstractConstraint._setValues(self, values) + + +# Logic operations on constraints + +class ConstraintsExclusion(AbstractConstraint): + """Create a ConstraintsExclusion logic operator object. + + The ConstraintsExclusion logic operator succeeds when the + value does *not* satisfy the operand constraint. + + The ConstraintsExclusion object can be applied to + any constraint and logic operator object. + + Parameters + ---------- + *constraints: + Constraint or logic operator objects. + + Examples + -------- + .. code-block:: python + + class LuckyNumber(Integer): + subtypeSpec = ConstraintsExclusion( + SingleValueConstraint(13) + ) + + # this will succeed + luckyNumber = LuckyNumber(12) + + # this will raise ValueConstraintError + luckyNumber = LuckyNumber(13) + + Note + ---- + The `FROM ... EXCEPT ...` ASN.1 clause should be modeled by combining + constraint objects into one. See `PermittedAlphabetConstraint` for more + information. + """ + def _testValue(self, value, idx): + for constraint in self._values: + try: + constraint(value, idx) + + except error.ValueConstraintError: + continue + + raise error.ValueConstraintError(value) + + def _setValues(self, values): + AbstractConstraint._setValues(self, values) + + +class AbstractConstraintSet(AbstractConstraint): + + def __getitem__(self, idx): + return self._values[idx] + + def __iter__(self): + return iter(self._values) + + def __add__(self, value): + return self.__class__(*(self._values + (value,))) + + def __radd__(self, value): + return self.__class__(*((value,) + self._values)) + + def __len__(self): + return len(self._values) + + # Constraints inclusion in sets + + def _setValues(self, values): + self._values = values + for constraint in values: + if constraint: + self._valueMap.add(constraint) + self._valueMap.update(constraint.getValueMap()) + + +class ConstraintsIntersection(AbstractConstraintSet): + """Create a ConstraintsIntersection logic operator object. + + The ConstraintsIntersection logic operator only succeeds + if *all* its operands succeed. + + The ConstraintsIntersection object can be applied to + any constraint and logic operator objects. + + The ConstraintsIntersection object duck-types the immutable + container object like Python :py:class:`tuple`. + + Parameters + ---------- + *constraints: + Constraint or logic operator objects. + + Examples + -------- + .. code-block:: python + + class CapitalAndSmall(IA5String): + ''' + ASN.1 specification: + + CapitalAndSmall ::= + IA5String (FROM ("A".."Z"|"a".."z")) + ''' + subtypeSpec = ConstraintsIntersection( + PermittedAlphabetConstraint('A', 'Z'), + PermittedAlphabetConstraint('a', 'z') + ) + + # this will succeed + capital_and_small = CapitalAndSmall('Hello') + + # this will raise ValueConstraintError + capital_and_small = CapitalAndSmall('hello') + """ + def _testValue(self, value, idx): + for constraint in self._values: + constraint(value, idx) + + +class ConstraintsUnion(AbstractConstraintSet): + """Create a ConstraintsUnion logic operator object. + + The ConstraintsUnion logic operator succeeds if + *at least* a single operand succeeds. + + The ConstraintsUnion object can be applied to + any constraint and logic operator objects. + + The ConstraintsUnion object duck-types the immutable + container object like Python :py:class:`tuple`. + + Parameters + ---------- + *constraints: + Constraint or logic operator objects. + + Examples + -------- + .. code-block:: python + + class CapitalOrSmall(IA5String): + ''' + ASN.1 specification: + + CapitalOrSmall ::= + IA5String (FROM ("A".."Z") | FROM ("a".."z")) + ''' + subtypeSpec = ConstraintsUnion( + PermittedAlphabetConstraint('A', 'Z'), + PermittedAlphabetConstraint('a', 'z') + ) + + # this will succeed + capital_or_small = CapitalAndSmall('Hello') + + # this will raise ValueConstraintError + capital_or_small = CapitalOrSmall('hello!') + """ + def _testValue(self, value, idx): + for constraint in self._values: + try: + constraint(value, idx) + except error.ValueConstraintError: + pass + else: + return + + raise error.ValueConstraintError( + 'all of %s failed for "%s"' % (self._values, value) + ) + +# TODO: +# refactor InnerTypeConstraint +# add tests for type check +# implement other constraint types +# make constraint validation easy to skip diff --git a/module/scripts/lib/pyasn1/type/error.py b/module/scripts/lib/pyasn1/type/error.py new file mode 100644 index 0000000..0ff082a --- /dev/null +++ b/module/scripts/lib/pyasn1/type/error.py @@ -0,0 +1,11 @@ +# +# This file is part of pyasn1 software. +# +# Copyright (c) 2005-2020, Ilya Etingof +# License: https://pyasn1.readthedocs.io/en/latest/license.html +# +from pyasn1.error import PyAsn1Error + + +class ValueConstraintError(PyAsn1Error): + pass diff --git a/module/scripts/lib/pyasn1/type/namedtype.py b/module/scripts/lib/pyasn1/type/namedtype.py new file mode 100644 index 0000000..5f6c4ca --- /dev/null +++ b/module/scripts/lib/pyasn1/type/namedtype.py @@ -0,0 +1,550 @@ +# +# This file is part of pyasn1 software. +# +# Copyright (c) 2005-2020, Ilya Etingof +# License: https://pyasn1.readthedocs.io/en/latest/license.html +# +import sys + +from pyasn1 import error +from pyasn1.type import tag +from pyasn1.type import tagmap + +__all__ = ['NamedType', 'OptionalNamedType', 'DefaultedNamedType', + 'NamedTypes'] + +class NamedType(object): + """Create named field object for a constructed ASN.1 type. + + The |NamedType| object represents a single name and ASN.1 type of a constructed ASN.1 type. + + |NamedType| objects are immutable and duck-type Python :class:`tuple` objects + holding *name* and *asn1Object* components. + + Parameters + ---------- + name: :py:class:`str` + Field name + + asn1Object: + ASN.1 type object + """ + isOptional = False + isDefaulted = False + + def __init__(self, name, asn1Object, openType=None): + self.__name = name + self.__type = asn1Object + self.__nameAndType = name, asn1Object + self.__openType = openType + + def __repr__(self): + representation = '%s=%r' % (self.name, self.asn1Object) + + if self.openType: + representation += ', open type %r' % self.openType + + return '<%s object, type %s>' % ( + self.__class__.__name__, representation) + + def __eq__(self, other): + return self.__nameAndType == other + + def __ne__(self, other): + return self.__nameAndType != other + + def __lt__(self, other): + return self.__nameAndType < other + + def __le__(self, other): + return self.__nameAndType <= other + + def __gt__(self, other): + return self.__nameAndType > other + + def __ge__(self, other): + return self.__nameAndType >= other + + def __hash__(self): + return hash(self.__nameAndType) + + def __getitem__(self, idx): + return self.__nameAndType[idx] + + def __iter__(self): + return iter(self.__nameAndType) + + @property + def name(self): + return self.__name + + @property + def asn1Object(self): + return self.__type + + @property + def openType(self): + return self.__openType + + # Backward compatibility + + def getName(self): + return self.name + + def getType(self): + return self.asn1Object + + +class OptionalNamedType(NamedType): + __doc__ = NamedType.__doc__ + + isOptional = True + + +class DefaultedNamedType(NamedType): + __doc__ = NamedType.__doc__ + + isDefaulted = True + + +class NamedTypes(object): + """Create a collection of named fields for a constructed ASN.1 type. + + The NamedTypes object represents a collection of named fields of a constructed ASN.1 type. + + *NamedTypes* objects are immutable and duck-type Python :class:`dict` objects + holding *name* as keys and ASN.1 type object as values. + + Parameters + ---------- + *namedTypes: :class:`~pyasn1.type.namedtype.NamedType` + + Examples + -------- + + .. code-block:: python + + class Description(Sequence): + ''' + ASN.1 specification: + + Description ::= SEQUENCE { + surname IA5String, + first-name IA5String OPTIONAL, + age INTEGER DEFAULT 40 + } + ''' + componentType = NamedTypes( + NamedType('surname', IA5String()), + OptionalNamedType('first-name', IA5String()), + DefaultedNamedType('age', Integer(40)) + ) + + descr = Description() + descr['surname'] = 'Smith' + descr['first-name'] = 'John' + """ + def __init__(self, *namedTypes, **kwargs): + self.__namedTypes = namedTypes + self.__namedTypesLen = len(self.__namedTypes) + self.__minTagSet = self.__computeMinTagSet() + self.__nameToPosMap = self.__computeNameToPosMap() + self.__tagToPosMap = self.__computeTagToPosMap() + self.__ambiguousTypes = 'terminal' not in kwargs and self.__computeAmbiguousTypes() or {} + self.__uniqueTagMap = self.__computeTagMaps(unique=True) + self.__nonUniqueTagMap = self.__computeTagMaps(unique=False) + self.__hasOptionalOrDefault = any([True for namedType in self.__namedTypes + if namedType.isDefaulted or namedType.isOptional]) + self.__hasOpenTypes = any([True for namedType in self.__namedTypes + if namedType.openType]) + + self.__requiredComponents = frozenset( + [idx for idx, nt in enumerate(self.__namedTypes) if not nt.isOptional and not nt.isDefaulted] + ) + self.__keys = frozenset([namedType.name for namedType in self.__namedTypes]) + self.__values = tuple([namedType.asn1Object for namedType in self.__namedTypes]) + self.__items = tuple([(namedType.name, namedType.asn1Object) for namedType in self.__namedTypes]) + + def __repr__(self): + representation = ', '.join(['%r' % x for x in self.__namedTypes]) + return '<%s object, types %s>' % ( + self.__class__.__name__, representation) + + def __eq__(self, other): + return self.__namedTypes == other + + def __ne__(self, other): + return self.__namedTypes != other + + def __lt__(self, other): + return self.__namedTypes < other + + def __le__(self, other): + return self.__namedTypes <= other + + def __gt__(self, other): + return self.__namedTypes > other + + def __ge__(self, other): + return self.__namedTypes >= other + + def __hash__(self): + return hash(self.__namedTypes) + + def __getitem__(self, idx): + try: + return self.__namedTypes[idx] + + except TypeError: + return self.__namedTypes[self.__nameToPosMap[idx]] + + def __contains__(self, key): + return key in self.__nameToPosMap + + def __iter__(self): + return (x[0] for x in self.__namedTypes) + + def __bool__(self): + return self.__namedTypesLen > 0 + + def __len__(self): + return self.__namedTypesLen + + # Python dict protocol + + def values(self): + return self.__values + + def keys(self): + return self.__keys + + def items(self): + return self.__items + + def clone(self): + return self.__class__(*self.__namedTypes) + + class PostponedError(object): + def __init__(self, errorMsg): + self.__errorMsg = errorMsg + + def __getitem__(self, item): + raise error.PyAsn1Error(self.__errorMsg) + + def __computeTagToPosMap(self): + tagToPosMap = {} + for idx, namedType in enumerate(self.__namedTypes): + tagMap = namedType.asn1Object.tagMap + if isinstance(tagMap, NamedTypes.PostponedError): + return tagMap + if not tagMap: + continue + for _tagSet in tagMap.presentTypes: + if _tagSet in tagToPosMap: + return NamedTypes.PostponedError('Duplicate component tag %s at %s' % (_tagSet, namedType)) + tagToPosMap[_tagSet] = idx + + return tagToPosMap + + def __computeNameToPosMap(self): + nameToPosMap = {} + for idx, namedType in enumerate(self.__namedTypes): + if namedType.name in nameToPosMap: + return NamedTypes.PostponedError('Duplicate component name %s at %s' % (namedType.name, namedType)) + nameToPosMap[namedType.name] = idx + + return nameToPosMap + + def __computeAmbiguousTypes(self): + ambiguousTypes = {} + partialAmbiguousTypes = () + for idx, namedType in reversed(tuple(enumerate(self.__namedTypes))): + if namedType.isOptional or namedType.isDefaulted: + partialAmbiguousTypes = (namedType,) + partialAmbiguousTypes + else: + partialAmbiguousTypes = (namedType,) + if len(partialAmbiguousTypes) == len(self.__namedTypes): + ambiguousTypes[idx] = self + else: + ambiguousTypes[idx] = NamedTypes(*partialAmbiguousTypes, **dict(terminal=True)) + return ambiguousTypes + + def getTypeByPosition(self, idx): + """Return ASN.1 type object by its position in fields set. + + Parameters + ---------- + idx: :py:class:`int` + Field index + + Returns + ------- + : + ASN.1 type + + Raises + ------ + ~pyasn1.error.PyAsn1Error + If given position is out of fields range + """ + try: + return self.__namedTypes[idx].asn1Object + + except IndexError: + raise error.PyAsn1Error('Type position out of range') + + def getPositionByType(self, tagSet): + """Return field position by its ASN.1 type. + + Parameters + ---------- + tagSet: :class:`~pysnmp.type.tag.TagSet` + ASN.1 tag set distinguishing one ASN.1 type from others. + + Returns + ------- + : :py:class:`int` + ASN.1 type position in fields set + + Raises + ------ + ~pyasn1.error.PyAsn1Error + If *tagSet* is not present or ASN.1 types are not unique within callee *NamedTypes* + """ + try: + return self.__tagToPosMap[tagSet] + + except KeyError: + raise error.PyAsn1Error('Type %s not found' % (tagSet,)) + + def getNameByPosition(self, idx): + """Return field name by its position in fields set. + + Parameters + ---------- + idx: :py:class:`idx` + Field index + + Returns + ------- + : :py:class:`str` + Field name + + Raises + ------ + ~pyasn1.error.PyAsn1Error + If given field name is not present in callee *NamedTypes* + """ + try: + return self.__namedTypes[idx].name + + except IndexError: + raise error.PyAsn1Error('Type position out of range') + + def getPositionByName(self, name): + """Return field position by filed name. + + Parameters + ---------- + name: :py:class:`str` + Field name + + Returns + ------- + : :py:class:`int` + Field position in fields set + + Raises + ------ + ~pyasn1.error.PyAsn1Error + If *name* is not present or not unique within callee *NamedTypes* + """ + try: + return self.__nameToPosMap[name] + + except KeyError: + raise error.PyAsn1Error('Name %s not found' % (name,)) + + def getTagMapNearPosition(self, idx): + """Return ASN.1 types that are allowed at or past given field position. + + Some ASN.1 serialisation allow for skipping optional and defaulted fields. + Some constructed ASN.1 types allow reordering of the fields. When recovering + such objects it may be important to know which types can possibly be + present at any given position in the field sets. + + Parameters + ---------- + idx: :py:class:`int` + Field index + + Returns + ------- + : :class:`~pyasn1.type.tagmap.TagMap` + Map if ASN.1 types allowed at given field position + + Raises + ------ + ~pyasn1.error.PyAsn1Error + If given position is out of fields range + """ + try: + return self.__ambiguousTypes[idx].tagMap + + except KeyError: + raise error.PyAsn1Error('Type position out of range') + + def getPositionNearType(self, tagSet, idx): + """Return the closest field position where given ASN.1 type is allowed. + + Some ASN.1 serialisation allow for skipping optional and defaulted fields. + Some constructed ASN.1 types allow reordering of the fields. When recovering + such objects it may be important to know at which field position, in field set, + given *tagSet* is allowed at or past *idx* position. + + Parameters + ---------- + tagSet: :class:`~pyasn1.type.tag.TagSet` + ASN.1 type which field position to look up + + idx: :py:class:`int` + Field position at or past which to perform ASN.1 type look up + + Returns + ------- + : :py:class:`int` + Field position in fields set + + Raises + ------ + ~pyasn1.error.PyAsn1Error + If *tagSet* is not present or not unique within callee *NamedTypes* + or *idx* is out of fields range + """ + try: + return idx + self.__ambiguousTypes[idx].getPositionByType(tagSet) + + except KeyError: + raise error.PyAsn1Error('Type position out of range') + + def __computeMinTagSet(self): + minTagSet = None + for namedType in self.__namedTypes: + asn1Object = namedType.asn1Object + + try: + tagSet = asn1Object.minTagSet + + except AttributeError: + tagSet = asn1Object.tagSet + + if minTagSet is None or tagSet < minTagSet: + minTagSet = tagSet + + return minTagSet or tag.TagSet() + + @property + def minTagSet(self): + """Return the minimal TagSet among ASN.1 type in callee *NamedTypes*. + + Some ASN.1 types/serialisation protocols require ASN.1 types to be + arranged based on their numerical tag value. The *minTagSet* property + returns that. + + Returns + ------- + : :class:`~pyasn1.type.tagset.TagSet` + Minimal TagSet among ASN.1 types in callee *NamedTypes* + """ + return self.__minTagSet + + def __computeTagMaps(self, unique): + presentTypes = {} + skipTypes = {} + defaultType = None + for namedType in self.__namedTypes: + tagMap = namedType.asn1Object.tagMap + if isinstance(tagMap, NamedTypes.PostponedError): + return tagMap + for tagSet in tagMap: + if unique and tagSet in presentTypes: + return NamedTypes.PostponedError('Non-unique tagSet %s of %s at %s' % (tagSet, namedType, self)) + presentTypes[tagSet] = namedType.asn1Object + skipTypes.update(tagMap.skipTypes) + + if defaultType is None: + defaultType = tagMap.defaultType + elif tagMap.defaultType is not None: + return NamedTypes.PostponedError('Duplicate default ASN.1 type at %s' % (self,)) + + return tagmap.TagMap(presentTypes, skipTypes, defaultType) + + @property + def tagMap(self): + """Return a *TagMap* object from tags and types recursively. + + Return a :class:`~pyasn1.type.tagmap.TagMap` object by + combining tags from *TagMap* objects of children types and + associating them with their immediate child type. + + Example + ------- + .. code-block:: python + + OuterType ::= CHOICE { + innerType INTEGER + } + + Calling *.tagMap* on *OuterType* will yield a map like this: + + .. code-block:: python + + Integer.tagSet -> Choice + """ + return self.__nonUniqueTagMap + + @property + def tagMapUnique(self): + """Return a *TagMap* object from unique tags and types recursively. + + Return a :class:`~pyasn1.type.tagmap.TagMap` object by + combining tags from *TagMap* objects of children types and + associating them with their immediate child type. + + Example + ------- + .. code-block:: python + + OuterType ::= CHOICE { + innerType INTEGER + } + + Calling *.tagMapUnique* on *OuterType* will yield a map like this: + + .. code-block:: python + + Integer.tagSet -> Choice + + Note + ---- + + Duplicate *TagSet* objects found in the tree of children + types would cause error. + """ + return self.__uniqueTagMap + + @property + def hasOptionalOrDefault(self): + return self.__hasOptionalOrDefault + + @property + def hasOpenTypes(self): + return self.__hasOpenTypes + + @property + def namedTypes(self): + return tuple(self.__namedTypes) + + @property + def requiredComponents(self): + return self.__requiredComponents diff --git a/module/scripts/lib/pyasn1/type/namedval.py b/module/scripts/lib/pyasn1/type/namedval.py new file mode 100644 index 0000000..46a6496 --- /dev/null +++ b/module/scripts/lib/pyasn1/type/namedval.py @@ -0,0 +1,192 @@ +# +# This file is part of pyasn1 software. +# +# Copyright (c) 2005-2020, Ilya Etingof +# License: https://pyasn1.readthedocs.io/en/latest/license.html +# +# ASN.1 named integers +# +from pyasn1 import error + +__all__ = ['NamedValues'] + + +class NamedValues(object): + """Create named values object. + + The |NamedValues| object represents a collection of string names + associated with numeric IDs. These objects are used for giving + names to otherwise numerical values. + + |NamedValues| objects are immutable and duck-type Python + :class:`dict` object mapping ID to name and vice-versa. + + Parameters + ---------- + *args: variable number of two-element :py:class:`tuple` + + name: :py:class:`str` + Value label + + value: :py:class:`int` + Numeric value + + Keyword Args + ------------ + name: :py:class:`str` + Value label + + value: :py:class:`int` + Numeric value + + Examples + -------- + + .. code-block:: pycon + + >>> nv = NamedValues('a', 'b', ('c', 0), d=1) + >>> nv + >>> {'c': 0, 'd': 1, 'a': 2, 'b': 3} + >>> nv[0] + 'c' + >>> nv['a'] + 2 + """ + def __init__(self, *args, **kwargs): + self.__names = {} + self.__numbers = {} + + anonymousNames = [] + + for namedValue in args: + if isinstance(namedValue, (tuple, list)): + try: + name, number = namedValue + + except ValueError: + raise error.PyAsn1Error('Not a proper attribute-value pair %r' % (namedValue,)) + + else: + anonymousNames.append(namedValue) + continue + + if name in self.__names: + raise error.PyAsn1Error('Duplicate name %s' % (name,)) + + if number in self.__numbers: + raise error.PyAsn1Error('Duplicate number %s=%s' % (name, number)) + + self.__names[name] = number + self.__numbers[number] = name + + for name, number in kwargs.items(): + if name in self.__names: + raise error.PyAsn1Error('Duplicate name %s' % (name,)) + + if number in self.__numbers: + raise error.PyAsn1Error('Duplicate number %s=%s' % (name, number)) + + self.__names[name] = number + self.__numbers[number] = name + + if anonymousNames: + + number = self.__numbers and max(self.__numbers) + 1 or 0 + + for name in anonymousNames: + + if name in self.__names: + raise error.PyAsn1Error('Duplicate name %s' % (name,)) + + self.__names[name] = number + self.__numbers[number] = name + + number += 1 + + def __repr__(self): + representation = ', '.join(['%s=%d' % x for x in self.items()]) + + if len(representation) > 64: + representation = representation[:32] + '...' + representation[-32:] + + return '<%s object, enums %s>' % ( + self.__class__.__name__, representation) + + def __eq__(self, other): + return dict(self) == other + + def __ne__(self, other): + return dict(self) != other + + def __lt__(self, other): + return dict(self) < other + + def __le__(self, other): + return dict(self) <= other + + def __gt__(self, other): + return dict(self) > other + + def __ge__(self, other): + return dict(self) >= other + + def __hash__(self): + return hash(self.items()) + + # Python dict protocol (read-only) + + def __getitem__(self, key): + try: + return self.__numbers[key] + + except KeyError: + return self.__names[key] + + def __len__(self): + return len(self.__names) + + def __contains__(self, key): + return key in self.__names or key in self.__numbers + + def __iter__(self): + return iter(self.__names) + + def values(self): + return iter(self.__numbers) + + def keys(self): + return iter(self.__names) + + def items(self): + for name in self.__names: + yield name, self.__names[name] + + # support merging + + def __add__(self, namedValues): + return self.__class__(*tuple(self.items()) + tuple(namedValues.items())) + + # XXX clone/subtype? + + def clone(self, *args, **kwargs): + new = self.__class__(*args, **kwargs) + return self + new + + # legacy protocol + + def getName(self, value): + if value in self.__numbers: + return self.__numbers[value] + + def getValue(self, name): + if name in self.__names: + return self.__names[name] + + def getValues(self, *names): + try: + return [self.__names[name] for name in names] + + except KeyError: + raise error.PyAsn1Error( + 'Unknown bit identifier(s): %s' % (set(names).difference(self.__names),) + ) diff --git a/module/scripts/lib/pyasn1/type/opentype.py b/module/scripts/lib/pyasn1/type/opentype.py new file mode 100644 index 0000000..5a15f89 --- /dev/null +++ b/module/scripts/lib/pyasn1/type/opentype.py @@ -0,0 +1,104 @@ +# +# This file is part of pyasn1 software. +# +# Copyright (c) 2005-2020, Ilya Etingof +# License: https://pyasn1.readthedocs.io/en/latest/license.html +# + +__all__ = ['OpenType'] + + +class OpenType(object): + """Create ASN.1 type map indexed by a value + + The *OpenType* object models an untyped field of a constructed ASN.1 + type. In ASN.1 syntax it is usually represented by the + `ANY DEFINED BY` for scalars or `SET OF ANY DEFINED BY`, + `SEQUENCE OF ANY DEFINED BY` for container types clauses. Typically + used together with :class:`~pyasn1.type.univ.Any` object. + + OpenType objects duck-type a read-only Python :class:`dict` objects, + however the passed `typeMap` is not copied, but stored by reference. + That means the user can manipulate `typeMap` at run time having this + reflected on *OpenType* object behavior. + + The |OpenType| class models an untyped field of a constructed ASN.1 + type. In ASN.1 syntax it is usually represented by the + `ANY DEFINED BY` for scalars or `SET OF ANY DEFINED BY`, + `SEQUENCE OF ANY DEFINED BY` for container types clauses. Typically + used with :class:`~pyasn1.type.univ.Any` type. + + Parameters + ---------- + name: :py:class:`str` + Field name + + typeMap: :py:class:`dict` + A map of value->ASN.1 type. It's stored by reference and can be + mutated later to register new mappings. + + Examples + -------- + + For untyped scalars: + + .. code-block:: python + + openType = OpenType( + 'id', {1: Integer(), + 2: OctetString()} + ) + Sequence( + componentType=NamedTypes( + NamedType('id', Integer()), + NamedType('blob', Any(), openType=openType) + ) + ) + + For untyped `SET OF` or `SEQUENCE OF` vectors: + + .. code-block:: python + + openType = OpenType( + 'id', {1: Integer(), + 2: OctetString()} + ) + Sequence( + componentType=NamedTypes( + NamedType('id', Integer()), + NamedType('blob', SetOf(componentType=Any()), + openType=openType) + ) + ) + """ + + def __init__(self, name, typeMap=None): + self.__name = name + if typeMap is None: + self.__typeMap = {} + else: + self.__typeMap = typeMap + + @property + def name(self): + return self.__name + + # Python dict protocol + + def values(self): + return self.__typeMap.values() + + def keys(self): + return self.__typeMap.keys() + + def items(self): + return self.__typeMap.items() + + def __contains__(self, key): + return key in self.__typeMap + + def __getitem__(self, key): + return self.__typeMap[key] + + def __iter__(self): + return iter(self.__typeMap) diff --git a/module/scripts/lib/pyasn1/type/tag.py b/module/scripts/lib/pyasn1/type/tag.py new file mode 100644 index 0000000..28cd3fd --- /dev/null +++ b/module/scripts/lib/pyasn1/type/tag.py @@ -0,0 +1,347 @@ +# +# This file is part of pyasn1 software. +# +# Copyright (c) 2005-2020, Ilya Etingof +# License: https://pyasn1.readthedocs.io/en/latest/license.html +# +from pyasn1 import error + +__all__ = ['tagClassUniversal', 'tagClassApplication', 'tagClassContext', + 'tagClassPrivate', 'tagFormatSimple', 'tagFormatConstructed', + 'tagCategoryImplicit', 'tagCategoryExplicit', + 'tagCategoryUntagged', 'Tag', 'TagSet'] + +#: Identifier for ASN.1 class UNIVERSAL +tagClassUniversal = 0x00 + +#: Identifier for ASN.1 class APPLICATION +tagClassApplication = 0x40 + +#: Identifier for ASN.1 class context-specific +tagClassContext = 0x80 + +#: Identifier for ASN.1 class private +tagClassPrivate = 0xC0 + +#: Identifier for "simple" ASN.1 structure (e.g. scalar) +tagFormatSimple = 0x00 + +#: Identifier for "constructed" ASN.1 structure (e.g. may have inner components) +tagFormatConstructed = 0x20 + +tagCategoryImplicit = 0x01 +tagCategoryExplicit = 0x02 +tagCategoryUntagged = 0x04 + + +def _tagIdToStr(tagId): + # Decimal rendering of a huge tag ID can exceed the interpreter's + # integer-to-string conversion limit (sys.get_int_max_str_digits(), + # Python 3.11+) and raise ValueError; hexadecimal is not limited + try: + return str(tagId) + except ValueError: + return hex(tagId) + + +class Tag(object): + """Create ASN.1 tag + + Represents ASN.1 tag that can be attached to a ASN.1 type to make + types distinguishable from each other. + + *Tag* objects are immutable and duck-type Python :class:`tuple` objects + holding three integer components of a tag. + + Parameters + ---------- + tagClass: :py:class:`int` + Tag *class* value + + tagFormat: :py:class:`int` + Tag *format* value + + tagId: :py:class:`int` + Tag ID value + """ + def __init__(self, tagClass, tagFormat, tagId): + if tagId < 0: + raise error.PyAsn1Error( + 'Negative tag ID (%s) not allowed' % _tagIdToStr(tagId)) + self.__tagClass = tagClass + self.__tagFormat = tagFormat + self.__tagId = tagId + self.__tagClassId = tagClass, tagId + self.__hash = hash(self.__tagClassId) + + def __repr__(self): + representation = '[%s:%s:%s]' % ( + self.__tagClass, self.__tagFormat, _tagIdToStr(self.__tagId)) + return '<%s object, tag %s>' % ( + self.__class__.__name__, representation) + + def __eq__(self, other): + return self.__tagClassId == other + + def __ne__(self, other): + return self.__tagClassId != other + + def __lt__(self, other): + return self.__tagClassId < other + + def __le__(self, other): + return self.__tagClassId <= other + + def __gt__(self, other): + return self.__tagClassId > other + + def __ge__(self, other): + return self.__tagClassId >= other + + def __hash__(self): + return self.__hash + + def __getitem__(self, idx): + if idx == 0: + return self.__tagClass + elif idx == 1: + return self.__tagFormat + elif idx == 2: + return self.__tagId + else: + raise IndexError + + def __iter__(self): + yield self.__tagClass + yield self.__tagFormat + yield self.__tagId + + def __and__(self, otherTag): + return self.__class__(self.__tagClass & otherTag.tagClass, + self.__tagFormat & otherTag.tagFormat, + self.__tagId & otherTag.tagId) + + def __or__(self, otherTag): + return self.__class__(self.__tagClass | otherTag.tagClass, + self.__tagFormat | otherTag.tagFormat, + self.__tagId | otherTag.tagId) + + @property + def tagClass(self): + """ASN.1 tag class + + Returns + ------- + : :py:class:`int` + Tag class + """ + return self.__tagClass + + @property + def tagFormat(self): + """ASN.1 tag format + + Returns + ------- + : :py:class:`int` + Tag format + """ + return self.__tagFormat + + @property + def tagId(self): + """ASN.1 tag ID + + Returns + ------- + : :py:class:`int` + Tag ID + """ + return self.__tagId + + +class TagSet(object): + """Create a collection of ASN.1 tags + + Represents a combination of :class:`~pyasn1.type.tag.Tag` objects + that can be attached to a ASN.1 type to make types distinguishable + from each other. + + *TagSet* objects are immutable and duck-type Python :class:`tuple` objects + holding arbitrary number of :class:`~pyasn1.type.tag.Tag` objects. + + Parameters + ---------- + baseTag: :class:`~pyasn1.type.tag.Tag` + Base *Tag* object. This tag survives IMPLICIT tagging. + + *superTags: :class:`~pyasn1.type.tag.Tag` + Additional *Tag* objects taking part in subtyping. + + Examples + -------- + .. code-block:: python + + class OrderNumber(NumericString): + ''' + ASN.1 specification + + Order-number ::= + [APPLICATION 5] IMPLICIT NumericString + ''' + tagSet = NumericString.tagSet.tagImplicitly( + Tag(tagClassApplication, tagFormatSimple, 5) + ) + + orderNumber = OrderNumber('1234') + """ + def __init__(self, baseTag=(), *superTags): + self.__baseTag = baseTag + self.__superTags = superTags + self.__superTagsClassId = tuple( + [(superTag.tagClass, superTag.tagId) for superTag in superTags] + ) + self.__lenOfSuperTags = len(superTags) + self.__hash = hash(self.__superTagsClassId) + + def __repr__(self): + representation = '-'.join( + ['%s:%s:%s' % (x.tagClass, x.tagFormat, _tagIdToStr(x.tagId)) + for x in self.__superTags]) + if representation: + representation = 'tags ' + representation + else: + representation = 'untagged' + + return '<%s object, %s>' % (self.__class__.__name__, representation) + + def __add__(self, superTag): + return self.__class__(self.__baseTag, *self.__superTags + (superTag,)) + + def __radd__(self, superTag): + return self.__class__(self.__baseTag, *(superTag,) + self.__superTags) + + def __getitem__(self, i): + if i.__class__ is slice: + return self.__class__(self.__baseTag, *self.__superTags[i]) + else: + return self.__superTags[i] + + def __eq__(self, other): + return self.__superTagsClassId == other + + def __ne__(self, other): + return self.__superTagsClassId != other + + def __lt__(self, other): + return self.__superTagsClassId < other + + def __le__(self, other): + return self.__superTagsClassId <= other + + def __gt__(self, other): + return self.__superTagsClassId > other + + def __ge__(self, other): + return self.__superTagsClassId >= other + + def __hash__(self): + return self.__hash + + def __len__(self): + return self.__lenOfSuperTags + + @property + def baseTag(self): + """Return base ASN.1 tag + + Returns + ------- + : :class:`~pyasn1.type.tag.Tag` + Base tag of this *TagSet* + """ + return self.__baseTag + + @property + def superTags(self): + """Return ASN.1 tags + + Returns + ------- + : :py:class:`tuple` + Tuple of :class:`~pyasn1.type.tag.Tag` objects that this *TagSet* contains + """ + return self.__superTags + + def tagExplicitly(self, superTag): + """Return explicitly tagged *TagSet* + + Create a new *TagSet* representing callee *TagSet* explicitly tagged + with passed tag(s). With explicit tagging mode, new tags are appended + to existing tag(s). + + Parameters + ---------- + superTag: :class:`~pyasn1.type.tag.Tag` + *Tag* object to tag this *TagSet* + + Returns + ------- + : :class:`~pyasn1.type.tag.TagSet` + New *TagSet* object + """ + if superTag.tagClass == tagClassUniversal: + raise error.PyAsn1Error("Can't tag with UNIVERSAL class tag") + if superTag.tagFormat != tagFormatConstructed: + superTag = Tag(superTag.tagClass, tagFormatConstructed, superTag.tagId) + return self + superTag + + def tagImplicitly(self, superTag): + """Return implicitly tagged *TagSet* + + Create a new *TagSet* representing callee *TagSet* implicitly tagged + with passed tag(s). With implicit tagging mode, new tag(s) replace the + last existing tag. + + Parameters + ---------- + superTag: :class:`~pyasn1.type.tag.Tag` + *Tag* object to tag this *TagSet* + + Returns + ------- + : :class:`~pyasn1.type.tag.TagSet` + New *TagSet* object + """ + if self.__superTags: + superTag = Tag(superTag.tagClass, self.__superTags[-1].tagFormat, superTag.tagId) + return self[:-1] + superTag + + def isSuperTagSetOf(self, tagSet): + """Test type relationship against given *TagSet* + + The callee is considered to be a supertype of given *TagSet* + tag-wise if all tags in *TagSet* are present in the callee and + they are in the same order. + + Parameters + ---------- + tagSet: :class:`~pyasn1.type.tag.TagSet` + *TagSet* object to evaluate against the callee + + Returns + ------- + : :py:class:`bool` + :obj:`True` if callee is a supertype of *tagSet* + """ + if len(tagSet) < self.__lenOfSuperTags: + return False + return self.__superTags == tagSet[:self.__lenOfSuperTags] + + # Backward compatibility + + def getBaseTag(self): + return self.__baseTag + +def initTagSet(tag): + return TagSet(tag, tag) diff --git a/module/scripts/lib/pyasn1/type/tagmap.py b/module/scripts/lib/pyasn1/type/tagmap.py new file mode 100644 index 0000000..7f8a955 --- /dev/null +++ b/module/scripts/lib/pyasn1/type/tagmap.py @@ -0,0 +1,96 @@ +# +# This file is part of pyasn1 software. +# +# Copyright (c) 2005-2020, Ilya Etingof +# License: https://pyasn1.readthedocs.io/en/latest/license.html +# +from pyasn1 import error + +__all__ = ['TagMap'] + + +class TagMap(object): + """Map *TagSet* objects to ASN.1 types + + Create an object mapping *TagSet* object to ASN.1 type. + + *TagMap* objects are immutable and duck-type read-only Python + :class:`dict` objects holding *TagSet* objects as keys and ASN.1 + type objects as values. + + Parameters + ---------- + presentTypes: :py:class:`dict` + Map of :class:`~pyasn1.type.tag.TagSet` to ASN.1 objects considered + as being unconditionally present in the *TagMap*. + + skipTypes: :py:class:`dict` + A collection of :class:`~pyasn1.type.tag.TagSet` objects considered + as absent in the *TagMap* even when *defaultType* is present. + + defaultType: ASN.1 type object + An ASN.1 type object callee *TagMap* returns for any *TagSet* key not present + in *presentTypes* (unless given key is present in *skipTypes*). + """ + def __init__(self, presentTypes=None, skipTypes=None, defaultType=None): + self.__presentTypes = presentTypes or {} + self.__skipTypes = skipTypes or {} + self.__defaultType = defaultType + + def __contains__(self, tagSet): + return (tagSet in self.__presentTypes or + self.__defaultType is not None and tagSet not in self.__skipTypes) + + def __getitem__(self, tagSet): + try: + return self.__presentTypes[tagSet] + except KeyError: + if self.__defaultType is None: + raise + elif tagSet in self.__skipTypes: + raise error.PyAsn1Error('Key in negative map') + else: + return self.__defaultType + + def __iter__(self): + return iter(self.__presentTypes) + + def __repr__(self): + representation = '%s object' % self.__class__.__name__ + + if self.__presentTypes: + representation += ', present %s' % repr(self.__presentTypes) + + if self.__skipTypes: + representation += ', skip %s' % repr(self.__skipTypes) + + if self.__defaultType is not None: + representation += ', default %s' % repr(self.__defaultType) + + return '<%s>' % representation + + @property + def presentTypes(self): + """Return *TagSet* to ASN.1 type map present in callee *TagMap*""" + return self.__presentTypes + + @property + def skipTypes(self): + """Return *TagSet* collection unconditionally absent in callee *TagMap*""" + return self.__skipTypes + + @property + def defaultType(self): + """Return default ASN.1 type being returned for any missing *TagSet*""" + return self.__defaultType + + # Backward compatibility + + def getPosMap(self): + return self.presentTypes + + def getNegMap(self): + return self.skipTypes + + def getDef(self): + return self.defaultType diff --git a/module/scripts/lib/pyasn1/type/univ.py b/module/scripts/lib/pyasn1/type/univ.py new file mode 100644 index 0000000..8b786dd --- /dev/null +++ b/module/scripts/lib/pyasn1/type/univ.py @@ -0,0 +1,3338 @@ +# +# This file is part of pyasn1 software. +# +# Copyright (c) 2005-2020, Ilya Etingof +# License: https://pyasn1.readthedocs.io/en/latest/license.html +# +import math +import sys + +from pyasn1 import error +from pyasn1.codec.ber import eoo +from pyasn1.compat import integer +from pyasn1.type import base +from pyasn1.type import constraint +from pyasn1.type import namedtype +from pyasn1.type import namedval +from pyasn1.type import tag +from pyasn1.type import tagmap + +NoValue = base.NoValue +noValue = NoValue() + +__all__ = ['Integer', 'Boolean', 'BitString', 'OctetString', 'Null', + 'ObjectIdentifier', 'Real', 'Enumerated', + 'SequenceOfAndSetOfBase', 'SequenceOf', 'SetOf', + 'SequenceAndSetBase', 'Sequence', 'Set', 'Choice', 'Any', + 'NoValue', 'noValue'] + +# "Simple" ASN.1 types (yet incomplete) + + +class Integer(base.SimpleAsn1Type): + """Create |ASN.1| schema or value object. + + |ASN.1| class is based on :class:`~pyasn1.type.base.SimpleAsn1Type`, its + objects are immutable and duck-type Python :class:`int` objects. + + Keyword Args + ------------ + value: :class:`int`, :class:`str` or |ASN.1| object + Python :class:`int` or :class:`str` literal or |ASN.1| class + instance. If `value` is not given, schema object will be created. + + tagSet: :py:class:`~pyasn1.type.tag.TagSet` + Object representing non-default ASN.1 tag(s) + + subtypeSpec: :py:class:`~pyasn1.type.constraint.ConstraintsIntersection` + Object representing non-default ASN.1 subtype constraint(s). Constraints + verification for |ASN.1| type occurs automatically on object + instantiation. + + namedValues: :py:class:`~pyasn1.type.namedval.NamedValues` + Object representing non-default symbolic aliases for numbers + + Raises + ------ + ~pyasn1.error.ValueConstraintError, ~pyasn1.error.PyAsn1Error + On constraint violation or bad initializer. + + Examples + -------- + + .. code-block:: python + + class ErrorCode(Integer): + ''' + ASN.1 specification: + + ErrorCode ::= + INTEGER { disk-full(1), no-disk(-1), + disk-not-formatted(2) } + + error ErrorCode ::= disk-full + ''' + namedValues = NamedValues( + ('disk-full', 1), ('no-disk', -1), + ('disk-not-formatted', 2) + ) + + error = ErrorCode('disk-full') + """ + #: Set (on class, not on instance) or return a + #: :py:class:`~pyasn1.type.tag.TagSet` object representing ASN.1 tag(s) + #: associated with |ASN.1| type. + tagSet = tag.initTagSet( + tag.Tag(tag.tagClassUniversal, tag.tagFormatSimple, 0x02) + ) + + #: Set (on class, not on instance) or return a + #: :py:class:`~pyasn1.type.constraint.ConstraintsIntersection` object + #: imposing constraints on |ASN.1| type initialization values. + subtypeSpec = constraint.ConstraintsIntersection() + + #: Default :py:class:`~pyasn1.type.namedval.NamedValues` object + #: representing symbolic aliases for numbers + namedValues = namedval.NamedValues() + + # Optimization for faster codec lookup + typeId = base.SimpleAsn1Type.getTypeId() + + def __init__(self, value=noValue, **kwargs): + if 'namedValues' not in kwargs: + kwargs['namedValues'] = self.namedValues + + base.SimpleAsn1Type.__init__(self, value, **kwargs) + + def __and__(self, value): + return self.clone(self._value & value) + + def __rand__(self, value): + return self.clone(value & self._value) + + def __or__(self, value): + return self.clone(self._value | value) + + def __ror__(self, value): + return self.clone(value | self._value) + + def __xor__(self, value): + return self.clone(self._value ^ value) + + def __rxor__(self, value): + return self.clone(value ^ self._value) + + def __lshift__(self, value): + return self.clone(self._value << value) + + def __rshift__(self, value): + return self.clone(self._value >> value) + + def __add__(self, value): + return self.clone(self._value + value) + + def __radd__(self, value): + return self.clone(value + self._value) + + def __sub__(self, value): + return self.clone(self._value - value) + + def __rsub__(self, value): + return self.clone(value - self._value) + + def __mul__(self, value): + return self.clone(self._value * value) + + def __rmul__(self, value): + return self.clone(value * self._value) + + def __mod__(self, value): + return self.clone(self._value % value) + + def __rmod__(self, value): + return self.clone(value % self._value) + + def __pow__(self, value, modulo=None): + return self.clone(pow(self._value, value, modulo)) + + def __rpow__(self, value): + return self.clone(pow(value, self._value)) + + def __floordiv__(self, value): + return self.clone(self._value // value) + + def __rfloordiv__(self, value): + return self.clone(value // self._value) + + def __truediv__(self, value): + return Real(self._value / value) + + def __rtruediv__(self, value): + return Real(value / self._value) + + def __divmod__(self, value): + return self.clone(divmod(self._value, value)) + + def __rdivmod__(self, value): + return self.clone(divmod(value, self._value)) + + __hash__ = base.SimpleAsn1Type.__hash__ + + def __int__(self): + return int(self._value) + + def __float__(self): + return float(self._value) + + def __abs__(self): + return self.clone(abs(self._value)) + + def __index__(self): + return int(self._value) + + def __pos__(self): + return self.clone(+self._value) + + def __neg__(self): + return self.clone(-self._value) + + def __invert__(self): + return self.clone(~self._value) + + def __round__(self, n=0): + r = round(self._value, n) + if n: + return self.clone(r) + else: + return r + + def __floor__(self): + return math.floor(self._value) + + def __ceil__(self): + return math.ceil(self._value) + + def __trunc__(self): + return self.clone(math.trunc(self._value)) + + def __lt__(self, value): + return self._value < value + + def __le__(self, value): + return self._value <= value + + def __eq__(self, value): + return self._value == value + + def __ne__(self, value): + return self._value != value + + def __gt__(self, value): + return self._value > value + + def __ge__(self, value): + return self._value >= value + + def prettyIn(self, value): + try: + return int(value) + + except ValueError: + try: + return self.namedValues[value] + + except KeyError as exc: + raise error.PyAsn1Error( + 'Can\'t coerce %r into integer: %s' % (value, exc) + ) + + def prettyOut(self, value): + try: + return str(self.namedValues[value]) + + except KeyError: + return str(value) + + # backward compatibility + + def getNamedValues(self): + return self.namedValues + + +class Boolean(Integer): + """Create |ASN.1| schema or value object. + + |ASN.1| class is based on :class:`~pyasn1.type.base.SimpleAsn1Type`, its + objects are immutable and duck-type Python :class:`int` objects. + + Keyword Args + ------------ + value: :class:`int`, :class:`str` or |ASN.1| object + Python :class:`int` or :class:`str` literal or |ASN.1| class + instance. If `value` is not given, schema object will be created. + + tagSet: :py:class:`~pyasn1.type.tag.TagSet` + Object representing non-default ASN.1 tag(s) + + subtypeSpec: :py:class:`~pyasn1.type.constraint.ConstraintsIntersection` + Object representing non-default ASN.1 subtype constraint(s).Constraints + verification for |ASN.1| type occurs automatically on object + instantiation. + + namedValues: :py:class:`~pyasn1.type.namedval.NamedValues` + Object representing non-default symbolic aliases for numbers + + Raises + ------ + ~pyasn1.error.ValueConstraintError, ~pyasn1.error.PyAsn1Error + On constraint violation or bad initializer. + + Examples + -------- + .. code-block:: python + + class RoundResult(Boolean): + ''' + ASN.1 specification: + + RoundResult ::= BOOLEAN + + ok RoundResult ::= TRUE + ko RoundResult ::= FALSE + ''' + ok = RoundResult(True) + ko = RoundResult(False) + """ + #: Set (on class, not on instance) or return a + #: :py:class:`~pyasn1.type.tag.TagSet` object representing ASN.1 tag(s) + #: associated with |ASN.1| type. + tagSet = tag.initTagSet( + tag.Tag(tag.tagClassUniversal, tag.tagFormatSimple, 0x01), + ) + + #: Set (on class, not on instance) or return a + #: :py:class:`~pyasn1.type.constraint.ConstraintsIntersection` object + #: imposing constraints on |ASN.1| type initialization values. + subtypeSpec = Integer.subtypeSpec + constraint.SingleValueConstraint(0, 1) + + #: Default :py:class:`~pyasn1.type.namedval.NamedValues` object + #: representing symbolic aliases for numbers + namedValues = namedval.NamedValues(('False', 0), ('True', 1)) + + # Optimization for faster codec lookup + typeId = Integer.getTypeId() + + +class SizedInteger(int): + bitLength = leadingZeroBits = None + + def setBitLength(self, bitLength): + self.bitLength = bitLength + self.leadingZeroBits = max(bitLength - self.bit_length(), 0) + return self + + def __len__(self): + if self.bitLength is None: + self.setBitLength(self.bit_length()) + + return self.bitLength + + +class BitString(base.SimpleAsn1Type): + """Create |ASN.1| schema or value object. + + |ASN.1| class is based on :class:`~pyasn1.type.base.SimpleAsn1Type`, its + objects are immutable and duck-type both Python :class:`tuple` (as a tuple + of bits) and :class:`int` objects. + + Keyword Args + ------------ + value: :class:`int`, :class:`str` or |ASN.1| object + Python :class:`int` or :class:`str` literal representing binary + or hexadecimal number or sequence of integer bits or |ASN.1| object. + If `value` is not given, schema object will be created. + + tagSet: :py:class:`~pyasn1.type.tag.TagSet` + Object representing non-default ASN.1 tag(s) + + subtypeSpec: :py:class:`~pyasn1.type.constraint.ConstraintsIntersection` + Object representing non-default ASN.1 subtype constraint(s). Constraints + verification for |ASN.1| type occurs automatically on object + instantiation. + + namedValues: :py:class:`~pyasn1.type.namedval.NamedValues` + Object representing non-default symbolic aliases for numbers + + binValue: :py:class:`str` + Binary string initializer to use instead of the *value*. + Example: '10110011'. + + hexValue: :py:class:`str` + Hexadecimal string initializer to use instead of the *value*. + Example: 'DEADBEEF'. + + Raises + ------ + ~pyasn1.error.ValueConstraintError, ~pyasn1.error.PyAsn1Error + On constraint violation or bad initializer. + + Examples + -------- + .. code-block:: python + + class Rights(BitString): + ''' + ASN.1 specification: + + Rights ::= BIT STRING { user-read(0), user-write(1), + group-read(2), group-write(3), + other-read(4), other-write(5) } + + group1 Rights ::= { group-read, group-write } + group2 Rights ::= '0011'B + group3 Rights ::= '3'H + ''' + namedValues = NamedValues( + ('user-read', 0), ('user-write', 1), + ('group-read', 2), ('group-write', 3), + ('other-read', 4), ('other-write', 5) + ) + + group1 = Rights(('group-read', 'group-write')) + group2 = Rights('0011') + group3 = Rights(0x3) + """ + #: Set (on class, not on instance) or return a + #: :py:class:`~pyasn1.type.tag.TagSet` object representing ASN.1 tag(s) + #: associated with |ASN.1| type. + tagSet = tag.initTagSet( + tag.Tag(tag.tagClassUniversal, tag.tagFormatSimple, 0x03) + ) + + #: Set (on class, not on instance) or return a + #: :py:class:`~pyasn1.type.constraint.ConstraintsIntersection` object + #: imposing constraints on |ASN.1| type initialization values. + subtypeSpec = constraint.ConstraintsIntersection() + + #: Default :py:class:`~pyasn1.type.namedval.NamedValues` object + #: representing symbolic aliases for numbers + namedValues = namedval.NamedValues() + + # Optimization for faster codec lookup + typeId = base.SimpleAsn1Type.getTypeId() + + defaultBinValue = defaultHexValue = noValue + + def __init__(self, value=noValue, **kwargs): + if value is noValue: + if kwargs: + try: + value = self.fromBinaryString(kwargs.pop('binValue'), internalFormat=True) + + except KeyError: + pass + + try: + value = self.fromHexString(kwargs.pop('hexValue'), internalFormat=True) + + except KeyError: + pass + + if value is noValue: + if self.defaultBinValue is not noValue: + value = self.fromBinaryString(self.defaultBinValue, internalFormat=True) + + elif self.defaultHexValue is not noValue: + value = self.fromHexString(self.defaultHexValue, internalFormat=True) + + if 'namedValues' not in kwargs: + kwargs['namedValues'] = self.namedValues + + base.SimpleAsn1Type.__init__(self, value, **kwargs) + + def __str__(self): + return self.asBinary() + + def __eq__(self, other): + other = self.prettyIn(other) + return self is other or self._value == other and len(self._value) == len(other) + + def __ne__(self, other): + other = self.prettyIn(other) + return self._value != other or len(self._value) != len(other) + + def __lt__(self, other): + other = self.prettyIn(other) + return len(self._value) < len(other) or len(self._value) == len(other) and self._value < other + + def __le__(self, other): + other = self.prettyIn(other) + return len(self._value) <= len(other) or len(self._value) == len(other) and self._value <= other + + def __gt__(self, other): + other = self.prettyIn(other) + return len(self._value) > len(other) or len(self._value) == len(other) and self._value > other + + def __ge__(self, other): + other = self.prettyIn(other) + return len(self._value) >= len(other) or len(self._value) == len(other) and self._value >= other + + # Immutable sequence object protocol + + def __len__(self): + return len(self._value) + + def __getitem__(self, i): + if i.__class__ is slice: + return self.clone([self[x] for x in range(*i.indices(len(self)))]) + else: + length = len(self._value) - 1 + if i > length or i < 0: + raise IndexError('bit index out of range') + return (self._value >> (length - i)) & 1 + + def __iter__(self): + length = len(self._value) + while length: + length -= 1 + yield (self._value >> length) & 1 + + def __reversed__(self): + return reversed(tuple(self)) + + # arithmetic operators + + def __add__(self, value): + value = self.prettyIn(value) + return self.clone(SizedInteger(self._value << len(value) | value).setBitLength(len(self._value) + len(value))) + + def __radd__(self, value): + value = self.prettyIn(value) + return self.clone(SizedInteger(value << len(self._value) | self._value).setBitLength(len(self._value) + len(value))) + + def __mul__(self, value): + bitString = self._value + while value > 1: + bitString <<= len(self._value) + bitString |= self._value + value -= 1 + return self.clone(bitString) + + def __rmul__(self, value): + return self * value + + def __lshift__(self, count): + return self.clone(SizedInteger(self._value << count).setBitLength(len(self._value) + count)) + + def __rshift__(self, count): + return self.clone(SizedInteger(self._value >> count).setBitLength(max(0, len(self._value) - count))) + + def __int__(self): + return int(self._value) + + def __float__(self): + return float(self._value) + + def asNumbers(self): + """Get |ASN.1| value as a sequence of 8-bit integers. + + If |ASN.1| object length is not a multiple of 8, result + will be left-padded with zeros. + """ + return tuple(self.asOctets()) + + def asOctets(self): + """Get |ASN.1| value as a sequence of octets. + + If |ASN.1| object length is not a multiple of 8, result + will be left-padded with zeros. + """ + return integer.to_bytes(self._value, length=len(self)) + + def asInteger(self): + """Get |ASN.1| value as a single integer value. + """ + return self._value + + def asBinary(self): + """Get |ASN.1| value as a text string of bits. + """ + binString = bin(self._value)[2:] + return '0' * (len(self._value) - len(binString)) + binString + + @classmethod + def fromHexString(cls, value, internalFormat=False, prepend=None): + """Create a |ASN.1| object initialized from the hex string. + + Parameters + ---------- + value: :class:`str` + Text string like 'DEADBEEF' + """ + try: + value = SizedInteger(value, 16).setBitLength(len(value) * 4) + + except ValueError as exc: + raise error.PyAsn1Error('%s.fromHexString() error: %s' % (cls.__name__, exc)) + + if prepend is not None: + value = SizedInteger( + (SizedInteger(prepend) << len(value)) | value + ).setBitLength(len(prepend) + len(value)) + + if not internalFormat: + value = cls(value) + + return value + + @classmethod + def fromBinaryString(cls, value, internalFormat=False, prepend=None): + """Create a |ASN.1| object initialized from a string of '0' and '1'. + + Parameters + ---------- + value: :class:`str` + Text string like '1010111' + """ + try: + value = SizedInteger(value or '0', 2).setBitLength(len(value)) + + except ValueError as exc: + raise error.PyAsn1Error('%s.fromBinaryString() error: %s' % (cls.__name__, exc)) + + if prepend is not None: + value = SizedInteger( + (SizedInteger(prepend) << len(value)) | value + ).setBitLength(len(prepend) + len(value)) + + if not internalFormat: + value = cls(value) + + return value + + @classmethod + def fromOctetString(cls, value, internalFormat=False, prepend=None, padding=0): + """Create a |ASN.1| object initialized from a string. + + Parameters + ---------- + value: :class:`bytes` + Text string like b'\\\\x01\\\\xff' + """ + value = SizedInteger(int.from_bytes(bytes(value), 'big') >> padding).setBitLength(len(value) * 8 - padding) + + if prepend is not None: + value = SizedInteger( + (SizedInteger(prepend) << len(value)) | value + ).setBitLength(len(prepend) + len(value)) + + if not internalFormat: + value = cls(value) + + return value + + def prettyIn(self, value): + if isinstance(value, SizedInteger): + return value + elif isinstance(value, str): + if not value: + return SizedInteger(0).setBitLength(0) + + elif value[0] == '\'': # "'1011'B" -- ASN.1 schema representation (deprecated) + if value[-2:] == '\'B': + return self.fromBinaryString(value[1:-2], internalFormat=True) + elif value[-2:] == '\'H': + return self.fromHexString(value[1:-2], internalFormat=True) + else: + raise error.PyAsn1Error( + 'Bad BIT STRING value notation %s' % (value,) + ) + + elif self.namedValues and not value.isdigit(): # named bits like 'Urgent, Active' + names = [x.strip() for x in value.split(',')] + + try: + + bitPositions = [self.namedValues[name] for name in names] + + except KeyError: + raise error.PyAsn1Error('unknown bit name(s) in %r' % (names,)) + + rightmostPosition = max(bitPositions) + + number = 0 + for bitPosition in bitPositions: + number |= 1 << (rightmostPosition - bitPosition) + + return SizedInteger(number).setBitLength(rightmostPosition + 1) + + elif value.startswith('0x'): + return self.fromHexString(value[2:], internalFormat=True) + + elif value.startswith('0b'): + return self.fromBinaryString(value[2:], internalFormat=True) + + else: # assume plain binary string like '1011' + return self.fromBinaryString(value, internalFormat=True) + + elif isinstance(value, (tuple, list)): + return self.fromBinaryString(''.join([b and '1' or '0' for b in value]), internalFormat=True) + + elif isinstance(value, BitString): + return SizedInteger(value).setBitLength(len(value)) + + elif isinstance(value, int): + return SizedInteger(value) + + else: + raise error.PyAsn1Error( + 'Bad BitString initializer type \'%s\'' % (value,) + ) + + +class OctetString(base.SimpleAsn1Type): + """Create |ASN.1| schema or value object. + + |ASN.1| class is based on :class:`~pyasn1.type.base.SimpleAsn1Type`, its + objects are immutable and duck-type :class:`bytes`. + When used in Unicode context, |ASN.1| type + assumes "|encoding|" serialisation. + + Keyword Args + ------------ + value: :class:`unicode`, :class:`str`, :class:`bytes` or |ASN.1| object + :class:`bytes`, alternatively :class:`str` + representing character string to be serialised into octets + (note `encoding` parameter) or |ASN.1| object. + If `value` is not given, schema object will be created. + + tagSet: :py:class:`~pyasn1.type.tag.TagSet` + Object representing non-default ASN.1 tag(s) + + subtypeSpec: :py:class:`~pyasn1.type.constraint.ConstraintsIntersection` + Object representing non-default ASN.1 subtype constraint(s). Constraints + verification for |ASN.1| type occurs automatically on object + instantiation. + + encoding: :py:class:`str` + Unicode codec ID to encode/decode + :class:`str` the payload when |ASN.1| object is used + in text string context. + + binValue: :py:class:`str` + Binary string initializer to use instead of the *value*. + Example: '10110011'. + + hexValue: :py:class:`str` + Hexadecimal string initializer to use instead of the *value*. + Example: 'DEADBEEF'. + + Raises + ------ + ~pyasn1.error.ValueConstraintError, ~pyasn1.error.PyAsn1Error + On constraint violation or bad initializer. + + Examples + -------- + .. code-block:: python + + class Icon(OctetString): + ''' + ASN.1 specification: + + Icon ::= OCTET STRING + + icon1 Icon ::= '001100010011001000110011'B + icon2 Icon ::= '313233'H + ''' + icon1 = Icon.fromBinaryString('001100010011001000110011') + icon2 = Icon.fromHexString('313233') + """ + #: Set (on class, not on instance) or return a + #: :py:class:`~pyasn1.type.tag.TagSet` object representing ASN.1 tag(s) + #: associated with |ASN.1| type. + tagSet = tag.initTagSet( + tag.Tag(tag.tagClassUniversal, tag.tagFormatSimple, 0x04) + ) + + #: Set (on class, not on instance) or return a + #: :py:class:`~pyasn1.type.constraint.ConstraintsIntersection` object + #: imposing constraints on |ASN.1| type initialization values. + subtypeSpec = constraint.ConstraintsIntersection() + + # Optimization for faster codec lookup + typeId = base.SimpleAsn1Type.getTypeId() + + defaultBinValue = defaultHexValue = noValue + encoding = 'iso-8859-1' + + def __init__(self, value=noValue, **kwargs): + if kwargs: + if value is noValue: + try: + value = self.fromBinaryString(kwargs.pop('binValue')) + + except KeyError: + pass + + try: + value = self.fromHexString(kwargs.pop('hexValue')) + + except KeyError: + pass + + if value is noValue: + if self.defaultBinValue is not noValue: + value = self.fromBinaryString(self.defaultBinValue) + + elif self.defaultHexValue is not noValue: + value = self.fromHexString(self.defaultHexValue) + + if 'encoding' not in kwargs: + kwargs['encoding'] = self.encoding + + base.SimpleAsn1Type.__init__(self, value, **kwargs) + + def prettyIn(self, value): + if isinstance(value, bytes): + return value + + elif isinstance(value, str): + try: + return value.encode(self.encoding) + + except UnicodeEncodeError as exc: + raise error.PyAsn1UnicodeEncodeError( + "Can't encode string '%s' with '%s' " + "codec" % (value, self.encoding), exc + ) + elif isinstance(value, OctetString): # a shortcut, bytes() would work the same way + return value.asOctets() + + elif isinstance(value, base.SimpleAsn1Type): # this mostly targets Integer objects + return self.prettyIn(str(value)) + + elif isinstance(value, (tuple, list)): + return self.prettyIn(bytes(value)) + + else: + return bytes(value) + + def __str__(self): + try: + return self._value.decode(self.encoding) + + except UnicodeDecodeError as exc: + raise error.PyAsn1UnicodeDecodeError( + "Can't decode string '%s' with '%s' codec at " + "'%s'" % (self._value, self.encoding, + self.__class__.__name__), exc + ) + + def __bytes__(self): + return bytes(self._value) + + def asOctets(self): + return bytes(self._value) + + def asNumbers(self): + return tuple(self._value) + + # + # Normally, `.prettyPrint()` is called from `__str__()`. Historically, + # OctetString.prettyPrint() used to return hexified payload + # representation in cases when non-printable content is present. At the + # same time `str()` used to produce either octet-stream (Py2) or + # text (Py3) representations. + # + # Therefore `OctetString.__str__()` -> `.prettyPrint()` call chain is + # reversed to preserve the original behaviour. + # + # Eventually we should deprecate `.prettyPrint()` / `.prettyOut()` harness + # and end up with just `__str__()` producing hexified representation while + # both text and octet-stream representation should only be requested via + # the `.asOctets()` method. + # + # Note: ASN.1 OCTET STRING is never mean to contain text! + # + + def prettyOut(self, value): + return value + + def prettyPrint(self, scope=0): + # first see if subclass has its own .prettyOut() + value = self.prettyOut(self._value) + + if value is not self._value: + return value + + numbers = self.asNumbers() + + for x in numbers: + # hexify if needed + if x < 32 or x > 126: + return '0x' + ''.join(('%.2x' % x for x in numbers)) + else: + # this prevents infinite recursion + return OctetString.__str__(self) + + @staticmethod + def fromBinaryString(value): + """Create a |ASN.1| object initialized from a string of '0' and '1'. + + Parameters + ---------- + value: :class:`str` + Text string like '1010111' + """ + bitNo = 8 + byte = 0 + r = [] + for v in value: + if bitNo: + bitNo -= 1 + else: + bitNo = 7 + r.append(byte) + byte = 0 + if v in ('0', '1'): + v = int(v) + else: + raise error.PyAsn1Error( + 'Non-binary OCTET STRING initializer %s' % (v,) + ) + byte |= v << bitNo + + r.append(byte) + + return bytes(r) + + @staticmethod + def fromHexString(value): + """Create a |ASN.1| object initialized from the hex string. + + Parameters + ---------- + value: :class:`str` + Text string like 'DEADBEEF' + """ + r = [] + p = [] + for v in value: + if p: + r.append(int(p + v, 16)) + p = None + else: + p = v + if p: + r.append(int(p + '0', 16)) + + return bytes(r) + + # Immutable sequence object protocol + + def __len__(self): + return len(self._value) + + def __getitem__(self, i): + if i.__class__ is slice: + return self.clone(self._value[i]) + else: + return self._value[i] + + def __iter__(self): + return iter(self._value) + + def __contains__(self, value): + return value in self._value + + def __add__(self, value): + return self.clone(self._value + self.prettyIn(value)) + + def __radd__(self, value): + return self.clone(self.prettyIn(value) + self._value) + + def __mul__(self, value): + return self.clone(self._value * value) + + def __rmul__(self, value): + return self * value + + def __int__(self): + return int(self._value) + + def __float__(self): + return float(self._value) + + def __reversed__(self): + return reversed(self._value) + + +class Null(OctetString): + """Create |ASN.1| schema or value object. + + |ASN.1| class is based on :class:`~pyasn1.type.base.SimpleAsn1Type`, its + objects are immutable and duck-type Python :class:`str` objects + (always empty). + + Keyword Args + ------------ + value: :class:`str` or |ASN.1| object + Python empty :class:`str` literal or any object that evaluates to :obj:`False` + If `value` is not given, schema object will be created. + + tagSet: :py:class:`~pyasn1.type.tag.TagSet` + Object representing non-default ASN.1 tag(s) + + Raises + ------ + ~pyasn1.error.ValueConstraintError, ~pyasn1.error.PyAsn1Error + On constraint violation or bad initializer. + + Examples + -------- + .. code-block:: python + + class Ack(Null): + ''' + ASN.1 specification: + + Ack ::= NULL + ''' + ack = Ack('') + """ + + #: Set (on class, not on instance) or return a + #: :py:class:`~pyasn1.type.tag.TagSet` object representing ASN.1 tag(s) + #: associated with |ASN.1| type. + tagSet = tag.initTagSet( + tag.Tag(tag.tagClassUniversal, tag.tagFormatSimple, 0x05) + ) + subtypeSpec = OctetString.subtypeSpec + constraint.SingleValueConstraint(b'') + + # Optimization for faster codec lookup + typeId = OctetString.getTypeId() + + def prettyIn(self, value): + if value: + return value + + return b'' + + +class ObjectIdentifier(base.SimpleAsn1Type): + """Create |ASN.1| schema or value object. + + |ASN.1| class is based on :class:`~pyasn1.type.base.SimpleAsn1Type`, its + objects are immutable and duck-type Python :class:`tuple` objects + (tuple of non-negative integers). + + Keyword Args + ------------ + value: :class:`tuple`, :class:`str` or |ASN.1| object + Python sequence of :class:`int` or :class:`str` literal or |ASN.1| object. + If `value` is not given, schema object will be created. + + tagSet: :py:class:`~pyasn1.type.tag.TagSet` + Object representing non-default ASN.1 tag(s) + + subtypeSpec: :py:class:`~pyasn1.type.constraint.ConstraintsIntersection` + Object representing non-default ASN.1 subtype constraint(s). Constraints + verification for |ASN.1| type occurs automatically on object + instantiation. + + Raises + ------ + ~pyasn1.error.ValueConstraintError, ~pyasn1.error.PyAsn1Error + On constraint violation or bad initializer. + + Examples + -------- + .. code-block:: python + + class ID(ObjectIdentifier): + ''' + ASN.1 specification: + + ID ::= OBJECT IDENTIFIER + + id-edims ID ::= { joint-iso-itu-t mhs-motif(6) edims(7) } + id-bp ID ::= { id-edims 11 } + ''' + id_edims = ID('2.6.7') + id_bp = id_edims + (11,) + """ + #: Set (on class, not on instance) or return a + #: :py:class:`~pyasn1.type.tag.TagSet` object representing ASN.1 tag(s) + #: associated with |ASN.1| type. + tagSet = tag.initTagSet( + tag.Tag(tag.tagClassUniversal, tag.tagFormatSimple, 0x06) + ) + + #: Set (on class, not on instance) or return a + #: :py:class:`~pyasn1.type.constraint.ConstraintsIntersection` object + #: imposing constraints on |ASN.1| type initialization values. + subtypeSpec = constraint.ConstraintsIntersection() + + # Optimization for faster codec lookup + typeId = base.SimpleAsn1Type.getTypeId() + + def __add__(self, other): + return self.clone(self._value + other) + + def __radd__(self, other): + return self.clone(other + self._value) + + def asTuple(self): + return self._value + + # Sequence object protocol + + def __len__(self): + return len(self._value) + + def __getitem__(self, i): + if i.__class__ is slice: + return self.clone(self._value[i]) + else: + return self._value[i] + + def __iter__(self): + return iter(self._value) + + def __contains__(self, value): + return value in self._value + + def index(self, suboid): + return self._value.index(suboid) + + def isPrefixOf(self, other): + """Indicate if this |ASN.1| object is a prefix of other |ASN.1| object. + + Parameters + ---------- + other: |ASN.1| object + |ASN.1| object + + Returns + ------- + : :class:`bool` + :obj:`True` if this |ASN.1| object is a parent (e.g. prefix) of the other |ASN.1| object + or :obj:`False` otherwise. + """ + l = len(self) + if l <= len(other): + if self._value[:l] == other[:l]: + return True + return False + + def prettyIn(self, value): + if isinstance(value, ObjectIdentifier): + return tuple(value) + elif isinstance(value, str): + if '-' in value: + raise error.PyAsn1Error( + # sys.exc_info in case prettyIn was called while handling an exception + 'Malformed Object ID %s at %s: %s' % (value, self.__class__.__name__, sys.exc_info()[1]) + ) + try: + return tuple([int(subOid) for subOid in value.split('.') if subOid]) + except ValueError as exc: + raise error.PyAsn1Error( + 'Malformed Object ID %s at %s: %s' % (value, self.__class__.__name__, exc) + ) + + try: + tupleOfInts = tuple([int(subOid) for subOid in value if subOid >= 0]) + + except (ValueError, TypeError) as exc: + raise error.PyAsn1Error( + 'Malformed Object ID %s at %s: %s' % (value, self.__class__.__name__, exc) + ) + + if len(tupleOfInts) == len(value): + return tupleOfInts + + raise error.PyAsn1Error('Malformed Object ID %s at %s' % (value, self.__class__.__name__)) + + def prettyOut(self, value): + return '.'.join([str(x) for x in value]) + + +class RelativeOID(base.SimpleAsn1Type): + """Create |ASN.1| schema or value object. + |ASN.1| class is based on :class:`~pyasn1.type.base.SimpleAsn1Type`, its + objects are immutable and duck-type Python :class:`tuple` objects + (tuple of non-negative integers). + Keyword Args + ------------ + value: :class:`tuple`, :class:`str` or |ASN.1| object + Python sequence of :class:`int` or :class:`str` literal or |ASN.1| object. + If `value` is not given, schema object will be created. + tagSet: :py:class:`~pyasn1.type.tag.TagSet` + Object representing non-default ASN.1 tag(s) + subtypeSpec: :py:class:`~pyasn1.type.constraint.ConstraintsIntersection` + Object representing non-default ASN.1 subtype constraint(s). Constraints + verification for |ASN.1| type occurs automatically on object + instantiation. + Raises + ------ + ~pyasn1.error.ValueConstraintError, ~pyasn1.error.PyAsn1Error + On constraint violation or bad initializer. + Examples + -------- + .. code-block:: python + class RelOID(RelativeOID): + ''' + ASN.1 specification: + id-pad-null RELATIVE-OID ::= { 0 } + id-pad-once RELATIVE-OID ::= { 5 6 } + id-pad-twice RELATIVE-OID ::= { 5 6 7 } + ''' + id_pad_null = RelOID('0') + id_pad_once = RelOID('5.6') + id_pad_twice = id_pad_once + (7,) + """ + #: Set (on class, not on instance) or return a + #: :py:class:`~pyasn1.type.tag.TagSet` object representing ASN.1 tag(s) + #: associated with |ASN.1| type. + tagSet = tag.initTagSet( + tag.Tag(tag.tagClassUniversal, tag.tagFormatSimple, 0x0d) + ) + + #: Set (on class, not on instance) or return a + #: :py:class:`~pyasn1.type.constraint.ConstraintsIntersection` object + #: imposing constraints on |ASN.1| type initialization values. + subtypeSpec = constraint.ConstraintsIntersection() + + # Optimization for faster codec lookup + typeId = base.SimpleAsn1Type.getTypeId() + + def __add__(self, other): + return self.clone(self._value + other) + + def __radd__(self, other): + return self.clone(other + self._value) + + def asTuple(self): + return self._value + + # Sequence object protocol + + def __len__(self): + return len(self._value) + + def __getitem__(self, i): + if i.__class__ is slice: + return self.clone(self._value[i]) + else: + return self._value[i] + + def __iter__(self): + return iter(self._value) + + def __contains__(self, value): + return value in self._value + + def index(self, suboid): + return self._value.index(suboid) + + def isPrefixOf(self, other): + """Indicate if this |ASN.1| object is a prefix of other |ASN.1| object. + Parameters + ---------- + other: |ASN.1| object + |ASN.1| object + Returns + ------- + : :class:`bool` + :obj:`True` if this |ASN.1| object is a parent (e.g. prefix) of the other |ASN.1| object + or :obj:`False` otherwise. + """ + l = len(self) + if l <= len(other): + if self._value[:l] == other[:l]: + return True + return False + + def prettyIn(self, value): + if isinstance(value, RelativeOID): + return tuple(value) + elif isinstance(value, str): + if '-' in value: + raise error.PyAsn1Error( + # sys.exc_info in case prettyIn was called while handling an exception + 'Malformed RELATIVE-OID %s at %s: %s' % (value, self.__class__.__name__, sys.exc_info()[1]) + ) + try: + return tuple([int(subOid) for subOid in value.split('.') if subOid]) + except ValueError as exc: + raise error.PyAsn1Error( + 'Malformed RELATIVE-OID %s at %s: %s' % (value, self.__class__.__name__, exc) + ) + + try: + tupleOfInts = tuple([int(subOid) for subOid in value if subOid >= 0]) + + except (ValueError, TypeError) as exc: + raise error.PyAsn1Error( + 'Malformed RELATIVE-OID %s at %s: %s' % (value, self.__class__.__name__, exc) + ) + + if len(tupleOfInts) == len(value): + return tupleOfInts + + raise error.PyAsn1Error('Malformed RELATIVE-OID %s at %s' % (value, self.__class__.__name__)) + + def prettyOut(self, value): + return '.'.join([str(x) for x in value]) + + +class Real(base.SimpleAsn1Type): + """Create |ASN.1| schema or value object. + + |ASN.1| class is based on :class:`~pyasn1.type.base.SimpleAsn1Type`, its + objects are immutable and duck-type Python :class:`float` objects. + Additionally, |ASN.1| objects behave like a :class:`tuple` in which case its + elements are mantissa, base and exponent. + + Keyword Args + ------------ + value: :class:`tuple`, :class:`float` or |ASN.1| object + Python sequence of :class:`int` (representing mantissa, base and + exponent) or :class:`float` instance or |ASN.1| object. + If `value` is not given, schema object will be created. + + tagSet: :py:class:`~pyasn1.type.tag.TagSet` + Object representing non-default ASN.1 tag(s) + + subtypeSpec: :py:class:`~pyasn1.type.constraint.ConstraintsIntersection` + Object representing non-default ASN.1 subtype constraint(s). Constraints + verification for |ASN.1| type occurs automatically on object + instantiation. + + Raises + ------ + ~pyasn1.error.ValueConstraintError, ~pyasn1.error.PyAsn1Error + On constraint violation or bad initializer. + + Examples + -------- + .. code-block:: python + + class Pi(Real): + ''' + ASN.1 specification: + + Pi ::= REAL + + pi Pi ::= { mantissa 314159, base 10, exponent -5 } + + ''' + pi = Pi((314159, 10, -5)) + """ + binEncBase = None # binEncBase = 16 is recommended for large numbers + + try: + _plusInf = float('inf') + _minusInf = float('-inf') + _inf = _plusInf, _minusInf + + except ValueError: + # Infinity support is platform and Python dependent + _plusInf = _minusInf = None + _inf = () + + #: Set (on class, not on instance) or return a + #: :py:class:`~pyasn1.type.tag.TagSet` object representing ASN.1 tag(s) + #: associated with |ASN.1| type. + tagSet = tag.initTagSet( + tag.Tag(tag.tagClassUniversal, tag.tagFormatSimple, 0x09) + ) + + #: Set (on class, not on instance) or return a + #: :py:class:`~pyasn1.type.constraint.ConstraintsIntersection` object + #: imposing constraints on |ASN.1| type initialization values. + subtypeSpec = constraint.ConstraintsIntersection() + + # Optimization for faster codec lookup + typeId = base.SimpleAsn1Type.getTypeId() + + @staticmethod + def __normalizeBase10(value): + m, b, e = value + while m and m % 10 == 0: + m //= 10 + e += 1 + return m, b, e + + def prettyIn(self, value): + if isinstance(value, tuple) and len(value) == 3: + if (not isinstance(value[0], (int, float)) or + not isinstance(value[1], int) or + not isinstance(value[2], int)): + raise error.PyAsn1Error('Lame Real value syntax: %s' % (value,)) + if (isinstance(value[0], float) and + self._inf and value[0] in self._inf): + return value[0] + if value[1] not in (2, 10): + raise error.PyAsn1Error( + 'Prohibited base for Real value: %s' % (value[1],) + ) + if value[1] == 10: + value = self.__normalizeBase10(value) + return value + elif isinstance(value, int): + return self.__normalizeBase10((value, 10, 0)) + elif isinstance(value, float) or isinstance(value, str): + if isinstance(value, str): + try: + value = float(value) + except ValueError: + raise error.PyAsn1Error( + 'Bad real value syntax: %s' % (value,) + ) + if self._inf and value in self._inf: + return value + else: + e = 0 + while int(value) != value: + value *= 10 + e -= 1 + return self.__normalizeBase10((int(value), 10, e)) + elif isinstance(value, Real): + return tuple(value) + raise error.PyAsn1Error( + 'Bad real value syntax: %s' % (value,) + ) + + def prettyPrint(self, scope=0): + try: + return self.prettyOut(float(self)) + + except OverflowError: + return '' + + @property + def isPlusInf(self): + """Indicate PLUS-INFINITY object value + + Returns + ------- + : :class:`bool` + :obj:`True` if calling object represents plus infinity + or :obj:`False` otherwise. + + """ + return self._value == self._plusInf + + @property + def isMinusInf(self): + """Indicate MINUS-INFINITY object value + + Returns + ------- + : :class:`bool` + :obj:`True` if calling object represents minus infinity + or :obj:`False` otherwise. + """ + return self._value == self._minusInf + + @property + def isInf(self): + return self._value in self._inf + + def __add__(self, value): + return self.clone(float(self) + value) + + def __radd__(self, value): + return self + value + + def __mul__(self, value): + return self.clone(float(self) * value) + + def __rmul__(self, value): + return self * value + + def __sub__(self, value): + return self.clone(float(self) - value) + + def __rsub__(self, value): + return self.clone(value - float(self)) + + def __mod__(self, value): + return self.clone(float(self) % value) + + def __rmod__(self, value): + return self.clone(value % float(self)) + + def __pow__(self, value, modulo=None): + return self.clone(pow(float(self), value, modulo)) + + def __rpow__(self, value): + return self.clone(pow(value, float(self))) + + def __truediv__(self, value): + return self.clone(float(self) / value) + + def __rtruediv__(self, value): + return self.clone(value / float(self)) + + def __divmod__(self, value): + return self.clone(float(self) // value) + + def __rdivmod__(self, value): + return self.clone(value // float(self)) + + def __int__(self): + return int(float(self)) + + def __float__(self): + if self._value in self._inf: + return self._value + + mantissa, base, exponent = self._value + + if not mantissa: + return 0.0 + + if base == 2: + return math.ldexp(float(mantissa), exponent) + + # base is 10 (prettyIn() rejects everything else); refuse to + # materialize astronomically large integers via pow() + if exponent > sys.float_info.max_10_exp: + raise OverflowError('Real value too large to convert to float') + + return float(mantissa * pow(base, exponent)) + + def __abs__(self): + return self.clone(abs(float(self))) + + def __pos__(self): + return self.clone(+float(self)) + + def __neg__(self): + return self.clone(-float(self)) + + def __round__(self, n=0): + r = round(float(self), n) + if n: + return self.clone(r) + else: + return r + + def __floor__(self): + return self.clone(math.floor(float(self))) + + def __ceil__(self): + return self.clone(math.ceil(float(self))) + + def __trunc__(self): + return self.clone(math.trunc(float(self))) + + def __lt__(self, value): + return float(self) < value + + def __le__(self, value): + return float(self) <= value + + def __eq__(self, value): + return float(self) == value + + def __ne__(self, value): + return float(self) != value + + def __gt__(self, value): + return float(self) > value + + def __ge__(self, value): + return float(self) >= value + + def __bool__(self): + return bool(float(self)) + + __hash__ = base.SimpleAsn1Type.__hash__ + + def __getitem__(self, idx): + if self._value in self._inf: + raise error.PyAsn1Error('Invalid infinite value operation') + else: + return self._value[idx] + + # compatibility stubs + + def isPlusInfinity(self): + return self.isPlusInf + + def isMinusInfinity(self): + return self.isMinusInf + + def isInfinity(self): + return self.isInf + + +class Enumerated(Integer): + """Create |ASN.1| schema or value object. + + |ASN.1| class is based on :class:`~pyasn1.type.base.SimpleAsn1Type`, its + objects are immutable and duck-type Python :class:`int` objects. + + Keyword Args + ------------ + value: :class:`int`, :class:`str` or |ASN.1| object + Python :class:`int` or :class:`str` literal or |ASN.1| object. + If `value` is not given, schema object will be created. + + tagSet: :py:class:`~pyasn1.type.tag.TagSet` + Object representing non-default ASN.1 tag(s) + + subtypeSpec: :py:class:`~pyasn1.type.constraint.ConstraintsIntersection` + Object representing non-default ASN.1 subtype constraint(s). Constraints + verification for |ASN.1| type occurs automatically on object + instantiation. + + namedValues: :py:class:`~pyasn1.type.namedval.NamedValues` + Object representing non-default symbolic aliases for numbers + + Raises + ------ + ~pyasn1.error.ValueConstraintError, ~pyasn1.error.PyAsn1Error + On constraint violation or bad initializer. + + Examples + -------- + + .. code-block:: python + + class RadioButton(Enumerated): + ''' + ASN.1 specification: + + RadioButton ::= ENUMERATED { button1(0), button2(1), + button3(2) } + + selected-by-default RadioButton ::= button1 + ''' + namedValues = NamedValues( + ('button1', 0), ('button2', 1), + ('button3', 2) + ) + + selected_by_default = RadioButton('button1') + """ + #: Set (on class, not on instance) or return a + #: :py:class:`~pyasn1.type.tag.TagSet` object representing ASN.1 tag(s) + #: associated with |ASN.1| type. + tagSet = tag.initTagSet( + tag.Tag(tag.tagClassUniversal, tag.tagFormatSimple, 0x0A) + ) + + #: Set (on class, not on instance) or return a + #: :py:class:`~pyasn1.type.constraint.ConstraintsIntersection` object + #: imposing constraints on |ASN.1| type initialization values. + subtypeSpec = constraint.ConstraintsIntersection() + + # Optimization for faster codec lookup + typeId = Integer.getTypeId() + + #: Default :py:class:`~pyasn1.type.namedval.NamedValues` object + #: representing symbolic aliases for numbers + namedValues = namedval.NamedValues() + + +# "Structured" ASN.1 types + +class SequenceOfAndSetOfBase(base.ConstructedAsn1Type): + """Create |ASN.1| schema or value object. + + |ASN.1| class is based on :class:`~pyasn1.type.base.ConstructedAsn1Type`, + its objects are mutable and duck-type Python :class:`list` objects. + + Keyword Args + ------------ + componentType : :py:class:`~pyasn1.type.base.PyAsn1Item` derivative + A pyasn1 object representing ASN.1 type allowed within |ASN.1| type + + tagSet: :py:class:`~pyasn1.type.tag.TagSet` + Object representing non-default ASN.1 tag(s) + + subtypeSpec: :py:class:`~pyasn1.type.constraint.ConstraintsIntersection` + Object representing non-default ASN.1 subtype constraint(s). Constraints + verification for |ASN.1| type can only occur on explicit + `.isInconsistent` call. + + Examples + -------- + + .. code-block:: python + + class LotteryDraw(SequenceOf): # SetOf is similar + ''' + ASN.1 specification: + + LotteryDraw ::= SEQUENCE OF INTEGER + ''' + componentType = Integer() + + lotteryDraw = LotteryDraw() + lotteryDraw.extend([123, 456, 789]) + """ + def __init__(self, *args, **kwargs): + # support positional params for backward compatibility + if args: + for key, value in zip(('componentType', 'tagSet', + 'subtypeSpec'), args): + if key in kwargs: + raise error.PyAsn1Error('Conflicting positional and keyword params!') + kwargs['componentType'] = value + + self._componentValues = noValue + + base.ConstructedAsn1Type.__init__(self, **kwargs) + + # Python list protocol + + def __getitem__(self, idx): + try: + return self.getComponentByPosition(idx) + + except error.PyAsn1Error as exc: + raise IndexError(exc) + + def __setitem__(self, idx, value): + try: + self.setComponentByPosition(idx, value) + + except error.PyAsn1Error as exc: + raise IndexError(exc) + + def append(self, value): + if self._componentValues is noValue: + pos = 0 + + else: + pos = len(self._componentValues) + + self[pos] = value + + def count(self, value): + return list(self._componentValues.values()).count(value) + + def extend(self, values): + for value in values: + self.append(value) + + if self._componentValues is noValue: + self._componentValues = {} + + def index(self, value, start=0, stop=None): + if stop is None: + stop = len(self) + + indices, values = zip(*self._componentValues.items()) + + # TODO: remove when Py2.5 support is gone + values = list(values) + + try: + return indices[values.index(value, start, stop)] + + except error.PyAsn1Error as exc: + raise ValueError(exc) + + def reverse(self): + self._componentValues.reverse() + + def sort(self, key=None, reverse=False): + self._componentValues = dict( + enumerate(sorted(self._componentValues.values(), + key=key, reverse=reverse))) + + def __len__(self): + if self._componentValues is noValue or not self._componentValues: + return 0 + + return max(self._componentValues) + 1 + + def __iter__(self): + for idx in range(0, len(self)): + yield self.getComponentByPosition(idx) + + def _cloneComponentValues(self, myClone, cloneValueFlag): + for idx, componentValue in self._componentValues.items(): + if componentValue is not noValue: + if isinstance(componentValue, base.ConstructedAsn1Type): + myClone.setComponentByPosition( + idx, componentValue.clone(cloneValueFlag=cloneValueFlag) + ) + else: + myClone.setComponentByPosition(idx, componentValue.clone()) + + def getComponentByPosition(self, idx, default=noValue, instantiate=True): + """Return |ASN.1| type component value by position. + + Equivalent to Python sequence subscription operation (e.g. `[]`). + + Parameters + ---------- + idx : :class:`int` + Component index (zero-based). Must either refer to an existing + component or to N+1 component (if *componentType* is set). In the latter + case a new component type gets instantiated and appended to the |ASN.1| + sequence. + + Keyword Args + ------------ + default: :class:`object` + If set and requested component is a schema object, return the `default` + object instead of the requested component. + + instantiate: :class:`bool` + If :obj:`True` (default), inner component will be automatically instantiated. + If :obj:`False` either existing component or the :class:`NoValue` object will be + returned. + + Returns + ------- + : :py:class:`~pyasn1.type.base.PyAsn1Item` + Instantiate |ASN.1| component type or return existing component value + + Examples + -------- + + .. code-block:: python + + # can also be SetOf + class MySequenceOf(SequenceOf): + componentType = OctetString() + + s = MySequenceOf() + + # returns component #0 with `.isValue` property False + s.getComponentByPosition(0) + + # returns None + s.getComponentByPosition(0, default=None) + + s.clear() + + # returns noValue + s.getComponentByPosition(0, instantiate=False) + + # sets component #0 to OctetString() ASN.1 schema + # object and returns it + s.getComponentByPosition(0, instantiate=True) + + # sets component #0 to ASN.1 value object + s.setComponentByPosition(0, 'ABCD') + + # returns OctetString('ABCD') value object + s.getComponentByPosition(0, instantiate=False) + + s.clear() + + # returns noValue + s.getComponentByPosition(0, instantiate=False) + """ + if isinstance(idx, slice): + indices = tuple(range(len(self))) + return [self.getComponentByPosition(subidx, default, instantiate) + for subidx in indices[idx]] + + if idx < 0: + idx = len(self) + idx + if idx < 0: + raise error.PyAsn1Error( + 'SequenceOf/SetOf index is out of range') + + try: + componentValue = self._componentValues[idx] + + except (KeyError, error.PyAsn1Error): + if not instantiate: + return default + + self.setComponentByPosition(idx) + + componentValue = self._componentValues[idx] + + if default is noValue or componentValue.isValue: + return componentValue + else: + return default + + def setComponentByPosition(self, idx, value=noValue, + verifyConstraints=True, + matchTags=True, + matchConstraints=True): + """Assign |ASN.1| type component by position. + + Equivalent to Python sequence item assignment operation (e.g. `[]`) + or list.append() (when idx == len(self)). + + Parameters + ---------- + idx: :class:`int` + Component index (zero-based). Must either refer to existing + component or to N+1 component. In the latter case a new component + type gets instantiated (if *componentType* is set, or given ASN.1 + object is taken otherwise) and appended to the |ASN.1| sequence. + + Keyword Args + ------------ + value: :class:`object` or :py:class:`~pyasn1.type.base.PyAsn1Item` derivative + A Python value to initialize |ASN.1| component with (if *componentType* is set) + or ASN.1 value object to assign to |ASN.1| component. + If `value` is not given, schema object will be set as a component. + + verifyConstraints: :class:`bool` + If :obj:`False`, skip constraints validation + + matchTags: :class:`bool` + If :obj:`False`, skip component tags matching + + matchConstraints: :class:`bool` + If :obj:`False`, skip component constraints matching + + Returns + ------- + self + + Raises + ------ + ~pyasn1.error.ValueConstraintError, ~pyasn1.error.PyAsn1Error + On constraint violation or bad initializer + IndexError + When idx > len(self) + """ + if isinstance(idx, slice): + indices = tuple(range(len(self))) + startIdx = indices and indices[idx][0] or 0 + for subIdx, subValue in enumerate(value): + self.setComponentByPosition( + startIdx + subIdx, subValue, verifyConstraints, + matchTags, matchConstraints) + return self + + if idx < 0: + idx = len(self) + idx + if idx < 0: + raise error.PyAsn1Error( + 'SequenceOf/SetOf index is out of range') + + componentType = self.componentType + + if self._componentValues is noValue: + componentValues = {} + + else: + componentValues = self._componentValues + + currentValue = componentValues.get(idx, noValue) + + if value is noValue: + if componentType is not None: + value = componentType.clone() + + elif currentValue is noValue: + raise error.PyAsn1Error('Component type not defined') + + elif not isinstance(value, base.Asn1Item): + if (componentType is not None and + isinstance(componentType, base.SimpleAsn1Type)): + value = componentType.clone(value=value) + + elif (currentValue is not noValue and + isinstance(currentValue, base.SimpleAsn1Type)): + value = currentValue.clone(value=value) + + else: + raise error.PyAsn1Error( + 'Non-ASN.1 value %r and undefined component' + ' type at %r' % (value, self)) + + elif componentType is not None and (matchTags or matchConstraints): + subtypeChecker = ( + self.strictConstraints and + componentType.isSameTypeWith or + componentType.isSuperTypeOf) + + if not subtypeChecker(value, verifyConstraints and matchTags, + verifyConstraints and matchConstraints): + # TODO: we should wrap componentType with UnnamedType to carry + # additional properties associated with componentType + if componentType.typeId != Any.typeId: + raise error.PyAsn1Error( + 'Component value is tag-incompatible: %r vs ' + '%r' % (value, componentType)) + + componentValues[idx] = value + + self._componentValues = componentValues + + return self + + @property + def componentTagMap(self): + if self.componentType is not None: + return self.componentType.tagMap + + @property + def components(self): + return [self._componentValues[idx] + for idx in sorted(self._componentValues)] + + def clear(self): + """Remove all components and become an empty |ASN.1| value object. + + Has the same effect on |ASN.1| object as it does on :class:`list` + built-in. + """ + self._componentValues = {} + return self + + def reset(self): + """Remove all components and become a |ASN.1| schema object. + + See :meth:`isValue` property for more information on the + distinction between value and schema objects. + """ + self._componentValues = noValue + return self + + def prettyPrint(self, scope=0): + scope += 1 + representation = self.__class__.__name__ + ':\n' + + if not self.isValue: + return representation + + for idx, componentValue in enumerate(self): + representation += ' ' * scope + if (componentValue is noValue and + self.componentType is not None): + representation += '' + else: + representation += componentValue.prettyPrint(scope) + + return representation + + def prettyPrintType(self, scope=0): + scope += 1 + representation = '%s -> %s {\n' % (self.tagSet, self.__class__.__name__) + if self.componentType is not None: + representation += ' ' * scope + representation += self.componentType.prettyPrintType(scope) + return representation + '\n' + ' ' * (scope - 1) + '}' + + + @property + def isValue(self): + """Indicate that |ASN.1| object represents ASN.1 value. + + If *isValue* is :obj:`False` then this object represents just ASN.1 schema. + + If *isValue* is :obj:`True` then, in addition to its ASN.1 schema features, + this object can also be used like a Python built-in object + (e.g. :class:`int`, :class:`str`, :class:`dict` etc.). + + Returns + ------- + : :class:`bool` + :obj:`False` if object represents just ASN.1 schema. + :obj:`True` if object represents ASN.1 schema and can be used as a normal value. + + Note + ---- + There is an important distinction between PyASN1 schema and value objects. + The PyASN1 schema objects can only participate in ASN.1 schema-related + operations (e.g. defining or testing the structure of the data). Most + obvious uses of ASN.1 schema is to guide serialisation codecs whilst + encoding/decoding serialised ASN.1 contents. + + The PyASN1 value objects can **additionally** participate in many operations + involving regular Python objects (e.g. arithmetic, comprehension etc). + """ + if self._componentValues is noValue: + return False + + if len(self._componentValues) != len(self): + return False + + for componentValue in self._componentValues.values(): + if componentValue is noValue or not componentValue.isValue: + return False + + return True + + @property + def isInconsistent(self): + """Run necessary checks to ensure |ASN.1| object consistency. + + Default action is to verify |ASN.1| object against constraints imposed + by `subtypeSpec`. + + Raises + ------ + :py:class:`~pyasn1.error.PyAsn1tError` on any inconsistencies found + """ + if self.componentType is noValue or not self.subtypeSpec: + return False + + if self._componentValues is noValue: + return True + + mapping = {} + + for idx, value in self._componentValues.items(): + # Absent fields are not in the mapping + if value is noValue: + continue + + mapping[idx] = value + + try: + # Represent SequenceOf/SetOf as a bare dict to constraints chain + self.subtypeSpec(mapping) + + except error.PyAsn1Error as exc: + return exc + + return False + +class SequenceOf(SequenceOfAndSetOfBase): + __doc__ = SequenceOfAndSetOfBase.__doc__ + + #: Set (on class, not on instance) or return a + #: :py:class:`~pyasn1.type.tag.TagSet` object representing ASN.1 tag(s) + #: associated with |ASN.1| type. + tagSet = tag.initTagSet( + tag.Tag(tag.tagClassUniversal, tag.tagFormatConstructed, 0x10) + ) + + #: Default :py:class:`~pyasn1.type.base.PyAsn1Item` derivative + #: object representing ASN.1 type allowed within |ASN.1| type + componentType = None + + #: Set (on class, not on instance) or return a + #: :py:class:`~pyasn1.type.constraint.ConstraintsIntersection` object + #: imposing constraints on |ASN.1| type initialization values. + subtypeSpec = constraint.ConstraintsIntersection() + + # Disambiguation ASN.1 types identification + typeId = SequenceOfAndSetOfBase.getTypeId() + + +class SetOf(SequenceOfAndSetOfBase): + __doc__ = SequenceOfAndSetOfBase.__doc__ + + #: Set (on class, not on instance) or return a + #: :py:class:`~pyasn1.type.tag.TagSet` object representing ASN.1 tag(s) + #: associated with |ASN.1| type. + tagSet = tag.initTagSet( + tag.Tag(tag.tagClassUniversal, tag.tagFormatConstructed, 0x11) + ) + + #: Default :py:class:`~pyasn1.type.base.PyAsn1Item` derivative + #: object representing ASN.1 type allowed within |ASN.1| type + componentType = None + + #: Set (on class, not on instance) or return a + #: :py:class:`~pyasn1.type.constraint.ConstraintsIntersection` object + #: imposing constraints on |ASN.1| type initialization values. + subtypeSpec = constraint.ConstraintsIntersection() + + # Disambiguation ASN.1 types identification + typeId = SequenceOfAndSetOfBase.getTypeId() + + +class SequenceAndSetBase(base.ConstructedAsn1Type): + """Create |ASN.1| schema or value object. + + |ASN.1| class is based on :class:`~pyasn1.type.base.ConstructedAsn1Type`, + its objects are mutable and duck-type Python :class:`dict` objects. + + Keyword Args + ------------ + componentType: :py:class:`~pyasn1.type.namedtype.NamedType` + Object holding named ASN.1 types allowed within this collection + + tagSet: :py:class:`~pyasn1.type.tag.TagSet` + Object representing non-default ASN.1 tag(s) + + subtypeSpec: :py:class:`~pyasn1.type.constraint.ConstraintsIntersection` + Object representing non-default ASN.1 subtype constraint(s). Constraints + verification for |ASN.1| type can only occur on explicit + `.isInconsistent` call. + + Examples + -------- + + .. code-block:: python + + class Description(Sequence): # Set is similar + ''' + ASN.1 specification: + + Description ::= SEQUENCE { + surname IA5String, + first-name IA5String OPTIONAL, + age INTEGER DEFAULT 40 + } + ''' + componentType = NamedTypes( + NamedType('surname', IA5String()), + OptionalNamedType('first-name', IA5String()), + DefaultedNamedType('age', Integer(40)) + ) + + descr = Description() + descr['surname'] = 'Smith' + descr['first-name'] = 'John' + """ + #: Default :py:class:`~pyasn1.type.namedtype.NamedTypes` + #: object representing named ASN.1 types allowed within |ASN.1| type + componentType = namedtype.NamedTypes() + + + class DynamicNames(object): + """Fields names/positions mapping for component-less objects""" + def __init__(self): + self._keyToIdxMap = {} + self._idxToKeyMap = {} + + def __len__(self): + return len(self._keyToIdxMap) + + def __contains__(self, item): + return item in self._keyToIdxMap or item in self._idxToKeyMap + + def __iter__(self): + return (self._idxToKeyMap[idx] for idx in range(len(self._idxToKeyMap))) + + def __getitem__(self, item): + try: + return self._keyToIdxMap[item] + + except KeyError: + return self._idxToKeyMap[item] + + def getNameByPosition(self, idx): + try: + return self._idxToKeyMap[idx] + + except KeyError: + raise error.PyAsn1Error('Type position out of range') + + def getPositionByName(self, name): + try: + return self._keyToIdxMap[name] + + except KeyError: + raise error.PyAsn1Error('Name %s not found' % (name,)) + + def addField(self, idx): + self._keyToIdxMap['field-%d' % idx] = idx + self._idxToKeyMap[idx] = 'field-%d' % idx + + + def __init__(self, **kwargs): + base.ConstructedAsn1Type.__init__(self, **kwargs) + self._componentTypeLen = len(self.componentType) + if self._componentTypeLen: + self._componentValues = [] + else: + self._componentValues = noValue + self._dynamicNames = self._componentTypeLen or self.DynamicNames() + + def __getitem__(self, idx): + if isinstance(idx, str): + try: + return self.getComponentByName(idx) + + except error.PyAsn1Error as exc: + # duck-typing dict + raise KeyError(exc) + + else: + try: + return self.getComponentByPosition(idx) + + except error.PyAsn1Error as exc: + # duck-typing list + raise IndexError(exc) + + def __setitem__(self, idx, value): + if isinstance(idx, str): + try: + self.setComponentByName(idx, value) + + except error.PyAsn1Error as exc: + # duck-typing dict + raise KeyError(exc) + + else: + try: + self.setComponentByPosition(idx, value) + + except error.PyAsn1Error as exc: + # duck-typing list + raise IndexError(exc) + + def __contains__(self, key): + if self._componentTypeLen: + return key in self.componentType + else: + return key in self._dynamicNames + + def __len__(self): + return len(self._componentValues) + + def __iter__(self): + return iter(self.componentType or self._dynamicNames) + + # Python dict protocol + + def values(self): + for idx in range(self._componentTypeLen or len(self._dynamicNames)): + yield self[idx] + + def keys(self): + return iter(self) + + def items(self): + for idx in range(self._componentTypeLen or len(self._dynamicNames)): + if self._componentTypeLen: + yield self.componentType[idx].name, self[idx] + else: + yield self._dynamicNames[idx], self[idx] + + def update(self, *iterValue, **mappingValue): + for k, v in iterValue: + self[k] = v + for k in mappingValue: + self[k] = mappingValue[k] + + def clear(self): + """Remove all components and become an empty |ASN.1| value object. + + Has the same effect on |ASN.1| object as it does on :class:`dict` + built-in. + """ + self._componentValues = [] + self._dynamicNames = self.DynamicNames() + return self + + def reset(self): + """Remove all components and become a |ASN.1| schema object. + + See :meth:`isValue` property for more information on the + distinction between value and schema objects. + """ + self._componentValues = noValue + self._dynamicNames = self.DynamicNames() + return self + + @property + def components(self): + return self._componentValues + + def _cloneComponentValues(self, myClone, cloneValueFlag): + if self._componentValues is noValue: + return + + for idx, componentValue in enumerate(self._componentValues): + if componentValue is not noValue: + if isinstance(componentValue, base.ConstructedAsn1Type): + myClone.setComponentByPosition( + idx, componentValue.clone(cloneValueFlag=cloneValueFlag) + ) + else: + myClone.setComponentByPosition(idx, componentValue.clone()) + + def getComponentByName(self, name, default=noValue, instantiate=True): + """Returns |ASN.1| type component by name. + + Equivalent to Python :class:`dict` subscription operation (e.g. `[]`). + + Parameters + ---------- + name: :class:`str` + |ASN.1| type component name + + Keyword Args + ------------ + default: :class:`object` + If set and requested component is a schema object, return the `default` + object instead of the requested component. + + instantiate: :class:`bool` + If :obj:`True` (default), inner component will be automatically + instantiated. + If :obj:`False` either existing component or the :class:`NoValue` + object will be returned. + + Returns + ------- + : :py:class:`~pyasn1.type.base.PyAsn1Item` + Instantiate |ASN.1| component type or return existing + component value + """ + if self._componentTypeLen: + idx = self.componentType.getPositionByName(name) + else: + try: + idx = self._dynamicNames.getPositionByName(name) + + except KeyError: + raise error.PyAsn1Error('Name %s not found' % (name,)) + + return self.getComponentByPosition(idx, default=default, instantiate=instantiate) + + def setComponentByName(self, name, value=noValue, + verifyConstraints=True, + matchTags=True, + matchConstraints=True): + """Assign |ASN.1| type component by name. + + Equivalent to Python :class:`dict` item assignment operation (e.g. `[]`). + + Parameters + ---------- + name: :class:`str` + |ASN.1| type component name + + Keyword Args + ------------ + value: :class:`object` or :py:class:`~pyasn1.type.base.PyAsn1Item` derivative + A Python value to initialize |ASN.1| component with (if *componentType* is set) + or ASN.1 value object to assign to |ASN.1| component. + If `value` is not given, schema object will be set as a component. + + verifyConstraints: :class:`bool` + If :obj:`False`, skip constraints validation + + matchTags: :class:`bool` + If :obj:`False`, skip component tags matching + + matchConstraints: :class:`bool` + If :obj:`False`, skip component constraints matching + + Returns + ------- + self + """ + if self._componentTypeLen: + idx = self.componentType.getPositionByName(name) + else: + try: + idx = self._dynamicNames.getPositionByName(name) + + except KeyError: + raise error.PyAsn1Error('Name %s not found' % (name,)) + + return self.setComponentByPosition( + idx, value, verifyConstraints, matchTags, matchConstraints + ) + + def getComponentByPosition(self, idx, default=noValue, instantiate=True): + """Returns |ASN.1| type component by index. + + Equivalent to Python sequence subscription operation (e.g. `[]`). + + Parameters + ---------- + idx: :class:`int` + Component index (zero-based). Must either refer to an existing + component or (if *componentType* is set) new ASN.1 schema object gets + instantiated. + + Keyword Args + ------------ + default: :class:`object` + If set and requested component is a schema object, return the `default` + object instead of the requested component. + + instantiate: :class:`bool` + If :obj:`True` (default), inner component will be automatically + instantiated. + If :obj:`False` either existing component or the :class:`NoValue` + object will be returned. + + Returns + ------- + : :py:class:`~pyasn1.type.base.PyAsn1Item` + a PyASN1 object + + Examples + -------- + + .. code-block:: python + + # can also be Set + class MySequence(Sequence): + componentType = NamedTypes( + NamedType('id', OctetString()) + ) + + s = MySequence() + + # returns component #0 with `.isValue` property False + s.getComponentByPosition(0) + + # returns None + s.getComponentByPosition(0, default=None) + + s.clear() + + # returns noValue + s.getComponentByPosition(0, instantiate=False) + + # sets component #0 to OctetString() ASN.1 schema + # object and returns it + s.getComponentByPosition(0, instantiate=True) + + # sets component #0 to ASN.1 value object + s.setComponentByPosition(0, 'ABCD') + + # returns OctetString('ABCD') value object + s.getComponentByPosition(0, instantiate=False) + + s.clear() + + # returns noValue + s.getComponentByPosition(0, instantiate=False) + """ + try: + if self._componentValues is noValue: + componentValue = noValue + + else: + componentValue = self._componentValues[idx] + + except IndexError: + componentValue = noValue + + if not instantiate: + if componentValue is noValue or not componentValue.isValue: + return default + else: + return componentValue + + if componentValue is noValue: + self.setComponentByPosition(idx) + + componentValue = self._componentValues[idx] + + if default is noValue or componentValue.isValue: + return componentValue + else: + return default + + def setComponentByPosition(self, idx, value=noValue, + verifyConstraints=True, + matchTags=True, + matchConstraints=True): + """Assign |ASN.1| type component by position. + + Equivalent to Python sequence item assignment operation (e.g. `[]`). + + Parameters + ---------- + idx : :class:`int` + Component index (zero-based). Must either refer to existing + component (if *componentType* is set) or to N+1 component + otherwise. In the latter case a new component of given ASN.1 + type gets instantiated and appended to |ASN.1| sequence. + + Keyword Args + ------------ + value: :class:`object` or :py:class:`~pyasn1.type.base.PyAsn1Item` derivative + A Python value to initialize |ASN.1| component with (if *componentType* is set) + or ASN.1 value object to assign to |ASN.1| component. + If `value` is not given, schema object will be set as a component. + + verifyConstraints : :class:`bool` + If :obj:`False`, skip constraints validation + + matchTags: :class:`bool` + If :obj:`False`, skip component tags matching + + matchConstraints: :class:`bool` + If :obj:`False`, skip component constraints matching + + Returns + ------- + self + """ + componentType = self.componentType + componentTypeLen = self._componentTypeLen + + if self._componentValues is noValue: + componentValues = [] + + else: + componentValues = self._componentValues + + try: + currentValue = componentValues[idx] + + except IndexError: + currentValue = noValue + if componentTypeLen: + if componentTypeLen < idx: + raise error.PyAsn1Error('component index out of range') + + componentValues = [noValue] * componentTypeLen + + if value is noValue: + if componentTypeLen: + value = componentType.getTypeByPosition(idx) + if isinstance(value, base.ConstructedAsn1Type): + value = value.clone(cloneValueFlag=componentType[idx].isDefaulted) + + elif currentValue is noValue: + raise error.PyAsn1Error('Component type not defined') + + elif not isinstance(value, base.Asn1Item): + if componentTypeLen: + subComponentType = componentType.getTypeByPosition(idx) + if isinstance(subComponentType, base.SimpleAsn1Type): + value = subComponentType.clone(value=value) + + else: + raise error.PyAsn1Error('%s can cast only scalar values' % componentType.__class__.__name__) + + elif currentValue is not noValue and isinstance(currentValue, base.SimpleAsn1Type): + value = currentValue.clone(value=value) + + else: + raise error.PyAsn1Error('%s undefined component type' % componentType.__class__.__name__) + + elif ((verifyConstraints or matchTags or matchConstraints) and + componentTypeLen): + subComponentType = componentType.getTypeByPosition(idx) + if subComponentType is not noValue: + subtypeChecker = (self.strictConstraints and + subComponentType.isSameTypeWith or + subComponentType.isSuperTypeOf) + + if not subtypeChecker(value, verifyConstraints and matchTags, + verifyConstraints and matchConstraints): + if not componentType[idx].openType: + raise error.PyAsn1Error('Component value is tag-incompatible: %r vs %r' % (value, componentType)) + + if componentTypeLen or idx in self._dynamicNames: + componentValues[idx] = value + + elif len(componentValues) == idx: + componentValues.append(value) + self._dynamicNames.addField(idx) + + else: + raise error.PyAsn1Error('Component index out of range') + + self._componentValues = componentValues + + return self + + @property + def isValue(self): + """Indicate that |ASN.1| object represents ASN.1 value. + + If *isValue* is :obj:`False` then this object represents just ASN.1 schema. + + If *isValue* is :obj:`True` then, in addition to its ASN.1 schema features, + this object can also be used like a Python built-in object (e.g. + :class:`int`, :class:`str`, :class:`dict` etc.). + + Returns + ------- + : :class:`bool` + :obj:`False` if object represents just ASN.1 schema. + :obj:`True` if object represents ASN.1 schema and can be used as a + normal value. + + Note + ---- + There is an important distinction between PyASN1 schema and value objects. + The PyASN1 schema objects can only participate in ASN.1 schema-related + operations (e.g. defining or testing the structure of the data). Most + obvious uses of ASN.1 schema is to guide serialisation codecs whilst + encoding/decoding serialised ASN.1 contents. + + The PyASN1 value objects can **additionally** participate in many operations + involving regular Python objects (e.g. arithmetic, comprehension etc). + + It is sufficient for |ASN.1| objects to have all non-optional and non-defaulted + components being value objects to be considered as a value objects as a whole. + In other words, even having one or more optional components not turned into + value objects, |ASN.1| object is still considered as a value object. Defaulted + components are normally value objects by default. + """ + if self._componentValues is noValue: + return False + + componentType = self.componentType + + if componentType: + for idx, subComponentType in enumerate(componentType.namedTypes): + if subComponentType.isDefaulted or subComponentType.isOptional: + continue + + if not self._componentValues: + return False + + componentValue = self._componentValues[idx] + if componentValue is noValue or not componentValue.isValue: + return False + + else: + for componentValue in self._componentValues: + if componentValue is noValue or not componentValue.isValue: + return False + + return True + + @property + def isInconsistent(self): + """Run necessary checks to ensure |ASN.1| object consistency. + + Default action is to verify |ASN.1| object against constraints imposed + by `subtypeSpec`. + + Raises + ------ + :py:class:`~pyasn1.error.PyAsn1tError` on any inconsistencies found + """ + if self.componentType is noValue or not self.subtypeSpec: + return False + + if self._componentValues is noValue: + return True + + mapping = {} + + for idx, value in enumerate(self._componentValues): + # Absent fields are not in the mapping + if value is noValue: + continue + + name = self.componentType.getNameByPosition(idx) + + mapping[name] = value + + try: + # Represent Sequence/Set as a bare dict to constraints chain + self.subtypeSpec(mapping) + + except error.PyAsn1Error as exc: + return exc + + return False + + def prettyPrint(self, scope=0): + """Return an object representation string. + + Returns + ------- + : :class:`str` + Human-friendly object representation. + """ + scope += 1 + representation = self.__class__.__name__ + ':\n' + for idx, componentValue in enumerate(self._componentValues): + if componentValue is not noValue and componentValue.isValue: + representation += ' ' * scope + if self.componentType: + representation += self.componentType.getNameByPosition(idx) + else: + representation += self._dynamicNames.getNameByPosition(idx) + representation = '%s=%s\n' % ( + representation, componentValue.prettyPrint(scope) + ) + return representation + + def prettyPrintType(self, scope=0): + scope += 1 + representation = '%s -> %s {\n' % (self.tagSet, self.__class__.__name__) + for idx, componentType in enumerate(self.componentType.values() or self._componentValues): + representation += ' ' * scope + if self.componentType: + representation += '"%s"' % self.componentType.getNameByPosition(idx) + else: + representation += '"%s"' % self._dynamicNames.getNameByPosition(idx) + representation = '%s = %s\n' % ( + representation, componentType.prettyPrintType(scope) + ) + return representation + '\n' + ' ' * (scope - 1) + '}' + + # backward compatibility + + def setDefaultComponents(self): + return self + + def getComponentType(self): + if self._componentTypeLen: + return self.componentType + + def getNameByPosition(self, idx): + if self._componentTypeLen: + return self.componentType[idx].name + +class Sequence(SequenceAndSetBase): + __doc__ = SequenceAndSetBase.__doc__ + + #: Set (on class, not on instance) or return a + #: :py:class:`~pyasn1.type.tag.TagSet` object representing ASN.1 tag(s) + #: associated with |ASN.1| type. + tagSet = tag.initTagSet( + tag.Tag(tag.tagClassUniversal, tag.tagFormatConstructed, 0x10) + ) + + #: Set (on class, not on instance) or return a + #: :py:class:`~pyasn1.type.constraint.ConstraintsIntersection` object + #: imposing constraints on |ASN.1| type initialization values. + subtypeSpec = constraint.ConstraintsIntersection() + + #: Default collection of ASN.1 types of component (e.g. :py:class:`~pyasn1.type.namedtype.NamedType`) + #: object imposing size constraint on |ASN.1| objects + componentType = namedtype.NamedTypes() + + # Disambiguation ASN.1 types identification + typeId = SequenceAndSetBase.getTypeId() + + # backward compatibility + + def getComponentTagMapNearPosition(self, idx): + if self.componentType: + return self.componentType.getTagMapNearPosition(idx) + + def getComponentPositionNearType(self, tagSet, idx): + if self.componentType: + return self.componentType.getPositionNearType(tagSet, idx) + else: + return idx + + +class Set(SequenceAndSetBase): + __doc__ = SequenceAndSetBase.__doc__ + + #: Set (on class, not on instance) or return a + #: :py:class:`~pyasn1.type.tag.TagSet` object representing ASN.1 tag(s) + #: associated with |ASN.1| type. + tagSet = tag.initTagSet( + tag.Tag(tag.tagClassUniversal, tag.tagFormatConstructed, 0x11) + ) + + #: Default collection of ASN.1 types of component (e.g. :py:class:`~pyasn1.type.namedtype.NamedType`) + #: object representing ASN.1 type allowed within |ASN.1| type + componentType = namedtype.NamedTypes() + + #: Set (on class, not on instance) or return a + #: :py:class:`~pyasn1.type.constraint.ConstraintsIntersection` object + #: imposing constraints on |ASN.1| type initialization values. + subtypeSpec = constraint.ConstraintsIntersection() + + # Disambiguation ASN.1 types identification + typeId = SequenceAndSetBase.getTypeId() + + def getComponent(self, innerFlag=False): + return self + + def getComponentByType(self, tagSet, default=noValue, + instantiate=True, innerFlag=False): + """Returns |ASN.1| type component by ASN.1 tag. + + Parameters + ---------- + tagSet : :py:class:`~pyasn1.type.tag.TagSet` + Object representing ASN.1 tags to identify one of + |ASN.1| object component + + Keyword Args + ------------ + default: :class:`object` + If set and requested component is a schema object, return the `default` + object instead of the requested component. + + instantiate: :class:`bool` + If :obj:`True` (default), inner component will be automatically + instantiated. + If :obj:`False` either existing component or the :class:`noValue` + object will be returned. + + Returns + ------- + : :py:class:`~pyasn1.type.base.PyAsn1Item` + a pyasn1 object + """ + componentValue = self.getComponentByPosition( + self.componentType.getPositionByType(tagSet), + default=default, instantiate=instantiate + ) + if innerFlag and isinstance(componentValue, Set): + # get inner component by inner tagSet + return componentValue.getComponent(innerFlag=True) + else: + # get outer component by inner tagSet + return componentValue + + def setComponentByType(self, tagSet, value=noValue, + verifyConstraints=True, + matchTags=True, + matchConstraints=True, + innerFlag=False): + """Assign |ASN.1| type component by ASN.1 tag. + + Parameters + ---------- + tagSet : :py:class:`~pyasn1.type.tag.TagSet` + Object representing ASN.1 tags to identify one of + |ASN.1| object component + + Keyword Args + ------------ + value: :class:`object` or :py:class:`~pyasn1.type.base.PyAsn1Item` derivative + A Python value to initialize |ASN.1| component with (if *componentType* is set) + or ASN.1 value object to assign to |ASN.1| component. + If `value` is not given, schema object will be set as a component. + + verifyConstraints : :class:`bool` + If :obj:`False`, skip constraints validation + + matchTags: :class:`bool` + If :obj:`False`, skip component tags matching + + matchConstraints: :class:`bool` + If :obj:`False`, skip component constraints matching + + innerFlag: :class:`bool` + If :obj:`True`, search for matching *tagSet* recursively. + + Returns + ------- + self + """ + idx = self.componentType.getPositionByType(tagSet) + + if innerFlag: # set inner component by inner tagSet + componentType = self.componentType.getTypeByPosition(idx) + + if componentType.tagSet: + return self.setComponentByPosition( + idx, value, verifyConstraints, matchTags, matchConstraints + ) + else: + componentType = self.getComponentByPosition(idx) + return componentType.setComponentByType( + tagSet, value, verifyConstraints, matchTags, matchConstraints, innerFlag=innerFlag + ) + else: # set outer component by inner tagSet + return self.setComponentByPosition( + idx, value, verifyConstraints, matchTags, matchConstraints + ) + + @property + def componentTagMap(self): + if self.componentType: + return self.componentType.tagMapUnique + + +class Choice(Set): + """Create |ASN.1| schema or value object. + + |ASN.1| class is based on :class:`~pyasn1.type.base.ConstructedAsn1Type`, + its objects are mutable and duck-type Python :class:`list` objects. + + Keyword Args + ------------ + componentType: :py:class:`~pyasn1.type.namedtype.NamedType` + Object holding named ASN.1 types allowed within this collection + + tagSet: :py:class:`~pyasn1.type.tag.TagSet` + Object representing non-default ASN.1 tag(s) + + subtypeSpec: :py:class:`~pyasn1.type.constraint.ConstraintsIntersection` + Object representing non-default ASN.1 subtype constraint(s). Constraints + verification for |ASN.1| type can only occur on explicit + `.isInconsistent` call. + + Examples + -------- + + .. code-block:: python + + class Afters(Choice): + ''' + ASN.1 specification: + + Afters ::= CHOICE { + cheese [0] IA5String, + dessert [1] IA5String + } + ''' + componentType = NamedTypes( + NamedType('cheese', IA5String().subtype( + implicitTag=Tag(tagClassContext, tagFormatSimple, 0) + ), + NamedType('dessert', IA5String().subtype( + implicitTag=Tag(tagClassContext, tagFormatSimple, 1) + ) + ) + + afters = Afters() + afters['cheese'] = 'Mascarpone' + """ + #: Set (on class, not on instance) or return a + #: :py:class:`~pyasn1.type.tag.TagSet` object representing ASN.1 tag(s) + #: associated with |ASN.1| type. + tagSet = tag.TagSet() # untagged + + #: Default collection of ASN.1 types of component (e.g. :py:class:`~pyasn1.type.namedtype.NamedType`) + #: object representing ASN.1 type allowed within |ASN.1| type + componentType = namedtype.NamedTypes() + + #: Set (on class, not on instance) or return a + #: :py:class:`~pyasn1.type.constraint.ConstraintsIntersection` object + #: imposing constraints on |ASN.1| type initialization values. + subtypeSpec = constraint.ConstraintsIntersection( + constraint.ValueSizeConstraint(1, 1) + ) + + # Disambiguation ASN.1 types identification + typeId = Set.getTypeId() + + _currentIdx = None + + def __eq__(self, other): + if self._componentValues: + return self._componentValues[self._currentIdx] == other + return NotImplemented + + def __ne__(self, other): + if self._componentValues: + return self._componentValues[self._currentIdx] != other + return NotImplemented + + def __lt__(self, other): + if self._componentValues: + return self._componentValues[self._currentIdx] < other + return NotImplemented + + def __le__(self, other): + if self._componentValues: + return self._componentValues[self._currentIdx] <= other + return NotImplemented + + def __gt__(self, other): + if self._componentValues: + return self._componentValues[self._currentIdx] > other + return NotImplemented + + def __ge__(self, other): + if self._componentValues: + return self._componentValues[self._currentIdx] >= other + return NotImplemented + + def __bool__(self): + return bool(self._componentValues) + + def __len__(self): + return self._currentIdx is not None and 1 or 0 + + def __contains__(self, key): + if self._currentIdx is None: + return False + return key == self.componentType[self._currentIdx].getName() + + def __iter__(self): + if self._currentIdx is None: + raise StopIteration + yield self.componentType[self._currentIdx].getName() + + # Python dict protocol + + def values(self): + if self._currentIdx is not None: + yield self._componentValues[self._currentIdx] + + def keys(self): + if self._currentIdx is not None: + yield self.componentType[self._currentIdx].getName() + + def items(self): + if self._currentIdx is not None: + yield self.componentType[self._currentIdx].getName(), self[self._currentIdx] + + def checkConsistency(self): + if self._currentIdx is None: + raise error.PyAsn1Error('Component not chosen') + + def _cloneComponentValues(self, myClone, cloneValueFlag): + try: + component = self.getComponent() + except error.PyAsn1Error: + pass + else: + if isinstance(component, Choice): + tagSet = component.effectiveTagSet + else: + tagSet = component.tagSet + if isinstance(component, base.ConstructedAsn1Type): + myClone.setComponentByType( + tagSet, component.clone(cloneValueFlag=cloneValueFlag) + ) + else: + myClone.setComponentByType(tagSet, component.clone()) + + def getComponentByPosition(self, idx, default=noValue, instantiate=True): + __doc__ = Set.__doc__ + + if self._currentIdx is None or self._currentIdx != idx: + return Set.getComponentByPosition(self, idx, default=default, + instantiate=instantiate) + + return self._componentValues[idx] + + def setComponentByPosition(self, idx, value=noValue, + verifyConstraints=True, + matchTags=True, + matchConstraints=True): + """Assign |ASN.1| type component by position. + + Equivalent to Python sequence item assignment operation (e.g. `[]`). + + Parameters + ---------- + idx: :class:`int` + Component index (zero-based). Must either refer to existing + component or to N+1 component. In the latter case a new component + type gets instantiated (if *componentType* is set, or given ASN.1 + object is taken otherwise) and appended to the |ASN.1| sequence. + + Keyword Args + ------------ + value: :class:`object` or :py:class:`~pyasn1.type.base.PyAsn1Item` derivative + A Python value to initialize |ASN.1| component with (if *componentType* is set) + or ASN.1 value object to assign to |ASN.1| component. Once a new value is + set to *idx* component, previous value is dropped. + If `value` is not given, schema object will be set as a component. + + verifyConstraints : :class:`bool` + If :obj:`False`, skip constraints validation + + matchTags: :class:`bool` + If :obj:`False`, skip component tags matching + + matchConstraints: :class:`bool` + If :obj:`False`, skip component constraints matching + + Returns + ------- + self + """ + oldIdx = self._currentIdx + Set.setComponentByPosition(self, idx, value, verifyConstraints, matchTags, matchConstraints) + self._currentIdx = idx + if oldIdx is not None and oldIdx != idx: + self._componentValues[oldIdx] = noValue + return self + + @property + def effectiveTagSet(self): + """Return a :class:`~pyasn1.type.tag.TagSet` object of the currently initialized component or self (if |ASN.1| is tagged).""" + if self.tagSet: + return self.tagSet + else: + component = self.getComponent() + return component.effectiveTagSet + + @property + def tagMap(self): + """"Return a :class:`~pyasn1.type.tagmap.TagMap` object mapping + ASN.1 tags to ASN.1 objects contained within callee. + """ + if self.tagSet: + return Set.tagMap.fget(self) + else: + return self.componentType.tagMapUnique + + def getComponent(self, innerFlag=False): + """Return currently assigned component of the |ASN.1| object. + + Returns + ------- + : :py:class:`~pyasn1.type.base.PyAsn1Item` + a PyASN1 object + """ + if self._currentIdx is None: + raise error.PyAsn1Error('Component not chosen') + else: + c = self._componentValues[self._currentIdx] + if innerFlag and isinstance(c, Choice): + return c.getComponent(innerFlag) + else: + return c + + def getName(self, innerFlag=False): + """Return the name of currently assigned component of the |ASN.1| object. + + Returns + ------- + : :py:class:`str` + |ASN.1| component name + """ + if self._currentIdx is None: + raise error.PyAsn1Error('Component not chosen') + else: + if innerFlag: + c = self._componentValues[self._currentIdx] + if isinstance(c, Choice): + return c.getName(innerFlag) + return self.componentType.getNameByPosition(self._currentIdx) + + @property + def isValue(self): + """Indicate that |ASN.1| object represents ASN.1 value. + + If *isValue* is :obj:`False` then this object represents just ASN.1 schema. + + If *isValue* is :obj:`True` then, in addition to its ASN.1 schema features, + this object can also be used like a Python built-in object (e.g. + :class:`int`, :class:`str`, :class:`dict` etc.). + + Returns + ------- + : :class:`bool` + :obj:`False` if object represents just ASN.1 schema. + :obj:`True` if object represents ASN.1 schema and can be used as a normal + value. + + Note + ---- + There is an important distinction between PyASN1 schema and value objects. + The PyASN1 schema objects can only participate in ASN.1 schema-related + operations (e.g. defining or testing the structure of the data). Most + obvious uses of ASN.1 schema is to guide serialisation codecs whilst + encoding/decoding serialised ASN.1 contents. + + The PyASN1 value objects can **additionally** participate in many operations + involving regular Python objects (e.g. arithmetic, comprehension etc). + """ + if self._currentIdx is None: + return False + + componentValue = self._componentValues[self._currentIdx] + + return componentValue is not noValue and componentValue.isValue + + def clear(self): + self._currentIdx = None + return Set.clear(self) + + # compatibility stubs + + def getMinTagSet(self): + return self.minTagSet + + +class Any(OctetString): + """Create |ASN.1| schema or value object. + + |ASN.1| class is based on :class:`~pyasn1.type.base.SimpleAsn1Type`, + its objects are immutable and duck-type :class:`bytes`. + When used in Unicode context, |ASN.1| type assumes + "|encoding|" serialisation. + + Keyword Args + ------------ + value: :class:`unicode`, :class:`str`, :class:`bytes` or |ASN.1| object + :class:`bytes`, alternatively :class:`str` + representing character string to be serialised into octets (note + `encoding` parameter) or |ASN.1| object. + If `value` is not given, schema object will be created. + + tagSet: :py:class:`~pyasn1.type.tag.TagSet` + Object representing non-default ASN.1 tag(s) + + subtypeSpec: :py:class:`~pyasn1.type.constraint.ConstraintsIntersection` + Object representing non-default ASN.1 subtype constraint(s). Constraints + verification for |ASN.1| type occurs automatically on object + instantiation. + + encoding: :py:class:`str` + Unicode codec ID to encode/decode + :class:`str` the payload when |ASN.1| object is used + in text string context. + + binValue: :py:class:`str` + Binary string initializer to use instead of the *value*. + Example: '10110011'. + + hexValue: :py:class:`str` + Hexadecimal string initializer to use instead of the *value*. + Example: 'DEADBEEF'. + + Raises + ------ + ~pyasn1.error.ValueConstraintError, ~pyasn1.error.PyAsn1Error + On constraint violation or bad initializer. + + Examples + -------- + .. code-block:: python + + class Error(Sequence): + ''' + ASN.1 specification: + + Error ::= SEQUENCE { + code INTEGER, + parameter ANY DEFINED BY code -- Either INTEGER or REAL + } + ''' + componentType=NamedTypes( + NamedType('code', Integer()), + NamedType('parameter', Any(), + openType=OpenType('code', {1: Integer(), + 2: Real()})) + ) + + error = Error() + error['code'] = 1 + error['parameter'] = Integer(1234) + """ + #: Set (on class, not on instance) or return a + #: :py:class:`~pyasn1.type.tag.TagSet` object representing ASN.1 tag(s) + #: associated with |ASN.1| type. + tagSet = tag.TagSet() # untagged + + #: Set (on class, not on instance) or return a + #: :py:class:`~pyasn1.type.constraint.ConstraintsIntersection` object + #: imposing constraints on |ASN.1| type initialization values. + subtypeSpec = constraint.ConstraintsIntersection() + + # Disambiguation ASN.1 types identification + typeId = OctetString.getTypeId() + + @property + def tagMap(self): + """"Return a :class:`~pyasn1.type.tagmap.TagMap` object mapping + ASN.1 tags to ASN.1 objects contained within callee. + """ + try: + return self._tagMap + + except AttributeError: + self._tagMap = tagmap.TagMap( + {self.tagSet: self}, + {eoo.endOfOctets.tagSet: eoo.endOfOctets}, + self + ) + + return self._tagMap + +# XXX +# coercion rules? diff --git a/module/scripts/lib/pyasn1/type/useful.py b/module/scripts/lib/pyasn1/type/useful.py new file mode 100644 index 0000000..49c00f5 --- /dev/null +++ b/module/scripts/lib/pyasn1/type/useful.py @@ -0,0 +1,190 @@ +# +# This file is part of pyasn1 software. +# +# Copyright (c) 2005-2020, Ilya Etingof +# License: https://pyasn1.readthedocs.io/en/latest/license.html +# +import datetime + +from pyasn1 import error +from pyasn1.type import char +from pyasn1.type import tag +from pyasn1.type import univ + +__all__ = ['ObjectDescriptor', 'GeneralizedTime', 'UTCTime'] + +NoValue = univ.NoValue +noValue = univ.noValue + + +class ObjectDescriptor(char.GraphicString): + __doc__ = char.GraphicString.__doc__ + + #: Default :py:class:`~pyasn1.type.tag.TagSet` object for |ASN.1| objects + tagSet = char.GraphicString.tagSet.tagImplicitly( + tag.Tag(tag.tagClassUniversal, tag.tagFormatSimple, 7) + ) + + # Optimization for faster codec lookup + typeId = char.GraphicString.getTypeId() + + +class TimeMixIn(object): + + _yearsDigits = 4 + _hasSubsecond = False + _optionalMinutes = False + _shortTZ = False + + class FixedOffset(datetime.tzinfo): + """Fixed offset in minutes east from UTC.""" + + # defaulted arguments required + # https: // docs.python.org / 2.3 / lib / datetime - tzinfo.html + def __init__(self, offset=0, name='UTC'): + self.__offset = datetime.timedelta(minutes=offset) + self.__name = name + + def utcoffset(self, dt): + return self.__offset + + def tzname(self, dt): + return self.__name + + def dst(self, dt): + return datetime.timedelta(0) + + UTC = FixedOffset() + + @property + def asDateTime(self): + """Create :py:class:`datetime.datetime` object from a |ASN.1| object. + + Returns + ------- + : + new instance of :py:class:`datetime.datetime` object + """ + text = str(self) + if text.endswith('Z'): + tzinfo = TimeMixIn.UTC + text = text[:-1] + + elif '-' in text or '+' in text: + if '+' in text: + text, plusminus, tz = text.partition('+') + else: + text, plusminus, tz = text.partition('-') + + if self._shortTZ and len(tz) == 2: + tz += '00' + + if len(tz) != 4: + raise error.PyAsn1Error('malformed time zone offset %s' % tz) + + try: + minutes = int(tz[:2]) * 60 + int(tz[2:]) + if plusminus == '-': + minutes *= -1 + + except ValueError: + raise error.PyAsn1Error('unknown time specification %s' % self) + + tzinfo = TimeMixIn.FixedOffset(minutes, '?') + + else: + tzinfo = None + + if '.' in text or ',' in text: + if '.' in text: + text, _, ms = text.partition('.') + else: + text, _, ms = text.partition(',') + + try: + # Normalize variable-length fraction to microseconds + ms = int(ms.ljust(6, '0')[:6]) + + except ValueError: + raise error.PyAsn1Error('bad sub-second time specification %s' % self) + + else: + ms = 0 + + if self._optionalMinutes and len(text) - self._yearsDigits == 6: + text += '0000' + elif len(text) - self._yearsDigits == 8: + text += '00' + + try: + dt = datetime.datetime.strptime(text, self._yearsDigits == 4 and '%Y%m%d%H%M%S' or '%y%m%d%H%M%S') + + except ValueError: + raise error.PyAsn1Error('malformed datetime format %s' % self) + + return dt.replace(microsecond=ms, tzinfo=tzinfo) + + @classmethod + def fromDateTime(cls, dt): + """Create |ASN.1| object from a :py:class:`datetime.datetime` object. + + Parameters + ---------- + dt: :py:class:`datetime.datetime` object + The `datetime.datetime` object to initialize the |ASN.1| object + from + + Returns + ------- + : + new instance of |ASN.1| value + """ + text = dt.strftime(cls._yearsDigits == 4 and '%Y%m%d%H%M%S' or '%y%m%d%H%M%S') + if cls._hasSubsecond and dt.microsecond: + text += ('.%06d' % dt.microsecond).rstrip('0') + + if dt.utcoffset(): + seconds = dt.utcoffset().seconds + if seconds < 0: + text += '-' + else: + text += '+' + text += '%.2d%.2d' % (seconds // 3600, seconds % 3600) + else: + text += 'Z' + + return cls(text) + + +class GeneralizedTime(char.VisibleString, TimeMixIn): + __doc__ = char.VisibleString.__doc__ + + #: Default :py:class:`~pyasn1.type.tag.TagSet` object for |ASN.1| objects + tagSet = char.VisibleString.tagSet.tagImplicitly( + tag.Tag(tag.tagClassUniversal, tag.tagFormatSimple, 24) + ) + + # Optimization for faster codec lookup + typeId = char.VideotexString.getTypeId() + + _yearsDigits = 4 + _hasSubsecond = True + _optionalMinutes = True + _shortTZ = True + + +class UTCTime(char.VisibleString, TimeMixIn): + __doc__ = char.VisibleString.__doc__ + + #: Default :py:class:`~pyasn1.type.tag.TagSet` object for |ASN.1| objects + tagSet = char.VisibleString.tagSet.tagImplicitly( + tag.Tag(tag.tagClassUniversal, tag.tagFormatSimple, 23) + ) + + # Optimization for faster codec lookup + typeId = char.VideotexString.getTypeId() + + _yearsDigits = 2 + _hasSubsecond = False + _optionalMinutes = False + _shortTZ = False diff --git a/module/scripts/lib/rsa/__init__.py b/module/scripts/lib/rsa/__init__.py new file mode 100644 index 0000000..e768907 --- /dev/null +++ b/module/scripts/lib/rsa/__init__.py @@ -0,0 +1,60 @@ +# Copyright 2011 Sybren A. Stüvel +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# https://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +"""RSA module + +Module for calculating large primes, and RSA encryption, decryption, signing +and verification. Includes generating public and private keys. + +WARNING: this implementation does not use compression of the cleartext input to +prevent repetitions, or other common security improvements. Use with care. + +""" + +from rsa.key import newkeys, PrivateKey, PublicKey +from rsa.pkcs1 import ( + encrypt, + decrypt, + sign, + verify, + DecryptionError, + VerificationError, + find_signature_hash, + sign_hash, + compute_hash, +) + +__author__ = "Sybren Stuvel, Barry Mead and Yesudeep Mangalapilly" +__date__ = "2025-04-16" +__version__ = "4.9.1" + +# Do doctest if we're run directly +if __name__ == "__main__": + import doctest + + doctest.testmod() + +__all__ = [ + "newkeys", + "encrypt", + "decrypt", + "sign", + "verify", + "PublicKey", + "PrivateKey", + "DecryptionError", + "VerificationError", + "find_signature_hash", + "compute_hash", + "sign_hash", +] diff --git a/module/scripts/lib/rsa/__pycache__/__init__.cpython-314.pyc b/module/scripts/lib/rsa/__pycache__/__init__.cpython-314.pyc new file mode 100644 index 0000000000000000000000000000000000000000..09569bbf137185b4fed79000df14ab782ef7005c GIT binary patch literal 1188 zcmZuwOK%)S5boI>@9fS#LSh^qvgAfwSYglDj)MRpq=cZ%3i-iU1UQ6Nd!~1{@x0Vs zvtfL)a)DpKf8eKZM9Lu=E=X|45FuZwo^=R9%;B5r>Z-4*K2^Ux8oC6(-+ui4>-!x- zel>^ZU+c|Gr; zh)2Tip#^K9^ClVh_-%`j@c0f*^SDfe>+UyyI?WZ0coLNf2bs-i!qrUBLdjIkmGD4a>^XMdtk}?*W&O|2ElDjBR5*an5SYW|H z&{I)q&vie)6CO+s?!QlAE;W^Dk%&}e&~VW>N17}LT53U~JS~*a8jH|;it*@51Xm!w z0V=bi1PZx}Aq&i>N)!U5=?I6xNppIbSYnizr;*Tjw|*kKmo3+7T@r-9 z3|zcQMju({?$iD&=iR5Hwe$W(huDMf&YrYi``Nzni{1bB@bTdjd+kqq*l#^{)a%Il EH_bI)pa1{> literal 0 HcmV?d00001 diff --git a/module/scripts/lib/rsa/__pycache__/asn1.cpython-314.pyc b/module/scripts/lib/rsa/__pycache__/asn1.cpython-314.pyc new file mode 100644 index 0000000000000000000000000000000000000000..ebdb8c7120d086ab606f0c15d1926697d7fb96fd GIT binary patch literal 2279 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 literal 0 HcmV?d00001 diff --git a/module/scripts/lib/rsa/__pycache__/core.cpython-314.pyc b/module/scripts/lib/rsa/__pycache__/core.cpython-314.pyc new file mode 100644 index 0000000000000000000000000000000000000000..20564de53a905372f0f1bf2e368b8faeb1354461 GIT binary patch literal 2263 zcmcgtO>7%Q6rSCk*p8hnqzNsMP=;DfYDlt(<`AKRNYjcQAZeD!k&uP8T~F+7?A>K% zonWI#KJ?II4mtFYLl3=F>amAPJ11-b!qzv5K3Y=Mdf*>%Y!FN0ZtbwxV z8+G;$LIu!`p(UlHm`V;T5t(YP9XK*I6B`R$LO?Q3kULf%XK}-70u^a z3N}6qA0G$SKrawh7L_B(x91P#xWl9Ieh}%8VJ5OWLBpRu_+-LvgU~>^b|N8l-Q>)) zR!qPgK~XwQltwwRUQ$;1SQ!A+WYJA=2``bbeWcB_yqA=Wx=qw$j`Dm-y&U@zLKB`ayE|>BtlQ!{*b?=E#@3$%Q6fU<0t`1hf*M zkeRvx!oPqP%!D-Ck%Y zrh2MgHMOoDY7fnx0kA+O$55`bqlvpPSW#!UVaAA@(SefLx1wHFCwpMjU6B(|khG#? z)tQ!8NMx-3*>%@uHK-e6kybiM6);pJG%`RtB9L7&A#ywcy~bl}!Yr%KBQ6^T#|#dG z*c-s=6N{n;xt=@a(jxSybxPc7pYK?J#Y?P zg1DbtbTU}NFid8xa8HHFW3uef`bmTUU4W`DS7sR6;S@ z9GiQ#^6ZPB$8PTGOU=a6K}rWl)^3V4G5McMA!*nA5>zhzE(mb}Zc1K;tOq8F_*K;~ z1@=xgB$VMJ1Z)TJ55oiKg)lsdhR0mmUQrFJGO4`{@xN3<7yrGu^|h>cb8!8}HQK>$ z6h9FOXyI2poP}sY2g4#Wr(swCf}IykU>Mq?XnyQ%A!ByZD3Qi7m;hfSYMZInJ`!LA zOfsdHL1+W4e(%a2zH}PE4)md|_wUc`>f?>ie@&ffL``%7dU-sbFL*Y*S@JLfw@mnc th-ChwofB80*Y8u729tOxxH$EprYOqMd92{)x_IE8>(J6(5V%RX_8;!e+*1Gm literal 0 HcmV?d00001 diff --git a/module/scripts/lib/rsa/__pycache__/key.cpython-314.pyc b/module/scripts/lib/rsa/__pycache__/key.cpython-314.pyc new file mode 100644 index 0000000000000000000000000000000000000000..8f39dda3bc696ad1bac095fbcd21b3c021d2aeb9 GIT binary patch literal 41767 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 H3E}?-)QaQv literal 0 HcmV?d00001 diff --git a/module/scripts/lib/rsa/__pycache__/parallel.cpython-314.pyc b/module/scripts/lib/rsa/__pycache__/parallel.cpython-314.pyc new file mode 100644 index 0000000000000000000000000000000000000000..1998e3a37461eaf68c2c95478c77bbce96f9265b GIT binary patch literal 2987 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 literal 0 HcmV?d00001 diff --git a/module/scripts/lib/rsa/__pycache__/pkcs1.cpython-314.pyc b/module/scripts/lib/rsa/__pycache__/pkcs1.cpython-314.pyc new file mode 100644 index 0000000000000000000000000000000000000000..16183d63d07a229e99b595514e61762b5cc26fea GIT binary patch literal 18695 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 literal 0 HcmV?d00001 diff --git a/module/scripts/lib/rsa/__pycache__/prime.cpython-314.pyc b/module/scripts/lib/rsa/__pycache__/prime.cpython-314.pyc new file mode 100644 index 0000000000000000000000000000000000000000..72fa140eebf63a9ac99f39d62ba00327c3fb7429 GIT binary patch literal 5680 zcmb_gUr-yz8Q;?%5GMgb;5atKUP8bKgM@%Mm?WN2?Kp9W9W1*z?HJTUI^b;S?!0#b zL#HVZeozt|D-$PWI_}h+rqg)F)5$~n(&7+ZO)!yEBx8HvI+u!%Q-P=$v5-4B&^`B?HZYJbQyl{)(X>5!Mge;L1G07xJ2}cCe z!~eaeC*?_bV;<9&@@*$6|8|lKoBmV{v}<|0CKZ5o9dFmBL}&+jJCF)NyPmh}QVq~< zxm^5jSwGPL{Smi6Ob)+Hh}k$%KSsIXrmCF` zIoI=MD)RA4bRZFEmC3HXxJ4kff;>BZyyv&r0l%vxHHDfB8YEUNUBvZ~(Q=>Xl z^0E;V#Upt=C+B6=l+cDwNd_1|!Sj9kTHpR7+%>OnQbdz`F5>9T#SWlyDmE*d^tuYI`f~xEJE!yASI^EJ3$~ zb5;$L%>q-e36@t;&8T42OlvGJnFfygq?L_lB~ywQ44K9I;~6a(%A76e@w}ETLU!+c`U&m^Ba1H-1+kaOEdFKWgkqpkrEf^*E z`8Il>0g(~_Tk{a95Mu8~;c=lFKP)^NArMW7i?Dbo#e-q+b)I1|_+yw9W}Sz%&nk0@ zp)smWQytfpo)g6b2M*AgOtyPyxR)k|`=erFK)gnx9)_#MYN0aFv#u@%gA>aOwyX(S ze0n>eu|(DyNHA19x7@WFX#-sGgXj@Sk0jPXcp+1vP6>Ks#*QTux?vDQQYkXE+H+IRG=o)rX#5vAqAC$EOt2 zP!?q%iex6Eu`E6Y8~oUj6DR56BV#833;X(pdTCPEWi_jurw4ms_fO?ydN4NFOCNDY zbesXVj`pR>3bLF+0O<+=v_!p@7kI*|v3H;q zfElGR@UX#J04Y|jJ4L?9sJ+SBfK)ku@R-WWDL+dEZIE#WNR zr`J3TC4=RI%#-`J{h9=;GHDJ7;dKJl2bneeyKet|oaLRi|K)MvIpiu%nty^BIq3z6 z(M~UzK7nt9UM^k05{eFzME8z+px2E7^kQhS%xpul9rwarq;sb@_K&&O_?FQQLQa?8 zFfO>8A4JAwxbHh%_Hqnx@o*VKzaJ{#skhow_l@97m8I@Kjr_;UsT7b75*HM`X1r!n z;9>+QxQ(3p$>w=;u>`hlWHhzObDV2ceB4oC9((yb%A>*iJzI^J1u{f&w42M7t;Thv z5aczGJMY+X$F@e{lLDTPdL<78?4Xp(6j0U}bbeL_)xxMs6$4Y5t{I9c)9(3M)F;p< z<>sXYyv3NDF{6|d4d@`zF@dKw5DBQ7atru?qB^#89u&|P+%(kF({~m)X=*_PJ&;>K z607FeM=2JV^e~LeWX@k1ndet!vj~&VOW4s%4Imj2NyY${k=59|a#qphtRlrUHWSCI zxIJ3u3j>|Q{hj*Y`X|X5E$NlSwm7ln;dy)|de<3Fn}{Km(M` zlO#}AOk613ZERT!kUFuLyy#y_t_JFslJ^?JrNpJeasjT2$1m0t1#CzcYu^;UsUZ!` zrNrB>6@{-tjh9B>7`+qPeKWNCs&F;=JO8av*N07?hI;tTl2DS~dIg>hhDyTnzS8j( z=~tbXe|$I8@>SDgrE@EZ_X_W~6_cNbn@dT!Y=1XYOs>Mn$>q5(x3@uSMR@Opw-^5& z-nQOIBD=nBAuT)BJ4wT1>)j+2ew5^qfRH9OzM}*yQx{4w248!G?SkstID8K7S81;O zsIvB7K7pER|CN*cs^S;e@`5S~{PJz!)117;VI*(Jra;0#44CX33NKS_6Ss#q2#&n)78dpD{VI$J8uL!*J?<8bLse{mzQ4#Yy)8HIP&<%0;R)+-13 zRmDqpFbpC5d>gMrgG0(Y4Efm3K*!0f(E2^4{poV%uH^hZ&gf2_8zdn2Va~$hZ5LXS zO?ED<%vlM(&p;ppZ5t+}sVp!m-NS>khhm5vZ0YoQ>V9l@CkBS2n^C0q0gSdlm%bGe z^}S_d{+4N`t}*=!m(nu8HAJAekY(1B^H8n+BLAg}l;+oE=jl2B7AvAq9=ucf&Ep4jktWZN(H zzO(mEByls6_^9(%WVl$1NNp*-cqiEQNw96Td1vuh1q3cszt5RNh5PuK1K*ge8|NWR z*|`00RCpbX%W^g-MAivZR{*#0_ksO){Sfc8Jui<4&pDgSw}GLBuTXbJyAx8g{k@JC3@aWmO!n|y(?aSX*}Vm+mM6zI_AMRd zQL+N*bY6n=onG9NDyS-Iv#gfEuOx%Qr)2+m6o3Ud(t$HFNy!zMto#FBz8L6KsENm8 zi}idwQ(PpbQorYJW)0POkAZBCU79BzCC<_lb<;msXfCuMPp=6G68^<^abWoM@ za1dnqA!UG0sdfBg?ReXtfe&}+M33QI$qG1L8kV0=f`#$zczR-smcVoN>C*xB0`wsc zjNd}F<`o2CeV0%0ZG?yr`HV#F1-ISU-giTcE!94#fn%lzkHay?gCY2KdT>U7?E7E{ z`0~LpfN!mh_(H|SPrN(s`s!|ogP-_@KJ!Js0Lfv#-&*vqwsrja)nC4PtF7;5U`O%s z($jwnHs1|3-3x|GgBPD&X}=w8{k{J;e&{OotQ0_oWd2^`nV6R@0@=mzyl*e9^6Y( z{#_&$*hLCWJd_GT4oSHo)c`px<#4JIvMS}ql(LT`BJP+K? zIZ2sj=By$$%f$(u*?pr4pA*Vj#iC_8jU1CU^ov;qKPWy?0{Hy+){mg7kTe;i8-#@3B?JC zQ#EdjGrOF8GN*AZS+;eSJd(uU^=XrH$-Gg}?PTAiUNlT2shQlEWZJaB=MHP7Qqs=i zg4wBL`oqI#wCweADOt2~<$^9Ij1sq#1!E$~Y%M7afZO+#=A39IqnW0~HLhneEQYgp zkfVXt>-i;g$e36iD*-c8c}UT*>|{?iZ2`Q~q?bS-DogA(Y29?t~oeX4pcPvtl@ zZ1W(e8l9Q43VNM|)XGyygkGwdRG$S6WuML%g#w+>De%?JoSwq~pHeNeFh}{6P7BtI z&Zszn+B)x39py|S`zq(xLL?kp`1ID?)7ITfT@QM`>{-72)!|xe_d;YfyrZgA zhre!I33o0;p2pknU%Pv4dB^hgT72N~)s^_^1@&1Z{v;4*+i=u>@>Q&Ni||z@?)XSy zjc5`Ii=c5GQeIj5hphU`g*Hg4%pY}0s(JK85Yi_>A}|mnW7e#JD5Qg5E?Xsg z6Ml&;6MH+2k}U&Rhhae=CNOt`E=hz6Na9jt`&Noz0qex9wA~;CFa(U~T7I|I++8{U zG#dXQy7#;2-et8GJyaQ93zML_aQV*hYX27}R$_Zr0(&aM&mv8g3!9ep!c5(=Zyld; zxLYh3x9-#edzlwVbAQCPz(mwlM$!iHdA`?Ku3aos5a-AXrz4y>!UfOA@l`1c6plcV?}sSr?aB+tf{IOHDnU)Ix-BM*4Z2~3SLOuDC(dawM%-| z$QxN|8@!Ci6CP826p74RPEB1$!W)$tNaYr_xyAHenVPZ%3y+sgRl}xG=pLXwJ#;id z57T~EFR$4gdVC5B`TCPhHIOj@y|B9zMhME-#=P#5Hoy*Gly!@#AZLbdGYZ*;ePwoX zBTs|KOCsV3&y#Gp0s#@Hi8Qr-e&hCy>TGR$cV*;RG*%g2RkuBD+PfUCH6<2;tC6Pf zBOR;J=Cu~Ex^iJ{yCC<#;>l|BXQv;O5td@{s;52eX3kW&i|l0&GS1631ff@L;I`cA4}*1Pg#Z8m literal 0 HcmV?d00001 diff --git a/module/scripts/lib/rsa/__pycache__/transform.cpython-314.pyc b/module/scripts/lib/rsa/__pycache__/transform.cpython-314.pyc new file mode 100644 index 0000000000000000000000000000000000000000..6cb9f39fbb5266b311dcb05c74b5f76badee0d80 GIT binary patch literal 2499 zcmai0O>7fK6rQy=v9k%`r%E9xZ3k#f782V@6JjE2piLvCNFlW#?IFY-d&l;Y^{zWR z8z+$}q6(>?9^k+ss*uWo1HJXoLyx`omMVo-+a8e0g;NuSifiA@uALMi^+}$cnfK<+ z`~AkJyZ31b=-JPIUtf7B@#CFRL zs}XKGHZFU1Nle_NrcE)YhKnnlx|lndVB4z|DH{yGVox#$G3BMCHd9jI7V#6L!cVxi zlZIgp&7p-z<|dd6dwu8=rK;qVdEf+;i_Axw^Hk&n(xcDC+V5bbk(YP)?Tm7d*?Pop z3pS#r&2`0Z4@`H(y~q7($^f5Io=aIOohmuCB&n>V%BDr#RI);ArfsH(&CLoUOD10# zBK3O8EiuAN)zsYN&^b&O{6We0A-?Jq?58sok9pEAvCNUAz4nGKqx-$ZkHMAR&SrNQfXbv)$ zi{zr?ArAtju0HwDv6m?0E+*JAU5=e{m@|iAV&lwE(d5|W%(N>qjW&kJ(F$dd;ZLj7 z#-1x!o^6&KgNBkWB-NH{I1Gt)of-{xgsf#QQhfEwR0fw#=5kP!&1Uhyz`*cG2A{$s zV`p@H8lM@}cNfEBT4ppomQIgLS((ETFl3;3a(+3JzA(R>E}zs}GMXPPnw2S$VArhJ z)X*a=2I26vG8AP{6S~4w*pjJFunxiPXz67KU@cS93zIeU;!V`G|4#OHc3ppP_4~0~ z+0D+g8|qos3Eck_ZkQ3Yb55T7-@*l{fO642MI05mVIog{d(bw12b7_ecg82a6M%)lljG78Sy6K67mQ;th_ZT18QB)Yz0#KWJPbZR^vlh_msTUe5fL%4LEU*F z&x0>0a|oL74bg=2%~pk~nnQ!aj*vz}PF0x?D-EFqWnKhZ((E4D=H4-A@mZ0BSEz|R z3-Lh_>X4c@jE2_MBXbJacCTq0=-MOQJT(W^-#l?b-1j<}$X*#eO1_3Gw6I4+KX`Uf zP|YrA!5AKZiW{P~VG>)Pn9#II2Y?S+9M>5IKv|;LqQnsO4=Hq9Y?kqg<6*qA#lbvzjc9~s*TW&Y4EAwqNN=f>Z`lb^F>pgzU8J~941*d{ zMZ*N;;CtCzmLBgu1unc5E;!4pgojonwTS?s-N1z$X@v3vMdH#Szyz>RM$!h*1iy4~ zgqSW3PZSE50msUg)A)!nhZPFAA<(H6nqA+8y~5+5u`nl#kSMQ?YYw9==i$w&Lb}U< zUjQ?XJW#Zp(qa-{sX{~;{oFGlW|C1TIN{xeg494*#j8jpi3pIP?#c? zOTktL3}OxN2A)Sps-VJ)R$Qg0Mp9c%%*a<1*F3;#2>5GCasu}oY#&s;-tKF2qE2K) z{Ai6V`zk!tRbMSp)ABWd_IywUew;fYEJhjhIAYM`$nTbbm=B#bT+z>o1P;*>0y827 zSRd?5q^ZJW4Q-u5@$S3I&DB4|R#f+sx7-US5$&D=9EY(*7C*?v2!s5=@`y8da0m-%HM;ON!` h1*u)PR)3B5{-GXvs&;;L`OC{s)Sm4aQjbc#{{mHodU5~& literal 0 HcmV?d00001 diff --git a/module/scripts/lib/rsa/__pycache__/util.cpython-314.pyc b/module/scripts/lib/rsa/__pycache__/util.cpython-314.pyc new file mode 100644 index 0000000000000000000000000000000000000000..1376afa3dd28d02816fcf7423cd1a05e386d8fdb GIT binary patch literal 3777 zcmbtXU2NOd6}}WniIyzcmSg!RPDI;FEyb~9JKo$T@zAWz?8Hr?va8c>UIkjBZQ3%) zkyH%xA=ZWgX|RXX=z~+Bz*1~L9biCFU_e`7Kpy(ipaEjT$s}B)z<}&!PjitB2Kuyf zNzqblrQMJ#i1+fIbI;Fr&e1*U3kCfM#+P^gJ^yI{p-1G!9iH;y@enKuNI)if2MN?! z%A{cSob`;;CLMM57?Ty~RwOX3DA#Cu1*!+dSu6aeDd%K!nV-ok8B4;rfdj zc}a=7A?X=0QhtAem7Y6XNW0az@Tc25jodoIIV>md!a;Ii46C zP8zZ)^=o2gL7W8!-GGx|d_r5s_35;zD5@!%QaY{If$&!N8WDID&<;9BQOCcvpErh` z`7uyXhCJNC!2*0>9`jyuXv*~$;1oOpJyM2R1CLY0|2;!s1Xl10K7kYJMp%a!eHO1@ z2nhAS8~h)5LqdblD0B%;Lih*eHVX%YmKwZed#X*I0xzABBA)LV!)5Q6z-v7`Nu}%* ze444Z{F?m@iTqU1r3#VS{3ob-B(wr8YxO_yTC+Sp+d%iO7(i!0(`3#ri97X5fuJW(5whza45k=_#@MHH> zie&4a@qJdN+V?QSt8mDToW@KYbm?y~?(P&RT6?_s7DBU4cFm#+=j1M@i^Uu@b=W)m6^d#_YMPz)id=<7!))8mVF_FDv<9?MDydQ6F-nH4DEBok;dX(IFJIKWC`d z)=tugkW76)A3?z3i+5Y2f+S`QUgR}hz6|#czaTC1qLSs+ys70)gE!|SKBMZoWN50A zm6cgu%TMRz3}H-Ik-P!7*a`lKrmM63uT=ZGHGXaV?zzPBKU2-QU5kh{6`ckT_pEJ| za|iQT$;jyPw3Ov#g-=bq@10xQ=>`JtTF51{usko#=PXJh}L<6w$>K+wD6xa$~2lhI%)kPvz#sJuD zhxmSgV@Ay9Og?qyJf`6hXLNE0>0x+aUuI5~Gm?RQWj3qMHqjRBBQ}BttNUrmuuTAY zO0?BZP_eV2DsOCzEQSQa@|A6aZiykDV~;+4!RmYFu`;v@f5a%C#H?s!WZ8-kbe>K; zf)w^FOIR zWu4sen1$j{O#J{^Lz-kIUB`^7NeX6kfC00%@z^UV85M|dAHkXFWe6#zp})g40E+!7 z2dN3N&Z%NHtu17XL0p%z>xpbr3Z^9NH^j@5L)6@a63QVk1wGm-X)D373K$HqYrQp2jLUzyJ~kN{xq0jV;gMX)XmLrQl!D(+%0Y^GWgqzg*Udnhznf3a`j5#$~RoFbYk>&;%|wsdhQ*)dvv4z&4*0~ z);d-@{=?FBe&S{Ly`HtiYU1XPioxFd!QSvL3#$w7FTOjrL!(e^BiQ@Ii|Rrf zp_89yZqI)){~$De*SKfhu^w>0d>C$7i?7CsgspIQF&y6v$Jcwe!b9)+{uyX^&-aJX z56|2WbZsyLe;!`<{b_WI8Q2JPfxO1%>r2;`iVaiyp=e9y4g)#82vDG(`{V!_yoLo)b94-c9_k*$Zu~L2W_4C)x--z9>?;%dd zH-oXV(=UDg%I%3SCLV;w?q=@I->^p;`{g=t6i5PolAUtu3{W=zyc&YB|Q-Rllb#Wi|rPEn8 z1AmctlAjz@uu}teQTw3c5|HM49Ef(N>ZCc9@Gk$g=c*%b*`{;iGm@pwg literal 0 HcmV?d00001 diff --git a/module/scripts/lib/rsa/asn1.py b/module/scripts/lib/rsa/asn1.py new file mode 100644 index 0000000..4cc4dd3 --- /dev/null +++ b/module/scripts/lib/rsa/asn1.py @@ -0,0 +1,52 @@ +# Copyright 2011 Sybren A. Stüvel +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# https://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +"""ASN.1 definitions. + +Not all ASN.1-handling code use these definitions, but when it does, they should be here. +""" + +from pyasn1.type import univ, namedtype, tag + + +class PubKeyHeader(univ.Sequence): + componentType = namedtype.NamedTypes( + namedtype.NamedType("oid", univ.ObjectIdentifier()), + namedtype.NamedType("parameters", univ.Null()), + ) + + +class OpenSSLPubKey(univ.Sequence): + componentType = namedtype.NamedTypes( + namedtype.NamedType("header", PubKeyHeader()), + # This little hack (the implicit tag) allows us to get a Bit String as Octet String + namedtype.NamedType( + "key", + univ.OctetString().subtype(implicitTag=tag.Tag(tagClass=0, tagFormat=0, tagId=3)), + ), + ) + + +class AsnPubKey(univ.Sequence): + """ASN.1 contents of DER encoded public key: + + RSAPublicKey ::= SEQUENCE { + modulus INTEGER, -- n + publicExponent INTEGER, -- e + """ + + componentType = namedtype.NamedTypes( + namedtype.NamedType("modulus", univ.Integer()), + namedtype.NamedType("publicExponent", univ.Integer()), + ) diff --git a/module/scripts/lib/rsa/cli.py b/module/scripts/lib/rsa/cli.py new file mode 100644 index 0000000..4db3f0b --- /dev/null +++ b/module/scripts/lib/rsa/cli.py @@ -0,0 +1,321 @@ +# Copyright 2011 Sybren A. Stüvel +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# https://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +"""Commandline scripts. + +These scripts are called by the executables defined in setup.py. +""" + +import abc +import sys +import typing +import optparse + +import rsa +import rsa.key +import rsa.pkcs1 + +HASH_METHODS = sorted(rsa.pkcs1.HASH_METHODS.keys()) +Indexable = typing.Union[typing.Tuple, typing.List[str]] + + +def keygen() -> None: + """Key generator.""" + + # Parse the CLI options + parser = optparse.OptionParser( + usage="usage: %prog [options] keysize", + description='Generates a new RSA key pair of "keysize" bits.', + ) + + parser.add_option( + "--pubout", + type="string", + help="Output filename for the public key. The public key is " + "not saved if this option is not present. You can use " + "pyrsa-priv2pub to create the public key file later.", + ) + + parser.add_option( + "-o", + "--out", + type="string", + help="Output filename for the private key. The key is " + "written to stdout if this option is not present.", + ) + + parser.add_option( + "--form", + help="key format of the private and public keys - default PEM", + choices=("PEM", "DER"), + default="PEM", + ) + + (cli, cli_args) = parser.parse_args(sys.argv[1:]) + + if len(cli_args) != 1: + parser.print_help() + raise SystemExit(1) + + try: + keysize = int(cli_args[0]) + except ValueError as ex: + parser.print_help() + print("Not a valid number: %s" % cli_args[0], file=sys.stderr) + raise SystemExit(1) from ex + + print("Generating %i-bit key" % keysize, file=sys.stderr) + (pub_key, priv_key) = rsa.newkeys(keysize) + + # Save public key + if cli.pubout: + print("Writing public key to %s" % cli.pubout, file=sys.stderr) + data = pub_key.save_pkcs1(format=cli.form) + with open(cli.pubout, "wb") as outfile: + outfile.write(data) + + # Save private key + data = priv_key.save_pkcs1(format=cli.form) + + if cli.out: + print("Writing private key to %s" % cli.out, file=sys.stderr) + with open(cli.out, "wb") as outfile: + outfile.write(data) + else: + print("Writing private key to stdout", file=sys.stderr) + sys.stdout.buffer.write(data) + + +class CryptoOperation(metaclass=abc.ABCMeta): + """CLI callable that operates with input, output, and a key.""" + + keyname = "public" # or 'private' + usage = "usage: %%prog [options] %(keyname)s_key" + description = "" + operation = "decrypt" + operation_past = "decrypted" + operation_progressive = "decrypting" + input_help = "Name of the file to %(operation)s. Reads from stdin if " "not specified." + output_help = ( + "Name of the file to write the %(operation_past)s file " + "to. Written to stdout if this option is not present." + ) + expected_cli_args = 1 + has_output = True + + key_class = rsa.PublicKey # type: typing.Type[rsa.key.AbstractKey] + + def __init__(self) -> None: + self.usage = self.usage % self.__class__.__dict__ + self.input_help = self.input_help % self.__class__.__dict__ + self.output_help = self.output_help % self.__class__.__dict__ + + @abc.abstractmethod + def perform_operation( + self, indata: bytes, key: rsa.key.AbstractKey, cli_args: Indexable + ) -> typing.Any: + """Performs the program's operation. + + Implement in a subclass. + + :returns: the data to write to the output. + """ + + def __call__(self) -> None: + """Runs the program.""" + + (cli, cli_args) = self.parse_cli() + + key = self.read_key(cli_args[0], cli.keyform) + + indata = self.read_infile(cli.input) + + print(self.operation_progressive.title(), file=sys.stderr) + outdata = self.perform_operation(indata, key, cli_args) + + if self.has_output: + self.write_outfile(outdata, cli.output) + + def parse_cli(self) -> typing.Tuple[optparse.Values, typing.List[str]]: + """Parse the CLI options + + :returns: (cli_opts, cli_args) + """ + + parser = optparse.OptionParser(usage=self.usage, description=self.description) + + parser.add_option("-i", "--input", type="string", help=self.input_help) + + if self.has_output: + parser.add_option("-o", "--output", type="string", help=self.output_help) + + parser.add_option( + "--keyform", + help="Key format of the %s key - default PEM" % self.keyname, + choices=("PEM", "DER"), + default="PEM", + ) + + (cli, cli_args) = parser.parse_args(sys.argv[1:]) + + if len(cli_args) != self.expected_cli_args: + parser.print_help() + raise SystemExit(1) + + return cli, cli_args + + def read_key(self, filename: str, keyform: str) -> rsa.key.AbstractKey: + """Reads a public or private key.""" + + print("Reading %s key from %s" % (self.keyname, filename), file=sys.stderr) + with open(filename, "rb") as keyfile: + keydata = keyfile.read() + + return self.key_class.load_pkcs1(keydata, keyform) + + def read_infile(self, inname: str) -> bytes: + """Read the input file""" + + if inname: + print("Reading input from %s" % inname, file=sys.stderr) + with open(inname, "rb") as infile: + return infile.read() + + print("Reading input from stdin", file=sys.stderr) + return sys.stdin.buffer.read() + + def write_outfile(self, outdata: bytes, outname: str) -> None: + """Write the output file""" + + if outname: + print("Writing output to %s" % outname, file=sys.stderr) + with open(outname, "wb") as outfile: + outfile.write(outdata) + else: + print("Writing output to stdout", file=sys.stderr) + sys.stdout.buffer.write(outdata) + + +class EncryptOperation(CryptoOperation): + """Encrypts a file.""" + + keyname = "public" + description = ( + "Encrypts a file. The file must be shorter than the key " "length in order to be encrypted." + ) + operation = "encrypt" + operation_past = "encrypted" + operation_progressive = "encrypting" + + def perform_operation( + self, indata: bytes, pub_key: rsa.key.AbstractKey, cli_args: Indexable = () + ) -> bytes: + """Encrypts files.""" + assert isinstance(pub_key, rsa.key.PublicKey) + return rsa.encrypt(indata, pub_key) + + +class DecryptOperation(CryptoOperation): + """Decrypts a file.""" + + keyname = "private" + description = ( + "Decrypts a file. The original file must be shorter than " + "the key length in order to have been encrypted." + ) + operation = "decrypt" + operation_past = "decrypted" + operation_progressive = "decrypting" + key_class = rsa.PrivateKey + + def perform_operation( + self, indata: bytes, priv_key: rsa.key.AbstractKey, cli_args: Indexable = () + ) -> bytes: + """Decrypts files.""" + assert isinstance(priv_key, rsa.key.PrivateKey) + return rsa.decrypt(indata, priv_key) + + +class SignOperation(CryptoOperation): + """Signs a file.""" + + keyname = "private" + usage = "usage: %%prog [options] private_key hash_method" + description = ( + "Signs a file, outputs the signature. Choose the hash " + "method from %s" % ", ".join(HASH_METHODS) + ) + operation = "sign" + operation_past = "signature" + operation_progressive = "Signing" + key_class = rsa.PrivateKey + expected_cli_args = 2 + + output_help = ( + "Name of the file to write the signature to. Written " + "to stdout if this option is not present." + ) + + def perform_operation( + self, indata: bytes, priv_key: rsa.key.AbstractKey, cli_args: Indexable + ) -> bytes: + """Signs files.""" + assert isinstance(priv_key, rsa.key.PrivateKey) + + hash_method = cli_args[1] + if hash_method not in HASH_METHODS: + raise SystemExit("Invalid hash method, choose one of %s" % ", ".join(HASH_METHODS)) + + return rsa.sign(indata, priv_key, hash_method) + + +class VerifyOperation(CryptoOperation): + """Verify a signature.""" + + keyname = "public" + usage = "usage: %%prog [options] public_key signature_file" + description = ( + "Verifies a signature, exits with status 0 upon success, " + "prints an error message and exits with status 1 upon error." + ) + operation = "verify" + operation_past = "verified" + operation_progressive = "Verifying" + key_class = rsa.PublicKey + expected_cli_args = 2 + has_output = False + + def perform_operation( + self, indata: bytes, pub_key: rsa.key.AbstractKey, cli_args: Indexable + ) -> None: + """Verifies files.""" + assert isinstance(pub_key, rsa.key.PublicKey) + + signature_file = cli_args[1] + + with open(signature_file, "rb") as sigfile: + signature = sigfile.read() + + try: + rsa.verify(indata, signature, pub_key) + except rsa.VerificationError as ex: + raise SystemExit("Verification failed.") from ex + + print("Verification OK", file=sys.stderr) + + +encrypt = EncryptOperation() +decrypt = DecryptOperation() +sign = SignOperation() +verify = VerifyOperation() diff --git a/module/scripts/lib/rsa/common.py b/module/scripts/lib/rsa/common.py new file mode 100644 index 0000000..ca732e5 --- /dev/null +++ b/module/scripts/lib/rsa/common.py @@ -0,0 +1,184 @@ +# Copyright 2011 Sybren A. Stüvel +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# https://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +"""Common functionality shared by several modules.""" + +import typing + + +class NotRelativePrimeError(ValueError): + def __init__(self, a: int, b: int, d: int, msg: str = "") -> None: + super().__init__(msg or "%d and %d are not relatively prime, divider=%i" % (a, b, d)) + self.a = a + self.b = b + self.d = d + + +def bit_size(num: int) -> int: + """ + Number of bits needed to represent a integer excluding any prefix + 0 bits. + + Usage:: + + >>> bit_size(1023) + 10 + >>> bit_size(1024) + 11 + >>> bit_size(1025) + 11 + + :param num: + Integer value. If num is 0, returns 0. Only the absolute value of the + number is considered. Therefore, signed integers will be abs(num) + before the number's bit length is determined. + :returns: + Returns the number of bits in the integer. + """ + + try: + return num.bit_length() + except AttributeError as ex: + raise TypeError("bit_size(num) only supports integers, not %r" % type(num)) from ex + + +def byte_size(number: int) -> int: + """ + Returns the number of bytes required to hold a specific long number. + + The number of bytes is rounded up. + + Usage:: + + >>> byte_size(1 << 1023) + 128 + >>> byte_size((1 << 1024) - 1) + 128 + >>> byte_size(1 << 1024) + 129 + + :param number: + An unsigned integer + :returns: + The number of bytes required to hold a specific long number. + """ + if number == 0: + return 1 + return ceil_div(bit_size(number), 8) + + +def ceil_div(num: int, div: int) -> int: + """ + Returns the ceiling function of a division between `num` and `div`. + + Usage:: + + >>> ceil_div(100, 7) + 15 + >>> ceil_div(100, 10) + 10 + >>> ceil_div(1, 4) + 1 + + :param num: Division's numerator, a number + :param div: Division's divisor, a number + + :return: Rounded up result of the division between the parameters. + """ + quanta, mod = divmod(num, div) + if mod: + quanta += 1 + return quanta + + +def extended_gcd(a: int, b: int) -> typing.Tuple[int, int, int]: + """Returns a tuple (r, i, j) such that r = gcd(a, b) = ia + jb""" + # r = gcd(a,b) i = multiplicitive inverse of a mod b + # or j = multiplicitive inverse of b mod a + # Neg return values for i or j are made positive mod b or a respectively + # Iterateive Version is faster and uses much less stack space + x = 0 + y = 1 + lx = 1 + ly = 0 + oa = a # Remember original a/b to remove + ob = b # negative values from return results + while b != 0: + q = a // b + (a, b) = (b, a % b) + (x, lx) = ((lx - (q * x)), x) + (y, ly) = ((ly - (q * y)), y) + if lx < 0: + lx += ob # If neg wrap modulo original b + if ly < 0: + ly += oa # If neg wrap modulo original a + return a, lx, ly # Return only positive values + + +def inverse(x: int, n: int) -> int: + """Returns the inverse of x % n under multiplication, a.k.a x^-1 (mod n) + + >>> inverse(7, 4) + 3 + >>> (inverse(143, 4) * 143) % 4 + 1 + """ + + (divider, inv, _) = extended_gcd(x, n) + + if divider != 1: + raise NotRelativePrimeError(x, n, divider) + + return inv + + +def crt(a_values: typing.Iterable[int], modulo_values: typing.Iterable[int]) -> int: + """Chinese Remainder Theorem. + + Calculates x such that x = a[i] (mod m[i]) for each i. + + :param a_values: the a-values of the above equation + :param modulo_values: the m-values of the above equation + :returns: x such that x = a[i] (mod m[i]) for each i + + + >>> crt([2, 3], [3, 5]) + 8 + + >>> crt([2, 3, 2], [3, 5, 7]) + 23 + + >>> crt([2, 3, 0], [7, 11, 15]) + 135 + """ + + m = 1 + x = 0 + + for modulo in modulo_values: + m *= modulo + + for (m_i, a_i) in zip(modulo_values, a_values): + M_i = m // m_i + inv = inverse(M_i, m_i) + + x = (x + a_i * M_i * inv) % m + + return x + + +if __name__ == "__main__": + import doctest + + doctest.testmod() diff --git a/module/scripts/lib/rsa/core.py b/module/scripts/lib/rsa/core.py new file mode 100644 index 0000000..84ed3f8 --- /dev/null +++ b/module/scripts/lib/rsa/core.py @@ -0,0 +1,53 @@ +# Copyright 2011 Sybren A. Stüvel +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# https://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +"""Core mathematical operations. + +This is the actual core RSA implementation, which is only defined +mathematically on integers. +""" + + +def assert_int(var: int, name: str) -> None: + if isinstance(var, int): + return + + raise TypeError("%s should be an integer, not %s" % (name, var.__class__)) + + +def encrypt_int(message: int, ekey: int, n: int) -> int: + """Encrypts a message using encryption key 'ekey', working modulo n""" + + assert_int(message, "message") + assert_int(ekey, "ekey") + assert_int(n, "n") + + if message < 0: + raise ValueError("Only non-negative numbers are supported") + + if message > n: + raise OverflowError("The message %i is too long for n=%i" % (message, n)) + + return pow(message, ekey, n) + + +def decrypt_int(cyphertext: int, dkey: int, n: int) -> int: + """Decrypts a cypher text using the decryption key 'dkey', working modulo n""" + + assert_int(cyphertext, "cyphertext") + assert_int(dkey, "dkey") + assert_int(n, "n") + + message = pow(cyphertext, dkey, n) + return message diff --git a/module/scripts/lib/rsa/key.py b/module/scripts/lib/rsa/key.py new file mode 100644 index 0000000..f800644 --- /dev/null +++ b/module/scripts/lib/rsa/key.py @@ -0,0 +1,858 @@ +# Copyright 2011 Sybren A. Stüvel +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# https://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +"""RSA key generation code. + +Create new keys with the newkeys() function. It will give you a PublicKey and a +PrivateKey object. + +Loading and saving keys requires the pyasn1 module. This module is imported as +late as possible, such that other functionality will remain working in absence +of pyasn1. + +.. note:: + + Storing public and private keys via the `pickle` module is possible. + However, it is insecure to load a key from an untrusted source. + The pickle module is not secure against erroneous or maliciously + constructed data. Never unpickle data received from an untrusted + or unauthenticated source. + +""" + +import threading +import typing +import warnings + +import rsa.prime +import rsa.pem +import rsa.common +import rsa.randnum +import rsa.core + + +DEFAULT_EXPONENT = 65537 + + +T = typing.TypeVar("T", bound="AbstractKey") + + +class AbstractKey: + """Abstract superclass for private and public keys.""" + + __slots__ = ("n", "e", "blindfac", "blindfac_inverse", "mutex") + + def __init__(self, n: int, e: int) -> None: + self.n = n + self.e = e + + # These will be computed properly on the first call to blind(). + self.blindfac = self.blindfac_inverse = -1 + + # Used to protect updates to the blinding factor in multi-threaded + # environments. + self.mutex = threading.Lock() + + @classmethod + def _load_pkcs1_pem(cls: typing.Type[T], keyfile: bytes) -> T: + """Loads a key in PKCS#1 PEM format, implement in a subclass. + + :param keyfile: contents of a PEM-encoded file that contains + the public key. + :type keyfile: bytes + + :return: the loaded key + :rtype: AbstractKey + """ + + @classmethod + def _load_pkcs1_der(cls: typing.Type[T], keyfile: bytes) -> T: + """Loads a key in PKCS#1 PEM format, implement in a subclass. + + :param keyfile: contents of a DER-encoded file that contains + the public key. + :type keyfile: bytes + + :return: the loaded key + :rtype: AbstractKey + """ + + def _save_pkcs1_pem(self) -> bytes: + """Saves the key in PKCS#1 PEM format, implement in a subclass. + + :returns: the PEM-encoded key. + :rtype: bytes + """ + + def _save_pkcs1_der(self) -> bytes: + """Saves the key in PKCS#1 DER format, implement in a subclass. + + :returns: the DER-encoded key. + :rtype: bytes + """ + + @classmethod + def load_pkcs1(cls: typing.Type[T], keyfile: bytes, format: str = "PEM") -> T: + """Loads a key in PKCS#1 DER or PEM format. + + :param keyfile: contents of a DER- or PEM-encoded file that contains + the key. + :type keyfile: bytes + :param format: the format of the file to load; 'PEM' or 'DER' + :type format: str + + :return: the loaded key + :rtype: AbstractKey + """ + + methods = { + "PEM": cls._load_pkcs1_pem, + "DER": cls._load_pkcs1_der, + } + + method = cls._assert_format_exists(format, methods) + return method(keyfile) + + @staticmethod + def _assert_format_exists( + file_format: str, methods: typing.Mapping[str, typing.Callable] + ) -> typing.Callable: + """Checks whether the given file format exists in 'methods'.""" + + try: + return methods[file_format] + except KeyError as ex: + formats = ", ".join(sorted(methods.keys())) + raise ValueError( + "Unsupported format: %r, try one of %s" % (file_format, formats) + ) from ex + + def save_pkcs1(self, format: str = "PEM") -> bytes: + """Saves the key in PKCS#1 DER or PEM format. + + :param format: the format to save; 'PEM' or 'DER' + :type format: str + :returns: the DER- or PEM-encoded key. + :rtype: bytes + """ + + methods = { + "PEM": self._save_pkcs1_pem, + "DER": self._save_pkcs1_der, + } + + method = self._assert_format_exists(format, methods) + return method() + + def blind(self, message: int) -> typing.Tuple[int, int]: + """Performs blinding on the message. + + :param message: the message, as integer, to blind. + :param r: the random number to blind with. + :return: tuple (the blinded message, the inverse of the used blinding factor) + + The blinding is such that message = unblind(decrypt(blind(encrypt(message))). + + See https://en.wikipedia.org/wiki/Blinding_%28cryptography%29 + """ + blindfac, blindfac_inverse = self._update_blinding_factor() + blinded = (message * pow(blindfac, self.e, self.n)) % self.n + return blinded, blindfac_inverse + + def unblind(self, blinded: int, blindfac_inverse: int) -> int: + """Performs blinding on the message using random number 'blindfac_inverse'. + + :param blinded: the blinded message, as integer, to unblind. + :param blindfac: the factor to unblind with. + :return: the original message. + + The blinding is such that message = unblind(decrypt(blind(encrypt(message))). + + See https://en.wikipedia.org/wiki/Blinding_%28cryptography%29 + """ + return (blindfac_inverse * blinded) % self.n + + def _initial_blinding_factor(self) -> int: + for _ in range(1000): + blind_r = rsa.randnum.randint(self.n - 1) + if rsa.prime.are_relatively_prime(self.n, blind_r): + return blind_r + raise RuntimeError("unable to find blinding factor") + + def _update_blinding_factor(self) -> typing.Tuple[int, int]: + """Update blinding factors. + + Computing a blinding factor is expensive, so instead this function + does this once, then updates the blinding factor as per section 9 + of 'A Timing Attack against RSA with the Chinese Remainder Theorem' + by Werner Schindler. + See https://tls.mbed.org/public/WSchindler-RSA_Timing_Attack.pdf + + :return: the new blinding factor and its inverse. + """ + + with self.mutex: + if self.blindfac < 0: + # Compute initial blinding factor, which is rather slow to do. + self.blindfac = self._initial_blinding_factor() + self.blindfac_inverse = rsa.common.inverse(self.blindfac, self.n) + else: + # Reuse previous blinding factor. + self.blindfac = pow(self.blindfac, 2, self.n) + self.blindfac_inverse = pow(self.blindfac_inverse, 2, self.n) + + return self.blindfac, self.blindfac_inverse + + +class PublicKey(AbstractKey): + """Represents a public RSA key. + + This key is also known as the 'encryption key'. It contains the 'n' and 'e' + values. + + Supports attributes as well as dictionary-like access. Attribute access is + faster, though. + + >>> PublicKey(5, 3) + PublicKey(5, 3) + + >>> key = PublicKey(5, 3) + >>> key.n + 5 + >>> key['n'] + 5 + >>> key.e + 3 + >>> key['e'] + 3 + + """ + + __slots__ = () + + def __getitem__(self, key: str) -> int: + return getattr(self, key) + + def __repr__(self) -> str: + return "PublicKey(%i, %i)" % (self.n, self.e) + + def __getstate__(self) -> typing.Tuple[int, int]: + """Returns the key as tuple for pickling.""" + return self.n, self.e + + def __setstate__(self, state: typing.Tuple[int, int]) -> None: + """Sets the key from tuple.""" + self.n, self.e = state + AbstractKey.__init__(self, self.n, self.e) + + def __eq__(self, other: typing.Any) -> bool: + if other is None: + return False + + if not isinstance(other, PublicKey): + return False + + return self.n == other.n and self.e == other.e + + def __ne__(self, other: typing.Any) -> bool: + return not (self == other) + + def __hash__(self) -> int: + return hash((self.n, self.e)) + + @classmethod + def _load_pkcs1_der(cls, keyfile: bytes) -> "PublicKey": + """Loads a key in PKCS#1 DER format. + + :param keyfile: contents of a DER-encoded file that contains the public + key. + :return: a PublicKey object + + First let's construct a DER encoded key: + + >>> import base64 + >>> b64der = 'MAwCBQCNGmYtAgMBAAE=' + >>> der = base64.standard_b64decode(b64der) + + This loads the file: + + >>> PublicKey._load_pkcs1_der(der) + PublicKey(2367317549, 65537) + + """ + + from pyasn1.codec.der import decoder + from rsa.asn1 import AsnPubKey + + (priv, _) = decoder.decode(keyfile, asn1Spec=AsnPubKey()) + return cls(n=int(priv["modulus"]), e=int(priv["publicExponent"])) + + def _save_pkcs1_der(self) -> bytes: + """Saves the public key in PKCS#1 DER format. + + :returns: the DER-encoded public key. + :rtype: bytes + """ + + from pyasn1.codec.der import encoder + from rsa.asn1 import AsnPubKey + + # Create the ASN object + asn_key = AsnPubKey() + asn_key.setComponentByName("modulus", self.n) + asn_key.setComponentByName("publicExponent", self.e) + + return encoder.encode(asn_key) + + @classmethod + def _load_pkcs1_pem(cls, keyfile: bytes) -> "PublicKey": + """Loads a PKCS#1 PEM-encoded public key file. + + The contents of the file before the "-----BEGIN RSA PUBLIC KEY-----" and + after the "-----END RSA PUBLIC KEY-----" lines is ignored. + + :param keyfile: contents of a PEM-encoded file that contains the public + key. + :return: a PublicKey object + """ + + der = rsa.pem.load_pem(keyfile, "RSA PUBLIC KEY") + return cls._load_pkcs1_der(der) + + def _save_pkcs1_pem(self) -> bytes: + """Saves a PKCS#1 PEM-encoded public key file. + + :return: contents of a PEM-encoded file that contains the public key. + :rtype: bytes + """ + + der = self._save_pkcs1_der() + return rsa.pem.save_pem(der, "RSA PUBLIC KEY") + + @classmethod + def load_pkcs1_openssl_pem(cls, keyfile: bytes) -> "PublicKey": + """Loads a PKCS#1.5 PEM-encoded public key file from OpenSSL. + + These files can be recognised in that they start with BEGIN PUBLIC KEY + rather than BEGIN RSA PUBLIC KEY. + + The contents of the file before the "-----BEGIN PUBLIC KEY-----" and + after the "-----END PUBLIC KEY-----" lines is ignored. + + :param keyfile: contents of a PEM-encoded file that contains the public + key, from OpenSSL. + :type keyfile: bytes + :return: a PublicKey object + """ + + der = rsa.pem.load_pem(keyfile, "PUBLIC KEY") + return cls.load_pkcs1_openssl_der(der) + + @classmethod + def load_pkcs1_openssl_der(cls, keyfile: bytes) -> "PublicKey": + """Loads a PKCS#1 DER-encoded public key file from OpenSSL. + + :param keyfile: contents of a DER-encoded file that contains the public + key, from OpenSSL. + :return: a PublicKey object + """ + + from rsa.asn1 import OpenSSLPubKey + from pyasn1.codec.der import decoder + from pyasn1.type import univ + + (keyinfo, _) = decoder.decode(keyfile, asn1Spec=OpenSSLPubKey()) + + if keyinfo["header"]["oid"] != univ.ObjectIdentifier("1.2.840.113549.1.1.1"): + raise TypeError("This is not a DER-encoded OpenSSL-compatible public key") + + return cls._load_pkcs1_der(keyinfo["key"][1:]) + + +class PrivateKey(AbstractKey): + """Represents a private RSA key. + + This key is also known as the 'decryption key'. It contains the 'n', 'e', + 'd', 'p', 'q' and other values. + + Supports attributes as well as dictionary-like access. Attribute access is + faster, though. + + >>> PrivateKey(3247, 65537, 833, 191, 17) + PrivateKey(3247, 65537, 833, 191, 17) + + exp1, exp2 and coef will be calculated: + + >>> pk = PrivateKey(3727264081, 65537, 3349121513, 65063, 57287) + >>> pk.exp1 + 55063 + >>> pk.exp2 + 10095 + >>> pk.coef + 50797 + + """ + + __slots__ = ("d", "p", "q", "exp1", "exp2", "coef") + + def __init__(self, n: int, e: int, d: int, p: int, q: int) -> None: + AbstractKey.__init__(self, n, e) + self.d = d + self.p = p + self.q = q + + # Calculate exponents and coefficient. + self.exp1 = int(d % (p - 1)) + self.exp2 = int(d % (q - 1)) + self.coef = rsa.common.inverse(q, p) + + def __getitem__(self, key: str) -> int: + return getattr(self, key) + + def __repr__(self) -> str: + return "PrivateKey(%i, %i, %i, %i, %i)" % ( + self.n, + self.e, + self.d, + self.p, + self.q, + ) + + def __getstate__(self) -> typing.Tuple[int, int, int, int, int, int, int, int]: + """Returns the key as tuple for pickling.""" + return self.n, self.e, self.d, self.p, self.q, self.exp1, self.exp2, self.coef + + def __setstate__(self, state: typing.Tuple[int, int, int, int, int, int, int, int]) -> None: + """Sets the key from tuple.""" + self.n, self.e, self.d, self.p, self.q, self.exp1, self.exp2, self.coef = state + AbstractKey.__init__(self, self.n, self.e) + + def __eq__(self, other: typing.Any) -> bool: + if other is None: + return False + + if not isinstance(other, PrivateKey): + return False + + return ( + self.n == other.n + and self.e == other.e + and self.d == other.d + and self.p == other.p + and self.q == other.q + and self.exp1 == other.exp1 + and self.exp2 == other.exp2 + and self.coef == other.coef + ) + + def __ne__(self, other: typing.Any) -> bool: + return not (self == other) + + def __hash__(self) -> int: + return hash((self.n, self.e, self.d, self.p, self.q, self.exp1, self.exp2, self.coef)) + + def blinded_decrypt(self, encrypted: int) -> int: + """Decrypts the message using blinding to prevent side-channel attacks. + + :param encrypted: the encrypted message + :type encrypted: int + + :returns: the decrypted message + :rtype: int + """ + + # Blinding and un-blinding should be using the same factor + blinded, blindfac_inverse = self.blind(encrypted) + + # Instead of using the core functionality, use the Chinese Remainder + # Theorem and be 2-4x faster. This the same as: + # + # decrypted = rsa.core.decrypt_int(blinded, self.d, self.n) + s1 = pow(blinded, self.exp1, self.p) + s2 = pow(blinded, self.exp2, self.q) + h = ((s1 - s2) * self.coef) % self.p + decrypted = s2 + self.q * h + + return self.unblind(decrypted, blindfac_inverse) + + def blinded_encrypt(self, message: int) -> int: + """Encrypts the message using blinding to prevent side-channel attacks. + + :param message: the message to encrypt + :type message: int + + :returns: the encrypted message + :rtype: int + """ + + blinded, blindfac_inverse = self.blind(message) + encrypted = rsa.core.encrypt_int(blinded, self.d, self.n) + return self.unblind(encrypted, blindfac_inverse) + + @classmethod + def _load_pkcs1_der(cls, keyfile: bytes) -> "PrivateKey": + """Loads a key in PKCS#1 DER format. + + :param keyfile: contents of a DER-encoded file that contains the private + key. + :type keyfile: bytes + :return: a PrivateKey object + + First let's construct a DER encoded key: + + >>> import base64 + >>> b64der = 'MC4CAQACBQDeKYlRAgMBAAECBQDHn4npAgMA/icCAwDfxwIDANcXAgInbwIDAMZt' + >>> der = base64.standard_b64decode(b64der) + + This loads the file: + + >>> PrivateKey._load_pkcs1_der(der) + PrivateKey(3727264081, 65537, 3349121513, 65063, 57287) + + """ + + from pyasn1.codec.der import decoder + + (priv, _) = decoder.decode(keyfile) + + # ASN.1 contents of DER encoded private key: + # + # RSAPrivateKey ::= SEQUENCE { + # version Version, + # modulus INTEGER, -- n + # publicExponent INTEGER, -- e + # privateExponent INTEGER, -- d + # prime1 INTEGER, -- p + # prime2 INTEGER, -- q + # exponent1 INTEGER, -- d mod (p-1) + # exponent2 INTEGER, -- d mod (q-1) + # coefficient INTEGER, -- (inverse of q) mod p + # otherPrimeInfos OtherPrimeInfos OPTIONAL + # } + + if priv[0] != 0: + raise ValueError("Unable to read this file, version %s != 0" % priv[0]) + + as_ints = map(int, priv[1:6]) + key = cls(*as_ints) + + exp1, exp2, coef = map(int, priv[6:9]) + + if (key.exp1, key.exp2, key.coef) != (exp1, exp2, coef): + warnings.warn( + "You have provided a malformed keyfile. Either the exponents " + "or the coefficient are incorrect. Using the correct values " + "instead.", + UserWarning, + ) + + return key + + def _save_pkcs1_der(self) -> bytes: + """Saves the private key in PKCS#1 DER format. + + :returns: the DER-encoded private key. + :rtype: bytes + """ + + from pyasn1.type import univ, namedtype + from pyasn1.codec.der import encoder + + class AsnPrivKey(univ.Sequence): + componentType = namedtype.NamedTypes( + namedtype.NamedType("version", univ.Integer()), + namedtype.NamedType("modulus", univ.Integer()), + namedtype.NamedType("publicExponent", univ.Integer()), + namedtype.NamedType("privateExponent", univ.Integer()), + namedtype.NamedType("prime1", univ.Integer()), + namedtype.NamedType("prime2", univ.Integer()), + namedtype.NamedType("exponent1", univ.Integer()), + namedtype.NamedType("exponent2", univ.Integer()), + namedtype.NamedType("coefficient", univ.Integer()), + ) + + # Create the ASN object + asn_key = AsnPrivKey() + asn_key.setComponentByName("version", 0) + asn_key.setComponentByName("modulus", self.n) + asn_key.setComponentByName("publicExponent", self.e) + asn_key.setComponentByName("privateExponent", self.d) + asn_key.setComponentByName("prime1", self.p) + asn_key.setComponentByName("prime2", self.q) + asn_key.setComponentByName("exponent1", self.exp1) + asn_key.setComponentByName("exponent2", self.exp2) + asn_key.setComponentByName("coefficient", self.coef) + + return encoder.encode(asn_key) + + @classmethod + def _load_pkcs1_pem(cls, keyfile: bytes) -> "PrivateKey": + """Loads a PKCS#1 PEM-encoded private key file. + + The contents of the file before the "-----BEGIN RSA PRIVATE KEY-----" and + after the "-----END RSA PRIVATE KEY-----" lines is ignored. + + :param keyfile: contents of a PEM-encoded file that contains the private + key. + :type keyfile: bytes + :return: a PrivateKey object + """ + + der = rsa.pem.load_pem(keyfile, b"RSA PRIVATE KEY") + return cls._load_pkcs1_der(der) + + def _save_pkcs1_pem(self) -> bytes: + """Saves a PKCS#1 PEM-encoded private key file. + + :return: contents of a PEM-encoded file that contains the private key. + :rtype: bytes + """ + + der = self._save_pkcs1_der() + return rsa.pem.save_pem(der, b"RSA PRIVATE KEY") + + +def find_p_q( + nbits: int, + getprime_func: typing.Callable[[int], int] = rsa.prime.getprime, + accurate: bool = True, +) -> typing.Tuple[int, int]: + """Returns a tuple of two different primes of nbits bits each. + + The resulting p * q has exactly 2 * nbits bits, and the returned p and q + will not be equal. + + :param nbits: the number of bits in each of p and q. + :param getprime_func: the getprime function, defaults to + :py:func:`rsa.prime.getprime`. + + *Introduced in Python-RSA 3.1* + + :param accurate: whether to enable accurate mode or not. + :returns: (p, q), where p > q + + >>> (p, q) = find_p_q(128) + >>> from rsa import common + >>> common.bit_size(p * q) + 256 + + When not in accurate mode, the number of bits can be slightly less + + >>> (p, q) = find_p_q(128, accurate=False) + >>> from rsa import common + >>> common.bit_size(p * q) <= 256 + True + >>> common.bit_size(p * q) > 240 + True + + """ + + total_bits = nbits * 2 + + # Make sure that p and q aren't too close or the factoring programs can + # factor n. + shift = nbits // 16 + pbits = nbits + shift + qbits = nbits - shift + + # Choose the two initial primes + p = getprime_func(pbits) + q = getprime_func(qbits) + + def is_acceptable(p: int, q: int) -> bool: + """Returns True iff p and q are acceptable: + + - p and q differ + - (p * q) has the right nr of bits (when accurate=True) + """ + + if p == q: + return False + + if not accurate: + return True + + # Make sure we have just the right amount of bits + found_size = rsa.common.bit_size(p * q) + return total_bits == found_size + + # Keep choosing other primes until they match our requirements. + change_p = False + while not is_acceptable(p, q): + # Change p on one iteration and q on the other + if change_p: + p = getprime_func(pbits) + else: + q = getprime_func(qbits) + + change_p = not change_p + + # We want p > q as described on + # http://www.di-mgt.com.au/rsa_alg.html#crt + return max(p, q), min(p, q) + + +def calculate_keys_custom_exponent(p: int, q: int, exponent: int) -> typing.Tuple[int, int]: + """Calculates an encryption and a decryption key given p, q and an exponent, + and returns them as a tuple (e, d) + + :param p: the first large prime + :param q: the second large prime + :param exponent: the exponent for the key; only change this if you know + what you're doing, as the exponent influences how difficult your + private key can be cracked. A very common choice for e is 65537. + :type exponent: int + + """ + + phi_n = (p - 1) * (q - 1) + + try: + d = rsa.common.inverse(exponent, phi_n) + except rsa.common.NotRelativePrimeError as ex: + raise rsa.common.NotRelativePrimeError( + exponent, + phi_n, + ex.d, + msg="e (%d) and phi_n (%d) are not relatively prime (divider=%i)" + % (exponent, phi_n, ex.d), + ) from ex + + if (exponent * d) % phi_n != 1: + raise ValueError( + "e (%d) and d (%d) are not mult. inv. modulo " "phi_n (%d)" % (exponent, d, phi_n) + ) + + return exponent, d + + +def calculate_keys(p: int, q: int) -> typing.Tuple[int, int]: + """Calculates an encryption and a decryption key given p and q, and + returns them as a tuple (e, d) + + :param p: the first large prime + :param q: the second large prime + + :return: tuple (e, d) with the encryption and decryption exponents. + """ + + return calculate_keys_custom_exponent(p, q, DEFAULT_EXPONENT) + + +def gen_keys( + nbits: int, + getprime_func: typing.Callable[[int], int], + accurate: bool = True, + exponent: int = DEFAULT_EXPONENT, +) -> typing.Tuple[int, int, int, int]: + """Generate RSA keys of nbits bits. Returns (p, q, e, d). + + Note: this can take a long time, depending on the key size. + + :param nbits: the total number of bits in ``p`` and ``q``. Both ``p`` and + ``q`` will use ``nbits/2`` bits. + :param getprime_func: either :py:func:`rsa.prime.getprime` or a function + with similar signature. + :param exponent: the exponent for the key; only change this if you know + what you're doing, as the exponent influences how difficult your + private key can be cracked. A very common choice for e is 65537. + :type exponent: int + """ + + # Regenerate p and q values, until calculate_keys doesn't raise a + # ValueError. + while True: + (p, q) = find_p_q(nbits // 2, getprime_func, accurate) + try: + (e, d) = calculate_keys_custom_exponent(p, q, exponent=exponent) + break + except ValueError: + pass + + return p, q, e, d + + +def newkeys( + nbits: int, + accurate: bool = True, + poolsize: int = 1, + exponent: int = DEFAULT_EXPONENT, +) -> typing.Tuple[PublicKey, PrivateKey]: + """Generates public and private keys, and returns them as (pub, priv). + + The public key is also known as the 'encryption key', and is a + :py:class:`rsa.PublicKey` object. The private key is also known as the + 'decryption key' and is a :py:class:`rsa.PrivateKey` object. + + :param nbits: the number of bits required to store ``n = p*q``. + :param accurate: when True, ``n`` will have exactly the number of bits you + asked for. However, this makes key generation much slower. When False, + `n`` may have slightly less bits. + :param poolsize: the number of processes to use to generate the prime + numbers. If set to a number > 1, a parallel algorithm will be used. + This requires Python 2.6 or newer. + :param exponent: the exponent for the key; only change this if you know + what you're doing, as the exponent influences how difficult your + private key can be cracked. A very common choice for e is 65537. + :type exponent: int + + :returns: a tuple (:py:class:`rsa.PublicKey`, :py:class:`rsa.PrivateKey`) + + The ``poolsize`` parameter was added in *Python-RSA 3.1* and requires + Python 2.6 or newer. + + """ + + if nbits < 16: + raise ValueError("Key too small") + + if poolsize < 1: + raise ValueError("Pool size (%i) should be >= 1" % poolsize) + + # Determine which getprime function to use + if poolsize > 1: + from rsa import parallel + + def getprime_func(nbits: int) -> int: + return parallel.getprime(nbits, poolsize=poolsize) + + else: + getprime_func = rsa.prime.getprime + + # Generate the key components + (p, q, e, d) = gen_keys(nbits, getprime_func, accurate=accurate, exponent=exponent) + + # Create the key objects + n = p * q + + return (PublicKey(n, e), PrivateKey(n, e, d, p, q)) + + +__all__ = ["PublicKey", "PrivateKey", "newkeys"] + +if __name__ == "__main__": + import doctest + + try: + for count in range(100): + (failures, tests) = doctest.testmod() + if failures: + break + + if (count % 10 == 0 and count) or count == 1: + print("%i times" % count) + except KeyboardInterrupt: + print("Aborted") + else: + print("Doctests done") diff --git a/module/scripts/lib/rsa/parallel.py b/module/scripts/lib/rsa/parallel.py new file mode 100644 index 0000000..5020edb --- /dev/null +++ b/module/scripts/lib/rsa/parallel.py @@ -0,0 +1,96 @@ +# Copyright 2011 Sybren A. Stüvel +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# https://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +"""Functions for parallel computation on multiple cores. + +Introduced in Python-RSA 3.1. + +.. note:: + + Requires Python 2.6 or newer. + +""" + +import multiprocessing as mp +from multiprocessing.connection import Connection + +import rsa.prime +import rsa.randnum + + +def _find_prime(nbits: int, pipe: Connection) -> None: + while True: + integer = rsa.randnum.read_random_odd_int(nbits) + + # Test for primeness + if rsa.prime.is_prime(integer): + pipe.send(integer) + return + + +def getprime(nbits: int, poolsize: int) -> int: + """Returns a prime number that can be stored in 'nbits' bits. + + Works in multiple threads at the same time. + + >>> p = getprime(128, 3) + >>> rsa.prime.is_prime(p-1) + False + >>> rsa.prime.is_prime(p) + True + >>> rsa.prime.is_prime(p+1) + False + + >>> from rsa import common + >>> common.bit_size(p) == 128 + True + + """ + + (pipe_recv, pipe_send) = mp.Pipe(duplex=False) + + # Create processes + try: + procs = [mp.Process(target=_find_prime, args=(nbits, pipe_send)) for _ in range(poolsize)] + # Start processes + for p in procs: + p.start() + + result = pipe_recv.recv() + finally: + pipe_recv.close() + pipe_send.close() + + # Terminate processes + for p in procs: + p.terminate() + + return result + + +__all__ = ["getprime"] + +if __name__ == "__main__": + print("Running doctests 1000x or until failure") + import doctest + + for count in range(100): + (failures, tests) = doctest.testmod() + if failures: + break + + if count % 10 == 0 and count: + print("%i times" % count) + + print("Doctests done") diff --git a/module/scripts/lib/rsa/pem.py b/module/scripts/lib/rsa/pem.py new file mode 100644 index 0000000..5d26e6e --- /dev/null +++ b/module/scripts/lib/rsa/pem.py @@ -0,0 +1,134 @@ +# Copyright 2011 Sybren A. Stüvel +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# https://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +"""Functions that load and write PEM-encoded files.""" + +import base64 +import typing + +# Should either be ASCII strings or bytes. +FlexiText = typing.Union[str, bytes] + + +def _markers(pem_marker: FlexiText) -> typing.Tuple[bytes, bytes]: + """ + Returns the start and end PEM markers, as bytes. + """ + + if not isinstance(pem_marker, bytes): + pem_marker = pem_marker.encode("ascii") + + return ( + b"-----BEGIN " + pem_marker + b"-----", + b"-----END " + pem_marker + b"-----", + ) + + +def _pem_lines(contents: bytes, pem_start: bytes, pem_end: bytes) -> typing.Iterator[bytes]: + """Generator over PEM lines between pem_start and pem_end.""" + + in_pem_part = False + seen_pem_start = False + + for line in contents.splitlines(): + line = line.strip() + + # Skip empty lines + if not line: + continue + + # Handle start marker + if line == pem_start: + if in_pem_part: + raise ValueError('Seen start marker "%r" twice' % pem_start) + + in_pem_part = True + seen_pem_start = True + continue + + # Skip stuff before first marker + if not in_pem_part: + continue + + # Handle end marker + if in_pem_part and line == pem_end: + in_pem_part = False + break + + # Load fields + if b":" in line: + continue + + yield line + + # Do some sanity checks + if not seen_pem_start: + raise ValueError('No PEM start marker "%r" found' % pem_start) + + if in_pem_part: + raise ValueError('No PEM end marker "%r" found' % pem_end) + + +def load_pem(contents: FlexiText, pem_marker: FlexiText) -> bytes: + """Loads a PEM file. + + :param contents: the contents of the file to interpret + :param pem_marker: the marker of the PEM content, such as 'RSA PRIVATE KEY' + when your file has '-----BEGIN RSA PRIVATE KEY-----' and + '-----END RSA PRIVATE KEY-----' markers. + + :return: the base64-decoded content between the start and end markers. + + @raise ValueError: when the content is invalid, for example when the start + marker cannot be found. + + """ + + # We want bytes, not text. If it's text, it can be converted to ASCII bytes. + if not isinstance(contents, bytes): + contents = contents.encode("ascii") + + (pem_start, pem_end) = _markers(pem_marker) + pem_lines = [line for line in _pem_lines(contents, pem_start, pem_end)] + + # Base64-decode the contents + pem = b"".join(pem_lines) + return base64.standard_b64decode(pem) + + +def save_pem(contents: bytes, pem_marker: FlexiText) -> bytes: + """Saves a PEM file. + + :param contents: the contents to encode in PEM format + :param pem_marker: the marker of the PEM content, such as 'RSA PRIVATE KEY' + when your file has '-----BEGIN RSA PRIVATE KEY-----' and + '-----END RSA PRIVATE KEY-----' markers. + + :return: the base64-encoded content between the start and end markers, as bytes. + + """ + + (pem_start, pem_end) = _markers(pem_marker) + + b64 = base64.standard_b64encode(contents).replace(b"\n", b"") + pem_lines = [pem_start] + + for block_start in range(0, len(b64), 64): + block = b64[block_start : block_start + 64] + pem_lines.append(block) + + pem_lines.append(pem_end) + pem_lines.append(b"") + + return b"\n".join(pem_lines) diff --git a/module/scripts/lib/rsa/pkcs1.py b/module/scripts/lib/rsa/pkcs1.py new file mode 100644 index 0000000..ec6998e --- /dev/null +++ b/module/scripts/lib/rsa/pkcs1.py @@ -0,0 +1,485 @@ +# Copyright 2011 Sybren A. Stüvel +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# https://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +"""Functions for PKCS#1 version 1.5 encryption and signing + +This module implements certain functionality from PKCS#1 version 1.5. For a +very clear example, read http://www.di-mgt.com.au/rsa_alg.html#pkcs1schemes + +At least 8 bytes of random padding is used when encrypting a message. This makes +these methods much more secure than the ones in the ``rsa`` module. + +WARNING: this module leaks information when decryption fails. The exceptions +that are raised contain the Python traceback information, which can be used to +deduce where in the process the failure occurred. DO NOT PASS SUCH INFORMATION +to your users. +""" + +import hashlib +import os +import sys +import typing +from hmac import compare_digest + +from . import common, transform, core, key + +if typing.TYPE_CHECKING: + HashType = hashlib._Hash +else: + HashType = typing.Any + +# ASN.1 codes that describe the hash algorithm used. +HASH_ASN1 = { + "MD5": b"\x30\x20\x30\x0c\x06\x08\x2a\x86\x48\x86\xf7\x0d\x02\x05\x05\x00\x04\x10", + "SHA-1": b"\x30\x21\x30\x09\x06\x05\x2b\x0e\x03\x02\x1a\x05\x00\x04\x14", + "SHA-224": b"\x30\x2d\x30\x0d\x06\x09\x60\x86\x48\x01\x65\x03\x04\x02\x04\x05\x00\x04\x1c", + "SHA-256": b"\x30\x31\x30\x0d\x06\x09\x60\x86\x48\x01\x65\x03\x04\x02\x01\x05\x00\x04\x20", + "SHA-384": b"\x30\x41\x30\x0d\x06\x09\x60\x86\x48\x01\x65\x03\x04\x02\x02\x05\x00\x04\x30", + "SHA-512": b"\x30\x51\x30\x0d\x06\x09\x60\x86\x48\x01\x65\x03\x04\x02\x03\x05\x00\x04\x40", +} + +HASH_METHODS: typing.Dict[str, typing.Callable[[], HashType]] = { + "MD5": hashlib.md5, + "SHA-1": hashlib.sha1, + "SHA-224": hashlib.sha224, + "SHA-256": hashlib.sha256, + "SHA-384": hashlib.sha384, + "SHA-512": hashlib.sha512, +} +"""Hash methods supported by this library.""" + + +if sys.version_info >= (3, 6): + # Python 3.6 introduced SHA3 support. + HASH_ASN1.update( + { + "SHA3-256": b"\x30\x31\x30\x0d\x06\x09\x60\x86\x48\x01\x65\x03\x04\x02\x08\x05\x00\x04\x20", + "SHA3-384": b"\x30\x41\x30\x0d\x06\x09\x60\x86\x48\x01\x65\x03\x04\x02\x09\x05\x00\x04\x30", + "SHA3-512": b"\x30\x51\x30\x0d\x06\x09\x60\x86\x48\x01\x65\x03\x04\x02\x0a\x05\x00\x04\x40", + } + ) + + HASH_METHODS.update( + { + "SHA3-256": hashlib.sha3_256, + "SHA3-384": hashlib.sha3_384, + "SHA3-512": hashlib.sha3_512, + } + ) + + +class CryptoError(Exception): + """Base class for all exceptions in this module.""" + + +class DecryptionError(CryptoError): + """Raised when decryption fails.""" + + +class VerificationError(CryptoError): + """Raised when verification fails.""" + + +def _pad_for_encryption(message: bytes, target_length: int) -> bytes: + r"""Pads the message for encryption, returning the padded message. + + :return: 00 02 RANDOM_DATA 00 MESSAGE + + >>> block = _pad_for_encryption(b'hello', 16) + >>> len(block) + 16 + >>> block[0:2] + b'\x00\x02' + >>> block[-6:] + b'\x00hello' + + """ + + max_msglength = target_length - 11 + msglength = len(message) + + if msglength > max_msglength: + raise OverflowError( + "%i bytes needed for message, but there is only" + " space for %i" % (msglength, max_msglength) + ) + + # Get random padding + padding = b"" + padding_length = target_length - msglength - 3 + + # We remove 0-bytes, so we'll end up with less padding than we've asked for, + # so keep adding data until we're at the correct length. + while len(padding) < padding_length: + needed_bytes = padding_length - len(padding) + + # Always read at least 8 bytes more than we need, and trim off the rest + # after removing the 0-bytes. This increases the chance of getting + # enough bytes, especially when needed_bytes is small + new_padding = os.urandom(needed_bytes + 5) + new_padding = new_padding.replace(b"\x00", b"") + padding = padding + new_padding[:needed_bytes] + + assert len(padding) == padding_length + + return b"".join([b"\x00\x02", padding, b"\x00", message]) + + +def _pad_for_signing(message: bytes, target_length: int) -> bytes: + r"""Pads the message for signing, returning the padded message. + + The padding is always a repetition of FF bytes. + + :return: 00 01 PADDING 00 MESSAGE + + >>> block = _pad_for_signing(b'hello', 16) + >>> len(block) + 16 + >>> block[0:2] + b'\x00\x01' + >>> block[-6:] + b'\x00hello' + >>> block[2:-6] + b'\xff\xff\xff\xff\xff\xff\xff\xff' + + """ + + max_msglength = target_length - 11 + msglength = len(message) + + if msglength > max_msglength: + raise OverflowError( + "%i bytes needed for message, but there is only" + " space for %i" % (msglength, max_msglength) + ) + + padding_length = target_length - msglength - 3 + + return b"".join([b"\x00\x01", padding_length * b"\xff", b"\x00", message]) + + +def encrypt(message: bytes, pub_key: key.PublicKey) -> bytes: + """Encrypts the given message using PKCS#1 v1.5 + + :param message: the message to encrypt. Must be a byte string no longer than + ``k-11`` bytes, where ``k`` is the number of bytes needed to encode + the ``n`` component of the public key. + :param pub_key: the :py:class:`rsa.PublicKey` to encrypt with. + :raise OverflowError: when the message is too large to fit in the padded + block. + + >>> from rsa import key, common + >>> (pub_key, priv_key) = key.newkeys(256) + >>> message = b'hello' + >>> crypto = encrypt(message, pub_key) + + The crypto text should be just as long as the public key 'n' component: + + >>> len(crypto) == common.byte_size(pub_key.n) + True + + """ + + keylength = common.byte_size(pub_key.n) + padded = _pad_for_encryption(message, keylength) + + payload = transform.bytes2int(padded) + encrypted = core.encrypt_int(payload, pub_key.e, pub_key.n) + block = transform.int2bytes(encrypted, keylength) + + return block + + +def decrypt(crypto: bytes, priv_key: key.PrivateKey) -> bytes: + r"""Decrypts the given message using PKCS#1 v1.5 + + The decryption is considered 'failed' when the resulting cleartext doesn't + start with the bytes 00 02, or when the 00 byte between the padding and + the message cannot be found. + + :param crypto: the crypto text as returned by :py:func:`rsa.encrypt` + :param priv_key: the :py:class:`rsa.PrivateKey` to decrypt with. + :raise DecryptionError: when the decryption fails. No details are given as + to why the code thinks the decryption fails, as this would leak + information about the private key. + + + >>> import rsa + >>> (pub_key, priv_key) = rsa.newkeys(256) + + It works with strings: + + >>> crypto = encrypt(b'hello', pub_key) + >>> decrypt(crypto, priv_key) + b'hello' + + And with binary data: + + >>> crypto = encrypt(b'\x00\x00\x00\x00\x01', pub_key) + >>> decrypt(crypto, priv_key) + b'\x00\x00\x00\x00\x01' + + Altering the encrypted information will *likely* cause a + :py:class:`rsa.pkcs1.DecryptionError`. If you want to be *sure*, use + :py:func:`rsa.sign`. + + + .. warning:: + + Never display the stack trace of a + :py:class:`rsa.pkcs1.DecryptionError` exception. It shows where in the + code the exception occurred, and thus leaks information about the key. + It's only a tiny bit of information, but every bit makes cracking the + keys easier. + + >>> crypto = encrypt(b'hello', pub_key) + >>> crypto = crypto[0:5] + b'X' + crypto[6:] # change a byte + >>> decrypt(crypto, priv_key) + Traceback (most recent call last): + ... + rsa.pkcs1.DecryptionError: Decryption failed + + """ + + blocksize = common.byte_size(priv_key.n) + encrypted = transform.bytes2int(crypto) + decrypted = priv_key.blinded_decrypt(encrypted) + cleartext = transform.int2bytes(decrypted, blocksize) + + # Detect leading zeroes in the crypto. These are not reflected in the + # encrypted value (as leading zeroes do not influence the value of an + # integer). This fixes CVE-2020-13757. + if len(crypto) > blocksize: + # This is operating on public information, so doesn't need to be constant-time. + raise DecryptionError("Decryption failed") + + # If we can't find the cleartext marker, decryption failed. + cleartext_marker_bad = not compare_digest(cleartext[:2], b"\x00\x02") + + # Find the 00 separator between the padding and the message + sep_idx = cleartext.find(b"\x00", 2) + + # sep_idx indicates the position of the `\x00` separator that separates the + # padding from the actual message. The padding should be at least 8 bytes + # long (see https://tools.ietf.org/html/rfc8017#section-7.2.2 step 3), which + # means the separator should be at least at index 10 (because of the + # `\x00\x02` marker that precedes it). + sep_idx_bad = sep_idx < 10 + + anything_bad = cleartext_marker_bad | sep_idx_bad + if anything_bad: + raise DecryptionError("Decryption failed") + + return cleartext[sep_idx + 1 :] + + +def sign_hash(hash_value: bytes, priv_key: key.PrivateKey, hash_method: str) -> bytes: + """Signs a precomputed hash with the private key. + + Hashes the message, then signs the hash with the given key. This is known + as a "detached signature", because the message itself isn't altered. + + :param hash_value: A precomputed hash to sign (ignores message). + :param priv_key: the :py:class:`rsa.PrivateKey` to sign with + :param hash_method: the hash method used on the message. Use 'MD5', 'SHA-1', + 'SHA-224', SHA-256', 'SHA-384' or 'SHA-512'. + :return: a message signature block. + :raise OverflowError: if the private key is too small to contain the + requested hash. + + """ + + # Get the ASN1 code for this hash method + if hash_method not in HASH_ASN1: + raise ValueError("Invalid hash method: %s" % hash_method) + asn1code = HASH_ASN1[hash_method] + + # Encrypt the hash with the private key + cleartext = asn1code + hash_value + keylength = common.byte_size(priv_key.n) + padded = _pad_for_signing(cleartext, keylength) + + payload = transform.bytes2int(padded) + encrypted = priv_key.blinded_encrypt(payload) + block = transform.int2bytes(encrypted, keylength) + + return block + + +def sign(message: bytes, priv_key: key.PrivateKey, hash_method: str) -> bytes: + """Signs the message with the private key. + + Hashes the message, then signs the hash with the given key. This is known + as a "detached signature", because the message itself isn't altered. + + :param message: the message to sign. Can be an 8-bit string or a file-like + object. If ``message`` has a ``read()`` method, it is assumed to be a + file-like object. + :param priv_key: the :py:class:`rsa.PrivateKey` to sign with + :param hash_method: the hash method used on the message. Use 'MD5', 'SHA-1', + 'SHA-224', SHA-256', 'SHA-384' or 'SHA-512'. + :return: a message signature block. + :raise OverflowError: if the private key is too small to contain the + requested hash. + + """ + + msg_hash = compute_hash(message, hash_method) + return sign_hash(msg_hash, priv_key, hash_method) + + +def verify(message: bytes, signature: bytes, pub_key: key.PublicKey) -> str: + """Verifies that the signature matches the message. + + The hash method is detected automatically from the signature. + + :param message: the signed message. Can be an 8-bit string or a file-like + object. If ``message`` has a ``read()`` method, it is assumed to be a + file-like object. + :param signature: the signature block, as created with :py:func:`rsa.sign`. + :param pub_key: the :py:class:`rsa.PublicKey` of the person signing the message. + :raise VerificationError: when the signature doesn't match the message. + :returns: the name of the used hash. + + """ + + keylength = common.byte_size(pub_key.n) + encrypted = transform.bytes2int(signature) + decrypted = core.decrypt_int(encrypted, pub_key.e, pub_key.n) + clearsig = transform.int2bytes(decrypted, keylength) + + # Get the hash method + method_name = _find_method_hash(clearsig) + message_hash = compute_hash(message, method_name) + + # Reconstruct the expected padded hash + cleartext = HASH_ASN1[method_name] + message_hash + expected = _pad_for_signing(cleartext, keylength) + + if len(signature) != keylength: + raise VerificationError("Verification failed") + + # Compare with the signed one + if expected != clearsig: + raise VerificationError("Verification failed") + + return method_name + + +def find_signature_hash(signature: bytes, pub_key: key.PublicKey) -> str: + """Returns the hash name detected from the signature. + + If you also want to verify the message, use :py:func:`rsa.verify()` instead. + It also returns the name of the used hash. + + :param signature: the signature block, as created with :py:func:`rsa.sign`. + :param pub_key: the :py:class:`rsa.PublicKey` of the person signing the message. + :returns: the name of the used hash. + """ + + keylength = common.byte_size(pub_key.n) + encrypted = transform.bytes2int(signature) + decrypted = core.decrypt_int(encrypted, pub_key.e, pub_key.n) + clearsig = transform.int2bytes(decrypted, keylength) + + return _find_method_hash(clearsig) + + +def yield_fixedblocks(infile: typing.BinaryIO, blocksize: int) -> typing.Iterator[bytes]: + """Generator, yields each block of ``blocksize`` bytes in the input file. + + :param infile: file to read and separate in blocks. + :param blocksize: block size in bytes. + :returns: a generator that yields the contents of each block + """ + + while True: + block = infile.read(blocksize) + + read_bytes = len(block) + if read_bytes == 0: + break + + yield block + + if read_bytes < blocksize: + break + + +def compute_hash(message: typing.Union[bytes, typing.BinaryIO], method_name: str) -> bytes: + """Returns the message digest. + + :param message: the signed message. Can be an 8-bit string or a file-like + object. If ``message`` has a ``read()`` method, it is assumed to be a + file-like object. + :param method_name: the hash method, must be a key of + :py:const:`rsa.pkcs1.HASH_METHODS`. + + """ + + if method_name not in HASH_METHODS: + raise ValueError("Invalid hash method: %s" % method_name) + + method = HASH_METHODS[method_name] + hasher = method() + + if isinstance(message, bytes): + hasher.update(message) + else: + assert hasattr(message, "read") and hasattr(message.read, "__call__") + # read as 1K blocks + for block in yield_fixedblocks(message, 1024): + hasher.update(block) + + return hasher.digest() + + +def _find_method_hash(clearsig: bytes) -> str: + """Finds the hash method. + + :param clearsig: full padded ASN1 and hash. + :return: the used hash method. + :raise VerificationFailed: when the hash method cannot be found + """ + + for (hashname, asn1code) in HASH_ASN1.items(): + if asn1code in clearsig: + return hashname + + raise VerificationError("Verification failed") + + +__all__ = [ + "encrypt", + "decrypt", + "sign", + "verify", + "DecryptionError", + "VerificationError", + "CryptoError", +] + +if __name__ == "__main__": + print("Running doctests 1000x or until failure") + import doctest + + for count in range(1000): + (failures, tests) = doctest.testmod() + if failures: + break + + if count % 100 == 0 and count: + print("%i times" % count) + + print("Doctests done") diff --git a/module/scripts/lib/rsa/pkcs1_v2.py b/module/scripts/lib/rsa/pkcs1_v2.py new file mode 100644 index 0000000..d68b907 --- /dev/null +++ b/module/scripts/lib/rsa/pkcs1_v2.py @@ -0,0 +1,100 @@ +# Copyright 2011 Sybren A. Stüvel +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# https://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +"""Functions for PKCS#1 version 2 encryption and signing + +This module implements certain functionality from PKCS#1 version 2. Main +documentation is RFC 2437: https://tools.ietf.org/html/rfc2437 +""" + +from rsa import ( + common, + pkcs1, + transform, +) + + +def mgf1(seed: bytes, length: int, hasher: str = "SHA-1") -> bytes: + """ + MGF1 is a Mask Generation Function based on a hash function. + + A mask generation function takes an octet string of variable length and a + desired output length as input, and outputs an octet string of the desired + length. The plaintext-awareness of RSAES-OAEP relies on the random nature of + the output of the mask generation function, which in turn relies on the + random nature of the underlying hash. + + :param bytes seed: seed from which mask is generated, an octet string + :param int length: intended length in octets of the mask, at most 2^32(hLen) + :param str hasher: hash function (hLen denotes the length in octets of the hash + function output) + + :return: mask, an octet string of length `length` + :rtype: bytes + + :raise OverflowError: when `length` is too large for the specified `hasher` + :raise ValueError: when specified `hasher` is invalid + """ + + try: + hash_length = pkcs1.HASH_METHODS[hasher]().digest_size + except KeyError as ex: + raise ValueError( + "Invalid `hasher` specified. Please select one of: {hash_list}".format( + hash_list=", ".join(sorted(pkcs1.HASH_METHODS.keys())) + ) + ) from ex + + # If l > 2^32(hLen), output "mask too long" and stop. + if length > (2 ** 32 * hash_length): + raise OverflowError( + "Desired length should be at most 2**32 times the hasher's output " + "length ({hash_length} for {hasher} function)".format( + hash_length=hash_length, + hasher=hasher, + ) + ) + + # Looping `counter` from 0 to ceil(l / hLen)-1, build `output` based on the + # hashes formed by (`seed` + C), being `C` an octet string of length 4 + # generated by converting `counter` with the primitive I2OSP + output = b"".join( + pkcs1.compute_hash( + seed + transform.int2bytes(counter, fill_size=4), + method_name=hasher, + ) + for counter in range(common.ceil_div(length, hash_length) + 1) + ) + + # Output the leading `length` octets of `output` as the octet string mask. + return output[:length] + + +__all__ = [ + "mgf1", +] + +if __name__ == "__main__": + print("Running doctests 1000x or until failure") + import doctest + + for count in range(1000): + (failures, tests) = doctest.testmod() + if failures: + break + + if count % 100 == 0 and count: + print("%i times" % count) + + print("Doctests done") diff --git a/module/scripts/lib/rsa/prime.py b/module/scripts/lib/rsa/prime.py new file mode 100644 index 0000000..ec486bc --- /dev/null +++ b/module/scripts/lib/rsa/prime.py @@ -0,0 +1,198 @@ +# Copyright 2011 Sybren A. Stüvel +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# https://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +"""Numerical functions related to primes. + +Implementation based on the book Algorithm Design by Michael T. Goodrich and +Roberto Tamassia, 2002. +""" + +import rsa.common +import rsa.randnum + +__all__ = ["getprime", "are_relatively_prime"] + + +def gcd(p: int, q: int) -> int: + """Returns the greatest common divisor of p and q + + >>> gcd(48, 180) + 12 + """ + + while q != 0: + (p, q) = (q, p % q) + return p + + +def get_primality_testing_rounds(number: int) -> int: + """Returns minimum number of rounds for Miller-Rabing primality testing, + based on number bitsize. + + According to NIST FIPS 186-4, Appendix C, Table C.3, minimum number of + rounds of M-R testing, using an error probability of 2 ** (-100), for + different p, q bitsizes are: + * p, q bitsize: 512; rounds: 7 + * p, q bitsize: 1024; rounds: 4 + * p, q bitsize: 1536; rounds: 3 + See: http://nvlpubs.nist.gov/nistpubs/FIPS/NIST.FIPS.186-4.pdf + """ + + # Calculate number bitsize. + bitsize = rsa.common.bit_size(number) + # Set number of rounds. + if bitsize >= 1536: + return 3 + if bitsize >= 1024: + return 4 + if bitsize >= 512: + return 7 + # For smaller bitsizes, set arbitrary number of rounds. + return 10 + + +def miller_rabin_primality_testing(n: int, k: int) -> bool: + """Calculates whether n is composite (which is always correct) or prime + (which theoretically is incorrect with error probability 4**-k), by + applying Miller-Rabin primality testing. + + For reference and implementation example, see: + https://en.wikipedia.org/wiki/Miller%E2%80%93Rabin_primality_test + + :param n: Integer to be tested for primality. + :type n: int + :param k: Number of rounds (witnesses) of Miller-Rabin testing. + :type k: int + :return: False if the number is composite, True if it's probably prime. + :rtype: bool + """ + + # prevent potential infinite loop when d = 0 + if n < 2: + return False + + # Decompose (n - 1) to write it as (2 ** r) * d + # While d is even, divide it by 2 and increase the exponent. + d = n - 1 + r = 0 + + while not (d & 1): + r += 1 + d >>= 1 + + # Test k witnesses. + for _ in range(k): + # Generate random integer a, where 2 <= a <= (n - 2) + a = rsa.randnum.randint(n - 3) + 1 + + x = pow(a, d, n) + if x == 1 or x == n - 1: + continue + + for _ in range(r - 1): + x = pow(x, 2, n) + if x == 1: + # n is composite. + return False + if x == n - 1: + # Exit inner loop and continue with next witness. + break + else: + # If loop doesn't break, n is composite. + return False + + return True + + +def is_prime(number: int) -> bool: + """Returns True if the number is prime, and False otherwise. + + >>> is_prime(2) + True + >>> is_prime(42) + False + >>> is_prime(41) + True + """ + + # Check for small numbers. + if number < 10: + return number in {2, 3, 5, 7} + + # Check for even numbers. + if not (number & 1): + return False + + # Calculate minimum number of rounds. + k = get_primality_testing_rounds(number) + + # Run primality testing with (minimum + 1) rounds. + return miller_rabin_primality_testing(number, k + 1) + + +def getprime(nbits: int) -> int: + """Returns a prime number that can be stored in 'nbits' bits. + + >>> p = getprime(128) + >>> is_prime(p-1) + False + >>> is_prime(p) + True + >>> is_prime(p+1) + False + + >>> from rsa import common + >>> common.bit_size(p) == 128 + True + """ + + assert nbits > 3 # the loop will hang on too small numbers + + while True: + integer = rsa.randnum.read_random_odd_int(nbits) + + # Test for primeness + if is_prime(integer): + return integer + + # Retry if not prime + + +def are_relatively_prime(a: int, b: int) -> bool: + """Returns True if a and b are relatively prime, and False if they + are not. + + >>> are_relatively_prime(2, 3) + True + >>> are_relatively_prime(2, 4) + False + """ + + d = gcd(a, b) + return d == 1 + + +if __name__ == "__main__": + print("Running doctests 1000x or until failure") + import doctest + + for count in range(1000): + (failures, tests) = doctest.testmod() + if failures: + break + + if count % 100 == 0 and count: + print("%i times" % count) + + print("Doctests done") diff --git a/module/scripts/lib/rsa/py.typed b/module/scripts/lib/rsa/py.typed new file mode 100644 index 0000000..6c27071 --- /dev/null +++ b/module/scripts/lib/rsa/py.typed @@ -0,0 +1 @@ +# Marker file for PEP 561. The rsa package uses inline types. diff --git a/module/scripts/lib/rsa/randnum.py b/module/scripts/lib/rsa/randnum.py new file mode 100644 index 0000000..c65facd --- /dev/null +++ b/module/scripts/lib/rsa/randnum.py @@ -0,0 +1,95 @@ +# Copyright 2011 Sybren A. Stüvel +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# https://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +"""Functions for generating random numbers.""" + +# Source inspired by code by Yesudeep Mangalapilly + +import os +import struct + +from rsa import common, transform + + +def read_random_bits(nbits: int) -> bytes: + """Reads 'nbits' random bits. + + If nbits isn't a whole number of bytes, an extra byte will be appended with + only the lower bits set. + """ + + nbytes, rbits = divmod(nbits, 8) + + # Get the random bytes + randomdata = os.urandom(nbytes) + + # Add the remaining random bits + if rbits > 0: + randomvalue = ord(os.urandom(1)) + randomvalue >>= 8 - rbits + randomdata = struct.pack("B", randomvalue) + randomdata + + return randomdata + + +def read_random_int(nbits: int) -> int: + """Reads a random integer of approximately nbits bits.""" + + randomdata = read_random_bits(nbits) + value = transform.bytes2int(randomdata) + + # Ensure that the number is large enough to just fill out the required + # number of bits. + value |= 1 << (nbits - 1) + + return value + + +def read_random_odd_int(nbits: int) -> int: + """Reads a random odd integer of approximately nbits bits. + + >>> read_random_odd_int(512) & 1 + 1 + """ + + value = read_random_int(nbits) + + # Make sure it's odd + return value | 1 + + +def randint(maxvalue: int) -> int: + """Returns a random integer x with 1 <= x <= maxvalue + + May take a very long time in specific situations. If maxvalue needs N bits + to store, the closer maxvalue is to (2 ** N) - 1, the faster this function + is. + """ + + bit_size = common.bit_size(maxvalue) + + tries = 0 + while True: + value = read_random_int(bit_size) + if value <= maxvalue: + break + + if tries % 10 == 0 and tries: + # After a lot of tries to get the right number of bits but still + # smaller than maxvalue, decrease the number of bits by 1. That'll + # dramatically increase the chances to get a large enough number. + bit_size -= 1 + tries += 1 + + return value diff --git a/module/scripts/lib/rsa/transform.py b/module/scripts/lib/rsa/transform.py new file mode 100644 index 0000000..c609b65 --- /dev/null +++ b/module/scripts/lib/rsa/transform.py @@ -0,0 +1,72 @@ +# Copyright 2011 Sybren A. Stüvel +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# https://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +"""Data transformation functions. + +From bytes to a number, number to bytes, etc. +""" + +import math + + +def bytes2int(raw_bytes: bytes) -> int: + r"""Converts a list of bytes or an 8-bit string to an integer. + + When using unicode strings, encode it to some encoding like UTF8 first. + + >>> (((128 * 256) + 64) * 256) + 15 + 8405007 + >>> bytes2int(b'\x80@\x0f') + 8405007 + + """ + return int.from_bytes(raw_bytes, "big", signed=False) + + +def int2bytes(number: int, fill_size: int = 0) -> bytes: + """ + Convert an unsigned integer to bytes (big-endian):: + + Does not preserve leading zeros if you don't specify a fill size. + + :param number: + Integer value + :param fill_size: + If the optional fill size is given the length of the resulting + byte string is expected to be the fill size and will be padded + with prefix zero bytes to satisfy that length. + :returns: + Raw bytes (base-256 representation). + :raises: + ``OverflowError`` when fill_size is given and the number takes up more + bytes than fit into the block. This requires the ``overflow`` + argument to this function to be set to ``False`` otherwise, no + error will be raised. + """ + + if number < 0: + raise ValueError("Number must be an unsigned integer: %d" % number) + + bytes_required = max(1, math.ceil(number.bit_length() / 8)) + + if fill_size > 0: + return number.to_bytes(fill_size, "big") + + return number.to_bytes(bytes_required, "big") + + +if __name__ == "__main__": + import doctest + + doctest.testmod() diff --git a/module/scripts/lib/rsa/util.py b/module/scripts/lib/rsa/util.py new file mode 100644 index 0000000..087caf8 --- /dev/null +++ b/module/scripts/lib/rsa/util.py @@ -0,0 +1,97 @@ +# Copyright 2011 Sybren A. Stüvel +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# https://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +"""Utility functions.""" + +import sys +from optparse import OptionParser + +import rsa.key + + +def private_to_public() -> None: + """Reads a private key and outputs the corresponding public key.""" + + # Parse the CLI options + parser = OptionParser( + usage="usage: %prog [options]", + description="Reads a private key and outputs the " + "corresponding public key. Both private and public keys use " + "the format described in PKCS#1 v1.5", + ) + + parser.add_option( + "-i", + "--input", + dest="infilename", + type="string", + help="Input filename. Reads from stdin if not specified", + ) + parser.add_option( + "-o", + "--output", + dest="outfilename", + type="string", + help="Output filename. Writes to stdout of not specified", + ) + + parser.add_option( + "--inform", + dest="inform", + help="key format of input - default PEM", + choices=("PEM", "DER"), + default="PEM", + ) + + parser.add_option( + "--outform", + dest="outform", + help="key format of output - default PEM", + choices=("PEM", "DER"), + default="PEM", + ) + + (cli, cli_args) = parser.parse_args(sys.argv) + + # Read the input data + if cli.infilename: + print( + "Reading private key from %s in %s format" % (cli.infilename, cli.inform), + file=sys.stderr, + ) + with open(cli.infilename, "rb") as infile: + in_data = infile.read() + else: + print("Reading private key from stdin in %s format" % cli.inform, file=sys.stderr) + in_data = sys.stdin.read().encode("ascii") + + assert type(in_data) == bytes, type(in_data) + + # Take the public fields and create a public key + priv_key = rsa.key.PrivateKey.load_pkcs1(in_data, cli.inform) + pub_key = rsa.key.PublicKey(priv_key.n, priv_key.e) + + # Save to the output file + out_data = pub_key.save_pkcs1(cli.outform) + + if cli.outfilename: + print( + "Writing public key to %s in %s format" % (cli.outfilename, cli.outform), + file=sys.stderr, + ) + with open(cli.outfilename, "wb") as outfile: + outfile.write(out_data) + else: + print("Writing public key to stdout in %s format" % cli.outform, file=sys.stderr) + sys.stdout.write(out_data.decode("ascii")) diff --git a/module/scripts/lib/telethon/__init__.py b/module/scripts/lib/telethon/__init__.py new file mode 100644 index 0000000..c3334d7 --- /dev/null +++ b/module/scripts/lib/telethon/__init__.py @@ -0,0 +1,13 @@ +from .client.telegramclient import TelegramClient +from .network import connection +from .tl.custom import Button +from .tl import patched as _ # import for its side-effects +from . import version, events, utils, errors, types, functions, custom + +__version__ = version.__version__ + +__all__ = [ + 'TelegramClient', 'Button', + 'types', 'functions', 'custom', 'errors', + 'events', 'utils', 'connection' +] diff --git a/module/scripts/lib/telethon/__pycache__/__init__.cpython-314.pyc b/module/scripts/lib/telethon/__pycache__/__init__.cpython-314.pyc new file mode 100644 index 0000000000000000000000000000000000000000..ea973851abb24aa091953280fb55d498cf8b620f GIT binary patch literal 653 zcmY*W%Wl*#6t(l1Op=+jfGUIp8WmO%G=@E5QP9ZQ02(27V1ky$;2w&HzjS9P#wvQn1z*Uld@2fFfj$ZHx~g~%MO|q%IP3s= z<~14Q$9x`p%3E^T>bM&4rA}&<+d&~(Ev2lbmauAgtNf#`>V0%1?^>wK=dmwsU0AvX zLIWcMV*_Sta%jLe;An_%sJ*;zEP-vG8`--kuj25bmM8sP<%38zWqNyMfsg}jqA*iP zddBn0y)X8E11e-}`!L3o$5L6%x-U)63MFTAn#XDji2FGh(E8-b Hn>yhiOi`^u literal 0 HcmV?d00001 diff --git a/module/scripts/lib/telethon/__pycache__/custom.cpython-314.pyc b/module/scripts/lib/telethon/__pycache__/custom.cpython-314.pyc new file mode 100644 index 0000000000000000000000000000000000000000..66a88176ee646c3d5a1a67738d1a68e41832e504 GIT binary patch literal 236 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 RN%Ygn6H}Gc&rSop{{ti9oX7wG literal 0 HcmV?d00001 diff --git a/module/scripts/lib/telethon/__pycache__/password.cpython-314.pyc b/module/scripts/lib/telethon/__pycache__/password.cpython-314.pyc new file mode 100644 index 0000000000000000000000000000000000000000..75cbec0c40d6d55959c746bdef00a85dd14dae80 GIT binary patch literal 12385 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 literal 0 HcmV?d00001 diff --git a/module/scripts/lib/telethon/__pycache__/requestiter.cpython-314.pyc b/module/scripts/lib/telethon/__pycache__/requestiter.cpython-314.pyc new file mode 100644 index 0000000000000000000000000000000000000000..7bd90242e8a68b3dcac5aea209adccfc5ecc65b2 GIT binary patch literal 6505 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 N44fbMFQYyu@_*?$-WdP@ literal 0 HcmV?d00001 diff --git a/module/scripts/lib/telethon/__pycache__/sync.cpython-314.pyc b/module/scripts/lib/telethon/__pycache__/sync.cpython-314.pyc new file mode 100644 index 0000000000000000000000000000000000000000..4d89672eba119f697a9081d740ba282e78f3a51a GIT binary patch literal 3072 zcmZ8jO>7&-6`tikmm(>P`mq(+8YQVD$_gpN0%{a)q{fE2tQ}XSUAhM`tJQKyZoS-P zW`-h|B7l%f)k~{hS{;2)s(-pSyY_`pXqXHH8ju^PSHYg!DqjD}?zHg37Kj!kgavTaOBms*@)1IF#nj%}H^MR?P#F>E=QH_1S2Ih@eC zVG`A?OTBIFP=od`bH!&5n}&mF$Dd=nZX3IDY*FlX9c;P7nF0c@%hC7T<2+=iqOpKW@r~}Lr37C9Htm$^_8iq*J zaF+2-hx;Hb$E2iY?btmLq294^-KF^Bj%7YjnMtiSXV`$H#F7A#TP=d!T|%1x8^b|T zVjJArB{1K1y9HIf>w*Vt$_9<;wpy_D0itfxGMm_-1UC&@^H;24w_b-`aB(Zfj%8cC zhj)5vjns{f&6hFjh&c>zZxqW0Vl z+;tCk4TodyVi{t2up|GLWCZ;G5ilTylkPx_2CUt4OqJ{sh|q+iunA@Pf_f((^x5?uJ)`d6n>0rHrPKomdSV+M%uZz_$`AE8r_ugl86= zS?(*9(3!p*g3Jz^zMC%v(pmWR4?tX3_R!tP9IBuqswf{Q5d^FFejqo8Xoo5^hlY5< zmGGvB_Qo@?F9iA;pqxP!Kmz{59h^-9yuqFUt8ynFp)oLeqYc9KAn|okCOp+*1BEws zMs7%>woNzz#zhR;08TuBn}&D9bgAn_~Ge7wRV!4d6JrWo}1dg^;K@_B)9Z9w{*06oLl)~WTg*b&?iY<=eAq`j?at3$r+r} zAWvKuk}fK+FfY!06L=}yOZkVQJ4TDpIS51rF_>WH{5)Q#7zSSYTic;apt}e^whjU| zaGD-J_{nkl-Mx)}Ml-+EKhqB?$I+SBp;+`vKa64*zZRp{V<1Er75!%*9PXy*HjYw3QsF?vD*}d4ORMJBaoph!%8Q$o!%A zEV^|cp(MJ5Zdv}j90s;WKKY~S7aY+?ps|WRGUx|ES8YK1fc_{7A6_(VgE8oKb<442 z0nDq=ZlQ1k`o&Oe}~g8VG@YL!rDvt;5j6WXF;%yvD+;UhO zdXAS9?I&1!P`c^6aHf(?`7jmdOBX?S5y3?!ZgUA^pMdcoo#<827U0J=LF}Ra7|P`K zmDBO5L*;iH`!^v$e!uqHwTD+vatlv#3rDMuzW40%_fK=vC%M9tT;UJhM;{*-*G_Wl zPjl<1<8u$^o{cZ{<7nb7;Ati&spzG2S#{*O5dwV>6}L+TKq{&u71W`k zs^s7H;<{dQOD;TEfLbs*Bf8OP<5A;7J zA}_@4HvoA_$bY{T(h;$y_&-ZCLS%)IdpIXXV?yLWc;mw<1rwSaY#f>#9T%p_GZT{? zFECDs$)WKua&smA|3I$7u1JP*@3lgFy6!_)h{U@4Y}PuVNWx;nmw|66l*E4K zFmn_=+I{rt^TgP}?S~6T-+grZPs^`ijZNj{2vRcp_Th(5(bB6(Bo+EBF5&$Tx|J|m literal 0 HcmV?d00001 diff --git a/module/scripts/lib/telethon/__pycache__/types.cpython-314.pyc b/module/scripts/lib/telethon/__pycache__/types.cpython-314.pyc new file mode 100644 index 0000000000000000000000000000000000000000..a25eb4cfff5c3385c7b7b54c6b923b92e169eb19 GIT binary patch literal 234 zcmdPqVNJ$c zj9Pv`lA|O?ucWddwRk1NXOO;InffV-C5if_#i>R52KveQxq6A|mHKI!IjP0^dg-aT znR%J|iFqZN=|zcUnI)CFi3J7v#mPm9CCM53VJ^B(iOJce1^T)9DWy57Ac@R^l4AXw x%q0Di)ST3kjQl)(uyuL`mA5!-Aj<5DIDj^ToK*~Bd|+l|WW2{9Uc?6E004G^Keqq? literal 0 HcmV?d00001 diff --git a/module/scripts/lib/telethon/__pycache__/utils.cpython-314.pyc b/module/scripts/lib/telethon/__pycache__/utils.cpython-314.pyc new file mode 100644 index 0000000000000000000000000000000000000000..390614e8f02e64359e8217f09e7022455a74b156 GIT binary patch literal 70777 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�>>Y_ti4^_54wu`ocbRY=-`i{|71tI0^8UA5HG~&KF)Q4yCqtP zQk~z@Vq+0&c8F^Pp+clf*BeL&G?}rCbxpD=5D_vUZsj7iSX@F*syOogBPZi14pa?x z?ICuq%d0?)CRq$Y%SGlKiA#up3%e45P|*=~g4fMoMIS)LfX!DnH!mZAp{Ec(2cBaa<2h!DM)R=6 zLQYUHq;HZpLnvy?n?K?*qiOqrrW`Fu4OdZ~dbiBGnaUl$N)#54b1Y7e#(CuCwsj(*{dI& z;n|_0I+;CY9p<58<6|Z8e}oK3D_G=>{Q?Gf^4N)l+ZOxTUASoS?Czi7fPx;{=NCG^ zvUSnsm|yda?pwA>9Pf0ldTsY@o8znd=XL?%oL@6v^z|*T?f$u)%PSr(nOi{0X)7-18UL+Dts?m)Is z7E$_I-M=aM2<56%sYWO@=Ke<|o-da5X)I5eLBaCGs6T9(bZ8L3nz#Y{zoxbFZ{T6* z$et5es{D2Qge@pib>+W>cg@igCIkO2zVD~e<5^cOvN5yS&-VVrQ8cH=vD!sj{)NZB zy6?5#pE?Q`^PCrEuFNd>7mj}Ov`_0)!vvqFfjhT7>{L!z!e9KXL%UnzIA@}TqWwk(%X)u`60(-k~GB4MZf0Q`+{2AA@_!-y>8S28dF%q5z zEQ->c_MVY->s%qk0!TOkW8Vv-JM&UfxKJ+ESUI3{6n5H!TBoo-hA|wK$JtH4FwAsv+4!XtA2E)IDTMqhSp54!KIo*XPCcJSxAPnLp@ZcQ)Pm6~XQfZ0 z_bAo0>A3E&ff3zc5%_@gfvI3*>;lYhcEBC2CnMKuI7n~JL;B--)=tJ)z^@#3?hG3; z_D-{w4vjL;2W3K8To!ncxg3_~Ug%30Hi?WFY22#0gV*tu_2rmUqMeoKUQvk}3>qqt z{vls`7nN$1&QPgJoy1o&D`(4!${}hZ&5@}ARogXKI?Anr%KgErI;=+>EUXT-E2@JA z14b!9$yZe;4aNudZ!*KA^JK$}kq-CNnmuORQ=20+Zbj>$iRwItyfGuhb>O>8zqD43 z$H}Bce=Eio)<6}D{h&2qQO#z|RhrHG@58v<(^};mZ(}{VvYf4|oH6T!ocRw9wrb{l zkmX#xqB`4DIipXJ^9�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 literal 0 HcmV?d00001 diff --git a/module/scripts/lib/telethon/__pycache__/version.cpython-314.pyc b/module/scripts/lib/telethon/__pycache__/version.cpython-314.pyc new file mode 100644 index 0000000000000000000000000000000000000000..a334d91f8f0d7daa3560ef8e0e08649637c605e9 GIT binary patch literal 235 zcmXv|yAASQ;==X3Ba@1!iRWF> z+ggePY^kP@DJEe6ei{=R xZ5AS44VkB}?lLqpVeS_MK-+@12WMHv` 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

UBN9{jo|IzLr?2hm6 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 literal 0 HcmV?d00001 diff --git a/module/scripts/lib/telethon/_updates/__pycache__/session.cpython-314.pyc b/module/scripts/lib/telethon/_updates/__pycache__/session.cpython-314.pyc new file mode 100644 index 0000000000000000000000000000000000000000..f3ad0a19c415c98a7a8da2120fbf503ae5032f0c GIT binary patch literal 10463 zcmd5?eQX@Zb)UV@z0;A$Z&4qX)}l=DWb)3DXi28!*d)bJOe{)1QB-70J}&Q;tK_hrvG(`U1Hxx2L&Fo>8^lxR! zK)1)5(Fk)~5IE7rZQ-~OaN!HSZm-=lav^eRyF*arxgmCErH@nFutm%# zAE$=C>tpvgJu1%)`|hpuacUd(ZFhQ9p6I&{HW4(MI5)R|n-Q18%}HMyi`D#rhQ~an zc(Pz*)x4ZD{UgOfPKgO-a42sKx@9sx0M;e zq~8!nnGs3`K(S~>PAj?&T2C9Yp~xbXBYfIWHAR<xkcwDZJRIK7E2(UzM>^EqHJ&vj@L*igBuy#c zBj}mUPx65@K+0waGm)jS`=v~p4?K8a<-i>1p+PDi2O{J&My(hvU71oerD|TC<{MXy z9w}?BET>LRDw(cqo)aj^^zv!+DH!@{Qm52h#-UOtHf_WT`o<+1FXuo`9%NotRxSH0 z%|-m(j2L>q87;bU$r;{dvwg+)l}kk>pT-oHNH!2Kt;&!uDv)rM9M_w$B0Wi+SfLH} zcuBXE4T>?Wf_4K6FX`&E;`FyD(WNWdTuxH+xfzmSfP)s9TaR%h{Yfo>L@f z6hwPmy`o%Jv`z`yDoqrPqNa2eG*z7-v1y8|t9iXsx}u<-R;Q;GXzYwyl%RnI)uHJ# zk_>&ZRWzsOC*$EXOf#b&m3sWha4tU&a8#IOqcF@z{*ti_ushjv!jzB{P75!IT&D46LZ$(vrt z*9=!g1JmNo<_*&~uBthWsD&vCSR2@^Nu|;`S=Te!w2?~b^p!ocrjelSlc2999!r2X zad~nkF_FzFdLlllOlR}igp3_IsmYhK#!Q!7C?xc>CS(66Mvry%$?1#5LSkCY6mtr_ z$b!T~E<2tul$>Hrsrf|8HWLX)^o1ESl1jm{skD_+DRW!31&UX{!l}vh6u_6W>^^Jo zKe9yNv-+m8pBe#H*H{ixO=PtV5Kv23qP?=?C>+;SUc2>$eO2N)PmUrwiJNrzYSkBAYQ+c0T zAItmQdLbWh>$CZwQ%|xsHZKg%i&_m(8hHm=lt3K;(mkz#KqG-BfLO@%=}KN`BrF` zT%OQ~b$DW-i5<(20?e{|(Z&z=ejNY1_+nG#uf3plI0Q|)^=mee3)dEm#1l$gi|SNo1u>VbEzoj z>^IR=Y9g!YMlPFI@+z${mCC5;R7xZNMSGaQegLx>Y{25P=@fAwJ6^=;jqdeTmLaX= za;en3pmE_7W1r!S(MXCMkpAi&0AIcgCMp@S8XfNNc6@*SR!=70*la8Cst7x{Y7^jyu8?0EE?ReyR6vpZ` z@&A*Z()Q9aX-`$#B74uST9jpmdT9XJBCU(SQvkzSoT`Ta^s0S(gpdRtqZj)C?0xIU zzKyW@cG|WOSDB=Ezi#!mrIBg;+~Npy`6ocF2tNiY+xCJVVX4Ql%SM2=y>2_m+dj7~ z^0wb?hj=^Sw!^#~blY2aTXfrzBtl!GCK)zr5!ghNTcG?AGduvl#h4kHDJXI)bT51k z9$#9{!v}<0iFk(M8v}2p%W|%Wsw#~u5?o?A=Y$r$r~8M7$R=oVn&JyJPmXF2MK$}* z?2|f9lDYB6Ro0lAI{FjPsFdW>Ap*co_MV~9pniaX%)06#VcXKrka-0FRO znB~x+uQtod83r-K;je8rgFY?8nV!_tV!@&bDrH!o@2Afn-gK~m@J`k;(zvF|nY64M z?i>`GTGN~$8aAC~TA7qrQcxK^Ktw#T2@!c|4-PhMS}p2(q&<^ad#XL_P%=!E9NA

Dau6Bl# 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 It may be useful to wait up to 0.5 seconds +POSSIBLE_GAP_TIMEOUT = 0.5 + +# After how long without updates the client will "timeout". +# +# When this timeout occurs, the client will attempt to fetch updates by itself, ignoring all the +# updates that arrive in the meantime. After all updates are fetched when this happens, the +# client will resume normal operation, and the timeout will reset. +# +# Documentation recommends 15 minutes without updates (https://core.telegram.org/api/updates). +NO_UPDATES_TIMEOUT = 15 * 60 + +# object() but with a tag to make it easier to debug +class Sentinel: + __slots__ = ('tag',) + + def __init__(self, tag=None): + self.tag = tag or '_' + + def __repr__(self): + return self.tag + +# Entry "enum". +# Account-wide `pts` includes private conversations (one-to-one) and small group chats. +ENTRY_ACCOUNT = Sentinel('ACCOUNT') +# Account-wide `qts` includes only "secret" one-to-one chats. +ENTRY_SECRET = Sentinel('SECRET') +# Integers will be Channel-specific `pts`, and includes "megagroup", "broadcast" and "supergroup" channels. + +# Python's logging doesn't define a TRACE level. Pick halfway between DEBUG and NOTSET. +# We don't define a name for this as libraries shouldn't do that though. +LOG_LEVEL_TRACE = (logging.DEBUG - logging.NOTSET) // 2 + +_sentinel = Sentinel() + +def next_updates_deadline(): + return get_running_loop().time() + NO_UPDATES_TIMEOUT + +def epoch(): + return datetime.datetime(*time.gmtime(0)[:6]).replace(tzinfo=datetime.timezone.utc) + +class GapError(ValueError): + def __repr__(self): + return 'GapError()' + + +class PrematureEndReason(Enum): + TEMPORARY_SERVER_ISSUES = 'tmp' + BANNED = 'ban' + + +# Represents the information needed to correctly handle a specific `tl::enums::Update`. +class PtsInfo: + __slots__ = ('pts', 'pts_count', 'entry') + + def __init__( + self, + pts: int, + pts_count: int, + entry: object + ): + self.pts = pts + self.pts_count = pts_count + self.entry = entry + + @classmethod + def from_update(cls, update): + pts = getattr(update, 'pts', None) + if pts: + pts_count = getattr(update, 'pts_count', None) or 0 + try: + entry = update.message.peer_id.channel_id + except AttributeError: + entry = getattr(update, 'channel_id', None) or ENTRY_ACCOUNT + return cls(pts=pts, pts_count=pts_count, entry=entry) + + qts = getattr(update, 'qts', None) + if qts: + return cls(pts=qts, pts_count=1, entry=ENTRY_SECRET) + + return None + + def __repr__(self): + return f'PtsInfo(pts={self.pts}, pts_count={self.pts_count}, entry={self.entry})' + + +# The state of a particular entry in the message box. +class State: + __slots__ = ('pts', 'deadline') + + def __init__( + self, + # Current local persistent timestamp. + pts: int, + # Next instant when we would get the update difference if no updates arrived before then. + deadline: float + ): + self.pts = pts + self.deadline = deadline + + def __repr__(self): + return f'State(pts={self.pts}, deadline={self.deadline})' + + +# > ### Recovering gaps +# > […] Manually obtaining updates is also required in the following situations: +# > • Loss of sync: a gap was found in `seq` / `pts` / `qts` (as described above). +# > It may be useful to wait up to 0.5 seconds in this situation and abort the sync in case a new update +# > arrives, that fills the gap. +# +# This is really easy to trigger by spamming messages in a channel (with as little as 3 members works), because +# the updates produced by the RPC request take a while to arrive (whereas the read update comes faster alone). +class PossibleGap: + __slots__ = ('deadline', 'updates') + + def __init__( + self, + deadline: float, + # Pending updates (those with a larger PTS, producing the gap which may later be filled). + updates: list # of updates + ): + self.deadline = deadline + self.updates = updates + + def __repr__(self): + return f'PossibleGap(deadline={self.deadline}, update_count={len(self.updates)})' + + +# Represents a "message box" (event `pts` for a specific entry). +# +# See https://core.telegram.org/api/updates#message-related-event-sequences. +class MessageBox: + __slots__ = ('_log', 'map', 'date', 'seq', 'next_deadline', 'possible_gaps', 'getting_diff_for') + + def __init__( + self, + log, + # Map each entry to their current state. + map: dict = _sentinel, # entry -> state + + # Additional fields beyond PTS needed by `ENTRY_ACCOUNT`. + date: datetime.datetime = epoch() + datetime.timedelta(seconds=1), + seq: int = NO_SEQ, + + # Holds the entry with the closest deadline (optimization to avoid recalculating the minimum deadline). + next_deadline: object = None, # entry + + # Which entries have a gap and may soon trigger a need to get difference. + # + # If a gap is found, stores the required information to resolve it (when should it timeout and what updates + # should be held in case the gap is resolved on its own). + # + # Not stored directly in `map` as an optimization (else we would need another way of knowing which entries have + # a gap in them). + possible_gaps: dict = _sentinel, # entry -> possiblegap + + # For which entries are we currently getting difference. + getting_diff_for: set = _sentinel, # entry + ): + self._log = log + self.map = {} if map is _sentinel else map + self.date = date + self.seq = seq + self.next_deadline = next_deadline + self.possible_gaps = {} if possible_gaps is _sentinel else possible_gaps + self.getting_diff_for = set() if getting_diff_for is _sentinel else getting_diff_for + + if __debug__: + self._trace('MessageBox initialized') + + def _trace(self, msg, *args, **kwargs): + # Calls to trace can't really be removed beforehand without some dark magic. + # So every call to trace is prefixed with `if __debug__`` instead, to remove + # it when using `python -O`. Probably unnecessary, but it's nice to avoid + # paying the cost for something that is not used. + self._log.log(LOG_LEVEL_TRACE, 'Current MessageBox state: seq = %r, date = %s, map = %r', + self.seq, self.date.isoformat(), self.map) + self._log.log(LOG_LEVEL_TRACE, msg, *args, **kwargs) + + # region Creation, querying, and setting base state. + + def load(self, session_state, channel_states): + """ + Create a [`MessageBox`] from a previously known update state. + """ + if __debug__: + self._trace('Loading MessageBox with session_state = %r, channel_states = %r', session_state, channel_states) + + deadline = next_updates_deadline() + + self.map.clear() + if session_state.pts != NO_SEQ: + self.map[ENTRY_ACCOUNT] = State(pts=session_state.pts, deadline=deadline) + if session_state.qts != NO_SEQ: + self.map[ENTRY_SECRET] = State(pts=session_state.qts, deadline=deadline) + self.map.update((s.channel_id, State(pts=s.pts, deadline=deadline)) for s in channel_states) + + self.date = datetime.datetime.fromtimestamp(session_state.date, tz=datetime.timezone.utc) + self.seq = session_state.seq + self.next_deadline = ENTRY_ACCOUNT + + def session_state(self): + """ + Return the current state. + + This should be used for persisting the state. + """ + return dict( + pts=self.map[ENTRY_ACCOUNT].pts if ENTRY_ACCOUNT in self.map else NO_SEQ, + qts=self.map[ENTRY_SECRET].pts if ENTRY_SECRET in self.map else NO_SEQ, + date=self.date, + seq=self.seq, + ), {id: state.pts for id, state in self.map.items() if isinstance(id, int)} + + def is_empty(self) -> bool: + """ + Return true if the message box is empty and has no state yet. + """ + return ENTRY_ACCOUNT not in self.map + + def check_deadlines(self): + """ + Return the next deadline when receiving updates should timeout. + + If a deadline expired, the corresponding entries will be marked as needing to get its difference. + While there are entries pending of getting their difference, this method returns the current instant. + """ + now = get_running_loop().time() + + if self.getting_diff_for: + return now + + deadline = next_updates_deadline() + + # Most of the time there will be zero or one gap in flight so finding the minimum is cheap. + if self.possible_gaps: + deadline = min(deadline, *(gap.deadline for gap in self.possible_gaps.values())) + elif self.next_deadline in self.map: + deadline = min(deadline, self.map[self.next_deadline].deadline) + + # asyncio's loop time precision only seems to be about 3 decimal places, so it's possible that + # we find the same number again on repeated calls. Without the "or equal" part we would log the + # timeout for updates several times (it also makes sense to get difference if now is the deadline). + if now >= deadline: + # Check all expired entries and add them to the list that needs getting difference. + self.getting_diff_for.update(entry for entry, gap in self.possible_gaps.items() if now >= gap.deadline) + self.getting_diff_for.update(entry for entry, state in self.map.items() if now >= state.deadline) + + if __debug__: + self._trace('Deadlines met, now getting diff for %r', self.getting_diff_for) + + # When extending `getting_diff_for`, it's important to have the moral equivalent of + # `begin_get_diff` (that is, clear possible gaps if we're now getting difference). + for entry in self.getting_diff_for: + self.possible_gaps.pop(entry, None) + + return deadline + + # Reset the deadline for the periods without updates for the given entries. + # + # It also updates the next deadline time to reflect the new closest deadline. + def reset_deadlines(self, entries, deadline): + if not entries: + return + for entry in entries: + if entry not in self.map: + raise RuntimeError('Called reset_deadline on an entry for which we do not have state') + self.map[entry].deadline = deadline + + if self.next_deadline in entries: + # If the updated deadline was the closest one, recalculate the new minimum. + self.next_deadline = min(self.map.items(), key=lambda entry_state: entry_state[1].deadline)[0] + elif self.next_deadline in self.map and deadline < self.map[self.next_deadline].deadline: + # If the updated deadline is smaller than the next deadline, change the next deadline to be the new one. + # Any entry will do, so the one from the last iteration is fine. + self.next_deadline = entry + # else an unrelated deadline was updated, so the closest one remains unchanged. + + # Convenience to reset a channel's deadline, with optional timeout. + def reset_channel_deadline(self, channel_id, timeout): + self.reset_deadlines({channel_id}, get_running_loop().time() + (timeout or NO_UPDATES_TIMEOUT)) + + # Sets the update state. + # + # Should be called right after login if [`MessageBox::new`] was used, otherwise undesirable + # updates will be fetched. + def set_state(self, state, reset=True): + if __debug__: + self._trace('Setting state %s', state) + + deadline = next_updates_deadline() + + if state.pts != NO_SEQ or not reset: + self.map[ENTRY_ACCOUNT] = State(pts=state.pts, deadline=deadline) + else: + self.map.pop(ENTRY_ACCOUNT, None) + + # Telegram seems to use the `qts` for bot accounts, but while applying difference, + # it might be reset back to 0. See issue #3873 for more details. + # + # During login, a value of zero would mean the `pts` is unknown, + # so the map shouldn't contain that entry. + # But while applying difference, if the value is zero, it (probably) + # truly means that's what should be used (hence the `reset` flag). + if state.qts != NO_SEQ or not reset: + self.map[ENTRY_SECRET] = State(pts=state.qts, deadline=deadline) + else: + self.map.pop(ENTRY_SECRET, None) + + self.date = state.date + self.seq = state.seq + + # Like [`MessageBox::set_state`], but for channels. Useful when getting dialogs. + # + # The update state will only be updated if no entry was known previously. + def try_set_channel_state(self, id, pts): + if __debug__: + self._trace('Trying to set channel state for %r: %r', id, pts) + + if id not in self.map: + self.map[id] = State(pts=pts, deadline=next_updates_deadline()) + + # Try to begin getting difference for the given entry. + # Fails if the entry does not have a previously-known state that can be used to get its difference. + # + # Clears any previous gaps. + def try_begin_get_diff(self, entry, reason): + if entry not in self.map: + # Won't actually be able to get difference for this entry if we don't have a pts to start off from. + if entry in self.possible_gaps: + raise RuntimeError('Should not have a possible_gap for an entry not in the state map') + + if __debug__: + self._trace('Should get difference for %r because %s but cannot due to missing hash', entry, reason) + return + + if __debug__: + self._trace('Marking %r as needing difference because %s', entry, reason) + self.getting_diff_for.add(entry) + self.possible_gaps.pop(entry, None) + + # Finish getting difference for the given entry. + # + # It also resets the deadline. + def end_get_diff(self, entry): + try: + self.getting_diff_for.remove(entry) + except KeyError: + raise RuntimeError('Called end_get_diff on an entry which was not getting diff for') + + self.reset_deadlines({entry}, next_updates_deadline()) + assert entry not in self.possible_gaps, "gaps shouldn't be created while getting difference" + + # endregion Creation, querying, and setting base state. + + # region "Normal" updates flow (processing and detection of gaps). + + # Process an update and return what should be done with it. + # + # Updates corresponding to entries for which their difference is currently being fetched + # will be ignored. While according to the [updates' documentation]: + # + # > Implementations [have] to postpone updates received via the socket while + # > filling gaps in the event and `Update` sequences, as well as avoid filling + # > gaps in the same sequence. + # + # In practice, these updates should have also been retrieved through getting difference. + # + # [updates documentation] https://core.telegram.org/api/updates + def process_updates( + self, + updates, + chat_hashes, + result, # out list of updates; returns list of user, chat, or raise if gap + ): + + # v1 has never sent updates produced by the client itself to the handlers. + # However proper update handling requires those to be processed. + # This is an ugly workaround for that. + self_outgoing = getattr(updates, '_self_outgoing', False) + real_result = result + result = [] + + date = getattr(updates, 'date', None) + seq = getattr(updates, 'seq', None) + seq_start = getattr(updates, 'seq_start', None) + users = getattr(updates, 'users', None) or [] + chats = getattr(updates, 'chats', None) or [] + + if __debug__: + self._trace('Processing updates with seq = %r, seq_start = %r, date = %s: %s', + seq, seq_start, date.isoformat() if date else None, updates) + + if date is None: + # updatesTooLong is the only one with no date (we treat it as a gap) + self.try_begin_get_diff(ENTRY_ACCOUNT, 'received updatesTooLong') + raise GapError + if seq is None: + seq = NO_SEQ + if seq_start is None: + seq_start = seq + + # updateShort is the only update which cannot be dispatched directly but doesn't have 'updates' field + updates = getattr(updates, 'updates', None) or [updates.update if isinstance(updates, tl.UpdateShort) else updates] + + for u in updates: + u._self_outgoing = self_outgoing + + # > For all the other [not `updates` or `updatesCombined`] `Updates` type constructors + # > there is no need to check `seq` or change a local state. + if seq_start != NO_SEQ: + if self.seq + 1 > seq_start: + # Skipping updates that were already handled + if __debug__: + self._trace('Skipping updates as they should have already been handled') + return (users, chats) + elif self.seq + 1 < seq_start: + # Gap detected + self.try_begin_get_diff(ENTRY_ACCOUNT, 'detected gap') + raise GapError + # else apply + + def _sort_gaps(update): + pts = PtsInfo.from_update(update) + return pts.pts - pts.pts_count if pts else 0 + + reset_deadlines = set() # temporary buffer + + result.extend(filter(None, ( + self.apply_pts_info(u, reset_deadlines=reset_deadlines) + # Telegram can send updates out of order (e.g. ReadChannelInbox first + # and then NewChannelMessage, both with the same pts, but the count is + # 0 and 1 respectively), so we sort them first. + for u in sorted(updates, key=_sort_gaps)))) + + self.reset_deadlines(reset_deadlines, next_updates_deadline()) + + if self.possible_gaps: + if __debug__: + self._trace('Trying to re-apply %r possible gaps', len(self.possible_gaps)) + + # For each update in possible gaps, see if the gap has been resolved already. + for key in list(self.possible_gaps.keys()): + self.possible_gaps[key].updates.sort(key=_sort_gaps) + + for _ in range(len(self.possible_gaps[key].updates)): + update = self.possible_gaps[key].updates.pop(0) + + # If this fails to apply, it will get re-inserted at the end. + # All should fail, so the order will be preserved (it would've cycled once). + update = self.apply_pts_info(update, reset_deadlines=None) + if update: + result.append(update) + if __debug__: + self._trace('Resolved gap with %r: %s', PtsInfo.from_update(update), update) + + # Clear now-empty gaps. + self.possible_gaps = {entry: gap for entry, gap in self.possible_gaps.items() if gap.updates} + + real_result.extend(u for u in result if not u._self_outgoing) + + if result and not self.possible_gaps: + # > If the updates were applied, local *Updates* state must be updated + # > with `seq` (unless it's 0) and `date` from the constructor. + if __debug__: + self._trace('Updating seq as all updates were applied') + if date != epoch(): + self.date = date + if seq != NO_SEQ: + self.seq = seq + + return (users, chats) + + # Tries to apply the input update if its `PtsInfo` follows the correct order. + # + # If the update can be applied, it is returned; otherwise, the update is stored in a + # possible gap (unless it was already handled or would be handled through getting + # difference) and `None` is returned. + def apply_pts_info( + self, + update, + *, + reset_deadlines, + ): + # This update means we need to call getChannelDifference to get the updates from the channel + if isinstance(update, tl.UpdateChannelTooLong): + self.try_begin_get_diff(update.channel_id, 'received updateChannelTooLong') + return None + + pts = PtsInfo.from_update(update) + if not pts: + # No pts means that the update can be applied in any order. + if __debug__: + self._trace('No pts in update, so it can be applied in any order: %s', update) + return update + + # As soon as we receive an update of any form related to messages (has `PtsInfo`), + # the "no updates" period for that entry is reset. + # + # Build the `HashSet` to avoid calling `reset_deadline` more than once for the same entry. + # + # By the time this method returns, self.map will have an entry for which we can reset its deadline. + if reset_deadlines: + reset_deadlines.add(pts.entry) + + if pts.entry in self.getting_diff_for: + # Note: early returning here also prevents gap from being inserted (which they should + # not be while getting difference). + if __debug__: + self._trace('Skipping update with %r as its difference is being fetched', pts) + return None + + if pts.entry in self.map: + local_pts = self.map[pts.entry].pts + if local_pts + pts.pts_count > pts.pts: + # Ignore + if __debug__: + self._trace('Skipping update since local pts %r > %r: %s', local_pts, pts, update) + return None + elif local_pts + pts.pts_count < pts.pts: + # Possible gap + # TODO store chats too? + if __debug__: + self._trace('Possible gap since local pts %r < %r: %s', local_pts, pts, update) + if pts.entry not in self.possible_gaps: + self.possible_gaps[pts.entry] = PossibleGap( + deadline=get_running_loop().time() + POSSIBLE_GAP_TIMEOUT, + updates=[] + ) + + self.possible_gaps[pts.entry].updates.append(update) + return None + else: + # Apply + if __debug__: + self._trace('Applying update pts since local pts %r = %r: %s', local_pts, pts, update) + + # In a channel, we may immediately receive: + # * ReadChannelInbox (pts = X, pts_count = 0) + # * NewChannelMessage (pts = X, pts_count = 1) + # + # Notice how both `pts` are the same. If they were to be applied out of order, the first + # one however would've triggered a gap because `local_pts` + `pts_count` of 0 would be + # less than `remote_pts`. So there is no risk by setting the `local_pts` to match the + # `remote_pts` here of missing the new message. + # + # The message would however be lost if we initialized the pts with the first one, since + # the second one would appear "already handled". To prevent this we set the pts to be + # one less when the count is 0 (which might be wrong and trigger a gap later on, but is + # unlikely). This will prevent us from losing updates in the unlikely scenario where these + # two updates arrive in different packets (and therefore couldn't be sorted beforehand). + if pts.entry in self.map: + self.map[pts.entry].pts = pts.pts + else: + # When a chat is migrated to a megagroup, the first update can be a `ReadChannelInbox` + # with `pts = 1, pts_count = 0` followed by a `NewChannelMessage` with `pts = 2, pts_count=1`. + # Note how the `pts` for the message is 2 and not 1 unlike the case described before! + # This is likely because the `pts` cannot be 0 (or it would fail with PERSISTENT_TIMESTAMP_EMPTY), + # which forces the first update to be 1. But if we got difference with 1 and the second update + # also used 1, we would miss it, so Telegram probably uses 2 to work around that. + self.map[pts.entry] = State( + pts=(pts.pts - (0 if pts.pts_count else 1)) or 1, + deadline=next_updates_deadline() + ) + + return update + + # endregion "Normal" updates flow (processing and detection of gaps). + + # region Getting and applying account difference. + + # Return the request that needs to be made to get the difference, if any. + def get_difference(self): + for entry in (ENTRY_ACCOUNT, ENTRY_SECRET): + if entry in self.getting_diff_for: + if entry not in self.map: + raise RuntimeError('Should not try to get difference for an entry without known state') + + gd = fn.updates.GetDifferenceRequest( + pts=self.map[ENTRY_ACCOUNT].pts, + pts_total_limit=None, + date=self.date, + qts=self.map[ENTRY_SECRET].pts if ENTRY_SECRET in self.map else NO_SEQ, + ) + if __debug__: + self._trace('Requesting account difference %s', gd) + return gd + + return None + + # Similar to [`MessageBox::process_updates`], but using the result from getting difference. + def apply_difference( + self, + diff, + chat_hashes, + ): + if __debug__: + self._trace('Applying account difference %s', diff) + + finish = None + result = None + + if isinstance(diff, tl.updates.DifferenceEmpty): + finish = True + self.date = diff.date + self.seq = diff.seq + result = [], [], [] + elif isinstance(diff, tl.updates.Difference): + finish = True + chat_hashes.extend(diff.users, diff.chats) + result = self.apply_difference_type(diff, chat_hashes) + elif isinstance(diff, tl.updates.DifferenceSlice): + finish = False + chat_hashes.extend(diff.users, diff.chats) + result = self.apply_difference_type(diff, chat_hashes) + elif isinstance(diff, tl.updates.DifferenceTooLong): + finish = True + self.map[ENTRY_ACCOUNT].pts = diff.pts # the deadline will be reset once the diff ends + result = [], [], [] + + if finish: + account = ENTRY_ACCOUNT in self.getting_diff_for + secret = ENTRY_SECRET in self.getting_diff_for + + if not account and not secret: + raise RuntimeError('Should not be applying the difference when neither account or secret was diff was active') + + # Both may be active if both expired at the same time. + if account: + self.end_get_diff(ENTRY_ACCOUNT) + if secret: + self.end_get_diff(ENTRY_SECRET) + + return result + + def apply_difference_type( + self, + diff, + chat_hashes, + ): + state = getattr(diff, 'intermediate_state', None) or diff.state + self.set_state(state, reset=False) + + # diff.other_updates can contain things like UpdateChannelTooLong and UpdateNewChannelMessage. + # We need to process those as if they were socket updates to discard any we have already handled. + updates = [] + self.process_updates(tl.Updates( + updates=diff.other_updates, + users=diff.users, + chats=diff.chats, + date=epoch(), + seq=NO_SEQ, # this way date is not used + ), chat_hashes, updates) + + updates.extend(tl.UpdateNewMessage( + message=m, + pts=NO_SEQ, + pts_count=NO_SEQ, + ) for m in diff.new_messages) + updates.extend(tl.UpdateNewEncryptedMessage( + message=m, + qts=NO_SEQ, + ) for m in diff.new_encrypted_messages) + + return updates, diff.users, diff.chats + + def end_difference(self): + if __debug__: + self._trace('Ending account difference') + + account = ENTRY_ACCOUNT in self.getting_diff_for + secret = ENTRY_SECRET in self.getting_diff_for + + if not account and not secret: + raise RuntimeError('Should not be ending get difference when neither account or secret was diff was active') + + # Both may be active if both expired at the same time. + if account: + self.end_get_diff(ENTRY_ACCOUNT) + if secret: + self.end_get_diff(ENTRY_SECRET) + + # endregion Getting and applying account difference. + + # region Getting and applying channel difference. + + # Return the request that needs to be made to get a channel's difference, if any. + def get_channel_difference( + self, + chat_hashes, + ): + entry = next((id for id in self.getting_diff_for if isinstance(id, int)), None) + if not entry: + return None + + packed = chat_hashes.get(entry) + if not packed: + # Cannot get channel difference as we're missing its hash + # TODO we should probably log this + self.end_get_diff(entry) + # Remove the outdated `pts` entry from the map so that the next update can correct + # it. Otherwise, it will spam that the access hash is missing. + self.map.pop(entry, None) + return None + + state = self.map.get(entry) + if not state: + raise RuntimeError('Should not try to get difference for an entry without known state') + + gd = fn.updates.GetChannelDifferenceRequest( + force=False, + channel=tl.InputChannel(packed.id, packed.hash), + filter=tl.ChannelMessagesFilterEmpty(), + pts=state.pts, + limit=BOT_CHANNEL_DIFF_LIMIT if chat_hashes.self_bot else USER_CHANNEL_DIFF_LIMIT + ) + if __debug__: + self._trace('Requesting channel difference %s', gd) + return gd + + # Similar to [`MessageBox::process_updates`], but using the result from getting difference. + def apply_channel_difference( + self, + request, + diff, + chat_hashes, + ): + entry = request.channel.channel_id + if __debug__: + self._trace('Applying channel difference for %r: %s', entry, diff) + + self.possible_gaps.pop(entry, None) + + if isinstance(diff, tl.updates.ChannelDifferenceEmpty): + assert diff.final + self.end_get_diff(entry) + self.map[entry].pts = diff.pts + return [], [], [] + elif isinstance(diff, tl.updates.ChannelDifferenceTooLong): + assert diff.final + self.map[entry].pts = diff.dialog.pts + chat_hashes.extend(diff.users, diff.chats) + self.reset_channel_deadline(entry, diff.timeout) + # This `diff` has the "latest messages and corresponding chats", but it would + # be strange to give the user only partial changes of these when they would + # expect all updates to be fetched. Instead, nothing is returned. + return [], [], [] + elif isinstance(diff, tl.updates.ChannelDifference): + if diff.final: + self.end_get_diff(entry) + + self.map[entry].pts = diff.pts + chat_hashes.extend(diff.users, diff.chats) + + updates = [] + self.process_updates(tl.Updates( + updates=diff.other_updates, + users=diff.users, + chats=diff.chats, + date=epoch(), + seq=NO_SEQ, # this way date is not used + ), chat_hashes, updates) + + updates.extend(tl.UpdateNewChannelMessage( + message=m, + pts=NO_SEQ, + pts_count=NO_SEQ, + ) for m in diff.new_messages) + self.reset_channel_deadline(entry, None) + + return updates, diff.users, diff.chats + + def end_channel_difference(self, request, reason: PrematureEndReason, chat_hashes): + entry = request.channel.channel_id + if __debug__: + self._trace('Ending channel difference for %r because %s', entry, reason) + + if reason == PrematureEndReason.TEMPORARY_SERVER_ISSUES: + # Temporary issues. End getting difference without updating the pts so we can retry later. + self.possible_gaps.pop(entry, None) + self.end_get_diff(entry) + elif reason == PrematureEndReason.BANNED: + # Banned in the channel. Forget its state since we can no longer fetch updates from it. + self.possible_gaps.pop(entry, None) + self.end_get_diff(entry) + del self.map[entry] + else: + raise RuntimeError('Unknown reason to end channel difference') + + # endregion Getting and applying channel difference. diff --git a/module/scripts/lib/telethon/_updates/session.py b/module/scripts/lib/telethon/_updates/session.py new file mode 100644 index 0000000..165c170 --- /dev/null +++ b/module/scripts/lib/telethon/_updates/session.py @@ -0,0 +1,198 @@ +from typing import Optional, Tuple +from enum import IntEnum +from ..tl.types import InputPeerUser, InputPeerChat, InputPeerChannel +import struct + +class SessionState: + """ + Stores the information needed to fetch updates and about the current user. + + * user_id: 64-bit number representing the user identifier. + * dc_id: 32-bit number relating to the datacenter identifier where the user is. + * bot: is the logged-in user a bot? + * pts: 64-bit number holding the state needed to fetch updates. + * qts: alternative 64-bit number holding the state needed to fetch updates. + * date: 64-bit number holding the date needed to fetch updates. + * seq: 64-bit-number holding the sequence number needed to fetch updates. + * takeout_id: 64-bit-number holding the identifier of the current takeout session. + + Note that some of the numbers will only use 32 out of the 64 available bits. + However, for future-proofing reasons, we recommend you pretend they are 64-bit long. + """ + __slots__ = ('user_id', 'dc_id', 'bot', 'pts', 'qts', 'date', 'seq', 'takeout_id') + + def __init__( + self, + user_id: int, + dc_id: int, + bot: bool, + pts: int, + qts: int, + date: int, + seq: int, + takeout_id: Optional[int] + ): + self.user_id = user_id + self.dc_id = dc_id + self.bot = bot + self.pts = pts + self.qts = qts + self.date = date + self.seq = seq + self.takeout_id = takeout_id + + def __repr__(self): + return repr({k: getattr(self, k) for k in self.__slots__}) + + +class ChannelState: + """ + Stores the information needed to fetch updates from a channel. + + * channel_id: 64-bit number representing the channel identifier. + * pts: 64-bit number holding the state needed to fetch updates. + """ + __slots__ = ('channel_id', 'pts') + + def __init__( + self, + channel_id: int, + pts: int, + ): + self.channel_id = channel_id + self.pts = pts + + def __repr__(self): + return repr({k: getattr(self, k) for k in self.__slots__}) + + +class EntityType(IntEnum): + """ + You can rely on the type value to be equal to the ASCII character one of: + + * 'U' (85): this entity belongs to a :tl:`User` who is not a ``bot``. + * 'B' (66): this entity belongs to a :tl:`User` who is a ``bot``. + * 'G' (71): this entity belongs to a small group :tl:`Chat`. + * 'C' (67): this entity belongs to a standard broadcast :tl:`Channel`. + * 'M' (77): this entity belongs to a megagroup :tl:`Channel`. + * 'E' (69): this entity belongs to an "enormous" "gigagroup" :tl:`Channel`. + * 'N' (78): this entity belongs to a :tl:`Community`. + """ + USER = ord('U') + BOT = ord('B') + GROUP = ord('G') + CHANNEL = ord('C') + MEGAGROUP = ord('M') + GIGAGROUP = ord('E') + COMMUNITY = ord('N') + + def canonical(self): + """ + Return the canonical version of this type. + """ + return _canon_entity_types[self] + + +_canon_entity_types = { + EntityType.USER: EntityType.USER, + EntityType.BOT: EntityType.USER, + EntityType.GROUP: EntityType.GROUP, + EntityType.CHANNEL: EntityType.CHANNEL, + EntityType.MEGAGROUP: EntityType.CHANNEL, + EntityType.GIGAGROUP: EntityType.CHANNEL, + EntityType.COMMUNITY: EntityType.COMMUNITY, +} + + +class Entity: + """ + Stores the information needed to use a certain user, chat or channel with the API. + + * ty: 8-bit number indicating the type of the entity (of type `EntityType`). + * id: 64-bit number uniquely identifying the entity among those of the same type. + * hash: 64-bit signed number needed to use this entity with the API. + + The string representation of this class is considered to be stable, for as long as + Telegram doesn't need to add more fields to the entities. It can also be converted + to bytes with ``bytes(entity)``, for a more compact representation. + """ + __slots__ = ('ty', 'id', 'hash') + + def __init__( + self, + ty: EntityType, + id: int, + hash: int + ): + self.ty = ty + self.id = id + self.hash = hash + + @property + def is_user(self): + """ + ``True`` if the entity is either a user or a bot. + """ + return self.ty in (EntityType.USER, EntityType.BOT) + + @property + def is_group(self): + """ + ``True`` if the entity is a small group chat or `megagroup`_. + + .. _megagroup: https://telegram.org/blog/supergroups5k + """ + return self.ty in (EntityType.GROUP, EntityType.MEGAGROUP) + + @property + def is_broadcast(self): + """ + ``True`` if the entity is a broadcast channel or `broadcast group`_. + + .. _broadcast group: https://telegram.org/blog/autodelete-inv2#groups-with-unlimited-members + """ + return self.ty in (EntityType.CHANNEL, EntityType.GIGAGROUP) + + @classmethod + def from_str(cls, string: str): + """ + Convert the string into an `Entity`. + """ + try: + ty, id, hash = string.split('.') + ty, id, hash = ord(ty), int(id), int(hash) + except AttributeError: + raise TypeError(f'expected str, got {string!r}') from None + except (TypeError, ValueError): + raise ValueError(f'malformed entity str (must be T.id.hash), got {string!r}') from None + + return cls(EntityType(ty), id, hash) + + @classmethod + def from_bytes(cls, blob): + """ + Convert the bytes into an `Entity`. + """ + try: + ty, id, hash = struct.unpack('&^P+}4o9!G0OqjjzKE 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 literal 0 HcmV?d00001 diff --git a/module/scripts/lib/telethon/client/__pycache__/auth.cpython-314.pyc b/module/scripts/lib/telethon/client/__pycache__/auth.cpython-314.pyc new file mode 100644 index 0000000000000000000000000000000000000000..7a7baf70be6a0ecfe40c310c06e01872cb32b3a7 GIT binary patch literal 30694 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 literal 0 HcmV?d00001 diff --git a/module/scripts/lib/telethon/client/__pycache__/buttons.cpython-314.pyc b/module/scripts/lib/telethon/client/__pycache__/buttons.cpython-314.pyc new file mode 100644 index 0000000000000000000000000000000000000000..5da6e22e81e1337cec1471cb66230d68c3b462d4 GIT binary patch literal 4257 zcmb6cU2GG{dDcIhc=KZ?j^q3}*<48cL$Je<0u2cTjs$WLNDXHWO4uyxjkC>qcRRZ# zXQY#OsMOp^m4f;JCsl%zPP(hqhkL2SL!YWjRqu(IBD>p2P!*}Vr-p(G>Qlej^~QnZ za_UGsJ2T(^&&)Tox4DTy(4PJIUqAg-6GHzW6Q_BqgBK@Ya0^{SX=)l7=$b3-nx;;> z4A*J5;qD`I_q6A<*YKXE4Z077P(Mn0!YJ(>b=e$@x->O_;`Fyjf%I@9O$%}Cq@Sw&VGN?2%T-WLhcRNzioF{`0P-!ZR}W+TV(ilQ34A#$AA^@c~2TVX!X zQSXQD^)2)Yag>*NZuOPW;SD#LKK1Q??j?g4fP}1QkO66g7X04$;)<7KUIr~#Q6O2= z4YOGb>CGx`tuE&PuLstXRFcxrOfrKUNOli_ zd_Tu)Xt=8;V;g_a$mXx1v|FR#bEPTG4W9=-FMKq7bxEYvXHWo5Qd56KNb|v*$@r}K zC73r{CH}Emrs1l!f5~LF(O%oyJ3DaGj#W#8hS}z=SoTc+~YrNDmyv25nx-{&>G=biLxZcpe*WotHI-}bEY7I(`y|>x{*W32xto)s% z&P*YcVP*&4B7kX4<37mVGgrt~?gd;&*G8MBo>lP{&>69KUFC7s0YZslj75>nNjF4= zbxH-poK!`fg-QXHFvm&@tSA8%h7|{g<*~?WSiK=-#VnCQxHxl$$GJitiVrJ+W-=D} zY!NiXq(ZWJtE?s&=CWE~^+mNHXIVj2bYO(pf=&d2Se?yVm7WMM0wNKYQ!SthjG$~< zdfUv{8zL^U>VhFEN%lO^bXn5HVOFvf05e!c6hvL;aS`NR+{#^N6A3^_Bv@WAD#9XG z6}6zVv8<{@4Nym9=jZulUNYvvlL~N*1Bc^P^K^!VR`nVin}7S>FdrYYFbLK~@Q;l- z%*v8(fVvLlFj$9qVt}(k9BP}kNpGmcOJFG2q`-u)IdbwNKCj85g_=wPT2|b5T~>vq zu`yOFlDh!2$z}nodDgiiuu@)AfuVg7s3BFoI>Fjkh~#yWW1JN}&W6QolB8s%S}eLK zMdR^JAp@)oR}8#4w;bp&(?uoAd&MrfGLUO`&OLiEC@qoBp8tNCZma1hj>7?nhl!F3*w?GLn_8Q%;pOpoOti-@e3EY^HbdPNwfL5VPFY@ zC{ALmVoVe>8zr41@q}&(xJe*IH+V%5G1<;0Z958+I)>o@L73DR+(|l8l}sA=!M)aO znz3DC;s9&{vczO~xnP@Q zwro2<%%HX8kj+@TH{OIBh*G{yC9R7trl08J~{QIrQ-{4nSHm!zWeo| z2m60FR%VZu*rSA4S~>N!t#id!vAUMf3S9|xt}vCh4p_5B?OiJkPyH<`p7mhY%6pae zj+ImEU6ECKJs7#W@WsNH`Oov^;Akm0S_wz)e0=-kQh21&AN?}&dE_n)OXYBEEgUO_ zM=OJS%Yz5k1`pn4tmWuhI9duHsO(9U_l&OX8NKV<4LiOTj+eqes0vy!<$Lxzg5s=iKdc z_Yaoa63_Rc?%r1;C=mX1`AM(`+;r#6?K8x=6D4-yLGBSdRc-^1ZZ01xv4J+2_CC^g47_Zr3osj~&d0odxkRBr?@L7aTFl14h8zC>)mtEWR$AZh zM&EQdyq|D?6Q`iZBt6f?>&<$OQ}{f2LNOd?zrbJ&kE@>*c)1GkbKHW2b%PvFib}w6 zDj1SrS2a9B@;U^sL2HrYor6UO%$6m(L6=LgOKzV?f(FbcjXNL literal 0 HcmV?d00001 diff --git a/module/scripts/lib/telethon/client/__pycache__/chats.cpython-314.pyc b/module/scripts/lib/telethon/client/__pycache__/chats.cpython-314.pyc new file mode 100644 index 0000000000000000000000000000000000000000..4babc3ea4e4c5887ba72318ab08d2123fca8622e GIT binary patch literal 61573 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 literal 0 HcmV?d00001 diff --git a/module/scripts/lib/telethon/client/__pycache__/dialogs.cpython-314.pyc b/module/scripts/lib/telethon/client/__pycache__/dialogs.cpython-314.pyc new file mode 100644 index 0000000000000000000000000000000000000000..7c71b65c4e57e386a6f9283c67868f08048c77ec GIT binary patch literal 27645 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

tKJ+;9&27{sl8b*rdv+ z7cvT^4j7o+fyBr8(I_9IGbPZTN7sxLE);;1Pmxq1bI7Gv$LM2Z1iVtEgx(KF4#(l< z5sff4B9(`IMX+M3oR`d}xj8)ivM6{fu8hAfzwzR0FD4883k7}vd2C6}*K}Swa=)fw_Q-wglvH-k zS7M8Dv9dc^(mk7ZudqB>*tk##Fuz#npS3L&6n?8RS<*6J(vmFcoGyDDHv91(>R0dt6rtE6o@*4PU%=X>) zRbPAI>I>gIl`LxdMN!j-r4`@0V0^1>-+bl1ADsK)SLU8OF;}^7vGPlcr6-f#lXKpa z_ltb9g`czurCZ?zfuqwaZj|{OY{iz>B+EMI%Q_!Im#M#BUH7MX%TC08ivv@|%^!MC zeyU!?2z|ZB(N`zl$rF3GI_|i78gcVpJAw80{Ob|_k*~+yyGM8*N27K*-miC1yr}{& z-rphiRXg6_nUDAf_6`aIVqdM}gDw}v@fP7O5$QkJQ`uKx|8bcOPk!tZDPBQ!{-ByKYqhu2_GV5U-naRV&-_tr^w7D6YfTG2)s3o&y&h(hVoF1 zp3!uA@MQ#_MR{O*O!(j`cA~_foA#qD2WoO2IPP*|@2Mc?)-+Q${th)O|AT_Rr(lkP zc?xC`z}fE-#c3CcX(ar3ruU!)8$4Y}xkpM$6h$y4<&cdI#8AkT6pAnB>Xe)!X2T3Ln96Y6Uy}Wp*P&{%{OWJ;1(;5=|)Ntw#i6q~M)2bLw@0Dm~+Dhlw34 zVn^HbpPP6L z@DzPBm()Ui!qLtvW~a5^tQls%6nRRfqbNE0WflOtZQW*fO}y|#4biI7Sv!U0hrFrjYr=#%m>5NS!;ro43H?9@@f$(t00tiW zVN-IL9gRU0GZ>}<=oFUaiB4ZKr*bOBY&zxV5*6H!6J;z#m^3Z04ee-iB5^Uu6VJpU z`_r40B#rT{ac-QTu^Eqr=_BXVI=b|Q0rm)1I1`^(4b7fqw2@>V&jy!C);U@VKnbq= z#~=hFyd2Vibp{MaEk5U?qdh#;;rKyI?=x93Cw_b4IAI5skwnjF5rrBP|ICDwA+{B8_A7pR!n7%*U3Ot^A)=ng_|Z(fz*cph+H5qV8XeBiXRn2( z7+>0~=5moQ`@~%;jsS-t{@9#hJPgn=k)T(48R;`tn-~P5?PxGMhge1;pd&tFpfgT0 zVk*$slTpn;ZU$X*{r2Nm`9y32GfONWzG%+pS4Q5xM1Ogr=$?OK$6Dnr!gT5sX2H?2RNk0EHfSe^FfsB za*MOJiJPd}$J(unQpg2>-bvmpYCYjYvyC*Z%(^qW#M{XPH)$|tGZ?%in=U;A?J$k3 zR=J*knXZGJ30y~6phSOQGU3d-X5jD$uDhG0!4 znMfz}A^OlNSU;@>K`J#zvsTNuT3h#L)J`?zZe>6TpAV61b6N;d3pmWc7=k-5w7(Pp z3W!ZlhTHv24S9*M1C?np^;0gojE@M9HzZUPzI1U`$ORV7wxro?}|4{dtlSH9M09%W@z z#JN?cvYJr`wi+^oD2oU(d>&TIz`P#x)2iI-!Aos1zUGmAR@M5BS$)XX3QOoYyHH#K zbKyc!6FZ_>x>R1fgu|)T>*#-VXxjd;DC2x8J0-zC2xc1`^p`3V)aXeH$c&`CNCE9x zr`DJsY2e?My+UuU(Hj@^ew>`wQu%ZkJdDr2$n-@+^9G+qWX|w!QjY&b!QWEwa|+H< zK2~L$wEyCfyNE z5daNd+xUq21bo8W29^hHWKYBMdWoCikQG)oW`QP*j&ZfoKP_x|x z&b>YKyzuzRpT@=M%3TxQE;$I4d|3~Ot6gvMwSoe|OpBMc1OMMrM?%VT| z_DcMlvseCuBk)O{Tl}*4uwqygpAoO@eh3pHA`dZeV(FERhXQ^cZF|lo;wz;TG79~F DSw=^Q literal 0 HcmV?d00001 diff --git a/module/scripts/lib/telethon/client/__pycache__/downloads.cpython-314.pyc b/module/scripts/lib/telethon/client/__pycache__/downloads.cpython-314.pyc new file mode 100644 index 0000000000000000000000000000000000000000..84d6add6ce51eac261cb5a14edf425ab214aac6d GIT binary patch literal 51769 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 literal 0 HcmV?d00001 diff --git a/module/scripts/lib/telethon/client/__pycache__/messages.cpython-314.pyc b/module/scripts/lib/telethon/client/__pycache__/messages.cpython-314.pyc new file mode 100644 index 0000000000000000000000000000000000000000..c72ac0bafbea861ea74ff2cb15af0bfb4fd0a71a GIT binary patch literal 66286 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�wS1_F2G)rIxUzDv(qlC9GgW8kkOW#{0**DaJ@|D#!5I z1hNShbpF0DQSdfb5p&K^uaVlt=jSjq!HJ~-XDr=>l0f)K$$+yy?dBs06xWa>DeUq@!t1b7O5-FJxjDNa_X(fY(j~ui zr|%T=9daiwnl~JB-7l68W8qCu&lzK77FKlQ)1H)fG+BgDlMyv0pYE&%Av~b-B5ydQ zC&8&&OVNf;=h4jyBXihVU8>1$P)t4LR!x>@a_O5e{ys|%@IC@oIFWg@VBR-B)zzv> zz$`=>queGexKkmYa??x_gg92bN~z!_eZ;BT`<5;RjrY$2t$~oz(i}_G_6cN;WeyO4 zGyY_}j;p2>H;b<#gaKzCp~-Hns>z z`~r)ut;KG38EsNTxbjimn<{=`i=L*kbQ$hJVa=@g60jw;xV%bYfxfH+P_#_{0i8~~ zP#V#@e7RBvNgk1KS*S4MuyTd_tKXBlGxhM1E$rP0!CxfHN3d{`RT{nE|D7@M& z_7C z^*0>75y!!>RJ`2{D(6g4qB zhs7`k|HfGq%LK_o2W13fHj0@p2!-s#NjW}-0HB~#5>A{6oTWz{6fnSFq}Wyph@ceC zvC8D^lRyh9zgT}jU;IY|&QgoCg1A_s>VHi!t|wR9WIGu(CMQiK<`4W|_%vq2+zrJt zc`>6-P8&ZtH*+fR24%xNn~FV>B@}`G6VHOB2q30V?w4uB;!FC{DoE_rSp5UKomp1% zn04$)YiTw`vzIL7)Zem~yg2Z+fg4pLH|@tiEUk)^wl0*mUd>!AU3ar^-F*5la|)vs zj&om_KXALeI#I~*_cq+wmSVcj+7VncVNy8DGgx2x;t z5B(=wR@ShN1568w;ApG9EmFUBp?>YPib(zb7mmyyxK&bdvGf}U=lf_3wp`Kwr0tcx zk^HuW{I+Ph3V-{-WwS(!uemJ$lL>H9itZ*H$Z{H-E=UzhEz;+{I(yuE%E z!gr8hefm3E%Ult?Vqr6_~!l*LpGs2gv2qIrt(MV(Ids(9 zIo(x=XGu$S6~)z1Is$W*y6BYUd@!^tk}>teP zHa}ChR#*CTBn@!w<1rgcW8VQ}*^gJ?1BxX`Nqa{lx}GU4uEN?JtQl9C+!|2P-j;7Y)V@39gRX}g;V5q$=;i$4(l zuQ-ayXLWg1%@1qHvsEppf)KBDsh*<_a!S=e(RjW(VIDd7`zraQ`Z)lw+!6ZyJ&Rc0 zCh_XAA!QlPQ$RRR9Iu!0t9orHWj1vP9QA><7K}=__dbBj~53>a;v@glh zV`=@vK^I#%WFdgDxRUw68l(+5;euBuoIb7O67a5*4liHHz2xL5+!BsrWu3r5*OGD2 zlX`#AlRe!!2|j$$aFMEk=Lu>@HF;;LO{gkv(XUDT%BXrJLVdOvxg_Jy7q9G13@MUv z-&gdo#P0GnaP$jTss($RX-d|Xl~T;=59r61WVwy3QcnllRLuOf%C>%WJg0v- z^-x-dmJNB4Pur1NtNC=~@bB>Vr}*10+y}e&gPhCmgTAZaUB)q+iN6Dyh1~Y@duR+1 z3srD}$W=@t=b!avAI>WaefkjG{!i_q!@`r_M<+2!(+=WFp*2)0^K2yHFZtM{ku14X z23=0Hw)~1xf;gnabmS;e*z{I%x0RSdIm|F}AW9B^!xk?rLde}NX;6btN`{>(9sC2p z6D*sYpb%tiWMCAJgpx@`B*K#r=oqI-OZNh7hs>x2TmuQ4Ye_Ej_p;gh{XW>dQS#0x zcL6YB?h$8E9HhWLO&E5vN|MjVJ_d;HF{&)oCn+f?W-`tu$MI}&B~^*}SYXn61-nMJ zNj38`8VX{Fn2obE8d4qLR#y9`AyjL*PZc__RWTZkc081Q3C#)|tp+ZjWqg>@1u4H# zhFH-3C+5#*D~g$dbSN7-M#9Ues~SjTgi0oDQVfBUDp>?5p-dPi_`d{_q$WKC9F;`b zf60OBv&cAS*7vR9e{-H|X!z5flHeVN9`Ux8yd!9Cu+gjXp+(z?l!1}PGma`2 zI|7|CDGJ}cN=mp!zUkGkPvnc$E8s{VBgGXdE87* zNu=D!JyCyw#slTGoR$KZq!t6`&3cLWO3+D6v|`XhMF_%b(8b(96EPB66Fh$8IqVh- zFs8ksPX%cnJe~1@%;dKL$A~A4L{k@rLV#mDDky&6 zkJx64Iyu{<`+gPtm24dXhC(73t}Js{X9SVRWI zvZEx&|2w6{nL6x=B$nnGCvk82=m`vK|LIYH=!BRQiBIiold_`>dUyDx5;?}h2n3;S>7mS0SNCGFMR zmvbYzoeR00WK2Y_=J%7K($nXkp5J#nr*0us% zGt#~@+`jX=C)%_rT2?LW@s_f*)w}L#w$)uY2(YCqoY^p+bFuFu`~53-tE^s`r}c)* zdg*lHiW+In1+y+>9%r<*l&h8Z>G!k@HjFBPUDk!?&p$6ty1F|2`p9b|;qq5S{fG89?TbwVk=lWC`yhIMwd&=ntNUQ4#-jJzRdtc7&Ecxe z*PdOh+7&6;6)xHJtJ;Put=~KzDQ~|~(4Iu9f6J7XZwVJ|p%b=@Q?YPLR!6j?0y-f# z%Uh$RZFCrOoMB+Js&T1ME8cof%PP*E&w%X;Y`OB%|{{5ajgl}c*2e#VY%BiP#yMADY?XA^&s&MmeMqR&6d$&bDu*>%D zs!EFQ*84Tvds-^u@1>UDoA=7~{*?6hDhepxruR45-g8zU{zF~wZ?XL_rKcS??|10^ zt=aFdu~U49PT?*cS^P-jFomA{$X43#Nc~Zz-d~gXqpEa@J8bm4h2OSP7)yu$T>mL= zocx`HimCX!M>Fl=4_Kap2k|LOh4<@eM{q(C|FB$`V{9YEF-V51 z#vvIN-(`~#RE~I&t+gD}?}!^AgC!8rg(jFX*dJrbkTO_)NkMXprsY;jlc=h)OLFPu zuuw9OWrU@YZb;YG@psywlv5f7Arfb~lwv&^8uBPZ2AS-96-a#yt+-j+K$Yd|B>HWG zc5A63PDfimAXVP-RD3-52(@TPi#ue~kb;_Ntnx2d0pXx)wgT#a^f7G&0wQrGJBcFc zDL(p0rAjkw9MG+#$FLuQk`>wC%%#j4Zy}wGgDPxqepj_lA`?wk>tuC??HFm`;dnpo*>yL3T2Bt#5M>v)Lm-`)-q0Y0ta;Lb-5Fn}(TFwKIu z)9DoCV|r$iaZ=EPOnHdU@GV&&k(|owX>jz@N!_!4;7;UTkQyd(;_`6r1K|~-eu`fz z_0K_}z#%;`GUA@S0E5LL$k7Ys(0D~^ZwmQs*J7RQp3P|X7vO}e?=%B0A$6)0{TF}IQN>$Kc}$Z2$Ee>68H2psioK zZky3h-OA+<`Rbg6#JAHQnC!yIE0bMwI8}bevxW!p_?b1{Y5z06U1Ot$Yb+xV7D!7t z8n7mpiLi{JdbRmFarTVyhQEBWR6_3 zC!kLYkgC3zn*s9zbP;?Ofr6s0rqxoe7fJiZ%3Cr;Q_K)b#B(6%@xsYF3WFEl6JAkg ztOzWaX85VKNft{~Tp|~R-9oaSFTWvI-IOUt$){l(#F74@G)4R;7+nON*~?V;H(+ z^YLWoF=vMPP*2(=3>uBXM? zm7b=a7X7d@FHzl{@ZbRTZUFZ1T3{0eFCd7eixDT-6R z`JK_~RTn$w58kmU7~ac3Y(D2>5ZOi1>iS6a+Hm#SYh{boTO*}g=l9(ztcVo0EEKj} znOH1b^TM8w(zT*WI57zqHb)Ab;X>#9p4&OOFO{oD9pS=`n>ii7sI0xUVe5Ch|FZjy zoSPdC!N*HI$juFhuDU-;)f~=9{km}dI;f+E>-WQn$txvS+<#jBpHp>5JHENYAK%mT zsw32QB}eonP0s~c1KOqTVROAzS$om_&GIX&9+d0dQmt&;CwKFxf_J#IPwu8$Zh}YA zFJD>P*QEbd34%8>^u8k7o0&bGxOr>y`o7Z4x7)H2e|x>&S7m#9LmlGp6t3^9$$U4J zp1+%`_chqw%}b+rX*z{96vpzz*%Io=u~@#ihwjKYJIRbAS&6Qoad>YRf`=zrNgbQL zis8Kcu#DH%-|m@jcU}kN>~jU2AL{ z)fJxc?%Ixb?X}mB_4~1YB#vKsVj7z~Fy4fPxUQRrk_wE2oe&WmGaK^|YK4+YrB$US zEmCPIf~vGo301WGXe#vw2?F&;f7o86O*TQK6;gjy8plyh_|tyhxihn~nO;zw8B z>oc=6_ciyPd%kN=XPPB|&TDNnldB29UJY^a~kaB;8UW%e3Z*8MMfO z&C5b&qEz-y5x!?=DSQ`>EdLvxqnKSpoi<}sDD+`%&a-z2u*~}8i(#kXD*_g*Xq@^? z6lCI9+gcL22N31YL8SKeSQ6+TV@>+lrqYb~77J1RfY+}5^sux>1Cn%5Yc8ys(+1eV zzjiINOM*uW&r!dW;Im6ShQ4gnCOohZbiL@4B)kyL9YUaFaXhBN+X@9Bs;lS#wzYy2cS-?#TMM>vq7C_F3|I?WZm77xrw9 zS{$s*C+`VtVhK9#gZ1fW`2GdoA$)_%d&c}LM8uG#A9pO=0CC{Rp6|Pg3@UFUizFy4 zikhf2C9l)T{NL;xc}Pk>@)>7ulo`aVd2d54w|aW(kR$lRg1Smy#;)W-YS74eFA~BAd1K5A?Bi@wrVS@ zHWKS9v)ba487jDc{_u6S6g8uef9s|LbAno9D&I(q&h>d1{$^oMB6_O(2;e0?htp*C zzxXoE_V3|_zk97wke6AftLRbn=&cvXfr6Z3@l>Yl*UYVx=p*1ltH)3}F}EwP{TQrx zGyE|+EV1seZOHfx)i{6j@!6A4pO~2jz61Ohga#avwkefyPFw}hhS&ZPw~j&k1QWUA zqdDq)t4^6~z75M=m2+GU$Y_#j1&L`jEda5e|IV#< z+CD(Gx*8)6A~M`z61JJ z=sIXUB^$ctd>}ah>~oZcxq(kyw$M8C$H6y8-l+Lq%{zlP<6fLTU0bj3>(&jf>e23U z7XI4`j$#ORmGF|s;Oiv%00X(jbI604Z*7+UcLvmvL}sa z?JO$zvikAax%pFkg_=8@8K_kt0;`!u2M%iO=um!Lt8;cssw1Dy6WnkZm_wlcDIKu% zN891Z^HN>?xx!B>Ym=3oOO>6k9ZOdBFIDzmZE9U)ZzfuY5=}$rqSu-_UOoJa!)K#8 zw5=ce>n?|CUVK)mdXwRM7Q^?PKbi=SCPL9lLJ-o=4?lLN8Ef!K$^ z*yX0S*SnLEEsK#Y$;j@-$nL9Eb;+uprK+Acqlv1`=cA@aYu^KJK->>fj{Xvl)4SPQ*E+So_Gp&@gTGBAnRH1O@XHz9}PfSASi^IG&1LzXeaJa5) zLUKnyR}<~*n%jfh&pIqMtZmZ&9p%Hw8kx#B=0gT0b7L!$|DYOR2`}TH!EO_l5P4q4 zx$!HWeFe$SIK8>E^C`VluJyr7s-yyTc_?6)1i|jGStOSz^{bF{Qn_48G85I3xpBJ> z>?yDk1!19DZ5)-${u~TV=m)oCyx-|@Ye*^gKj;-H-#hgQdbwmA{*_wO5-tRA6$c2? z%08?YR@fES{yiiDFmh2usWlei4;e(zU{D*F7ecv%g{H*W-*Q(7mdzSj8nqnpI~tpKReOt*&%c#w}AK4_lJ7?zEySqLw&eWtyIxd1q`lFS~0q z)HYv(;)o4&cQ*xIL_T>87-5?6v+=W|XQ6fyExt2(?c0#S1P8OE^xP4+H+vG!(k%V3 z^HlAg$-^3P{J453l%X)yj?T}zZ2~hyC6sO}Co%=VUTb2i$Q32gZcoZ5V<*kk#(uA- zypzbG+vnPko&rBfUlZpRm$*eyYr5Z6V5)wvnklvz|HJ)dtdgG%;f3h+(r1F z$(N^N&!}LKQX)RsvHN_UK7i$-WIy@D=W+E+d`F95*oWI*Cl%wt-3-8MjoP@gU19Sd zOqg4DAmqn98M$2hf5ghAJzt%f-x(L$&!?QZ%O$TltM%LAJsB)RDw@GGx=Z#!xcP=X z9mb4pAYnaBQAR1LrbuzB2uvYAQ0$;IBePUv7DLGRRi;k_83{|cJIxWM-@S_*A(jlr z7K&qw!Pre>JBoUgvz{ORx1^uqm5caANk6XLR?0_NAGZya%VRV0qQ_?J`xjvUoy;xk zliruk-W7*Oj)I4^ySHiD^af{-*xA|7Wowqx|8*X_?-M%ws@*>493Da79it$vA%J%b z&jDXr3Y;qxj|YW(i2U5rx}vnl?_-tHzM@?IwzKpBs44m}$Nv7oW9Uii35vw|Ye=0Z z@x4n~Tp4_3V_12(53i8z-$}_Rq|6PUWn^~BCGq1CTLDZs^ps;TOC4(rp?a=BFtnA5_j4glafSE* z*K>$p4TY1TuBA}dYfmRa1If^qh0vC_H@;AG=~lm9m;9l#Q$HTgz?!=j!@J(?N`&uE zgks4+Y#|W490<7_YwcJJ?SQE;!iK=B){EO{7#wah{aWQtK*vJb0xy@jqfJn?Tk%ri zt7Wg2y;O#*utt?v*-;9V4SJa$w}v6a?-fKi~r4}7>~nfVA9286O&;kyO{7{ z8F5~Bipe~ZI8JFh6?pq8Pcqq$lkvMTVn6)>9^P_b+~X@TOSHa?reDZ_*4$#23R$K# zG@3yn%Sl!U8PZx>%}ODww1yV5TF4shnEtqKhUK7^+0_YIueElV4MH|*+waxQCZU_P zrij@hWGfY)Z9;KW>C@P+F|`+-KT6M5aYhP=2D|)@;+M^D7wZWm)KA~4M`M3N%iT7XzScb%VFw1zi#CtHy zSqq8xV1}qnw&OjZT#5GpXL;~TCGj2*<)^!ERpLFYsE88>yyBz^;ZHW1w%07_M{%2l zyj9o9MXKk7tkSyHI}WngYjRd~nvnIsXhm3J2YIC$74@K1GXn*JWRne8Ni0<8-Es#Bk60H#oLrC;i=c;Tx zTvz)R6S~N;yku2`*A)?U&EZ-Mrt@1(jsO3)e^vfpmsG@CnM!y$W)ElGw>YGb$+b1T?t`Rhp$T#0 caPd~X_l4(fX!y4}r#}?ZN5F&SpLELq09G%9@c;k- literal 0 HcmV?d00001 diff --git a/module/scripts/lib/telethon/client/__pycache__/telegrambaseclient.cpython-314.pyc b/module/scripts/lib/telethon/client/__pycache__/telegrambaseclient.cpython-314.pyc new file mode 100644 index 0000000000000000000000000000000000000000..e9359deecb2d111c6a5a040fee1e5dd18b6f4e9f GIT binary patch literal 51402 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 literal 0 HcmV?d00001 diff --git a/module/scripts/lib/telethon/client/__pycache__/uploads.cpython-314.pyc b/module/scripts/lib/telethon/client/__pycache__/uploads.cpython-314.pyc new file mode 100644 index 0000000000000000000000000000000000000000..ce858f7e25de8c951ff5f51c577b9b2f96cd6acd GIT binary patch literal 38271 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 literal 0 HcmV?d00001 diff --git a/module/scripts/lib/telethon/client/account.py b/module/scripts/lib/telethon/client/account.py new file mode 100644 index 0000000..d82235b --- /dev/null +++ b/module/scripts/lib/telethon/client/account.py @@ -0,0 +1,243 @@ +import functools +import inspect +import typing + +from .users import _NOT_A_REQUEST +from .. import helpers, utils +from ..tl import functions, TLRequest + +if typing.TYPE_CHECKING: + from .telegramclient import TelegramClient + + +# TODO Make use of :tl:`InvokeWithMessagesRange` somehow +# For that, we need to use :tl:`GetSplitRanges` first. +class _TakeoutClient: + """ + Proxy object over the client. + """ + __PROXY_INTERFACE = ('__enter__', '__exit__', '__aenter__', '__aexit__') + + def __init__(self, finalize, client, request): + # We use the name mangling for attributes to make them inaccessible + # from within the shadowed client object and to distinguish them from + # its own attributes where needed. + self.__finalize = finalize + self.__client = client + self.__request = request + self.__success = None + + @property + def success(self): + return self.__success + + @success.setter + def success(self, value): + self.__success = value + + async def __aenter__(self): + # Enter/Exit behaviour is "overrode", we don't want to call start. + client = self.__client + if client.session.takeout_id is None: + client.session.takeout_id = (await client(self.__request)).id + elif self.__request is not None: + raise ValueError("Can't send a takeout request while another " + "takeout for the current session still not been finished yet.") + return self + + async def __aexit__(self, exc_type, exc_value, traceback): + if self.__success is None and self.__finalize: + self.__success = exc_type is None + + if self.__success is not None: + result = await self(functions.account.FinishTakeoutSessionRequest( + self.__success)) + if not result: + raise ValueError("Failed to finish the takeout.") + self.session.takeout_id = None + + __enter__ = helpers._sync_enter + __exit__ = helpers._sync_exit + + async def __call__(self, request, ordered=False): + takeout_id = self.__client.session.takeout_id + if takeout_id is None: + raise ValueError('Takeout mode has not been initialized ' + '(are you calling outside of "with"?)') + + single = not utils.is_list_like(request) + requests = ((request,) if single else request) + wrapped = [] + for r in requests: + if not isinstance(r, TLRequest): + raise _NOT_A_REQUEST() + await r.resolve(self, utils) + wrapped.append(functions.InvokeWithTakeoutRequest(takeout_id, r)) + + return await self.__client( + wrapped[0] if single else wrapped, ordered=ordered) + + def __getattribute__(self, name): + # We access class via type() because __class__ will recurse infinitely. + # Also note that since we've name-mangled our own class attributes, + # they'll be passed to __getattribute__() as already decorated. For + # example, 'self.__client' will be passed as '_TakeoutClient__client'. + # https://docs.python.org/3/tutorial/classes.html#private-variables + if name.startswith('__') and name not in type(self).__PROXY_INTERFACE: + raise AttributeError # force call of __getattr__ + + # Try to access attribute in the proxy object and check for the same + # attribute in the shadowed object (through our __getattr__) if failed. + return super().__getattribute__(name) + + def __getattr__(self, name): + value = getattr(self.__client, name) + if inspect.ismethod(value): + # Emulate bound methods behavior by partially applying our proxy + # class as the self parameter instead of the client. + return functools.partial( + getattr(self.__client.__class__, name), self) + + return value + + def __setattr__(self, name, value): + if name.startswith('_{}__'.format(type(self).__name__.lstrip('_'))): + # This is our own name-mangled attribute, keep calm. + return super().__setattr__(name, value) + return setattr(self.__client, name, value) + + +class AccountMethods: + def takeout( + self: 'TelegramClient', + finalize: bool = True, + *, + contacts: bool = None, + users: bool = None, + chats: bool = None, + megagroups: bool = None, + channels: bool = None, + files: bool = None, + max_file_size: bool = None) -> 'TelegramClient': + """ + Returns a :ref:`telethon-client` which calls methods behind a takeout session. + + It does so by creating a proxy object over the current client through + which making requests will use :tl:`InvokeWithTakeoutRequest` to wrap + them. In other words, returns the current client modified so that + requests are done as a takeout: + + Some of the calls made through the takeout session will have lower + flood limits. This is useful if you want to export the data from + conversations or mass-download media, since the rate limits will + be lower. Only some requests will be affected, and you will need + to adjust the `wait_time` of methods like `client.iter_messages + `. + + By default, all parameters are `None`, and you need to enable those + you plan to use by setting them to either `True` or `False`. + + You should ``except errors.TakeoutInitDelayError as e``, since this + exception will raise depending on the condition of the session. You + can then access ``e.seconds`` to know how long you should wait for + before calling the method again. + + There's also a `success` property available in the takeout proxy + object, so from the `with` body you can set the boolean result that + will be sent back to Telegram. But if it's left `None` as by + default, then the action is based on the `finalize` parameter. If + it's `True` then the takeout will be finished, and if no exception + occurred during it, then `True` will be considered as a result. + Otherwise, the takeout will not be finished and its ID will be + preserved for future usage as `client.session.takeout_id + `. + + Arguments + finalize (`bool`): + Whether the takeout session should be finalized upon + exit or not. + + contacts (`bool`): + Set to `True` if you plan on downloading contacts. + + users (`bool`): + Set to `True` if you plan on downloading information + from users and their private conversations with you. + + chats (`bool`): + Set to `True` if you plan on downloading information + from small group chats, such as messages and media. + + megagroups (`bool`): + Set to `True` if you plan on downloading information + from megagroups (channels), such as messages and media. + + channels (`bool`): + Set to `True` if you plan on downloading information + from broadcast channels, such as messages and media. + + files (`bool`): + Set to `True` if you plan on downloading media and + you don't only wish to export messages. + + max_file_size (`int`): + The maximum file size, in bytes, that you plan + to download for each message with media. + + Example + .. code-block:: python + + from telethon import errors + + try: + async with client.takeout() as takeout: + await client.get_messages('me') # normal call + await takeout.get_messages('me') # wrapped through takeout (less limits) + + async for message in takeout.iter_messages(chat, wait_time=0): + ... # Do something with the message + + except errors.TakeoutInitDelayError as e: + print('Must wait', e.seconds, 'before takeout') + """ + request_kwargs = dict( + contacts=contacts, + message_users=users, + message_chats=chats, + message_megagroups=megagroups, + message_channels=channels, + files=files, + file_max_size=max_file_size + ) + arg_specified = (arg is not None for arg in request_kwargs.values()) + + if self.session.takeout_id is None or any(arg_specified): + request = functions.account.InitTakeoutSessionRequest( + **request_kwargs) + else: + request = None + + return _TakeoutClient(finalize, self, request) + + async def end_takeout(self: 'TelegramClient', success: bool) -> bool: + """ + Finishes the current takeout session. + + Arguments + success (`bool`): + Whether the takeout completed successfully or not. + + Returns + `True` if the operation was successful, `False` otherwise. + + Example + .. code-block:: python + + await client.end_takeout(success=False) + """ + try: + async with _TakeoutClient(True, self, None) as takeout: + takeout.success = success + except ValueError: + return False + return True diff --git a/module/scripts/lib/telethon/client/auth.py b/module/scripts/lib/telethon/client/auth.py new file mode 100644 index 0000000..0c9eddb --- /dev/null +++ b/module/scripts/lib/telethon/client/auth.py @@ -0,0 +1,677 @@ +import getpass +import inspect +import os +import sys +import typing +import warnings + +from .. import utils, helpers, errors, password as pwd_mod +from ..tl import types, functions, custom +from .._updates import SessionState + +if typing.TYPE_CHECKING: + from .telegramclient import TelegramClient + + +class AuthMethods: + + # region Public methods + + def start( + self: 'TelegramClient', + phone: typing.Union[typing.Callable[[], str], str] = lambda: input('Please enter your phone (or bot token): '), + password: typing.Union[typing.Callable[[], str], str] = lambda: getpass.getpass('Please enter your password: '), + *, + bot_token: str = None, + force_sms: bool = False, + code_callback: typing.Callable[[], typing.Union[str, int]] = None, + first_name: str = 'New User', + last_name: str = '', + max_attempts: int = 3) -> 'TelegramClient': + """ + Starts the client (connects and logs in if necessary). + + By default, this method will be interactive (asking for + user input if needed), and will handle 2FA if enabled too. + + If the event loop is already running, this method returns a + coroutine that you should await on your own code; otherwise + the loop is ran until said coroutine completes. + + Arguments + phone (`str` | `int` | `callable`): + The phone (or callable without arguments to get it) + to which the code will be sent. If a bot-token-like + string is given, it will be used as such instead. + The argument may be a coroutine. + + password (`str`, `callable`, optional): + The password for 2 Factor Authentication (2FA). + This is only required if it is enabled in your account. + The argument may be a coroutine. + + bot_token (`str`): + Bot Token obtained by `@BotFather `_ + to log in as a bot. Cannot be specified with ``phone`` (only + one of either allowed). + + force_sms (`bool`, optional): + Whether to force sending the code request as SMS. + This only makes sense when signing in with a `phone`. + + code_callback (`callable`, optional): + A callable that will be used to retrieve the Telegram + login code. Defaults to `input()`. + The argument may be a coroutine. + + first_name (`str`, optional): + The first name to be used if signing up. This has no + effect if the account already exists and you sign in. + + last_name (`str`, optional): + Similar to the first name, but for the last. Optional. + + max_attempts (`int`, optional): + How many times the code/password callback should be + retried or switching between signing in and signing up. + + Returns + This `TelegramClient`, so initialization + can be chained with ``.start()``. + + Example + .. code-block:: python + + client = TelegramClient('anon', api_id, api_hash) + + # Starting as a bot account + await client.start(bot_token=bot_token) + + # Starting as a user account + await client.start(phone) + # Please enter the code you received: 12345 + # Please enter your password: ******* + # (You are now logged in) + + # Starting using a context manager (this calls start()): + with client: + pass + """ + if code_callback is None: + def code_callback(): + return input('Please enter the code you received: ') + elif not callable(code_callback): + raise ValueError( + 'The code_callback parameter needs to be a callable ' + 'function that returns the code you received by Telegram.' + ) + + if not phone and not bot_token: + raise ValueError('No phone number or bot token provided.') + + if phone and bot_token and not callable(phone): + raise ValueError('Both a phone and a bot token provided, ' + 'must only provide one of either') + + coro = self._start( + phone=phone, + password=password, + bot_token=bot_token, + force_sms=force_sms, + code_callback=code_callback, + first_name=first_name, + last_name=last_name, + max_attempts=max_attempts + ) + return ( + coro if self.loop.is_running() + else self.loop.run_until_complete(coro) + ) + + async def _start( + self: 'TelegramClient', phone, password, bot_token, force_sms, + code_callback, first_name, last_name, max_attempts): + if not self.is_connected(): + await self.connect() + + # Rather than using `is_user_authorized`, use `get_me`. While this is + # more expensive and needs to retrieve more data from the server, it + # enables the library to warn users trying to login to a different + # account. See #1172. + me = await self.get_me() + if me is not None: + # The warnings here are on a best-effort and may fail. + if bot_token: + # bot_token's first part has the bot ID, but it may be invalid + # so don't try to parse as int (instead cast our ID to string). + if bot_token[:bot_token.find(':')] != str(me.id): + warnings.warn( + 'the session already had an authorized user so it did ' + 'not login to the bot account using the provided bot_token; ' + 'if you were expecting a different user, check whether ' + 'you are accidentally reusing an existing session' + ) + elif phone and not callable(phone) and utils.parse_phone(phone) != me.phone: + warnings.warn( + 'the session already had an authorized user so it did ' + 'not login to the user account using the provided phone; ' + 'if you were expecting a different user, check whether ' + 'you are accidentally reusing an existing session' + ) + + return self + + if not bot_token: + # Turn the callable into a valid phone number (or bot token) + while callable(phone): + value = phone() + if inspect.isawaitable(value): + value = await value + + if ':' in value: + # Bot tokens have 'user_id:access_hash' format + bot_token = value + break + + phone = utils.parse_phone(value) or phone + + if bot_token: + await self.sign_in(bot_token=bot_token) + return self + + me = None + attempts = 0 + two_step_detected = False + + await self.send_code_request(phone, force_sms=force_sms) + while attempts < max_attempts: + try: + value = code_callback() + if inspect.isawaitable(value): + value = await value + + # Since sign-in with no code works (it sends the code) + # we must double-check that here. Else we'll assume we + # logged in, and it will return None as the User. + if not value: + raise errors.PhoneCodeEmptyError(request=None) + + # Raises SessionPasswordNeededError if 2FA enabled + me = await self.sign_in(phone, code=value) + break + except errors.SessionPasswordNeededError: + two_step_detected = True + break + except (errors.PhoneCodeEmptyError, + errors.PhoneCodeExpiredError, + errors.PhoneCodeHashEmptyError, + errors.PhoneCodeInvalidError): + print('Invalid code. Please try again.', file=sys.stderr) + + attempts += 1 + else: + raise RuntimeError( + '{} consecutive sign-in attempts failed. Aborting' + .format(max_attempts) + ) + + if two_step_detected: + if not password: + raise ValueError( + "Two-step verification is enabled for this account. " + "Please provide the 'password' argument to 'start()'." + ) + + if callable(password): + for _ in range(max_attempts): + try: + value = password() + if inspect.isawaitable(value): + value = await value + + me = await self.sign_in(phone=phone, password=value) + break + except errors.PasswordHashInvalidError: + print('Invalid password. Please try again', + file=sys.stderr) + else: + raise errors.PasswordHashInvalidError(request=None) + else: + me = await self.sign_in(phone=phone, password=password) + + # We won't reach here if any step failed (exit by exception) + signed, name = 'Signed in successfully as ', utils.get_display_name(me) + tos = '; remember to not break the ToS or you will risk an account ban!' + try: + print(signed, name, tos, sep='') + except UnicodeEncodeError: + # Some terminals don't support certain characters + print(signed, name.encode('utf-8', errors='ignore') + .decode('ascii', errors='ignore'), tos, sep='') + + return self + + def _parse_phone_and_hash(self, phone, phone_hash): + """ + Helper method to both parse and validate phone and its hash. + """ + phone = utils.parse_phone(phone) or self._phone + if not phone: + raise ValueError( + 'Please make sure to call send_code_request first.' + ) + + phone_hash = phone_hash or self._phone_code_hash.get(phone, None) + if not phone_hash: + raise ValueError('You also need to provide a phone_code_hash.') + + return phone, phone_hash + + async def sign_in( + self: 'TelegramClient', + phone: str = None, + code: typing.Union[str, int] = None, + *, + password: str = None, + bot_token: str = None, + phone_code_hash: str = None) -> 'typing.Union[types.User, types.auth.SentCode]': + """ + Logs in to Telegram to an existing user or bot account. + + You should only use this if you are not authorized yet. + + This method will send the code if it's not provided. + + .. note:: + + In most cases, you should simply use `start()` and not this method. + + Arguments + phone (`str` | `int`): + The phone to send the code to if no code was provided, + or to override the phone that was previously used with + these requests. + + code (`str` | `int`): + The code that Telegram sent. Note that if you have sent this + code through the application itself it will immediately + expire. If you want to send the code, obfuscate it somehow. + If you're not doing any of this you can ignore this note. + + password (`str`): + 2FA password, should be used if a previous call raised + ``SessionPasswordNeededError``. + + bot_token (`str`): + Used to sign in as a bot. Not all requests will be available. + This should be the hash the `@BotFather `_ + gave you. + + phone_code_hash (`str`, optional): + The hash returned by `send_code_request`. This can be left as + `None` to use the last hash known for the phone to be used. + + Returns + The signed in user, or the information about + :meth:`send_code_request`. + + Example + .. code-block:: python + + phone = '+34 123 123 123' + await client.sign_in(phone) # send code + + code = input('enter code: ') + await client.sign_in(phone, code) + """ + me = await self.get_me() + if me: + return me + + if phone and not code and not password: + return await self.send_code_request(phone) + elif code: + phone, phone_code_hash = \ + self._parse_phone_and_hash(phone, phone_code_hash) + + # May raise PhoneCodeEmptyError, PhoneCodeExpiredError, + # PhoneCodeHashEmptyError or PhoneCodeInvalidError. + request = functions.auth.SignInRequest( + phone, phone_code_hash, str(code) + ) + elif password: + pwd = await self(functions.account.GetPasswordRequest()) + request = functions.auth.CheckPasswordRequest( + pwd_mod.compute_check(pwd, password) + ) + elif bot_token: + request = functions.auth.ImportBotAuthorizationRequest( + flags=0, bot_auth_token=bot_token, + api_id=self.api_id, api_hash=self.api_hash + ) + else: + raise ValueError( + 'You must provide a phone and a code the first time, ' + 'and a password only if an RPCError was raised before.' + ) + + try: + result = await self(request) + except errors.PhoneCodeExpiredError: + self._phone_code_hash.pop(phone, None) + raise + + if isinstance(result, types.auth.AuthorizationSignUpRequired): + # Emulate pre-layer 104 behaviour + self._tos = result.terms_of_service + raise errors.PhoneNumberUnoccupiedError(request=request) + + return await self._on_login(result.user) + + async def sign_up( + self: 'TelegramClient', + code: typing.Union[str, int], + first_name: str, + last_name: str = '', + *, + phone: str = None, + phone_code_hash: str = None) -> 'types.User': + """ + This method can no longer be used, and will immediately raise a ``ValueError``. + See `issue #4050 `_ for context. + """ + raise ValueError('Third-party applications cannot sign up for Telegram. See https://github.com/LonamiWebs/Telethon/issues/4050 for details') + + async def _on_login(self, user): + """ + Callback called whenever the login or sign up process completes. + + Returns the input user parameter. + """ + self._mb_entity_cache.set_self_user(user.id, user.bot, user.access_hash) + self._authorized = True + + state = await self(functions.updates.GetStateRequest()) + # the server may send an old qts in getState + difference = await self(functions.updates.GetDifferenceRequest(pts=state.pts, date=state.date, qts=state.qts)) + + if isinstance(difference, types.updates.Difference): + state = difference.state + elif isinstance(difference, types.updates.DifferenceSlice): + state = difference.intermediate_state + elif isinstance(difference, types.updates.DifferenceTooLong): + state.pts = difference.pts + + self._message_box.load(SessionState(0, 0, 0, state.pts, state.qts, int(state.date.timestamp()), state.seq, 0), []) + + return user + + async def send_code_request( + self: 'TelegramClient', + phone: str, + *, + force_sms: bool = False, + _retry_count: int = 0) -> 'types.auth.SentCode': + """ + Sends the Telegram code needed to login to the given phone number. + + Arguments + phone (`str` | `int`): + The phone to which the code will be sent. + + force_sms (`bool`, optional): + Whether to force sending as SMS. This has been deprecated. + See `issue #4050 `_ for context. + + Returns + An instance of :tl:`SentCode`. + + Example + .. code-block:: python + + phone = '+34 123 123 123' + sent = await client.send_code_request(phone) + print(sent) + """ + if force_sms: + warnings.warn('force_sms has been deprecated and no longer works') + force_sms = False + + result = None + phone = utils.parse_phone(phone) or self._phone + phone_hash = self._phone_code_hash.get(phone) + + if not phone_hash: + try: + result = await self(functions.auth.SendCodeRequest( + phone, self.api_id, self.api_hash, types.CodeSettings())) + except errors.AuthRestartError: + if _retry_count > 2: + raise + return await self.send_code_request( + phone, force_sms=force_sms, _retry_count=_retry_count+1) + + # TODO figure out when/if/how this can happen + if isinstance(result, types.auth.SentCodeSuccess): + raise RuntimeError('logged in right after sending the code') + + # If we already sent a SMS, do not resend the code (hash may be empty) + if isinstance(result.type, types.auth.SentCodeTypeSms): + force_sms = False + + # phone_code_hash may be empty, if it is, do not save it (#1283) + if result.phone_code_hash: + self._phone_code_hash[phone] = phone_hash = result.phone_code_hash + else: + force_sms = True + + self._phone = phone + + if force_sms: + try: + result = await self( + functions.auth.ResendCodeRequest(phone, phone_hash)) + except errors.PhoneCodeExpiredError: + if _retry_count > 2: + raise + self._phone_code_hash.pop(phone, None) + self._log[__name__].info( + "Phone code expired in ResendCodeRequest, requesting a new code" + ) + return await self.send_code_request( + phone, force_sms=False, _retry_count=_retry_count+1) + + if isinstance(result, types.auth.SentCodeSuccess): + raise RuntimeError('logged in right after resending the code') + + self._phone_code_hash[phone] = result.phone_code_hash + + return result + + async def qr_login(self: 'TelegramClient', ignored_ids: typing.List[int] = None) -> custom.QRLogin: + """ + Initiates the QR login procedure. + + Note that you must be connected before invoking this, as with any + other request. + + It is up to the caller to decide how to present the code to the user, + whether it's the URL, using the token bytes directly, or generating + a QR code and displaying it by other means. + + See the documentation for `QRLogin` to see how to proceed after this. + + Arguments + ignored_ids (List[`int`]): + List of already logged-in user IDs, to prevent logging in + twice with the same user. + + Returns + An instance of `QRLogin`. + + Example + .. code-block:: python + + def display_url_as_qr(url): + pass # do whatever to show url as a qr to the user + + qr_login = await client.qr_login() + display_url_as_qr(qr_login.url) + + # Important! You need to wait for the login to complete! + await qr_login.wait() + + # If you have 2FA enabled, `wait` will raise `telethon.errors.SessionPasswordNeededError`. + # You should except that error and call `sign_in` with the password if this happens. + """ + qr_login = custom.QRLogin(self, ignored_ids or []) + await qr_login.recreate() + return qr_login + + async def log_out(self: 'TelegramClient') -> bool: + """ + Logs out Telegram and deletes the current ``*.session`` file. + + The client is unusable after logging out and a new instance should be created. + + Returns + `True` if the operation was successful. + + Example + .. code-block:: python + + # Note: you will need to login again! + await client.log_out() + """ + try: + await self(functions.auth.LogOutRequest()) + except errors.RPCError: + return False + + self._mb_entity_cache.set_self_user(None, None, None) + self._authorized = False + + await self.disconnect() + await utils.maybe_async(self.session.delete()) + self.session = None + return True + + async def edit_2fa( + self: 'TelegramClient', + current_password: str = None, + new_password: str = None, + *, + hint: str = '', + email: str = None, + email_code_callback: typing.Callable[[int], str] = None) -> bool: + """ + Changes the 2FA settings of the logged in user. + + Review carefully the parameter explanations before using this method. + + Note that this method may be *incredibly* slow depending on the + prime numbers that must be used during the process to make sure + that everything is safe. + + Has no effect if both current and new password are omitted. + + Arguments + current_password (`str`, optional): + The current password, to authorize changing to ``new_password``. + Must be set if changing existing 2FA settings. + Must **not** be set if 2FA is currently disabled. + Passing this by itself will remove 2FA (if correct). + + new_password (`str`, optional): + The password to set as 2FA. + If 2FA was already enabled, ``current_password`` **must** be set. + Leaving this blank or `None` will remove the password. + + hint (`str`, optional): + Hint to be displayed by Telegram when it asks for 2FA. + Leaving unspecified is highly discouraged. + Has no effect if ``new_password`` is not set. + + email (`str`, optional): + Recovery and verification email. If present, you must also + set `email_code_callback`, else it raises ``ValueError``. + + email_code_callback (`callable`, optional): + If an email is provided, a callback that returns the code sent + to it must also be set. This callback may be asynchronous. + It should return a string with the code. The length of the + code will be passed to the callback as an input parameter. + + If the callback returns an invalid code, it will raise + ``CodeInvalidError``. + + Returns + `True` if successful, `False` otherwise. + + Example + .. code-block:: python + + # Setting a password for your account which didn't have + await client.edit_2fa(new_password='I_<3_Telethon') + + # Removing the password + await client.edit_2fa(current_password='I_<3_Telethon') + """ + if new_password is None and current_password is None: + return False + + if email and not callable(email_code_callback): + raise ValueError('email present without email_code_callback') + + pwd = await self(functions.account.GetPasswordRequest()) + pwd.new_algo.salt1 += os.urandom(32) + assert isinstance(pwd, types.account.Password) + if not pwd.has_password and current_password: + current_password = None + + if current_password: + password = pwd_mod.compute_check(pwd, current_password) + else: + password = types.InputCheckPasswordEmpty() + + if new_password: + new_password_hash = pwd_mod.compute_digest( + pwd.new_algo, new_password) + else: + new_password_hash = b'' + + try: + await self(functions.account.UpdatePasswordSettingsRequest( + password=password, + new_settings=types.account.PasswordInputSettings( + new_algo=pwd.new_algo, + new_password_hash=new_password_hash, + hint=hint, + email=email, + new_secure_settings=None + ) + )) + except errors.EmailUnconfirmedError as e: + code = email_code_callback(e.code_length) + if inspect.isawaitable(code): + code = await code + + code = str(code) + await self(functions.account.ConfirmPasswordEmailRequest(code)) + + return True + + # endregion + + # region with blocks + + async def __aenter__(self): + return await self.start() + + async def __aexit__(self, *args): + await self.disconnect() + + __enter__ = helpers._sync_enter + __exit__ = helpers._sync_exit + + # endregion diff --git a/module/scripts/lib/telethon/client/bots.py b/module/scripts/lib/telethon/client/bots.py new file mode 100644 index 0000000..044d851 --- /dev/null +++ b/module/scripts/lib/telethon/client/bots.py @@ -0,0 +1,72 @@ +import typing + +from .. import hints +from ..tl import types, functions, custom + +if typing.TYPE_CHECKING: + from .telegramclient import TelegramClient + + +class BotMethods: + async def inline_query( + self: 'TelegramClient', + bot: 'hints.EntityLike', + query: str, + *, + entity: 'hints.EntityLike' = None, + offset: str = None, + geo_point: 'types.GeoPoint' = None) -> custom.InlineResults: + """ + Makes an inline query to the specified bot (``@vote New Poll``). + + Arguments + bot (`entity`): + The bot entity to which the inline query should be made. + + query (`str`): + The query that should be made to the bot. + + entity (`entity`, optional): + The entity where the inline query is being made from. Certain + bots use this to display different results depending on where + it's used, such as private chats, groups or channels. + + If specified, it will also be the default entity where the + message will be sent after clicked. Otherwise, the "empty + peer" will be used, which some bots may not handle correctly. + + offset (`str`, optional): + The string offset to use for the bot. + + geo_point (:tl:`GeoPoint`, optional) + The geo point location information to send to the bot + for localised results. Available under some bots. + + Returns + A list of `custom.InlineResult + `. + + Example + .. code-block:: python + + # Make an inline query to @like + results = await client.inline_query('like', 'Do you like Telethon?') + + # Send the first result to some chat + message = await results[0].click('TelethonOffTopic') + """ + bot = await self.get_input_entity(bot) + if entity: + peer = await self.get_input_entity(entity) + else: + peer = types.InputPeerEmpty() + + result = await self(functions.messages.GetInlineBotResultsRequest( + bot=bot, + peer=peer, + query=query, + offset=offset or '', + geo_point=geo_point + )) + + return custom.InlineResults(self, result, entity=peer if entity else None) diff --git a/module/scripts/lib/telethon/client/buttons.py b/module/scripts/lib/telethon/client/buttons.py new file mode 100644 index 0000000..7590aed --- /dev/null +++ b/module/scripts/lib/telethon/client/buttons.py @@ -0,0 +1,104 @@ +import typing + +from .. import utils, hints +from ..tl import types, custom + + +class ButtonMethods: + @staticmethod + def build_reply_markup( + buttons: 'typing.Optional[hints.MarkupLike]' + ) -> 'typing.Optional[types.TypeReplyMarkup]': + """ + Builds a :tl:`ReplyInlineMarkup` or :tl:`ReplyKeyboardMarkup` for + the given buttons. + + Does nothing if either no buttons are provided or the provided + argument is already a reply markup. + + You should consider using this method if you are going to reuse + the markup very often. Otherwise, it is not necessary. + + This method is **not** asynchronous (don't use ``await`` on it). + + Arguments + buttons (`hints.MarkupLike`): + The button, list of buttons, array of buttons or markup + to convert into a markup. + + Example + .. code-block:: python + + from telethon import Button + + markup = client.build_reply_markup(Button.inline('hi')) + # later + await client.send_message(chat, 'click me', buttons=markup) + """ + if buttons is None: + return None + + try: + if buttons.SUBCLASS_OF_ID == 0xe2e10ef2: # crc32(b'ReplyMarkup'): + return buttons + except AttributeError: + pass + + if not utils.is_list_like(buttons): + buttons = [[buttons]] + elif not buttons or not utils.is_list_like(buttons[0]): + buttons = [buttons] + + is_inline = False + is_normal = False + resize = None + single_use = None + selective = None + persistent = None + placeholder = None + + rows = [] + for row in buttons: + current = [] + for button in row: + if isinstance(button, custom.Button): + if button.resize is not None: + resize = button.resize + if button.single_use is not None: + single_use = button.single_use + if button.selective is not None: + selective = button.selective + if button.persistent is not None: + persistent = button.persistent + if button.placeholder is not None: + placeholder = button.placeholder + + button = button.button + elif isinstance(button, custom.MessageButton): + button = button.button + + inline = custom.Button._is_inline(button) + is_inline |= inline + is_normal |= not inline + + if isinstance(button, (types.KeyboardButton, types.KeyboardInlineButton)): + current.append(button) + + if current: + if is_inline: + rows.append(types.KeyboardInlineButtonRow(current)) + else: + rows.append(types.KeyboardButtonRow(current)) + + if is_inline and is_normal: + raise ValueError('You cannot mix inline with normal buttons') + elif is_inline: + return types.ReplyInlineMarkup(rows) + return types.ReplyKeyboardMarkup( + rows=rows, + resize=resize, + single_use=single_use, + selective=selective, + persistent=persistent, + placeholder=placeholder + ) diff --git a/module/scripts/lib/telethon/client/chats.py b/module/scripts/lib/telethon/client/chats.py new file mode 100644 index 0000000..28faae8 --- /dev/null +++ b/module/scripts/lib/telethon/client/chats.py @@ -0,0 +1,1336 @@ +import asyncio +import inspect +import itertools +import string +import typing + +from .. import helpers, utils, hints, errors +from ..requestiter import RequestIter +from ..tl import types, functions, custom + +if typing.TYPE_CHECKING: + from .telegramclient import TelegramClient + +_MAX_PARTICIPANTS_CHUNK_SIZE = 200 +_MAX_ADMIN_LOG_CHUNK_SIZE = 100 +_MAX_PROFILE_PHOTO_CHUNK_SIZE = 100 + + +class _ChatAction: + _str_mapping = { + 'typing': types.SendMessageTypingAction(), + 'contact': types.SendMessageChooseContactAction(), + 'game': types.SendMessageGamePlayAction(), + 'location': types.SendMessageGeoLocationAction(), + 'sticker': types.SendMessageChooseStickerAction(), + + 'record-audio': types.SendMessageRecordAudioAction(), + 'record-voice': types.SendMessageRecordAudioAction(), # alias + 'record-round': types.SendMessageRecordRoundAction(), + 'record-video': types.SendMessageRecordVideoAction(), + + 'audio': types.SendMessageUploadAudioAction(1), + 'voice': types.SendMessageUploadAudioAction(1), # alias + 'song': types.SendMessageUploadAudioAction(1), # alias + 'round': types.SendMessageUploadRoundAction(1), + 'video': types.SendMessageUploadVideoAction(1), + + 'photo': types.SendMessageUploadPhotoAction(1), + 'document': types.SendMessageUploadDocumentAction(1), + 'file': types.SendMessageUploadDocumentAction(1), # alias + + 'cancel': types.SendMessageCancelAction() + } + + def __init__(self, client, chat, action, *, delay, auto_cancel): + self._client = client + self._chat = chat + self._action = action + self._delay = delay + self._auto_cancel = auto_cancel + self._request = None + self._task = None + self._running = False + + async def __aenter__(self): + self._chat = await self._client.get_input_entity(self._chat) + + # Since `self._action` is passed by reference we can avoid + # recreating the request all the time and still modify + # `self._action.progress` directly in `progress`. + self._request = functions.messages.SetTypingRequest( + self._chat, self._action) + + self._running = True + self._task = self._client.loop.create_task(self._update()) + return self + + async def __aexit__(self, *args): + self._running = False + if self._task: + self._task.cancel() + try: + await self._task + except asyncio.CancelledError: + pass + + self._task = None + + __enter__ = helpers._sync_enter + __exit__ = helpers._sync_exit + + async def _update(self): + try: + while self._running: + await self._client(self._request) + await asyncio.sleep(self._delay) + except ConnectionError: + pass + except asyncio.CancelledError: + if self._auto_cancel: + await self._client(functions.messages.SetTypingRequest( + self._chat, types.SendMessageCancelAction())) + + def progress(self, current, total): + if hasattr(self._action, 'progress'): + self._action.progress = 100 * round(current / total) + + +class _ParticipantsIter(RequestIter): + async def _init(self, entity, filter, search): + if isinstance(filter, type): + if filter in (types.ChannelParticipantsBanned, + types.ChannelParticipantsKicked, + types.ChannelParticipantsSearch, + types.ChannelParticipantsContacts): + # These require a `q` parameter (support types for convenience) + filter = filter('') + else: + filter = filter() + + entity = await self.client.get_input_entity(entity) + ty = helpers._entity_type(entity) + if search and (filter or ty != helpers._EntityType.CHANNEL): + # We need to 'search' ourselves unless we have a PeerChannel + search = search.casefold() + + self.filter_entity = lambda ent: ( + search in utils.get_display_name(ent).casefold() or + search in (getattr(ent, 'username', None) or '').casefold() + ) + else: + self.filter_entity = lambda ent: True + + # Only used for channels, but we should always set the attribute + # Called `requests` even though it's just one for legacy purposes. + self.requests = None + + if ty == helpers._EntityType.CHANNEL: + if self.limit <= 0: + # May not have access to the channel, but getFull can get the .total. + self.total = (await self.client( + functions.channels.GetFullChannelRequest(entity) + )).full_chat.participants_count + raise StopAsyncIteration + + self.seen = set() + self.requests = functions.channels.GetParticipantsRequest( + channel=entity, + filter=filter or types.ChannelParticipantsSearch(search), + offset=0, + limit=_MAX_PARTICIPANTS_CHUNK_SIZE, + hash=0 + ) + + elif ty == helpers._EntityType.CHAT: + full = await self.client( + functions.messages.GetFullChatRequest(entity.chat_id)) + if not isinstance( + full.full_chat.participants, types.ChatParticipants): + # ChatParticipantsForbidden won't have ``.participants`` + self.total = 0 + raise StopAsyncIteration + + self.total = len(full.full_chat.participants.participants) + + users = {user.id: user for user in full.users} + for participant in full.full_chat.participants.participants: + if isinstance(participant, types.ChannelParticipantLeft): + # See issue #3231 to learn why this is ignored. + continue + elif isinstance(participant, types.ChannelParticipantBanned): + user_id = participant.peer.user_id + else: + user_id = participant.user_id + user = users[user_id] + if not self.filter_entity(user): + continue + + user = users[user_id] + user.participant = participant + self.buffer.append(user) + + return True + else: + self.total = 1 + if self.limit != 0: + user = await self.client.get_entity(entity) + if self.filter_entity(user): + user.participant = None + self.buffer.append(user) + + return True + + async def _load_next_chunk(self): + if not self.requests: + return True + + self.requests.limit = min(self.limit - self.requests.offset, _MAX_PARTICIPANTS_CHUNK_SIZE) + + if self.requests.offset > self.limit: + return True + + if self.total is None: + f = self.requests.filter + if ( + not isinstance(f, types.ChannelParticipantsRecent) + and (not isinstance(f, types.ChannelParticipantsSearch) or f.q) + ): + # Only do an additional getParticipants here to get the total + # if there's a filter which would reduce the real total number. + # getParticipants is cheaper than getFull. + self.total = (await self.client(functions.channels.GetParticipantsRequest( + channel=self.requests.channel, + filter=types.ChannelParticipantsRecent(), + offset=0, + limit=1, + hash=0 + ))).count + + participants = await self.client(self.requests) + if self.total is None: + # Will only get here if there was one request with a filter that matched all users. + self.total = participants.count + if not participants.users: + self.requests = None + return + + self.requests.offset += len(participants.participants) + users = {user.id: user for user in participants.users} + for participant in participants.participants: + if isinstance(participant, types.ChannelParticipantLeft): + # See issue #3231 to learn why this is ignored. + continue + elif isinstance(participant, types.ChannelParticipantBanned): + if not isinstance(participant.peer, types.PeerUser): + # May have the entire channel banned. See #3105. + continue + user_id = participant.peer.user_id + else: + user_id = participant.user_id + + user = users[user_id] + if not self.filter_entity(user) or user.id in self.seen: + continue + self.seen.add(user_id) + user = users[user_id] + user.participant = participant + self.buffer.append(user) + + +class _AdminLogIter(RequestIter): + async def _init( + self, entity, admins, search, min_id, max_id, + join, leave, invite, restrict, unrestrict, ban, unban, + promote, demote, info, settings, pinned, edit, delete, + group_call + ): + if any((join, leave, invite, restrict, unrestrict, ban, unban, + promote, demote, info, settings, pinned, edit, delete, + group_call)): + events_filter = types.ChannelAdminLogEventsFilter( + join=join, leave=leave, invite=invite, ban=restrict, + unban=unrestrict, kick=ban, unkick=unban, promote=promote, + demote=demote, info=info, settings=settings, pinned=pinned, + edit=edit, delete=delete, group_call=group_call + ) + else: + events_filter = None + + self.entity = await self.client.get_input_entity(entity) + + admin_list = [] + if admins: + if not utils.is_list_like(admins): + admins = (admins,) + + for admin in admins: + admin_list.append(await self.client.get_input_entity(admin)) + + self.request = functions.channels.GetAdminLogRequest( + self.entity, q=search or '', min_id=min_id, max_id=max_id, + limit=0, events_filter=events_filter, admins=admin_list or None + ) + + async def _load_next_chunk(self): + self.request.limit = min(self.left, _MAX_ADMIN_LOG_CHUNK_SIZE) + r = await self.client(self.request) + entities = {utils.get_peer_id(x): x + for x in itertools.chain(r.users, r.chats)} + + self.request.max_id = min((e.id for e in r.events), default=0) + for ev in r.events: + if isinstance(ev.action, + types.ChannelAdminLogEventActionEditMessage): + ev.action.prev_message._finish_init( + self.client, entities, self.entity) + + ev.action.new_message._finish_init( + self.client, entities, self.entity) + + elif isinstance(ev.action, + types.ChannelAdminLogEventActionDeleteMessage): + ev.action.message._finish_init( + self.client, entities, self.entity) + + self.buffer.append(custom.AdminLogEvent(ev, entities)) + + if len(r.events) < self.request.limit: + return True + + +class _ProfilePhotoIter(RequestIter): + async def _init( + self, entity, offset, max_id + ): + entity = await self.client.get_input_entity(entity) + ty = helpers._entity_type(entity) + if ty == helpers._EntityType.USER: + self.request = functions.photos.GetUserPhotosRequest( + entity, + offset=offset, + max_id=max_id, + limit=1 + ) + else: + self.request = functions.messages.SearchRequest( + peer=entity, + q='', + filter=types.InputMessagesFilterChatPhotos(), + min_date=None, + max_date=None, + offset_id=0, + add_offset=offset, + limit=1, + max_id=max_id, + min_id=0, + hash=0 + ) + + if self.limit == 0: + self.request.limit = 1 + result = await self.client(self.request) + if isinstance(result, types.photos.Photos): + self.total = len(result.photos) + elif isinstance(result, types.messages.Messages): + self.total = len(result.messages) + else: + # Luckily both photosSlice and messages have a count for total + self.total = getattr(result, 'count', None) + + async def _load_next_chunk(self): + self.request.limit = min(self.left, _MAX_PROFILE_PHOTO_CHUNK_SIZE) + result = await self.client(self.request) + + if isinstance(result, types.photos.Photos): + self.buffer = result.photos + self.left = len(self.buffer) + self.total = len(self.buffer) + elif isinstance(result, types.messages.Messages): + self.buffer = [x.action.photo for x in result.messages + if isinstance(x.action, types.MessageActionChatEditPhoto)] + + self.left = len(self.buffer) + self.total = len(self.buffer) + elif isinstance(result, types.photos.PhotosSlice): + self.buffer = result.photos + self.total = result.count + if len(self.buffer) < self.request.limit: + self.left = len(self.buffer) + else: + self.request.offset += len(result.photos) + else: + # Some broadcast channels have a photo that this request doesn't + # retrieve for whatever random reason the Telegram server feels. + # + # This means the `total` count may be wrong but there's not much + # that can be done around it (perhaps there are too many photos + # and this is only a partial result so it's not possible to just + # use the len of the result). + self.total = getattr(result, 'count', None) + + # Unconditionally fetch the full channel to obtain this photo and + # yield it with the rest (unless it's a duplicate). + seen_id = None + if isinstance(result, types.messages.ChannelMessages): + channel = await self.client(functions.channels.GetFullChannelRequest(self.request.peer)) + photo = channel.full_chat.chat_photo + if isinstance(photo, types.Photo): + self.buffer.append(photo) + seen_id = photo.id + + self.buffer.extend( + x.action.photo for x in result.messages + if isinstance(x.action, types.MessageActionChatEditPhoto) + and x.action.photo.id != seen_id + ) + + if len(result.messages) < self.request.limit: + self.left = len(self.buffer) + elif result.messages: + self.request.add_offset = 0 + self.request.offset_id = result.messages[-1].id + + +class ChatMethods: + + # region Public methods + + def iter_participants( + self: 'TelegramClient', + entity: 'hints.EntityLike', + limit: float = None, + *, + search: str = '', + filter: 'types.TypeChannelParticipantsFilter' = None, + aggressive: bool = False) -> _ParticipantsIter: + """ + Iterator over the participants belonging to the specified chat. + + The order is unspecified. + + Arguments + entity (`entity`): + The entity from which to retrieve the participants list. + + limit (`int`): + Limits amount of participants fetched. + + search (`str`, optional): + Look for participants with this string in name/username. + + filter (:tl:`ChannelParticipantsFilter`, optional): + The filter to be used, if you want e.g. only admins + Note that you might not have permissions for some filter. + This has no effect for normal chats or users. + + .. note:: + + The filter :tl:`ChannelParticipantsBanned` will return + *restricted* users. If you want *banned* users you should + use :tl:`ChannelParticipantsKicked` instead. + + aggressive (`bool`, optional): + Does nothing. This is kept for backwards-compatibility. + + There have been several changes to Telegram's API that limits + the amount of members that can be retrieved, and this was a + hack that no longer works. + + Yields + The :tl:`User` objects returned by :tl:`GetParticipantsRequest` + with an additional ``.participant`` attribute which is the + matched :tl:`ChannelParticipant` type for channels/megagroups + or :tl:`ChatParticipants` for normal chats. + + Example + .. code-block:: python + + # Show all user IDs in a chat + async for user in client.iter_participants(chat): + print(user.id) + + # Search by name + async for user in client.iter_participants(chat, search='name'): + print(user.username) + + # Filter by admins + from telethon.tl.types import ChannelParticipantsAdmins + async for user in client.iter_participants(chat, filter=ChannelParticipantsAdmins): + print(user.first_name) + """ + return _ParticipantsIter( + self, + limit, + entity=entity, + filter=filter, + search=search + ) + + async def get_participants( + self: 'TelegramClient', + *args, + **kwargs) -> 'hints.TotalList': + """ + Same as `iter_participants()`, but returns a + `TotalList ` instead. + + Example + .. code-block:: python + + users = await client.get_participants(chat) + print(users[0].first_name) + + for user in users: + if user.username is not None: + print(user.username) + """ + return await self.iter_participants(*args, **kwargs).collect() + + get_participants.__signature__ = inspect.signature(iter_participants) + + + def iter_admin_log( + self: 'TelegramClient', + entity: 'hints.EntityLike', + limit: float = None, + *, + max_id: int = 0, + min_id: int = 0, + search: str = None, + admins: 'hints.EntitiesLike' = None, + join: bool = None, + leave: bool = None, + invite: bool = None, + restrict: bool = None, + unrestrict: bool = None, + ban: bool = None, + unban: bool = None, + promote: bool = None, + demote: bool = None, + info: bool = None, + settings: bool = None, + pinned: bool = None, + edit: bool = None, + delete: bool = None, + group_call: bool = None) -> _AdminLogIter: + """ + Iterator over the admin log for the specified channel. + + The default order is from the most recent event to to the oldest. + + Note that you must be an administrator of it to use this method. + + If none of the filters are present (i.e. they all are `None`), + *all* event types will be returned. If at least one of them is + `True`, only those that are true will be returned. + + Arguments + entity (`entity`): + The channel entity from which to get its admin log. + + limit (`int` | `None`, optional): + Number of events to be retrieved. + + The limit may also be `None`, which would eventually return + the whole history. + + max_id (`int`): + All the events with a higher (newer) ID or equal to this will + be excluded. + + min_id (`int`): + All the events with a lower (older) ID or equal to this will + be excluded. + + search (`str`): + The string to be used as a search query. + + admins (`entity` | `list`): + If present, the events will be filtered by these admins + (or single admin) and only those caused by them will be + returned. + + join (`bool`): + If `True`, events for when a user joined will be returned. + + leave (`bool`): + If `True`, events for when a user leaves will be returned. + + invite (`bool`): + If `True`, events for when a user joins through an invite + link will be returned. + + restrict (`bool`): + If `True`, events with partial restrictions will be + returned. This is what the API calls "ban". + + unrestrict (`bool`): + If `True`, events removing restrictions will be returned. + This is what the API calls "unban". + + ban (`bool`): + If `True`, events applying or removing all restrictions will + be returned. This is what the API calls "kick" (restricting + all permissions removed is a ban, which kicks the user). + + unban (`bool`): + If `True`, events removing all restrictions will be + returned. This is what the API calls "unkick". + + promote (`bool`): + If `True`, events with admin promotions will be returned. + + demote (`bool`): + If `True`, events with admin demotions will be returned. + + info (`bool`): + If `True`, events changing the group info will be returned. + + settings (`bool`): + If `True`, events changing the group settings will be + returned. + + pinned (`bool`): + If `True`, events of new pinned messages will be returned. + + edit (`bool`): + If `True`, events of message edits will be returned. + + delete (`bool`): + If `True`, events of message deletions will be returned. + + group_call (`bool`): + If `True`, events related to group calls will be returned. + + Yields + Instances of `AdminLogEvent `. + + Example + .. code-block:: python + + async for event in client.iter_admin_log(channel): + if event.changed_title: + print('The title changed from', event.old, 'to', event.new) + """ + return _AdminLogIter( + self, + limit, + entity=entity, + admins=admins, + search=search, + min_id=min_id, + max_id=max_id, + join=join, + leave=leave, + invite=invite, + restrict=restrict, + unrestrict=unrestrict, + ban=ban, + unban=unban, + promote=promote, + demote=demote, + info=info, + settings=settings, + pinned=pinned, + edit=edit, + delete=delete, + group_call=group_call + ) + + async def get_admin_log( + self: 'TelegramClient', + *args, + **kwargs) -> 'hints.TotalList': + """ + Same as `iter_admin_log()`, but returns a ``list`` instead. + + Example + .. code-block:: python + + # Get a list of deleted message events which said "heck" + events = await client.get_admin_log(channel, search='heck', delete=True) + + # Print the old message before it was deleted + print(events[0].old) + """ + return await self.iter_admin_log(*args, **kwargs).collect() + + get_admin_log.__signature__ = inspect.signature(iter_admin_log) + + def iter_profile_photos( + self: 'TelegramClient', + entity: 'hints.EntityLike', + limit: int = None, + *, + offset: int = 0, + max_id: int = 0) -> _ProfilePhotoIter: + """ + Iterator over a user's profile photos or a chat's photos. + + The order is from the most recent photo to the oldest. + + Arguments + entity (`entity`): + The entity from which to get the profile or chat photos. + + limit (`int` | `None`, optional): + Number of photos to be retrieved. + + The limit may also be `None`, which would eventually all + the photos that are still available. + + offset (`int`): + How many photos should be skipped before returning the first one. + + max_id (`int`): + The maximum ID allowed when fetching photos. + + Yields + Instances of :tl:`Photo`. + + Example + .. code-block:: python + + # Download all the profile photos of some user + async for photo in client.iter_profile_photos(user): + await client.download_media(photo) + """ + return _ProfilePhotoIter( + self, + limit, + entity=entity, + offset=offset, + max_id=max_id + ) + + async def get_profile_photos( + self: 'TelegramClient', + *args, + **kwargs) -> 'hints.TotalList': + """ + Same as `iter_profile_photos()`, but returns a + `TotalList ` instead. + + Example + .. code-block:: python + + # Get the photos of a channel + photos = await client.get_profile_photos(channel) + + # Download the oldest photo + await client.download_media(photos[-1]) + """ + return await self.iter_profile_photos(*args, **kwargs).collect() + + get_profile_photos.__signature__ = inspect.signature(iter_profile_photos) + + def action( + self: 'TelegramClient', + entity: 'hints.EntityLike', + action: 'typing.Union[str, types.TypeSendMessageAction]', + *, + delay: float = 4, + auto_cancel: bool = True) -> 'typing.Union[_ChatAction, typing.Coroutine]': + """ + Returns a context-manager object to represent a "chat action". + + Chat actions indicate things like "user is typing", "user is + uploading a photo", etc. + + If the action is ``'cancel'``, you should just ``await`` the result, + since it makes no sense to use a context-manager for it. + + See the example below for intended usage. + + Arguments + entity (`entity`): + The entity where the action should be showed in. + + action (`str` | :tl:`SendMessageAction`): + The action to show. You can either pass a instance of + :tl:`SendMessageAction` or better, a string used while: + + * ``'typing'``: typing a text message. + * ``'contact'``: choosing a contact. + * ``'game'``: playing a game. + * ``'location'``: choosing a geo location. + * ``'sticker'``: choosing a sticker. + * ``'record-audio'``: recording a voice note. + You may use ``'record-voice'`` as alias. + * ``'record-round'``: recording a round video. + * ``'record-video'``: recording a normal video. + * ``'audio'``: sending an audio file (voice note or song). + You may use ``'voice'`` and ``'song'`` as aliases. + * ``'round'``: uploading a round video. + * ``'video'``: uploading a video file. + * ``'photo'``: uploading a photo. + * ``'document'``: uploading a document file. + You may use ``'file'`` as alias. + * ``'cancel'``: cancel any pending action in this chat. + + Invalid strings will raise a ``ValueError``. + + delay (`int` | `float`): + The delay, in seconds, to wait between sending actions. + For example, if the delay is 5 and it takes 7 seconds to + do something, three requests will be made at 0s, 5s, and + 7s to cancel the action. + + auto_cancel (`bool`): + Whether the action should be cancelled once the context + manager exists or not. The default is `True`, since + you don't want progress to be shown when it has already + completed. + + Returns + Either a context-manager object or a coroutine. + + Example + .. code-block:: python + + # Type for 2 seconds, then send a message + async with client.action(chat, 'typing'): + await asyncio.sleep(2) + await client.send_message(chat, 'Hello world! I type slow ^^') + + # Cancel any previous action + await client.action(chat, 'cancel') + + # Upload a document, showing its progress (most clients ignore this) + async with client.action(chat, 'document') as action: + await client.send_file(chat, zip_file, progress_callback=action.progress) + """ + if isinstance(action, str): + try: + action = _ChatAction._str_mapping[action.lower()] + except KeyError: + raise ValueError( + 'No such action "{}"'.format(action)) from None + elif not isinstance(action, types.TLObject) or action.SUBCLASS_OF_ID != 0x20b2cc21: + # 0x20b2cc21 = crc32(b'SendMessageAction') + if isinstance(action, type): + raise ValueError('You must pass an instance, not the class') + else: + raise ValueError('Cannot use {} as action'.format(action)) + + if isinstance(action, types.SendMessageCancelAction): + # ``SetTypingRequest.resolve`` will get input peer of ``entity``. + return self(functions.messages.SetTypingRequest( + entity, types.SendMessageCancelAction())) + + return _ChatAction( + self, entity, action, delay=delay, auto_cancel=auto_cancel) + + async def edit_admin( + self: 'TelegramClient', + entity: 'hints.EntityLike', + user: 'hints.EntityLike', + *, + change_info: bool = None, + post_messages: bool = None, + edit_messages: bool = None, + delete_messages: bool = None, + ban_users: bool = None, + invite_users: bool = None, + pin_messages: bool = None, + add_admins: bool = None, + manage_call: bool = None, + anonymous: bool = None, + is_admin: bool = None, + title: str = None) -> types.Updates: + """ + Edits admin permissions for someone in a chat. + + Raises an error if a wrong combination of rights are given + (e.g. you don't have enough permissions to grant one). + + Unless otherwise stated, permissions will work in channels and megagroups. + + Arguments + entity (`entity`): + The channel, megagroup or chat where the promotion should happen. + + user (`entity`): + The user to be promoted. + + change_info (`bool`, optional): + Whether the user will be able to change info. + + post_messages (`bool`, optional): + Whether the user will be able to post in the channel. + This will only work in broadcast channels. + + edit_messages (`bool`, optional): + Whether the user will be able to edit messages in the channel. + This will only work in broadcast channels. + + delete_messages (`bool`, optional): + Whether the user will be able to delete messages. + + ban_users (`bool`, optional): + Whether the user will be able to ban users. + + invite_users (`bool`, optional): + Whether the user will be able to invite users. Needs some testing. + + pin_messages (`bool`, optional): + Whether the user will be able to pin messages. + + add_admins (`bool`, optional): + Whether the user will be able to add admins. + + manage_call (`bool`, optional): + Whether the user will be able to manage group calls. + + anonymous (`bool`, optional): + Whether the user will remain anonymous when sending messages. + The sender of the anonymous messages becomes the group itself. + + .. note:: + + Users may be able to identify the anonymous admin by its + custom title, so additional care is needed when using both + ``anonymous`` and custom titles. For example, if multiple + anonymous admins share the same title, users won't be able + to distinguish them. + + is_admin (`bool`, optional): + Whether the user will be an admin in the chat. + This will only work in small group chats. + Whether the user will be an admin in the chat. This is the + only permission available in small group chats, and when + used in megagroups, all non-explicitly set permissions will + have this value. + + Essentially, only passing ``is_admin=True`` will grant all + permissions, but you can still disable those you need. + + title (`str`, optional): + The custom title (also known as "rank") to show for this admin. + This text will be shown instead of the "admin" badge. + This will only work in channels and megagroups. + + When left unspecified or empty, the default localized "admin" + badge will be shown. + + Returns + The resulting :tl:`Updates` object. + + Example + .. code-block:: python + + # Allowing `user` to pin messages in `chat` + await client.edit_admin(chat, user, pin_messages=True) + + # Granting all permissions except for `add_admins` + await client.edit_admin(chat, user, is_admin=True, add_admins=False) + """ + entity = await self.get_input_entity(entity) + user = await self.get_input_entity(user) + + perm_names = ( + 'change_info', 'post_messages', 'edit_messages', 'delete_messages', + 'ban_users', 'invite_users', 'pin_messages', 'add_admins', + 'anonymous', 'manage_call', + ) + + ty = helpers._entity_type(entity) + if ty == helpers._EntityType.CHANNEL: + # If we try to set these permissions in a megagroup, we + # would get a RIGHT_FORBIDDEN. However, it makes sense + # that an admin can post messages, so we want to avoid the error + if post_messages or edit_messages: + # TODO get rid of this once sessions cache this information + if entity.channel_id not in self._megagroup_cache: + full_entity = await self.get_entity(entity) + self._megagroup_cache[entity.channel_id] = getattr(full_entity, 'megagroup', False) + + if self._megagroup_cache[entity.channel_id]: + post_messages = None + edit_messages = None + + perms = locals() + return await self(functions.channels.EditAdminRequest(entity, user, types.ChatAdminRights(**{ + # A permission is its explicit (not-None) value or `is_admin`. + # This essentially makes `is_admin` be the default value. + name: perms[name] if perms[name] is not None else is_admin + for name in perm_names + }), rank=title or '')) + + elif ty == helpers._EntityType.CHAT: + # If the user passed any permission in a small + # group chat, they must be a full admin to have it. + if is_admin is None: + is_admin = any(locals()[x] for x in perm_names) + + return await self(functions.messages.EditChatAdminRequest( + entity.chat_id, user, is_admin=is_admin)) + + else: + raise ValueError( + 'You can only edit permissions in groups and channels') + + async def edit_permissions( + self: 'TelegramClient', + entity: 'hints.EntityLike', + user: 'typing.Optional[hints.EntityLike]' = None, + until_date: 'hints.DateLike' = None, + *, + view_messages: bool = True, + send_messages: bool = True, + send_media: bool = True, + send_stickers: bool = True, + send_gifs: bool = True, + send_games: bool = True, + send_inline: bool = True, + embed_link_previews: bool = True, + send_polls: bool = True, + change_info: bool = True, + invite_users: bool = True, + pin_messages: bool = True) -> types.Updates: + """ + Edits user restrictions in a chat. + + Set an argument to `False` to apply a restriction (i.e. remove + the permission), or omit them to use the default `True` (i.e. + don't apply a restriction). + + Raises an error if a wrong combination of rights are given + (e.g. you don't have enough permissions to revoke one). + + By default, each boolean argument is `True`, meaning that it + is true that the user has access to the default permission + and may be able to make use of it. + + If you set an argument to `False`, then a restriction is applied + regardless of the default permissions. + + It is important to note that `True` does *not* mean grant, only + "don't restrict", and this is where the default permissions come + in. A user may have not been revoked the ``pin_messages`` permission + (it is `True`) but they won't be able to use it if the default + permissions don't allow it either. + + Arguments + entity (`entity`): + The channel or megagroup where the restriction should happen. + + user (`entity`, optional): + If specified, the permission will be changed for the specific user. + If left as `None`, the default chat permissions will be updated. + + until_date (`DateLike`, optional): + When the user will be unbanned. + + If the due date or duration is longer than 366 days or shorter than + 30 seconds, the ban will be forever. Defaults to ``0`` (ban forever). + + view_messages (`bool`, optional): + Whether the user is able to view messages or not. + Forbidding someone from viewing messages equals to banning them. + This will only work if ``user`` is set. + + send_messages (`bool`, optional): + Whether the user is able to send messages or not. + + send_media (`bool`, optional): + Whether the user is able to send media or not. + + send_stickers (`bool`, optional): + Whether the user is able to send stickers or not. + + send_gifs (`bool`, optional): + Whether the user is able to send animated gifs or not. + + send_games (`bool`, optional): + Whether the user is able to send games or not. + + send_inline (`bool`, optional): + Whether the user is able to use inline bots or not. + + embed_link_previews (`bool`, optional): + Whether the user is able to enable the link preview in the + messages they send. Note that the user will still be able to + send messages with links if this permission is removed, but + these links won't display a link preview. + + send_polls (`bool`, optional): + Whether the user is able to send polls or not. + + change_info (`bool`, optional): + Whether the user is able to change info or not. + + invite_users (`bool`, optional): + Whether the user is able to invite other users or not. + + pin_messages (`bool`, optional): + Whether the user is able to pin messages or not. + + Returns + The resulting :tl:`Updates` object. + + Example + .. code-block:: python + + from datetime import timedelta + + # Banning `user` from `chat` for 1 minute + await client.edit_permissions(chat, user, timedelta(minutes=1), + view_messages=False) + + # Banning `user` from `chat` forever + await client.edit_permissions(chat, user, view_messages=False) + + # Kicking someone (ban + un-ban) + await client.edit_permissions(chat, user, view_messages=False) + await client.edit_permissions(chat, user) + """ + entity = await self.get_input_entity(entity) + ty = helpers._entity_type(entity) + if ty != helpers._EntityType.CHANNEL: + raise ValueError('You must pass either a channel or a supergroup') + + rights = types.ChatBannedRights( + until_date=until_date, + view_messages=not view_messages, + send_messages=not send_messages, + send_media=not send_media, + send_stickers=not send_stickers, + send_gifs=not send_gifs, + send_games=not send_games, + send_inline=not send_inline, + embed_links=not embed_link_previews, + send_polls=not send_polls, + change_info=not change_info, + invite_users=not invite_users, + pin_messages=not pin_messages + ) + + if user is None: + return await self(functions.messages.EditChatDefaultBannedRightsRequest( + peer=entity, + banned_rights=rights + )) + + user = await self.get_input_entity(user) + + return await self(functions.channels.EditBannedRequest( + channel=entity, + participant=user, + banned_rights=rights + )) + + async def kick_participant( + self: 'TelegramClient', + entity: 'hints.EntityLike', + user: 'typing.Optional[hints.EntityLike]' + ): + """ + Kicks a user from a chat. + + Kicking yourself (``'me'``) will result in leaving the chat. + + .. note:: + + Attempting to kick someone who was banned will remove their + restrictions (and thus unbanning them), since kicking is just + ban + unban. + + Arguments + entity (`entity`): + The channel or chat where the user should be kicked from. + + user (`entity`, optional): + The user to kick. + + Returns + Returns the service `Message ` + produced about a user being kicked, if any. + + Example + .. code-block:: python + + # Kick some user from some chat, and deleting the service message + msg = await client.kick_participant(chat, user) + await msg.delete() + + # Leaving chat + await client.kick_participant(chat, 'me') + """ + entity = await self.get_input_entity(entity) + user = await self.get_input_entity(user) + + ty = helpers._entity_type(entity) + if ty == helpers._EntityType.CHAT: + resp = await self(functions.messages.DeleteChatUserRequest(entity.chat_id, user)) + elif ty == helpers._EntityType.CHANNEL: + if isinstance(user, types.InputPeerSelf): + # Despite no longer being in the channel, the account still + # seems to get the service message. + resp = await self(functions.channels.LeaveChannelRequest(entity)) + else: + resp = await self(functions.channels.EditBannedRequest( + channel=entity, + participant=user, + banned_rights=types.ChatBannedRights( + until_date=None, view_messages=True) + )) + await asyncio.sleep(0.5) + await self(functions.channels.EditBannedRequest( + channel=entity, + participant=user, + banned_rights=types.ChatBannedRights(until_date=None) + )) + else: + raise ValueError('You must pass either a channel or a chat') + + return self._get_response_message(None, resp, entity) + + async def get_permissions( + self: 'TelegramClient', + entity: 'hints.EntityLike', + user: 'hints.EntityLike' = None + ) -> 'typing.Optional[custom.ParticipantPermissions]': + """ + Fetches the permissions of a user in a specific chat or channel or + get Default Restricted Rights of Chat or Channel. + + .. note:: + + This request has to fetch the entire chat for small group chats, + which can get somewhat expensive, so use of a cache is advised. + + Arguments + entity (`entity`): + The channel or chat the user is participant of. + + user (`entity`, optional): + Target user. + + Returns + A `ParticipantPermissions ` + instance. Refer to its documentation to see what properties are + available. + + Example + .. code-block:: python + + permissions = await client.get_permissions(chat, user) + if permissions.is_admin: + # do something + + # Get Banned Permissions of Chat + await client.get_permissions(chat) + """ + entity = await self.get_entity(entity) + + if not user: + if isinstance(entity, (types.Channel, types.Community)): + FullChat = await self(functions.channels.GetFullChannelRequest(entity)) + elif isinstance(entity, types.Chat): + FullChat = await self(functions.messages.GetFullChatRequest(entity.id)) + else: + return + return FullChat.chats[0].default_banned_rights + + entity = await self.get_input_entity(entity) + user = await self.get_input_entity(user) + if helpers._entity_type(entity) == helpers._EntityType.CHANNEL: + participant = await self(functions.channels.GetParticipantRequest( + entity, + user + )) + return custom.ParticipantPermissions(participant.participant, False) + elif helpers._entity_type(entity) == helpers._EntityType.CHAT: + chat = await self(functions.messages.GetFullChatRequest( + entity.chat_id + )) + if isinstance(user, types.InputPeerSelf): + user = await self.get_me(input_peer=True) + for participant in chat.full_chat.participants.participants: + if participant.user_id == user.user_id: + return custom.ParticipantPermissions(participant, True) + raise errors.UserNotParticipantError(None) + + raise ValueError('You must pass either a channel or a chat') + + async def get_stats( + self: 'TelegramClient', + entity: 'hints.EntityLike', + message: 'typing.Union[int, types.Message]' = None, + ): + """ + Retrieves statistics from the given megagroup or broadcast channel. + + Note that some restrictions apply before being able to fetch statistics, + in particular the channel must have enough members (for megagroups, this + requires `at least 500 members`_). + + Arguments + entity (`entity`): + The channel from which to get statistics. + + message (`int` | ``Message``, optional): + The message ID from which to get statistics, if your goal is + to obtain the statistics of a single message. + + Raises + If the given entity is not a channel (broadcast or megagroup), + a `TypeError` is raised. + + If there are not enough members (poorly named) errors such as + ``telethon.errors.ChatAdminRequiredError`` will appear. + + Returns + If both ``entity`` and ``message`` were provided, returns + :tl:`MessageStats`. Otherwise, either :tl:`BroadcastStats` or + :tl:`MegagroupStats`, depending on whether the input belonged to a + broadcast channel or megagroup. + + Example + .. code-block:: python + + # Some megagroup or channel username or ID to fetch + channel = -100123 + stats = await client.get_stats(channel) + print('Stats from', stats.period.min_date, 'to', stats.period.max_date, ':') + print(stats.stringify()) + + .. _`at least 500 members`: https://telegram.org/blog/profile-videos-people-nearby-and-more + """ + entity = await self.get_input_entity(entity) + if helpers._entity_type(entity) != helpers._EntityType.CHANNEL: + raise TypeError('You must pass a channel entity') + + message = utils.get_message_id(message) + if message is not None: + try: + req = functions.stats.GetMessageStatsRequest(entity, message) + return await self(req) + except errors.StatsMigrateError as e: + dc = e.dc + else: + # Don't bother fetching the Channel entity (costs a request), instead + # try to guess and if it fails we know it's the other one (best case + # no extra request, worst just one). + try: + req = functions.stats.GetBroadcastStatsRequest(entity) + return await self(req) + except errors.StatsMigrateError as e: + dc = e.dc + except errors.BroadcastRequiredError: + req = functions.stats.GetMegagroupStatsRequest(entity) + try: + return await self(req) + except errors.StatsMigrateError as e: + dc = e.dc + + sender = await self._borrow_exported_sender(dc) + try: + # req will be resolved to use the right types inside by now + return await sender.send(req) + finally: + await self._return_exported_sender(sender) + + # endregion diff --git a/module/scripts/lib/telethon/client/dialogs.py b/module/scripts/lib/telethon/client/dialogs.py new file mode 100644 index 0000000..b0db0a6 --- /dev/null +++ b/module/scripts/lib/telethon/client/dialogs.py @@ -0,0 +1,610 @@ +import asyncio +import inspect +import itertools +import typing + +from .. import helpers, utils, hints, errors +from ..requestiter import RequestIter +from ..tl import types, functions, custom + +_MAX_CHUNK_SIZE = 100 + +if typing.TYPE_CHECKING: + from .telegramclient import TelegramClient + + +def _dialog_message_key(peer, message_id): + """ + Get the key to get messages from a dialog. + + We cannot just use the message ID because channels share message IDs, + and the peer ID is required to distinguish between them. But it is not + necessary in small group chats and private chats. + """ + return (peer.channel_id if isinstance(peer, types.PeerChannel) else None), message_id + + +class _DialogsIter(RequestIter): + async def _init( + self, offset_date, offset_id, offset_peer, ignore_pinned, ignore_migrated, folder + ): + self.request = functions.messages.GetDialogsRequest( + offset_date=offset_date, + offset_id=offset_id, + offset_peer=offset_peer, + limit=1, + hash=0, + exclude_pinned=ignore_pinned, + folder_id=folder + ) + + if self.limit <= 0: + # Special case, get a single dialog and determine count + dialogs = await self.client(self.request) + self.total = getattr(dialogs, 'count', len(dialogs.dialogs)) + raise StopAsyncIteration + + self.seen = set() + self.offset_date = offset_date + self.ignore_migrated = ignore_migrated + + async def _load_next_chunk(self): + self.request.limit = min(self.left, _MAX_CHUNK_SIZE) + r = await self.client(self.request) + + self.total = getattr(r, 'count', len(r.dialogs)) + + entities = {utils.get_peer_id(x): x + for x in itertools.chain(r.users, r.chats) + if not isinstance(x, (types.UserEmpty, types.ChatEmpty))} + + self.client._mb_entity_cache.extend(r.users, r.chats) + + messages = {} + for m in r.messages: + m._finish_init(self.client, entities, None) + messages[_dialog_message_key(m.peer_id, m.id)] = m + + for d in r.dialogs: + # We check the offset date here because Telegram may ignore it + message = messages.get(_dialog_message_key(d.peer, d.top_message)) + if self.offset_date: + date = getattr(message, 'date', None) + if not date or date.timestamp() > self.offset_date.timestamp(): + continue + + peer_id = utils.get_peer_id(d.peer) + if peer_id not in self.seen: + self.seen.add(peer_id) + if peer_id not in entities: + # > In which case can a UserEmpty appear in the list of banned members? + # > In a very rare cases. This is possible but isn't an expected behavior. + # Real world example: https://t.me/TelethonChat/271471 + continue + + cd = custom.Dialog(self.client, d, entities, message) + if cd.dialog.pts: + self.client._message_box.try_set_channel_state( + utils.get_peer_id(d.peer, add_mark=False), cd.dialog.pts) + + if not self.ignore_migrated or getattr( + cd.entity, 'migrated_to', None) is None: + self.buffer.append(cd) + + if not self.buffer or len(r.dialogs) < self.request.limit\ + or not isinstance(r, types.messages.DialogsSlice): + # Buffer being empty means all returned dialogs were skipped (due to offsets). + # Less than we requested means we reached the end, or + # we didn't get a DialogsSlice which means we got all. + return True + + # We can't use `messages[-1]` as the offset ID / date. + # Why? Because pinned dialogs will mess with the order + # in this list. Instead, we find the last dialog which + # has a message, and use it as an offset. + last_message = next(filter(None, ( + messages.get(_dialog_message_key(d.peer, d.top_message)) + for d in reversed(r.dialogs) + )), None) + + self.request.exclude_pinned = True + self.request.offset_id = last_message.id if last_message else 0 + self.request.offset_date = last_message.date if last_message else None + self.request.offset_peer = self.buffer[-1].input_entity + + +class _DraftsIter(RequestIter): + async def _init(self, entities, **kwargs): + if not entities: + r = await self.client(functions.messages.GetAllDraftsRequest()) + items = r.updates + else: + peers = [] + for entity in entities: + peers.append(types.InputDialogPeer( + await self.client.get_input_entity(entity))) + + r = await self.client(functions.messages.GetPeerDialogsRequest(peers)) + items = r.dialogs + + # TODO Maybe there should be a helper method for this? + entities = {utils.get_peer_id(x): x + for x in itertools.chain(r.users, r.chats)} + + self.buffer.extend( + custom.Draft(self.client, entities[utils.get_peer_id(d.peer)], d.draft) + for d in items + ) + + async def _load_next_chunk(self): + return [] + + +class DialogMethods: + + # region Public methods + + def iter_dialogs( + self: 'TelegramClient', + limit: float = None, + *, + offset_date: 'hints.DateLike' = None, + offset_id: int = 0, + offset_peer: 'hints.EntityLike' = types.InputPeerEmpty(), + ignore_pinned: bool = False, + ignore_migrated: bool = False, + folder: int = None, + archived: bool = None + ) -> _DialogsIter: + """ + Iterator over the dialogs (open conversations/subscribed channels). + + The order is the same as the one seen in official applications + (first pinned, them from those with the most recent message to + those with the oldest message). + + Arguments + limit (`int` | `None`): + How many dialogs to be retrieved as maximum. Can be set to + `None` to retrieve all dialogs. Note that this may take + whole minutes if you have hundreds of dialogs, as Telegram + will tell the library to slow down through a + ``FloodWaitError``. + + offset_date (`datetime`, optional): + The offset date to be used. + + offset_id (`int`, optional): + The message ID to be used as an offset. + + offset_peer (:tl:`InputPeer`, optional): + The peer to be used as an offset. + + ignore_pinned (`bool`, optional): + Whether pinned dialogs should be ignored or not. + When set to `True`, these won't be yielded at all. + + ignore_migrated (`bool`, optional): + Whether :tl:`Chat` that have ``migrated_to`` a :tl:`Channel` + should be included or not. By default all the chats in your + dialogs are returned, but setting this to `True` will ignore + (i.e. skip) them in the same way official applications do. + + folder (`int`, optional): + The folder from which the dialogs should be retrieved. + + If left unspecified, all dialogs (including those from + folders) will be returned. + + If set to ``0``, all dialogs that don't belong to any + folder will be returned. + + If set to a folder number like ``1``, only those from + said folder will be returned. + + By default Telegram assigns the folder ID ``1`` to + archived chats, so you should use that if you need + to fetch the archived dialogs. + + archived (`bool`, optional): + Alias for `folder`. If unspecified, all will be returned, + `False` implies ``folder=0`` and `True` implies ``folder=1``. + Yields + Instances of `Dialog `. + + Example + .. code-block:: python + + # Print all dialog IDs and the title, nicely formatted + async for dialog in client.iter_dialogs(): + print('{:>14}: {}'.format(dialog.id, dialog.title)) + """ + if archived is not None: + folder = 1 if archived else 0 + + return _DialogsIter( + self, + limit, + offset_date=offset_date, + offset_id=offset_id, + offset_peer=offset_peer, + ignore_pinned=ignore_pinned, + ignore_migrated=ignore_migrated, + folder=folder + ) + + async def get_dialogs(self: 'TelegramClient', *args, **kwargs) -> 'hints.TotalList': + """ + Same as `iter_dialogs()`, but returns a + `TotalList ` instead. + + Example + .. code-block:: python + + # Get all open conversation, print the title of the first + dialogs = await client.get_dialogs() + first = dialogs[0] + print(first.title) + + # Use the dialog somewhere else + await client.send_message(first, 'hi') + + # Getting only non-archived dialogs (both equivalent) + non_archived = await client.get_dialogs(folder=0) + non_archived = await client.get_dialogs(archived=False) + + # Getting only archived dialogs (both equivalent) + archived = await client.get_dialogs(folder=1) + archived = await client.get_dialogs(archived=True) + """ + return await self.iter_dialogs(*args, **kwargs).collect() + + get_dialogs.__signature__ = inspect.signature(iter_dialogs) + + def iter_drafts( + self: 'TelegramClient', + entity: 'hints.EntitiesLike' = None + ) -> _DraftsIter: + """ + Iterator over draft messages. + + The order is unspecified. + + Arguments + entity (`hints.EntitiesLike`, optional): + The entity or entities for which to fetch the draft messages. + If left unspecified, all draft messages will be returned. + + Yields + Instances of `Draft `. + + Example + .. code-block:: python + + # Clear all drafts + async for draft in client.get_drafts(): + await draft.delete() + + # Getting the drafts with 'bot1' and 'bot2' + async for draft in client.iter_drafts(['bot1', 'bot2']): + print(draft.text) + """ + if entity and not utils.is_list_like(entity): + entity = (entity,) + + # TODO Passing a limit here makes no sense + return _DraftsIter(self, None, entities=entity) + + async def get_drafts( + self: 'TelegramClient', + entity: 'hints.EntitiesLike' = None + ) -> 'hints.TotalList': + """ + Same as `iter_drafts()`, but returns a list instead. + + Example + .. code-block:: python + + # Get drafts, print the text of the first + drafts = await client.get_drafts() + print(drafts[0].text) + + # Get the draft in your chat + draft = await client.get_drafts('me') + print(drafts.text) + """ + items = await self.iter_drafts(entity).collect() + if not entity or utils.is_list_like(entity): + return items + else: + return items[0] + + async def edit_folder( + self: 'TelegramClient', + entity: 'hints.EntitiesLike' = None, + folder: typing.Union[int, typing.Sequence[int]] = None, + *, + unpack=None + ) -> types.Updates: + """ + Edits the folder used by one or more dialogs to archive them. + + Arguments + entity (entities): + The entity or list of entities to move to the desired + archive folder. + + folder (`int`): + The folder to which the dialog should be archived to. + + If you want to "archive" a dialog, use ``folder=1``. + + If you want to "un-archive" it, use ``folder=0``. + + You may also pass a list with the same length as + `entities` if you want to control where each entity + will go. + + unpack (`int`, optional): + If you want to unpack an archived folder, set this + parameter to the folder number that you want to + delete. + + When you unpack a folder, all the dialogs inside are + moved to the folder number 0. + + You can only use this parameter if the other two + are not set. + + Returns + The :tl:`Updates` object that the request produces. + + Example + .. code-block:: python + + # Archiving the first 5 dialogs + dialogs = await client.get_dialogs(5) + await client.edit_folder(dialogs, 1) + + # Un-archiving the third dialog (archiving to folder 0) + await client.edit_folder(dialog[2], 0) + + # Moving the first dialog to folder 0 and the second to 1 + dialogs = await client.get_dialogs(2) + await client.edit_folder(dialogs, [0, 1]) + + # Un-archiving all dialogs + await client.edit_folder(unpack=1) + """ + if (entity is None) == (unpack is None): + raise ValueError('You can only set either entities or unpack, not both') + + if unpack is not None: + return await self(functions.folders.DeleteFolderRequest( + folder_id=unpack + )) + + if not utils.is_list_like(entity): + entities = [await self.get_input_entity(entity)] + else: + entities = await asyncio.gather( + *(self.get_input_entity(x) for x in entity)) + + if folder is None: + raise ValueError('You must specify a folder') + elif not utils.is_list_like(folder): + folder = [folder] * len(entities) + elif len(entities) != len(folder): + raise ValueError('Number of folders does not match number of entities') + + return await self(functions.folders.EditPeerFoldersRequest([ + types.InputFolderPeer(x, folder_id=y) + for x, y in zip(entities, folder) + ])) + + async def delete_dialog( + self: 'TelegramClient', + entity: 'hints.EntityLike', + *, + revoke: bool = False + ): + """ + Deletes a dialog (leaves a chat or channel). + + This method can be used as a user and as a bot. However, + bots will only be able to use it to leave groups and channels + (trying to delete a private conversation will do nothing). + + See also `Dialog.delete() `. + + Arguments + entity (entities): + The entity of the dialog to delete. If it's a chat or + channel, you will leave it. Note that the chat itself + is not deleted, only the dialog, because you left it. + + revoke (`bool`, optional): + On private chats, you may revoke the messages from + the other peer too. By default, it's `False`. Set + it to `True` to delete the history for both. + + This makes no difference for bot accounts, who can + only leave groups and channels. + + Returns + The :tl:`Updates` object that the request produces, + or nothing for private conversations. + + Example + .. code-block:: python + + # Deleting the first dialog + dialogs = await client.get_dialogs(5) + await client.delete_dialog(dialogs[0]) + + # Leaving a channel by username + await client.delete_dialog('username') + """ + # If we have enough information (`Dialog.delete` gives it to us), + # then we know we don't have to kick ourselves in deactivated chats. + if isinstance(entity, types.Chat): + deactivated = entity.deactivated + else: + deactivated = False + + entity = await self.get_input_entity(entity) + ty = helpers._entity_type(entity) + if ty == helpers._EntityType.CHANNEL: + return await self(functions.channels.LeaveChannelRequest(entity)) + + if ty == helpers._EntityType.CHAT and not deactivated: + try: + result = await self(functions.messages.DeleteChatUserRequest( + entity.chat_id, types.InputUserSelf(), revoke_history=revoke + )) + except errors.PeerIdInvalidError: + # Happens if we didn't have the deactivated information + result = None + else: + result = None + + if not await self.is_bot(): + await self(functions.messages.DeleteHistoryRequest(entity, 0, revoke=revoke)) + + return result + + def conversation( + self: 'TelegramClient', + entity: 'hints.EntityLike', + *, + timeout: float = 60, + total_timeout: float = None, + max_messages: int = 100, + exclusive: bool = True, + replies_are_responses: bool = True) -> custom.Conversation: + """ + Creates a `Conversation ` + with the given entity. + + .. note:: + + This Conversation API has certain shortcomings, such as lacking + persistence, poor interaction with other event handlers, and + overcomplicated usage for anything beyond the simplest case. + + If you plan to interact with a bot without handlers, this works + fine, but when running a bot yourself, you may instead prefer + to follow the advice from https://stackoverflow.com/a/62246569/. + + This is not the same as just sending a message to create a "dialog" + with them, but rather a way to easily send messages and await for + responses or other reactions. Refer to its documentation for more. + + Arguments + entity (`entity`): + The entity with which a new conversation should be opened. + + timeout (`int` | `float`, optional): + The default timeout (in seconds) *per action* to be used. You + may also override this timeout on a per-method basis. By + default each action can take up to 60 seconds (the value of + this timeout). + + total_timeout (`int` | `float`, optional): + The total timeout (in seconds) to use for the whole + conversation. This takes priority over per-action + timeouts. After these many seconds pass, subsequent + actions will result in ``asyncio.TimeoutError``. + + max_messages (`int`, optional): + The maximum amount of messages this conversation will + remember. After these many messages arrive in the + specified chat, subsequent actions will result in + ``ValueError``. + + exclusive (`bool`, optional): + By default, conversations are exclusive within a single + chat. That means that while a conversation is open in a + chat, you can't open another one in the same chat, unless + you disable this flag. + + If you try opening an exclusive conversation for + a chat where it's already open, it will raise + ``AlreadyInConversationError``. + + replies_are_responses (`bool`, optional): + Whether replies should be treated as responses or not. + + If the setting is enabled, calls to `conv.get_response + ` + and a subsequent call to `conv.get_reply + ` + will return different messages, otherwise they may return + the same message. + + Consider the following scenario with one outgoing message, + 1, and two incoming messages, the second one replying:: + + Hello! <1 + 2> (reply to 1) Hi! + 3> (reply to 1) How are you? + + And the following code: + + .. code-block:: python + + async with client.conversation(chat) as conv: + msg1 = await conv.send_message('Hello!') + msg2 = await conv.get_response() + msg3 = await conv.get_reply() + + With the setting enabled, ``msg2`` will be ``'Hi!'`` and + ``msg3`` be ``'How are you?'`` since replies are also + responses, and a response was already returned. + + With the setting disabled, both ``msg2`` and ``msg3`` will + be ``'Hi!'`` since one is a response and also a reply. + + Returns + A `Conversation `. + + Example + .. code-block:: python + + # denotes outgoing messages you sent + # denotes incoming response messages + with bot.conversation(chat) as conv: + # Hi! + conv.send_message('Hi!') + + # Hello! + hello = conv.get_response() + + # Please tell me your name + conv.send_message('Please tell me your name') + + # ? + name = conv.get_response().raw_text + + while not any(x.isalpha() for x in name): + # Your name didn't have any letters! Try again + conv.send_message("Your name didn't have any letters! Try again") + + # Human + name = conv.get_response().raw_text + + # Thanks Human! + conv.send_message('Thanks {}!'.format(name)) + """ + return custom.Conversation( + self, + entity, + timeout=timeout, + total_timeout=total_timeout, + max_messages=max_messages, + exclusive=exclusive, + replies_are_responses=replies_are_responses + + ) + + # endregion diff --git a/module/scripts/lib/telethon/client/downloads.py b/module/scripts/lib/telethon/client/downloads.py new file mode 100644 index 0000000..4601709 --- /dev/null +++ b/module/scripts/lib/telethon/client/downloads.py @@ -0,0 +1,1092 @@ +import datetime +import io +import os +import pathlib +import typing +import inspect +import asyncio + +from ..crypto import AES + +from .. import utils, helpers, errors, hints +from ..requestiter import RequestIter +from ..tl import TLObject, types, functions + +try: + import aiohttp +except ImportError: + aiohttp = None + +if typing.TYPE_CHECKING: + from .telegramclient import TelegramClient + +# Chunk sizes for upload.getFile must be multiples of the smallest size +MIN_CHUNK_SIZE = 4096 +MAX_CHUNK_SIZE = 512 * 1024 + +# 2021-01-15, users reported that `errors.TimeoutError` can occur while downloading files. +TIMED_OUT_SLEEP = 1 + + +class _CdnRedirect(Exception): + def __init__(self, cdn_redirect=None): + self.cdn_redirect = cdn_redirect + + +class _DirectDownloadIter(RequestIter): + async def _init( + self, file, dc_id, offset, stride, chunk_size, request_size, file_size, msg_data, cdn_redirect=None): + self.request = functions.upload.GetFileRequest( + file, offset=offset, limit=request_size) + self._client = self.client + self._cdn_redirect = cdn_redirect + if cdn_redirect is not None: + self.request = functions.upload.GetCdnFileRequest(cdn_redirect.file_token, offset=offset, limit=request_size) + self._client = await self.client._get_cdn_client(cdn_redirect) + + self.total = file_size + self._stride = stride + self._chunk_size = chunk_size + self._last_part = None + self._msg_data = msg_data + self._timed_out = False + + self._exported = dc_id and self._client.session.dc_id != dc_id + if not self._exported: + # The used sender will also change if ``FileMigrateError`` occurs + self._sender = self.client._sender + else: + try: + self._sender = await self.client._borrow_exported_sender(dc_id) + except errors.DcIdInvalidError: + # Can't export a sender for the ID we are currently in + config = await self.client(functions.help.GetConfigRequest()) + for option in config.dc_options: + if option.ip_address == self.client.session.server_address: + await utils.maybe_async( + self.client.session.set_dc( + option.id, option.ip_address, option.port + ) + ) + await utils.maybe_async(self.client.session.save()) + break + + # TODO Figure out why the session may have the wrong DC ID + self._sender = self.client._sender + self._exported = False + + async def _load_next_chunk(self): + cur = await self._request() + self.buffer.append(cur) + if len(cur) < self.request.limit: + self.left = len(self.buffer) + await self.close() + else: + self.request.offset += self._stride + + async def _request(self): + try: + result = await self._client._call(self._sender, self.request) + self._timed_out = False + if isinstance(result, types.upload.FileCdnRedirect): + if self.client._mb_entity_cache.self_bot: + raise ValueError('FileCdnRedirect but the GetCdnFileRequest API access for bot users is restricted. Try to change api_id to avoid FileCdnRedirect') + raise _CdnRedirect(result) + if isinstance(result, types.upload.CdnFileReuploadNeeded): + await self.client._call(self.client._sender, functions.upload.ReuploadCdnFileRequest(file_token=self._cdn_redirect.file_token, request_token=result.request_token)) + result = await self._client._call(self._sender, self.request) + return result.bytes + else: + return result.bytes + + except errors.TimedOutError as e: + if self._timed_out: + self.client._log[__name__].warning('Got two timeouts in a row while downloading file') + raise + + self._timed_out = True + self.client._log[__name__].info('Got timeout while downloading file, retrying once') + await asyncio.sleep(TIMED_OUT_SLEEP) + return await self._request() + + except errors.FileMigrateError as e: + self.client._log[__name__].info('File lives in another DC') + self._sender = await self.client._borrow_exported_sender(e.new_dc) + self._exported = True + return await self._request() + + except (errors.FilerefUpgradeNeededError, errors.FileReferenceExpiredError) as e: + # Only implemented for documents which are the ones that may take that long to download + if not self._msg_data \ + or not isinstance(self.request.location, types.InputDocumentFileLocation) \ + or self.request.location.thumb_size != '': + raise + + self.client._log[__name__].info('File ref expired during download; refetching message') + chat, msg_id = self._msg_data + msg = await self.client.get_messages(chat, ids=msg_id) + + if not isinstance(msg.media, types.MessageMediaDocument): + raise + + document = msg.media.document + + # Message media may have been edited for something else + if document.id != self.request.location.id: + raise + + self.request.location.file_reference = document.file_reference + return await self._request() + + async def close(self): + if not self._sender: + return + + try: + if self._exported: + await self.client._return_exported_sender(self._sender) + elif self._sender != self.client._sender: + await self._sender.disconnect() + finally: + self._sender = None + + async def __aenter__(self): + return self + + async def __aexit__(self, *args): + await self.close() + + __enter__ = helpers._sync_enter + __exit__ = helpers._sync_exit + + +class _GenericDownloadIter(_DirectDownloadIter): + async def _load_next_chunk(self): + # 1. Fetch enough for one chunk + data = b'' + + # 1.1. ``bad`` is how much into the data we have we need to offset + bad = self.request.offset % self.request.limit + before = self.request.offset + + # 1.2. We have to fetch from a valid offset, so remove that bad part + self.request.offset -= bad + + done = False + while not done and len(data) - bad < self._chunk_size: + cur = await self._request() + self.request.offset += self.request.limit + + data += cur + done = len(cur) < self.request.limit + + # 1.3 Restore our last desired offset + self.request.offset = before + + # 2. Fill the buffer with the data we have + # 2.1. Slicing `bytes` is expensive, yield `memoryview` instead + mem = memoryview(data) + + # 2.2. The current chunk starts at ``bad`` offset into the data, + # and each new chunk is ``stride`` bytes apart of the other + for i in range(bad, len(data), self._stride): + self.buffer.append(mem[i:i + self._chunk_size]) + + # 2.3. We will yield this offset, so move to the next one + self.request.offset += self._stride + + # 2.4. If we are in the last chunk, we will return the last partial data + if done: + self.left = len(self.buffer) + await self.close() + return + + # 2.5. If we are not done, we can't return incomplete chunks. + if len(self.buffer[-1]) != self._chunk_size: + self._last_part = self.buffer.pop().tobytes() + + # 3. Be careful with the offsets. Re-fetching a bit of data + # is fine, since it greatly simplifies things. + # TODO Try to not re-fetch data + self.request.offset -= self._stride + + +class DownloadMethods: + + # region Public methods + + async def download_profile_photo( + self: 'TelegramClient', + entity: 'hints.EntityLike', + file: 'hints.FileLike' = None, + *, + download_big: bool = True) -> typing.Optional[str]: + """ + Downloads the profile photo from the given user, chat or channel. + + Arguments + entity (`entity`): + From who the photo will be downloaded. + + .. note:: + + This method expects the full entity (which has the data + to download the photo), not an input variant. + + It's possible that sometimes you can't fetch the entity + from its input (since you can get errors like + ``ChannelPrivateError``) but you already have it through + another call, like getting a forwarded message from it. + + file (`str` | `file`, optional): + The output file path, directory, or stream-like object. + If the path exists and is a file, it will be overwritten. + If file is the type `bytes`, it will be downloaded in-memory + and returned as a bytestring (i.e. ``file=bytes``, without + parentheses or quotes). + + download_big (`bool`, optional): + Whether to use the big version of the available photos. + + Returns + `None` if no photo was provided, or if it was Empty. On success + the file path is returned since it may differ from the one given. + + Example + .. code-block:: python + + # Download your own profile photo + path = await client.download_profile_photo('me') + print(path) + """ + # hex(crc32(x.encode('ascii'))) for x in + # ('User', 'Chat', 'UserFull', 'ChatFull') + ENTITIES = (0x2da17977, 0xc5af5d94, 0x1f4661b9, 0xd49a2697) + # ('InputPeer', 'InputUser', 'InputChannel') + INPUTS = (0xc91c90b6, 0xe669bf46, 0x40f202fd) + if not isinstance(entity, TLObject) or entity.SUBCLASS_OF_ID in INPUTS: + entity = await self.get_entity(entity) + + thumb = -1 if download_big else 0 + + possible_names = [] + if entity.SUBCLASS_OF_ID not in ENTITIES: + photo = entity + else: + if not hasattr(entity, 'photo'): + # Special case: may be a ChatFull with photo:Photo + # This is different from a normal UserProfilePhoto and Chat + if not hasattr(entity, 'chat_photo'): + return None + + return await self._download_photo( + entity.chat_photo, file, date=None, + thumb=thumb, progress_callback=None + ) + + for attr in ('username', 'first_name', 'title'): + possible_names.append(getattr(entity, attr, None)) + + photo = entity.photo + + if isinstance(photo, (types.UserProfilePhoto, types.ChatPhoto)): + dc_id = photo.dc_id + loc = types.InputPeerPhotoFileLocation( + # min users can be used to download profile photos + # self.get_input_entity would otherwise not accept those + peer=utils.get_input_peer(entity, check_hash=False), + photo_id=photo.photo_id, + big=download_big + ) + else: + # It doesn't make any sense to check if `photo` can be used + # as input location, because then this method would be able + # to "download the profile photo of a message", i.e. its + # media which should be done with `download_media` instead. + return None + + file = self._get_proper_filename( + file, 'profile_photo', '.jpg', + possible_names=possible_names + ) + + try: + result = await self.download_file(loc, file, dc_id=dc_id) + return result if file is bytes else file + except errors.LocationInvalidError: + # See issue #500, Android app fails as of v4.6.0 (1155). + # The fix seems to be using the full channel chat photo. + ie = await self.get_input_entity(entity) + ty = helpers._entity_type(ie) + if ty == helpers._EntityType.CHANNEL: + full = await self(functions.channels.GetFullChannelRequest(ie)) + return await self._download_photo( + full.full_chat.chat_photo, file, + date=None, progress_callback=None, + thumb=thumb + ) + else: + # Until there's a report for chats, no need to. + return None + + async def download_media( + self: 'TelegramClient', + message: 'hints.MessageLike', + file: 'hints.FileLike' = None, + *, + thumb: 'typing.Union[int, types.TypePhotoSize]' = None, + progress_callback: 'hints.ProgressCallback' = None) -> typing.Optional[typing.Union[str, bytes]]: + """ + Downloads the given media from a message object. + + Note that if the download is too slow, you should consider installing + ``cryptg`` (through ``pip install cryptg``) so that decrypting the + received data is done in C instead of Python (much faster). + + See also `Message.download_media() `. + + Arguments + message (`Message ` | :tl:`Media`): + The media or message containing the media that will be downloaded. + + file (`str` | `file`, optional): + The output file path, directory, or stream-like object. + If the path exists and is a file, it will be overwritten. + If file is the type `bytes`, it will be downloaded in-memory + and returned as a bytestring (i.e. ``file=bytes``, without + parentheses or quotes). + + progress_callback (`callable`, optional): + A callback function accepting two parameters: + ``(received bytes, total)``. + + thumb (`int` | :tl:`PhotoSize`, optional): + Which thumbnail size from the document or photo to download, + instead of downloading the document or photo itself. + + If it's specified but the file does not have a thumbnail, + this method will return `None`. + + The parameter should be an integer index between ``0`` and + ``len(sizes)``. ``0`` will download the smallest thumbnail, + and ``len(sizes) - 1`` will download the largest thumbnail. + You can also use negative indices, which work the same as + they do in Python's `list`. + + You can also pass the :tl:`PhotoSize` instance to use. + Alternatively, the thumb size type `str` may be used. + + In short, use ``thumb=0`` if you want the smallest thumbnail + and ``thumb=-1`` if you want the largest thumbnail. + + .. note:: + The largest thumbnail may be a video instead of a photo, + as they are available since layer 116 and are bigger than + any of the photos. + + Returns + `None` if no media was provided, or if it was Empty. On success + the file path is returned since it may differ from the one given. + + Example + .. code-block:: python + + path = await client.download_media(message) + await client.download_media(message, filename) + # or + path = await message.download_media() + await message.download_media(filename) + + # Downloading to memory + blob = await client.download_media(message, bytes) + + # Printing download progress + def callback(current, total): + print('Downloaded', current, 'out of', total, + 'bytes: {:.2%}'.format(current / total)) + + await client.download_media(message, progress_callback=callback) + """ + # Downloading large documents may be slow enough to require a new file reference + # to be obtained mid-download. Store (input chat, message id) so that the message + # can be re-fetched. + msg_data = None + + # TODO This won't work for messageService + if isinstance(message, types.Message): + date = message.date + media = message.media + msg_data = (message.input_chat, message.id) if message.input_chat else None + else: + date = datetime.datetime.now() + media = message + + if isinstance(media, str): + media = utils.resolve_bot_file_id(media) + + if isinstance(media, types.MessageService): + if isinstance(message.action, + types.MessageActionChatEditPhoto): + media = media.photo + + if isinstance(media, types.MessageMediaWebPage): + if isinstance(media.webpage, types.WebPage): + media = media.webpage.document or media.webpage.photo + + if isinstance(media, (types.MessageMediaPhoto, types.Photo)): + return await self._download_photo( + media, file, date, thumb, progress_callback + ) + elif isinstance(media, (types.MessageMediaDocument, types.Document)): + return await self._download_document( + media, file, date, thumb, progress_callback, msg_data + ) + elif isinstance(media, types.MessageMediaContact) and thumb is None: + return self._download_contact( + media, file + ) + elif isinstance(media, (types.WebDocument, types.WebDocumentNoProxy)) and thumb is None: + return await self._download_web_document( + media, file, progress_callback + ) + + async def download_file( + self: 'TelegramClient', + input_location: 'hints.FileLike', + file: 'hints.OutFileLike' = None, + *, + part_size_kb: float = None, + file_size: int = None, + progress_callback: 'hints.ProgressCallback' = None, + dc_id: int = None, + key: bytes = None, + iv: bytes = None) -> typing.Optional[bytes]: + """ + Low-level method to download files from their input location. + + .. note:: + + Generally, you should instead use `download_media`. + This method is intended to be a bit more low-level. + + Arguments + input_location (:tl:`InputFileLocation`): + The file location from which the file will be downloaded. + See `telethon.utils.get_input_location` source for a complete + list of supported types. + + file (`str` | `file`, optional): + The output file path, directory, or stream-like object. + If the path exists and is a file, it will be overwritten. + + If the file path is `None` or `bytes`, then the result + will be saved in memory and returned as `bytes`. + + part_size_kb (`int`, optional): + Chunk size when downloading files. The larger, the less + requests will be made (up to 512KB maximum). + + file_size (`int`, optional): + The file size that is about to be downloaded, if known. + Only used if ``progress_callback`` is specified. + + progress_callback (`callable`, optional): + A callback function accepting two parameters: + ``(downloaded bytes, total)``. Note that the + ``total`` is the provided ``file_size``. + + dc_id (`int`, optional): + The data center the library should connect to in order + to download the file. You shouldn't worry about this. + + key ('bytes', optional): + In case of an encrypted upload (secret chats) a key is supplied + + iv ('bytes', optional): + In case of an encrypted upload (secret chats) an iv is supplied + + + Example + .. code-block:: python + + # Download a file and print its header + data = await client.download_file(input_file, bytes) + print(data[:16]) + """ + return await self._download_file( + input_location, + file, + part_size_kb=part_size_kb, + file_size=file_size, + progress_callback=progress_callback, + dc_id=dc_id, + key=key, + iv=iv, + ) + + async def _download_file( + self: 'TelegramClient', + input_location: 'hints.FileLike', + file: 'hints.OutFileLike' = None, + *, + part_size_kb: float = None, + file_size: int = None, + progress_callback: 'hints.ProgressCallback' = None, + dc_id: int = None, + key: bytes = None, + iv: bytes = None, + msg_data: tuple = None, + cdn_redirect: types.upload.FileCdnRedirect = None + ) -> typing.Optional[bytes]: + if not part_size_kb: + if not file_size: + part_size_kb = 64 # Reasonable default + else: + part_size_kb = utils.get_appropriated_part_size(file_size) + + part_size = int(part_size_kb * 1024) + if part_size % MIN_CHUNK_SIZE != 0: + raise ValueError( + 'The part size must be evenly divisible by 4096.') + + if isinstance(file, pathlib.Path): + file = str(file.absolute()) + + in_memory = file is None or file is bytes + if in_memory: + f = io.BytesIO() + elif isinstance(file, str): + # Ensure that we'll be able to download the media + helpers.ensure_parent_dir_exists(file) + f = open(file, 'wb') + else: + f = file + + try: + async for chunk in self._iter_download( + input_location, request_size=part_size, dc_id=dc_id, msg_data=msg_data, cdn_redirect=cdn_redirect): + if iv and key: + chunk = AES.decrypt_ige(chunk, key, iv) + r = f.write(chunk) + if inspect.isawaitable(r): + await r + + if progress_callback: + r = progress_callback(f.tell(), file_size) + if inspect.isawaitable(r): + await r + + # Not all IO objects have flush (see #1227) + if callable(getattr(f, 'flush', None)): + f.flush() + + if in_memory: + return f.getvalue() + except _CdnRedirect as e: + self._log[__name__].info('FileCdnRedirect to CDN data center %s', e.cdn_redirect.dc_id) + return await self._download_file( + input_location=input_location, + file=file, + part_size_kb=part_size_kb, + file_size=file_size, + progress_callback=progress_callback, + dc_id=e.cdn_redirect.dc_id, + key=e.cdn_redirect.encryption_key, + iv=e.cdn_redirect.encryption_iv, + msg_data=msg_data, + cdn_redirect=e.cdn_redirect + ) + finally: + if isinstance(file, str) or in_memory: + f.close() + + def iter_download( + self: 'TelegramClient', + file: 'hints.FileLike', + *, + offset: int = 0, + stride: int = None, + limit: int = None, + chunk_size: int = None, + request_size: int = MAX_CHUNK_SIZE, + file_size: int = None, + dc_id: int = None + ): + """ + Iterates over a file download, yielding chunks of the file. + + This method can be used to stream files in a more convenient + way, since it offers more control (pausing, resuming, etc.) + + .. note:: + + Using a value for `offset` or `stride` which is not a multiple + of the minimum allowed `request_size`, or if `chunk_size` is + different from `request_size`, the library will need to do a + bit more work to fetch the data in the way you intend it to. + + You normally shouldn't worry about this. + + Arguments + file (`hints.FileLike`): + The file of which contents you want to iterate over. + + offset (`int`, optional): + The offset in bytes into the file from where the + download should start. For example, if a file is + 1024KB long and you just want the last 512KB, you + would use ``offset=512 * 1024``. + + stride (`int`, optional): + The stride of each chunk (how much the offset should + advance between reading each chunk). This parameter + should only be used for more advanced use cases. + + It must be bigger than or equal to the `chunk_size`. + + limit (`int`, optional): + The limit for how many *chunks* will be yielded at most. + + chunk_size (`int`, optional): + The maximum size of the chunks that will be yielded. + Note that the last chunk may be less than this value. + By default, it equals to `request_size`. + + request_size (`int`, optional): + How many bytes will be requested to Telegram when more + data is required. By default, as many bytes as possible + are requested. If you would like to request data in + smaller sizes, adjust this parameter. + + Note that values outside the valid range will be clamped, + and the final value will also be a multiple of the minimum + allowed size. + + file_size (`int`, optional): + If the file size is known beforehand, you should set + this parameter to said value. Depending on the type of + the input file passed, this may be set automatically. + + dc_id (`int`, optional): + The data center the library should connect to in order + to download the file. You shouldn't worry about this. + + Yields + + `bytes` objects representing the chunks of the file if the + right conditions are met, or `memoryview` objects instead. + + Example + .. code-block:: python + + # Streaming `media` to an output file + # After the iteration ends, the sender is cleaned up + with open('photo.jpg', 'wb') as fd: + async for chunk in client.iter_download(media): + fd.write(chunk) + + # Fetching only the header of a file (32 bytes) + # You should manually close the iterator in this case. + # + # "stream" is a common name for asynchronous generators, + # and iter_download will yield `bytes` (chunks of the file). + stream = client.iter_download(media, request_size=32) + header = await stream.__anext__() # "manual" version of `async for` + await stream.close() + assert len(header) == 32 + """ + return self._iter_download( + file, + offset=offset, + stride=stride, + limit=limit, + chunk_size=chunk_size, + request_size=request_size, + file_size=file_size, + dc_id=dc_id, + ) + + def _iter_download( + self: 'TelegramClient', + file: 'hints.FileLike', + *, + offset: int = 0, + stride: int = None, + limit: int = None, + chunk_size: int = None, + request_size: int = MAX_CHUNK_SIZE, + file_size: int = None, + dc_id: int = None, + msg_data: tuple = None, + cdn_redirect: types.upload.FileCdnRedirect = None + ): + info = utils._get_file_info(file) + if info.dc_id is not None: + dc_id = info.dc_id + + if file_size is None: + file_size = info.size + + file = info.location + + if chunk_size is None: + chunk_size = request_size + + if limit is None and file_size is not None: + limit = (file_size + chunk_size - 1) // chunk_size + + if stride is None: + stride = chunk_size + elif stride < chunk_size: + raise ValueError('stride must be >= chunk_size') + + request_size -= request_size % MIN_CHUNK_SIZE + if request_size < MIN_CHUNK_SIZE: + request_size = MIN_CHUNK_SIZE + elif request_size > MAX_CHUNK_SIZE: + request_size = MAX_CHUNK_SIZE + + if chunk_size == request_size \ + and offset % MIN_CHUNK_SIZE == 0 \ + and stride % MIN_CHUNK_SIZE == 0 \ + and (limit is None or offset % limit == 0): + cls = _DirectDownloadIter + self._log[__name__].info('Starting direct file download in chunks of ' + '%d at %d, stride %d', request_size, offset, stride) + else: + cls = _GenericDownloadIter + self._log[__name__].info('Starting indirect file download in chunks of ' + '%d at %d, stride %d', request_size, offset, stride) + + return cls( + self, + limit, + file=file, + dc_id=dc_id, + offset=offset, + stride=stride, + chunk_size=chunk_size, + request_size=request_size, + file_size=file_size, + msg_data=msg_data, + cdn_redirect=cdn_redirect + ) + + # endregion + + # region Private methods + + @staticmethod + def _get_thumb(thumbs, thumb): + if not thumbs: + return None + + # Seems Telegram has changed the order and put `PhotoStrippedSize` + # last while this is the smallest (layer 116). Ensure we have the + # sizes sorted correctly with a custom function. + def sort_thumbs(thumb): + if isinstance(thumb, types.PhotoStrippedSize): + return 1, len(thumb.bytes) + if isinstance(thumb, types.PhotoCachedSize): + return 1, len(thumb.bytes) + if isinstance(thumb, types.PhotoSize): + return 1, thumb.size + if isinstance(thumb, types.PhotoSizeProgressive): + return 1, max(thumb.sizes) + if isinstance(thumb, types.VideoSize): + return 2, thumb.size + + # Empty size or invalid should go last + return 0, 0 + + thumbs = list(sorted(thumbs, key=sort_thumbs)) + + for i in reversed(range(len(thumbs))): + # :tl:`PhotoPathSize` is used for animated stickers preview, and the thumb is actually + # a SVG path of the outline. Users expect thumbnails to be JPEG files, so pretend this + # thumb size doesn't actually exist (#1655). + if isinstance(thumbs[i], types.PhotoPathSize): + thumbs.pop(i) + + if thumb is None: + return thumbs[-1] + elif isinstance(thumb, int): + return thumbs[thumb] + elif isinstance(thumb, str): + return next((t for t in thumbs if getattr(t, 'type', None) == thumb), None) + elif isinstance(thumb, (types.PhotoSize, types.PhotoCachedSize, + types.PhotoStrippedSize, types.VideoSize)): + return thumb + else: + return None + + def _download_cached_photo_size(self: 'TelegramClient', size, file): + # No need to download anything, simply write the bytes + if isinstance(size, types.PhotoStrippedSize): + data = utils.stripped_photo_to_jpg(size.bytes) + else: + data = size.bytes + + if file is bytes: + return data + elif isinstance(file, str): + helpers.ensure_parent_dir_exists(file) + f = open(file, 'wb') + else: + f = file + + try: + f.write(data) + finally: + if isinstance(file, str): + f.close() + return file + + async def _download_photo(self: 'TelegramClient', photo, file, date, thumb, progress_callback): + """Specialized version of .download_media() for photos""" + # Determine the photo and its largest size + if isinstance(photo, types.MessageMediaPhoto): + photo = photo.photo + if not isinstance(photo, types.Photo): + return + + # Include video sizes here (but they may be None so provide an empty list) + size = self._get_thumb(photo.sizes + (photo.video_sizes or []), thumb) + if not size or isinstance(size, types.PhotoSizeEmpty): + return + + if isinstance(size, types.VideoSize): + file = self._get_proper_filename(file, 'video', '.mp4', date=date) + else: + file = self._get_proper_filename(file, 'photo', '.jpg', date=date) + + if isinstance(size, (types.PhotoCachedSize, types.PhotoStrippedSize)): + return self._download_cached_photo_size(size, file) + + if isinstance(size, types.PhotoSizeProgressive): + file_size = max(size.sizes) + else: + file_size = size.size + + result = await self.download_file( + types.InputPhotoFileLocation( + id=photo.id, + access_hash=photo.access_hash, + file_reference=photo.file_reference, + thumb_size=size.type + ), + file, + file_size=file_size, + progress_callback=progress_callback + ) + return result if file is bytes else file + + @staticmethod + def _get_kind_and_names(attributes): + """Gets kind and possible names for :tl:`DocumentAttribute`.""" + kind = 'document' + possible_names = [] + for attr in attributes: + if isinstance(attr, types.DocumentAttributeFilename): + possible_names.insert(0, attr.file_name) + + elif isinstance(attr, types.DocumentAttributeAudio): + kind = 'audio' + if attr.performer and attr.title: + possible_names.append('{} - {}'.format( + attr.performer, attr.title + )) + elif attr.performer: + possible_names.append(attr.performer) + elif attr.title: + possible_names.append(attr.title) + elif attr.voice: + kind = 'voice' + + return kind, possible_names + + async def _download_document( + self, document, file, date, thumb, progress_callback, msg_data): + """Specialized version of .download_media() for documents.""" + if isinstance(document, types.MessageMediaDocument): + document = document.document + if not isinstance(document, types.Document): + return + + if thumb is None: + kind, possible_names = self._get_kind_and_names(document.attributes) + file = self._get_proper_filename( + file, kind, utils.get_extension(document), + date=date, possible_names=possible_names + ) + size = None + else: + file = self._get_proper_filename(file, 'photo', '.jpg', date=date) + size = self._get_thumb(document.thumbs, thumb) + if not size or isinstance(size, types.PhotoSizeEmpty): + return + + if isinstance(size, (types.PhotoCachedSize, types.PhotoStrippedSize)): + return self._download_cached_photo_size(size, file) + + result = await self._download_file( + types.InputDocumentFileLocation( + id=document.id, + access_hash=document.access_hash, + file_reference=document.file_reference, + thumb_size=size.type if size else '' + ), + file, + file_size=size.size if size else document.size, + progress_callback=progress_callback, + msg_data=msg_data, + ) + + return result if file is bytes else file + + @classmethod + def _download_contact(cls, mm_contact, file): + """ + Specialized version of .download_media() for contacts. + Will make use of the vCard 4.0 format. + """ + first_name = mm_contact.first_name + last_name = mm_contact.last_name + phone_number = mm_contact.phone_number + + # Remove these pesky characters + first_name = first_name.replace(';', '') + last_name = (last_name or '').replace(';', '') + result = ( + 'BEGIN:VCARD\n' + 'VERSION:4.0\n' + 'N:{f};{l};;;\n' + 'FN:{f} {l}\n' + 'TEL;TYPE=cell;VALUE=uri:tel:+{p}\n' + 'END:VCARD\n' + ).format(f=first_name, l=last_name, p=phone_number).encode('utf-8') + + file = cls._get_proper_filename( + file, 'contact', '.vcard', + possible_names=[first_name, phone_number, last_name] + ) + if file is bytes: + return result + f = file if hasattr(file, 'write') else open(file, 'wb') + + try: + f.write(result) + finally: + # Only close the stream if we opened it + if f != file: + f.close() + + return file + + @classmethod + async def _download_web_document(cls, web, file, progress_callback): + """ + Specialized version of .download_media() for web documents. + """ + if not aiohttp: + raise ValueError( + 'Cannot download web documents without the aiohttp ' + 'dependency install it (pip install aiohttp)' + ) + + # TODO Better way to get opened handles of files and auto-close + kind, possible_names = cls._get_kind_and_names(web.attributes) + file = cls._get_proper_filename( + file, kind, utils.get_extension(web), + possible_names=possible_names + ) + if file is bytes: + f = io.BytesIO() + elif hasattr(file, 'write'): + f = file + else: + f = open(file, 'wb') + + try: + async with aiohttp.ClientSession() as session: + # TODO Use progress_callback; get content length from response + # https://github.com/telegramdesktop/tdesktop/blob/c7e773dd9aeba94e2be48c032edc9a78bb50234e/Telegram/SourceFiles/ui/images.cpp#L1318-L1319 + async with session.get(web.url) as response: + while True: + chunk = await response.content.read(128 * 1024) + if not chunk: + break + f.write(chunk) + finally: + if f != file: + f.close() + + return f.getvalue() if file is bytes else file + + @staticmethod + def _get_proper_filename(file, kind, extension, + date=None, possible_names=None): + """Gets a proper filename for 'file', if this is a path. + + 'kind' should be the kind of the output file (photo, document...) + 'extension' should be the extension to be added to the file if + the filename doesn't have any yet + 'date' should be when this file was originally sent, if known + 'possible_names' should be an ordered list of possible names + + If no modification is made to the path, any existing file + will be overwritten. + If any modification is made to the path, this method will + ensure that no existing file will be overwritten. + """ + if isinstance(file, pathlib.Path): + file = str(file.absolute()) + + if file is not None and not isinstance(file, str): + # Probably a stream-like object, we cannot set a filename here + return file + + if file is None: + file = '' + elif os.path.isfile(file): + # Make no modifications to valid existing paths + return file + + if os.path.isdir(file) or not file: + try: + isreserved = getattr(os.path, 'isreserved', lambda _: False) # Python 3.13 and above + name = None if possible_names is None else next( + x # basename to prevent path traversal (#4713) + for x in map(os.path.basename, filter(None, possible_names)) + if not isreserved(x) + ) + except StopIteration: + name = None + + if not name: + if not date: + date = datetime.datetime.now() + name = '{}_{}-{:02}-{:02}_{:02}-{:02}-{:02}'.format( + kind, + date.year, date.month, date.day, + date.hour, date.minute, date.second, + ) + file = os.path.join(file, name) + + directory, name = os.path.split(file) + name, ext = os.path.splitext(name) + if not ext: + ext = extension + + result = os.path.join(directory, name + ext) + if not os.path.isfile(result): + return result + + i = 1 + while True: + result = os.path.join(directory, '{} ({}){}'.format(name, i, ext)) + if not os.path.isfile(result): + return result + i += 1 + + # endregion diff --git a/module/scripts/lib/telethon/client/messageparse.py b/module/scripts/lib/telethon/client/messageparse.py new file mode 100644 index 0000000..4ddfa52 --- /dev/null +++ b/module/scripts/lib/telethon/client/messageparse.py @@ -0,0 +1,233 @@ +import itertools +import re +import typing + +from .. import helpers, utils +from ..tl import types + +if typing.TYPE_CHECKING: + from .telegramclient import TelegramClient + + +class MessageParseMethods: + + # region Public properties + + @property + def parse_mode(self: 'TelegramClient'): + """ + This property is the default parse mode used when sending messages. + Defaults to `telethon.extensions.markdown`. It will always + be either `None` or an object with ``parse`` and ``unparse`` + methods. + + When setting a different value it should be one of: + + * Object with ``parse`` and ``unparse`` methods. + * A ``callable`` to act as the parse method. + * A `str` indicating the ``parse_mode``. For Markdown ``'md'`` + or ``'markdown'`` may be used. For HTML, ``'htm'`` or ``'html'`` + may be used. + + The ``parse`` method should be a function accepting a single + parameter, the text to parse, and returning a tuple consisting + of ``(parsed message str, [MessageEntity instances])``. + + The ``unparse`` method should be the inverse of ``parse`` such + that ``assert text == unparse(*parse(text))``. + + See :tl:`MessageEntity` for allowed message entities. + + Example + .. code-block:: python + + # Disabling default formatting + client.parse_mode = None + + # Enabling HTML as the default format + client.parse_mode = 'html' + """ + return self._parse_mode + + @parse_mode.setter + def parse_mode(self: 'TelegramClient', mode: str): + self._parse_mode = utils.sanitize_parse_mode(mode) + + # endregion + + # region Private methods + + async def _replace_with_mention(self: 'TelegramClient', entities, i, user): + """ + Helper method to replace ``entities[i]`` to mention ``user``, + or do nothing if it can't be found. + """ + try: + entities[i] = types.InputMessageEntityMentionName( + entities[i].offset, entities[i].length, + await self.get_input_entity(user) + ) + return True + except (ValueError, TypeError): + return False + + async def _parse_message_text(self: 'TelegramClient', message, parse_mode): + """ + Returns a (parsed message, entities) tuple depending on ``parse_mode``. + """ + if parse_mode == (): + parse_mode = self._parse_mode + else: + parse_mode = utils.sanitize_parse_mode(parse_mode) + + if not parse_mode: + return message, [] + + original_message = message + message, msg_entities = parse_mode.parse(message) + if original_message and not message and not msg_entities: + raise ValueError("Failed to parse message") + + for i in reversed(range(len(msg_entities))): + e = msg_entities[i] + if not e.length: + # 0-length MessageEntity is no longer valid #3884. + # Because the user can provide their own parser (with reasonable 0-length + # entities), strip them here rather than fixing the built-in parsers. + del msg_entities[i] + elif isinstance(e, types.MessageEntityTextUrl): + m = re.match(r'^@|\+|tg://user\?id=(\d+)', e.url) + if m: + user = int(m.group(1)) if m.group(1) else e.url + is_mention = await self._replace_with_mention(msg_entities, i, user) + if not is_mention: + del msg_entities[i] + elif isinstance(e, (types.MessageEntityMentionName, + types.InputMessageEntityMentionName)): + is_mention = await self._replace_with_mention(msg_entities, i, e.user_id) + if not is_mention: + del msg_entities[i] + + return message, msg_entities + + def _get_response_message(self: 'TelegramClient', request, result, input_chat): + """ + Extracts the response message known a request and Update result. + The request may also be the ID of the message to match. + + If ``request is None`` this method returns ``{id: message}``. + + If ``request.random_id`` is a list, this method returns a list too. + """ + if isinstance(result, types.UpdateShort): + updates = [result.update] + entities = {} + elif isinstance(result, (types.Updates, types.UpdatesCombined)): + updates = result.updates + entities = {utils.get_peer_id(x): x + for x in + itertools.chain(result.users, result.chats)} + else: + return None + + random_to_id = {} + id_to_message = {} + for update in updates: + if isinstance(update, types.UpdateMessageID): + random_to_id[update.random_id] = update.id + + elif isinstance(update, ( + types.UpdateNewChannelMessage, types.UpdateNewMessage)): + update.message._finish_init(self, entities, input_chat) + + # Pinning a message with `updatePinnedMessage` seems to + # always produce a service message we can't map so return + # it directly. The same happens for kicking users. + # + # It could also be a list (e.g. when sending albums). + # + # TODO this method is getting messier and messier as time goes on + if hasattr(request, 'random_id') or utils.is_list_like(request): + id_to_message[update.message.id] = update.message + else: + return update.message + + elif (isinstance(update, types.UpdateEditMessage) + and helpers._entity_type(request.peer) != helpers._EntityType.CHANNEL): + update.message._finish_init(self, entities, input_chat) + + # Live locations use `sendMedia` but Telegram responds with + # `updateEditMessage`, which means we won't have `id` field. + if hasattr(request, 'random_id'): + id_to_message[update.message.id] = update.message + elif request.id == update.message.id: + return update.message + + elif (isinstance(update, types.UpdateEditChannelMessage) + and utils.get_peer_id(request.peer) == + utils.get_peer_id(update.message.peer_id)): + if request.id == update.message.id: + update.message._finish_init(self, entities, input_chat) + return update.message + + elif isinstance(update, types.UpdateNewScheduledMessage): + update.message._finish_init(self, entities, input_chat) + # Scheduled IDs may collide with normal IDs. However, for a + # single request there *shouldn't* be a mix between "some + # scheduled and some not". + id_to_message[update.message.id] = update.message + + elif isinstance(update, types.UpdateMessagePoll): + if request.media.poll.id == update.poll_id: + m = types.Message( + id=request.id, + peer_id=utils.get_peer(request.peer), + media=types.MessageMediaPoll( + poll=update.poll, + results=update.results + ) + ) + m._finish_init(self, entities, input_chat) + return m + + if request is None: + return id_to_message + + random_id = request if isinstance(request, (int, list)) else getattr(request, 'random_id', None) + if random_id is None: + # Can happen when pinning a message does not actually produce a service message. + self._log[__name__].warning( + 'No random_id in %s to map to, returning None message for %s', request, result) + return None + + if not utils.is_list_like(random_id): + msg = id_to_message.get(random_to_id.get(random_id)) + + if not msg: + self._log[__name__].warning( + 'Request %s had missing message mapping %s', request, result) + + return msg + + try: + return [id_to_message[random_to_id[rnd]] for rnd in random_id] + except KeyError: + # Sometimes forwards fail (`MESSAGE_ID_INVALID` if a message gets + # deleted or `WORKER_BUSY_TOO_LONG_RETRY` if there are issues at + # Telegram), in which case we get some "missing" message mappings. + # Log them with the hope that we can better work around them. + # + # This also happens when trying to forward messages that can't + # be forwarded because they don't exist (0, service, deleted) + # among others which could be (like deleted or existing). + self._log[__name__].warning( + 'Request %s had missing message mappings %s', request, result) + + return [ + id_to_message.get(random_to_id[rnd]) + if rnd in random_to_id + else None + for rnd in random_id + ] + + # endregion diff --git a/module/scripts/lib/telethon/client/messages.py b/module/scripts/lib/telethon/client/messages.py new file mode 100644 index 0000000..bbcc52a --- /dev/null +++ b/module/scripts/lib/telethon/client/messages.py @@ -0,0 +1,1526 @@ +import inspect +import itertools +import typing +import warnings + +from .. import helpers, utils, errors, hints +from ..requestiter import RequestIter +from ..tl import types, functions + +_MAX_CHUNK_SIZE = 100 + +if typing.TYPE_CHECKING: + from .telegramclient import TelegramClient + + +class _MessagesIter(RequestIter): + """ + Common factor for all requests that need to iterate over messages. + """ + async def _init( + self, entity, offset_id, min_id, max_id, + from_user, offset_date, add_offset, filter, search, reply_to, + scheduled + ): + # Note that entity being `None` will perform a global search. + if entity: + self.entity = await self.client.get_input_entity(entity) + else: + self.entity = None + if self.reverse: + raise ValueError('Cannot reverse global search') + + # Telegram doesn't like min_id/max_id. If these IDs are low enough + # (starting from last_id - 100), the request will return nothing. + # + # We can emulate their behaviour locally by setting offset = max_id + # and simply stopping once we hit a message with ID <= min_id. + if self.reverse: + offset_id = max(offset_id, min_id) + if offset_id and max_id: + if max_id - offset_id <= 1: + raise StopAsyncIteration + + if not max_id: + max_id = float('inf') + else: + offset_id = max(offset_id, max_id) + if offset_id and min_id: + if offset_id - min_id <= 1: + raise StopAsyncIteration + + if self.reverse: + if offset_id: + offset_id += 1 + elif not offset_date: + # offset_id has priority over offset_date, so don't + # set offset_id to 1 if we want to offset by date. + offset_id = 1 + + if from_user: + from_user = await self.client.get_input_entity(from_user) + self.from_id = await self.client.get_peer_id(from_user) + else: + self.from_id = None + + # `messages.searchGlobal` only works with text `search` or `filter` queries. + # If we want to perform global a search with `from_user` we have to perform + # a normal `messages.search`, *but* we can make the entity be `inputPeerEmpty`. + if not self.entity and from_user: + self.entity = types.InputPeerEmpty() + + if filter is None: + filter = types.InputMessagesFilterEmpty() + else: + filter = filter() if isinstance(filter, type) else filter + + if not self.entity: + self.request = functions.messages.SearchGlobalRequest( + q=search or '', + filter=filter, + min_date=None, + max_date=offset_date, + offset_rate=0, + offset_peer=types.InputPeerEmpty(), + offset_id=offset_id, + limit=1 + ) + elif scheduled: + self.request = functions.messages.GetScheduledHistoryRequest( + peer=entity, + hash=0 + ) + elif reply_to is not None: + self.request = functions.messages.GetRepliesRequest( + peer=self.entity, + msg_id=reply_to, + offset_id=offset_id, + offset_date=offset_date, + add_offset=add_offset, + limit=1, + max_id=0, + min_id=0, + hash=0 + ) + elif search is not None or not isinstance(filter, types.InputMessagesFilterEmpty) or from_user: + # Telegram completely ignores `from_id` in private chats + ty = helpers._entity_type(self.entity) + if ty == helpers._EntityType.USER: + # Don't bother sending `from_user` (it's ignored anyway), + # but keep `from_id` defined above to check it locally. + from_user = None + else: + # Do send `from_user` to do the filtering server-side, + # and set `from_id` to None to avoid checking it locally. + self.from_id = None + + self.request = functions.messages.SearchRequest( + peer=self.entity, + q=search or '', + filter=filter, + min_date=None, + max_date=offset_date, + offset_id=offset_id, + add_offset=add_offset, + limit=0, # Search actually returns 0 items if we ask it to + max_id=0, + min_id=0, + hash=0, + from_id=from_user + ) + + # Workaround issue #1124 until a better solution is found. + # Telegram seemingly ignores `max_date` if `filter` (and + # nothing else) is specified, so we have to rely on doing + # a first request to offset from the ID instead. + # + # Even better, using `filter` and `from_id` seems to always + # trigger `RPC_CALL_FAIL` which is "internal issues"... + if not isinstance(filter, types.InputMessagesFilterEmpty) \ + and offset_date and not search and not offset_id: + async for m in self.client.iter_messages( + self.entity, 1, offset_date=offset_date): + self.request.offset_id = m.id + 1 + else: + self.request = functions.messages.GetHistoryRequest( + peer=self.entity, + limit=1, + offset_date=offset_date, + offset_id=offset_id, + min_id=0, + max_id=0, + add_offset=add_offset, + hash=0 + ) + + if self.limit <= 0: + # No messages, but we still need to know the total message count + result = await self.client(self.request) + if isinstance(result, types.messages.MessagesNotModified): + self.total = result.count + else: + self.total = getattr(result, 'count', len(result.messages)) + raise StopAsyncIteration + + if self.wait_time is None: + self.wait_time = 1 if self.limit > 3000 else 0 + + # When going in reverse we need an offset of `-limit`, but we + # also want to respect what the user passed, so add them together. + if self.reverse and hasattr(self.request, 'add_offset'): + self.request.add_offset -= _MAX_CHUNK_SIZE + + self.add_offset = add_offset + self.max_id = max_id + self.min_id = min_id + self.last_id = 0 if self.reverse else float('inf') + + async def _load_next_chunk(self): + if hasattr(self.request, 'limit'): + self.request.limit = min(self.left, _MAX_CHUNK_SIZE) + if self.reverse and self.request.limit != _MAX_CHUNK_SIZE: + # Remember that we need -limit when going in reverse + self.request.add_offset = self.add_offset - self.request.limit + + r = await self.client(self.request) + self.total = getattr(r, 'count', len(r.messages)) + + entities = {utils.get_peer_id(x): x + for x in itertools.chain(r.users, r.chats)} + + messages = reversed(r.messages) if self.reverse else r.messages + for message in messages: + if (isinstance(message, types.MessageEmpty) + or self.from_id and message.sender_id != self.from_id): + continue + + if not self._message_in_range(message): + return True + + # There has been reports that on bad connections this method + # was returning duplicated IDs sometimes. Using ``last_id`` + # is an attempt to avoid these duplicates, since the message + # IDs are returned in descending order (or asc if reverse). + self.last_id = message.id + message._finish_init(self.client, entities, self.entity) + self.buffer.append(message) + + # Not a slice (using offset would return the same, with e.g. SearchGlobal). + if isinstance(r, types.messages.Messages) or not hasattr(self.request, 'limit'): + return True + + # Some channels are "buggy" and may return less messages than + # requested (apparently, the messages excluded are, for example, + # "not displayable due to local laws"). + # + # This means it's not safe to rely on `len(r.messages) < req.limit` as + # the stop condition. Unfortunately more requests must be made. + # + # However we can still check if the highest ID is equal to or lower + # than the limit, in which case there won't be any more messages + # because the lowest message ID is 1. + # + # We also assume the API will always return, at least, one message if + # there is more to fetch. + if not r.messages or (not self.reverse and r.messages[0].id <= self.request.limit): + return True + + # Get the last message that's not empty (in some rare cases + # it can happen that the last message is :tl:`MessageEmpty`) + if self.buffer: + self._update_offset(self.buffer[-1], r) + else: + # There are some cases where all the messages we get start + # being empty. This can happen on migrated mega-groups if + # the history was cleared, and we're using search. Telegram + # acts incredibly weird sometimes. Messages are returned but + # only "empty", not their contents. If this is the case we + # should just give up since there won't be any new Message. + return True + + def _message_in_range(self, message): + """ + Determine whether the given message is in the range or + it should be ignored (and avoid loading more chunks). + """ + # No entity means message IDs between chats may vary + if self.entity: + if self.reverse: + if message.id <= self.last_id or message.id >= self.max_id: + return False + else: + if message.id >= self.last_id or message.id <= self.min_id: + return False + + return True + + def _update_offset(self, last_message, response): + """ + After making the request, update its offset with the last message. + """ + self.request.offset_id = last_message.id + if self.reverse: + # We want to skip the one we already have + self.request.offset_id += 1 + + if isinstance(self.request, functions.messages.SearchRequest): + # Unlike getHistory and searchGlobal that use *offset* date, + # this is *max* date. This means that doing a search in reverse + # will break it. Since it's not really needed once we're going + # (only for the first request), it's safe to just clear it off. + self.request.max_date = None + else: + # getHistory, searchGlobal and getReplies call it offset_date + self.request.offset_date = last_message.date + + if isinstance(self.request, functions.messages.SearchGlobalRequest): + if last_message.input_chat: + self.request.offset_peer = last_message.input_chat + else: + self.request.offset_peer = types.InputPeerEmpty() + + self.request.offset_rate = getattr(response, 'next_rate', 0) + + +class _IDsIter(RequestIter): + async def _init(self, entity, ids): + self.total = len(ids) + self._ids = list(reversed(ids)) if self.reverse else ids + self._offset = 0 + self._entity = (await self.client.get_input_entity(entity)) if entity else None + self._ty = helpers._entity_type(self._entity) if self._entity else None + + # 30s flood wait every 300 messages (3 requests of 100 each, 30 of 10, etc.) + if self.wait_time is None: + self.wait_time = 10 if self.limit > 300 else 0 + + async def _load_next_chunk(self): + ids = self._ids[self._offset:self._offset + _MAX_CHUNK_SIZE] + if not ids: + raise StopAsyncIteration + + self._offset += _MAX_CHUNK_SIZE + + from_id = None # By default, no need to validate from_id + if self._ty == helpers._EntityType.CHANNEL: + try: + r = await self.client( + functions.channels.GetMessagesRequest(self._entity, ids)) + except errors.MessageIdsEmptyError: + # All IDs were invalid, use a dummy result + r = types.messages.MessagesNotModified(len(ids)) + else: + r = await self.client(functions.messages.GetMessagesRequest(ids)) + if self._entity: + from_id = await self.client._get_peer(self._entity) + + if isinstance(r, types.messages.MessagesNotModified): + self.buffer.extend(None for _ in ids) + return + + entities = {utils.get_peer_id(x): x + for x in itertools.chain(r.users, r.chats)} + + # Telegram seems to return the messages in the order in which + # we asked them for, so we don't need to check it ourselves, + # unless some messages were invalid in which case Telegram + # may decide to not send them at all. + # + # The passed message IDs may not belong to the desired entity + # since the user can enter arbitrary numbers which can belong to + # arbitrary chats. Validate these unless ``from_id is None``. + for message in r.messages: + if isinstance(message, types.MessageEmpty) or ( + from_id and message.peer_id != from_id): + self.buffer.append(None) + else: + message._finish_init(self.client, entities, self._entity) + self.buffer.append(message) + + +class MessageMethods: + + # region Public methods + + # region Message retrieval + + def iter_messages( + self: 'TelegramClient', + entity: 'hints.EntityLike', + limit: float = None, + *, + offset_date: 'hints.DateLike' = None, + offset_id: int = 0, + max_id: int = 0, + min_id: int = 0, + add_offset: int = 0, + search: str = None, + filter: 'typing.Union[types.TypeMessagesFilter, typing.Type[types.TypeMessagesFilter]]' = None, + from_user: 'hints.EntityLike' = None, + wait_time: float = None, + ids: 'typing.Union[int, typing.Sequence[int]]' = None, + reverse: bool = False, + reply_to: int = None, + scheduled: bool = False + ) -> 'typing.Union[_MessagesIter, _IDsIter]': + """ + Iterator over the messages for the given chat. + + The default order is from newest to oldest, but this + behaviour can be changed with the `reverse` parameter. + + If either `search`, `filter` or `from_user` are provided, + :tl:`messages.Search` will be used instead of :tl:`messages.getHistory`. + + .. note:: + + Telegram's flood wait limit for :tl:`GetHistoryRequest` seems to + be around 30 seconds per 10 requests, therefore a sleep of 1 + second is the default for this limit (or above). + + Arguments + entity (`entity`): + The entity from whom to retrieve the message history. + + It may be `None` to perform a global search, or + to get messages by their ID from no particular chat. + Note that some of the offsets will not work if this + is the case. + + Note that if you want to perform a global search, + you **must** set a non-empty `search` string, a `filter`. + or `from_user`. + + limit (`int` | `None`, optional): + Number of messages to be retrieved. Due to limitations with + the API retrieving more than 3000 messages will take longer + than half a minute (or even more based on previous calls). + + The limit may also be `None`, which would eventually return + the whole history. + + offset_date (`datetime`): + Offset date (messages *previous* to this date will be + retrieved). Exclusive. + + offset_id (`int`): + Offset message ID (only messages *previous* to the given + ID will be retrieved). Exclusive. + + max_id (`int`): + All the messages with a higher (newer) ID or equal to this will + be excluded. + + min_id (`int`): + All the messages with a lower (older) ID or equal to this will + be excluded. + + add_offset (`int`): + Additional message offset (all of the specified offsets + + this offset = older messages). + + search (`str`): + The string to be used as a search query. + + filter (:tl:`MessagesFilter` | `type`): + The filter to use when returning messages. For instance, + :tl:`InputMessagesFilterPhotos` would yield only messages + containing photos. + + from_user (`entity`): + Only messages from this entity will be returned. + + wait_time (`int`): + Wait time (in seconds) between different + :tl:`GetHistoryRequest`. Use this parameter to avoid hitting + the ``FloodWaitError`` as needed. If left to `None`, it will + default to 1 second only if the limit is higher than 3000. + + If the ``ids`` parameter is used, this time will default + to 10 seconds only if the amount of IDs is higher than 300. + + ids (`int`, `list`): + A single integer ID (or several IDs) for the message that + should be returned. This parameter takes precedence over + the rest (which will be ignored if this is set). This can + for instance be used to get the message with ID 123 from + a channel. Note that if the message doesn't exist, `None` + will appear in its place, so that zipping the list of IDs + with the messages can match one-to-one. + + .. note:: + + At the time of writing, Telegram will **not** return + :tl:`MessageEmpty` for :tl:`InputMessageReplyTo` IDs that + failed (i.e. the message is not replying to any, or is + replying to a deleted message). This means that it is + **not** possible to match messages one-by-one, so be + careful if you use non-integers in this parameter. + + reverse (`bool`, optional): + If set to `True`, the messages will be returned in reverse + order (from oldest to newest, instead of the default newest + to oldest). This also means that the meaning of `offset_id` + and `offset_date` parameters is reversed, although they will + still be exclusive. `min_id` becomes equivalent to `offset_id` + instead of being `max_id` as well since messages are returned + in ascending order. + + You cannot use this if both `entity` and `ids` are `None`. + + reply_to (`int`, optional): + If set to a message ID, the messages that reply to this ID + will be returned. This feature is also known as comments in + posts of broadcast channels, or viewing threads in groups. + + This feature can only be used in broadcast channels and their + linked megagroups. Using it in a chat or private conversation + will result in ``telethon.errors.PeerIdInvalidError`` to occur. + + When using this parameter, the ``filter`` and ``search`` + parameters have no effect, since Telegram's API doesn't + support searching messages in replies. + + .. note:: + + This feature is used to get replies to a message in the + *discussion* group. If the same broadcast channel sends + a message and replies to it itself, that reply will not + be included in the results. + + scheduled (`bool`, optional): + If set to `True`, messages which are scheduled will be returned. + All other parameter will be ignored for this, except `entity`. + + Yields + Instances of `Message `. + + Example + .. code-block:: python + + # From most-recent to oldest + async for message in client.iter_messages(chat): + print(message.id, message.text) + + # From oldest to most-recent + async for message in client.iter_messages(chat, reverse=True): + print(message.id, message.text) + + # Filter by sender + async for message in client.iter_messages(chat, from_user='me'): + print(message.text) + + # Server-side search with fuzzy text + async for message in client.iter_messages(chat, search='hello'): + print(message.id) + + # Filter by message type: + from telethon.tl.types import InputMessagesFilterPhotos + async for message in client.iter_messages(chat, filter=InputMessagesFilterPhotos): + print(message.photo) + + # Getting comments from a post in a channel: + async for message in client.iter_messages(channel, reply_to=123): + print(message.chat.title, message.text) + """ + if ids is not None: + if not utils.is_list_like(ids): + ids = [ids] + + return _IDsIter( + client=self, + reverse=reverse, + wait_time=wait_time, + limit=len(ids), + entity=entity, + ids=ids + ) + + return _MessagesIter( + client=self, + reverse=reverse, + wait_time=wait_time, + limit=limit, + entity=entity, + offset_id=offset_id, + min_id=min_id, + max_id=max_id, + from_user=from_user, + offset_date=offset_date, + add_offset=add_offset, + filter=filter, + search=search, + reply_to=reply_to, + scheduled=scheduled + ) + + async def get_messages( + self: 'TelegramClient', *args, **kwargs + ) -> typing.Union['hints.TotalList', typing.Optional['types.Message']]: + """ + Same as `iter_messages()`, but returns a + `TotalList ` instead. + + If the `limit` is not set, it will be 1 by default unless both + `min_id` **and** `max_id` are set (as *named* arguments), in + which case the entire range will be returned. + + This is so because any integer limit would be rather arbitrary and + it's common to only want to fetch one message, but if a range is + specified it makes sense that it should return the entirety of it. + + If `ids` is present in the *named* arguments and is not a list, + a single `Message ` will be + returned for convenience instead of a list. + + Example + .. code-block:: python + + # Get 0 photos and print the total to show how many photos there are + from telethon.tl.types import InputMessagesFilterPhotos + photos = await client.get_messages(chat, 0, filter=InputMessagesFilterPhotos) + print(photos.total) + + # Get all the photos + photos = await client.get_messages(chat, None, filter=InputMessagesFilterPhotos) + + # Get messages by ID: + message_1337 = await client.get_messages(chat, ids=1337) + """ + if len(args) == 1 and 'limit' not in kwargs: + if 'min_id' in kwargs and 'max_id' in kwargs: + kwargs['limit'] = None + else: + kwargs['limit'] = 1 + + it = self.iter_messages(*args, **kwargs) + + ids = kwargs.get('ids') + if ids and not utils.is_list_like(ids): + async for message in it: + return message + else: + # Iterator exhausted = empty, to handle InputMessageReplyTo + return None + + return await it.collect() + + get_messages.__signature__ = inspect.signature(iter_messages) + + # endregion + + # region Message sending/editing/deleting + + async def _get_comment_data( + self: 'TelegramClient', + entity: 'hints.EntityLike', + message: 'typing.Union[int, types.Message]' + ): + r = await self(functions.messages.GetDiscussionMessageRequest( + peer=entity, + msg_id=utils.get_message_id(message) + )) + m = min(r.messages, key=lambda msg: msg.id) + chat = next(c for c in r.chats if c.id == m.peer_id.channel_id) + return utils.get_input_peer(chat), m.id + + async def send_message( + self: 'TelegramClient', + entity: 'hints.EntityLike', + message: 'hints.MessageLike' = '', + *, + reply_to: 'typing.Union[int, types.Message]' = None, + attributes: 'typing.Sequence[types.TypeDocumentAttribute]' = None, + parse_mode: typing.Optional[str] = (), + formatting_entities: typing.Optional[typing.List[types.TypeMessageEntity]] = None, + link_preview: bool = True, + file: 'typing.Union[hints.FileLike, typing.Sequence[hints.FileLike]]' = None, + thumb: 'hints.FileLike' = None, + force_document: bool = False, + clear_draft: bool = False, + buttons: typing.Optional['hints.MarkupLike'] = None, + silent: bool = None, + background: bool = None, + supports_streaming: bool = False, + schedule: 'hints.DateLike' = None, + comment_to: 'typing.Union[int, types.Message]' = None, + nosound_video: bool = None, + send_as: typing.Optional['hints.EntityLike'] = None, + message_effect_id: typing.Optional[int] = None + ) -> 'types.Message': + """ + Sends a message to the specified user, chat or channel. + + The default parse mode is the same as the official applications + (a custom flavour of markdown). ``**bold**, `code` or __italic__`` + are available. In addition you can send ``[links](https://example.com)`` + and ``[mentions](@username)`` (or using IDs like in the Bot API: + ``[mention](tg://user?id=123456789)``) and ``pre`` blocks with three + backticks. + + Sending a ``/start`` command with a parameter (like ``?start=data``) + is also done through this method. Simply send ``'/start data'`` to + the bot. + + See also `Message.respond() ` + and `Message.reply() `. + + Arguments + entity (`entity`): + To who will it be sent. + + message (`str` | `Message `): + The message to be sent, or another message object to resend. + + The maximum length for a message is 35,000 bytes or 4,096 + characters. Longer messages will not be sliced automatically, + and you should slice them manually if the text to send is + longer than said length. + + reply_to (`int` | `Message `, optional): + Whether to reply to a message or not. If an integer is provided, + it should be the ID of the message that it should reply to. + + attributes (`list`, optional): + Optional attributes that override the inferred ones, like + :tl:`DocumentAttributeFilename` and so on. + + parse_mode (`object`, optional): + See the `TelegramClient.parse_mode + ` + property for allowed values. Markdown parsing will be used by + default. + + formatting_entities (`list`, optional): + A list of message formatting entities. When provided, the ``parse_mode`` is ignored. + + link_preview (`bool`, optional): + Should the link preview be shown? + + file (`file`, optional): + Sends a message with a file attached (e.g. a photo, + video, audio or document). The ``message`` may be empty. + + thumb (`str` | `bytes` | `file`, optional): + Optional JPEG thumbnail (for documents). **Telegram will + ignore this parameter** unless you pass a ``.jpg`` file! + The file must also be small in dimensions and in disk size. + Successful thumbnails were files below 20kB and 320x320px. + Width/height and dimensions/size ratios may be important. + For Telegram to accept a thumbnail, you must provide the + dimensions of the underlying media through ``attributes=`` + with :tl:`DocumentAttributeVideo` or by installing the + optional ``hachoir`` dependency. + + force_document (`bool`, optional): + Whether to send the given file as a document or not. + + clear_draft (`bool`, optional): + Whether the existing draft should be cleared or not. + + buttons (`list`, `custom.Button `, :tl:`KeyboardButton`, :tl:`KeyboardInlineButton`): + The matrix (list of lists), row list or button to be shown + after sending the message. This parameter will only work if + you have signed in as a bot. You can also pass your own + :tl:`ReplyMarkup` here. + + All the following limits apply together: + + * There can be 100 buttons at most (any more are ignored). + * There can be 8 buttons per row at most (more are ignored). + * The maximum callback data per button is 64 bytes. + * The maximum data that can be embedded in total is just + over 4KB, shared between inline callback data and text. + + silent (`bool`, optional): + Whether the message should notify people in a broadcast + channel or not. Defaults to `False`, which means it will + notify them. Set it to `True` to alter this behaviour. + + background (`bool`, optional): + Whether the message should be send in background. + + supports_streaming (`bool`, optional): + Whether the sent video supports streaming or not. Note that + Telegram only recognizes as streamable some formats like MP4, + and others like AVI or MKV will not work. You should convert + these to MP4 before sending if you want them to be streamable. + Unsupported formats will result in ``VideoContentTypeError``. + + schedule (`hints.DateLike`, optional): + If set, the message won't send immediately, and instead + it will be scheduled to be automatically sent at a later + time. + + comment_to (`int` | `Message `, optional): + Similar to ``reply_to``, but replies in the linked group of a + broadcast channel instead (effectively leaving a "comment to" + the specified message). + + This parameter takes precedence over ``reply_to``. If there is + no linked chat, `telethon.errors.sgIdInvalidError` is raised. + + nosound_video (`bool`, optional): + Only applicable when sending a video file without an audio + track. If set to ``True``, the video will be displayed in + Telegram as a video. If set to ``False``, Telegram will attempt + to display the video as an animated gif. (It may still display + as a video due to other factors.) The value is ignored if set + on non-video files. This is set to ``True`` for albums, as gifs + cannot be sent in albums. + + send_as (`entity`): + Unique identifier (int) or username (str) of the chat or channel to send the message as. + You can use this to send the message on behalf of a chat or channel where you have appropriate permissions. + Use the GetSendAs to return the list of message sender identifiers, which can be used to send messages in the chat, + This setting applies to the current message and will remain effective for future messages unless explicitly changed. + To set this behavior permanently for all messages, use SaveDefaultSendAs. + + message_effect_id (`int`, optional): + Unique identifier of the message effect to be added to the message; for private chats only + + Returns + The sent `custom.Message `. + + Example + .. code-block:: python + + # Markdown is the default + await client.send_message('me', 'Hello **world**!') + + # Default to another parse mode + client.parse_mode = 'html' + + await client.send_message('me', 'Some bold and italic text') + await client.send_message('me', 'An URL') + # code and pre tags also work, but those break the documentation :) + await client.send_message('me', 'Mentions') + + # Explicit parse mode + # No parse mode by default + client.parse_mode = None + + # ...but here I want markdown + await client.send_message('me', 'Hello, **world**!', parse_mode='md') + + # ...and here I need HTML + await client.send_message('me', 'Hello, world!', parse_mode='html') + + # If you logged in as a bot account, you can send buttons + from telethon import events, Button + + @client.on(events.CallbackQuery) + async def callback(event): + await event.edit('Thank you for clicking {}!'.format(event.data)) + + # Single inline button + await client.send_message(chat, 'A single button, with "clk1" as data', + buttons=Button.inline('Click me', b'clk1')) + + # Matrix of inline buttons + await client.send_message(chat, 'Pick one from this grid', buttons=[ + [Button.inline('Left'), Button.inline('Right')], + [Button.url('Check this site!', 'https://example.com')] + ]) + + # Reply keyboard + await client.send_message(chat, 'Welcome', buttons=[ + Button.text('Thanks!', resize=True, single_use=True), + Button.request_phone('Send phone'), + Button.request_location('Send location') + ]) + + # Forcing replies or clearing buttons. + await client.send_message(chat, 'Reply to me', buttons=Button.force_reply()) + await client.send_message(chat, 'Bye Keyboard!', buttons=Button.clear()) + + # Scheduling a message to be sent after 5 minutes + from datetime import timedelta + await client.send_message(chat, 'Hi, future!', schedule=timedelta(minutes=5)) + """ + if file is not None: + if isinstance(message, types.Message): + formatting_entities = formatting_entities or message.entities + message = message.message + return await self.send_file( + entity, file, caption=message, reply_to=reply_to, + attributes=attributes, parse_mode=parse_mode, + force_document=force_document, thumb=thumb, + buttons=buttons, clear_draft=clear_draft, silent=silent, + schedule=schedule, supports_streaming=supports_streaming, + formatting_entities=formatting_entities, + comment_to=comment_to, background=background, + nosound_video=nosound_video, + send_as=send_as, message_effect_id=message_effect_id + ) + + entity = await self.get_input_entity(entity) + if comment_to is not None: + entity, reply_to = await self._get_comment_data(entity, comment_to) + else: + reply_to = utils.get_message_id(reply_to) + + if isinstance(message, types.Message): + if buttons is None: + markup = message.reply_markup + else: + markup = self.build_reply_markup(buttons) + + if silent is None: + silent = message.silent + + if (message.media and not isinstance( + message.media, types.MessageMediaWebPage)): + return await self.send_file( + entity, + message.media, + caption=message.message, + silent=silent, + background=background, + reply_to=reply_to, + buttons=markup, + formatting_entities=message.entities, + parse_mode=None, # explicitly disable parse_mode to force using even empty formatting_entities + schedule=schedule, + send_as=send_as, message_effect_id=message_effect_id + ) + + request = functions.messages.SendMessageRequest( + peer=entity, + message=message.message or '', + silent=silent, + background=background, + reply_to=None if reply_to is None else types.InputReplyToMessage(reply_to), + reply_markup=markup, + entities=message.entities, + clear_draft=clear_draft, + no_webpage=not isinstance( + message.media, types.MessageMediaWebPage), + schedule_date=schedule, + send_as=await self.get_input_entity(send_as) if send_as else None, + effect=message_effect_id + ) + message = message.message + else: + if formatting_entities is None: + message, formatting_entities = await self._parse_message_text(message, parse_mode) + if not message: + raise ValueError( + 'The message cannot be empty unless a file is provided' + ) + + request = functions.messages.SendMessageRequest( + peer=entity, + message=message, + entities=formatting_entities, + no_webpage=not link_preview, + reply_to=None if reply_to is None else types.InputReplyToMessage(reply_to), + clear_draft=clear_draft, + silent=silent, + background=background, + reply_markup=self.build_reply_markup(buttons), + schedule_date=schedule, + send_as=await self.get_input_entity(send_as) if send_as else None, + effect=message_effect_id + ) + + result = await self(request) + if isinstance(result, types.UpdateShortSentMessage): + message = types.Message( + id=result.id, + peer_id=await self._get_peer(entity), + message=message, + date=result.date, + out=result.out, + media=result.media, + entities=result.entities, + reply_markup=request.reply_markup, + ttl_period=result.ttl_period, + reply_to=request.reply_to + ) + message._finish_init(self, {}, entity) + return message + + return self._get_response_message(request, result, entity) + + async def forward_messages( + self: 'TelegramClient', + entity: 'hints.EntityLike', + messages: 'typing.Union[hints.MessageIDLike, typing.Sequence[hints.MessageIDLike]]', + from_peer: 'hints.EntityLike' = None, + *, + background: bool = None, + with_my_score: bool = None, + silent: bool = None, + as_album: bool = None, + schedule: 'hints.DateLike' = None, + drop_author: bool = None, + drop_media_captions: bool = None, + ) -> 'typing.Sequence[types.Message]': + """ + Forwards the given messages to the specified entity. + + If you want to "forward" a message without the forward header + (the "forwarded from" text), you should use `send_message` with + the original message instead. This will send a copy of it. + + See also `Message.forward_to() `. + + Arguments + entity (`entity`): + To which entity the message(s) will be forwarded. + + messages (`list` | `int` | `Message `): + The message(s) to forward, or their integer IDs. + + from_peer (`entity`): + If the given messages are integer IDs and not instances + of the ``Message`` class, this *must* be specified in + order for the forward to work. This parameter indicates + the entity from which the messages should be forwarded. + + silent (`bool`, optional): + Whether the message should notify people with sound or not. + Defaults to `False` (send with a notification sound unless + the person has the chat muted). Set it to `True` to alter + this behaviour. + + background (`bool`, optional): + Whether the message should be forwarded in background. + + with_my_score (`bool`, optional): + Whether forwarded should contain your game score. + + as_album (`bool`, optional): + This flag no longer has any effect. + + schedule (`hints.DateLike`, optional): + If set, the message(s) won't forward immediately, and + instead they will be scheduled to be automatically sent + at a later time. + + drop_author (`bool`, optional): + Whether to forward messages without quoting the original author. + + drop_media_captions (`bool`, optional): + Whether to strip captions from media. Setting this to `True` requires that `drop_author` also be set to `True`. + + Returns + The list of forwarded `Message `, + or a single one if a list wasn't provided as input. + + Note that if all messages are invalid (i.e. deleted) the call + will fail with ``MessageIdInvalidError``. If only some are + invalid, the list will have `None` instead of those messages. + + Example + .. code-block:: python + + # a single one + await client.forward_messages(chat, message) + # or + await client.forward_messages(chat, message_id, from_chat) + # or + await message.forward_to(chat) + + # multiple + await client.forward_messages(chat, messages) + # or + await client.forward_messages(chat, message_ids, from_chat) + + # Forwarding as a copy + await client.send_message(chat, message) + """ + if as_album is not None: + warnings.warn('the as_album argument is deprecated and no longer has any effect') + + single = not utils.is_list_like(messages) + if single: + messages = (messages,) + + entity = await self.get_input_entity(entity) + + if from_peer: + from_peer = await self.get_input_entity(from_peer) + from_peer_id = await self.get_peer_id(from_peer) + else: + from_peer_id = None + + def get_key(m): + if isinstance(m, int): + if from_peer_id is not None: + return from_peer_id + + raise ValueError('from_peer must be given if integer IDs are used') + elif isinstance(m, types.Message): + return m.chat_id + else: + raise TypeError('Cannot forward messages of type {}'.format(type(m))) + + sent = [] + for _chat_id, chunk in itertools.groupby(messages, key=get_key): + chunk = list(chunk) + if isinstance(chunk[0], int): + chat = from_peer + else: + chat = from_peer or await self.get_input_entity(chunk[0].peer_id) + chunk = [m.id for m in chunk] + + req = functions.messages.ForwardMessagesRequest( + from_peer=chat, + id=chunk, + to_peer=entity, + silent=silent, + background=background, + with_my_score=with_my_score, + schedule_date=schedule, + drop_author=drop_author, + drop_media_captions=drop_media_captions + ) + result = await self(req) + sent.extend(self._get_response_message(req, result, entity)) + + return sent[0] if single else sent + + async def edit_message( + self: 'TelegramClient', + entity: 'typing.Union[hints.EntityLike, types.Message]', + message: 'typing.Union[int, types.Message, types.InputMessageID, str]' = None, + text: str = None, + *, + parse_mode: typing.Optional[str] = (), + attributes: 'typing.Sequence[types.TypeDocumentAttribute]' = None, + formatting_entities: typing.Optional[typing.List[types.TypeMessageEntity]] = None, + link_preview: bool = True, + file: 'hints.FileLike' = None, + thumb: 'hints.FileLike' = None, + force_document: bool = False, + buttons: typing.Optional['hints.MarkupLike'] = None, + supports_streaming: bool = False, + schedule: 'hints.DateLike' = None + ) -> 'types.Message': + """ + Edits the given message to change its text or media. + + See also `Message.edit() `. + + Arguments + entity (`entity` | `Message `): + From which chat to edit the message. This can also be + the message to be edited, and the entity will be inferred + from it, so the next parameter will be assumed to be the + message text. + + You may also pass a :tl:`InputBotInlineMessageID` or :tl:`InputBotInlineMessageID64`, + which is the only way to edit messages that were sent + after the user selects an inline query result. + + message (`int` | `Message ` | :tl:`InputMessageID` | `str`): + The ID of the message (or `Message + ` itself) to be edited. + If the `entity` was a `Message + `, then this message + will be treated as the new text. + + text (`str`, optional): + The new text of the message. Does nothing if the `entity` + was a `Message `. + + parse_mode (`object`, optional): + See the `TelegramClient.parse_mode + ` + property for allowed values. Markdown parsing will be used by + default. + + attributes (`list`, optional): + Optional attributes that override the inferred ones, like + :tl:`DocumentAttributeFilename` and so on. + + formatting_entities (`list`, optional): + A list of message formatting entities. When provided, the ``parse_mode`` is ignored. + + link_preview (`bool`, optional): + Should the link preview be shown? + + file (`str` | `bytes` | `file` | `media`, optional): + The file object that should replace the existing media + in the message. + + thumb (`str` | `bytes` | `file`, optional): + Optional JPEG thumbnail (for documents). **Telegram will + ignore this parameter** unless you pass a ``.jpg`` file! + The file must also be small in dimensions and in disk size. + Successful thumbnails were files below 20kB and 320x320px. + Width/height and dimensions/size ratios may be important. + For Telegram to accept a thumbnail, you must provide the + dimensions of the underlying media through ``attributes=`` + with :tl:`DocumentAttributeVideo` or by installing the + optional ``hachoir`` dependency. + + force_document (`bool`, optional): + Whether to send the given file as a document or not. + + buttons (`list`, `custom.Button `, :tl:`KeyboardButton`, :tl:`KeyboardInlineButton`): + The matrix (list of lists), row list or button to be shown + after sending the message. This parameter will only work if + you have signed in as a bot. You can also pass your own + :tl:`ReplyMarkup` here. + + supports_streaming (`bool`, optional): + Whether the sent video supports streaming or not. Note that + Telegram only recognizes as streamable some formats like MP4, + and others like AVI or MKV will not work. You should convert + these to MP4 before sending if you want them to be streamable. + Unsupported formats will result in ``VideoContentTypeError``. + + schedule (`hints.DateLike`, optional): + If set, the message won't be edited immediately, and instead + it will be scheduled to be automatically edited at a later + time. + + Note that this parameter will have no effect if you are + trying to edit a message that was sent via inline bots. + + Returns + The edited `Message `, + unless `entity` was a :tl:`InputBotInlineMessageID` or :tl:`InputBotInlineMessageID64` in which + case this method returns a boolean. + + Raises + ``MessageAuthorRequiredError`` if you're not the author of the + message but tried editing it anyway. + + ``MessageNotModifiedError`` if the contents of the message were + not modified at all. + + ``MessageIdInvalidError`` if the ID of the message is invalid + (the ID itself may be correct, but the message with that ID + cannot be edited). For example, when trying to edit messages + with a reply markup (or clear markup) this error will be raised. + + Example + .. code-block:: python + + message = await client.send_message(chat, 'hello') + + await client.edit_message(chat, message, 'hello!') + # or + await client.edit_message(chat, message.id, 'hello!!') + # or + await client.edit_message(message, 'hello!!!') + """ + if isinstance(entity, (types.InputBotInlineMessageID, types.InputBotInlineMessageID64)): + text = text or message + message = entity + elif isinstance(entity, types.Message): + text = message # Shift the parameters to the right + message = entity + entity = entity.peer_id + + if formatting_entities is None: + text, formatting_entities = await self._parse_message_text(text, parse_mode) + file_handle, media, image = await self._file_to_media(file, + supports_streaming=supports_streaming, + thumb=thumb, + attributes=attributes, + force_document=force_document) + + if isinstance(entity, (types.InputBotInlineMessageID, types.InputBotInlineMessageID64)): + request = functions.messages.EditInlineBotMessageRequest( + id=entity, + message=text, + no_webpage=not link_preview, + entities=formatting_entities, + media=media, + reply_markup=self.build_reply_markup(buttons) + ) + # Invoke `messages.editInlineBotMessage` from the right datacenter. + # Otherwise, Telegram will error with `MESSAGE_ID_INVALID` and do nothing. + exported = self.session.dc_id != entity.dc_id + if exported: + try: + sender = await self._borrow_exported_sender(entity.dc_id) + return await self._call(sender, request) + finally: + await self._return_exported_sender(sender) + else: + return await self(request) + + entity = await self.get_input_entity(entity) + request = functions.messages.EditMessageRequest( + peer=entity, + id=utils.get_message_id(message), + message=text, + no_webpage=not link_preview, + entities=formatting_entities, + media=media, + reply_markup=self.build_reply_markup(buttons), + schedule_date=schedule + ) + msg = self._get_response_message(request, await self(request), entity) + return msg + + async def delete_messages( + self: 'TelegramClient', + entity: 'hints.EntityLike', + message_ids: 'typing.Union[hints.MessageIDLike, typing.Sequence[hints.MessageIDLike]]', + *, + revoke: bool = True) -> 'typing.Sequence[types.messages.AffectedMessages]': + """ + Deletes the given messages, optionally "for everyone". + + See also `Message.delete() `. + + .. warning:: + + This method does **not** validate that the message IDs belong + to the chat that you passed! It's possible for the method to + delete messages from different private chats and small group + chats at once, so make sure to pass the right IDs. + + Arguments + entity (`entity`): + From who the message will be deleted. This can actually + be `None` for normal chats, but **must** be present + for channels and megagroups. + + message_ids (`list` | `int` | `Message `): + The IDs (or ID) or messages to be deleted. + + revoke (`bool`, optional): + Whether the message should be deleted for everyone or not. + By default it has the opposite behaviour of official clients, + and it will delete the message for everyone. + + `Since 24 March 2019 + `_, you can + also revoke messages of any age (i.e. messages sent long in + the past) the *other* person sent in private conversations + (and of course your messages too). + + Disabling this has no effect on channels or megagroups, + since it will unconditionally delete the message for everyone. + + Returns + A list of :tl:`AffectedMessages`, each item being the result + for the delete calls of the messages in chunks of 100 each. + + Example + .. code-block:: python + + await client.delete_messages(chat, messages) + """ + if not utils.is_list_like(message_ids): + message_ids = (message_ids,) + + message_ids = ( + m.id if isinstance(m, ( + types.Message, types.MessageService, types.MessageEmpty)) + else int(m) for m in message_ids + ) + + if entity: + entity = await self.get_input_entity(entity) + ty = helpers._entity_type(entity) + else: + # no entity (None), set a value that's not a channel for private delete + ty = helpers._EntityType.USER + + if ty == helpers._EntityType.CHANNEL: + return await self([functions.channels.DeleteMessagesRequest( + entity, list(c)) for c in utils.chunks(message_ids)]) + else: + return await self([functions.messages.DeleteMessagesRequest( + list(c), revoke) for c in utils.chunks(message_ids)]) + + # endregion + + # region Miscellaneous + + async def send_read_acknowledge( + self: 'TelegramClient', + entity: 'hints.EntityLike', + message: 'typing.Union[hints.MessageIDLike, typing.Sequence[hints.MessageIDLike]]' = None, + *, + max_id: int = None, + clear_mentions: bool = False, + clear_reactions: bool = False) -> bool: + """ + Marks messages as read and optionally clears mentions. + + This effectively marks a message as read (or more than one) in the + given conversation. + + If neither message nor maximum ID are provided, all messages will be + marked as read by assuming that ``max_id = 0``. + + If a message or maximum ID is provided, all the messages up to and + including such ID will be marked as read (for all messages whose ID + ≤ max_id). + + See also `Message.mark_read() `. + + Arguments + entity (`entity`): + The chat where these messages are located. + + message (`list` | `Message `): + Either a list of messages or a single message. + + max_id (`int`): + Until which message should the read acknowledge be sent for. + This has priority over the ``message`` parameter. + + clear_mentions (`bool`): + Whether the mention badge should be cleared (so that + there are no more mentions) or not for the given entity. + + If no message is provided, this will be the only action + taken. + + clear_reactions (`bool`): + Whether the reactions badge should be cleared (so that + there are no more reaction notifications) or not for the given entity. + + If no message is provided, this will be the only action + taken. + + Example + .. code-block:: python + + # using a Message object + await client.send_read_acknowledge(chat, message) + # ...or using the int ID of a Message + await client.send_read_acknowledge(chat, message_id) + # ...or passing a list of messages to mark as read + await client.send_read_acknowledge(chat, messages) + """ + if max_id is None: + if not message: + max_id = 0 + else: + if utils.is_list_like(message): + max_id = max(msg.id for msg in message) + else: + max_id = message.id + + entity = await self.get_input_entity(entity) + if clear_mentions: + await self(functions.messages.ReadMentionsRequest(entity)) + if max_id is None and not clear_reactions: + return True + if clear_reactions: + await self(functions.messages.ReadReactionsRequest(entity)) + if max_id is None: + return True + + if max_id is not None: + if helpers._entity_type(entity) == helpers._EntityType.CHANNEL: + return await self(functions.channels.ReadHistoryRequest( + utils.get_input_channel(entity), max_id=max_id)) + else: + return await self(functions.messages.ReadHistoryRequest( + entity, max_id=max_id)) + + return False + + async def pin_message( + self: 'TelegramClient', + entity: 'hints.EntityLike', + message: 'typing.Optional[hints.MessageIDLike]', + *, + notify: bool = False, + pm_oneside: bool = False + ): + """ + Pins a message in a chat. + + The default behaviour is to *not* notify members, unlike the + official applications. + + See also `Message.pin() `. + + Arguments + entity (`entity`): + The chat where the message should be pinned. + + message (`int` | `Message `): + The message or the message ID to pin. If it's + `None`, all messages will be unpinned instead. + + notify (`bool`, optional): + Whether the pin should notify people or not. + + pm_oneside (`bool`, optional): + Whether the message should be pinned for everyone or not. + By default it has the opposite behaviour of official clients, + and it will pin the message for both sides, in private chats. + + Example + .. code-block:: python + + # Send and pin a message to annoy everyone + message = await client.send_message(chat, 'Pinotifying is fun!') + await client.pin_message(chat, message, notify=True) + """ + return await self._pin(entity, message, unpin=False, notify=notify, pm_oneside=pm_oneside) + + async def unpin_message( + self: 'TelegramClient', + entity: 'hints.EntityLike', + message: 'typing.Optional[hints.MessageIDLike]' = None, + *, + notify: bool = False + ): + """ + Unpins a message in a chat. + + If no message ID is specified, all pinned messages will be unpinned. + + See also `Message.unpin() `. + + Arguments + entity (`entity`): + The chat where the message should be pinned. + + message (`int` | `Message `): + The message or the message ID to unpin. If it's + `None`, all messages will be unpinned instead. + + Example + .. code-block:: python + + # Unpin all messages from a chat + await client.unpin_message(chat) + """ + return await self._pin(entity, message, unpin=True, notify=notify) + + async def _pin(self, entity, message, *, unpin, notify=False, pm_oneside=False): + message = utils.get_message_id(message) or 0 + entity = await self.get_input_entity(entity) + if message <= 0: # old behaviour accepted negative IDs to unpin + await self(functions.messages.UnpinAllMessagesRequest(entity)) + return + + request = functions.messages.UpdatePinnedMessageRequest( + peer=entity, + id=message, + silent=not notify, + unpin=unpin, + pm_oneside=pm_oneside + ) + result = await self(request) + + # Unpinning does not produce a service message. + # Pinning a message that was already pinned also produces no service message. + # Pinning a message in your own chat does not produce a service message, + # but pinning on a private conversation with someone else does. + if unpin or not result.updates: + return + + # Pinning a message that doesn't exist would RPC-error earlier + return self._get_response_message(request, result, entity) + + # endregion + + # endregion diff --git a/module/scripts/lib/telethon/client/telegrambaseclient.py b/module/scripts/lib/telethon/client/telegrambaseclient.py new file mode 100644 index 0000000..0f8cf5d --- /dev/null +++ b/module/scripts/lib/telethon/client/telegrambaseclient.py @@ -0,0 +1,979 @@ +import abc +import inspect +import re +import asyncio +import collections +import logging +import platform +import time +import typing +import datetime +import pathlib +import warnings + +from .. import utils, version, helpers, __name__ as __base_name__ +from ..crypto import rsa +from ..extensions import markdown +from ..network import MTProtoSender, Connection, ConnectionTcpFull, TcpMTProxy +from ..sessions import Session, SQLiteSession, MemorySession +from ..tl import functions, types +from ..tl.alltlobjects import LAYER +from .._updates import MessageBox, EntityCache as MbEntityCache, SessionState, ChannelState, Entity, EntityType + +try: + import python_socks +except ImportError: + python_socks = None + +DEFAULT_DC_ID = 2 +DEFAULT_IPV4_IP = '149.154.167.51' +DEFAULT_IPV6_IP = '2001:67c:4e8:f002::a' +DEFAULT_PORT = 443 + +if typing.TYPE_CHECKING: + from .telegramclient import TelegramClient + +_base_log = logging.getLogger(__base_name__) + + +# In seconds, how long to wait before disconnecting a exported sender. +_DISCONNECT_EXPORTED_AFTER = 60 + + +class _ExportState: + def __init__(self): + # ``n`` is the amount of borrows a given sender has; + # once ``n`` reaches ``0``, disconnect the sender after a while. + self._n = 0 + self._zero_ts = 0 + self._connected = False + + def add_borrow(self): + self._n += 1 + self._connected = True + + def add_return(self): + self._n -= 1 + assert self._n >= 0, 'returned sender more than it was borrowed' + if self._n == 0: + self._zero_ts = time.time() + + def should_disconnect(self): + return (self._n == 0 + and self._connected + and (time.time() - self._zero_ts) > _DISCONNECT_EXPORTED_AFTER) + + def need_connect(self): + return not self._connected + + def mark_disconnected(self): + assert self.should_disconnect(), 'marked as disconnected when it was borrowed' + self._connected = False + + +# TODO How hard would it be to support both `trio` and `asyncio`? +class TelegramBaseClient(abc.ABC): + """ + This is the abstract base class for the client. It defines some + basic stuff like connecting, switching data center, etc, and + leaves the `__call__` unimplemented. + + Arguments + session (`str` | `telethon.sessions.abstract.Session`, `None`): + The file name of the session file to be used if a string is + given (it may be a full path), or the Session instance to be + used otherwise. If it's `None`, the session will not be saved, + and you should call :meth:`.log_out()` when you're done. + + Note that if you pass a string it will be a file in the current + working directory, although you can also pass absolute paths. + + The session file contains enough information for you to login + without re-sending the code, so if you have to enter the code + more than once, maybe you're changing the working directory, + renaming or removing the file, or using random names. + + api_id (`int` | `str`): + The API ID you obtained from https://my.telegram.org. + + api_hash (`str`): + The API hash you obtained from https://my.telegram.org. + + connection (`telethon.network.connection.common.Connection`, optional): + The connection instance to be used when creating a new connection + to the servers. It **must** be a type. + + Defaults to `telethon.network.connection.tcpfull.ConnectionTcpFull`. + + use_ipv6 (`bool`, optional): + Whether to connect to the servers through IPv6 or not. + By default this is `False` as IPv6 support is not + too widespread yet. + + proxy (`dict`, optional): + An iterable consisting of the proxy info. If `connection` is + one of `MTProxy`, then it should contain MTProxy credentials: + ``hostname, port, secret``. Otherwise, it's meant to store + function parameters for PySocks, like ``type, hostname, port``. + See https://github.com/romis2012/python-socks for more. + + local_addr (`str` | `tuple`, optional): + Local host address (and port, optionally) used to bind the socket to locally. + You only need to use this if you have multiple network cards and + want to use a specific one. + + timeout (`int` | `float`, optional): + The timeout in seconds to be used when connecting. + This is **not** the timeout to be used when ``await``'ing for + invoked requests, and you should use ``asyncio.wait`` or + ``asyncio.wait_for`` for that. + + request_retries (`int` | `None`, optional): + How many times a request should be retried. Request are retried + when Telegram is having internal issues (due to either + ``errors.ServerError`` or ``errors.RpcCallFailError``), + when there is a ``errors.FloodWaitError`` less than + `flood_sleep_threshold`, or when there's a migrate error. + + May take a negative or `None` value for infinite retries, but + this is not recommended, since some requests can always trigger + a call fail (such as searching for messages). + + connection_retries (`int` | `None`, optional): + How many times the reconnection should retry, either on the + initial connection or when Telegram disconnects us. May be + set to a negative or `None` value for infinite retries, but + this is not recommended, since the program can get stuck in an + infinite loop. + + retry_delay (`int` | `float`, optional): + The delay in seconds to sleep between automatic reconnections. + + auto_reconnect (`bool`, optional): + Whether reconnection should be retried `connection_retries` + times automatically if Telegram disconnects us or not. + + sequential_updates (`bool`, optional): + By default every incoming update will create a new task, so + you can handle several updates in parallel. Some scripts need + the order in which updates are processed to be sequential, and + this setting allows them to do so. + + If set to `True`, incoming updates will be put in a queue + and processed sequentially. This means your event handlers + should *not* perform long-running operations since new + updates are put inside of an unbounded queue. + + flood_sleep_threshold (`int` | `float`, optional): + The threshold below which the library should automatically + sleep on flood wait and slow mode wait errors (inclusive). For instance, if a + ``FloodWaitError`` for 17s occurs and `flood_sleep_threshold` + is 20s, the library will ``sleep`` automatically. If the error + was for 21s, it would ``raise FloodWaitError`` instead. Values + larger than a day (like ``float('inf')``) will be changed to a day. + + raise_last_call_error (`bool`, optional): + When API calls fail in a way that causes Telethon to retry + automatically, should the RPC error of the last attempt be raised + instead of a generic ValueError. This is mostly useful for + detecting when Telegram has internal issues. + + device_model (`str`, optional): + "Device model" to be sent when creating the initial connection. + Defaults to 'PC (n)bit' derived from ``platform.uname().machine``, or its direct value if unknown. + + system_version (`str`, optional): + "System version" to be sent when creating the initial connection. + Defaults to ``platform.uname().release`` stripped of everything ahead of -. + + app_version (`str`, optional): + "App version" to be sent when creating the initial connection. + Defaults to `telethon.version.__version__`. + + lang_code (`str`, optional): + "Language code" to be sent when creating the initial connection. + Defaults to ``'en'``. + + system_lang_code (`str`, optional): + "System lang code" to be sent when creating the initial connection. + Defaults to `lang_code`. + + loop (`asyncio.AbstractEventLoop`, optional): + Asyncio event loop to use. Defaults to `asyncio.get_running_loop()`. + This argument is ignored. + + base_logger (`str` | `logging.Logger`, optional): + Base logger name or instance to use. + If a `str` is given, it'll be passed to `logging.getLogger()`. If a + `logging.Logger` is given, it'll be used directly. If something + else or nothing is given, the default logger will be used. + + receive_updates (`bool`, optional): + Whether the client will receive updates or not. By default, updates + will be received from Telegram as they occur. + + Turning this off means that Telegram will not send updates at all + so event handlers, conversations, and QR login will not work. + However, certain scripts don't need updates, so this will reduce + the amount of bandwidth used. + + entity_cache_limit (`int`, optional): + How many users, chats and channels to keep in the in-memory cache + at most. This limit is checked against when processing updates. + + When this limit is reached or exceeded, all entities that are not + required for update handling will be flushed to the session file. + + Note that this implies that there is a lower bound to the amount + of entities that must be kept in memory. + + Setting this limit too low will cause the library to attempt to + flush entities to the session file even if no entities can be + removed from the in-memory cache, which will degrade performance. + """ + + # Current TelegramClient version + __version__ = version.__version__ + + # Cached server configuration (with .dc_options), can be "global" + _config = None + _cdn_config = None + + # region Initialization + + def __init__( + self: 'TelegramClient', + session: 'typing.Union[str, pathlib.Path, Session]', + api_id: int, + api_hash: str, + *, + connection: 'typing.Type[Connection]' = ConnectionTcpFull, + use_ipv6: bool = False, + proxy: typing.Union[tuple, dict] = None, + local_addr: typing.Union[str, tuple] = None, + timeout: int = 10, + request_retries: int = 5, + connection_retries: int = 5, + retry_delay: int = 1, + auto_reconnect: bool = True, + sequential_updates: bool = False, + flood_sleep_threshold: int = 60, + raise_last_call_error: bool = False, + device_model: str = None, + system_version: str = None, + app_version: str = None, + lang_code: str = 'en', + system_lang_code: str = 'en', + loop: asyncio.AbstractEventLoop = None, + base_logger: typing.Union[str, logging.Logger] = None, + receive_updates: bool = True, + catch_up: bool = False, + entity_cache_limit: int = 5000 + ): + if not api_id or not api_hash: + raise ValueError( + "Your API ID or Hash cannot be empty or None. " + "Refer to telethon.rtfd.io for more information.") + + self._use_ipv6 = use_ipv6 + + if isinstance(base_logger, str): + base_logger = logging.getLogger(base_logger) + elif not isinstance(base_logger, logging.Logger): + base_logger = _base_log + + class _Loggers(dict): + def __missing__(self, key): + if key.startswith("telethon."): + key = key.split('.', maxsplit=1)[1] + + return base_logger.getChild(key) + + self._log = _Loggers() + + # Determine what session object we have + if isinstance(session, (str, pathlib.Path)): + try: + session = SQLiteSession(str(session)) + except ImportError: + warnings.warn( + 'The sqlite3 module is not available under this ' + 'Python installation and no custom session ' + 'instance was given; using MemorySession.\n' + 'You will need to re-login every time unless ' + 'you use another session storage' + ) + session = MemorySession() + elif session is None: + session = MemorySession() + elif not isinstance(session, Session): + raise TypeError( + 'The given session must be a str or a Session instance.' + ) + + self.flood_sleep_threshold = flood_sleep_threshold + + # TODO Use AsyncClassWrapper(session) + # ChatGetter and SenderGetter can use the in-memory _mb_entity_cache + # to avoid network access and the need for await in session files. + # + # The session files only wants the entities to persist + # them to disk, and to save additional useful information. + # TODO Session should probably return all cached + # info of entities, not just the input versions + self.session = session + self.api_id = int(api_id) + self.api_hash = api_hash + + if local_addr is not None: + if use_ipv6 is False and ':' in local_addr: + raise TypeError( + 'A local IPv6 address must only be used with `use_ipv6=True`.' + ) + elif use_ipv6 is True and ':' not in local_addr: + raise TypeError( + '`use_ipv6=True` must only be used with a local IPv6 address.' + ) + + self._raise_last_call_error = raise_last_call_error + + self._request_retries = request_retries + self._connection_retries = connection_retries + self._retry_delay = retry_delay or 0 + self._proxy = proxy + self._local_addr = local_addr + self._timeout = timeout + self._auto_reconnect = auto_reconnect + + if proxy and not python_socks: + warnings.warn('proxy argument will be ignored because python-socks is not installed') + + assert isinstance(connection, type) + self._connection = connection + init_proxy = None if not issubclass(connection, TcpMTProxy) else \ + types.InputClientProxy(*connection.address_info(proxy)) + + # Used on connection. Capture the variables in a lambda since + # exporting clients need to create this InvokeWithLayerRequest. + system = platform.uname() + + if system.machine in ('x86_64', 'AMD64'): + default_device_model = 'PC 64bit' + elif system.machine in ('i386','i686','x86'): + default_device_model = 'PC 32bit' + else: + default_device_model = system.machine + default_system_version = re.sub(r'-.+','',system.release) + + self._init_request = functions.InitConnectionRequest( + api_id=self.api_id, + device_model=device_model or default_device_model or 'Unknown', + system_version=system_version or default_system_version or '1.0', + app_version=app_version or self.__version__, + lang_code=lang_code, + system_lang_code=system_lang_code, + lang_pack='', # "langPacks are for official apps only" + query=None, + proxy=init_proxy + ) + + # Remember flood-waited requests to avoid making them again + self._flood_waited_requests = {} + + # Cache ``{dc_id: (_ExportState, MTProtoSender)}`` for all borrowed senders + self._borrowed_senders = {} + self._borrow_sender_lock = asyncio.Lock() + self._exported_sessions = {} + + self._loop = None # only used as a sanity check + self._updates_error = None + self._updates_handle = None + self._keepalive_handle = None + self._last_request = time.time() + self._no_updates = not receive_updates + + # Used for non-sequential updates, in order to terminate all pending tasks on disconnect. + self._sequential_updates = sequential_updates + self._event_handler_tasks = set() + + self._authorized = None # None = unknown, False = no, True = yes + + # Some further state for subclasses + self._event_builders = [] + + # {chat_id: {Conversation}} + self._conversations = collections.defaultdict(set) + + # Hack to workaround the fact Telegram may send album updates as + # different Updates when being sent from a different data center. + # {grouped_id: AlbumHack} + # + # FIXME: We don't bother cleaning this up because it's not really + # worth it, albums are pretty rare and this only holds them + # for a second at most. + self._albums = {} + + # Default parse mode + self._parse_mode = markdown + + # Some fields to easy signing in. Let {phone: hash} be + # a dictionary because the user may change their mind. + self._phone_code_hash = {} + self._phone = None + self._tos = None + + # A place to store if channels are a megagroup or not (see `edit_admin`) + self._megagroup_cache = {} + + # This is backported from v2 in a very ad-hoc way just to get proper update handling + self._catch_up = catch_up + self._updates_queue = asyncio.Queue() + self._message_box = MessageBox(self._log['messagebox']) + self._mb_entity_cache = MbEntityCache() # required for proper update handling (to know when to getDifference) + self._entity_cache_limit = entity_cache_limit + + self._sender = MTProtoSender( + self.session.auth_key, + loggers=self._log, + retries=self._connection_retries, + delay=self._retry_delay, + auto_reconnect=self._auto_reconnect, + connect_timeout=self._timeout, + auth_key_callback=self._auth_key_callback, + updates_queue=self._updates_queue, + auto_reconnect_callback=self._handle_auto_reconnect + ) + + + # endregion + + # region Properties + + @property + def loop(self: 'TelegramClient') -> asyncio.AbstractEventLoop: + """ + Property with the ``asyncio`` event loop used by this client. + + Example + .. code-block:: python + + # Download media in the background + task = client.loop.create_task(message.download_media()) + + # Do some work + ... + + # Join the task (wait for it to complete) + await task + """ + return helpers.get_running_loop() + + @property + def disconnected(self: 'TelegramClient') -> asyncio.Future: + """ + Property with a ``Future`` that resolves upon disconnection. + + Example + .. code-block:: python + + # Wait for a disconnection to occur + try: + await client.disconnected + except OSError: + print('Error on disconnect') + """ + return self._sender.disconnected + + @property + def flood_sleep_threshold(self): + return self._flood_sleep_threshold + + @flood_sleep_threshold.setter + def flood_sleep_threshold(self, value): + # None -> 0, negative values don't really matter + self._flood_sleep_threshold = min(value or 0, 24 * 60 * 60) + + # endregion + + # region Connecting + + async def connect(self: 'TelegramClient') -> None: + """ + Connects to Telegram. + + .. note:: + + Connect means connect and nothing else, and only one low-level + request is made to notify Telegram about which layer we will be + using. + + Before Telegram sends you updates, you need to make a high-level + request, like `client.get_me() `, + as described in https://core.telegram.org/api/updates. + + Example + .. code-block:: python + + try: + await client.connect() + except OSError: + print('Failed to connect') + """ + if self.session is None: + raise ValueError('TelegramClient instance cannot be reused after logging out') + + if self._loop is None: + self._loop = helpers.get_running_loop() + elif self._loop != helpers.get_running_loop(): + raise RuntimeError('The asyncio event loop must not change after connection (see the FAQ for details)') + + # Current proxy implementation requires `sock_connect`, and some + # event loops lack this method. If the current loop is missing it, + # bail out early and suggest an alternative. + # + # TODO A better fix is obviously avoiding the use of `sock_connect` + # + # See https://github.com/LonamiWebs/Telethon/issues/1337 for details. + if not callable(getattr(self._loop, 'sock_connect', None)): + raise TypeError( + 'Event loop of type {} lacks `sock_connect`, which is needed to use proxies.\n\n' + 'Change the event loop in use to use proxies:\n' + '# https://github.com/LonamiWebs/Telethon/issues/1337\n' + 'import asyncio\n' + 'asyncio.set_event_loop(asyncio.SelectorEventLoop())'.format( + self._loop.__class__.__name__ + ) + ) + + # ':' in session.server_address is True if it's an IPv6 address + if (not self.session.server_address or + (':' in self.session.server_address) != self._use_ipv6): + await utils.maybe_async( + self.session.set_dc( + DEFAULT_DC_ID, + DEFAULT_IPV6_IP if self._use_ipv6 else DEFAULT_IPV4_IP, + DEFAULT_PORT + ) + ) + await utils.maybe_async(self.session.save()) + + if not await self._sender.connect(self._connection( + self.session.server_address, + self.session.port, + self.session.dc_id, + loggers=self._log, + proxy=self._proxy, + local_addr=self._local_addr + )): + # We don't want to init or modify anything if we were already connected + return + + self.session.auth_key = self._sender.auth_key + await utils.maybe_async(self.session.save()) + + try: + # See comment when saving entities to understand this hack + self_entity = await utils.maybe_async(self.session.get_input_entity(0)) + self_id = self_entity.access_hash + self_user = await utils.maybe_async(self.session.get_input_entity(self_id)) + self._mb_entity_cache.set_self_user(self_id, None, self_user.access_hash) + except ValueError: + pass + + if self._catch_up: + ss = SessionState(0, 0, False, 0, 0, 0, 0, None) + cs = [] + + update_states = await utils.maybe_async(self.session.get_update_states()) + for entity_id, state in update_states: + if entity_id == 0: + # TODO current session doesn't store self-user info but adding that is breaking on downstream session impls + ss = SessionState(0, 0, False, state.pts, state.qts, int(state.date.timestamp()), state.seq, None) + else: + cs.append(ChannelState(entity_id, state.pts)) + + self._message_box.load(ss, cs) + for state in cs: + try: + entity = await utils.maybe_async(self.session.get_input_entity(state.channel_id)) + except ValueError: + self._log[__name__].warning( + 'No access_hash in cache for channel %s, will not catch up', state.channel_id) + else: + self._mb_entity_cache.put(Entity(EntityType.CHANNEL, entity.channel_id, entity.access_hash)) + + self._init_request.query = functions.help.GetConfigRequest() + + req = self._init_request + if self._no_updates: + req = functions.InvokeWithoutUpdatesRequest(req) + + await self._sender.send(functions.InvokeWithLayerRequest(LAYER, req)) + + if self._message_box.is_empty(): + me = await self.get_me() + if me: + await self._on_login(me) # also calls GetState to initialize the MessageBox + + self._updates_handle = self.loop.create_task(self._update_loop()) + self._keepalive_handle = self.loop.create_task(self._keepalive_loop()) + + def is_connected(self: 'TelegramClient') -> bool: + """ + Returns `True` if the user has connected. + + This method is **not** asynchronous (don't use ``await`` on it). + + Example + .. code-block:: python + + while client.is_connected(): + await asyncio.sleep(1) + """ + sender = getattr(self, '_sender', None) + return sender and sender.is_connected() + + def disconnect(self: 'TelegramClient'): + """ + Disconnects from Telegram. + + If the event loop is already running, this method returns a + coroutine that you should await on your own code; otherwise + the loop is ran until said coroutine completes. + + Event handlers which are currently running will be cancelled before + this function returns (in order to properly clean-up their tasks). + In particular, this means that using ``disconnect`` in a handler + will cause code after the ``disconnect`` to never run. If this is + needed, consider spawning a separate task to do the remaining work. + + Example + .. code-block:: python + + # You don't need to use this if you used "with client" + await client.disconnect() + """ + if self.loop.is_running(): + # Disconnect may be called from an event handler, which would + # cancel itself during itself and never actually complete the + # disconnection. Shield the task to prevent disconnect itself + # from being cancelled. See issue #3942 for more details. + return asyncio.shield(self.loop.create_task(self._disconnect_coro())) + else: + try: + self.loop.run_until_complete(self._disconnect_coro()) + except RuntimeError: + # Python 3.5.x complains when called from + # `__aexit__` and there were pending updates with: + # "Event loop stopped before Future completed." + # + # However, it doesn't really make a lot of sense. + pass + + def set_proxy(self: 'TelegramClient', proxy: typing.Union[tuple, dict]): + """ + Changes the proxy which will be used on next (re)connection. + + Method has no immediate effects if the client is currently connected. + + The new proxy will take it's effect on the next reconnection attempt: + - on a call `await client.connect()` (after complete disconnect) + - on auto-reconnect attempt (e.g, after previous connection was lost) + """ + init_proxy = None if not issubclass(self._connection, TcpMTProxy) else \ + types.InputClientProxy(*self._connection.address_info(proxy)) + + self._init_request.proxy = init_proxy + self._proxy = proxy + + # While `await client.connect()` passes new proxy on each new call, + # auto-reconnect attempts use already set up `_connection` inside + # the `_sender`, so the only way to change proxy between those + # is to directly inject parameters. + + connection = getattr(self._sender, "_connection", None) + if connection: + if isinstance(connection, TcpMTProxy): + connection._ip = proxy[0] + connection._port = proxy[1] + else: + connection._proxy = proxy + + async def _save_states_and_entities(self: 'TelegramClient'): + # As a hack to not need to change the session files, save ourselves with ``id=0`` and ``access_hash`` of our ``id``. + # This way it is possible to determine our own ID by querying for 0. However, whether we're a bot is not saved. + # Piggy-back on an arbitrary TL type with users and chats so the session can understand to read the entities. + # It doesn't matter if we put users in the list of chats. + if self._mb_entity_cache.self_id: + await utils.maybe_async( + self.session.process_entities( + types.contacts.ResolvedPeer(None, [types.InputPeerUser(0, self._mb_entity_cache.self_id)], []) + ) + ) + + ss, cs = self._message_box.session_state() + await utils.maybe_async(self.session.set_update_state(0, types.updates.State(**ss, unread_count=0))) + now = datetime.datetime.now() # any datetime works; channels don't need it + for channel_id, pts in cs.items(): + await utils.maybe_async( + self.session.set_update_state( + channel_id, types.updates.State(pts, 0, now, 0, unread_count=0) + ) + ) + + async def _disconnect_coro(self: 'TelegramClient'): + if self.session is None: + return # already logged out and disconnected + + await self._disconnect() + + # Also clean-up all exported senders because we're done with them + async with self._borrow_sender_lock: + for state, sender in self._borrowed_senders.values(): + # Note that we're not checking for `state.should_disconnect()`. + # If the user wants to disconnect the client, ALL connections + # to Telegram (including exported senders) should be closed. + # + # Disconnect should never raise, so there's no try/except. + await sender.disconnect() + # Can't use `mark_disconnected` because it may be borrowed. + state._connected = False + + # If any was borrowed + self._borrowed_senders.clear() + + # trio's nurseries would handle this for us, but this is asyncio. + # All tasks spawned in the background should properly be terminated. + if self._event_handler_tasks: + for task in self._event_handler_tasks: + task.cancel() + + await asyncio.wait(self._event_handler_tasks) + self._event_handler_tasks.clear() + + await self._save_states_and_entities() + + await utils.maybe_async(self.session.close()) + + async def _disconnect(self: 'TelegramClient'): + """ + Disconnect only, without closing the session. Used in reconnections + to different data centers, where we don't want to close the session + file; user disconnects however should close it since it means that + their job with the client is complete and we should clean it up all. + """ + await self._sender.disconnect() + await helpers._cancel(self._log[__name__], + updates_handle=self._updates_handle, + keepalive_handle=self._keepalive_handle) + + async def _switch_dc(self: 'TelegramClient', new_dc): + """ + Permanently switches the current connection to the new data center. + """ + self._log[__name__].info('Reconnecting to new data center %s', new_dc) + dc = await self._get_dc(new_dc) + + await utils.maybe_async(self.session.set_dc(dc.id, dc.ip_address, dc.port)) + # auth_key's are associated with a server, which has now changed + # so it's not valid anymore. Set to None to force recreating it. + self._sender.auth_key.key = None + self.session.auth_key = None + await utils.maybe_async(self.session.save()) + await self._disconnect() + return await self.connect() + + async def _auth_key_callback(self: 'TelegramClient', auth_key): + """ + Callback from the sender whenever it needed to generate a + new authorization key. This means we are not authorized. + """ + self.session.auth_key = auth_key + await utils.maybe_async(self.session.save()) + + # endregion + + # region Working with different connections/Data Centers + + async def _get_dc(self: 'TelegramClient', dc_id, cdn=False): + """Gets the Data Center (DC) associated to 'dc_id'""" + cls = self.__class__ + if not cls._config: + cls._config = await self(functions.help.GetConfigRequest()) + + if cdn and not self._cdn_config: + cls._cdn_config = await self(functions.help.GetCdnConfigRequest()) + for pk in cls._cdn_config.public_keys: + if pk.dc_id == dc_id: + rsa.add_key(pk.public_key, old=False) + + try: + return next( + dc for dc in cls._config.dc_options + if dc.id == dc_id + and bool(dc.ipv6) == self._use_ipv6 and bool(dc.cdn) == cdn + ) + except StopIteration: + self._log[__name__].warning( + 'Failed to get DC %s (cdn = %s) with use_ipv6 = %s; retrying ignoring IPv6 check', + dc_id, cdn, self._use_ipv6 + ) + try: + return next( + dc for dc in cls._config.dc_options + if dc.id == dc_id and bool(dc.cdn) == cdn + ) + except StopIteration: + raise ValueError(f'Failed to get DC {dc_id} (cdn = {cdn})') + + async def _create_exported_sender(self: 'TelegramClient', dc_id): + """ + Creates a new exported `MTProtoSender` for the given `dc_id` and + returns it. This method should be used by `_borrow_exported_sender`. + """ + # Thanks badoualy/kotlogram on /telegram/api/DefaultTelegramClient.kt + # for clearly showing how to export the authorization + dc = await self._get_dc(dc_id) + # Can't reuse self._sender._connection as it has its own seqno. + # + # If one were to do that, Telegram would reset the connection + # with no further clues. + sender = MTProtoSender(None, loggers=self._log) + await sender.connect(self._connection( + dc.ip_address, + dc.port, + dc.id, + loggers=self._log, + proxy=self._proxy, + local_addr=self._local_addr + )) + self._log[__name__].info('Exporting auth for new borrowed sender in %s', dc) + auth = await self(functions.auth.ExportAuthorizationRequest(dc_id)) + self._init_request.query = functions.auth.ImportAuthorizationRequest(id=auth.id, bytes=auth.bytes) + req = functions.InvokeWithLayerRequest(LAYER, self._init_request) + await sender.send(req) + return sender + + async def _borrow_exported_sender(self: 'TelegramClient', dc_id): + """ + Borrows a connected `MTProtoSender` for the given `dc_id`. + If it's not cached, creates a new one if it doesn't exist yet, + and imports a freshly exported authorization key for it to be usable. + + Once its job is over it should be `_return_exported_sender`. + """ + async with self._borrow_sender_lock: + self._log[__name__].debug('Borrowing sender for dc_id %d', dc_id) + state, sender = self._borrowed_senders.get(dc_id, (None, None)) + + if state is None: + state = _ExportState() + sender = await self._create_exported_sender(dc_id) + sender.dc_id = dc_id + self._borrowed_senders[dc_id] = (state, sender) + + elif state.need_connect(): + dc = await self._get_dc(dc_id) + await sender.connect(self._connection( + dc.ip_address, + dc.port, + dc.id, + loggers=self._log, + proxy=self._proxy, + local_addr=self._local_addr + )) + + state.add_borrow() + return sender + + async def _return_exported_sender(self: 'TelegramClient', sender): + """ + Returns a borrowed exported sender. If all borrows have + been returned, the sender is cleanly disconnected. + """ + async with self._borrow_sender_lock: + self._log[__name__].debug('Returning borrowed sender for dc_id %d', sender.dc_id) + state, _ = self._borrowed_senders[sender.dc_id] + state.add_return() + + async def _clean_exported_senders(self: 'TelegramClient'): + """ + Cleans-up all unused exported senders by disconnecting them. + """ + async with self._borrow_sender_lock: + for dc_id, (state, sender) in self._borrowed_senders.items(): + if state.should_disconnect(): + self._log[__name__].info( + 'Disconnecting borrowed sender for DC %d', dc_id) + + # Disconnect should never raise + await sender.disconnect() + state.mark_disconnected() + + async def _get_cdn_client(self: 'TelegramClient', cdn_redirect): + """Similar to ._borrow_exported_client, but for CDNs""" + session = self._exported_sessions.get(cdn_redirect.dc_id) + if not session: + dc = await self._get_dc(cdn_redirect.dc_id, cdn=True) + session = await utils.maybe_async(self.session.clone()) + await utils.maybe_async(session.set_dc(dc.id, dc.ip_address, dc.port)) + self._exported_sessions[cdn_redirect.dc_id] = session + + self._log[__name__].info('Creating new CDN client') + client = self.__class__( + session, self.api_id, self.api_hash, + proxy=self._proxy, + timeout=self._timeout, + loop=self.loop + ) + + session.auth_key = self._sender.auth_key + await client._sender.connect(self._connection( + session.server_address, + session.port, + session.dc_id, + loggers=self._log, + proxy=self._proxy, + local_addr=self._local_addr + )) + return client + + # endregion + + # region Invoking Telegram requests + + @abc.abstractmethod + def __call__(self: 'TelegramClient', request, ordered=False): + """ + Invokes (sends) one or more MTProtoRequests and returns (receives) + their result. + + Args: + request (`TLObject` | `list`): + The request or requests to be invoked. + + ordered (`bool`, optional): + Whether the requests (if more than one was given) should be + executed sequentially on the server. They run in arbitrary + order by default. + + flood_sleep_threshold (`int` | `None`, optional): + The flood sleep threshold to use for this request. This overrides + the default value stored in + `client.flood_sleep_threshold ` + + Returns: + The result of the request (often a `TLObject`) or a list of + results if more than one request was given. + """ + raise NotImplementedError + + @abc.abstractmethod + def _update_loop(self: 'TelegramClient'): + raise NotImplementedError + + @abc.abstractmethod + async def _handle_auto_reconnect(self: 'TelegramClient'): + raise NotImplementedError + + # endregion diff --git a/module/scripts/lib/telethon/client/telegramclient.py b/module/scripts/lib/telethon/client/telegramclient.py new file mode 100644 index 0000000..144a6b2 --- /dev/null +++ b/module/scripts/lib/telethon/client/telegramclient.py @@ -0,0 +1,13 @@ +from . import ( + AccountMethods, AuthMethods, DownloadMethods, DialogMethods, ChatMethods, + BotMethods, MessageMethods, UploadMethods, ButtonMethods, UpdateMethods, + MessageParseMethods, UserMethods, TelegramBaseClient +) + + +class TelegramClient( + AccountMethods, AuthMethods, DownloadMethods, DialogMethods, ChatMethods, + BotMethods, MessageMethods, UploadMethods, ButtonMethods, UpdateMethods, + MessageParseMethods, UserMethods, TelegramBaseClient +): + pass diff --git a/module/scripts/lib/telethon/client/updates.py b/module/scripts/lib/telethon/client/updates.py new file mode 100644 index 0000000..ead9f94 --- /dev/null +++ b/module/scripts/lib/telethon/client/updates.py @@ -0,0 +1,733 @@ +import asyncio +import inspect +import itertools +import random +import sys +import time +import traceback +import typing +import logging +import warnings +from collections import deque + +try: + import sqlite3 + + OperationalError = sqlite3.OperationalError +except ImportError as e: + sqlite3 = None + + class OperationalError(Exception): + pass # won't be created and thus never caught + +from .. import events, utils, errors +from ..events.common import EventBuilder, EventCommon +from ..tl import types, functions +from .._updates import GapError, PrematureEndReason +from ..helpers import get_running_loop +from ..version import __version__ + + +if typing.TYPE_CHECKING: + from .telegramclient import TelegramClient + + +Callback = typing.Callable[[typing.Any], typing.Any] + +class UpdateMethods: + + # region Public methods + + async def _run_until_disconnected(self: 'TelegramClient'): + try: + # Make a high-level request to notify that we want updates + await self(functions.updates.GetStateRequest()) + result = await self.disconnected + if self._updates_error is not None: + raise self._updates_error + return result + except KeyboardInterrupt: + pass + finally: + await self.disconnect() + + async def set_receive_updates(self: 'TelegramClient', receive_updates): + """ + Change the value of `receive_updates`. + + This is an `async` method, because in order for Telegram to start + sending updates again, a request must be made. + """ + self._no_updates = not receive_updates + if receive_updates: + await self(functions.updates.GetStateRequest()) + + def run_until_disconnected(self: 'TelegramClient'): + """ + Runs the event loop until the library is disconnected. + + It also notifies Telegram that we want to receive updates + as described in https://core.telegram.org/api/updates. + If an unexpected error occurs during update handling, + the client will disconnect and said error will be raised. + + Manual disconnections can be made by calling `disconnect() + ` + or sending a ``KeyboardInterrupt`` (e.g. by pressing ``Ctrl+C`` on + the console window running the script). + + If a disconnection error occurs (i.e. the library fails to reconnect + automatically), said error will be raised through here, so you have a + chance to ``except`` it on your own code. + + If the loop is already running, this method returns a coroutine + that you should await on your own code. + + .. note:: + + If you want to handle ``KeyboardInterrupt`` in your code, + simply run the event loop in your code too in any way, such as + ``loop.run_forever()`` or ``await client.disconnected`` (e.g. + ``loop.run_until_complete(client.disconnected)``). + + Example + .. code-block:: python + + # Blocks the current task here until a disconnection occurs. + # + # You will still receive updates, since this prevents the + # script from exiting. + await client.run_until_disconnected() + """ + if self.loop.is_running(): + return self._run_until_disconnected() + try: + return self.loop.run_until_complete(self._run_until_disconnected()) + except KeyboardInterrupt: + pass + finally: + # No loop.run_until_complete; it's already syncified + self.disconnect() + + def on(self: 'TelegramClient', event: EventBuilder): + """ + Decorator used to `add_event_handler` more conveniently. + + + Arguments + event (`_EventBuilder` | `type`): + The event builder class or instance to be used, + for instance ``events.NewMessage``. + + Example + .. code-block:: python + + from telethon import TelegramClient, events + client = TelegramClient(...) + + # Here we use client.on + @client.on(events.NewMessage) + async def handler(event): + ... + """ + def decorator(f): + self.add_event_handler(f, event) + return f + + return decorator + + def add_event_handler( + self: 'TelegramClient', + callback: Callback, + event: EventBuilder = None): + """ + Registers a new event handler callback. + + The callback will be called when the specified event occurs. + + Arguments + callback (`callable`): + The callable function accepting one parameter to be used. + + Note that if you have used `telethon.events.register` in + the callback, ``event`` will be ignored, and instead the + events you previously registered will be used. + + event (`_EventBuilder` | `type`, optional): + The event builder class or instance to be used, + for instance ``events.NewMessage``. + + If left unspecified, `telethon.events.raw.Raw` (the + :tl:`Update` objects with no further processing) will + be passed instead. + + Example + .. code-block:: python + + from telethon import TelegramClient, events + client = TelegramClient(...) + + async def handler(event): + ... + + client.add_event_handler(handler, events.NewMessage) + """ + builders = events._get_handlers(callback) + if builders is not None: + for event in builders: + self._event_builders.append((event, callback)) + return + + if isinstance(event, type): + event = event() + elif not event: + event = events.Raw() + + self._event_builders.append((event, callback)) + + def remove_event_handler( + self: 'TelegramClient', + callback: Callback, + event: EventBuilder = None) -> int: + """ + Inverse operation of `add_event_handler()`. + + If no event is given, all events for this callback are removed. + Returns how many callbacks were removed. + + Example + .. code-block:: python + + @client.on(events.Raw) + @client.on(events.NewMessage) + async def handler(event): + ... + + # Removes only the "Raw" handling + # "handler" will still receive "events.NewMessage" + client.remove_event_handler(handler, events.Raw) + + # "handler" will stop receiving anything + client.remove_event_handler(handler) + """ + found = 0 + if event and not isinstance(event, type): + event = type(event) + + i = len(self._event_builders) + while i: + i -= 1 + ev, cb = self._event_builders[i] + if cb == callback and (not event or isinstance(ev, event)): + del self._event_builders[i] + found += 1 + + return found + + def list_event_handlers(self: 'TelegramClient')\ + -> 'typing.Sequence[typing.Tuple[Callback, EventBuilder]]': + """ + Lists all registered event handlers. + + Returns + A list of pairs consisting of ``(callback, event)``. + + Example + .. code-block:: python + + @client.on(events.NewMessage(pattern='hello')) + async def on_greeting(event): + '''Greets someone''' + await event.reply('Hi') + + for callback, event in client.list_event_handlers(): + print(id(callback), type(event)) + """ + return [(callback, event) for event, callback in self._event_builders] + + async def catch_up(self: 'TelegramClient'): + """ + "Catches up" on the missed updates while the client was offline. + You should call this method after registering the event handlers + so that the updates it loads can by processed by your script. + + This can also be used to forcibly fetch new updates if there are any. + + Example + .. code-block:: python + + await client.catch_up() + """ + await self._updates_queue.put(types.UpdatesTooLong()) + + # endregion + + # region Private methods + + async def _update_loop(self: 'TelegramClient'): + # If the MessageBox is not empty, the account had to be logged-in to fill in its state. + # This flag is used to propagate the "you got logged-out" error up (but getting logged-out + # can only happen if it was once logged-in). + was_once_logged_in = self._authorized is True or not self._message_box.is_empty() + + self._updates_error = None + try: + if self._catch_up: + # User wants to catch up as soon as the client is up and running, + # so this is the best place to do it. + await self.catch_up() + + updates_to_dispatch = deque() + + while self.is_connected(): + if updates_to_dispatch: + if self._sequential_updates: + await self._dispatch_update(updates_to_dispatch.popleft()) + else: + while updates_to_dispatch: + # TODO if _dispatch_update fails for whatever reason, it's not logged! this should be fixed + task = self.loop.create_task(self._dispatch_update(updates_to_dispatch.popleft())) + self._event_handler_tasks.add(task) + task.add_done_callback(self._event_handler_tasks.discard) + + continue + + if len(self._mb_entity_cache) >= self._entity_cache_limit: + self._log[__name__].info( + 'In-memory entity cache limit reached (%s/%s), flushing to session', + len(self._mb_entity_cache), + self._entity_cache_limit + ) + await self._save_states_and_entities() + self._mb_entity_cache.retain(lambda id: id == self._mb_entity_cache.self_id or id in self._message_box.map) + if len(self._mb_entity_cache) >= self._entity_cache_limit: + warnings.warn('in-memory entities exceed entity_cache_limit after flushing; consider setting a larger limit') + + self._log[__name__].info( + 'In-memory entity cache at %s/%s after flushing to session', + len(self._mb_entity_cache), + self._entity_cache_limit + ) + + + get_diff = self._message_box.get_difference() + if get_diff: + self._log[__name__].debug('Getting difference for account updates') + try: + diff = await self(get_diff) + except ( + errors.ServerError, + errors.TimedOutError, + errors.FloodWaitError, + ValueError + ) as e: + # Telegram is having issues + self._log[__name__].info('Cannot get difference since Telegram is having issues: %s', type(e).__name__) + self._message_box.end_difference() + continue + except (errors.UnauthorizedError, errors.AuthKeyError) as e: + # Not logged in or broken authorization key, can't get difference + self._log[__name__].info('Cannot get difference since the account is not logged in: %s', type(e).__name__) + self._message_box.end_difference() + if was_once_logged_in: + self._updates_error = e + await self.disconnect() + break + continue + except (errors.TypeNotFoundError, OperationalError) as e: + # User is likely doing weird things with their account or session and Telegram gets confused as to what layer they use + self._log[__name__].warning('Cannot get difference since the account is likely misusing the session: %s', e) + self._message_box.end_difference() + self._updates_error = e + await self.disconnect() + break + except errors.RPCError as e: + # Fallback; treat as transient error (the amount of "fatal errors" reported seem to indicate this is most likely what we need to do) + self._log[__name__].warning( + "Cannot get difference due to unexpected error (this may be a bug " + f"in Telethon v{__version__} in that it could be handled better, but it's unlikely): %s", + e + ) + self._message_box.end_difference() + continue + except OSError as e: + # Network is likely down, but it's unclear for how long. + # If disconnect is called this task will be cancelled along with the sleep. + # If disconnect is not called, getting difference should be retried after a few seconds. + self._log[__name__].info('Cannot get difference since the network is down: %s: %s', type(e).__name__, e) + await asyncio.sleep(5) + continue + updates, users, chats = self._message_box.apply_difference(diff, self._mb_entity_cache) + if updates: + self._log[__name__].info('Got difference for account updates') + + _preprocess_updates = await self._preprocess_updates(updates, users, chats) + updates_to_dispatch.extend(_preprocess_updates) + continue + + get_diff = self._message_box.get_channel_difference(self._mb_entity_cache) + if get_diff: + self._log[__name__].debug('Getting difference for channel %s updates', get_diff.channel.channel_id) + try: + diff = await self(get_diff) + except (errors.UnauthorizedError, errors.AuthKeyError) as e: + # Not logged in or broken authorization key, can't get difference + self._log[__name__].warning( + 'Cannot get difference for channel %s since the account is not logged in: %s', + get_diff.channel.channel_id, type(e).__name__ + ) + self._message_box.end_channel_difference( + get_diff, + PrematureEndReason.TEMPORARY_SERVER_ISSUES, + self._mb_entity_cache + ) + if was_once_logged_in: + self._updates_error = e + await self.disconnect() + break + continue + except (errors.TypeNotFoundError, OperationalError) as e: + self._log[__name__].warning( + 'Cannot get difference for channel %s since the account is likely misusing the session: %s', + get_diff.channel.channel_id, e + ) + self._message_box.end_channel_difference( + get_diff, + PrematureEndReason.TEMPORARY_SERVER_ISSUES, + self._mb_entity_cache + ) + self._updates_error = e + await self.disconnect() + break + except ( + errors.PersistentTimestampOutdatedError, + errors.PersistentTimestampInvalidError, + errors.ServerError, + errors.TimedOutError, + errors.FloodWaitError, + ValueError + ) as e: + # According to Telegram's docs: + # "Channel internal replication issues, try again later (treat this like an RPC_CALL_FAIL)." + # We can treat this as "empty difference" and not update the local pts. + # Then this same call will be retried when another gap is detected or timeout expires. + # + # Another option would be to literally treat this like an RPC_CALL_FAIL and retry after a few + # seconds, but if Telegram is having issues it's probably best to wait for it to send another + # update (hinting it may be okay now) and retry then. + # + # This is a bit hacky because MessageBox doesn't really have a way to "not update" the pts. + # Instead we manually extract the previously-known pts and use that. + # + # For PersistentTimestampInvalidError: + # Somehow our pts is either too new or the server does not know about this. + # We treat this as PersistentTimestampOutdatedError for now. + # TODO investigate why/when this happens and if this is the proper solution + self._log[__name__].warning( + 'Getting difference for channel updates %s caused %s;' + ' ending getting difference prematurely until server issues are resolved', + get_diff.channel.channel_id, type(e).__name__ + ) + self._message_box.end_channel_difference( + get_diff, + PrematureEndReason.TEMPORARY_SERVER_ISSUES, + self._mb_entity_cache + ) + continue + except (errors.ChannelPrivateError, errors.ChannelInvalidError): + # Timeout triggered a get difference, but we have been banned in the channel since then. + # Because we can no longer fetch updates from this channel, we should stop keeping track + # of it entirely. + self._log[__name__].info( + 'Account is now banned in %d so we can no longer fetch updates from it', + get_diff.channel.channel_id + ) + self._message_box.end_channel_difference( + get_diff, + PrematureEndReason.BANNED, + self._mb_entity_cache + ) + continue + except errors.RPCError as e: + # Fallback; treat as transient error (the amount of "fatal errors" reported seem to indicate this is most likely what we need to do) + self._log[__name__].warning( + "Cannot get difference for channel %d due to unexpected error (this may be a bug " + f"in Telethon v{__version__} in that it could be handled better, but it's unlikely): %s", + get_diff.channel.channel_id, e + ) + self._message_box.end_channel_difference( + get_diff, + PrematureEndReason.TEMPORARY_SERVER_ISSUES, + self._mb_entity_cache + ) + continue + except OSError as e: + self._log[__name__].info( + 'Cannot get difference for channel %d since the network is down: %s: %s', + get_diff.channel.channel_id, type(e).__name__, e + ) + await asyncio.sleep(5) + continue + + updates, users, chats = self._message_box.apply_channel_difference(get_diff, diff, self._mb_entity_cache) + if updates: + self._log[__name__].info('Got difference for channel %d updates', get_diff.channel.channel_id) + + _preprocess_updates = await self._preprocess_updates(updates, users, chats) + updates_to_dispatch.extend(_preprocess_updates) + continue + + deadline = self._message_box.check_deadlines() + deadline_delay = deadline - get_running_loop().time() + if deadline_delay > 0: + # Don't bother sleeping and timing out if the delay is already 0 (pollutes the logs). + try: + updates = await asyncio.wait_for(self._updates_queue.get(), deadline_delay) + except asyncio.TimeoutError: + self._log[__name__].debug('Timeout waiting for updates expired') + continue + else: + continue + + processed = [] + try: + users, chats = self._message_box.process_updates(updates, self._mb_entity_cache, processed) + except GapError: + continue # get(_channel)_difference will start returning requests + + _preprocess_updates = await self._preprocess_updates(processed, users, chats) + updates_to_dispatch.extend(_preprocess_updates) + except asyncio.CancelledError: + pass + except Exception as e: + self._log[__name__].exception(f'Fatal error handling updates (this is a bug in Telethon v{__version__}, please report it)') + self._updates_error = e + await self.disconnect() + + async def _preprocess_updates(self, updates, users, chats): + self._mb_entity_cache.extend(users, chats) + await utils.maybe_async(self.session.process_entities(types.contacts.ResolvedPeer(None, users, chats))) + entities = {utils.get_peer_id(x): x + for x in itertools.chain(users, chats)} + for u in updates: + u._entities = entities + return updates + + async def _keepalive_loop(self: 'TelegramClient'): + # Pings' ID don't really need to be secure, just "random" + rnd = lambda: random.randrange(-2**63, 2**63) + while self.is_connected(): + try: + await asyncio.wait_for( + self.disconnected, timeout=60 + ) + continue # We actually just want to act upon timeout + except asyncio.TimeoutError: + pass + except asyncio.CancelledError: + return + except Exception: + continue # Any disconnected exception should be ignored + + # Check if we have any exported senders to clean-up periodically + await self._clean_exported_senders() + + # Don't bother sending pings until the low-level connection is + # ready, otherwise a lot of pings will be batched to be sent upon + # reconnect, when we really don't care about that. + if not self._sender._transport_connected(): + continue + + # We also don't really care about their result. + # Just send them periodically. + try: + self._sender._keepalive_ping(rnd()) + except (ConnectionError, asyncio.CancelledError): + return + + # Entities and cached files are not saved when they are + # inserted because this is a rather expensive operation + # (default's sqlite3 takes ~0.1s to commit changes). Do + # it every minute instead. No-op if there's nothing new. + await self._save_states_and_entities() + + await utils.maybe_async(self.session.save()) + + async def _dispatch_update(self: 'TelegramClient', update): + # TODO only used for AlbumHack, and MessageBox is not really designed for this + others = None + + if not self._mb_entity_cache.self_id: + # Some updates require our own ID, so we must make sure + # that the event builder has offline access to it. Calling + # `get_me()` will cache it under `self._mb_entity_cache`. + # + # It will return `None` if we haven't logged in yet which is + # fine, we will just retry next time anyway. + try: + await self.get_me(input_peer=True) + except OSError: + pass # might not have connection + + built = EventBuilderDict(self, update, others) + for conv_set in self._conversations.values(): + for conv in conv_set: + ev = built[events.NewMessage] + if ev: + conv._on_new_message(ev) + + ev = built[events.MessageEdited] + if ev: + conv._on_edit(ev) + + ev = built[events.MessageRead] + if ev: + conv._on_read(ev) + + if conv._custom: + await conv._check_custom(built) + + for builder, callback in self._event_builders: + event = built[type(builder)] + if not event: + continue + + if not builder.resolved: + await builder.resolve(self) + + filter = builder.filter(event) + if inspect.isawaitable(filter): + filter = await filter + if not filter: + continue + + try: + await callback(event) + except errors.AlreadyInConversationError: + name = getattr(callback, '__name__', repr(callback)) + self._log[__name__].debug( + 'Event handler "%s" already has an open conversation, ' + 'ignoring new one', name) + except events.StopPropagation: + name = getattr(callback, '__name__', repr(callback)) + self._log[__name__].debug( + 'Event handler "%s" stopped chain of propagation ' + 'for event %s.', name, type(event).__name__ + ) + break + except Exception as e: + if not isinstance(e, asyncio.CancelledError) or self.is_connected(): + name = getattr(callback, '__name__', repr(callback)) + self._log[__name__].exception('Unhandled exception on %s', name) + + async def _dispatch_event(self: 'TelegramClient', event): + """ + Dispatches a single, out-of-order event. Used by `AlbumHack`. + """ + # We're duplicating a most logic from `_dispatch_update`, but all in + # the name of speed; we don't want to make it worse for all updates + # just because albums may need it. + for builder, callback in self._event_builders: + if isinstance(builder, events.Raw): + continue + if not isinstance(event, builder.Event): + continue + + if not builder.resolved: + await builder.resolve(self) + + filter = builder.filter(event) + if inspect.isawaitable(filter): + filter = await filter + if not filter: + continue + + try: + await callback(event) + except errors.AlreadyInConversationError: + name = getattr(callback, '__name__', repr(callback)) + self._log[__name__].debug( + 'Event handler "%s" already has an open conversation, ' + 'ignoring new one', name) + except events.StopPropagation: + name = getattr(callback, '__name__', repr(callback)) + self._log[__name__].debug( + 'Event handler "%s" stopped chain of propagation ' + 'for event %s.', name, type(event).__name__ + ) + break + except Exception as e: + if not isinstance(e, asyncio.CancelledError) or self.is_connected(): + name = getattr(callback, '__name__', repr(callback)) + self._log[__name__].exception('Unhandled exception on %s', name) + + async def _handle_auto_reconnect(self: 'TelegramClient'): + # TODO Catch-up + # For now we make a high-level request to let Telegram + # know we are still interested in receiving more updates. + try: + await self.get_me() + except Exception as e: + self._log[__name__].warning('Error executing high-level request ' + 'after reconnect: %s: %s', type(e), e) + + return + try: + self._log[__name__].info( + 'Asking for the current state after reconnect...') + + # TODO consider: + # If there aren't many updates while the client is disconnected + # (I tried with up to 20), Telegram seems to send them without + # asking for them (via updates.getDifference). + # + # On disconnection, the library should probably set a "need + # difference" or "catching up" flag so that any new updates are + # ignored, and then the library should call updates.getDifference + # itself to fetch them. + # + # In any case (either there are too many updates and Telegram + # didn't send them, or there isn't a lot and Telegram sent them + # but we dropped them), we fetch the new difference to get all + # missed updates. I feel like this would be the best solution. + + # If a disconnection occurs, the old known state will be + # the latest one we were aware of, so we can catch up since + # the most recent state we were aware of. + await self.catch_up() + + self._log[__name__].info('Successfully fetched missed updates') + except errors.RPCError as e: + self._log[__name__].warning('Failed to get missed updates after ' + 'reconnect: %r', e) + except Exception: + self._log[__name__].exception( + 'Unhandled exception while getting update difference after reconnect') + + # endregion + + +class EventBuilderDict: + """ + Helper "dictionary" to return events from types and cache them. + """ + def __init__(self, client: 'TelegramClient', update, others): + self.client = client + self.update = update + self.others = others + + def __getitem__(self, builder): + try: + return self.__dict__[builder] + except KeyError: + event = self.__dict__[builder] = builder.build( + self.update, self.others, self.client._self_id) + + if isinstance(event, EventCommon): + event.original_update = self.update + event._entities = self.update._entities + event._set_client(self.client) + elif event: + event._client = self.client + + return event diff --git a/module/scripts/lib/telethon/client/uploads.py b/module/scripts/lib/telethon/client/uploads.py new file mode 100644 index 0000000..007f613 --- /dev/null +++ b/module/scripts/lib/telethon/client/uploads.py @@ -0,0 +1,870 @@ +import hashlib +import io +import itertools +import os +import pathlib +import re +import typing +from io import BytesIO + +from ..crypto import AES + +from .. import utils, helpers, hints +from ..tl import types, functions, custom + +try: + import PIL + import PIL.Image +except ImportError: + PIL = None + +if typing.TYPE_CHECKING: + from .telegramclient import TelegramClient + + +class _CacheType: + """Like functools.partial but pretends to be the wrapped class.""" + def __init__(self, cls): + self._cls = cls + + def __call__(self, *args, **kwargs): + return self._cls(*args, file_reference=b'', **kwargs) + + def __eq__(self, other): + return self._cls == other + + +def _resize_photo_if_needed( + file, is_image, width=2560, height=2560, background=(255, 255, 255)): + + # https://github.com/telegramdesktop/tdesktop/blob/12905f0dcb9d513378e7db11989455a1b764ef75/Telegram/SourceFiles/boxes/photo_crop_box.cpp#L254 + if (not is_image + or PIL is None + or (isinstance(file, io.IOBase) and not file.seekable())): + return file + + if isinstance(file, bytes): + file = io.BytesIO(file) + + if isinstance(file, io.IOBase): + # Pillow seeks to 0 unconditionally later anyway + old_pos = file.tell() + file.seek(0, io.SEEK_END) + before = file.tell() + elif isinstance(file, str) and os.path.exists(file): + # Check if file exists as a path and if so, get its size on disk + before = os.path.getsize(file) + else: + # Would be weird... + before = None + + try: + # Don't use a `with` block for `image`, or `file` would be closed. + # See https://github.com/LonamiWebs/Telethon/issues/1121 for more. + image = PIL.Image.open(file) + try: + kwargs = {'exif': image.info['exif']} + except KeyError: + kwargs = {} + + if image.mode == 'RGB': + # Check if image is within acceptable bounds, if so, check if the image is at or below 10 MB, or assume it isn't if size is None or 0 + if image.width <= width and image.height <= height and (before <= 10000000 if before else False): + return file + + # If the image is already RGB, don't convert it + # certain modes such as 'P' have no alpha index but can't be saved as JPEG directly + image.thumbnail((width, height), PIL.Image.LANCZOS) + result = image + else: + # We could save the resized image with the original format, but + # JPEG often compresses better -> smaller size -> faster upload + # We need to mask away the alpha channel ([3]), since otherwise + # IOError is raised when trying to save alpha channels in JPEG. + image.thumbnail((width, height), PIL.Image.LANCZOS) + result = PIL.Image.new('RGB', image.size, background) + mask = None + + if image.has_transparency_data: + if image.mode == 'RGBA': + mask = image.getchannel('A') + else: + mask = image.convert('RGBA').getchannel('A') + + result.paste(image, mask=mask) + + buffer = io.BytesIO() + result.save(buffer, 'JPEG', progressive=True, **kwargs) + buffer.seek(0) + buffer.name = 'a.jpg' + return buffer + except IOError: + return file + finally: + # The original position might matter + if isinstance(file, io.IOBase): + file.seek(old_pos) + + +class UploadMethods: + + # region Public methods + + async def send_file( + self: 'TelegramClient', + entity: 'hints.EntityLike', + file: 'typing.Union[hints.FileLike, typing.Sequence[hints.FileLike]]', + *, + caption: typing.Union[str, typing.Sequence[str]] = None, + force_document: bool = False, + mime_type: str = None, + file_size: int = None, + clear_draft: bool = False, + progress_callback: 'hints.ProgressCallback' = None, + reply_to: 'hints.MessageIDLike' = None, + attributes: 'typing.Sequence[types.TypeDocumentAttribute]' = None, + thumb: 'hints.FileLike' = None, + allow_cache: bool = True, + parse_mode: typing.Optional[str] = (), + formatting_entities: typing.Optional[ + typing.Union[ + typing.List[types.TypeMessageEntity], typing.List[typing.List[types.TypeMessageEntity]] + ] + ] = None, + voice_note: bool = False, + video_note: bool = False, + buttons: typing.Optional['hints.MarkupLike'] = None, + silent: bool = None, + background: bool = None, + supports_streaming: bool = False, + schedule: 'hints.DateLike' = None, + comment_to: 'typing.Union[int, types.Message]' = None, + ttl: int = None, + nosound_video: bool = None, + send_as: typing.Optional['hints.EntityLike'] = None, + message_effect_id: typing.Optional[int] = None, + **kwargs) -> typing.Union[typing.List[typing.Any], typing.Any]: + """ + Sends message with the given file to the specified entity. + + .. note:: + + If the ``hachoir3`` package (``hachoir`` module) is installed, + it will be used to determine metadata from audio and video files. + + If the ``pillow`` package is installed and you are sending a photo, + it will be resized to fit within the maximum dimensions allowed + by Telegram to avoid ``errors.PhotoInvalidDimensionsError``. This + cannot be done if you are sending :tl:`InputFile`, however. + + Arguments + entity (`entity`): + Who will receive the file. + + file (`str` | `bytes` | `file` | `media`): + The file to send, which can be one of: + + * A local file path to an in-disk file. The file name + will be the path's base name. + + * A `bytes` byte array with the file's data to send + (for example, by using ``text.encode('utf-8')``). + A default file name will be used. + + * A bytes `io.IOBase` stream over the file to send + (for example, by using ``open(file, 'rb')``). + Its ``.name`` property will be used for the file name, + or a default if it doesn't have one. + + * An external URL to a file over the internet. This will + send the file as "external" media, and Telegram is the + one that will fetch the media and send it. + + * A Bot API-like ``file_id``. You can convert previously + sent media to file IDs for later reusing with + `telethon.utils.pack_bot_file_id`. + + * A handle to an existing file (for example, if you sent a + message with media before, you can use its ``message.media`` + as a file here). + + * A handle to an uploaded file (from `upload_file`). + + * A :tl:`InputMedia` instance. For example, if you want to + send a dice use :tl:`InputMediaDice`, or if you want to + send a contact use :tl:`InputMediaContact`. + + To send an album, you should provide a list in this parameter. + + If a list or similar is provided, the files in it will be + sent as an album in the order in which they appear, sliced + in chunks of 10 if more than 10 are given. + + caption (`str`, optional): + Optional caption for the sent media message. When sending an + album, the caption may be a list of strings, which will be + assigned to the files pairwise. + + force_document (`bool`, optional): + If left to `False` and the file is a path that ends with + the extension of an image file or a video file, it will be + sent as such. Otherwise always as a document. + + mime_type (`str`, optional): + Custom mime type to use for the file to be sent (for example, + ``audio/mpeg``, ``audio/x-vorbis+ogg``, etc.). + It can change the type of files displayed. + If not set to any value, the mime type will be determined + automatically based on the file's extension. + + file_size (`int`, optional): + The size of the file to be uploaded if it needs to be uploaded, + which will be determined automatically if not specified. + + If the file size can't be determined beforehand, the entire + file will be read in-memory to find out how large it is. + + clear_draft (`bool`, optional): + Whether the existing draft should be cleared or not. + + progress_callback (`callable`, optional): + A callback function accepting two parameters: + ``(sent bytes, total)``. + + reply_to (`int` | `Message `): + Same as `reply_to` from `send_message`. + + attributes (`list`, optional): + Optional attributes that override the inferred ones, like + :tl:`DocumentAttributeFilename` and so on. + + thumb (`str` | `bytes` | `file`, optional): + Optional JPEG thumbnail (for documents). **Telegram will + ignore this parameter** unless you pass a ``.jpg`` file! + + The file must also be small in dimensions and in disk size. + Successful thumbnails were files below 20kB and 320x320px. + Width/height and dimensions/size ratios may be important. + For Telegram to accept a thumbnail, you must provide the + dimensions of the underlying media through ``attributes=`` + with :tl:`DocumentAttributeVideo` or by installing the + optional ``hachoir`` dependency. + + + allow_cache (`bool`, optional): + This parameter currently does nothing, but is kept for + backward-compatibility (and it may get its use back in + the future). + + parse_mode (`object`, optional): + See the `TelegramClient.parse_mode + ` + property for allowed values. Markdown parsing will be used by + default. + + formatting_entities (`list`, optional): + Optional formatting entities for the sent media message. When sending an album, + `formatting_entities` can be a list of lists, where each inner list contains + `types.TypeMessageEntity`. Each inner list will be assigned to the corresponding + file in a pairwise manner with the caption. If provided, the ``parse_mode`` + parameter will be ignored. + + voice_note (`bool`, optional): + If `True` the audio will be sent as a voice note. + + video_note (`bool`, optional): + If `True` the video will be sent as a video note, + also known as a round video message. + + buttons (`list`, `custom.Button `, :tl:`KeyboardButton`, :tl:`KeyboardInlineButton`): + The matrix (list of lists), row list or button to be shown + after sending the message. This parameter will only work if + you have signed in as a bot. You can also pass your own + :tl:`ReplyMarkup` here. + + silent (`bool`, optional): + Whether the message should notify people with sound or not. + Defaults to `False` (send with a notification sound unless + the person has the chat muted). Set it to `True` to alter + this behaviour. + + background (`bool`, optional): + Whether the message should be send in background. + + supports_streaming (`bool`, optional): + Whether the sent video supports streaming or not. Note that + Telegram only recognizes as streamable some formats like MP4, + and others like AVI or MKV will not work. You should convert + these to MP4 before sending if you want them to be streamable. + Unsupported formats will result in ``VideoContentTypeError``. + + schedule (`hints.DateLike`, optional): + If set, the file won't send immediately, and instead + it will be scheduled to be automatically sent at a later + time. + + comment_to (`int` | `Message `, optional): + Similar to ``reply_to``, but replies in the linked group of a + broadcast channel instead (effectively leaving a "comment to" + the specified message). + + This parameter takes precedence over ``reply_to``. If there is + no linked chat, `telethon.errors.sgIdInvalidError` is raised. + + ttl (`int`. optional): + The Time-To-Live of the file (also known as "self-destruct timer" + or "self-destructing media"). If set, files can only be viewed for + a short period of time before they disappear from the message + history automatically. + + The value must be at least 1 second, and at most 60 seconds, + otherwise Telegram will ignore this parameter. + + Not all types of media can be used with this parameter, such + as text documents, which will fail with ``TtlMediaInvalidError``. + + nosound_video (`bool`, optional): + Only applicable when sending a video file without an audio + track. If set to ``True``, the video will be displayed in + Telegram as a video. If set to ``False``, Telegram will attempt + to display the video as an animated gif. (It may still display + as a video due to other factors.) The value is ignored if set + on non-video files. This is set to ``True`` for albums, as gifs + cannot be sent in albums. + + send_as (`entity`): + Unique identifier (int) or username (str) of the chat or channel to send the message as. + You can use this to send the message on behalf of a chat or channel where you have appropriate permissions. + Use the GetSendAs to return the list of message sender identifiers, which can be used to send messages in the chat, + This setting applies to the current message and will remain effective for future messages unless explicitly changed. + To set this behavior permanently for all messages, use SaveDefaultSendAs. + + message_effect_id (`int`, optional): + Unique identifier of the message effect to be added to the message; for private chats only + + Returns + The `Message ` (or messages) + containing the sent file, or messages if a list of them was passed. + + Example + .. code-block:: python + + # Normal files like photos + await client.send_file(chat, '/my/photos/me.jpg', caption="It's me!") + # or + await client.send_message(chat, "It's me!", file='/my/photos/me.jpg') + + # Voice notes or round videos + await client.send_file(chat, '/my/songs/song.mp3', voice_note=True) + await client.send_file(chat, '/my/videos/video.mp4', video_note=True) + + # Custom thumbnails + await client.send_file(chat, '/my/documents/doc.txt', thumb='photo.jpg') + + # Only documents + await client.send_file(chat, '/my/photos/photo.png', force_document=True) + + # Albums + await client.send_file(chat, [ + '/my/photos/holiday1.jpg', + '/my/photos/holiday2.jpg', + '/my/drawings/portrait.png' + ]) + + # Printing upload progress + def callback(current, total): + print('Uploaded', current, 'out of', total, + 'bytes: {:.2%}'.format(current / total)) + + await client.send_file(chat, file, progress_callback=callback) + + # Dices, including dart and other future emoji + from telethon.tl import types + await client.send_file(chat, types.InputMediaDice('')) + await client.send_file(chat, types.InputMediaDice('🎯')) + + # Contacts + await client.send_file(chat, types.InputMediaContact( + phone_number='+34 123 456 789', + first_name='Example', + last_name='', + vcard='' + )) + """ + # TODO Properly implement allow_cache to reuse the sha256 of the file + # i.e. `None` was used + if not file: + raise TypeError('Cannot use {!r} as file'.format(file)) + + if not caption: + caption = '' + + if not formatting_entities: + formatting_entities = [] + + entity = await self.get_input_entity(entity) + if comment_to is not None: + entity, reply_to = await self._get_comment_data(entity, comment_to) + else: + reply_to = utils.get_message_id(reply_to) + + # First check if the user passed an iterable, in which case + # we may want to send grouped. + if utils.is_list_like(file): + sent_count = 0 + used_callback = None if not progress_callback else ( + lambda s, t: progress_callback(sent_count + s, len(file)) + ) + + if utils.is_list_like(caption): + captions = caption + else: + captions = [caption] + + # Check that formatting_entities list is valid + if all(utils.is_list_like(obj) for obj in formatting_entities): + formatting_entities = formatting_entities + elif utils.is_list_like(formatting_entities): + formatting_entities = [formatting_entities] + else: + raise TypeError('The formatting_entities argument must be a list or a sequence of lists') + + # Check that all entities in all lists are of the correct type + if not all(isinstance(ent, types.TypeMessageEntity) for sublist in formatting_entities for ent in sublist): + raise TypeError('All entities must be instances of ') + + result = [] + while file: + result += await self._send_album( + entity, file[:10], caption=captions[:10], formatting_entities=formatting_entities[:10], + progress_callback=used_callback, reply_to=reply_to, + parse_mode=parse_mode, silent=silent, schedule=schedule, + supports_streaming=supports_streaming, clear_draft=clear_draft, + force_document=force_document, background=background, + send_as=send_as, message_effect_id=message_effect_id + ) + file = file[10:] + captions = captions[10:] + formatting_entities = formatting_entities[10:] + sent_count += 10 + + return result + + if formatting_entities: + msg_entities = formatting_entities + else: + caption, msg_entities =\ + await self._parse_message_text(caption, parse_mode) + + file_handle, media, image = await self._file_to_media( + file, force_document=force_document, + mime_type=mime_type, + file_size=file_size, + progress_callback=progress_callback, + attributes=attributes, allow_cache=allow_cache, thumb=thumb, + voice_note=voice_note, video_note=video_note, + supports_streaming=supports_streaming, ttl=ttl, + nosound_video=nosound_video, + ) + + # e.g. invalid cast from :tl:`MessageMediaWebPage` + if not media: + raise TypeError('Cannot use {!r} as file'.format(file)) + + markup = self.build_reply_markup(buttons) + reply_to = None if reply_to is None else types.InputReplyToMessage(reply_to) + request = functions.messages.SendMediaRequest( + entity, media, reply_to=reply_to, message=caption, + entities=msg_entities, reply_markup=markup, silent=silent, + schedule_date=schedule, clear_draft=clear_draft, + background=background, + send_as=await self.get_input_entity(send_as) if send_as else None, + effect=message_effect_id + ) + return self._get_response_message(request, await self(request), entity) + + async def _send_album(self: 'TelegramClient', entity, files, caption='', + formatting_entities=None, + progress_callback=None, reply_to=None, + parse_mode=(), silent=None, schedule=None, + supports_streaming=None, clear_draft=None, + force_document=False, background=None, ttl=None, + send_as: typing.Optional['hints.EntityLike'] = None, + message_effect_id: typing.Optional[int] = None): + """Specialized version of .send_file for albums""" + # We don't care if the user wants to avoid cache, we will use it + # anyway. Why? The cached version will be exactly the same thing + # we need to produce right now to send albums (uploadMedia), and + # cache only makes a difference for documents where the user may + # want the attributes used on them to change. + # + # In theory documents can be sent inside the albums, but they appear + # as different messages (not inside the album), and the logic to set + # the attributes/avoid cache is already written in .send_file(). + entity = await self.get_input_entity(entity) + if not utils.is_list_like(caption): + caption = (caption,) + if not all(isinstance(obj, list) for obj in formatting_entities): + formatting_entities = (formatting_entities,) + + captions = [] + # If the formatting_entities argument is provided, we don't use parse_mode + if formatting_entities: + # Pop from the end (so reverse) + capt_with_ent = itertools.zip_longest(reversed(caption), reversed(formatting_entities), fillvalue=None) + for msg_caption, msg_entities in capt_with_ent: + captions.append((msg_caption, msg_entities)) + else: + for c in reversed(caption): # Pop from the end (so reverse) + captions.append(await self._parse_message_text(c or '', parse_mode)) + + reply_to = utils.get_message_id(reply_to) + + used_callback = None if not progress_callback else ( + # use an integer when sent matches total, to easily determine a file has been fully sent + lambda s, t: progress_callback(sent_count + 1 if s == t else sent_count + s / t, len(files)) + ) + + # Need to upload the media first, but only if they're not cached yet + media = [] + for sent_count, file in enumerate(files): + # Albums want :tl:`InputMedia` which, in theory, includes + # :tl:`InputMediaUploadedPhoto`. However, using that will + # make it `raise MediaInvalidError`, so we need to upload + # it as media and then convert that to :tl:`InputMediaPhoto`. + fh, fm, _ = await self._file_to_media( + file, supports_streaming=supports_streaming, + force_document=force_document, ttl=ttl, + progress_callback=used_callback, nosound_video=True) + if isinstance(fm, (types.InputMediaUploadedPhoto, types.InputMediaPhotoExternal)): + r = await self(functions.messages.UploadMediaRequest( + entity, media=fm + )) + + fm = utils.get_input_media(r.photo) + elif isinstance(fm, (types.InputMediaUploadedDocument, types.InputMediaDocumentExternal)): + r = await self(functions.messages.UploadMediaRequest( + entity, media=fm + )) + + fm = utils.get_input_media( + r.document, supports_streaming=supports_streaming) + + if captions: + caption, msg_entities = captions.pop() + else: + caption, msg_entities = '', None + media.append(types.InputSingleMedia( + fm, + message=caption, + entities=msg_entities + # random_id is autogenerated + )) + + # Now we can construct the multi-media request + request = functions.messages.SendMultiMediaRequest( + entity, reply_to=None if reply_to is None else types.InputReplyToMessage(reply_to), multi_media=media, + silent=silent, schedule_date=schedule, clear_draft=clear_draft, + background=background, + send_as=await self.get_input_entity(send_as) if send_as else None, + effect=message_effect_id + ) + result = await self(request) + + random_ids = [m.random_id for m in media] + return self._get_response_message(random_ids, result, entity) + + async def upload_file( + self: 'TelegramClient', + file: 'hints.FileLike', + *, + part_size_kb: float = None, + file_size: int = None, + file_name: str = None, + use_cache: type = None, + key: bytes = None, + iv: bytes = None, + progress_callback: 'hints.ProgressCallback' = None) -> 'types.TypeInputFile': + """ + Uploads a file to Telegram's servers, without sending it. + + .. note:: + + Generally, you want to use `send_file` instead. + + This method returns a handle (an instance of :tl:`InputFile` or + :tl:`InputFileBig`, as required) which can be later used before + it expires (they are usable during less than a day). + + Uploading a file will simply return a "handle" to the file stored + remotely in the Telegram servers, which can be later used on. This + will **not** upload the file to your own chat or any chat at all. + + Arguments + file (`str` | `bytes` | `file`): + The path of the file, byte array, or stream that will be sent. + Note that if a byte array or a stream is given, a filename + or its type won't be inferred, and it will be sent as an + "unnamed application/octet-stream". + + part_size_kb (`int`, optional): + Chunk size when uploading files. The larger, the less + requests will be made (up to 512KB maximum). + + file_size (`int`, optional): + The size of the file to be uploaded, which will be determined + automatically if not specified. + + If the file size can't be determined beforehand, the entire + file will be read in-memory to find out how large it is. + + file_name (`str`, optional): + The file name which will be used on the resulting InputFile. + If not specified, the name will be taken from the ``file`` + and if this is not a `str`, it will be ``"unnamed"``. + + use_cache (`type`, optional): + This parameter currently does nothing, but is kept for + backward-compatibility (and it may get its use back in + the future). + + key ('bytes', optional): + In case of an encrypted upload (secret chats) a key is supplied + + iv ('bytes', optional): + In case of an encrypted upload (secret chats) an iv is supplied + + progress_callback (`callable`, optional): + A callback function accepting two parameters: + ``(sent bytes, total)``. + + When sending an album, the callback will receive a number + between 0 and the amount of files as the "sent" parameter, + and the amount of files as the "total". Note that the first + parameter will be a floating point number to indicate progress + within a file (e.g. ``2.5`` means it has sent 50% of the third + file, because it's between 2 and 3). + + Returns + :tl:`InputFileBig` if the file size is larger than 10MB, + `InputSizedFile ` + (subclass of :tl:`InputFile`) otherwise. + + Example + .. code-block:: python + + # Photos as photo and document + file = await client.upload_file('photo.jpg') + await client.send_file(chat, file) # sends as photo + await client.send_file(chat, file, force_document=True) # sends as document + + file.name = 'not a photo.jpg' + await client.send_file(chat, file, force_document=True) # document, new name + + # As song or as voice note + file = await client.upload_file('song.ogg') + await client.send_file(chat, file) # sends as song + await client.send_file(chat, file, voice_note=True) # sends as voice note + """ + if isinstance(file, (types.InputFile, types.InputFileBig)): + return file # Already uploaded + + pos = 0 + async with helpers._FileStream(file, file_size=file_size) as stream: + # Opening the stream will determine the correct file size + file_size = stream.file_size + + if not part_size_kb: + part_size_kb = utils.get_appropriated_part_size(file_size) + + if part_size_kb > 512: + raise ValueError('The part size must be less or equal to 512KB') + + part_size = int(part_size_kb * 1024) + if part_size % 1024 != 0: + raise ValueError( + 'The part size must be evenly divisible by 1024') + + # Set a default file name if None was specified + file_id = helpers.generate_random_long() + if not file_name: + file_name = stream.name or str(file_id) + + # If the file name lacks extension, add it if possible. + # Else Telegram complains with `PHOTO_EXT_INVALID_ERROR` + # even if the uploaded image is indeed a photo. + if not os.path.splitext(file_name)[-1]: + file_name += utils._get_extension(stream) + + # Determine whether the file is too big (over 10MB) or not + # Telegram does make a distinction between smaller or larger files + is_big = file_size > 10 * 1024 * 1024 + hash_md5 = hashlib.md5() + + part_count = (file_size + part_size - 1) // part_size + self._log[__name__].info('Uploading file of %d bytes in %d chunks of %d', + file_size, part_count, part_size) + + pos = 0 + for part_index in range(part_count): + # Read the file by in chunks of size part_size + part = await helpers._maybe_await(stream.read(part_size)) + + if not isinstance(part, bytes): + raise TypeError( + 'file descriptor returned {}, not bytes (you must ' + 'open the file in bytes mode)'.format(type(part))) + + # `file_size` could be wrong in which case `part` may not be + # `part_size` before reaching the end. + if len(part) != part_size and part_index < part_count - 1: + raise ValueError( + 'read less than {} before reaching the end; either ' + '`file_size` or `read` are wrong'.format(part_size)) + + pos += len(part) + + # Encryption part if needed + if key and iv: + part = AES.encrypt_ige(part, key, iv) + + if not is_big: + # Bit odd that MD5 is only needed for small files and not + # big ones with more chance for corruption, but that's + # what Telegram wants. + hash_md5.update(part) + + # The SavePartRequest is different depending on whether + # the file is too large or not (over or less than 10MB) + if is_big: + request = functions.upload.SaveBigFilePartRequest( + file_id, part_index, part_count, part) + else: + request = functions.upload.SaveFilePartRequest( + file_id, part_index, part) + + result = await self(request) + if result: + self._log[__name__].debug('Uploaded %d/%d', + part_index + 1, part_count) + if progress_callback: + await helpers._maybe_await(progress_callback(pos, file_size)) + else: + raise RuntimeError( + 'Failed to upload file part {}.'.format(part_index)) + + if is_big: + return types.InputFileBig(file_id, part_count, file_name) + else: + return custom.InputSizedFile( + file_id, part_count, file_name, md5=hash_md5, size=file_size + ) + + # endregion + + async def _file_to_media( + self, file, force_document=False, file_size=None, + progress_callback=None, attributes=None, thumb=None, + allow_cache=True, voice_note=False, video_note=False, + supports_streaming=False, mime_type=None, as_image=None, + ttl=None, nosound_video=None): + if not file: + return None, None, None + + if isinstance(file, pathlib.Path): + file = str(file.absolute()) + + is_image = utils.is_image(file) + if as_image is None: + as_image = is_image and not force_document + + # `aiofiles` do not base `io.IOBase` but do have `read`, so we + # just check for the read attribute to see if it's file-like. + if not isinstance(file, (str, bytes, types.InputFile, types.InputFileBig)) \ + and not hasattr(file, 'read'): + # The user may pass a Message containing media (or the media, + # or anything similar) that should be treated as a file. Try + # getting the input media for whatever they passed and send it. + # + # We pass all attributes since these will be used if the user + # passed :tl:`InputFile`, and all information may be relevant. + try: + return (None, utils.get_input_media( + file, + is_photo=as_image, + attributes=attributes, + force_document=force_document, + voice_note=voice_note, + video_note=video_note, + supports_streaming=supports_streaming, + ttl=ttl + ), as_image) + except TypeError: + # Can't turn whatever was given into media + return None, None, as_image + + media = None + file_handle = None + + if isinstance(file, (types.InputFile, types.InputFileBig)): + file_handle = file + elif not isinstance(file, str) or os.path.isfile(file): + file_handle = await self.upload_file( + _resize_photo_if_needed(file, as_image), + file_size=file_size, + progress_callback=progress_callback + ) + elif re.match('https?://', file): + if as_image: + media = types.InputMediaPhotoExternal(file, ttl_seconds=ttl) + else: + media = types.InputMediaDocumentExternal(file, ttl_seconds=ttl) + else: + bot_file = utils.resolve_bot_file_id(file) + if bot_file: + media = utils.get_input_media(bot_file, ttl=ttl) + + if media: + pass # Already have media, don't check the rest + elif not file_handle: + raise ValueError( + 'Failed to convert {} to media. Not an existing file, ' + 'an HTTP URL or a valid bot-API-like file ID'.format(file) + ) + elif as_image: + media = types.InputMediaUploadedPhoto(file_handle, ttl_seconds=ttl) + else: + attributes, mime_type = utils.get_attributes( + file, + mime_type=mime_type, + attributes=attributes, + force_document=force_document and not is_image, + voice_note=voice_note, + video_note=video_note, + supports_streaming=supports_streaming, + thumb=thumb + ) + + if not thumb: + thumb = None + else: + if isinstance(thumb, pathlib.Path): + thumb = str(thumb.absolute()) + thumb = await self.upload_file(thumb, file_size=file_size) + + # setting `nosound_video` to `True` doesn't affect videos with sound + # instead it prevents sending silent videos as GIFs + nosound_video = nosound_video if mime_type.split("/")[0] == 'video' else None + + media = types.InputMediaUploadedDocument( + file=file_handle, + mime_type=mime_type, + attributes=attributes, + thumb=thumb, + force_file=force_document and not is_image, + ttl_seconds=ttl, + nosound_video=nosound_video + ) + return file_handle, media, as_image + + # endregion diff --git a/module/scripts/lib/telethon/client/users.py b/module/scripts/lib/telethon/client/users.py new file mode 100644 index 0000000..994856b --- /dev/null +++ b/module/scripts/lib/telethon/client/users.py @@ -0,0 +1,620 @@ +import asyncio +import datetime +import itertools +import time +import typing + +from .. import errors, helpers, utils, hints +from ..errors import MultiError, RPCError +from ..helpers import retry_range +from ..tl import TLRequest, types, functions + +_NOT_A_REQUEST = lambda: TypeError('You can only invoke requests, not types!') + +if typing.TYPE_CHECKING: + from .telegramclient import TelegramClient + + +def _fmt_flood(delay, request, *, early=False, td=datetime.timedelta): + return ( + 'Sleeping%s for %ds (%s) on %s flood wait', + ' early' if early else '', + delay, + td(seconds=delay), + request.__class__.__name__ + ) + + +class UserMethods: + async def __call__(self: 'TelegramClient', request, ordered=False, flood_sleep_threshold=None): + return await self._call(self._sender, request, ordered=ordered) + + async def _call(self: 'TelegramClient', sender, request, ordered=False, flood_sleep_threshold=None): + if self._loop is not None and self._loop != helpers.get_running_loop(): + raise RuntimeError('The asyncio event loop must not change after connection (see the FAQ for details)') + # if the loop is None it will fail with a connection error later on + + if flood_sleep_threshold is None: + flood_sleep_threshold = self.flood_sleep_threshold + requests = list(request) if utils.is_list_like(request) else [request] + request = list(request) if utils.is_list_like(request) else request + for i, r in enumerate(requests): + if not isinstance(r, TLRequest): + raise _NOT_A_REQUEST() + await r.resolve(self, utils) + + # Avoid making the request if it's already in a flood wait + if r.CONSTRUCTOR_ID in self._flood_waited_requests: + due = self._flood_waited_requests[r.CONSTRUCTOR_ID] + diff = round(due - time.time()) + if diff <= 3: # Flood waits below 3 seconds are "ignored" + self._flood_waited_requests.pop(r.CONSTRUCTOR_ID, None) + elif diff <= flood_sleep_threshold: + self._log[__name__].info(*_fmt_flood(diff, r, early=True)) + await asyncio.sleep(diff) + self._flood_waited_requests.pop(r.CONSTRUCTOR_ID, None) + else: + raise errors.FloodWaitError(request=r, capture=diff) + + if self._no_updates: + if utils.is_list_like(request): + request[i] = functions.InvokeWithoutUpdatesRequest(r) + else: + # This should only run once as requests should be a list of 1 item + request = functions.InvokeWithoutUpdatesRequest(r) + + request_index = 0 + last_error = None + self._last_request = time.time() + + for attempt in retry_range(self._request_retries): + try: + future = sender.send(request, ordered=ordered) + if isinstance(future, list): + results = [] + exceptions = [] + for f in future: + try: + result = await f + except RPCError as e: + exceptions.append(e) + results.append(None) + continue + await utils.maybe_async(self.session.process_entities(result)) + exceptions.append(None) + results.append(result) + request_index += 1 + if any(x is not None for x in exceptions): + raise MultiError(exceptions, results, requests) + else: + return results + else: + result = await future + await utils.maybe_async(self.session.process_entities(result)) + return result + except (errors.ServerError, errors.RpcCallFailError, + errors.RpcMcgetFailError, errors.InterdcCallErrorError, + errors.TimedOutError, + errors.InterdcCallRichErrorError) as e: + last_error = e + self._log[__name__].warning( + 'Telegram is having internal issues %s: %s', + e.__class__.__name__, e) + + await asyncio.sleep(2) + except (errors.FloodWaitError, errors.FloodPremiumWaitError, + errors.SlowModeWaitError, errors.FloodTestPhoneWaitError) as e: + last_error = e + if utils.is_list_like(request): + request = request[request_index] + + # SLOW_MODE_WAIT is chat-specific, not request-specific + if not isinstance(e, errors.SlowModeWaitError): + self._flood_waited_requests\ + [request.CONSTRUCTOR_ID] = time.time() + e.seconds + + # In test servers, FLOOD_WAIT_0 has been observed, and sleeping for + # such a short amount will cause retries very fast leading to issues. + if e.seconds == 0: + e.seconds = 1 + + if e.seconds <= self.flood_sleep_threshold: + self._log[__name__].info(*_fmt_flood(e.seconds, request)) + await asyncio.sleep(e.seconds) + else: + raise + except (errors.PhoneMigrateError, errors.NetworkMigrateError, + errors.UserMigrateError) as e: + last_error = e + self._log[__name__].info('Phone migrated to %d', e.new_dc) + should_raise = isinstance(e, ( + errors.PhoneMigrateError, errors.NetworkMigrateError + )) + if should_raise and await self.is_user_authorized(): + raise + await self._switch_dc(e.new_dc) + + if self._raise_last_call_error and last_error is not None: + raise last_error + raise ValueError('Request was unsuccessful {} time(s)' + .format(attempt)) + + # region Public methods + + async def get_me(self: 'TelegramClient', input_peer: bool = False) \ + -> 'typing.Union[types.User, types.InputPeerUser]': + """ + Gets "me", the current :tl:`User` who is logged in. + + If the user has not logged in yet, this method returns `None`. + + Arguments + input_peer (`bool`, optional): + Whether to return the :tl:`InputPeerUser` version or the normal + :tl:`User`. This can be useful if you just need to know the ID + of yourself. + + Returns + Your own :tl:`User`. + + Example + .. code-block:: python + + me = await client.get_me() + print(me.username) + """ + if input_peer and self._mb_entity_cache.self_id: + return self._mb_entity_cache.get(self._mb_entity_cache.self_id)._as_input_peer() + + try: + me = (await self( + functions.users.GetUsersRequest([types.InputUserSelf()])))[0] + + if not self._mb_entity_cache.self_id: + self._mb_entity_cache.set_self_user(me.id, me.bot, me.access_hash) + + return utils.get_input_peer(me, allow_self=False) if input_peer else me + except errors.UnauthorizedError: + return None + + @property + def _self_id(self: 'TelegramClient') -> typing.Optional[int]: + """ + Returns the ID of the logged-in user, if known. + + This property is used in every update, and some like `updateLoginToken` + occur prior to login, so it gracefully handles when no ID is known yet. + """ + return self._mb_entity_cache.self_id + + async def is_bot(self: 'TelegramClient') -> bool: + """ + Return `True` if the signed-in user is a bot, `False` otherwise. + + Example + .. code-block:: python + + if await client.is_bot(): + print('Beep') + else: + print('Hello') + """ + if self._mb_entity_cache.self_bot is None: + await self.get_me(input_peer=True) + + return self._mb_entity_cache.self_bot + + async def is_user_authorized(self: 'TelegramClient') -> bool: + """ + Returns `True` if the user is authorized (logged in). + + Example + .. code-block:: python + + if not await client.is_user_authorized(): + await client.send_code_request(phone) + code = input('enter code: ') + await client.sign_in(phone, code) + """ + if self._authorized is None: + try: + # Any request that requires authorization will work + await self(functions.updates.GetStateRequest()) + self._authorized = True + except errors.RPCError: + self._authorized = False + + return self._authorized + + async def get_entity( + self: 'TelegramClient', + entity: 'hints.EntitiesLike') -> typing.Union['hints.Entity', typing.List['hints.Entity']]: + """ + Turns the given entity into a valid Telegram :tl:`User`, :tl:`Chat` + or :tl:`Channel`. You can also pass a list or iterable of entities, + and they will be efficiently fetched from the network. + + Arguments + entity (`str` | `int` | :tl:`Peer` | :tl:`InputPeer`): + If a username is given, **the username will be resolved** making + an API call every time. Resolving usernames is an expensive + operation and will start hitting flood waits around 50 usernames + in a short period of time. + + If you want to get the entity for a *cached* username, you should + first `get_input_entity(username) ` which will + use the cache), and then use `get_entity` with the result of the + previous call. + + Similar limits apply to invite links, and you should use their + ID instead. + + Using phone numbers (from people in your contact list), exact + names, integer IDs or :tl:`Peer` rely on a `get_input_entity` + first, which in turn needs the entity to be in cache, unless + a :tl:`InputPeer` was passed. + + Unsupported types will raise ``TypeError``. + + If the entity can't be found, ``ValueError`` will be raised. + + Returns + :tl:`User`, :tl:`Chat` or :tl:`Channel` corresponding to the + input entity. A list will be returned if more than one was given. + + Example + .. code-block:: python + + from telethon import utils + + me = await client.get_entity('me') + print(utils.get_display_name(me)) + + chat = await client.get_input_entity('username') + async for message in client.iter_messages(chat): + ... + + # Note that you could have used the username directly, but it's + # good to use get_input_entity if you will reuse it a lot. + async for message in client.iter_messages('username'): + ... + + # Note that for this to work the phone number must be in your contacts + some_id = await client.get_peer_id('+34123456789') + """ + single = not utils.is_list_like(entity) + if single: + entity = (entity,) + + # Group input entities by string (resolve username), + # input users (get users), input chat (get chats) and + # input channels (get channels) to get the most entities + # in the less amount of calls possible. + inputs = [] + for x in entity: + if isinstance(x, str): + inputs.append(x) + else: + inputs.append(await self.get_input_entity(x)) + + lists = { + helpers._EntityType.USER: [], + helpers._EntityType.CHAT: [], + helpers._EntityType.CHANNEL: [], + } + for x in inputs: + try: + lists[helpers._entity_type(x)].append(x) + except TypeError: + pass + + users = lists[helpers._EntityType.USER] + chats = lists[helpers._EntityType.CHAT] + channels = lists[helpers._EntityType.CHANNEL] + if users: + # GetUsersRequest has a limit of 200 per call + tmp = [] + while users: + curr, users = users[:200], users[200:] + tmp.extend(await self(functions.users.GetUsersRequest(curr))) + users = tmp + if chats: # TODO Handle chats slice? + chats = (await self( + functions.messages.GetChatsRequest([x.chat_id for x in chats]))).chats + if channels: + channels = (await self( + functions.channels.GetChannelsRequest(channels))).chats + + # Merge users, chats and channels into a single dictionary + id_entity = { + # `get_input_entity` might've guessed the type from a non-marked ID, + # so the only way to match that with the input is by not using marks here. + utils.get_peer_id(x, add_mark=False): x + for x in itertools.chain(users, chats, channels) + } + + # We could check saved usernames and put them into the users, + # chats and channels list from before. While this would reduce + # the amount of ResolveUsername calls, it would fail to catch + # username changes. + result = [] + for x in inputs: + if isinstance(x, str): + result.append(await self._get_entity_from_string(x)) + elif not isinstance(x, types.InputPeerSelf): + result.append(id_entity[utils.get_peer_id(x, add_mark=False)]) + else: + result.append(next( + u for u in id_entity.values() + if isinstance(u, types.User) and u.is_self + )) + + return result[0] if single else result + + async def get_input_entity( + self: 'TelegramClient', + peer: 'hints.EntityLike') -> 'types.TypeInputPeer': + """ + Turns the given entity into its input entity version. + + Most requests use this kind of :tl:`InputPeer`, so this is the most + suitable call to make for those cases. **Generally you should let the + library do its job** and don't worry about getting the input entity + first, but if you're going to use an entity often, consider making the + call: + + Arguments + entity (`str` | `int` | :tl:`Peer` | :tl:`InputPeer`): + If a username or invite link is given, **the library will + use the cache**. This means that it's possible to be using + a username that *changed* or an old invite link (this only + happens if an invite link for a small group chat is used + after it was upgraded to a mega-group). + + If the username or ID from the invite link is not found in + the cache, it will be fetched. The same rules apply to phone + numbers (``'+34 123456789'``) from people in your contact list. + + If an exact name is given, it must be in the cache too. This + is not reliable as different people can share the same name + and which entity is returned is arbitrary, and should be used + only for quick tests. + + If a positive integer ID is given, the entity will be searched + in cached users, chats or channels, without making any call. + + If a negative integer ID is given, the entity will be searched + exactly as either a chat (prefixed with ``-``) or as a channel + (prefixed with ``-100``). + + If a :tl:`Peer` is given, it will be searched exactly in the + cache as either a user, chat or channel. + + If the given object can be turned into an input entity directly, + said operation will be done. + + Unsupported types will raise ``TypeError``. + + If the entity can't be found, ``ValueError`` will be raised. + + Returns + :tl:`InputPeerUser`, :tl:`InputPeerChat` or :tl:`InputPeerChannel` + or :tl:`InputPeerSelf` if the parameter is ``'me'`` or ``'self'``. + + If you need to get the ID of yourself, you should use + `get_me` with ``input_peer=True``) instead. + + Example + .. code-block:: python + + # If you're going to use "username" often in your code + # (make a lot of calls), consider getting its input entity + # once, and then using the "user" everywhere instead. + user = await client.get_input_entity('username') + + # The same applies to IDs, chats or channels. + chat = await client.get_input_entity(-123456789) + """ + # Short-circuit if the input parameter directly maps to an InputPeer + try: + return utils.get_input_peer(peer) + except TypeError: + pass + + # Next in priority is having a peer (or its ID) cached in-memory + try: + # 0x2d45687 == crc32(b'Peer') + if isinstance(peer, int) or peer.SUBCLASS_OF_ID == 0x2d45687: + return self._mb_entity_cache.get(utils.get_peer_id(peer, add_mark=False))._as_input_peer() + except AttributeError: + pass + + # Then come known strings that take precedence + if peer in ('me', 'self'): + return types.InputPeerSelf() + + # No InputPeer, cached peer, or known string. Fetch from disk cache + try: + input_entity = await utils.maybe_async(self.session.get_input_entity(peer)) + return input_entity + except ValueError: + pass + + # Only network left to try + if isinstance(peer, str): + return utils.get_input_peer( + await self._get_entity_from_string(peer)) + + # If we're a bot and the user has messaged us privately users.getUsers + # will work with access_hash = 0. Similar for channels.getChannels. + # If we're not a bot but the user is in our contacts, it seems to work + # regardless. These are the only two special-cased requests. + peer = utils.get_peer(peer) + if isinstance(peer, types.PeerUser): + users = await self(functions.users.GetUsersRequest([ + types.InputUser(peer.user_id, access_hash=0)])) + if users and not isinstance(users[0], types.UserEmpty): + # If the user passed a valid ID they expect to work for + # channels but would be valid for users, we get UserEmpty. + # Avoid returning the invalid empty input peer for that. + # + # We *could* try to guess if it's a channel first, and if + # it's not, work as a chat and try to validate it through + # another request, but that becomes too much work. + return utils.get_input_peer(users[0]) + elif isinstance(peer, types.PeerChat): + return types.InputPeerChat(peer.chat_id) + elif isinstance(peer, types.PeerChannel): + try: + channels = await self(functions.channels.GetChannelsRequest([ + types.InputChannel(peer.channel_id, access_hash=0)])) + return utils.get_input_peer(channels.chats[0]) + except errors.ChannelInvalidError: + pass + + raise ValueError( + 'Could not find the input entity for {} ({}). Please read https://' + 'docs.telethon.dev/en/stable/concepts/entities.html to' + ' find out more details.' + .format(peer, type(peer).__name__) + ) + + async def _get_peer(self: 'TelegramClient', peer: 'hints.EntityLike'): + i, cls = utils.resolve_id(await self.get_peer_id(peer)) + return cls(i) + + async def get_peer_id( + self: 'TelegramClient', + peer: 'hints.EntityLike', + add_mark: bool = True) -> int: + """ + Gets the ID for the given entity. + + This method needs to be ``async`` because `peer` supports usernames, + invite-links, phone numbers (from people in your contact list), etc. + + If ``add_mark is False``, then a positive ID will be returned + instead. By default, bot-API style IDs (signed) are returned. + + Example + .. code-block:: python + + print(await client.get_peer_id('me')) + """ + if isinstance(peer, int): + return utils.get_peer_id(peer, add_mark=add_mark) + + try: + if peer.SUBCLASS_OF_ID not in (0x2d45687, 0xc91c90b6): + # 0x2d45687, 0xc91c90b6 == crc32(b'Peer') and b'InputPeer' + peer = await self.get_input_entity(peer) + except AttributeError: + peer = await self.get_input_entity(peer) + + if isinstance(peer, types.InputPeerSelf): + peer = await self.get_me(input_peer=True) + + return utils.get_peer_id(peer, add_mark=add_mark) + + # endregion + + # region Private methods + + async def _get_entity_from_string(self: 'TelegramClient', string): + """ + Gets a full entity from the given string, which may be a phone or + a username, and processes all the found entities on the session. + The string may also be a user link, or a channel/chat invite link. + + This method has the side effect of adding the found users to the + session database, so it can be queried later without API calls, + if this option is enabled on the session. + + Returns the found entity, or raises TypeError if not found. + """ + phone = utils.parse_phone(string) + if phone: + try: + for user in (await self( + functions.contacts.GetContactsRequest(0))).users: + if user.phone == phone: + return user + except errors.BotMethodInvalidError: + raise ValueError('Cannot get entity by phone number as a ' + 'bot (try using integer IDs, not strings)') + elif string.lower() in ('me', 'self'): + return await self.get_me() + else: + username, is_join_chat = utils.parse_username(string) + if is_join_chat: + invite = await self( + functions.messages.CheckChatInviteRequest(username)) + + if isinstance(invite, types.ChatInvite): + raise ValueError( + 'Cannot get entity from a channel (or group) ' + 'that you are not part of. Join the group and retry' + ) + elif isinstance(invite, types.ChatInviteAlready): + return invite.chat + elif username: + try: + result = await self( + functions.contacts.ResolveUsernameRequest(username)) + except errors.UsernameNotOccupiedError as e: + raise ValueError('No user has "{}" as username' + .format(username)) from e + + try: + pid = utils.get_peer_id(result.peer, add_mark=False) + if isinstance(result.peer, types.PeerUser): + return next(x for x in result.users if x.id == pid) + else: + return next(x for x in result.chats if x.id == pid) + except StopIteration: + pass + try: + # Nobody with this username, maybe it's an exact name/title + input_entity = await utils.maybe_async(self.session.get_input_entity(string)) + return await self.get_entity(input_entity) + except ValueError: + pass + + raise ValueError( + 'Cannot find any entity corresponding to "{}"'.format(string) + ) + + async def _get_input_dialog(self: 'TelegramClient', dialog): + """ + Returns a :tl:`InputDialogPeer`. This is a bit tricky because + it may or not need access to the client to convert what's given + into an input entity. + """ + try: + if dialog.SUBCLASS_OF_ID == 0xa21c9795: # crc32(b'InputDialogPeer') + dialog.peer = await self.get_input_entity(dialog.peer) + return dialog + elif dialog.SUBCLASS_OF_ID == 0xc91c90b6: # crc32(b'InputPeer') + return types.InputDialogPeer(dialog) + except AttributeError: + pass + + return types.InputDialogPeer(await self.get_input_entity(dialog)) + + async def _get_input_notify(self: 'TelegramClient', notify): + """ + Returns a :tl:`InputNotifyPeer`. This is a bit tricky because + it may or not need access to the client to convert what's given + into an input entity. + """ + try: + if notify.SUBCLASS_OF_ID == 0x58981615: + if isinstance(notify, types.InputNotifyPeer): + notify.peer = await self.get_input_entity(notify.peer) + return notify + except AttributeError: + pass + + return types.InputNotifyPeer(await self.get_input_entity(notify)) + + # endregion diff --git a/module/scripts/lib/telethon/crypto/__init__.py b/module/scripts/lib/telethon/crypto/__init__.py new file mode 100644 index 0000000..69be1da --- /dev/null +++ b/module/scripts/lib/telethon/crypto/__init__.py @@ -0,0 +1,10 @@ +""" +This module contains several utilities regarding cryptographic purposes, +such as the AES IGE mode used by Telegram, the authorization key bound with +their data centers, and so on. +""" +from .aes import AES +from .aesctr import AESModeCTR +from .authkey import AuthKey +from .factorization import Factorization +from .cdndecrypter import CdnDecrypter diff --git a/module/scripts/lib/telethon/crypto/__pycache__/__init__.cpython-314.pyc b/module/scripts/lib/telethon/crypto/__pycache__/__init__.cpython-314.pyc new file mode 100644 index 0000000000000000000000000000000000000000..94500b710ab9f17b93cd515202408fc066e9999d GIT binary patch literal 630 zcmYjP&5qMB5O$jMhp=67*%OEz2?ih-QX9Pa78AJ9i zm<35N?O}_zA^rr(HY;ZTZHH|UeXS3b|HtJt}j_0}du(DZI zD3I2m(8hyTm&%C(D%6EWtvooj6fVXplc0=iUl={T3$+E`tRy(JcJN|@A zyPAk)&ChkAJfAGpN*m3EK~3$r)L0*gvgBSmfimZBP7aQQyr@dPol0BKC3;@y1xM;3 z_090@6nL5v6{hK=tee5p?SW_R4r!qO!~9(`Kf@phes!WC`W>@iZ_Vzn*}XM;^p!ny HyHx)J_L;zI literal 0 HcmV?d00001 diff --git a/module/scripts/lib/telethon/crypto/__pycache__/aes.cpython-314.pyc b/module/scripts/lib/telethon/crypto/__pycache__/aes.cpython-314.pyc new file mode 100644 index 0000000000000000000000000000000000000000..8c70a53943b379fecfed8c2808dfcc16e50647c2 GIT binary patch literal 4634 zcmcInTWs6b89o$srD$8Wd}(aUqUFS?6In{!G#KKnYwTQ`pwP5J##-3~EzvR)n&b{? zr*674vb z4j4fko^$^H{Fih2zQf~<4GetmlgNvd{3v(Kwv}OZ%T*G#)qwcF1D{yKGA#BQze&wAsDxGD|zUany!- zQP$sqvVl_`TgPb3QhkU+<6%E4zK&2gibGsr%a?hYB0||AYb(` zqZVgnkyBK|N?4F(NL^5hT-6{EOsTU9gfIm{NhxuYLg5BysN9RXtlkvyfRkN4oPvAh ztD6b!o`+rmHuyL)DA?qliI_pRBb96^@3=4P=|HEd;|xEvX;(Xt^%}t(GD2Bj9~$tR zG_3H#o6M*z=sIUi3kIi)cuvGRC%{Wq+7R)SP!LI+)wryzilUV_V~B4XFc7P=rD^zz zoU>(MKExDLpj{aRpbl+zcN0>%jq){7ZO6lD=yvsIoRn*t2+r0RmGhjc+U-MmuOmE2 zd!OZ}>h8Ck^JJ+!iP?IIN=h=E$3N*d>pkps%;h591zr83&IbJkh z3Tb|qUF1k6B5wa$U;?Em==0TWX~BGh)5_IQEP^in8X+xImrX<*n;?4;WXxD-ucaK5 zbPT~a=|ZBh*W9h73qi-;1>izgk0BI7Jt*hPG`UPvtGS@#SqncQo|qNSC&Qj$kK5DO z8FwprsBU|fp7mIsTeC7cq2^xSZW2@c}=dCmL z(oj`ZQY1qXWNE>M-#LIX6%(LiOur33X1IDFd8s5~vOP^&78TQ{%?qNA$-XyzSWrr$ z8K`QSemJ_KQZ#*5JOiv5E=U?6G*3b`X^6t~NOLAF&7CC0kV%6nlhI^BQfwX8&Verg zt{G;tYdCL7%&_(0C8a37ZBmjMsSma_bh2;y`};ZPF_m;tFoZNP2Tq?z7u1=QP?}Fq zNwTP?QzdZ*<{~X9hE&4BoMg-o3YwPI3s^7;)9Kv#!4bgJSuJg&F%d|bp%YL~8=@={ z&ax>%O+zv%Z61>YZZ-jGrCc+ZfKL5e)-B>I7O^ewoqiBKv=Ke>MfAwZjWy4Dbhyk8 zKL|H(gk$iQ!?6RzTaA&W*av+}lXs3TfA_;=xiP*-Z#G6gh%ROCbS$6yuzM+H^*wBi zzAwEiEz|EQi}Y4o$0GBfqjPz9z2oTWP^DvVk==^*ERU`vetEeZJoKQ8Tj{>nm0G;| zpsjOx=qKM_qPAM&tCJP(#K$Kq+^I_I%WL$X{FT;MwrSLySdSdup;3E$JBYZxk8b_+ zR{8kZ_4v2TZ1+QF_-2ILi1e-KcjqdR!Ahjh*7u`m&+15}C$)NOEn7){=hH+beZJgs zVR3Y`C9ycV#kOy-vCr99xhHjxP49G}?)0M`6lq&}bG?Zx2f1CCKy)zpD*)X9{#7tV zDCY(?KN)8~0IPHjzK;-M$f0}u#x(5*T+1+^}L?nU^VYIoe#~I3uHQ+m8ySptP?L#(jTjoRHMT>?N=%3f_=yRzf)2J zCTGX=sJa=L1;#0=Gnkw!GhDqs)))$hpxdG*Opb@?l_lLU164IlHY_IRcAx>nUk<|L z8i6BFm;p^F7J)M{*?_o(6oiX0+1Z#ZEhh7Y$z)*igN56n(1|TK)o_?xXD|naO#rR{ zFF~h1Z4tmD7Dd~auHQ+oj8t0s7X$wYw`{WQzes#^>}SVTva3h$=GQxiD(ukWYyS#2 zyNTlZ$9O$@rp%r>m?&!E|BWcxUswziqBwKshpUN7M{<#USm%a#-)5w9wWZQ^eD&&D zqLMtjhAYW)<;ch`?co{${vY{KdzZ~0Pa=Tc!@mjtDqK#UUFU!>VlKky-H04srGM|Q zL{b%pV1O_V6LRPV(uxhP-dG!{q+a>7rIH#gx11-0(Xqkye9rci<0tO1CwJn6FcO3? z-dbMYOyU{;W;k|v&?lCW;Tf0U@GAP(?+ zQ7sTXo&2O0W(e08Eq1q8xQ*v+2`?B1mL_N64hHJYJYOI`KSilv6hv9(dH8#5aCO2k z0kp2=BU%vxvw&S6FK_FRw%Z?}E>)oZHwoPi=mnp0Bs4ro4e(8al zm8)fCpa~x%7JEoRj)NJH)lvzzwu$UpT&N@(*Pu(pxZjfVJZSK|eWtvUGNqc~D>Isk zjSCp7*hTIe<)1@V*-b(HzcqZwLjV8( literal 0 HcmV?d00001 diff --git a/module/scripts/lib/telethon/crypto/__pycache__/aesctr.cpython-314.pyc b/module/scripts/lib/telethon/crypto/__pycache__/aesctr.cpython-314.pyc new file mode 100644 index 0000000000000000000000000000000000000000..a6c435358bd4a2f8521ffa1dd08996e849c76055 GIT binary patch literal 2024 zcmcIl&u<$=6rNqL?ah*8)6^+Sh|r`-92rzEt)i-`P{of{B8RrYRE9&yYO)m zH@ymOvfGnD5EdAZ?1bn5bc=83@0QSISOmC_RY@e2;c@$a4p+CE22~ zverWap#+nq-oYxIm_?tWnzAO(M`JaUtOoNOcvgx8-#)`)TBQD#f{d(i$h)NbzS&Mr3E^S+^ zg&TzNqG4QZ5Kih+c{O4REU@tL2F1;QRxkPW>w{Ld^!wgemY8YAy>z zsM`*ZNl;!$g<3$}%`gRdhbn6Yk}vsuKD=jDh#(eRf?Mxdj$bd5YTMdmE)A?=mDVA1 zi+F-nIcYJ`z5q>O1rBi0safUg7d|4+-Dbmz8%b`kh6pT|ZCHZ3RMdRWa(KHTd<#-@ z1TQw)kd-a*ub8~v{{9}4xF6^5N9etr=E*zSZ-(UJ}Yw!HS`yG9D_x@r2ou4lKSliVe zjXW4R&}W|NGr#NeUC9rp-x2U3QGwrR_X;d_5O59VlVep{ofw0jfX-I7zcFrj(Y<4u z#e{0Ogn3xduLUp&y1ZJG?~UZ&fM2p0F>!{iBc9RNBdq|(I2BFq#fE)7c37hZFP4l@ zGzuyofh5ChZ~uwPWjBSI3=zmn=kt65CO#$AlS8HGotOcAcN)YFIvPh4xkv2>?E@1( zHSrI*XJ)>m=lNOK{ht85PEP?ibib1b_8W8n;4$k6!Ak7%6oP#{9)LasU~ikBgUcQR z?k!Lp1FmN|4|*49R?t)G09EuQc|j#7%E{H>~r*3BSg>{Vu7S?VGXk>7s+sUhy} literal 0 HcmV?d00001 diff --git a/module/scripts/lib/telethon/crypto/__pycache__/authkey.cpython-314.pyc b/module/scripts/lib/telethon/crypto/__pycache__/authkey.cpython-314.pyc new file mode 100644 index 0000000000000000000000000000000000000000..32edc542fcdde003ded4037bc88bfe490b25ad18 GIT binary patch literal 3420 zcma)8T}&L;6}~gGJ1oP(5`JP>6PN_N@!FX64~|`kqT0bV$SI*Ci%1q`G|XJsiFao= zcka4bq`LGewrbUmR4JrJGE(zkq)H`EaivO?+Lu=91B6DpL5U8J?DJ)Xj7v?pndhbf4r;L5%L!tge_7fDsKR>Ks3V1G|_}HfeY{z z$HY;IOFd*-)TFV*bJ4!bPX zqCTzUsp}eZ)K&2S+;o}d=o~Zhuez#ba6>g=o-*)@8JdhrBP|gUue1QMK;CI~aMo|W zx5ar&8FOrI8##L(PB&DCe)oCZ1{7teXn-_d){uW4zEs;$XUZHHdDh@63oV2p+#s5W zmIZeu0hJcv%4lxZiy*g)+NDw+%it&j=CjQ4j9JJJ7<1*!tevB7W?+uyZO6_S4!6PF z>ozYRGm6EGYchkI+05j~vGay`tyIhe@q!C>k-M3kJ(J*5m*e?T;xnK%q0eCM7)MjJnT7u8{*I{kVlEuJLOyDwO9vhgo!zJ6rSd{Nnr}D4rnr1Oldm}rekSa;Lbp-mqYPdUSry4^3DRhh9_IG}${{&v=GcC;DaL=%y3nj56 z>9x!*ij(5R3(v5@P!lxqOjXl8I*Svn&+nd&gc5ql|8-u?dm7xLMh4-QvGoAmOH^`{ zgtssUJG+Q2_K=rg75{L@u=EnKg|syOtrfwOU3<=?j>d80_wwEs@5P$a4p)x-mKmV%o8HR561Dz-+;heoT$YyE(dK|#kkR=GgZ!_0J&DX>;19=!SNk+0#ARY zg)z~;YrZH=VQul-L@h(Y8A5jDiWElV|H=z3+K+Xu+V<-1L-g;JH;tTG${9iFr#@;p z1&AaSLrk495Xu185Vy?|V+dLX=ggid0m}}Y0NUt2Uw|nHKEW}7a5KSS)EWrE$0_A! zC>v6PFQOnIYLNm=&d}KcqrNy)05}-sCFTUj`Z~b|e|^rrPMsZv2NX}v*_`L-W!453 z&r7eL|IX0YSh2pw@pwAs#dtyYVGfdtvy#cZND)AXy$I``WINnbW?3O0?C}$bq5sm6 zAX(~bR(f)vI>SQiqKs6SHeRaZ~qGRc``@=sOUM@YD{Ve}!e&gWC7m1OZk$=jhx%JMCTQ`=h<-vRJ zZgywZyE7ZzCszBu=sxqXgQz|ZcW#vq1Ytb!r9K&EPIP>5?m4NKJ) z23#*;Sc>rpFjFYx{HnyT#O{Jbgk@aZ3(LXGFmAA8`uOonrYGwjD<8#_Q3>})>xp?T4wq@(5$&Ez+X5!*n;^J!i zXPuvRKD2&i|H58Bad9nn(RT%e9lFVn&Okc8E5_4J=v~LiQxK%+dhlKXIN`mfN4=oy{^Q-UO>WX0Tr<4g8+T0t8%EPN10adwdD!7=$>9Ht8J1Y`U+emH8M6Unf$`7tQ7Mr5q!1?BFq@KJIk4^|rx`b%YQ(1_P3flyG z-($XzbR9d30{fuz>ab+M6EZ+Ug}%?J$D$}iDl!q8{zN+dN)A1hPYa_$g<$U=@gH#=;dTH3 literal 0 HcmV?d00001 diff --git a/module/scripts/lib/telethon/crypto/__pycache__/cdndecrypter.cpython-314.pyc b/module/scripts/lib/telethon/crypto/__pycache__/cdndecrypter.cpython-314.pyc new file mode 100644 index 0000000000000000000000000000000000000000..09063ea05b008ad2c8f6890d4ac27c3acc1414bf GIT binary patch literal 4788 zcma)AO>7&-6`mz`Nou(gWzzD>mL;w%M=~L2Y0Huo8*ULtc0#3z!muuVkxi_YD{*aV zN$(DA%RvpJJybyrBsrx)i^e%61$uDM0=X6ma_G&h5%@vIcnrg09sb)$ws+V=TCaGn`vSdER z*deQ=OwF851?+torh&!M2Kd`i-bgQHp zQn7N&z(eZBilj)(aDK_utXi2`l5PNH0#yRsIjyLhno1`mTH%)}v?Ntct*V%sqh~mj zGkRPqy6SeQjx+@X(bh87aY-?Xyk=N6kJXt}QY>SFN=q8-hufr$R!mK% zT3K5$l~uW7uB6qX!K<971N@)@gMr=f9=T<$lJ>Xgfp!L-^Z3}4mc^7968xFyGc zngVzJ%g-A+)s?b-NAnEm!h(M!&6IimEF4)`m0}*3!z$Hs4l= z*$XN)`>+CBAQs-zI2QX>x~Nbk4QDjdC*V?7Wo2b8jpD4dyrQk@hMraos>3bcgm?O^ zQmv*f)x^D}^B10dK~b;Qs%iI)i|I9)J$;EUC|cT>4vzDzwf|Yge2cD zxftsAT;!`PT?4cROjM|(Du}$$h9E*!%4JyE)M(8FxIQ-5;X024s|kRqmTJazR}5lW zia)#X72d}JLE9{6xyN6^0K8-n=9+9I)xc7xz}{#4A(8B5+=TUyH7J5 z5YsD00p;{|^!1xIx3oqz%fyH3;w(O26J7@~(S*$*sBB)hbi<;Gp=u_guZb_-Jd8t; z6^%Lo?f|qM_N3dvYNcv(YDrVCr@|(l#N3DTg-#{9JUuOFwIeu+`0cjP$yj&_0DB3F ziSOIq=fw$yHs466O66~iOTcE$gLQocp!#G#Nenmp4>$TJ>;02o#)cZPR6UmZXzX!J z{_ViS8^0NR96R$Q%qIBt@RNQrFxnIjGzQc4!E{qdGzL!A2TtyA!2y1~XPYO7j&2^E zX^LYFQLc+}Gm&g0()C38A@evf)kvJFC(bnEBaOIRkINt3cpN|3h|kvJvmpH$euuD! zqE89CKe`@y!V}@Z+oerurV%~;hv@0Q#0NJIrkg_(jiIUf&{T6|yfHFeADMo_1rPP@ zkYFPAlmvTYTTUgA!O@18s*9-&adJJgC6NAuk77q2MUHIskdcYTuv{OO8^fpT!>1qK z{^ah*cQ=M#T+iHJXo^xpJW&@yP?kOF>#DL_^XwIA2c7`yH4tGz< zGhJO4(+wJ@y&bf`FaR{%X|#&P_3Q?(uT~RF?FO#03LvAA~XXwj>8MWd3~Fsz`3+jlnIz9zrI* zU#V;S4biR-f?h85BF=Tw1IV8QC%`kuV6!e{po(2sYJrg(2p++KBvwbULLUd{h+%_y z3@bDt3%LO|9MmvJp|dAi^P%e%octbC5UuqQp}!GL!r!B4vMCNV#L2oi*$~rp5nlC% zI0Y|69D4VSciz|>n{M{+e>e6{?7`rN$q$kbk9{J4EbjyZN8yDA24nCNLuBO0`-|@_ zHil2uhflf_HipkTk5A}->*Y6Je*5;jci*}D;P$3)Vk<)W2iIdyI5L?0MeV`sKfk>p z9$U|R857rICfb1783E$i;9uCe=Ipy8QfztWZcoIIB*4&FVBAxlyG2MbijS`SP<)OO z1AD2*ihdJwKyYC)pHp-TvL3)@yyjiGs-(M_N}Jzt71FYz zm%&Z@ej4Sto)%k}r9evKq#nDI8YH>%wA)oTV?vUXfS9N@J5Pm+d(2sIX9PL+v$sI*qG8Rqy$VP3}sb=M1n6A+(ZQ0LZNV@rj)&rSOHuj z#0X{G(2NQUhYN*bMJ*IeeATu9j+yG}DkdyNGX*5a)}4o84-^XSa6zHe)R$`D>_Ni5 z0wkO8n?+Gqsj8LBg#!Gj(Az--HiYfxU4=N&0aob8t?xkf@1KAYwgW5|`I;BF^wwb# zif;!}+}KVKT3guqI?wFm2DiqYksdDPjQlGAEk`F#$gz;>Gcmum!+0%_dAXOBpk;Zv zW~dZ`7E5-25^-Y^G+H|b!*s2xwT7IS1Tg@TP5f!5;)XsU4SyD~oZEJm`xSAQcEX6r zIalA`HO~Vlp4LLt*a|WXvmFR9>_0<<8Tvc<-k-_2FG%tWl6o4t!Z4{Fg0&;)e-kxP AIRF3v literal 0 HcmV?d00001 diff --git a/module/scripts/lib/telethon/crypto/__pycache__/factorization.cpython-314.pyc b/module/scripts/lib/telethon/crypto/__pycache__/factorization.cpython-314.pyc new file mode 100644 index 0000000000000000000000000000000000000000..71ec612d78983f624f54c754bb48685fef8e1fd0 GIT binary patch literal 2601 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 literal 0 HcmV?d00001 diff --git a/module/scripts/lib/telethon/crypto/__pycache__/rsa.cpython-314.pyc b/module/scripts/lib/telethon/crypto/__pycache__/rsa.cpython-314.pyc new file mode 100644 index 0000000000000000000000000000000000000000..4220bd7bf74ffbde98cd505370e67a6585a230fe GIT binary patch literal 6954 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 1: + break + + p, q = g, pq // g + return (p, q) if p < q else (q, p) + + @staticmethod + def gcd(a, b): + """ + Calculates the Greatest Common Divisor. + + :param a: the first number. + :param b: the second number. + :return: GCD(a, b) + """ + while b: + a, b = b, a % b + + return a diff --git a/module/scripts/lib/telethon/crypto/libssl.py b/module/scripts/lib/telethon/crypto/libssl.py new file mode 100644 index 0000000..e0fea00 --- /dev/null +++ b/module/scripts/lib/telethon/crypto/libssl.py @@ -0,0 +1,140 @@ +""" +Helper module around the system's libssl library if available for IGE mode. +""" +import ctypes +import ctypes.util +import platform +import sys +try: + import ctypes.macholib.dyld +except ImportError: + pass +import logging +import os + +__log__ = logging.getLogger(__name__) + + +def _find_ssl_lib(): + lib = ctypes.util.find_library('ssl') + # macOS 10.15 segfaults on unversioned crypto libraries. + # We therefore pin the current stable version here + # Credit for fix goes to Sarah Harvey (@worldwise001) + # https://www.shh.sh/2020/01/04/python-abort-trap-6.html + if sys.platform == 'darwin': + release, _version_info, _machine = platform.mac_ver() + ver, major, *_ = release.split('.') + # macOS 10.14 "mojave" is the last known major release + # to support unversioned libssl.dylib. Anything above + # needs specific versions + if int(ver) > 10 or int(ver) == 10 and int(major) > 14: + lib = ( + ctypes.util.find_library('libssl.46') or + ctypes.util.find_library('libssl.44') or + ctypes.util.find_library('libssl.42') + ) + if not lib: + raise OSError('no library called "ssl" found') + + # First, let ctypes try to handle it itself. + try: + libssl = ctypes.cdll.LoadLibrary(lib) + except OSError: + pass + else: + return libssl + + # This is a best-effort attempt at finding the full real path of lib. + # + # Unfortunately ctypes doesn't tell us *where* it finds the library, + # so we have to do that ourselves. + try: + # This is not documented, so it could fail. Be on the safe side. + paths = ctypes.macholib.dyld.DEFAULT_LIBRARY_FALLBACK + except AttributeError: + paths = [ + os.path.expanduser("~/lib"), + "/usr/local/lib", + "/lib", + "/usr/lib", + ] + + for path in paths: + if os.path.isdir(path): + for root, _, files in os.walk(path): + if lib in files: + # Manually follow symbolic links on *nix systems. + # Fix for https://github.com/LonamiWebs/Telethon/issues/1167 + lib = os.path.realpath(os.path.join(root, lib)) + return ctypes.cdll.LoadLibrary(lib) + else: + raise OSError('no absolute path for "%s" and cannot load by name' % lib) + + +try: + _libssl = _find_ssl_lib() +except OSError as e: + # See https://github.com/LonamiWebs/Telethon/issues/1167 + # Sometimes `find_library` returns improper filenames. + __log__.info('Failed to load SSL library: %s (%s)', type(e), e) + _libssl = None + +if not _libssl: + decrypt_ige = None + encrypt_ige = None +else: + # https://github.com/openssl/openssl/blob/master/include/openssl/aes.h + AES_ENCRYPT = ctypes.c_int(1) + AES_DECRYPT = ctypes.c_int(0) + AES_MAXNR = 14 + + class AES_KEY(ctypes.Structure): + """Helper class representing an AES key""" + _fields_ = [ + ('rd_key', ctypes.c_uint32 * (4 * (AES_MAXNR + 1))), + ('rounds', ctypes.c_uint), + ] + + def decrypt_ige(cipher_text, key, iv): + aes_key = AES_KEY() + key_len = ctypes.c_int(8 * len(key)) + key = (ctypes.c_ubyte * len(key))(*key) + iv = (ctypes.c_ubyte * len(iv))(*iv) + + in_len = ctypes.c_size_t(len(cipher_text)) + in_ptr = (ctypes.c_ubyte * len(cipher_text))(*cipher_text) + out_ptr = (ctypes.c_ubyte * len(cipher_text))() + + _libssl.AES_set_decrypt_key(key, key_len, ctypes.byref(aes_key)) + _libssl.AES_ige_encrypt( + ctypes.byref(in_ptr), + ctypes.byref(out_ptr), + in_len, + ctypes.byref(aes_key), + ctypes.byref(iv), + AES_DECRYPT + ) + + return bytes(out_ptr) + + def encrypt_ige(plain_text, key, iv): + aes_key = AES_KEY() + key_len = ctypes.c_int(8 * len(key)) + key = (ctypes.c_ubyte * len(key))(*key) + iv = (ctypes.c_ubyte * len(iv))(*iv) + + in_len = ctypes.c_size_t(len(plain_text)) + in_ptr = (ctypes.c_ubyte * len(plain_text))(*plain_text) + out_ptr = (ctypes.c_ubyte * len(plain_text))() + + _libssl.AES_set_encrypt_key(key, key_len, ctypes.byref(aes_key)) + _libssl.AES_ige_encrypt( + ctypes.byref(in_ptr), + ctypes.byref(out_ptr), + in_len, + ctypes.byref(aes_key), + ctypes.byref(iv), + AES_ENCRYPT + ) + + return bytes(out_ptr) diff --git a/module/scripts/lib/telethon/crypto/rsa.py b/module/scripts/lib/telethon/crypto/rsa.py new file mode 100644 index 0000000..91ca7ba --- /dev/null +++ b/module/scripts/lib/telethon/crypto/rsa.py @@ -0,0 +1,165 @@ +""" +This module holds several utilities regarding RSA and server fingerprints. +""" +import os +import struct +from hashlib import sha1 +try: + import rsa + import rsa.core +except ImportError: + rsa = None + raise ImportError('Missing module "rsa", please install via pip.') + +from ..tl import TLObject + + +# {fingerprint: (Crypto.PublicKey.RSA._RSAobj, old)} dictionary +_server_keys = {} + + +def get_byte_array(integer): + """Return the variable length bytes corresponding to the given int""" + # Operate in big endian (unlike most of Telegram API) since: + # > "...pq is a representation of a natural number + # (in binary *big endian* format)..." + # > "...current value of dh_prime equals + # (in *big-endian* byte order)..." + # Reference: https://core.telegram.org/mtproto/auth_key + return int.to_bytes( + integer, + (integer.bit_length() + 8 - 1) // 8, # 8 bits per byte, + byteorder='big', + signed=False + ) + + +def _compute_fingerprint(key): + """ + Given a RSA key, computes its fingerprint like Telegram does. + + :param key: the Crypto.RSA key. + :return: its 8-bytes-long fingerprint. + """ + n = TLObject.serialize_bytes(get_byte_array(key.n)) + e = TLObject.serialize_bytes(get_byte_array(key.e)) + # Telegram uses the last 8 bytes as the fingerprint + return struct.unpack('R^unbFr2c~5>INw=LgLU$JrRV;0XA!mUn!b(Ho@<*# zg|@BI9#XGCf>h|3D)gz_htx7SWuSJEgD|AXR1wB5CY%>iA)qWF8+9@4tLK`FY7df{ zdIcesaxCq+jvfiRSPKPf*NN+>D=y~dF{&w-s|p)yA!yd2V<6i`=BL5{+q*RI&}Ek{ zxV{siN{h}-Xj^8zhm0GlvVw4k8$IXK-bFNs<=My;Wcb*kgOFaTo6dq| zqb1n)5ROfj&)JweGFWu#u5%M%!lQScNaJ~EUP6SxE(#IKCEuo209Pd@%^7h4MICu@ zK@tFY^Iu66Q(r`7cYyqX^>vC8;HODG}e*cUsB$C<8uk?kSK170@Zg z(z|83b0DVje2>TQ&Of%&DaW|&Sb2`Q%{{y2V|QIGc|NLhFm6l<`-$=?Q{!=&A!@5x zSAzypsmm$Nff$%@zG{kc%7bF6uxIE9CZtf~xy0I0&2alQ*d1sqe2oyT)yFW^GhX3@ax#DyN5))9aN_9@BN@!fO3RMtL?mwl@D{ zbn5r))ZgWqyTv=X=Y`_E#y5@oUu>xJLv?<=aPp4yB44=s;nV8eqw$C1kIFxn9~f(s zKhAtN^V8(_a}V6dr8}QJ+qZx1){}iF|0tY%l>-N7x5hz!|2^%l_QRC0er#AdwqE|| zm(SOgGpqGy`QrUs8?$GJvuB@{CN@g5!_w?lQYy^71X5~lle-V9dss%-P1n$MEQXY& zd2aizgT;hnF-TbKN1PIus0S9S5sR&cbK(-44Cnb3bc<+#py-4Zc=7_!wg?T9_7LD{ z!Tp}#&VC#g;|%_QKgG5ohxzp~DM`|+L{gGBvp`Z_%d(Vs4dQF72&9SU;NVvJkTe%4 F`wvwU|Cj&( literal 0 HcmV?d00001 diff --git a/module/scripts/lib/telethon/errors/__pycache__/common.cpython-314.pyc b/module/scripts/lib/telethon/errors/__pycache__/common.cpython-314.pyc new file mode 100644 index 0000000000000000000000000000000000000000..24f39331b975e4e81becbba3346e5815c11db765 GIT binary patch literal 9663 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 PQ)vI9>pO-UC(-`^5}r?@ literal 0 HcmV?d00001 diff --git a/module/scripts/lib/telethon/errors/__pycache__/rpcbaseerrors.cpython-314.pyc b/module/scripts/lib/telethon/errors/__pycache__/rpcbaseerrors.cpython-314.pyc new file mode 100644 index 0000000000000000000000000000000000000000..c075d9debf78655339c27972a4aad0a5d8fdceb3 GIT binary patch literal 5763 zcma)AOKcm*8J^`!Mh67Hgaq+rA0L|+$J%a=t0GTZAxA}oV2Ft<#Hsgz1*dD zhk7^&AQVOGq-m<4MdO~@Lk{+#XpZiwhn{*UAT=%0AG~mnh*R17LLg*=T=U7o6ge+2p`YZ0 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 literal 0 HcmV?d00001 diff --git a/module/scripts/lib/telethon/errors/__pycache__/rpcerrorlist.cpython-314.pyc b/module/scripts/lib/telethon/errors/__pycache__/rpcerrorlist.cpython-314.pyc new file mode 100644 index 0000000000000000000000000000000000000000..4f2bd82736ed5bff60ba12c480772bf60e945742 GIT binary patch literal 385563 zcmdR1349yH^|oavm?I?QzM~u@&K*b~Kmv&qJBf2Rj$=FL#*ro2RxC@(N{-{)6e#yy z?)xePO1TQv<;(DsqEy;<8A+J2I@4{Q5FJ3!L*W$i#{2T9s~tQ`#P5J}sgwL_sDCTRz-b~v;n zB<(=f+MyjOX$P@(6ttr?n|U1uvvv%$M@v^{2y4eeJ5JILWo<6B<0b7d)=q$SqIQV+ zyTe&K3EIh08Ah;n3ba!tt(~>gpgl&?j%4kz&`y`MqgXow+T$ecXx1JN?Mz8KhP5X^ zd!nR0nzbiEJ4@1zW$kQePnNXfSbGYzb0lppYv)3nCuzsCb{@3zCG7;(=0jT`X(zIF z0kjJx?IhMNg0@i7PG;?5XqQOZDXd)z?J`L_m9<6CE|;{^SX&J33Q2nmYfGSADQS;o zZ7H;6l6E?4%b{H*X=ku@HMDCa?QyJK3+*~ddpv8`L#s*JnXKIaZH1&gfwdcDVQ#{VB9(O4_BYJqy~iCG9fS zo&)Wi?Ml{O4($~Z zOG;ULCA3#b$Ck17YG|*KwB@Y57TW71?JCw@5A6+-b~S5ng!U%sY-?D1Gqkrz$F61V zt$Il|V)_GDfDIt#t#L0+DLLL=}n~^FBc}yVHjMPZ82EP%A zhmlPZ^0+{18SzTUZv|4vh)+7t69Vxw5|EH51+tkDT|#~*kRT%=iSfS|NIfH4B;+Z9 zG%&JN`n5j@q>+(r67sY_wllIrLjEX_os8^~kY@z4n~^;dvQHqVGV&7%c~&5&F><NKrUnCatUb`$Q6uSDIqTjKT*Jt<64EM=>lnFS zLfQm!10y#|$jbt`iIJP7>-LI3ZeiqB={x%cavLK*mylNlayuh;NXTmf*~`eC67sr0 z?qcK@(zSj=Aa^tJOX)jr3gjL}?v;?Y1acoE_e;pz0(pRuUrESc1@a&x4@t;70{Jx~ z4@=131o8+Yk4nh90(p#)-$=;c1@br}zm<^p1o8wUPfEx?1oAsZelOL}`vQ52kv~Yt zKLzqMBY%{B?E`^4!^l1f`A{IwGV&)0`A8toG4i~G{7WEzX5*NywK1d7Y6rB;+fB zyvfL067sb`-e%;l67r2e-eKf#67p|>yvxYnCFDN>d5@8QNXWMWd7qJgO2~Ht`GAoR zCFFa7e8kAVB;*Hye9Xuv?MOjpC_`YMGWMB*WeV&7W1lOSO<-Rz_N9Vl3G6GzzE-eo zfqlc+zZI;T!2ZM7w+hx>VBazJy@DMgupfZgN;2B-4cM2%n5`s}F`I%NDzGfZvfF(T zzIT|wx-r&W!44PLA&liHWjR7%hcb4Ug7pyC;fx)jU`Gn92V+MnSWkiVWUQBh^%B@o zjP+KqqXgE6vAzn{TVVYd>#tya1U7)NfeO}FV1pPNtX$820vp2EQ00651vZSa;R-fD zU?Ui_E7(ARjbv<;a=t+V8_n1l<$Hq#b~IySmG2D^*f_>=6>O-$#xpjdeFOkb3=`Ny z#wID}8!oWPj7?Fn5dxda*fa&R3+x!ij#aRc0-Mg*426TE1a=%_$1C3(EwGu4ouFW2 z1a=~0Cn?y`0-MFyYy}%Du#*`(MZv}iYz|{{6)aa^d5q0du<-($&se^KO%PZCV+)iz znkcY^j4e{WH%VZHj4f8M$pTx#*ir?XBCutQ6)D(Mfh}jOSiz6oIT({h zT(}=j6qu7Sm-4-n1mvkoHb-E7 z#sUg9S74hN(-kaFU_r)03N}w*^^9#%{KR~LH88eS`Ch)j8X4QBU>>p#7TCp%U7}zs1a>K7KU1(0fnCPf?+2tRlnLU!Bz?E2F7kwu+;*)iLsj%Y>mKfVeD1~ zTPv{J7?VbAsK0dryPdH+l<%z<*j~o&R4`3ocQN)01=}F7yBYhXf>j9Y9>(rf+OdrS zyN|K^mHXWxum>3XmGZqxfj!9BLkd5g0{b;%4=b2UV2?2Ns8UY1z#e1lHwsoIu*Vtu zt%6kx>yScGWI70 zs}tCBj6JVlK7sw2u@@A~FR&LGYf`X)z`~6EMZq=;teLTw6igRb3uCRyH46%?jj@-N z?}Y^R3S;{ftX^QRGWME+Z4ub(jJ=^?4FY?Uv9}a#tH9o7?5_&eD6n@J`?Z>Ig0U|Z>@yBDzPc|-`N7o;_qcE^>B{Bx-r&W!Oj)fA&liH=Q~eehcb4U zf}Jn0!x=k5!7dP355|sEunPs&ld)b3c9Fo2Vyw4 zHc-KSCa^(_4OXzr1U7`Rp$c}nz=kn4T=B_Q2y6sncIA6l3Tz}}qZI5afsJNtj8eB( z3+!md#_&kd z!`QJ3c9X!SGd4rPZWh>aj2*9Fw+L(|V<#xwy;Wc*GIo;kz1svfi?P}5&o*$)elD<+ z89PP!-t7XL!`NH}yF*}kjLlQ9y#kxhSiXYYDX;>@7AV+V0$a$~A_e<}zzP{#tn|z8 z7T6NTmMY)-rNEXkR-|C}2y8iH#R_-t71#>KN|f*2C$N=_l`7c%0xM&zT)`d?*eb?W zE7-3DwuZ5_3ihDD)-kqT!5$Kr#@Gf0`?bI-7~7~|4-3q}SfzqJA}}XoE(Lp3U~a~$ z6znm9RWnwjV80QVhp|lx_PD@m8S^UGZv|Gzm`}l;5SX8_fPy_Ku+5C=3idmJ1sMw| z*zW~a&)62_9(zh)4UBD7us;Z_k+E&cZ$2%s?TqbE?!P|@Y$s#8l_b}?g@DBlYU>{7;lreJ>&*kz1eu3*gqyMnPR73?K}UB%ed3f3a9 zYZ$v$!CD1&9b?xkSew9ZVC+T(ds$#NF?O?py&|w%7`s)$_6zJb#(u6~uL|sT#_rH^ zj3@{A`PT%tm$8c!%(&Kw6+}PlF6B307vDp~Ah5fYI(kE3h#LeZMGoS8Zwd_2gTU@n zeCt~RLkuA>DT)x^ds|?LBm^eK5(4|Hzz|OeOo}K3_Kv_1RR~OqD+Km8fg!dKm=s+I z>|KE&!Vs7gV+ic;0z;f3Fe%ay*n0v)v>~wHDfazCV2C*c_LOox-xnAn4}nRshxpBZ z3Jmdwz@AaQ_kq9=g$V3f1^ZB7h(!eUoPvEMFhnE*lVTEazJCb}af!gB$V6Zt3k=bT zz{1LJej+f$C<1F%uulbsNJU^$tRjB%Gl3yq5ttOQ2<(8s5VZ(QidzKsxxf&+2uzAz z1onl%5WxuSb){eTrN9u!2<%M-`$}MlW(4-O(#QN-V2Ei1CPg*keBTHRk&VEl*hXOg z78v3ifk_dL!2TmJL^%SJ;v9i}D=@@50+XU0fqf@1L_7lfNa^={FEGSC0+S*i@x31e zhUiCNQv73qO*|XSDnSe+umh6qwos<{9wH%ueW74Bfgv6em=qC--^>yiq9TDwago5X z1%}v2U{Z7>ux=4GXIaV@Tj+F$KBQT1V%*Lvr_Hm;v zbf~~6UNT!=JB05YCNPSa%$8RW0Xtk^6f>DEuOk9>gup0jG8?Oj~vu>lI!M_>aP8>C=;1vZ$m zAyOM)3-uG&P{xKS-|H{1;f#%t`aQPL0D;*V8>xJ6puk2kHd^7}Ac2ix>}UlWEU>YR zjZ?580?TD=yn+p#N_8_qJG?Rz`9$pq&L?R-IG?N?$@vtmC+AbOUYt+Uj^g|ntvBb# zYJE7LuJz@7hSrbs z=lm3H1m|-!JLhw?k(}pgqd1?Zjpls5Hiq+j?P$&mw6UBo(8h7TP|M|fkv5+5LTv)) zi?xZIFVQAdX3me4WvMoqb<4CVoEK?RIbW_#Y(M&EULLJC5@* z?Rd`1wV9l+(oW!fwRR%sYqXO%U#rdHe4RF%^Yz-vW?3}t6xMCf=5Su2&EwX+>sP zeA;r>`L$xs1KJACH)|!F>)J}rgIX!)A+3z_daa!EE!ry18?@D&Z`Iat-l(nRe4Dn8 z^X=Mt&Ua`U=R36xobS>q%(Cp(HnMJy=HUEPt&;PfXim;g(_EaNuDLlsL#yKar&=}V zXKFQ^pQU*?KU>?x`8irG=jUo(&d<~8I6q(Waejg3=lnt~V3y?~Z8PgG)^yG<(Sn>` zs)acJnO4vFW!e_bFV`A4ze3x}`ITBD=T~XlIKNuk&iOUk4$iODc5;55wu|%YwcVWG zpzY!OM(tFyEH`OCVcpHzX`J7pozD5K+8LbRru~%jpKE7we!F%S=XYplbG}zQhx0qN zb2-0DJCE~UXy`&-)py+WqC@wm34p6ZsYuE?dP2TQM;Y2Ql`Lo(xoc~Gt1?SIccXR%{ z_DjzHtlh);3);P$zo^~Ed6V`4=V9$voc~38kn?8kA+szmX}@M&i}o<*t=c1;w`q@Z z{<8KM=dWl_aK2xAlJi%!-*Nt$_Iu7>*Pi114ebw{zo|XV`CHl_Ie%MwhV#E_`#66` zdzSORX@4@y@~-wA>;A4i&-r`WpE>`B_5$bcYcF#CPpygb5414nA8LQ${3ETI^M7eC zasIK^!ucm!E9akTZJd9mz0COm?G?^H*YvFVDI6qYTl=H*1&p1C^JHYu7+UK12(7xdONbO6`dum^C-b?$M z^P{wHIPb0foAW-}e>m@}eam@2?K{rNWg%fNz6ErYd#AZ4;N7?O>pA&|0I z8VV_!rD2e|u{0b~ca}y#I)o)Vq#Tw;LOPVCQIHN}X*8t6SsDZB2$qh9)Ptq5kd9<& z9HgEsyrOA-`u`~rzf0m{~8o<&tNCR0q2GSsw zj)gRsrRk7{urveGP?nB^G@Pa5A&p>ZCL}vcCqNp>(ut5pnWa4m|BYt-EJ$Nmnhoh_ zmQIE=mZei5jbmvJq+FKfLK@Ff9;68@&4VfHaGx5=gUI zS_$c7mP#R=!crNeIV_b!n#%+hBS|*HIVXIS_`RwrFD=Nn5A8h{}!@dgS3dH z4Uh_1s(`eZrHzo5u;hTWl%+~Y%UE(kDq_h6X*o-7NX0BwL0Z95HKY=jY9Os-$pfjB zrA?5^SgM6o&XO0>Dzmh8_-{4qeUR3$tOX$wo|LTX^? zJV;wvIv-LaOBX=eW|sCs{I{L;7eU&=(#4Q=vUCZgT`XM+X*WwhgS3aG%OIV~(&dnT z!qOFxPGjjxNT;)O6{Is*x*F0?S-J+&nJirk=`5D6gLF1a*F!por5hlfYnJv#{C6Jf zZ-R6_OE*KhfTdd?UC7d{kS=2BHb@t<^m9m;uyi}5OIf-D($84h3+XbJ?u2wXOLsxK zf~8+Tx{{^4Azj7NFCksc(mjx_Vd-8-*P5lh5C2`q`uic>z|sSdZe;0KkZxk>K}a{V z^bn+5So$@jTUmM-(rqj~0_k>^9))xVOOHX?%hGQk-O1A9knUpXw~&6p(i4z=$q z(p!+4Sb7^$n5Dl$`U^|%Kx#Hi`#1de66@cE)WXu=A+@sf9;7yw{sHM_mfnZ-3QPZl zw4bF9Aic`chmc-l=_5#yQv3_j>#X}2(i<#&0_jbbK85raOP@h{o23Jg{>swlklry% z`vv~{8|%M>^e#(ZLHavOUqgD2rEehpgQb5%dY`5LK>8<3-$MF;rSBkp$kO+aK4R$y zNdJP=y(9zD$1G(+`h+DLq)%DOg7g_n*^myf)D6<-qO{#hy5qkuSbqqlFImcg^c72o zLi(De!ytXb(&3Q)&C(H&{=-raNZ+z_B&6?H>IvxwmUNQbgC7}8-Z4S{qxOG6dVs6kovJS7E*tf#z7iDrA5u;LLP{G zJn})vCml0AxbDMPnhEJRmQH{)pQRHaxmY?0()BFOg7kBiW<$E0rIR83 zk)=~0eLvkc9W@pjj`GbJosm1Ydq&3lnb^K*{Gt1EN{Z(f=(=Cue?(q=sAh?~G4fx} zc@9^Jdvm=z7>XzkTj2HkT@m@n1%AEK<8rxu;{QV57Khj4$`?O%_)53F#jQuc9;Kc- zw`+O5_?@H5d=8w*uY0z+BR}UfcBZ0S>?)OUU^f17WH>VSWM~;7ti%u5He`pg`e*nv zOR~!|rejt9fDCVrH#3w?J5q(Zm1KpwmuH3!DbJXNH7TPr#&_G_ue8Cxz%k$NtMce| zs~nz437p0$=ybRebjq9qFF=@$ldbCIBRzlCn@5)0X3+J*r#!=Fi%1z6>*?G4S)XG_ z9Q5xoB_B{zLQ#YxZY( zd?CFTinqU8us-0{_a9oY)#(m|JbvH)oQevM&l9Ss*x$X%uh%(3`;VxostZ+!@9xds zpB;32tMuOV^+TMFK&W1K??1Gn!s&GcgB2A)x{hPVj@`4rVXDg!a!jocy7j5kraJv~ zQykTeQ>#2)cW~;IYIhw@Gu7b>d8&2C7Eh>gvLg_f8g%N8kh5lLdH&>i4rgtBU}~M; zRqu5Z;0c6+Q@x(bsUf-^HGbbzH{BhzOVos0qJXiJ&=ZyOVH{x>JS4hikpK=OKh0RH-3R^Ad zD5Jy5Z3mEgN%q@f%*Z%&IjJ%- zoPu*C^G(Nh!6N*#Cu6;dm7AOb3r*hhcQ}d7Xt*2jZ*OMN-mLxEp+?j_)gyCZZ>ByN za*)n2Rv(6R|6vuVc2~XA4K~>0T~Ff(+ScyCa3p&&UhCQCqWp8WU73Hw(yNxd~geFP5KYm?9WJ~SD7;qWRde2zLg8BQFjcgROnRBWzycm>k4q5>`> z81j02Zl53JqsrEgMzX&j$QN>iJkAP7D5QHT>qBnvasN?*nl6ts9qfnXn=@hOSGKHf%d@^ZEVJ7QpJgCn+xR#q4lzGQ z?Ho7(st%6x$cW=?cbjpYX7>K0@+$rHp;Etpsoz)49^7gCG@Z+InA`AAl*2TSTu(Oe z2M!TVtptv<_)tUFp8vm`TPB>FZQD?CZt!MXJTADcN)NnQ&>r&p?OqhTXm7Vj<@d$8 zbh?(2IvfaTZx(Yff>Wgg$l2!Mv%-MIV@o#Js7^JGm)$!M?t2g=@ow zYn#?pw5_WSud8k@tZB)uY0dF)y~V!mj>WGX%xyP4lWs75B)$e-jYbwRW(*|h)`~E8 zXe?8V`-wJ#JTL%Bhx_TuIZZt$H04Z)+(?;`pPxu4noj4CX?r|rDLE2<2p$l${laGJ zwkh+goXl<$iMA6Xw58)L({{i?&~`wc)9DTdOZ~Ozlxz)nU|>3v_o#XP5Xg$c+TD@T z7U^{3>L((J!AE->^GsF}vE#CG*2Av0Q9A!+E%GzZO!p^DHA(b zVla`Kd}~ZxLPt_!$Dhi?x+o^nah91l{@|Dx@@#RJp*Ku}w{-V{Gl_|H)Xm1YBWU;g zyp49p7Kg{{sPwww9_eNl_PUnbb*(w;Q%<&FF)c87HWS6S#&W+XYrV8HG4RC(a^m~HmLd~%YW$W&Wm>J5!ys>!|C zR0;70FiZBi+KMW|MHNjOU2Pk^;f>zrqPmvcy4D&^ts?F}#?K%wWq-7q<6&mjfm;^1LTAGF=mUCRqBWrJXowuoOb6Z_wxUR8z-L{t8 zZLK-mQ_j%b&I~fRidQXI{=sEi8w4@ z8I!w3RBM#P;_lX_?Yq&X+>yDAx|Eb*&bG)_LKC?~wzZ_B>;PfwxSzR>`@OY zANJHYuiDa*yQMX!A?4n7RD4qmR&r0%($vv*n5|P%PWCI)mRqCzEFEW=l@$lU%A@iE zp8ts+=>+nzj)14a|n@rq@tE}&$gy#MJ>r>?k zCMAc)DwM$~7nR5or%t0IDXAdgR7%JaR#~P`%|Xy-AeXziz7kI*Yuv8(NAzhrt~s%^ z21A&<;zGN}#qDyyC_0`-*SQ<;yxBd;-srEV$IatHG}}YZo_+p?#7=Hxyz5|c%Y%A| zJLltUs@l|6)ex>~Xxh31ZE+)_I?xtVhPJpma{;%-3%D&_nEJMObOP%PlGYK=ERpnV zI+Btax+IF@TNu?G({YxWyZIoP+dogoLsYN3l*SJQb%9V?x5hjk=b?_>UlnP2BgH4T z=cdvp=I4mm)|%|psZOdWA%Brd&o?q|$qZ&~p<-EL*2Q!rC7Vg0J1*k>NM)Hs+YW+6 zgYxQK9zOznLT+EEO!uZekB+BSRCm`Q1jk-hvQ(Pl5gj^&)Q~N9O#66wPTT^yM#a4f zCcmiv64(1}Y;sn&IYVJ*sHwgY-fv6hJo0{&!TY%|@l7+{^V$35r{4RGNo1Zu(%nQh zOC-I5j--SF7*zL{ALah&ILpjE?I4)jmyP=0aDK-UX(P_hjCnELAl-oWy82*^PbR;FGk?~5KJU5Dzbev^Uo_7$W>_-ux^BNJ+Bu{OmX;<=SVx<}WV}z6* z+*o9IOoL4(QsiD-CN6I(DQ_!rhD)6Mz%USY1)57Xx8!ba&CyfNM0-3#3@TC+Y>kSy z(~*>%N7gG;3`UKM={U<&yzC&T*q;nSFmir zh$I>tQ#k5)9`9g4gJ;-27tK62`0XxFRTYLjXz&Y*F?Z=gbS=JP4|?kAy&;Fs?XM4d z8zB~f>ddO9a8xH=DvS;VWl+{WhY_xa}RheK_1ionE z=2WDdbKLyE$#t;8Alxmm!y4g!O-E94J(;ff?2S=An~t+gxZ4kcaNK=>ImK?hF6~ER zD~NLKp-1(;_P8UgNU(>{jrHpe9g!X#-TEYc5Q-paIAw$=A-WwAA#mY|X8?@4Rxggp z**mD>=#fgaH*Lz@4zIZz!!UTw-I>KSHlqy66}B=`uE1g}C@F(0-JQ9C2W%U7z*dp^ z0ULKlqIdvP4PyTSc3LC$6Lcgc)X`9hT@lqUrsFIV``&{f_CVoD%Y3@K8p}V>VyAU? zWSrzqDXO!*)W&n8U?Z&P>FTypcevEeldF2TN^dR= zw&Vs|b3!Tiw4>wIpv1}tVTd(WK0`-RaxeZ=R@O(kTRP4%D<3%sR`w#BO0Yi9p^JrW z=IRTaGJV!*j1*;zOq(j+6vHM30NxIV3|dXu5gQ{BXCb=Nj&jyBB~)_hI= zV4Ht?*uTA5+tHG{qcvw|${C6W6T;m@nPPCW3C37+mao&1lsrfFE8N@_#Z5ZSGB;m3 z2yXHiN7PQ*FEU$2o-@uAX~cyhio;lc#*<7Te4##o71bc|ijv7rzYBvTF}bh1CkOFV z-q)F<<&jR*B{JztZX?0G(L5U06xa%BTvK4<1<{m2xyrVVl&i2X8iO6mFxV-uxp=VS z;=zu)iv~NftTh<^3hcGU@OSA*N?xK%q%hnaHQ1r!EHnJ|{};pMX^C`v2(Z_-Z6bWH zClU}Jw~Juh0EyjhJ8NaLP1e3aRT9tXmNl(j-?rKlUhQexF5|naWxNX95iWK#RaUiC`ofjI z=3;+KuD>-WkaBm$vrO_0VOnC)j_G5GcHa`wD52X>^;est{1qK%nTuZ^1Q&;**Xwua z&Kf*_qcuLpde(Gi$^z8If|1 zjq9X*2GhQy!dYTkb|#Lbud8(Ha?|d~L^;y2e@1(>6#ATg42hlCKMGfww~^9ab{`v&xI;ENzQrb`Y*Y=i zjH{o@u*stnmd+qlcPgJHLLEj&QbLuf=NTrkDk+2-2m>&bEAdpEw)Z zQ;raV9zqn2M$h3q9Bx6Yy&@h=Y$rC!?wR7ALR*(%$0qC~fEk%B9(O}U9p;`L)rhyC zi9~FGR^>p{MPgQtjCa{gP9BcS7c4^sMn4omt319)d2Ux3d2UxpN~f)w#l#92!;??p#aLL>_+L)^_#LtHv8Dcm10&k=MN zV{UG#->G=gK6J+Pe2Rhw0PQkOC}Mvr1Vr(Y8!Jn}?Qx7X3ELM&#|@Xre=^jzaCLa$ z>ZUbX+ZuOxjk|eaRZDJFYfg2_oAFWcEisrl5VlxjVt+c45~_2xuUHe+SES>T!o*(K z21?oZ!ZVakm7WxwgkcN1gR~T!wprtSFe*`g)7@eXZD=YII1F!MH#(@J-6k7{QFX+d zXennFZVIp2)Ku$lt8EC^HZ-r<+LF7qHK#GQYX>VKI#`2si;|SR)`AtW!eu ztrE}|ML;?(DFhrz*7)n-Q(XD($TqBK;grUC3Y|MW`M8)Sbg&qfijRJzS$+=r8kCT8x0PA`*5W=<3vWKqpx-*`WP&7;TOA#3r2*`crAYI;tni)Pqkj zKL!nBY_@t(!}|X1#wlE<9ih{d>vnbW(qO&S$lVdKEjDR;0@cfSN=D#nr*cRyxgfbN zP%?vEv#D5?#yQ8+k(5wH>XRrTb6AxW|2d%jva0>bxj1*4GHDbs$#^sFWAgV>DwAgtzY|}_t2{q%CLAlblij*sD6{MsL&3K+|BRAt4xfyq)z8R-`HJT2R zb1j0g*7%)IM^ZvuQu*zOYP#vTr15(p7F`ANJwe*8G!hZ+DWUVFC%-473hdQXWEwgV zhs7!~a{y0!_0wD`4dgp#ZnX6#b5~N$#2dx0YFfLYZEbCMZEce`(B|D5_HJ!n+t`xZ z*qXB~<(`*#8PgEM@2-L=)_A#;j--S*pz?Bil;5S}lE+KkUmpn0!$e`N!OL}YzVzhf zNZu5t5`2to`T|yBDI`28R@YRsB^J0CWwFV{^{~8aqQse7u?l8zYafuT3k{N9BP}wZ8X)PT|3Du^4w_y&elES8*d|7$IBQm*IhZCnMjRr7{ zz$9uR9g!{ZiB*Y-Gq8h&CS!cCH0~HA@5%hO{IYO;YjXjpRX^@M;c2^DdLSHQs>US)$PO!im-t+J>bUg zCFU>avA()drdSJ2?2)P~tJ%G@;gpbW|?+a^+0*>qA;h7n0A-e2_v9*75d zL=x(n5lKvI4c2dmz1CP?Lq}3V3{zPjiW-s7aYbi5blE;&+Dk0wBqlnl2SB6(q@~&6@u+!hXBG8f> zXwBK&B`&vv2?qI2hY{Au7oa04q3Tx2r$@P5IxZ>XqY236K6el=<0=wCkCB%CEIMNv zk`U2p$Dqu{W5fV@iDo`FN^X+koNDF#E&hcVU|+DJ!ILR?T8x;u)YbX-yx zO8%nQ(O8GA8am9`Tui4-LyGF$?@F|sU}QX-Ov+sX+w>46sUEP5PEa<3Lzh#*EOjhT zr6Vb!%F?MT!UnM_DRiMeWU=4t?ZB0*aprX73f$R5DLN9-wphf6crh3t_M74+)p#3h z=N-Xl+f7bgLsuU?xzuxtJZ#K@8HBolie`yWXVQ_BP>o7!R7jN+LQxBetq=l!Ul6NnBWo90aCGvPI zF)#IF32H9m7-cZ?4x*VQW^%(y2@ykWF6Tv!Cg`}N(3P6Yl?dLa?%Xmzc8|j#Gt#5` z1)VZI`8tUB8l+NF=nd^4jwwGbdSX&iI~vnila+Tpc_)D>Rs zYF^-O$#u8pRCP(S8_yJji}%48Yh1jHj--StUFBkRRM(A;O9~f<%+vi2m(vjp&4}xbXH7#o?zQdDNX)u13CuEy_Yhrp+CL`R9o}KPqqJ@EDdEYdJREvr$0IwMCogTu zUD}$nEajvdo7fVAcaOprYrMOTj--TYSLIz%6z}M`r0{NlY1RV2Ug^O@DPP0|{+7;| zhMYr_J&jzTSok7M1sYF6Dvdjd9`aJ4b|BPpS()!#AfVO5g3rM~~g^9%}*rrhdJ+!DO8yjHoLL8_r9gZ9DTxJ~QaCug+H zFAdKx<*k<-;W9__{K}Tx%GMlb%Dr4W;i4=t2=_d(#}eVFL!nbb^{sk1SCogNw@M^R~OT$gr2+Cd1>DEo>D>7V-;coQd1zNfs`O z$=)z7j?Q}nkATxiXcB$>(`mSkW@)wVSm#S<*n78SLlT4 zNxb7aG_&Rd3~p!}Jct;?giFW9HZku;#J&tB)%N3Z#C?lL1#jjM3|9u5LR;~)tsY}y z%+XMW5wa66^Ps0~^LT_jKlLNzk#TJ^`1uBr%@RK!qa!IHeyFY!o8;zXcANNF29n4G zAsv?_(sD#-z9ZyNo-+THPMDsg<&8(oh-)LF6$V&t%ty?NX|zevcVN8!gW)k!=?zE5 zEtx^E_o!@^2*%w;N~kV%-eq6}*+i;ja;en4712jhn9K)s!t~_Q7@le|Ur{Z{6oLf@ zOQw!in`HWsu6~z1yAWiGQ8I%}pHSH>k?C1Fk`k&+>B$ADl1e57^D1QeoKBdYWa9T? z8PUfSPcPoG7GYJ8-w0^>E$d5-*jJTyxDF=0zM%T(lAfDEujrB)^!kR%W{F-e(vg%< z^-7&JQYD36{UVMQuUk{t^&OorJ=xVeQeuANn#anX3BzNWXcFgpy3)M)G_?w;Y~<|m z3TE)eh8nQOn-(&H5~?`;aihs*RZ@6klpPUm#aO8Aa5mzJ32(?^JXq+C!lx;PZl~6c z)|aE$wBrnKA|sp>#pp@fx#208!@kbr3)F}35`;=@1?Cd9#+Li(Wk6c)RIC_+_4Uq> zU!R)z;PLHik0Q_Dyk36;?WcrYK(L-TSdVo`7;CVzOFY0BFBc?M6_^x11XYnB)~|$K z_n{2R>a1E)R%ZoCNf|~TWq9)sjXpwI^md>6td!5(OtfIQaSI)8 z`4)PW?i@;pxav5hKC9z6gpNzfeKnAtiNxB^-P1FD@26orlZb-z5_w}6cM5)`?dTJ; zcw{_-P4@Pp`iS?iZsEQ$ZHw217q4qtzp-t7O?Z7x^I}g+uBSC;Q_6WcI)Nz$ANyM7 z<2%GhN~q#hKGsG}DbjIC=VQLxNlQq?gX4k!YkYKx;#=pVVz9}_K~#l*r`kz%he0M- z$Fdm&8)lhcJc6NwYEpmSFi7WNDtZv+2QYtr?EVP-=>AxaZS@0<3mj-S1(8PL_tUha zFvoOkRa{`Kv2;4nL*RL6?kQmSV)(l{M=iDB4n)>qtuTkvB&OQ%nAgMP*(h9>0}F8F zqn{Nw<$8Fv<<78|Rt}r5lUhQPgu64B&?I49=2D&{T*{M#%Thl{Xpd)}LDjLuJ4?f& zPl=qA5LZ-RxGZXpkd8}=FYHSO6}vlZ*f}A6>B}!$F-O`?YM=uszTIEBi8k@HI`keE z%WjjV6R8$o22E4$4oAc(m%*>8mifg4bV{f)^;ZpJbW$aSN|8&d`$K-G-@Cx;s8)CF zoSwd%8bTMHHu~VA+pDPTh!x~%&cyC+ELT@Nn@v8=fbDU+djOVpR3A7m=^AegIKbfkxEN0V==6t+H>+Ck- zWhfYXZ?tWuMx8P!b=wwF>b6~^qzo34HWKR5LQa(g^$jXNOE@*O}^6*?uv zHuVme8#M~2mtnRs5(8O8r^cW8Ngrw-OM@^TY|#s@Fi{GxjO|3A^m^*B2|aBWUSEk9 z%iF8nek)syjPLAfn1o)AE27_wYm)NL#Y78bG}yS3N@-~vHGqz!gsNM=%`l8rNg>_{ z#f(T)n!>wPC`g*}t`^Z~<7kUyx@Y(qrm;`5pqo>~+)l#wVOXgj^w*j3l)=d31uWV^ zCxecwZl5#oVbi9_BT)>%#BU^aySbfQMU$_qaWxb4EBMW8Yw%)exZE{Z#t)ZE8T@c9 zHmHZP*498u%HW6B*tW7C-pYQsG4+0ULg#iH_rQ80w55B1C&(xv>Zu;NG0G#;aY?x` z`WmJwcf&?H9sbNu`gq~V6q{M&*y6Tt^3xowg8TV_20GKJ^xtH1dLnNSp4#xzm(Q z{isbB#G!epN`5azVlOx(+C-Bxn_z0(NrO30j%k~>GCXf3zr=iFxO8LlJV#5eqcx{8 zlbmR)wUyUU{)&6q7_7=`wm~VOhe0WT=O*-v>`EmCVDQ;T1s%@n!ywcU=_O!V- zhuxc-SL!XfdTUNF<<83(MymBE`L8`hBum^ok&dK1YS9q$oAdS=rZd6F~5zKjPT*s++#8v#c!$m3(3-Unuv&tZU1N*~94tHbaKX%DMdyLl}c&e$FM?2;?#W;svcEHaz8$5Uf>h=3;BU7W; zPoN6(so08zcTcXP1>3=TPp6?DLt|Wh^R74pH4uzHMY@*4le8=cg(qoQJ_=8w3`#$u zl%VuyZ6_sV7|Li_J2+-(2gfY!O#Kw8=>sIDNjJ{fu-4j*!&9l05KV*qK+T@U=f%;`t zQaENfXO1roxg%qm%W>W`Wmcm+rfKgpz?*WYDdvLcg))s8MtfBP~+`0NNQY@n*DIvnCZgydmo2BEDatjPH{0z@;FQnb4jTd^|KdSEm%`V=A)hQi5;56hxW^A^7MXO+5|1IPK|B74UZ-$TR@F1Wl%26T0#Sz zg;^^}Nf}z;dRr;Cz@^**m!-Z1Hs;V34#8N1{tv-iYxJ+6BPk&Qs!ecNR1-|cCFP!= zCODK=iC~JNj$5ZaO6SC%`AMG}V=$N4&I@?ysW}aBoctKFljRuH_{KKZBL+P4fH!cE){BbMlkfZF5(I=dNffDQ_#;5H8u!Jh!4H zx1u#?W6DR+G}jr^4ufa=V2L%J)zXoaP(`bAosOuvPC70rI z>9qJWKk384qv^@B%g;WHA3)PI4Nq-SqfZF6@QGP@bRy$Ta{d_?pljX-Gp>{J8KetS z2`xD{8qMmIP|Zqjg&|c^_|@MuL1k78oe+QKCw-XpC>2@6_s|?odVKyxYUX-13Up+FexyF679|l{yZyM=H zN{F{=uX9m$$AC>bE-5z?J8QhF%UO>%fcSl7J}kQMHyA7C-k{Us&-|pxE!Fveo&9h; zyG(k%3ER46=+1sPLHP_my+b9m{}9|}zq6dK1bc%jl8jc+jWgv7z?)v)9d z4Ql7C;!!4Upjel|q}QiZCvV|mq`a}CXOBd$IK?w~_5~Ht63@=1BPpROmNv^JRZ@7y z9%`W*FI8?|1pW=341eY)P3YBmlg4+AYK_T~f72E1vN`BbRH+Pdd{5=FM2?H;NJ^-J zq~?oMNg+q%a#9@1O7x>Uv}3lc^kfOzu^r@uC^VY8>n^kF7U`~ILb{5=Vx0audSwx2 zohM2WcumJ7zs?~?*Dim81MwO$5-V~ z+)T1VTNwM2npDlgrA;s-sHSP?O*S7*LvONerw755LAk?*g?W@MqHzLc7!qu<74kH6 zAx}duPW?1Ad&&4V8#lrsu-e*gq z$E7zGu2L@!WJ=5-ewI<_ieb1(rhZfhU9u@DJLPzVGgviU3B_)+J z8LZ+?oEix18HRJGC!HK?@pQ)so)z1lAc7lhvfaT$g5-zL;^maN#$z~oQ^(XgceNw3 zT59~kN7k8c|n8MEy>&D=;WEA@SEcDbF*dMfWtwV z1l^7W-O*LFt_gEA=Bb*~`hy6)Aaqzv7T!tOQQX(qC!JIzFTx~F__ z#$$i=u7Ih=?K}#`TDzS&c!d(WrPSVrr+dd)NIK4$jC(xN+mIrbJ!4Uv^t{9ACB#fx zd0!(jeU8Z_>QRIGi5rYaODWlF(sUfvT$i*Hk();?lfkEnR4Pl&!#$vgP6<_@)I5;N znGEXC$}rmf920uQZoSUo!>A|HVV#N+q$!0?YM)5LuCVZ)@UQ_7I!%~@x4Y;SoX9C| zm(8JvHZ)&}JrQd=dHgss-qkP(HVs$hz&zAMtj*qBP1Ut+)my{WTbmkp!s%_xl32lf54?GV|w1LPgOejP1J$Risy_DTgyX{(v-z(OctYK?u>LbJHGqVo*VLn z=uvyb{bFSs&5_Zj8j~WA@}Y5uoKv_H8$zET2*^T&X4Hi;**y;zEI}VNpZIKUWVD+2 zywJ9ql;&0&Zc?tXxryWpZBPgPQ0Ud)GC0D%w=2~O<)5LO0h*D}VwK|IB zbeuC8EN55U@p08+lqgL}exgZo?w1<7=Kps*bcgF;a(D?<>Up^QDIXHCPmL~?;Zv7W z!7OzQo+GwVLUpTOXxPFkXEKN-N=|zS;=NGrG9Q+I2mCsoB1XCcD^ZR#{(Pn zUDl1vtBrMzt;W9?+tWNm`;LY*3sN7XsWqNJ@cDiG*a1CVJbOqyV&Zs-5_h~yWYVt` zmnv=tA=tEK8~OqbnTyCxQU>2yXv5CFl$DW^GWgDh%yRafcufiMN$o4Fit?g#oHH3@A550fBe#WUE;~GSTaSXJDbYvCLpF}PtETO> z5QfLgKG28gWX#Wlm)Lfj?A5p~jrj0XM!a*pRNT@TTyt>wOoup^q}anhLRG5I zGfZNYGr6?#RCRn4sT*ZTQ(D?1ly{g`oLNeXox@2Jxy0c_%9S4Lk;<72lJQ(yDb2r1 zA!wK2`7FIcv!J3{lw}2(j=8hxR;KXUlmYcjnD2a2qi!i0nfLFJaz8GjVofTRMSl|{SucT?moC{n&|zmb>a1OO+LNN zw=L}3*1UduOYZj8oE<55UYHq(vcX{8{X`*4tm{cfQbKjF&in6-+7^P2b0&j|N6z=V zLN^#mw4t)#cGm|%cvqsh2m!`80Z$!1Vgi46WcMrE?Z zp8<3vB~)e7_BEt(n$-uvsLda{_@pltdXXboBS-Rjo}kkir=ZiO4`Gg?0^_9>L2S7Y ziJ(mEn})?Q&}7S#beR{xz?A!&5pl|8aOMwGGE1DHaAKVjsx`gPFoso5syyRkROpZ8 zOfM?B!k2wGbDHvHIP*nN#h&Y#;+_&&K#aEldc2)HQA6@QVLGdECOT{S_^jbdBbQfPX!qbTk;PZ6j!9s&Nyopy zc%9?GN5Jw_cG|}#E}lWPRw|$+s&N23B~+pMcuL47om5V$SL0*U^^bLD@DS~+3Y24( z75tK>!uHdNjg!&I(}!4n^8x`o-hN6Yw%BhPm)Kg9QLn=4xDM`=C;PVLm4x$3_)QiS z;guE5c^g}DH@4!`?9`t!M+7;6&qZ!lVa6vn2UX%jjx zqCvHPP$4b3I|{ScDWS^LA2;k`m6Ph!_!#y4V_7A+yQO%CVKMftmwPy#4^fUZB~=5@ zS_IrqPn8D~6kZg+-hs`51GM+PnAhc(mQS(sj4xDmZoJ2WLdT2+5|xd3Ph%BcaN+RU z9f1H|G0IDUf>;LRZN&Q?v3^pYWcRx1t$0{cX<;NPdDK`J)FkdlxUdOA&MPTG(rxol zgrpm3n;|n^gaV9!z`uN=0bbFvT z(1>?H>v-pnSOa9QtH(bMZ-b)|FXiK9zleTbPW4J(P94v^&!sW;hDDGdttvrNG?L_tcZ+V$%U(^!=h z3igK~A%_$DDZ7+c+zv`#+F>r5i%f$Vhl}SA>+s?eur4vPXci%csV18S(Zq|HYT}uWOA05A))7+!O6xcyeMvb$6qLd*iB!hqM`8*Z zZDR*RO%5JM71t%5e4}lQE}2296D^a9*JDva^(S@kNtG1-^qY^E*^t|ahBKxdPEKF? zjHjLV_;qrAC%;IT-X=$TR65BLbB9WV_a!FO@R*jHd^&|HfY-XwBhAz{k+G^oY9i-Z zrc)zmqf)4XD4{x* z$VRH9kgXTC1#u|sTAse_>R;e<>Wu+<2Lkp5p#`CQS0wgPgF0Gha;KQCahI%v84{yR z23bliljRH|3nf%#(jpjAC50?~so0D0MsI~MtJ9Y-BbC1U;+4yb1aU07p<%HMHu02Y& z&&YX96J)#CeJQxN?(Q@Mn$#qkY<2tlS!^x*cOaKt|RUZ zwar`J?AdV=Xot^kJx@emL11E<%8V?SU=eW=E-d$vWk1V)FP7C`Y!A`vWs&4PJ% zII-(=War69o9V=APq&V^Azre=qefo{F+`Ywpde-biOzar9j={8;t*A7SxgU!fS~c~ zC@g4$t&+lmHrV_WD?}NT0b7Wa0ox8zQikY?4Yr*eU9pp+D|V$mx?)I2l1PN#YMJmi z5aB5yhN*<#6%|rJ$0dpIH4F2U{@#xCCH&#l6_!T@9n3PhxRb7S9EaND;q0~(3d1K9 z?i=&sq-Q5JpRlSWcU5c7>XbXwA^Fk8H(ctemKk;%F^m$bQk7wAqFgE+mlTHe&G$R& z>6KCn%g#t&mYqVQ3YRD}g3mB-Mi0WpXw=vQ6|eGxLp64(7Q19od~jkXIV{%IFvCD4g__Ym`W;r(Uy!~OKC#G;R{(f<-)UYB z4Yp#BEqXyT8G=p$b_uXI;LUE4DPgqkh$OD`hdS9zbxeZmVUqYls^l)&$aiew;u$Qw z)G`YnAQnJ__*3fxQFU z*Wt6DM5qVic|BE)2t`lqC5Okn3?{>_q}u3`kRf}#f*IVp)-tyqA#PDZl`BOFkt!+N zI*QA#w8A&0FRyqheYIx`Ml}(3S$(!JILb_uH#gCh9@G^v9YbRk%HYgxmO1kTafTAA zI4Mw;R7v4X#2=OUFbYHzKK5yh>;|Hu#@E2WBI4WH>VSWM~&&xCD z8BoVhN#;!Rw~KprMCpvh(aH>?Jd-5M(2d9_vBBo7ZFBfGm&rE?T`jzT$?dBS)zHII z`p=0`LSz{WMiATSzlz8KJI814KV161Xnc;|8|TyeQ1U#kj^39fT1uq%rv#>F=mRMk zgoI`iGRBS_yGI{JDn8;FeCq+|gFk`FoS_~v{K|%FW}lV)QqI8Fa(bNSxw3CKXIxXR zr!6-S&JD=p3x4~}uFN7^Q*Ho5jPc89UZ%uWPNRMLF2RZ(a0i~kgwht z{2QbbjE!8OqY0+X4<4o~WXFGdGG6Q1=c4>`w_TZk!_upk-Z7?a!b#x?Cp8V3)zouV zQ_d`Q1LJLx%HX;gPrng+FLU$0quZc;0RELNtDA27>afgivp>r~5^*i~8S`UU`(xKR z3}8ND5jZ+sN{8|{C{UU%a^S#D?k9@HjH3O=CMl7ZoprWuCgM1wjW&Jy<1k*I0~(~< zr;mzLA1IpP)qlr@0~5G#`dpG?v+G6j2b54{O2M6^a+*|v@ZVw+npXcNkJ5pBI?Z_$ zG?&-H@gk(tO%_m?iiOAmbR;{NYclAMUB)1EB%=#uaOY2!xzj@2p@b?>;tr{j!X0!Z zQEseb!>WH??;Pn!zL38B8A#KB5!PV(&xaWl?4RkY?#%mO=o-UNlQ}QK;J6!6%zLu5 zt)M(yP~Nm^UE3;Wc$Krcz}1rLYRz${+*QGg#O7I0Y1QW>0j%`&In zAx=?3RVXLTTh?}W~aXi)NBmMQruQIZm>bp0{IE>MP5f`ieM3 z3Du^wP?c0k;nZ+p0&SY;#Uc<_W1iFLrkL@_fZ{*t%c`L(-B^Fa!wA~Ih6`_Zh|(t} zmQitWtTw6jE!9Go>~=mralJ@wXht`b(bE1&-x0Yep-PoDJttL)$d&KL%M5%5xw298 zG;J-%aBoi{7F?fh3S;rC@9_&!dB;#FbD51)gUQk1- zq>yYt`<-jhPDj#p?-HLiDt^%n9`&}&qeIC@Q9{)yIV(~n zg+~M1OWz@6xqtc+X)F=R+#o+F`KyE2xnX2H%S}!VpgNds^l($TGIexi-wO93(;gEZ0WdZ(`Ya$Vmq-tn8QVnZf6`jEpY=nCc(us zscEO0tnG@{b8O<`8SETmnVq~N5+y_o-DlXOlPW3f>|5Z&Thcu4puCXDlbgP@bQB8u z@d7i@GT_G^N0nZF{dbUFuN$m))-Y%+c@26H9DrzPPpHuza#RQHxmB2=b8mIj1@JWy zXB={G4cW^|mIgc7@qbvX3uaPrJTBaU_{%bu(nI~_ww3fyf4OZV&C^i^rNibXrNdT7 zO3E;Kx7_CA$vYoU-ub&?@-7BDV5~vx$(D&dgxoGAL@jml&L1^-N5>_F?P%bM1@o{$ zfGem>og9BHR43TSX` zj%BWm2Yqx(s6M6nLsBJ$R(*JyuoUg5v_0AU^d-}1Wx(X-O?}KcX-lsb8ydr6lT7(^ z*?+)ur<6b97#_P=289+`rqEQP5G7QLy3Oo`k}4?_>K}=uqczV;tGqOQS(MB3H)bpp zuhEDs5O&e}bT}x{E+_U-BVt=_vS}IBz{_31rjc<=W)NzHWkMZCgrbBhRDablhgC@- zRNqM9J1j3NOH(@OH_$D10$r99kp#1lC?UuDhsCnTBuP14)timmG?jBRg1+i?M!5`% zthG#$Sws;^sFL*C3}aZ86pE0~!OH#l4&9~roDFG861)p@V6pDsg260>E$P-@5Jr_0 z!%~wz6?Dbl0O?ZRVv&!DT{43}PRj(!BLY!E6{x>$n8T{15NJTbR&4L!b9l>iufm|} z^koqIVx-J8)nToLvKwFNV5Ui*8oJ&yjO(3B?=vcX(G320E%S%>#iN9(PoHVnqmwEr z{OPlRA`sO*VLgHL<<4a4@kF8~8}M!zUU$Qr`YtFeSejo^l(($Fa=_5g7`B^)+DsL& z+-Lz(NvPqmi)E0h-ZGh%ku#!%>QgTgB}5u^=D#Rv?vsv73YmwHQS-6&H>M3s8w2Vz!@228XEcswi$fA1kjRo9+p%O>a%_hpQ1zKpiwDhI4mERN$DWnBjD22DsI+W5I|KD%Unb|qJ zXU|R`XZQT6KcCDx=e6<-%$j8-yydA@`T&~fraPd0g*6h7ZA^UJ8X`4#qY zN8&z+WvSNd8Y*uzMrDGiu%ODcpbxBy1eJyC#yX#(H^U-OyB1v0*bq~B(W|J zC66&8l+24Ht9g-Rwb1?~t7{s;(niE%#v>xn5y65~`fn%; z4T)9xJ>M`%!eGf?&q9!kCoiK!gEsb@2!P7V8T%a^DDp?}$NXeRyb!N+#F%q;taPg? zm+5$gxML#G0fUH^E7-iEu8Q$AoD6WifT*mn3?k9P669_>Q(;U>u3 z@Wf3_H+B6a_sQ=;PhD7xzts>0<9$~>e%0Kn8|E&(VQ#?(Nu%5>M zG=!&7-#i9+++PSkgAg(!3>x!XSy%h~kwVFIBa}?yw7B>!U>#vl-f?${DNz99Ej8>6 z0Wzy$@d3!*nX6ye8G;)oIs71-X7ohl0n){Sn$#|TSrsiv*ZAI1emK4$!E%dDG|@?2 zWA;E&f#Z`=f%6e|HG?v>{RT#XQ{ONN&c}?w`B{R)f|_$qN@+w@B;bfmHD@5r@8lEl zrDk2@rdl9GUnU;S!Vh9%k?{+w@cAS=dc0O&)&a21{3+m4~C`A!f zk+8E^PMEsE*#5Ja(=)E-9d8*kbu&?V(#2K$e70l!tP4^D+9e5^GbZS@B!~rds1d}f zNC;Xfo4#K}qSHIp{;T+MBwjUoF2dLZrEA+D<6Y3k=8`LmhMJztWVJ9Qj5fV8aIqWk zNhUw9mU6{(Hhke*->f?nYfn)z-%g@nn!$G77psv_uPSW{`>HW%ZzgFhsAuOpQXo+k zJ!!rP?CbF*&Hvu94ms2H&2p145u?mGH|fmvq@U<1&rP3(p?3zc$zbT^ z2duw)6N9w=?oHgmN`AolJJWqwSbq-->+g}aT7P}9jzu4Cg1imZUv-43>nAx?|2KOo z*aCdz<5$kDdg0uqFPvNOLa}#26l7ui6+bN`3iA7?Q5GB#ehV;O*dYq?4YUBeR%P)$ z-$0c%*B{pcEbfL!q?e6mgh^mr#?%J9hqJvHL+WzTjT>&Avdg%iEXr$0-df_dyPX#A z*YV~(lP>VWa*PqwT3Ty_SuLPz)c8rFi!RV)qsEUGGu)Z0&SEG`yOI3(f$^~i?m?*B z<6fEY1LI@5`wlTa?hxbSaM<{WCcK~UhYA){4uFdws+sBe_lPwM0_1lULU8m2-Se*^ z)>nz4BEA<1*5cZ$CX${s=6Uf2+X(<$i|_4H%%GI=-Msr~7_WoD_83Au6!0(}_?NBR z__nNtSHabc$ZKC41ho<1-tUL+i|WYGx={^+=aY)fEeFXb11D z<};yB?;u6r5_D9#*%UlIH+vSd?P;Wc!)}Bhn1c5whB*cAPaI_>KQINK?!Hq@!FP%& z_~q@Ng8eMli7F@I6~-)oiY&7rNVUBM->Sd7VG`!|A^}{SA`jUWI@n>gbx)CN;*0ir zzk?lKoqKL}DqL_lui_M0C$A!PEtNNp%|hD^T;dej*epra1~$y-R$0W+XF(lmr$|;s z0uWD-a7goef!Uk!(_^|b717BRE!?A``Ki4F#%6gQ*cv2O3~dprk`GbZxz_T46x1#W z(N)F}%>fpN1=XlUC}&k95b=b=rWZ1~YF3|#u8uE9S96iI-4-7&S@g-AhU+E6z_irud?@gJ?BOQMF@%7v2pUH^3#{ESzeQm6GRV=&+#|6kabvZ(P{X+==Y@ z6DQA|I63D`pLgy*=iL9qi3dK{^T5A$bv?JQ{W~#_$RKGXkuNnSQY3C*K_L8x;l74_ z7{3<@ru6cIg6g&ac3*rU7578#{%c&M`1nz#J??uN#a2}Aq!v4JIphtPI(?vF60qII zfc+h(JQh@@b~D7Ps918!6+U>@4=MJ(__DOZJ+PwHDe=;|c32JH%>$nRNfkb~v%@u_fDj~%)xfj+fTE~?!-QtRdypfApVpes63iD#>%5^tU!L?`@0b( zlaB!`VQU!=Tj8Je#{kX-cGxBq4Oc)Ln)j9*S9fxdY~jDaZL-tDBIJVSyBI_c3vyjE z!qCNkTwL(nL`(Qh?mx!wMK)<1Ey_dGS6(x{42){3nJVySHJy@q51U_G~#JEZkkpWohTt?g>tEpvfPAmA1AS?D!kBYs85{G+A7B zRS=3f%X`W=H5mk|}v~D-!47vXMec8NSDEp&i+CLLttgauiG6>*+XXd%$1oAU) z!x9amaFyU0d(&Cu??6fr@j4-u7UG4QM0#3uhl1@D)X!@=aZz<;>n6%M>>*?BL@p*4 zRJZepl*?gNB;2jlq!31uI_w^fFLu|o!j7|=xLlxp1161dy-NPxRS2r+_?FO|M(WfB29%EDo%j=NupFL$F{MZM4(u#zF4 z41A4XMWg>e!f6QKxw=~}?AYi82MZj`YfdVy3tmpmS}oArZq#Rr(AQIyx5>GqotO95 zm^M^``X*ygR}xefB*WP!r4v<=fVyfoeC53~vO9~F0dR`EL0(A1x5O9EtI$}n^1>~S zGaz05Ju&bMpun3}*Il#dl*4DlO(8v?V0$aofzxdJcYB+|nP6VK`mr`)K=}@n!2pocbp0X{#WxI?i2Qie4jfrw4y`)Zt+iwtJZB81=#p z^%YLqnxKwQh`o#Icvz~Vo%0!X<&6qLyChrhF=lHsr!N-NuJcP$7*Q1oTZ?W($73q+ zq4oawqP5*i+QU~9A~Bor=beBMDHT6jF1lmI77OO;0O1u*AD{+kKyUl`bZv06BuyVQ zrfDl_VnKyEC#5W+DiWF&-)0qui*~_}(}&}W(`t|V)Pz+;&OanA#3s!wh~=6fSrt7W zVUKqt1nRmb?UML>%orbLUvpScfm-ZSRz-r(%G>Pm{wCpcKM`MumfyB_x9gdvqUpNX z^TJoNjm@Q17=4mG-;pdDSA@1s!t`llm~J9WET~8=lLo6If$4GxQ0J)qXYpldEw@Z^ z;z<#xXW*pGapMWIbytsGhMlohJk@$nZ0RE)K*K(WXH=%L;MgKnSDB8V_ZVYLggza0#uniM77F4zK0V$BE ziUc+^zgR705YW9Zn`P_~J7bs9@Wk1jMqn>*Y5h)2xoe$6ajSuN3Zn0_&pVP*al2VF zB~MQq^CYrFvY-mJ42!Iagr~JNNpg;r&d$Q(&cuZhrGJYlN(dD#q9jgmIA9*j<$P{A zSt&*MM71ScdPUj~sEv+9O6+afLJ8S_8AEoAlNJjqSBsX&sz@MPT`$CF1v8gKtotJT z*m!z^{erN(%NbXk1*GE_7DtM(Rfa=I-3voqDvNVMhCUh9GBZ})RpJYX?ffIr1^wXk z?w-j?rBr_L)mJ0ohPAx{pRx&<>$ex3iK~&HW3rgNI^b`$$fj{cSPv_V6X@xXyc$({ zOP)&HM{mhfiM>pr!4FjKOAaxw#=hiXR`LTbl&2C$L|%;}BCp2L_UF}T7`-}H5L%9m z=Rj;TXSOkp3>IXv<}S&&3P&3O@ky*0k>dj8`0j+B%dDeh#`sp@QDeg32*e*i>-KCBa|kGayxn2 z6xKL7IjuKlszRn%P@!7r4pv3X6rSS`7PB@UgL_BVE8@$P+w1N!2k$ZlU@NC)n8p;0 z(4E{K4-z1d9bV=v*rW5>=5f zT1)V^4pibLWA8hDmOA8FTZCO zRGj9I#;Qn=S+U2?S|jeOAj|O`AxGlNjtHp6qoWAe!Wb=lvzfQ?0tFi~cwF;;rKt-y zr&(OXg|B8Cn)ZWYYm_Sa8$RA{_x}^&>zY@JGzJRUVZB;Up0;cg4Y!hsKYFu3BhI6zwP*laxG4^;z(!ZygHB9nz!kC|5 zBR?#tKrIVAt0LiN+1@;|bPPL1CUn*b%Ep(VwQMKzyd$v)lb*VbCs8(q38yI4L}0~{ z#h&ko&*O&XO_LDkosKV4OYSI46^b)>tSk88BQ=N+J|}Gsl~%Dd!_M!>p}(h910_;t zjFI|nBE^Eb)K31aiUg^}t_*%pcl&|(l2jLc-Q(#QIQDyk$S<*?=|Q2YPTb>ob7n)k zBt)+;hUi0thy~TCUE#1Q5{Q=WE#z>kh9{3a*wIVgwXr`GUyMW=)HD+3;B8vIit`-S zHY70Nyaa1xWfG>wiAi``Bjy=JOX1z4FgA^)GgINy*1F)%P=x(FmC}(UsOy`yOfvRr zW5zy4##m6tS_)KFMZ(x}xABNxQO#Anmn1G_uZ=Hd%V~1*2tphb9O=AQB;g2oifWWq zLG;V)^^Uw@+t^GGXq&|7^~M-|iWsq=615j>tcnC9JWefDD|iqo12Oolmv4$MO2Q*X zSueFn62?Z%E5bxqWc?}?(Gka(EiKw7>3XX%U7sOcET~t_KZaG2(6v~IAWS)8ZgMB6 zx5byL9n6O0ILtYc5l{THp7{WY<&YE>Lv+1{Ki}lO~pB z4E#FPXmi%PjH#L%oNT^y=YdUQ_E>!P{FI4MXiy0ziHZU~XI*q(LLkmSI zX%#>3WtTG*T-#0JYXg*^;qw7wd_GTnSWtt`K`D!g76p?TBf688~fn!ZGuSWubTS&vnb(1f!d z=F$D>*}Bk4AB!(cYkU{KIX2yxBX`+o5LaRJhwS#RcLF0E5`q+=%}Jj$rs%7rhy}Ii zd{c@esv@Dtos$N|OBde`{L}Hp=w952`I!JzMF;#B`=Q?JkP8VkLc+bNN zVfYuoe-CLq+&^k%F?Z*y#tu=S{V6rmk;k&`d==O@g<|I|Jx*Q=z*Xd+h2FI8NV=2~Njmk#7dpF50;e`)jgT&1F+PIeSX@pW?|5H~$sb zFucTs`mr-p%;H6@{RNgvo|2$Ok1Ma7V7v=rhac{Vq@h`IPJYUmr>Dsi3u@1qFJ%!`k)X3e42L@~9SvBt!A~=Mi90EO zHNF6ea8fmZL;y~@eYpuCTDf~Q6E_wQ$L6CLY;}Omr9Nndpw4@5!9&$;?emou4S2n<&inWS;0L zB5^F%^>X(BpOx?fi~X*|UOrRbl{m;seqgaLcaMm-CL`jl$!OSHlV?5k+~Y}SBfCTN zr0B#=_}_*!blB_%ca$7!|BSA>uoi#y!LBb|y680nuR8tcz+3mfasRtEy>H9oTjo|> zH+SiEa|^B$`P$scwh(H^uiN8>*Q8mG~pJ z_aMt1uHXBgHC%7#OF8I(U=w`vA?qZd#vx}OHQH=2;?&lplhuIySHl5$;(X75C?oHgmN`7EK&U7CZ1M;vKkVo1& zAf1cYM}dP7i(c~%k^}KS&_&L3`ESq#+qsy(3mIe2=by76$Nn*Pq+yKld%iJ-jhFa! z;U;{fyVBhccHP}o=^p8-Bu2U`$)#Pz?!#R>Fug77$}OnO8%Zwfs>~mmS9#9i?#e}n zyI#^=S+KFIcRuIgX}e;nxDiL}$|N5yr_1gj`ymIJ{9*iwXo$Nec0B9Wzw8|s2THNI zq6sC9gmbW|RLcmb;yv9?>yID#6_MQrvC6jcug=0vgKuzc(Fb1E2mW)+5+VNWiQ3c2 zF)NeCpP?ED*%@*Abt|%#`-AP%B16VjoU`ROd7;nl^}de-IAUfq-oHF6Bd^8;Iy=X3 z><&VXZWRV19+Tz23Hy-@ueNs}+!X+qqG7sJW4UVqfV%^FRB2upT=KB}=;Ct=wqRWL z-aQZHiFsL`jZe%w$+Iy(Q0dH@W~DRlepd1WL-xeH2gHH-0dZiyb$;6q%)KqemLqf? z1UHN8vXFz51^KTHPCnPXb$%X3gzP+?cBToQp9vfdOj=yd5lQ$Xv*Nw zjCBQ*n;>e4;QtA`P}{P8YY7(wrVXL+T3EdPSkO5WUm%340_-d82v4 z4x?$1_93T4cd0SDYl$ukqI1Fbavd3X7j)0Rif4hV+)WR^7d6k24ZbFx4QOS2d0t7L zZJ`L>@R+|K)hN|vRg|ux?mF^LOXI4pWs<9P#$0VCS1hPP?V%Q{qD08MN*KjP%$*W9 z#uuTRWi+BxxtihovhivTn_I_w5-W1dim6eAC=zogt`oieqR~`X-x~_DP1MSsB#L%F zDD@x-9uCJ0riCW`p-9_C)pW$k9mo3SjgzolYYf|UgpCC?t@*jLDiYXM zqWwiXV-2T;uN?lbwlKahzO?mlVf4{PAyOq*3S+czQQAy;#nuhf#20YXZTl<+59*NC zNw{8Y4A)JBiv<UQksGK$_;t%UKU@lZt#N@l<^LB9kF2QhN18oY+Z0a zD8z1|KHent(auZV^-WtQ@#;6m>t)1?1=Z`kS&Ad7BEf6%{&ZoYAATXeS?jj=!nN5C zR}Rgu!hHpar{LRSx*Vsz^Xu zA)AiigQL|FU(ibjf#LYlw6z6I^b;&gX~HL{4NW^hfpw6&ST9{Q+X>dDu*ONc?l7in zAL(L2)jFG`M4~DZy2La!jE9`QY3k1Sf^`MKQg6R4<#ZkFZK$=t^M`Q8jNSN|-X*epg)z1W-MmZHa z8TNK(O!@mEN`3#MT@sxMV{{G^9TrrcX3A$(Bn~7ifs2Fn#!@^ z!h#?tr0uCu%StuRXOS-1S2LVUd1Dq4K==R*H>KM|P52_XvZ0d{bUAcZ$aQBwoe_2? z_R$$(cj5@05%_`1qlsgzJetU|k{`J4+?}w5hk_+M6vo3m6r!>1uMUz_Vq7xD_!u!} zL6-bCoZ}5g3(59l+6;r-~l3(Z|c4se1W_yxsvmjmmTT!QB`sDW_0e;b7`V{gq z`Ztw77hm4R;iP8y&8JV<`D#AJJebBi%+&!hD_&no1$N|YvNpI`lB`!5lT{*FET~8A zXu_&UAi5ka_eYYle<{8Q`4e@6pl6h*yFpY%&TH7)9eGhfp#A-iMoDg7XUxqsxnV)& zX)h^Q6$v*B(e8tlvTv<@G``^YUv?inG%8ZC$n`aRuePdDR0Yl(+1(v^)x9RLRg#=H z8`cm(sGf?q(5Eaw%_+; z``z(nX_+`gx^3pZN50h#XJ~yhQ58hL$xf$1w(Ymw8$;SADSDqVMGui87Sy3LE(H=* zkx+E0XujrS{X6l6r$~6DJ;LNgxg2IJ4=&aeTG} zbP?VHK`PR@v1;CAMh)l*Me#?e+m1MQxHD>S+a!4(Hzx1(B##AEthslvDiX*p7vsf8 z*B{51u1iE~&CUYuOrT4qX!#R%?(32$+U+yDx}m$Z3FN92 zVyo2(yC$fM6mow_McpQMNNv50Z_+L~IXz)a-D9MV1@-RilfsCqNT^#Pnm!D7W*>Iv zV~$-B0hjV((ZjPi^(@Td@(CdutQ_8{s{P!LhTYo0K2qF$p<~>onl(&< z_a$TS-bL_OQ1cpitcnCYce*=N@uyDw+n93ayp=|+M z+nN+IhC{a{yl)hH7pSX0lCI0`oVa>hwooGYePaYaLIhb*_0A`ySfVNt1eYoy_=c~R z;^jZ$i{1-4gL%&?iD0T&z&lG=$?yza?yj)+@NVr5IfDi+{jC+|E>vAZJ%aooX?txtxFQ$56vdv`~pJv>KiB`G^ zwJ^wH{txIL1>JlqcAA=Ow}}EPesIGibI&zq?oY@Z3o6~2mC}f+NSMQbVcQQN??q{L z*g=v^XBPOmxeHwWE{QLHTSdEtV;s$;CAa;U&M1@O$|!^El`T6$k@f=WrXz1owwg3j zQn%2Uy3dk27F4zN_JmcDPzOt+h>+Y@Mf`Bbzm_hIFK#%&h-RnKqTOOyL9 znXn3}W$bt^lx?59t_W?NWNDQ#OaGTFv7jEc8%|b5!qO^FfB`FKY2n`7xocf~VH&TU z3EbGZ*qI?_z8m>1?;=x9&FYs!?)SE)@6Sg`C}=QsEPzcTw97&sOHdMUmIWiwuokma4g=7t2++u z-gc8BknaZ=a58*$liQ4%%nGIJ*zKQ@-QG@=aG_bJ2sBCpbb~QKe@B2=P>ar=OG!jk zBmlv}FWS8~Tdwt|{o?pCw0a1WliTK$oh`dgvC_RL92{8x8)a4Oyo5de64~SJWCwqXJ?ZdJ@+H~k48<*)Yukm_ ztcamoL=I)|P>OUjTpv9S!>NOTBcO{Eb_3|D@(4857~FQQ=RWwC-I^Gnf7z{WAb5VD zQoUii$9=(3Rjt#;3}cf^;dYdK|>&|bk%Ml@OkwB^M^ z!A;u*!yIlOrr{riY*~fy8BwlxbzPv)I!smECr@$hT#45QD+eu;WWC&&tp6lgET~!M z0V$5CiiE61UgLeQ3hs$7R&whps4{y}Aa~iihzgtI?B|Z$z{@4Ru33_qj4?9_ret72 zwQ0BStcrx0g^j5X?TPpzBcf3C4e!OyOQfjOycQW?8}kuYIPV%Xd=i3zk^h9gZB-;q zviCc36@Vj3K&vE3Ib)C(5F{2pw_W&AUUoOitQb1U zj_=UR?!abAh^odAy?_w0pdPiC-K>fPqGf~S2|GJr(YjbhOyY_&>u!!_;|r3{6T{0g ziV4U;G+~0V)xXTaeaHdT(}}utQK!29H^BIax0X6hFy<`Ywf?ITpiRX z3DGN!AzDd@SWuH%)E-tv0?`Z5ZtV095 zm1Oy}n`HTP5=OLR$umj5+&q(HKIQw8ZNIu-(IBQA6R$BI6Df`f7G%?JJ-aVip96*8 zi)5fIgd|7Oc13qPdR=@6OR5hiQCs8?%^=Y#m+-1nz#0xKhvhg$Cwz`w)hMk(^jE04 zUTFeor$u8;V5=liZ!{)qGl^nB1v*=$D55G7c$N)Tkn$vZuxeE;{akIoIi>`)etnW^ zB9g-7E$rKlJV4pdylIk^$BbFoN>*4hcY8La3x791B;e}!FI(%JlFDSa+No~AFj@))mLhGBhOv3e> z#&BInxL8oR&LdJBQ56YXi-#;L+mD@(ZYr|h7hkki3v<0woVE!O_iVET};3hMZNA;IouXcO86v0DUOF0Nw5b$aA~aOs%*b&lVB4 zg0@!c6rOCQdKd)MI+f0s5ThlH>+))T4Bld=trpJz>w-H+5%*zqQkhYvf-c9abEodZ zo_@A_AMe2Vfj#~0i37YXzugUt%n$78XSUwEZ0=` zadItx3V#xAz`F4N3oiTxeseSOJ|DTPz%=m6Ky(dXB5P`&hr5R7)&5WZZlwE0^=}VfDr;+>_*=FAQNNSz*{W)~x@6rt zcwitObrS11-ARg72ab_@hg0cn)`5{vIPI3p#f*)9@r-~2{i!nu#Be;DIqOu>$RTG2 zWz!tzs{=Y$jf+oE^$yyl#>LVVk8XJ+_t3nDpZ6t|9C@zip7TA{xgKjSfAW0()Vchr zxgP6@o>>f&v)z3hCTF{cICl7f$^(f}Rvt*)#Y%o)?40dBCdSS&F?Q|_8#{Z@pa;8- zd0kCK%I-o3E0xxIGgw(W5$In5s0gx{66lD z{M&ps30rPeXt>aQIR7fX;M@ZrP8tfac!;dXd_9c^(=->>1%TTs{7m3u zLd?`(5w^sVl@WeSJSLsMA0NlRBKqJ&xSd8Qx}m!ifuBRSg~Wl|%ZYM-Vwe-<{>0s! zDEWcPdlF-;yeDxlEBS%hbAMt=%$`$X_RNLNp3!7qNCm8^FYn?+MuH`xsWgn`O z;W|pVSWu1Hxr|kjaD;Q&smg)zai~P^SNrSn#prJCHIXv4YKf!^1qg7wC?wDp-|Z<8 zsn~T{#`7^Ip)VAjJf3pH6yT&Yc-M^=Wwmtl6&Rv&#ll4RtcjCaPEQpqW z-kfTfH~GCtc)t|xvVA%FA$Zr8--#*ct?lT`>qJoyewRAwi2ZzJW0NE(PaA_WPEc4- zHJaU=Rgo~UFkt@sx0qr9n|X!vpPO{XoAxvZh^>qXoUg(d1@D`zJ#Ck51`HmXOg8MTtc6%TuuyUnC~hN*t`K5_$@4g$eS{;mIzVnk)AQK*g(M2v z;o5oUxHh<15~NFwK{`c{SWuVFNhyn{iUg#EZp(eAl11_5Xcs3X7aQ$eWo<$dXN|i1 zBJXCql6EFIgSBLARb!VZ#1^A#E;PF%D$Y66SW3&?16*JEf%SD)VlUU%UG9rYeqenq zcaMl8$A~y`jJE&Cu_jPP$?I}sUhgNbEXa)ixB>J2g6{cO(UyIcYs=>MB7t}X(;kl^ z3{YhV?h8JaSI3v-pYa-+@?sFfzCK64M+4PNJV%On#tpAi0N5k^{+MR`LVm!buzy`_6-6-#OC$edney ziRJKEZ#+DHj>CfmG4yXgM;f-D{9Yu(W2H|h5$_DI1>8Nu74aP(SM|w}AqIvR1R{Sd z*;Tn^g-<{^eAlqCc?T$vH&dw~vHhEeD?(c*p}W!;x`zoJ3+h($QD;>od@Z2>hwMtu zKlN^pFIp*XBfM!>z*S-EHN$2&Piq4ORw!M~e(#6_ZmL(++Y#*rjx#!gI}TYsQ_TZj3KS%V3F1S%qSCViK!&t@+{jKufi+YLY5|Zeo{r z-*tVufGMjHNF(BU^}b0q??(vF$?9> zQ_NG5fdJtgVOeV?u0pAgUH_;wNVM}zjAbpPb&{rC#x%W!G_jy2onMtAiK<9w!l^+^tLc16}cs^K%8*f3#}-b68IoCIr#%`-CI{RY8e zL9J?0Sy&YbSgW-5?Hk@Ypy+}sSI!&y@uy!E(#er%iOW#*_ z1l<+i5p+bDFEJ7veY;q5aL2HL-6mBnag0%8o0u9iwLb_u&V zw+StdzdbE_ObvizMByJgF{NMGx`}dPKVi)4`^hT{65)JH$|b5IL381-=Vz!zx6HcI&hHT&7F3_p zD@74ik)X3&HhfRIJn6Gw=i>{|O9epEx3-6VUU-dz2MKs;kHvq|^&jgM;Vm(F;qkrj zz_m?$5!W~C5Jg!59dltfMzwmEpgl&In7g-d{@yd^?mdI`Sv^45%Zr(_-FtZlGle%i zbo<&T?qF_j{~ZjDZ|(@HSGl%0JI2KR0f}WnUi>#Pw>R9x@OzP9yjaTQ+tW_R6ye7c zr_)4OB><;KkBBUB-L`Q!FXEjU(=-5G+s&rq?R&GS9_)NQ?e)S32rtsGR3o97j1|UU zg+`IV)P$vXm_mGp>R&5gmbCNqj$M`uXI;DGRC>l3%uf+a7No#gFNG0RkubPgHhl!~ zMy4%(X@6 zPM)^3XrJWj6~%dM>i2XdZ@tE9ew-YhgteQ1V()DUK&d4Zqj&!l0a-FwJ zc|=ttbm0;x86leV_8%122O0P)we@e*2*!mA^g-#qWXH<1+ibNI8AE9p<0@P+Zj48On+RGv!Q$I4TQ0oow>f$xteb_pBgE@6Y*9cF`k_TN))VE1i8 z(eR#HjllZ8lOyogIGncd@6&+b^swk<5&WY1%N&L*I0kBlNxrDwo&5gA-Scmv7urqk zKgRF*#-??_47SoyWr5aBxpnBJRqiyAI(r^4yf-gb)-tmI+MlSDqw1{&=g zzD5{XQ02~XDU+y*gu55;UBT{LvFO>q-y2`l9wln>w2jkp&d%G6S1{5Q7E;6ll@iR0O}^e@j=-P9>t5i5g%_1s&l85?2t zNc5zYE6In8w~pcm?<}@R0=3NF!;0G9LQjWW7zsbe-N}8tFxs6w!g-k=n5>T`kFoM- zGRsPSV6xtww8Ug>iOG6AY_g8#`SywiF(v)KZA||+Nk0oxSeuIZ;AXtxj)>ojWJoNb zBzxTa?lt?g{d+MT8LeL-t_>7Of%w0vpN_mmOf_qmMCbR7(fJP1VL>%%FA`Z52_*~J zcE5Ln{f99{1m>Yv^%dkZe$ne<3C4|Kc6(|tJq3S8x)9;sQ0#0gG$q*+liri9%4~@@ zi?q+2WL?~vf}1$Y60sQwW;IAqS8=3aI-ah7}8*~C6hkNiO8?a2e2GjDe%Qhs30oJ|}RbLLSoXWrTV zIde^*s1p898N>fQ!q0*L`sd9%8|F=ZFOm@<7P`Gzcr}i?SIqKG{ZHdNEJSjJ+ASuA zwCCu=wcfiaZ<&C~)nbF(irW7}MZTLSkapi_ZfM>#N!g#XaYlR7{~%>7s821)0;?hc z3c~{)8fB&kW9nK=CHa!JAfH1sVtOiCS!=mSO)HviaxuQS_7?+0Rz&?@YG8%5TDQ|q z+S96mlB>@fbJe{7!myx1oz+q*Q56YS>jGr(owuKiFIlT=ZP&;$x)cvmoi4Vb>#wPY zjs)ttvL>CvzpjarufH+o>mu^Sf@;+Q_OL1vzOcb+n7901W?zdhVA`as0ao;!M^^JMunZZE&+BIR9)6&LV=tg1XZlCa@|JaF*0su6s6o zFTUukCptJmj@yV0R)NRvGxToIbfrnGAo^E!{CFpJ=czzdpkWfDe>cWx1u(ko?*;13hIB4-uX+dhP*!&o>&S z|H_Z+DdX-JU~TuuqS?sND1U};Gh*7=^GN$Q5CIL7(|iw`X0#E#k+iX(W}UA}X+%{dV7c4qAu+f0l}e-bM4?*R zU3A85_y!hSZ|$w|r7XoLiMVy$f{QCOo$o-UYa+QTmBNv_N=&e!(ks5UQ6n9RQM1jc zl@hjVjbVE!VPioBYXNIm6$xxhH9>abs$AZczA(Oko#h6AE(58OHc^SN?PC_4`o@G9 zsd1;6PaTpkkLfnxxdM1kn&3$=o5zzUIv&iTE;DImrcyGi@6621cKK|1d(oM=dOUsd zI32vg>0KAxor>5S(5)ek1Cw0$rxHbO2BzQ(i_HK(Pp$T*aC`e52i7+gSEB!7WAyupJ`18(Th-}wb$f&J0KXT>c<@$rG5Kolh%bxpaM;ZG zHeH^=c;JSYOSyP;RgnQ|E4;IA5hT$)ueq~sO>BkjE!5)8*wwV%bHF{X1vE?o)^7~h zUINB~%5-j#(uk@^SaNNXZrgP`hJ9OnX=WbBn zQPnj~Vzl2FqXWc<1-0nRm-2|JNHAKiO|t&8#&CQ&(rl9j_ms}fUyhmVXTcNK@9>pW zUs`-pE1nJtl}UqMJ9*mJOe1KU1nLfBpza`0ET~S+LdmK~fO2gZ?!0tIp_s{3ODdG= zo$*C#bI{aOfPdU9B4UqOeEq~-VK`R0>gr@yV7;7r=*Vh!g$7n*>m*wD7^8I;(PBZh zYD*rgB0>hcQ`rC9p!9%aS}-q`6>~^z_psuhH%Hoe%HvxN9ml!Kpc#> zRvNhw@mm1s z|LuOYz!|t{<0OthZH!}?IILsxRnp#8G1PQHBdLz63 z-LmW3i4rfUD#bcwXdJ!Sn4_O1M=YpG?HL}cBH@TvGnHb)Qugcd1!*M?t6~Xr*)d0_n_IO90-MN!wy$&>f-eJtotH=)vDo}fF$Ery9;Zv1LvF@q-yW>lb+uju3 zwVO^l?uphfp@QZ&+06xcUD3`t$ECTUS#s%mpD{A8B{D3iHRq(1MN~zC481LZjC=j% zZh3wuzQ9}}T5EP&$2$POWD1nuW#_&wiK5*HE{|WWyS16+L&liAftawMuADbWNkmm7 zn9xG0I&%PK8L@FFyTJTGe1W;gyHUY;KC4=tN+SY?XHiP^OxlIYi{T1xmr#f2;|M=g zLJXqf%n9LbZ~}QVvd6iwouE|!f&6elNZI-Hgw-m(hI=UT##UXYDHcCU4UWpAayut2 z+Bci&RHO3a##FwURI(rt&S5E&sEUM2*BUjNp0>DN`zEnJjxUqbH6~|qFXLXx04;*1 zd2p2>R+hQyrH9w{R3Gq5m$6~br)O!O5>aB1;@&!yv8-%4b?r5SNNbPuFD|Cyil>6x z9wwh?kte`CfCY7>BK1$ur6HbOX*zLb6O;7p$|kDx?BWM1rxT}HIi1)^&n|wzvn!kE z7oJ`H!n13j{hnR!=xidfT)97E4FB5*KMRuQckUW!aPH#wA{h~`9plg>91?uCM*A<~ zJ06BP9{k2wq$kLa6&Mfs**D0#8~nyDQk?%K6*z*UtBMJ#B`i1%p6~D8NuKZTK0wI% zfy&{;Ayy72j1KgU^U~#);%9z$d{p^n6?YE*j%vr*p7(yhg!()n$4G0k$|)i?H@s`Bgd{?w*PH>33^fO)d3Jf28(i; zdr&%YlpK4SS*S9I8drAXY`T|JC97DmR9)jy#av(7b7quYWS;>-WhQ3#wOpBFm~s_*z6p|>~ZPr=ev}Ka0?;8{KaT3LX z8g))eaYR)lL@j6I50n`1WuRKhF^7`n8g~98zD#ZHOJ!waQ$^8u@pzwEPPrsjaHc4y z^GKWr;li;J$_tWTf%Tu%#QIL`u{Ji-4caEr>Uus}XY|nX(?p8}73*x00*R_f&=QlC z+qJ&QY95*#(UZ>NB0{AzI>h(h7qGF;^^1w1~DVX0OJ+P>{{1 z8b2g0{_QjbY6C#ml7`rGjS>5EBF2I$c79)qBdQ`nOw3*ftJue5{?eOoUlL!)wyO=- zNw4K%^0I8O<<#8HxYZ(A2J=@?4=A)=KwT8%-nX4-iTSHZ+vNPU&={^K2p0>g);TE! z5>=7FwV2cI9R(Gh*j*Nv#uuy`ghzHRJqwJoMVt<~zmj=~xfF`?B4;qToK<0la5L$I z1GXlp7ZhB}sEapAU9=M}F{9RNN1L@)8H4pjg2jTWb>1w65mk|ZwS--I7+&%tmX_Sg zUKd}ouIo=D>t;GNjYi827J-tLM!Nlmm9D{*vf(2d5` zCS$JthFr0rVzu;itcr@Oa;1czR#`^%MJ!o&#_EYLS9n9hO}f`=+&^m%VMq6#YFZVnH=(??zY^2}P@h9V>5F^L?32v0AA3 z=csGr%hL7OySpuynTP0DR;G#xOe6tD>R{{{!`Hq|Azh&;yN-J4h%IY# zc=IG{HyE?_EwaXfYS!#otcrxSlqW%d)p2lk&&}RZNKfPDkLDnE@_KPhfoq)(sJE4D zilCRUw>y#<=qiIYN{ntX#^|4k5eq6$OA^E?A4W;6@ci{`L=NOwowkeBazE}t_U4(T ztPewc+B%Z9D}%NdSgM~m$sfxf{gWL`@cL0CZ_A|-HU+PnQsc}&hqI=Q48wV*QRi<{ zXYzIdvts!yUfhY_^madak@wr7-UZG=^pdlP#Sf_0#r)k87E4(yW3imY3KV6859`{z zdGmwL8dmWePi`|&j2a6AP*B;y-&6PRK{+|+dhR*jW1Z`<<_gaF!s&B`({nx66Fp~e z75_kYKd<5+=pN=(JU>u*FmVSf4<_zrB|mUK|3LRW;(q=faX)`N?0$X^8uYA&66{Sn z8`(pmb49;y!v8iT;k??g@8lTW%K^8A|1gc&Fc^xOA8onVH$!ty!NXXOKfy{t*5oHDo&;ad4&BzPim$O<9Qi zVHiwiU9hZb6bw<19r1u)-?U|tu@Sb-$m0Cp z`0{gYzni@cS&?0LKH(#RjgVcj-OK$nf(#ZWa>x^heJsqJ;rrFq0rD%Rj!+5rNwa@D z&(v#!nRlker@%U1;TA!6v3PJB0BOD+( z`Wxg`6rEttcO+@i#%8)e+ayX^W0Wo?N-U^IEn5+*B0*`( z^F3`p8DC`fcus1mVj(vRYYWd{sR^fu5V^v#hbah|q>)g%FjW@MD@sC2&vOlt7RVap zEr-*#p=swR<{awj!!nj}JJ-5RVU3gIO&gQ9l;p9X+MPd;5{arv$Xn{RADeK$;qP>O zv0Dzqp9_+=1(!CzaE#UkOR5+;!ybPsiK6`uMM9i<9cb&^1I7TYCO|BxK<6t`98nbs zK$qjf*O}d$)q=zPOnli{!o3o9)_66ucYwz?5%nb;oRtj{DrR26PTns49@;tYtPX0F zoNpd7#$^L>VL_ca*Gfr5RV279^4je?@;@A3V3ySbW3;c9+R>#{(7c*mO;*}I(X0*V zd~KF_tuZz|#D)bmr@8O5DiUlKik9P?KZ^7x-aY59#Fv^iHEQ_efYBEjW(v>#*!=M6 zW>ur83Z2)pyE~HKb4_5YW@}CMPa9wbhDGJ|8)vc9#({`?U>zlSrlK3`b60awTEJ%X0UWy~CA|Vlrf_=Kb zh?~z$#d|ZNZJOR0UmiF2x%`PcBsZ@P0_BO6vqBip#XlUXE5f8#XuXTN=t!*MEiKw7 z0eg=zU@sA78{aVZlV42Bb7`Co#Ntf2M#1C{UMP zA@%`kqtc1(S8up}&_W5>2aO@Sm5{NZa-A6|mZ*vZGHe9uAk#Ow_J`xk*j8@JGVc8A zOw@K1V12e@V6AW3GC60>8DljdQ$>QAAZF ztSpud_pNPTjxR0i=y6?~wzKfnchux#>I0?pq>HQgd5WDrBs;yGHU%{k*0oC#^i^Ym z?jb=es6*$V6h>4R(qo9hP zZ?MZdauc($nG(=8$fk1$UJR~%)La01=Z&Kjub^yMS{$d5zBo|BLZ?E z%$4hS`ET*1<{doxdqI@cCYcYNbclTVKF4Zu6@QXi`Jj%4Yhmsb9Gx7tK ztjIi7S_$qzWoA4--*#Ky9S*jQZ-e-hin|sOU^UQs??4igtU~tZD3bZDoEA+ht^; zm?lLms5IwBDT=6y1enFL;l8!`;`kzSom!ja#3`5ORFCT-$az!*WX8ka;*6DTG2+Hr8S6JT7g@2inBCtI$BHXLTPGL0<;Fz)G>KwCZE6k{tcrxF z6HNSgda* zy8>$swa}3)slYy1FWN-4-WaQgh!qPeRZEk~s;ICk`FXvNcJcK>)M7CrD3tPqW-D~);jMe@Xg+H`Cw zkf@4;rxjZBZ$qp=4XEw$1?p-ZT-7ehsi%u4%?SoRH1lNM(6kQ}UsqEd3zI0?y-(j1 z);PHi?l1=Ibp(tBwd*XA5{arv09&TDU*E6VH^vt)y4^Iv%harobGn)qbg>mtH?jXa z;%}qlUe`3q)6K>_y@@=rpgJ{28&*Zm6C9ap51em}FHbKfPa<5Vl^?URyeRU#P91&f zau-AdFK6f{MwmuW|L~2LPMO{rin2axr6U0~b;|0RCMnxxOxdrIG8R;_7EzN`kx+)+ zd)X2}pYYn;=ODc|zL=$8n^O7bk)+8&6ksnlesKc--{L_bc3HtPE1LGP_d60v47;qR zZId_+8RPU0;>3c=)PjkzDiWL)q4{ui@@MK8i7!bueQHD)X)zBmWe5171guLfX&-2 z9(}A1=r={GYNyUqkBhUBRn3tn|~>bO(+_I+~6Lgff2|m={{mi_j^b;3j*f1>(4Z# zsNwe_L4K(#PJ<`eUGb&<7UlGog^8mKS8Ktm7zZBy+f#$-DMVGLjRe7@;82$0#aPwi zUkG2TVI8G@>pMi@e2fZyT3%DO({9U&Ke%NQz7xjq{WjraK|MR)m*R-3NI1j&k=uCf zsi{)eleQK*#)y54h_RrOo#Rp%Q56Yd-m-USw&eHCpN=nN-sUt_L7UC|&OH6p z3#rJNVP|(F3AMMB1++{~PiKtT`4rhuR>}Zqu$Z;SZF%GPP<#m*;0i}iv2+%}R#SMn4FQBN1Wt{3!tE_(#T87Ca9lQ0%JjpVi~bbuD*mRJj*Q0>r(Yto ztJ7ROnVzP3k6_ zo#-4yL)U|$uWS%Tf$k_p;YF%kkLWlS7am&@jo+2aFl| zCK+NuB|5!Q8c`JqLn}u!lUBBxvv$H}RCoUTV0=O9MRVQL0#Bfx_cS+LWltRWx}}9} zeq(d-6-qop974U8v~FuIjnoF;up>iwpW{8MUhN?H!uxpexFtZSQ`-#%-M^VxkdBw;}|oQ+Z>Q56ZFi{)yBp;e1}_r>^dCfH>SA3(z+_)yFgA{pBQca)WakkyIigiueC7FB5n7NC{91CjQ`C}=JsEUNS z%SY)P=G*LjHNL=Y-koE0a_sd60JrAJGwh!ZZdeMmltxrVg3mg)?MHCovmaSjV9*$?mY9Y` z+Qy!aFG#zzrO$ieluE<5hvUH`OYHj#bk&e~WKviIxlt?g(G#;qhi|ufTJ(>??|W3( zPhEtf-7DXft(z!8{DCouD+nSB>fd>VluJ}a0^*`k3!zN1{xCxS6<_!UM|lOBFJ>8M z7+05Nci3Q;WjPgI`d}eM{=HO>wS8iH>YA;oYp=o8&<+;Y=C`I^i2tsAVO|_eubphM z=UN@mHwwJ}Mju^Z+%uK`bipML+m9|jw_wX$&&=HE`*DMLrh7k~y7_@?&B4S$I&}{w z?qnrDaIJZ!`{lx^`{lx^`>qZ+bq5P6(M`Mn9Nr>#2F|Vg-Bm1hpeW;D&~@vrZVuUX zM3)8WxxmD;KX@ga&>5@+9_|ae=U??NT`0sj72k`5=cS`oA=_6Twk*fDvwIHYh-rAx zZQgu}=tOYj&dSp*ti~|8_%?kdNEJSBVqFOmUr~Dz)p&WZQsN=BqO>Sc_kFA8WkQyBv^?oP*8Ow3tkdmh( z6kUKF*#wvpVN;x#z(38s0@oZm45M3HOu~YjpsrU;Uq%R?k`T0$>5a{>K-(nMi;bz? zMygqmDd#IvAW;J6QSPm z*q9e zt$_q(WZ#~)`bFN`Ixo*F;|tZxu-$XG({r1TI}5m)6u9@RFpULHGU@@1>#&Y1G@hM# z0oU@}OvN_u7De7w)Y5MyQM7wD!@)khfs(zejoG_}?6IKIoexNvh9;!B)JYUgGevroI8z-|at(fsg<410tISrtatv%@1v#<#w;8*y}cLwX|h{!yv(Heyd8&DoJ4s@Mf3*C z!6O}@$$U^a@RnWWwQ#PJlbqf~3ccOvs`3bS{y~Pm`(fAHnb=Lc-p<4*?Rxw`<)Oq; zRvt>6U?o3b?Aw`03u9ke82iRLV(b$z*9j^q-e*klK~l_uAo<@gj5Qeh_&p!mfL{E% za5FyAUFq%zyYB9)bdPjZ5+mJ}*J9j|fh1-r}5%;RIJ0X7bCyduBomsk~L0EM^_Hb;!9KrLzJ>nX+y6 zyUSxcF2M3>K@Fh;gyhxuTWSa`TlwgXkKFgryoaCvC1p0psOi0pgQjy9Kcti&H-H1%fr`-#L9|IS3>6u-a^RL&;OvT`=Di!X@yf$vWwc8eE8yTuEl+rnND z?Lk|gN&J*xh3`nzqN;4bzZe`<1t#0Tu9JiE5IWD1V3l4$W z$ozgHIsdlg_b=|Ae-q8{H@W{9zn8(krG81|FYs+d@W`x<%ci4pK1Zc{n(>*Gj-p91 zJ?htdY2`6YSU_b2$M8)u=x>x((SIiu*bzTkpcU9IiPqh0nUTrpZc@d9D%5;vSrrLD zOGhWu*S+wDJ+LpATfk}<-=|lVRSgky1?4h$RSZGDk7(4%m7@2K9wy$s6 zGRe}oF-sY;#Dcoie05kA2}>(Q?)d^d4-RB9)zU2Ar^$`*z47H~vJat$#dS&%%|^mz z?;-~e9K{07wVffax$Wih&kXh3D5ZN}gVo%vuh^L>e5aJoOl?mc;_Jv1p5Bk+f(KS7 zem{J2+t|GK6qr-!#gM1IVn=*9@p7)ohZ7TAsQH0~dNPq?+Kj1&Ac^@@1@X8qz9d~K z4oD>{BVvxYbEP6lUUS2H2u6hP*$_)~knD=6v(&-;9a^HBv`ixPQ^rVDi4+T})cGkX zj;M+Rsmn+0iNfB3zB2!8d}&(FcJl3$n}ni_wsS?c3*qdnY!Fgm^KhN=PE}XpE_adHj{SX35J-L%%LuQ9ry$fdpi8B38z>sh10eryn_^azeI&C#KFJq z==HX2q2%xpV-A0c9I~MPoh4E%QRU+hgo1U&jQM(C}Rz_TZXN8R(xKHA6BAt1}(^7n&Z>CJ_jw>5_UUB^>G*JEp^n9q% zajxeq&L6$Ic>F+#Bgs%=gh!Gg99K}s4^-lhXmPxYzrQB|6E{DK`~p8vS;El;m8Hae ztmFs2e-F%%Jjk3C2bufB4l?buIIa%rG&$7X%yDCGZhR%j8w&zoJKE6Pcz>ea+{o|w z2H~@1Zd^W^FVjb`AO4E|R93&vaVUR`ZycV+|213$sBbc=sQU|sei*<zj3e8V-+9uO0FH-5Am~N!>fxIwMQu zs|g$nYF6|8WmP0>T|VlR_GW!Y=6A;zs%2h7Q==op4B;&8k>PuVl?^f~Zhn*9%nfPV zw+E|(8YO{wpD{3x5EvHJnlmmX5mk`@vkdK)z6`E&n&;l}JMqP4v1~96)*=`nt`NhS zNi`8uk@LIk?~Y`S-O#*glAjM5^YcdX!-DG5(#NtY5`Grr5x-LzP9tH3_OkR3;tS7e zVO(LOVPrf&SE)Z04+!K%Wdb^H4T*wpt0?{XMIA{Q`Qp zTtLq%qsPJ}ubFscD?F_(s(D(?_1t~FCws0ZJ2#m-KUq9CS)A+1KG9Q}E6>1&QRx=e z5Prb0u{*JkhK=2@iomeJ4;VHo-J`;=F)9ojhuUk{xQKnT2{JY$s9GJ=F><(kne0E8 zf0rh9=VJaY#E3V5NEkMD(2r_|`;YN^zJVm|2tTeL71lU$d(X>z@3;OG zZC^p_;%MJM(B|8KKOd3eKTgJZ=4j&pfHOiZIFeUE`IHG9524dp`jaCeGh7nGXYigD zJ*0-lSE>Jw*txH4-9!oL*V#xTJIMbgpDaj#X6R;BBq(BE>FUm^=-Vy(A7e@&ENp&{ zEDtrfzIs_F4PqwA@rS>o3$38~7CZlkoKHBZwDaB$`-32Lpkv=}3#`3!6iGQ?dFqdNssJ z>jYQ(4BWD?+GkvUEPkM}nwVu}HPJ_JYJOk~kV)(m-qbsVH+6rQH+3}GDCNYDp&7uC zHV*#HcpQ9+n{ixoM}JK~ z1?hiLSsh8trRF4GtK?+bjXNN-h50jthy`_~CFNpOB)q_0=yoU0iM807^U>fqZVa6d zvwy-;FDOOm@+d%_Lq&9C8>jXHb?uUfTx^WU9J_!8m89+6SQQB(a>NZ3Gu1rYe)YYK z{eqa1f(_hzUCXk-J2lR`QLdS`7w4l#xWb5P-deb+8B(=WF*1b%4%wM01OUs|C&sPt zd|rfSbw>EyAP%s2C50%$%!)LF;C!j(d8E??8-Kyd9``1{R?)3DdBZeF2a4`Xp^K0; z`Y6*Q-GzimSdQ;X=C~a51C{yYNmk~Qv#jI?*66#Er^On5TCCA$+P_B2okP4J$Oi7s+s1?x}KDzIdGsjmEVMS}g}r_~BX}G8zO` zz%Hliw!_@f_5;wyW=cTYBv`AB!TJ)xVnGc$*GhpzRU{1I&O=OVyD_Wz5K6@tA-U6V zr($m!4kswpL`((HdiHmRZi$06pz(8sF+X1=KP;#|?UtBTk?^y^>)6}u@d1Q|K~R-q z!8<>63*M_Fupa{E?8hNY}HX6C)H|j`ZY`ZaI z|3JoAP{GdIr9`4C62|0`QRn`2U3~f4K)&QDR2@s<2L+>z(R{VmTN zr@c>3qzw&|Yu$~;9Q_kHVnIbZJEb(DDiV$s$xd}Q?yiyVCGn+bT_8oK2;mk=tnB)# zt^m53T|Oj-ZaV=|WNS*$rlDJn@p+o~u%HH=gHjez6$w6at2Br;LGM(1AifCg9@Gz; zBEUwPv8fTB634~~T>kPjE8pmfl)S0Eg;KS`{b0=#CVcUeb3%Q`DB5@AZuf0(qON=%GnRX1G(-s8#%la@a|juYPspBJSC+1D3}u_M$-M`FoI z3vzwCBwlwJ z#uurV1k6}yx4V_|%~)a%t4&z=(lek-c7>wLrb0TBiQY$-)-Fj`-k7e1q>BahtK}SL zRU~xD{nepKBsB0%xc15TBGxAngO5^$EmzK>%@#)=YFPKfL6cjs#$Hjh@!9Q#-A&F6 zQ*#C9eBps}g$How)R)*rzmUGfAYDWGfjg&S_mFT69TKjg2f|!Kqd6@|MI z8q2$9(qD-$R@b(ep=jPf1Qg0b(=gnN!q-1(|E=!>#n$VojQivTPdn!;xzP-0nuP03 z#&BInxL8oP&I3{&Q56+jMF&>;etM4SLCM|{U$~HD$w6uc7&k=I@$d|7_nhKdViYK? z!sxB+_Kv)Q1V+u8CNX-OF-A8LBNo)6_6(9$kzfS3%UZ_mQAAcxJ4}D&&QtG; zIljUibg{E-DwbmAUF_q>`(9D~5&RP- zLF&%H&eq#cGaE{n-u5SkX?oj_EHkL%2P*MLFu?WR$=@GC#u@q4T0+T zJ>PJ|DOvpbao=+z z=HPWr+9hc^XH3&!(!_%Lbe@*Nh^k0vT73xnx4vp+@^)*s&rJ!wvsf*7QSCk#U#j+U z_M-0{oJcB=0Y^!sznHRS=?*NTzK>B9F7a~uy7I~rvdA503cBO??r}SN<(1*{*A-zM zq}cmB)%91>-rCL`?3NbolMFs-%-}IH$b#B;&P$<0RU`~9(L~TrL{G&RzO{H4U4xD^ z3G$?KdN@&Q1I1M+{SAA)BW`kdUa97SnjW-O@oUBmjgcW1RHEi0$Et`JnzZs39c+DG z#NUW7L#w=o^3;L6KH{$56rdnk6+r*MF7Jp%FNHa|Q3)DA-!=wlk^r%w1~q#gt0Dnt zncH?@nfD>|-S{H3rVb&|c=bdkd=~O_)gZ6J=%3l~9Z3L9dlN7UYMNZlzGnl?w2wfstYv$v;YF1iF_>g-YZiERF@s%8IOkh);@a(H3H@GGQMs4g8FpcE9DVYkpOl1q2iQb^WWuIw&TmhH6D-L`gH-sSGL z-A=phZug#E>~g6jm8Du-i)48#YbncRODee}dAF4SfsllZn=r&M33ymLfk`p}0y8jd z6Bq~sk>L>1A`@T|&Vew)RGXx81~SX{{eJJhcb_)b8BSR>`s364e(%0pclqvq-+ct; zoi$rTQ@La_mK(s%9eh~FS#%3ju537I0kt5+vh{0FgS~i-1G&tKp(~ezOyngwU=DJb z8z>`}wvy%YQ% zG$wgi3VK61tu+_d(Z;z^cZJl+z*PfT6qq?p`GvRJld{$LsD94T2kpP_UJ%sp$VjHa?k zrx78KWCwG7KJ6T@ThHJxA%Y+H)q*wzKY zW#rUXvYb9gIT4tlg~v=}#8ehJH6iHnY$2ETe~2|(xq9-SM98{G!(Vch#Iip(%C3w~ zOJ3iFT*-}Au7aASBhBzSZ&IKa zgv~ei`lM#_tSGvQ$uo_=x*Tx7*Izfx4U*Byc*$D%ZEA(UEGzt#iHDfVqLqydbr7fO zP822w@Jf-dCA(O;x{=c>C%C{m;03&6b0j>4L(Ur}6nZ!%dk#3$H zAS0Cbl`ND$qEHCTF{=wrQ#php>uakv`6}>+IgxP-`$w3ROY#EnruhP83j9Y) z7R8sTa00W(dhM5{vM8h833ntraVGCf^!}>~l?L7odxiZ|eC!=zZMlqXUlWBK-?PYL z*gQOJjh#tx1M_+Ks@;>p)+oMpdyj*iHqK26S63fnZY;_1otx(kl@ZqMl7;nE3X8zZ zwGQv3sVu^3N{>$DFq1do-}wGhm8-4SQfKfnCTzt)Csub8ViLxiB*q~rvaQg(7xwrB z-s_nh=k}c_KBKs6@!rXnd6UBx*iSQ8KD8WhzsE@}3kA$*>=Pwx?CaDRftg(Rw>m|cOfG0b-7hjQA!<^D~DYV6*pTx?0t7a>Kn=8KAhg8(rTWSa;^}6 znOXNbzh-{FtE&xjgJdN5t0haYNC^_Cgu)M*c!;Si5?q@e$C6@KZY1ab4B~H8t`cW$ ze4b>y>GLIv>1~RMz|6EZKGRedF*T;gvxQNV zVPB>8dzCAtSEKp<{Ghx5t^FnGN1=w3Sg=}}K&|I;OgT@E_6-bV$0qu463XKJs?GBy zhO4mOXO{fN1AzNoPeJ>0GYR1``uf9?_4OU8%|q7Z2b`S)7b*PJ=@~$tsj`gr&nPxP8V0#wuO?yRoEXhQ@YJIzWcc( z_UIyEGb;O1$ttUU81W!5j|;sfLSibr%3=cplVh&RW-3=@vTw*phwUI$hJhhWdKk@R zj09V zFYLLmRj#mP->WSwb5h6zj{O>4ypq~7Z){x^eVvKEBzs?577Cb=(BG6Sp*55cfq7`{ zf2FA`5^@tYK3N#R;Zxbbg}&S{`y)Di{@*H8OZT2**lBWQ{+PMC`R`2WC7Ep4bJNBB8HopH$Khw}rv!FFu2 ze>gX=c$N0pB8le;^lj$kxuw{Rv}5t$8Fl_+$?DudbrL9y!l;Rpn98Eg4NeTwW5fBr zLBBZvcjb!nMT%3-$UoC}frqutmS2As%H%+Rvtu&M)WxF3bXEbT;>A*$a2F7p5=r*vw1IT6x+f|KYUD)5}6~+GR*iyKGzRw9E2*o@V>P zxsCW4xh}7R_V&9y{{y^wg76GroM%5AI8fM3Lks0fpj7eHywei-v0%tMODm z-zBDGzD!?EJ8_^>E4BdYsxKFf*-B!qHR~8P%Djj7$!kQ61G* zu8tzI0N!^gH!?ZWGK!CNp{dQEb|mE#bF1mCH> z?<(6gC%Y_}HI*#0J(L-Nd0p6UVj`xp$V|qhfx(fS|6R=Wl`FMaD2I7jSu@&+HkDHy zeOP>A6r80bZ~H|*@0gohf0Er9gSo-cofB-NNAeT*n8=j@*PQfm1-Aj|G@d}UHed65 zxmZb>y8e{jKWil_%!3#B^5{ZLedQbWUl-qD0PpW%Ln*szMv&N7$9rUZfr` z%eVa)a_u%wB1VgM8Z{U7Sv%6MWFM7WG=2H|IW_#q0{LOplsik7*t;n)0yEQkUxKEx zs7d+}j+)|q1srse8|xc&&iQ$wauxM5n`WMXfc1qO2D^|FRm5pxGn zzs02{!{G%|#Fg7_X3vs*u~mk{3kT2CYEP9cxdW6Ofw^scxRs`|NKTf#4r0?+7Vmlm z&+uYcyMoVFu-;IEv0+Bri*V}_7r#5rq3=j`oD24Rg}G&X5@$;f74nl~<9jjAVBGGp z6>=NvkNJ|r729)5CN>$XFAz4Pw3kX&S{s!{U_M()qp2)PlR@i2Oop8^i>j|yuEHLs z4qEy(w2N0!G9b0%#BZGQ2y=UGZ>;&1#y@-M(4ChK{ob=*eC79F`IDE9{ORLI zzO<_MzK<%T&6yQ^qo#u;tEr1>A}~+=y|JUqzP)PM$|vqt0VHxpwB*l58xJp0E`jqoK}{HI$%+ z2+TNZI}uG~kxPSA_Idxtv{;3z25*S%O!076=N$lBJkFkfeiWvqKYx$acbP^&M%jC) zT>ZqE^j}ztIW!ps&kdH5QL1Da9ifZ}%*nzZnz)FmEHaW^X$ZN0E#XLo>Zov(>9feI zWHYj{Y<7HHh4e0&LS$pvJmE4DIZ?7iGE4#jGs%(&O=XdY^s=4awQpK6*H@u3!Fp&U zh8;c5NR7j}asPWqeS>mtq8Z?!yUkC2kMxaR=D1G!+Iy`I_~NaZ^t{IC=K*MiWDNpe>rxdG-iv-OZw<`8o07L$6R=nd9oynMzg|XI6Km= zh9;QwpEMnf`#IeyP2xP^GCI0cvX0JEIRxgUbvh?aWzmt07p$OTVnb`EYZWS`!Z?#? zv5u3>i_a4!S18w)OeiuhK7Y`RR^D5(RxU9i2+S%=D>RiwE7FAdPUL)Fg^C2-rfx2; zXcNXcNo~Q#vuLL#%;cooj&TxPzv5}njyu&8r^s`K7Q7FFZ|2=&P$vzXokFhWz85Jq zo}PCS3d@AM0@c9Q>JBazwpORPSjc}cc2^&zvAepTM*hPJVr%t)tRN1^3gY1XUqO`V zs#y^=3jP41TH4~_4Jw#G9r>rL2Inju(p_0(UC;T4u>Xv;4l0izXJIg}m}8y;N0a7;WO^SvE_M8Y3n?uZbLayO=kw>B0C;b^@Fj#1?t>ejupIZJc zl(wnmZ&y{l^T3Mx-r1B6%$%s2g7XnXwzPutJuEl`>eOFw4y>3x&r5e@DLB#|*z;Gp zk0Lk~T5R}@u9l(P1-!m2y-@4b=EdbHb-Eo(SDd$)U+b4*eO-%;b)-7NWK{IAl2!Bp zs))dBD{L|m5mQ+Nqc!cgz08rjUAdB}VZ*@H^O6>zG*5hybCvS1nZPHPVrZf@&g@Vb zt^9P!TDeKB5SU+uJ`)u&l|?ISq*WfrM5B&JK2fdc8H8@K2@@8eweZ$F#8JoO*F(*7THLd?aX3zu!*{$qrtBkc^0a zxnvQ2j3Od1FAGy99%Ax|XgR7S{{6!%fWE4}W$&pvQ8lqFQ8iJWST?bIZB>3*Vr6&L zGs_ay>#BxV4KJHmkyyU2YGP$##l$zq=~F!eiztvFruc?>IitIr|S!%{GCX@a(qk8Qa#XL^Tp%`IR1am{go+^62R2 z2DzXl~t=hl)Ke2URN+W3#o!W>WUI>GBx)fjd1 z-+4OVKNt(u7icV0-=LBIIFP8GlJWPHjK2>oUt}QBfv~=(>bR3j)PPB8%7{a?n057gY zCMPEQhH)(M1SY(-H2x04RiQ(TT`W1~i{)B`P$XpgI^G>)On68!qq2B?F0+O6#@;Os zzsp=5MOCwi0?YGCmuv}LC~!t}UtsV`dZX(N>W#pBEu1$o5>r{ECH*agf6fNqKdM|| zg($3vzQPdZFr{YpWR*29;!MDacV^&7cBoHwUh;(rXZ>z*(aG%jJn7;}?u*Qz_nK2+ z?q_Ghl!w__hM*b2{YlA!`xFI7V15_A$HYfWWf7coh1{31alSfUSTrVw-Y812Yn{``sI3hm3xm?b&cMqXE4Fmy(1UnyB@ zzeueSn9tVPHZ+w*YqF&-fsabrBKynA6&d#%a3{g|Sl>t?JC-k?Wb76}=?aeQE|3=$ zWJ8NnnN5t}V{@kLv740)uDt$=d9fs04rGtr++Z19{dLK@`uEfofmv(qI-sd6y23Gn z*+O@IcyiX+Z+~04(wcLOAg1&#NDMem$(8bfHCf)D8*5id-(c!bpfi5oJ7L@A&kw6X z{QHu{^qUkDfthLT`=zNYV%pS=A!VMOdJHW2g$sLl^y7J&8&}q- zr%}A4ifJ3I!SqN}cF1(y0gKBxG;u=Sz0d8*?lCUMGpMMueR!>P=pN^xZeJ|PTs{5+ zl5UYjvo;QE!_^TEYr|MweoOzsc&s`@rv2O1EuQl(yN2E=? z`E5kIw86&jQtAYX%p2VDrPbV=b!WOOi~gG=UTyfwh)?+cgfLa;z;+9TuUdBGj}4|! zyz{NN#RnE!=8di^+V3#;b}R?n?;U_G3kA#wtE#?KVf|+ci@;1Q>@u+tQ&}|BC}Edx z*IVEBT7dvpsD$JTao+e;WrS(PVW~b#_|n(r`C{yzXNf}pjYnV;ny%KQ0YpxxO2hT{azGUfrnbIRL*9+fk;v}ZBNN;@) zzLPT6hxdc$w1aCaS9tQ-RBZ?4>^Tflt!5C5A1!@2I^T)d;;rH%3nqZ8uXW6eCHZ#h zV~YjOsB2@%>iR0xMPSxiA5f*KEb3}JI@D_KTgr;G^9$3o7;MoeXqkylAY@D)R!lpe2K zDM=;e^|UUU`luxmj)dF+Yp#?D5U-M&odB+~b}=uOxRRPDa7JBEmaMLCP+bIOtyM|U zR2FqL_Y7bT3s2? zQ_BKTcE$8E(|(sp`}?|3xp2^omiCvdrMIai0<*HP+r&pqWzo_`EA+Fg)LgJ~^|WI? zJ&mBn*2bwf=G`sVZu3Rl`@}|-GwLJgqLFs~;6=PRW-Aw(L&9LZxwtKdq=7!D) z>S)P=T17z-n3;uuiISMgBB-?}5o0*EE;v~jZo}s`%>JB{l`E#*3@2aroAteVE<0Y- z*yuXs0JOHaxVFw8dskQ)=E9PU@wP1(G9#*il0~(Kq9QO;tpOfQWffHf_xpWs<&1XbhRkSZxMb}#Qac3ZUE#cmj+n}# z9db{EE1D85BbxXz3fxi<;qpqA$FkVQCN0(I5&o) zk6ZAOMI2hximxx`vQKuvjPtM{LVowBs$9|G*vENAH^E4hyqD1A z+nlJon)+^L!jhc*$k8gR*!jX{RP}u&t7->TMPQyifbKxTF~>3v-d&I6qCO71qyGuI!$I?A$}w*t;^4LNloE1mQq7>{vu)R>}uVb}X6@uF!s# z`SO=$p7DOxYLDM5fJSXUSF+k(quL0}X6r+CG?hhdn{YbsL=uPX$+pDtSsO_|UAba= zf_0oT4KD|{$R5#g_PZvBakL8$Rpy!Rc!BmFuWWB$ED2nheTEtG)>6o9=RLw_RQ8J{ ztE`nOBQSpp-!zdDQ+ZVuKa-!x&sN#5{C}jfF^RxGM`iORfvd8AxAZDoAbdt;|Dj}+ z9iqwz%wJ1oG?iCnvaNr%%6{wrBbCW37IW(|Rs@Up|Gm15ue!}j0#{|fz4R)ZFN+X9 zqq5&ES!FzTr$Au-S}LQdEGpY*s^j=@Uv6|(gZYKZ71_IHH<)7#|Nlcx_Shl`;7aTd zm>V}t%W^+^WjhuRo>ABrOIBDn6-Hp@7T#;(B&L})frGJhDwB%EGF`D~`%GgX7z`zo zFm#5Znb47VI1y@{WerEW16^UUro!E!Ogb4#WD=pnX>nT4RP2WJjnq)7BM>wazpI5~*;oE0jrvQ@~Ipp6Z=h7f82;W0_zqnhHfz87x&h zH|q3;?Y2IUPIY7s_NGEfHx>kRc~TGR2aa4?WU;A>r@3A*Pj9!f-Z zdpMa2F`XY4b0U;XA;2>mg=tO%x)Py4Yj1|x6Xy>#S~o*SXbi`qNIQnppLw>;A1HO_ z*!CfnyoVj*k(u={#sZ082Xk7osXGwv3LNYT%`_im1_lF()=V@VIS3IL@0)NZiqubp z+dEQTiou)=W#X|!3StYou2&z7!I7(%8qiFzgFbX6Gtt1zRx!jPkwCOH(;7+!6Jd$1 z>p_F*=;N772u-?(<@64@8i;t>Gl&1{Km>dm@&OCo=~T+D_Qycf!%G za5UsOe25OskIGY#P^u&5sa+gI!pUS9<*SLVW6@AD+-fDKn&W}q2$VQG5b;P*GUGj1?>TX6f`)_XR4NwDv<6ZEH-(Hb6*}T2n``Wi zhg_HS&f_eYCVjUU8>^gnBHSGa_A)V2iExmmqKT12O){&n_|2`MAhQs8EUsh0U^?EL zX@jg`s}oy1WT|uR)AhmDXl7Q9iw$)GQr^v=t*)ZAdV{1`VR*9N zGRuLJ(r&S+bML#RHI6Bx#SO97Qp0>fZD}-DZ_HMf*0?sOGT&ef@c_~?91oyMG#OK%!kAMr|+c4l5K_s||2#ZisU>NDLQ@ zY*VO$a4O_AHO>Qa&sE)ec>u*Fm`0I<{L)d=vM{|;mS~#|??D}wv~|RIp!kR2T4kZW zwx=PrOfrOe$dmqRaf`HdniWIaL(xzId2GB;Kdn4?SY0|9z|4~p!6>q+2t-jC%+!h* zdPP%t1noppk~Ae%v72)0wN(x_$y13?H=1#lKQz+OWIE2Omz9Ftm1wZmv*Kdk3Afn= zT=PMmacV#>1vGKwUL@2S4$SW%>oMz&22mbLqH*r@*C!fny9w6RGP-J%DD)(lSYAMo z6>+KOB+X|Y48~B)YPQtLbw?oC;Z`OOi^)_i>(LR2hJtKHGqbuCn|!z8nQnxY&eZ44 z*4@@ncQ_cLq(i1RVL#jzh_+|qfncW_hGy&Gu0XF<>oi$+SWLoE#I+Sg&kJU<7ySm) zrPwz2hWwnB1`TkM3Iti4THV53Z;X~QU0am$&}h03qoi36qozd`NDb)}mCeC)A`yzB zVot?U0o2BkSQ?dyN#}=s%|V^8LrJ8jaj;rUZUyk5$XCO#x)r?!uVr1++L39`cx_8< zYb=;%6YBc49&$`&oEF-zmHH6+09_eTgz4w5R+|Y)ozip^Nz@ijK-)8oYJvj7ZM|;) z7|*d6Yo3GGrc>({(KWC^=B{dLU2dM*=2=Z9Qaqdrqxa-y8vKB%7b%~PqGmNwL}0|sJd&!J z&A7Q)8%iW%iHvj%9LZ_EXa(julR=49>KovUeM;$x8fT5@UxX5<`lVxUthMMAb)mcJ z$-3UM7*}iC&=p98+A`^Qdm_M&S}4?t%wJEtJ77rJos|dWRw$B+_s-PFwUu=E4Lpa2 z)#?j3xVFqwZgJ8l>Cn_#o+}f$X3x|WingP>Z|+F2aAbJ=<>F*Lgm< z)vA~fbwodkicIrCJ+Q3KuyD5Bc#Kq@HP+%!Av^5C+h|PDU{~6$rj3;~p4VV80r|My z9>B{)G9@`_ez&GA*43)Nq5-e-493moHZ+qRPTFX{!fxg^`EFypiLSoazC5aT;RAXG z94*BX7%zyFJxA-?5;1hXoI=XF$L+h<$t_>!vQ}DPr-qVYbzOTXhCx!;s>Yhz!)^2H zZVm5PQfq`@;~l1ALYIb}&>6?-q=B(Esap&mSjA$rrjEDCs7Gpawi8sN(;-AcX>Vww z<;1CQ*x^Diilt=kO&QPPR)crTORqZ5XfkQ7XF7z0aLb2f_w^h%d+w9TM9>?M>kI@U zigBFOoa~*LU#SG7XD}v82N6g&x^;T1EtCpEAgpU=YHbrkJk#h{oZ{;AO%Ve6>hGX9PG^mM z)~Zt{&Ab-D`6;V*cU;QsQELd@5{%~9!`Xyn@-@|&o-k@T3L${ruX9)1ZJEeH=;Q%k zQ|>dGgf~VLI`lpl-O zug}!2uOnF7g)zFXHe1~lj-c~mJgbv2fKT?dGKgeDY8=%@0!RGKsw; zah6JXqy5H6s6C*C)QZ7I&k7GrNkmEI1O|LIkGMqzHkqA@a*h+DZY0=Di(_+oG8ouK z0w~Y8Z|b;prUut~-Ya>{<_7xsA>Z(0y}2X3PEO6Z`Afl^>p1J zCMP>g$ZDyUWB?VLvDAwtluBV1%_Lc)&t_c0Lrg8j+S=Gi8{l}wF)inGwYvO@qvL}XlT&G;S+}9e`x@OZ3u6HeL!Qihq zBtn!NKiylHAF8vg&Uyg?Ez_QCIlIx^dNt~Uk{>#QUoY3Duxl`DX6ILvG5BlS4VX2Di%ugMj~-1s+;D$XYyt9ym#GntJVIYHNvj9El!i%WZA4%%XVsh&$0O$GRMX! zxfSw6#;=?JN}`C$on!!uBfc)46QWcHlG<8Y zS{Dz?!U!tGt{Cbu7Ij%9TZ0>@f8B}fMycBa(PR(j#Z0a=SXS5ABu1xVa+jiJ)a|VL zO5I9#*4J$iFYGD8da-bt-)s2l;<023%MPf)&9Xx?THr_+{RXT{$t;iC=x#Q5bT%l} z)ys)kckQUbOx|<-%tWu&7~S@w$yl)Bh}Ok(hz^VnZ_Nqf3J8XP-+#tqvV zBmk-^WVlu&wJtY5P@@)xLn`-~2GmwZGnyZ+=NrU`MHf_YXkDDlQa?A(d(X}9jq}`g zqt_%ir2&mY0W$yT=u%c# zjm;#L%Z=yjFfY)B_+z9YZLFKcYq7A&u+E+bhdmB8pug3L8tY&hZHBJL`s^|+)*Fq! z8|HVLd{$2}Iyg5yd{aZz>Nv?NaShJMX;y0LaLScNo#)E@uzAo|XCdvRu-M&<250$` zHg_JdMvPWPy*d#Odae7+8q!G6EIrKBCz1i_p<(XN^tqao=&!e?Aq@@*?VoIN@Ax`; zYyD_)8N0U^-O)LBZRc}b+{TGn@*{j%vr5gL!AeG zIn|&}`~$KFOb2JR-`$k0e)G-$txDlss=@r^#LNS{;>GH8v@?ow=S&B# zF;r&`X6mtShb1y!Q__%*3+JGNF^IK?qcacLdo&u-vO49Ty{hFjpKpZR4 zfK!_@1&)lAC0hAySI5@KduFY;cQ!|%N_TpzGY_jBvrpV+v`SeFi~r`zEk2SC((Jq8 zR!<$TtPYNPD$M|=dc`Yyp0#ndh1*D+ptvFxH|Px+k*cSL)#z<+htxx^0ijpY$;t)m z)^bO(I-N>mSq>8ATJ($tgE#DyQs-XgxdS8pN%YFhmLJFs`O+Qh!pg53%ne$>@i%1D zZ!gu=>pfrXQKvS0eW_U|f1Un@y+*XQ2ZPIa0Hx4h1=V?WlR%bJU!{YyESUh9G0}~O z#(e|o-Z^bqvu*TwFfc3oLF zXP-buZ|K5rB#ebNbj2LUB0R=M4JI;IHp-j{8c%F7LWr^ODD$lPRIT2$EX`^$!)*Hw zmJUM+Y?Y8lb!;K)Jy$l*a|gS(_S%x6lA;t`yGnL0Y)Fn_2l{3lYcfJcj z>JFm^$Lb9Yzb=5u`i>nX%tl{vyW4X-c00LNyx0}RxZnS4&89!W{V{A@1HC5krDE{8 zjy}E>uiVb8E{qNI_l;+>g+jhCURcg8(fBrLRRM?3R~7h?+ycJMhhxA9?<71(c!+>~ z)K!J&2|+?9AxSt!;Ew&mIl?&MD&e~c-%og*@KM5#6MmNPX~HiPeuMCz2wx=pDdE2m z{+jUL32zg=Ls-H0%^xJx64ny75Oxsu5S}NzLTDv)5mJPsgj0kwgfYTp!g~qdNB9B4 zza)H|@Uw(}L--}auM++v;r9t&B>XAi&k0{6{2k#f!v7@vAHoBC-MNOaj3Ev^ssyod09!iNbzMEFs{ZNkqGexC4I!mkj1o$z_W z7YNgYFA=^%_^*V&A^bhzn}mNP@Z;WvhY9tBX2KT24#IB2KEjKH*9fhILxeb?o4}8J z7y1cj2xEi`glmNFBD|mQVZsj+K1TQn!W)E75q^>I?+L$2_+7$(Cj2qs%Y?5IzE1cC zp-6a}@Et-mzizsUu!hh`*g$9@>>%tW>?6ELc#Y6XI7EmOju1`~P7{U+teXp$2sa4d zOZX7s2MMELgh9et!WiKK;Tqw)2=6C+nD9e{A0^x-{0!me37;kW3gOoY zpC^2QFirRp;VXo%5&o9&KM3C<{1agr8tTFWgw=!w!g|71!cM}Igl7mZ5%v>8gigX? zLJ#2tVStb$_|w>dPoAtcT&wo9=Wv_K(t95PJZ^taVeVw=LHP-c%b)E69 zZ`*G6d-hwlpT|A>^oA!avgt{SY<+M&Azsty={fIB ztM{x==P}D}U)$ld?6xNDFfSArJHC$lw^_g2*UqzIchX$0oq9&EYb2b0`vc2fp&R^r zTiTDj%R7C`Nm|4zQBKL|5|qHgwD z*Vb>@Y2xIUy0_zKKWq7U`(e*dM@#aolNajGR>#c)%U-y7gK7AJrlIAJFa3>srlA!t zM`P;el=r)Ke4R4*cKrj(p69dpHz&WGrdIs1uia;7{jM+l?KGJ${Oo6(m43$Cjkvyf z)Vp^29~z#8;%NDQTbp|c=NnyKeC>3$(`TXZ`_iq(^Jl?$S!w%*r7@=-&~$sv@<;0* z%@-?8?6mb=TXFQ|o%O6;X6kESwFJIZT?5_XxXR8%o%Z=@)?cTQgu4^oR z_AhX~*RtF0?J(HsHs!f@+Ak+BzUAdnjdi`>x{g`bwmt9JZ)d#gTlRIQXMf`%@A~!@ z>w1rMz1O?GRZ4iQF!+Ag_Ghc*r)@uH*{$EYX@}o_)^=~ZdD-&km~)Ma_7>vh*0=WW z#w_=F>)P`3=BQ=2!!TvpZ9gqHw`)8%w=Mgv1%CJTM$5hZtmXghyyxbY?WY}{{hpiK z*7Ys>*_h*obv3T>PwM2`d%b6U>2KV#U9f*Q@43Hixv|4}`>N;WmgWCK>EN>)Kbenf ztb83caZpG189&P)6>)EJvZPgXGQFf5TQb*ms=3GN8Z@l7Z#^=8sNBg&)u>3(j z8b5u{)>wAipRJZX<6Zmm#kR*hH@ECCm^xv$hW9Vveye4-{j}}Yvpy}^nxFFg@#SOQ zv)g&?3$vXL8O!|wVeqA=9cKGkD^G9Q;e5#Q-+I;;FDou~IDKyRTmIBo*S4Q|%f8pT zw!>-khWzs7wdMY{rCUq~c^*w~+h-B?4y!u^&s2B3o==Lt=vcw_C9joSZnI1OAGG|R zviz^HuJhLQ0%34}i(MuZ8Mm8cKb!IVx!vVm`>JTmf4i!&v(J{FPoj3*_gem2Veq-P ze#@7(mfd!5U;Er!@v_5q>zo&c+g5H{X@x0HEB);-+hMS-=WT9SMObmPt81ToE1Wl9 z_QK|KKV{h$^0UTrv(>s@ApCY1Zd!iY`Fk_s`E&DC@7fo(jg~!TU0YhR{dd!u>0_s% z9Y_1x^3yFNEY7>V-?GcvP5apz&y5{lJ74Vh+IqJAxoL-UujQw0w|>`tc6y@~P|SsU zPv5-ZUE8J2e%fcxc<$X4MW=@Tn9ixMG~_+^b|;;k*?DU$J2o-Pjb*n}%lBKBd!OA( zm4#B>&OQ6urxYvhwm&{M)^GVrww30$u6XhCbv9qK>xuS7;?atzI zUjNmK4bz*RDX!%8Tkoh^xsmJvVXGYWPGJvLtz0|3ZddUkVGmcWTsPhPL~*sSHB~Dg zoqjA(tPxgQwQ~LRhP}l)Vf9rjH%)JTw%8!7v1(-#f4fQ8+NzbCr?)&`Tqmr#YUPIM zjZYWX3)@h&a?AAA7mFK(ZK_(?JiY$O;$~r6oL|@~ti^e5o3KYTWN$qx>@m6bRB^ko z9TJx3iaUipUbV7ey795%E@AJgTKUNIqX&vl2;1$%{YhbaoZs6k>?wymEo`6j>(2;# z)?v>Hd%kLA%k;Kai!TU!v1;Yk>6UjFUlR6iCoC@ud&NnmSB1S+wX$)#X?tpjYkD1WZls~o$p^w*9lKMpRUJE8m}xQHU792>Qm zY%dr4l=p)pbo+_}$_LH!S>^Z!>dMEbcXizzQjR@d{Jp1&Ipt^d`KOELln)y|qI}fw zymD+0)9{{GUI5=dy`!x-u6zO<@qMm1sr-WB7nNT!&tF!41zeH;nr! z%u+lxWvt>6exs2Eb-1}^@$EAIeDk$e1ZSotAv32&$JE^tvtM0pe( z1^r#cnDRLIw&_QZ{mK*Oeo}b~TncJhdAHu*RqRoI1YE+?tNf^Oe@yvtaOCGx#S_X; zg5&pID`u3R()&*r`;_;COQ9N2J_yeE6|>4ugNt53dcAHE>j(2aDI0 z-vF0tVoLdeef`!!a7mw_@>X!9$1BB<@;2kXU3mw%_#ak&2wcL~sk{rk;b!b^ zL}?UMiepT999)v;u<`_W-OWA4q|y|q#4fG88(dPSNBI$O30<%9qu>&nW6F<%KQ_I+ zwRl4LNpnA={1mvBe&zk(lG+2x2f?Afmx@{Cr@>MBJBvfg&wxu^mQ#KfT-1I}`LN+5 z%10gdck{}}%>DDqu~}BbJFa{JT;e;a`~tY7$3^9rz$N)EE58EH)~b*h4bhm@ZI7qRD*p9L2=oKrrm_jeaZl#iPG zdF5l^l3wSPW1F=2cXwR*1h}Nvr1A^kXuMu1UQ~VwT=MI(@+;ty{YtNaO5Jo_`3-RK zZc6!ql}_Cl(Eiy$aB&}0j?J8!93kax;8J(AEAIf8wlu69`#zO-D(}+gcNZhdqu^Tj zm1B#jM(42dgyBi$DR3t5-L!J-Ce`rtC_e%&{`D$9YWOkb*m$b(J)!)hKEJ1!QGNh4shLmGF>&kacKM^nHl%EBc>^P@<7+mBuq8vM4RW5nu zW9I&O<=7yv={v3*n`Bu$b`~dGJz%CVnT<$YE8HE{M-ir1Cj0GHx2 zrThRorYI+PPA2iQrm7fNe;y$DtJAgGjIpt@;wfHL^Hup!AkAh2i$t%a^VfF94 z@`B!fp*XI50$lVqsT})^H9i-WUjmokyR7_*!|z^IjxEW0|GM%U;F7*m$`3r?v~B?% zksSnwzV;P^%CTEme=nrGP2Im(Y**f4?uV6QV=?2iyV$9`3tXCui1Mg$A5)G!$m;&E z@`Umyib>@uaM6ESc{jL}*B<5AzO3ortNbXqr0+51$H6hOxO+k=Haf4|bO&AOjPg_9 zjd#$R^(pTMZ@z<3;DGW$@bz~v(#$Hy9%;Qlr2Gu{#yc3#=9HfWf8-8YqjSo!otn=- zRUA=1s(f!TuY3%A=bgvH#q-Ls^Lpi$JLqhTE1v-0dIv+3N#z&7TkfDMc2PMtX>0f| zE58CR;lHZ<8n}f2x^nF2mhj%4Qhop<@C|n`A`Yz5;t#&=4o2NU<*ndNchL0>DQ`3P z+m&~KOZ>yi52^c|#ZKj2;PtP+R*Wc(g08)TA%0AG9DMVgEzcDXD^GyKyM4u^@)Wp4 zC#}32T!P!9{0O)Nw^#X5aEb0Q<;TG#cqf#fG|y+0p8~IcegEA)rTw51+X3Z+;0WIH z#jNtv;PP`r%Fh`0Ipt@;k$W!|&nX`UNAA75IHG(MT!NQZJ_atqJFgtCLpiyxd;%Q5 z|3Yz6`2}!M!$swnz#qMXzR_jnSHLCwSCwA_7qwqkeghog-(8$ij=kno)2qdRPQV-l zXL#-gmA8T;`BC{R$F_9+{dVOY;B~K~>!@)Y=EceaO$Y31GElKef&u`OPIuUGj|aPj|`^5f?I3FX*9ukJI-PZ{2) zydV7WJLuUDD92`dQQO_D^3%rskn%I&l6^Vl*q5)#eNOo>xD@6Q<)g-ZUOBe#YjU1f zju+vW9!R{Y;1b`<%CCTH;a83q12n$ZmEQoD^qW$C026Lf z`2`--!VfO;3My{}7deNNw}JEb?zSuMF!#gC4}pvPI+b^UOZr5VN5Q4^#+1jw;U7xB z@`SmcRGu>T)5^QSMZP`CkAREbdX*mq7r7o&ejHrlb3*w^aM4pn`6+M-PoMIBa7mv5 z<%8f7zO3@o=KhfKGvF+qcXP_mf{TCWln;X=J$4pHl#iO{^UBA-r8YdT9PdEt^W(}V zz(o&}$}fN;{K4Wy<(I(G*c>QcR(=Is!h2QuHE;>fb>%m}<@ctPA9%>oOF(B_4}#0{ zLFIVELGv@DybWB^yIpyQxgS=Jmmws7?sh8g0vG*9lt;m(I*lpEdlSkJD^IBV7mG>d zDR9wGS~*^^VECRb_9#E19P@t4kAh479aE0CF*N=sl%E8b_-B-#0+;gAryMVCsQUrs zgWwY1tn$<5{*ZFK<3a!4Rm>?r3y$)%y?9RfFgWx7?uc@{4x;|$m5+f-^Kf2y0bJ65 zTshtx(e#;AegRz4^P=)g;1d4J%JGtkmglR=uYpVWuPeU+zWvUQP;p8*Ui#4R2Oie) z503Ok-%oifxP&L99Pfvy9@~|7fJ> z0+;ZlmE#Lr%6pU_0T=!CDnF|C_Z5#RKMpSC^Mvw~;L?6%l%E1e{kgx`r@S8=zyDNm zK=~lJmVf1^!6iIH%FlpH`CBxF`|4Fe9iY?E9RAsna9s7$BXYy@mD?p z&Y!zGsr-U*e^L1*!!Ikp0xs3tRpr;fr8>N>{06vW-<0wLSW}be1=eWs2S;bP7*yH{ zDryTUZv(G+eRZ*2X$L4H6IOZ%RMMhTc^9~7IifrYEF z=6*@+07q_`S-Hf-`mQ9#eiCToV6;@{{0F4`!5~Qulj`eai7}pWYu(KByeSKINyu z5uSa;A?0Vl#s8f0v&!FHJg0mZT*Ne@d{m!*x|ml!X83vK1#n59ape={{-p8?;3D>m z$}fQ%H~^prdRBG6H{i7yfD4u zMeAbw^v(m*D*|sKWw-8|UhxcW-ErJ(yt5fk?R|6Q(|4ZfnO<=O9^cuf9=o^P3-_rF UcO(?W?G1I+-?O@~6)$7{Upkhv?f?J) literal 0 HcmV?d00001 diff --git a/module/scripts/lib/telethon/errors/common.py b/module/scripts/lib/telethon/errors/common.py new file mode 100644 index 0000000..86bdf82 --- /dev/null +++ b/module/scripts/lib/telethon/errors/common.py @@ -0,0 +1,180 @@ +"""Errors not related to the Telegram API itself""" +import struct +import textwrap + +from ..tl import TLRequest + + +class ReadCancelledError(Exception): + """Occurs when a read operation was cancelled.""" + def __init__(self): + super().__init__('The read operation was cancelled.') + + +class TypeNotFoundError(Exception): + """ + Occurs when a type is not found, for example, + when trying to read a TLObject with an invalid constructor code. + """ + def __init__(self, invalid_constructor_id, remaining): + super().__init__( + 'Could not find a matching Constructor ID for the TLObject ' + 'that was supposed to be read with ID {:08x}. See the FAQ ' + 'for more details. ' + 'Remaining bytes: {!r}'.format(invalid_constructor_id, remaining)) + + self.invalid_constructor_id = invalid_constructor_id + self.remaining = remaining + + +class InvalidChecksumError(Exception): + """ + Occurs when using the TCP full mode and the checksum of a received + packet doesn't match the expected checksum. + """ + def __init__(self, checksum, valid_checksum): + super().__init__( + 'Invalid checksum ({} when {} was expected). ' + 'This packet should be skipped.' + .format(checksum, valid_checksum)) + + self.checksum = checksum + self.valid_checksum = valid_checksum + + +class InvalidBufferError(BufferError): + """ + Occurs when the buffer is invalid, and may contain an HTTP error code. + For instance, 404 means "forgotten/broken authorization key", while + """ + def __init__(self, payload): + self.payload = payload + if len(payload) == 4: + self.code = -struct.unpack('".' + self._fmt_request(self.request)) + + def __reduce__(self): + return type(self), (self.request,) + + +class PackShortNameOccupiedError(BadRequestError): + def __init__(self, request): + self.request = request + super(Exception, self).__init__('A stickerpack with this name already exists' + self._fmt_request(self.request)) + + def __reduce__(self): + return type(self), (self.request,) + + +class PackTitleInvalidError(BadRequestError): + def __init__(self, request): + self.request = request + super(Exception, self).__init__('The stickerpack title is invalid' + self._fmt_request(self.request)) + + def __reduce__(self): + return type(self), (self.request,) + + +class ParticipantsTooFewError(BadRequestError): + def __init__(self, request): + self.request = request + super(Exception, self).__init__('Not enough participants' + self._fmt_request(self.request)) + + def __reduce__(self): + return type(self), (self.request,) + + +class ParticipantCallFailedError(ServerError): + def __init__(self, request): + self.request = request + super(Exception, self).__init__('Failure while making call' + self._fmt_request(self.request)) + + def __reduce__(self): + return type(self), (self.request,) + + +class ParticipantIdInvalidError(BadRequestError): + def __init__(self, request): + self.request = request + super(Exception, self).__init__('The specified participant ID is invalid' + self._fmt_request(self.request)) + + def __reduce__(self): + return type(self), (self.request,) + + +class ParticipantJoinMissingError(BadRequestError): + def __init__(self, request): + self.request = request + super(Exception, self).__init__("Trying to enable a presentation, when the user hasn't joined the Video Chat with [phone.joinGroupCall](https://core.telegram.org/method/phone.joinGroupCall)" + self._fmt_request(self.request)) + + def __reduce__(self): + return type(self), (self.request,) + + +class ParticipantVersionOutdatedError(BadRequestError): + def __init__(self, request): + self.request = request + super(Exception, self).__init__('The other participant does not use an up to date telegram client with support for calls' + self._fmt_request(self.request)) + + def __reduce__(self): + return type(self), (self.request,) + + +class PasswordEmptyError(BadRequestError): + def __init__(self, request): + self.request = request + super(Exception, self).__init__('The provided password is empty' + self._fmt_request(self.request)) + + def __reduce__(self): + return type(self), (self.request,) + + +class PasswordHashInvalidError(BadRequestError): + def __init__(self, request): + self.request = request + super(Exception, self).__init__('The password (and thus its hash value) you entered is invalid' + self._fmt_request(self.request)) + + def __reduce__(self): + return type(self), (self.request,) + + +class PasswordMissingError(BadRequestError): + def __init__(self, request): + self.request = request + super(Exception, self).__init__('The account must have 2-factor authentication enabled (a password) before this method can be used' + self._fmt_request(self.request)) + + def __reduce__(self): + return type(self), (self.request,) + + +class PasswordRecoveryExpiredError(BadRequestError): + def __init__(self, request): + self.request = request + super(Exception, self).__init__('The recovery code has expired' + self._fmt_request(self.request)) + + def __reduce__(self): + return type(self), (self.request,) + + +class PasswordRecoveryNaError(BadRequestError): + def __init__(self, request): + self.request = request + super(Exception, self).__init__("No email was set, can't recover password via email" + self._fmt_request(self.request)) + + def __reduce__(self): + return type(self), (self.request,) + + +class PasswordRequiredError(BadRequestError): + def __init__(self, request): + self.request = request + super(Exception, self).__init__('The account must have 2-factor authentication enabled (a password) before this method can be used' + self._fmt_request(self.request)) + + def __reduce__(self): + return type(self), (self.request,) + + +class PasswordTooFreshError(BadRequestError): + def __init__(self, request, capture=0): + self.request = request + self.seconds = int(capture) + super(Exception, self).__init__('The password was added too recently and {seconds} seconds must pass before using the method'.format(seconds=self.seconds) + self._fmt_request(self.request)) + + def __reduce__(self): + return type(self), (self.request, self.seconds) + + +class PaymentProviderInvalidError(BadRequestError): + def __init__(self, request): + self.request = request + super(Exception, self).__init__('The payment provider was not recognised or its token was invalid' + self._fmt_request(self.request)) + + def __reduce__(self): + return type(self), (self.request,) + + +class PeerFloodError(BadRequestError): + def __init__(self, request): + self.request = request + super(Exception, self).__init__('The account is limited (spam-reported) and cannot message non-contacts; account-wide with no defined duration, so retrying will not help. Check @SpamBot' + self._fmt_request(self.request)) + + def __reduce__(self): + return type(self), (self.request,) + + +class PeerHistoryEmptyError(BadRequestError): + def __init__(self, request): + self.request = request + super(Exception, self).__init__('' + self._fmt_request(self.request)) + + def __reduce__(self): + return type(self), (self.request,) + + +class PeerIdInvalidError(BadRequestError): + def __init__(self, request): + self.request = request + super(Exception, self).__init__('An invalid Peer was used. Make sure to pass the right peer type and that the value is valid (for instance, bots cannot start conversations)' + self._fmt_request(self.request)) + + def __reduce__(self): + return type(self), (self.request,) + + +class PeerIdNotSupportedError(BadRequestError): + def __init__(self, request): + self.request = request + super(Exception, self).__init__('The provided peer ID is not supported' + self._fmt_request(self.request)) + + def __reduce__(self): + return type(self), (self.request,) + + +class PersistentTimestampEmptyError(BadRequestError): + def __init__(self, request): + self.request = request + super(Exception, self).__init__('Persistent timestamp empty' + self._fmt_request(self.request)) + + def __reduce__(self): + return type(self), (self.request,) + + +class PersistentTimestampInvalidError(BadRequestError): + def __init__(self, request): + self.request = request + super(Exception, self).__init__('Persistent timestamp invalid' + self._fmt_request(self.request)) + + def __reduce__(self): + return type(self), (self.request,) + + +class PersistentTimestampOutdatedError(ServerError): + def __init__(self, request): + self.request = request + super(Exception, self).__init__('Persistent timestamp outdated' + self._fmt_request(self.request)) + + def __reduce__(self): + return type(self), (self.request,) + + +class PhoneCodeEmptyError(BadRequestError): + def __init__(self, request): + self.request = request + super(Exception, self).__init__('The phone code is missing' + self._fmt_request(self.request)) + + def __reduce__(self): + return type(self), (self.request,) + + +class PhoneCodeExpiredError(BadRequestError): + def __init__(self, request): + self.request = request + super(Exception, self).__init__('The confirmation code has expired' + self._fmt_request(self.request)) + + def __reduce__(self): + return type(self), (self.request,) + + +class PhoneCodeHashEmptyError(BadRequestError): + def __init__(self, request): + self.request = request + super(Exception, self).__init__('The phone code hash is missing' + self._fmt_request(self.request)) + + def __reduce__(self): + return type(self), (self.request,) + + +class PhoneCodeInvalidError(BadRequestError): + def __init__(self, request): + self.request = request + super(Exception, self).__init__('The phone code entered was invalid' + self._fmt_request(self.request)) + + def __reduce__(self): + return type(self), (self.request,) + + +class PhoneHashExpiredError(BadRequestError): + def __init__(self, request): + self.request = request + super(Exception, self).__init__('An invalid or expired `phone_code_hash` was provided' + self._fmt_request(self.request)) + + def __reduce__(self): + return type(self), (self.request,) + + +class PhoneMigrateError(InvalidDCError): + def __init__(self, request, capture=0): + self.request = request + self.new_dc = int(capture) + super(Exception, self).__init__('The phone number a user is trying to use for authorization is associated with DC {new_dc}'.format(new_dc=self.new_dc) + self._fmt_request(self.request)) + + def __reduce__(self): + return type(self), (self.request, self.new_dc) + + +class PhoneNotOccupiedError(BadRequestError): + def __init__(self, request): + self.request = request + super(Exception, self).__init__('No user is associated to the specified phone number' + self._fmt_request(self.request)) + + def __reduce__(self): + return type(self), (self.request,) + + +class PhoneNumberAppSignupForbiddenError(BadRequestError): + def __init__(self, request): + self.request = request + super(Exception, self).__init__("You can't sign up using this app" + self._fmt_request(self.request)) + + def __reduce__(self): + return type(self), (self.request,) + + +class PhoneNumberBannedError(BadRequestError): + def __init__(self, request): + self.request = request + super(Exception, self).__init__('The used phone number has been banned from Telegram and cannot be used anymore. Maybe check https://www.telegram.org/faq_spam' + self._fmt_request(self.request)) + + def __reduce__(self): + return type(self), (self.request,) + + +class PhoneNumberFloodError(BadRequestError): + def __init__(self, request): + self.request = request + super(Exception, self).__init__('You asked for the code too many times.' + self._fmt_request(self.request)) + + def __reduce__(self): + return type(self), (self.request,) + + +class PhoneNumberInvalidError(BadRequestError): + def __init__(self, request): + self.request = request + super(Exception, self).__init__('The phone number is invalid' + self._fmt_request(self.request)) + + def __reduce__(self): + return type(self), (self.request,) + + +class PhoneNumberOccupiedError(BadRequestError): + def __init__(self, request): + self.request = request + super(Exception, self).__init__('The phone number is already in use' + self._fmt_request(self.request)) + + def __reduce__(self): + return type(self), (self.request,) + + +class PhoneNumberUnoccupiedError(BadRequestError): + def __init__(self, request): + self.request = request + super(Exception, self).__init__('The phone number is not yet being used' + self._fmt_request(self.request)) + + def __reduce__(self): + return type(self), (self.request,) + + +class PhonePasswordFloodError(AuthKeyError): + def __init__(self, request): + self.request = request + super(Exception, self).__init__('You have tried logging in too many times' + self._fmt_request(self.request)) + + def __reduce__(self): + return type(self), (self.request,) + + +class PhonePasswordProtectedError(BadRequestError): + def __init__(self, request): + self.request = request + super(Exception, self).__init__('This phone is password protected' + self._fmt_request(self.request)) + + def __reduce__(self): + return type(self), (self.request,) + + +class PhotoContentTypeInvalidError(BadRequestError): + def __init__(self, request): + self.request = request + super(Exception, self).__init__('Photo mime-type invalid' + self._fmt_request(self.request)) + + def __reduce__(self): + return type(self), (self.request,) + + +class PhotoContentUrlEmptyError(BadRequestError): + def __init__(self, request): + self.request = request + super(Exception, self).__init__('The content from the URL used as a photo appears to be empty or has caused another HTTP error' + self._fmt_request(self.request)) + + def __reduce__(self): + return type(self), (self.request,) + + +class PhotoCropFileMissingError(BadRequestError): + def __init__(self, request): + self.request = request + super(Exception, self).__init__('Photo crop file missing' + self._fmt_request(self.request)) + + def __reduce__(self): + return type(self), (self.request,) + + +class PhotoCropSizeSmallError(BadRequestError): + def __init__(self, request): + self.request = request + super(Exception, self).__init__('Photo is too small' + self._fmt_request(self.request)) + + def __reduce__(self): + return type(self), (self.request,) + + +class PhotoExtInvalidError(BadRequestError): + def __init__(self, request): + self.request = request + super(Exception, self).__init__('The extension of the photo is invalid' + self._fmt_request(self.request)) + + def __reduce__(self): + return type(self), (self.request,) + + +class PhotoFileMissingError(BadRequestError): + def __init__(self, request): + self.request = request + super(Exception, self).__init__('Profile photo file missing' + self._fmt_request(self.request)) + + def __reduce__(self): + return type(self), (self.request,) + + +class PhotoIdInvalidError(BadRequestError): + def __init__(self, request): + self.request = request + super(Exception, self).__init__('Photo id is invalid' + self._fmt_request(self.request)) + + def __reduce__(self): + return type(self), (self.request,) + + +class PhotoInvalidError(BadRequestError): + def __init__(self, request): + self.request = request + super(Exception, self).__init__('Photo invalid' + self._fmt_request(self.request)) + + def __reduce__(self): + return type(self), (self.request,) + + +class PhotoInvalidDimensionsError(BadRequestError): + def __init__(self, request): + self.request = request + super(Exception, self).__init__('The photo dimensions are invalid (hint: `pip install pillow` for `send_file` to resize images)' + self._fmt_request(self.request)) + + def __reduce__(self): + return type(self), (self.request,) + + +class PhotoSaveFileInvalidError(BadRequestError): + def __init__(self, request): + self.request = request + super(Exception, self).__init__('The photo you tried to send cannot be saved by Telegram. A reason may be that it exceeds 10MB. Try resizing it locally' + self._fmt_request(self.request)) + + def __reduce__(self): + return type(self), (self.request,) + + +class PhotoThumbUrlEmptyError(BadRequestError): + def __init__(self, request): + self.request = request + super(Exception, self).__init__('The URL used as a thumbnail appears to be empty or has caused another HTTP error' + self._fmt_request(self.request)) + + def __reduce__(self): + return type(self), (self.request,) + + +class PinnedDialogsTooMuchError(BadRequestError): + def __init__(self, request): + self.request = request + super(Exception, self).__init__('Too many pinned dialogs' + self._fmt_request(self.request)) + + def __reduce__(self): + return type(self), (self.request,) + + +class PinRestrictedError(BadRequestError): + def __init__(self, request): + self.request = request + super(Exception, self).__init__("You can't pin messages in private chats with other people" + self._fmt_request(self.request)) + + def __reduce__(self): + return type(self), (self.request,) + + +class PollAnswersInvalidError(BadRequestError): + def __init__(self, request): + self.request = request + super(Exception, self).__init__('The poll did not have enough answers or had too many' + self._fmt_request(self.request)) + + def __reduce__(self): + return type(self), (self.request,) + + +class PollAnswerInvalidError(BadRequestError): + def __init__(self, request): + self.request = request + super(Exception, self).__init__('One of the poll answers is not acceptable' + self._fmt_request(self.request)) + + def __reduce__(self): + return type(self), (self.request,) + + +class PollOptionDuplicateError(BadRequestError): + def __init__(self, request): + self.request = request + super(Exception, self).__init__('A duplicate option was sent in the same poll' + self._fmt_request(self.request)) + + def __reduce__(self): + return type(self), (self.request,) + + +class PollOptionInvalidError(BadRequestError): + def __init__(self, request): + self.request = request + super(Exception, self).__init__('A poll option used invalid data (the data may be too long)' + self._fmt_request(self.request)) + + def __reduce__(self): + return type(self), (self.request,) + + +class PollQuestionInvalidError(BadRequestError): + def __init__(self, request): + self.request = request + super(Exception, self).__init__('The poll question was either empty or too long' + self._fmt_request(self.request)) + + def __reduce__(self): + return type(self), (self.request,) + + +class PollUnsupportedError(BadRequestError): + def __init__(self, request): + self.request = request + super(Exception, self).__init__('This layer does not support polls in the issued method' + self._fmt_request(self.request)) + + def __reduce__(self): + return type(self), (self.request,) + + +class PollVoteRequiredError(ForbiddenError): + def __init__(self, request): + self.request = request + super(Exception, self).__init__('Cast a vote in the poll before calling this method' + self._fmt_request(self.request)) + + def __reduce__(self): + return type(self), (self.request,) + + +class PostponedTimeoutError(ServerError): + def __init__(self, request): + self.request = request + super(Exception, self).__init__('The postponed call has timed out' + self._fmt_request(self.request)) + + def __reduce__(self): + return type(self), (self.request,) + + +class PremiumAccountRequiredError(ForbiddenError): + def __init__(self, request): + self.request = request + super(Exception, self).__init__('A premium account is required to execute this action' + self._fmt_request(self.request)) + + def __reduce__(self): + return type(self), (self.request,) + + +class PremiumCurrentlyUnavailableError(AuthKeyError): + def __init__(self, request): + self.request = request + super(Exception, self).__init__('' + self._fmt_request(self.request)) + + def __reduce__(self): + return type(self), (self.request,) + + +class PreviousChatImportActiveWaitMinError(AuthKeyError): + def __init__(self, request, capture=0): + self.request = request + self.minutes = int(capture) + super(Exception, self).__init__('Similar to a flood wait, must wait {minutes} minutes'.format(minutes=self.minutes) + self._fmt_request(self.request)) + + def __reduce__(self): + return type(self), (self.request, self.minutes) + + +class PrivacyKeyInvalidError(BadRequestError): + def __init__(self, request): + self.request = request + super(Exception, self).__init__('The privacy key is invalid' + self._fmt_request(self.request)) + + def __reduce__(self): + return type(self), (self.request,) + + +class PrivacyTooLongError(BadRequestError): + def __init__(self, request): + self.request = request + super(Exception, self).__init__('Cannot add that many entities in a single request' + self._fmt_request(self.request)) + + def __reduce__(self): + return type(self), (self.request,) + + +class PrivacyValueInvalidError(BadRequestError): + def __init__(self, request): + self.request = request + super(Exception, self).__init__('The privacy value is invalid' + self._fmt_request(self.request)) + + def __reduce__(self): + return type(self), (self.request,) + + +class PtsChangeEmptyError(ServerError): + def __init__(self, request): + self.request = request + super(Exception, self).__init__('No PTS change' + self._fmt_request(self.request)) + + def __reduce__(self): + return type(self), (self.request,) + + +class PublicChannelMissingError(ForbiddenError): + def __init__(self, request): + self.request = request + super(Exception, self).__init__('You can only export group call invite links for public chats or channels' + self._fmt_request(self.request)) + + def __reduce__(self): + return type(self), (self.request,) + + +class PublicKeyRequiredError(BadRequestError): + def __init__(self, request): + self.request = request + super(Exception, self).__init__('A public key is required' + self._fmt_request(self.request)) + + def __reduce__(self): + return type(self), (self.request,) + + +class QueryIdEmptyError(BadRequestError): + def __init__(self, request): + self.request = request + super(Exception, self).__init__('The query ID is empty' + self._fmt_request(self.request)) + + def __reduce__(self): + return type(self), (self.request,) + + +class QueryIdInvalidError(BadRequestError): + def __init__(self, request): + self.request = request + super(Exception, self).__init__('The query ID is invalid' + self._fmt_request(self.request)) + + def __reduce__(self): + return type(self), (self.request,) + + +class QueryTooShortError(BadRequestError): + def __init__(self, request): + self.request = request + super(Exception, self).__init__('The query string is too short' + self._fmt_request(self.request)) + + def __reduce__(self): + return type(self), (self.request,) + + +class QuizAnswerMissingError(BadRequestError): + def __init__(self, request): + self.request = request + super(Exception, self).__init__('You can forward a quiz while hiding the original author only after choosing an option in the quiz' + self._fmt_request(self.request)) + + def __reduce__(self): + return type(self), (self.request,) + + +class QuizCorrectAnswersEmptyError(BadRequestError): + def __init__(self, request): + self.request = request + super(Exception, self).__init__('A quiz must specify one correct answer' + self._fmt_request(self.request)) + + def __reduce__(self): + return type(self), (self.request,) + + +class QuizCorrectAnswersTooMuchError(BadRequestError): + def __init__(self, request): + self.request = request + super(Exception, self).__init__('There can only be one correct answer' + self._fmt_request(self.request)) + + def __reduce__(self): + return type(self), (self.request,) + + +class QuizCorrectAnswerInvalidError(BadRequestError): + def __init__(self, request): + self.request = request + super(Exception, self).__init__('The correct answer is not an existing answer' + self._fmt_request(self.request)) + + def __reduce__(self): + return type(self), (self.request,) + + +class QuizMultipleInvalidError(BadRequestError): + def __init__(self, request): + self.request = request + super(Exception, self).__init__('A poll cannot be both multiple choice and quiz' + self._fmt_request(self.request)) + + def __reduce__(self): + return type(self), (self.request,) + + +class RandomIdDuplicateError(ServerError): + def __init__(self, request): + self.request = request + super(Exception, self).__init__('You provided a random ID that was already used' + self._fmt_request(self.request)) + + def __reduce__(self): + return type(self), (self.request,) + + +class RandomIdEmptyError(BadRequestError): + def __init__(self, request): + self.request = request + super(Exception, self).__init__('Random ID empty' + self._fmt_request(self.request)) + + def __reduce__(self): + return type(self), (self.request,) + + +class RandomIdInvalidError(BadRequestError): + def __init__(self, request): + self.request = request + super(Exception, self).__init__('A provided random ID is invalid' + self._fmt_request(self.request)) + + def __reduce__(self): + return type(self), (self.request,) + + +class RandomLengthInvalidError(BadRequestError): + def __init__(self, request): + self.request = request + super(Exception, self).__init__('Random length invalid' + self._fmt_request(self.request)) + + def __reduce__(self): + return type(self), (self.request,) + + +class RangesInvalidError(BadRequestError): + def __init__(self, request): + self.request = request + super(Exception, self).__init__('Invalid range provided' + self._fmt_request(self.request)) + + def __reduce__(self): + return type(self), (self.request,) + + +class ReactionsTooManyError(BadRequestError): + def __init__(self, request): + self.request = request + super(Exception, self).__init__("The message already has exactly `reactions_uniq_max` reaction emojis, you can't react with a new emoji, see [the docs for more info](/api/config#client-configuration)" + self._fmt_request(self.request)) + + def __reduce__(self): + return type(self), (self.request,) + + +class ReactionEmptyError(BadRequestError): + def __init__(self, request): + self.request = request + super(Exception, self).__init__('No reaction provided' + self._fmt_request(self.request)) + + def __reduce__(self): + return type(self), (self.request,) + + +class ReactionInvalidError(BadRequestError): + def __init__(self, request): + self.request = request + super(Exception, self).__init__('Invalid reaction provided (only emoji are allowed)' + self._fmt_request(self.request)) + + def __reduce__(self): + return type(self), (self.request,) + + +class ReflectorNotAvailableError(BadRequestError): + def __init__(self, request): + self.request = request + super(Exception, self).__init__('Invalid call reflector server' + self._fmt_request(self.request)) + + def __reduce__(self): + return type(self), (self.request,) + + +class RegIdGenerateFailedError(ServerError): + def __init__(self, request): + self.request = request + super(Exception, self).__init__('Failure while generating registration ID' + self._fmt_request(self.request)) + + def __reduce__(self): + return type(self), (self.request,) + + +class ReplyMarkupBuyEmptyError(BadRequestError): + def __init__(self, request): + self.request = request + super(Exception, self).__init__('Reply markup for buy button empty' + self._fmt_request(self.request)) + + def __reduce__(self): + return type(self), (self.request,) + + +class ReplyMarkupGameEmptyError(BadRequestError): + def __init__(self, request): + self.request = request + super(Exception, self).__init__('The provided reply markup for the game is empty' + self._fmt_request(self.request)) + + def __reduce__(self): + return type(self), (self.request,) + + +class ReplyMarkupInvalidError(BadRequestError): + def __init__(self, request): + self.request = request + super(Exception, self).__init__('The provided reply markup is invalid' + self._fmt_request(self.request)) + + def __reduce__(self): + return type(self), (self.request,) + + +class ReplyMarkupTooLongError(BadRequestError): + def __init__(self, request): + self.request = request + super(Exception, self).__init__('The data embedded in the reply markup buttons was too much' + self._fmt_request(self.request)) + + def __reduce__(self): + return type(self), (self.request,) + + +class ResetRequestMissingError(BadRequestError): + def __init__(self, request): + self.request = request + super(Exception, self).__init__('No password reset is in progress' + self._fmt_request(self.request)) + + def __reduce__(self): + return type(self), (self.request,) + + +class ResultsTooMuchError(BadRequestError): + def __init__(self, request): + self.request = request + super(Exception, self).__init__('You sent too many results, see https://core.telegram.org/bots/api#answerinlinequery for the current limit' + self._fmt_request(self.request)) + + def __reduce__(self): + return type(self), (self.request,) + + +class ResultIdDuplicateError(BadRequestError): + def __init__(self, request): + self.request = request + super(Exception, self).__init__('Duplicated IDs on the sent results. Make sure to use unique IDs' + self._fmt_request(self.request)) + + def __reduce__(self): + return type(self), (self.request,) + + +class ResultIdEmptyError(BadRequestError): + def __init__(self, request): + self.request = request + super(Exception, self).__init__('Result ID empty' + self._fmt_request(self.request)) + + def __reduce__(self): + return type(self), (self.request,) + + +class ResultIdInvalidError(BadRequestError): + def __init__(self, request): + self.request = request + super(Exception, self).__init__('The given result cannot be used to send the selection to the bot' + self._fmt_request(self.request)) + + def __reduce__(self): + return type(self), (self.request,) + + +class ResultTypeInvalidError(BadRequestError): + def __init__(self, request): + self.request = request + super(Exception, self).__init__('Result type invalid' + self._fmt_request(self.request)) + + def __reduce__(self): + return type(self), (self.request,) + + +class RevoteNotAllowedError(BadRequestError): + def __init__(self, request): + self.request = request + super(Exception, self).__init__('You cannot change your vote' + self._fmt_request(self.request)) + + def __reduce__(self): + return type(self), (self.request,) + + +class RightsNotModifiedError(BadRequestError): + def __init__(self, request): + self.request = request + super(Exception, self).__init__('The new admin rights are equal to the old rights, no change was made' + self._fmt_request(self.request)) + + def __reduce__(self): + return type(self), (self.request,) + + +class RightForbiddenError(ForbiddenError): + def __init__(self, request): + self.request = request + super(Exception, self).__init__('Either your admin rights do not allow you to do this or you passed the wrong rights combination (some rights only apply to channels and vice versa)' + self._fmt_request(self.request)) + + def __reduce__(self): + return type(self), (self.request,) + + +class RpcCallFailError(ServerError): + def __init__(self, request): + self.request = request + super(Exception, self).__init__('Telegram is having internal issues, please try again later.' + self._fmt_request(self.request)) + + def __reduce__(self): + return type(self), (self.request,) + + +class RpcMcgetFailError(ServerError): + def __init__(self, request): + self.request = request + super(Exception, self).__init__('Telegram is having internal issues, please try again later.' + self._fmt_request(self.request)) + + def __reduce__(self): + return type(self), (self.request,) + + +class RsaDecryptFailedError(BadRequestError): + def __init__(self, request): + self.request = request + super(Exception, self).__init__('Internal RSA decryption failed' + self._fmt_request(self.request)) + + def __reduce__(self): + return type(self), (self.request,) + + +class ScheduleBotNotAllowedError(BadRequestError): + def __init__(self, request): + self.request = request + super(Exception, self).__init__('Bots are not allowed to schedule messages' + self._fmt_request(self.request)) + + def __reduce__(self): + return type(self), (self.request,) + + +class ScheduleDateInvalidError(BadRequestError): + def __init__(self, request): + self.request = request + super(Exception, self).__init__('Invalid schedule date provided' + self._fmt_request(self.request)) + + def __reduce__(self): + return type(self), (self.request,) + + +class ScheduleDateTooLateError(BadRequestError): + def __init__(self, request): + self.request = request + super(Exception, self).__init__('The date you tried to schedule is too far in the future (last known limit of 1 year and a few hours)' + self._fmt_request(self.request)) + + def __reduce__(self): + return type(self), (self.request,) + + +class ScheduleStatusPrivateError(BadRequestError): + def __init__(self, request): + self.request = request + super(Exception, self).__init__('You cannot schedule a message until the person comes online if their privacy does not show this information' + self._fmt_request(self.request)) + + def __reduce__(self): + return type(self), (self.request,) + + +class ScheduleTooMuchError(BadRequestError): + def __init__(self, request): + self.request = request + super(Exception, self).__init__('You cannot schedule more messages in this chat (last known limit of 100 per chat)' + self._fmt_request(self.request)) + + def __reduce__(self): + return type(self), (self.request,) + + +class ScoreInvalidError(BadRequestError): + def __init__(self, request): + self.request = request + super(Exception, self).__init__('The specified game score is invalid' + self._fmt_request(self.request)) + + def __reduce__(self): + return type(self), (self.request,) + + +class SearchQueryEmptyError(BadRequestError): + def __init__(self, request): + self.request = request + super(Exception, self).__init__('The search query is empty' + self._fmt_request(self.request)) + + def __reduce__(self): + return type(self), (self.request,) + + +class SearchWithLinkNotSupportedError(BadRequestError): + def __init__(self, request): + self.request = request + super(Exception, self).__init__('You cannot provide a search query and an invite link at the same time' + self._fmt_request(self.request)) + + def __reduce__(self): + return type(self), (self.request,) + + +class SecondsInvalidError(BadRequestError): + def __init__(self, request): + self.request = request + super(Exception, self).__init__('Slow mode only supports certain values (e.g. 0, 10s, 30s, 1m, 5m, 15m and 1h)' + self._fmt_request(self.request)) + + def __reduce__(self): + return type(self), (self.request,) + + +class SendAsPeerInvalidError(BadRequestError): + def __init__(self, request): + self.request = request + super(Exception, self).__init__("You can't send messages as the specified peer" + self._fmt_request(self.request)) + + def __reduce__(self): + return type(self), (self.request,) + + +class SendCodeUnavailableError(AuthKeyError): + def __init__(self, request): + self.request = request + super(Exception, self).__init__('Returned when all available options for this type of number were already used (e.g. flash-call, then SMS, then this error might be returned to trigger a second resend)' + self._fmt_request(self.request)) + + def __reduce__(self): + return type(self), (self.request,) + + +class SendMessageMediaInvalidError(BadRequestError): + def __init__(self, request): + self.request = request + super(Exception, self).__init__('The message media was invalid or not specified' + self._fmt_request(self.request)) + + def __reduce__(self): + return type(self), (self.request,) + + +class SendMessageTypeInvalidError(BadRequestError): + def __init__(self, request): + self.request = request + super(Exception, self).__init__('The message type is invalid' + self._fmt_request(self.request)) + + def __reduce__(self): + return type(self), (self.request,) + + +class SensitiveChangeForbiddenError(ForbiddenError): + def __init__(self, request): + self.request = request + super(Exception, self).__init__('Your sensitive content settings cannot be changed at this time' + self._fmt_request(self.request)) + + def __reduce__(self): + return type(self), (self.request,) + + +class SessionExpiredError(UnauthorizedError): + def __init__(self, request): + self.request = request + super(Exception, self).__init__('The authorization has expired' + self._fmt_request(self.request)) + + def __reduce__(self): + return type(self), (self.request,) + + +class SessionPasswordNeededError(UnauthorizedError): + def __init__(self, request): + self.request = request + super(Exception, self).__init__('Two-steps verification is enabled and a password is required' + self._fmt_request(self.request)) + + def __reduce__(self): + return type(self), (self.request,) + + +class SessionRevokedError(UnauthorizedError): + def __init__(self, request): + self.request = request + super(Exception, self).__init__('The authorization has been invalidated, because of the user terminating all sessions' + self._fmt_request(self.request)) + + def __reduce__(self): + return type(self), (self.request,) + + +class SessionTooFreshError(BadRequestError): + def __init__(self, request, capture=0): + self.request = request + self.seconds = int(capture) + super(Exception, self).__init__('The session logged in too recently and {seconds} seconds must pass before calling the method'.format(seconds=self.seconds) + self._fmt_request(self.request)) + + def __reduce__(self): + return type(self), (self.request, self.seconds) + + +class SettingsInvalidError(BadRequestError): + def __init__(self, request): + self.request = request + super(Exception, self).__init__('Invalid settings were provided' + self._fmt_request(self.request)) + + def __reduce__(self): + return type(self), (self.request,) + + +class Sha256HashInvalidError(BadRequestError): + def __init__(self, request): + self.request = request + super(Exception, self).__init__('The provided SHA256 hash is invalid' + self._fmt_request(self.request)) + + def __reduce__(self): + return type(self), (self.request,) + + +class ShortnameOccupyFailedError(BadRequestError): + def __init__(self, request): + self.request = request + super(Exception, self).__init__('An error occurred when trying to register the short-name used for the sticker pack. Try a different name' + self._fmt_request(self.request)) + + def __reduce__(self): + return type(self), (self.request,) + + +class ShortNameInvalidError(BadRequestError): + def __init__(self, request): + self.request = request + super(Exception, self).__init__('The specified short name is invalid' + self._fmt_request(self.request)) + + def __reduce__(self): + return type(self), (self.request,) + + +class ShortNameOccupiedError(BadRequestError): + def __init__(self, request): + self.request = request + super(Exception, self).__init__('The specified short name is already in use' + self._fmt_request(self.request)) + + def __reduce__(self): + return type(self), (self.request,) + + +class SignInFailedError(ServerError): + def __init__(self, request): + self.request = request + super(Exception, self).__init__('Failure while signing in' + self._fmt_request(self.request)) + + def __reduce__(self): + return type(self), (self.request,) + + +class SlowModeMultiMsgsDisabledError(BadRequestError): + def __init__(self, request): + self.request = request + super(Exception, self).__init__('Slowmode is enabled, you cannot forward multiple messages to this group' + self._fmt_request(self.request)) + + def __reduce__(self): + return type(self), (self.request,) + + +class SlowModeWaitError(FloodError): + def __init__(self, request, capture=0): + self.request = request + self.seconds = int(capture) + super(Exception, self).__init__('A wait of {seconds} seconds is required before sending another message in this chat'.format(seconds=self.seconds) + self._fmt_request(self.request)) + + def __reduce__(self): + return type(self), (self.request, self.seconds) + + +class SmsCodeCreateFailedError(BadRequestError): + def __init__(self, request): + self.request = request + super(Exception, self).__init__('An error occurred while creating the SMS code' + self._fmt_request(self.request)) + + def __reduce__(self): + return type(self), (self.request,) + + +class SrpIdInvalidError(BadRequestError): + def __init__(self, request): + self.request = request + super(Exception, self).__init__('Invalid SRP ID provided' + self._fmt_request(self.request)) + + def __reduce__(self): + return type(self), (self.request,) + + +class SrpPasswordChangedError(BadRequestError): + def __init__(self, request): + self.request = request + super(Exception, self).__init__('Password has changed' + self._fmt_request(self.request)) + + def __reduce__(self): + return type(self), (self.request,) + + +class StartParamEmptyError(BadRequestError): + def __init__(self, request): + self.request = request + super(Exception, self).__init__('The start parameter is empty' + self._fmt_request(self.request)) + + def __reduce__(self): + return type(self), (self.request,) + + +class StartParamInvalidError(BadRequestError): + def __init__(self, request): + self.request = request + super(Exception, self).__init__('Start parameter invalid' + self._fmt_request(self.request)) + + def __reduce__(self): + return type(self), (self.request,) + + +class StartParamTooLongError(BadRequestError): + def __init__(self, request): + self.request = request + super(Exception, self).__init__('Start parameter is too long' + self._fmt_request(self.request)) + + def __reduce__(self): + return type(self), (self.request,) + + +class StatsMigrateError(InvalidDCError): + def __init__(self, request, capture=0): + self.request = request + self.dc = int(capture) + super(Exception, self).__init__('The channel statistics must be fetched from DC {dc}'.format(dc=self.dc) + self._fmt_request(self.request)) + + def __reduce__(self): + return type(self), (self.request, self.dc) + + +class StickerpackStickersTooMuchError(BadRequestError): + def __init__(self, request): + self.request = request + super(Exception, self).__init__("There are too many stickers in this stickerpack, you can't add any more" + self._fmt_request(self.request)) + + def __reduce__(self): + return type(self), (self.request,) + + +class StickersetInvalidError(BadRequestError): + def __init__(self, request): + self.request = request + super(Exception, self).__init__('The provided sticker set is invalid' + self._fmt_request(self.request)) + + def __reduce__(self): + return type(self), (self.request,) + + +class StickersetOwnerAnonymousError(AuthKeyError): + def __init__(self, request): + self.request = request + super(Exception, self).__init__("This sticker set can't be used as the group's official stickers because it was created by one of its anonymous admins" + self._fmt_request(self.request)) + + def __reduce__(self): + return type(self), (self.request,) + + +class StickersEmptyError(BadRequestError): + def __init__(self, request): + self.request = request + super(Exception, self).__init__('No sticker provided' + self._fmt_request(self.request)) + + def __reduce__(self): + return type(self), (self.request,) + + +class StickersTooMuchError(BadRequestError): + def __init__(self, request): + self.request = request + super(Exception, self).__init__("There are too many stickers in this stickerpack, you can't add any more" + self._fmt_request(self.request)) + + def __reduce__(self): + return type(self), (self.request,) + + +class StickerDocumentInvalidError(BadRequestError): + def __init__(self, request): + self.request = request + super(Exception, self).__init__('The sticker file was invalid (this file has failed Telegram internal checks, make sure to use the correct format and comply with https://core.telegram.org/animated_stickers)' + self._fmt_request(self.request)) + + def __reduce__(self): + return type(self), (self.request,) + + +class StickerEmojiInvalidError(BadRequestError): + def __init__(self, request): + self.request = request + super(Exception, self).__init__('Sticker emoji invalid' + self._fmt_request(self.request)) + + def __reduce__(self): + return type(self), (self.request,) + + +class StickerFileInvalidError(BadRequestError): + def __init__(self, request): + self.request = request + super(Exception, self).__init__('Sticker file invalid' + self._fmt_request(self.request)) + + def __reduce__(self): + return type(self), (self.request,) + + +class StickerGifDimensionsError(BadRequestError): + def __init__(self, request): + self.request = request + super(Exception, self).__init__('The specified video sticker has invalid dimensions' + self._fmt_request(self.request)) + + def __reduce__(self): + return type(self), (self.request,) + + +class StickerIdInvalidError(BadRequestError): + def __init__(self, request): + self.request = request + super(Exception, self).__init__('The provided sticker ID is invalid' + self._fmt_request(self.request)) + + def __reduce__(self): + return type(self), (self.request,) + + +class StickerInvalidError(BadRequestError): + def __init__(self, request): + self.request = request + super(Exception, self).__init__('The provided sticker is invalid' + self._fmt_request(self.request)) + + def __reduce__(self): + return type(self), (self.request,) + + +class StickerMimeInvalidError(BadRequestError): + def __init__(self, request): + self.request = request + super(Exception, self).__init__('Make sure to pass a valid image file for the right InputFile parameter' + self._fmt_request(self.request)) + + def __reduce__(self): + return type(self), (self.request,) + + +class StickerPngDimensionsError(BadRequestError): + def __init__(self, request): + self.request = request + super(Exception, self).__init__('Sticker png dimensions invalid' + self._fmt_request(self.request)) + + def __reduce__(self): + return type(self), (self.request,) + + +class StickerPngNopngError(BadRequestError): + def __init__(self, request): + self.request = request + super(Exception, self).__init__('Stickers must be a png file but the used image was not a png' + self._fmt_request(self.request)) + + def __reduce__(self): + return type(self), (self.request,) + + +class StickerTgsNodocError(BadRequestError): + def __init__(self, request): + self.request = request + super(Exception, self).__init__('You must send the animated sticker as a document' + self._fmt_request(self.request)) + + def __reduce__(self): + return type(self), (self.request,) + + +class StickerTgsNotgsError(BadRequestError): + def __init__(self, request): + self.request = request + super(Exception, self).__init__('Stickers must be a tgs file but the used file was not a tgs' + self._fmt_request(self.request)) + + def __reduce__(self): + return type(self), (self.request,) + + +class StickerThumbPngNopngError(BadRequestError): + def __init__(self, request): + self.request = request + super(Exception, self).__init__('Stickerset thumb must be a png file but the used file was not png' + self._fmt_request(self.request)) + + def __reduce__(self): + return type(self), (self.request,) + + +class StickerThumbTgsNotgsError(BadRequestError): + def __init__(self, request): + self.request = request + super(Exception, self).__init__('Stickerset thumb must be a tgs file but the used file was not tgs' + self._fmt_request(self.request)) + + def __reduce__(self): + return type(self), (self.request,) + + +class StickerVideoBigError(BadRequestError): + def __init__(self, request): + self.request = request + super(Exception, self).__init__('The specified video sticker is too big' + self._fmt_request(self.request)) + + def __reduce__(self): + return type(self), (self.request,) + + +class StickerVideoNodocError(BadRequestError): + def __init__(self, request): + self.request = request + super(Exception, self).__init__('You must send the video sticker as a document' + self._fmt_request(self.request)) + + def __reduce__(self): + return type(self), (self.request,) + + +class StickerVideoNowebmError(BadRequestError): + def __init__(self, request): + self.request = request + super(Exception, self).__init__('The specified video sticker is not in webm format' + self._fmt_request(self.request)) + + def __reduce__(self): + return type(self), (self.request,) + + +class StorageCheckFailedError(ServerError): + def __init__(self, request): + self.request = request + super(Exception, self).__init__('Server storage check failed' + self._fmt_request(self.request)) + + def __reduce__(self): + return type(self), (self.request,) + + +class StoreInvalidScalarTypeError(ServerError): + def __init__(self, request): + self.request = request + super(Exception, self).__init__('' + self._fmt_request(self.request)) + + def __reduce__(self): + return type(self), (self.request,) + + +class SwitchPmTextEmptyError(BadRequestError): + def __init__(self, request): + self.request = request + super(Exception, self).__init__('The switch_pm.text field was empty' + self._fmt_request(self.request)) + + def __reduce__(self): + return type(self), (self.request,) + + +class TakeoutInitDelayError(FloodError): + def __init__(self, request, capture=0): + self.request = request + self.seconds = int(capture) + super(Exception, self).__init__('A wait of {seconds} seconds is required before being able to initiate the takeout'.format(seconds=self.seconds) + self._fmt_request(self.request)) + + def __reduce__(self): + return type(self), (self.request, self.seconds) + + +class TakeoutInvalidError(BadRequestError): + def __init__(self, request): + self.request = request + super(Exception, self).__init__('The takeout session has been invalidated by another data export session' + self._fmt_request(self.request)) + + def __reduce__(self): + return type(self), (self.request,) + + +class TakeoutRequiredError(BadRequestError): + def __init__(self, request): + self.request = request + super(Exception, self).__init__('You must initialize a takeout request first' + self._fmt_request(self.request)) + + def __reduce__(self): + return type(self), (self.request,) + + +class TempAuthKeyAlreadyBoundError(BadRequestError): + def __init__(self, request): + self.request = request + super(Exception, self).__init__('The passed temporary key is already bound to another **perm_auth_key_id**' + self._fmt_request(self.request)) + + def __reduce__(self): + return type(self), (self.request,) + + +class TempAuthKeyEmptyError(BadRequestError): + def __init__(self, request): + self.request = request + super(Exception, self).__init__('No temporary auth key provided' + self._fmt_request(self.request)) + + def __reduce__(self): + return type(self), (self.request,) + + +class ThemeFileInvalidError(BadRequestError): + def __init__(self, request): + self.request = request + super(Exception, self).__init__('Invalid theme file provided' + self._fmt_request(self.request)) + + def __reduce__(self): + return type(self), (self.request,) + + +class ThemeFormatInvalidError(BadRequestError): + def __init__(self, request): + self.request = request + super(Exception, self).__init__('Invalid theme format provided' + self._fmt_request(self.request)) + + def __reduce__(self): + return type(self), (self.request,) + + +class ThemeInvalidError(BadRequestError): + def __init__(self, request): + self.request = request + super(Exception, self).__init__('Theme invalid' + self._fmt_request(self.request)) + + def __reduce__(self): + return type(self), (self.request,) + + +class ThemeMimeInvalidError(BadRequestError): + def __init__(self, request): + self.request = request + super(Exception, self).__init__('You cannot create this theme, the mime-type is invalid' + self._fmt_request(self.request)) + + def __reduce__(self): + return type(self), (self.request,) + + +class ThemeTitleInvalidError(BadRequestError): + def __init__(self, request): + self.request = request + super(Exception, self).__init__('The specified theme title is invalid' + self._fmt_request(self.request)) + + def __reduce__(self): + return type(self), (self.request,) + + +class TimeoutError(ServerError): + def __init__(self, request): + self.request = request + super(Exception, self).__init__('A timeout occurred while fetching data from the worker' + self._fmt_request(self.request)) + + def __reduce__(self): + return type(self), (self.request,) + + +class TitleInvalidError(BadRequestError): + def __init__(self, request): + self.request = request + super(Exception, self).__init__('The specified stickerpack title is invalid' + self._fmt_request(self.request)) + + def __reduce__(self): + return type(self), (self.request,) + + +class TmpPasswordDisabledError(BadRequestError): + def __init__(self, request): + self.request = request + super(Exception, self).__init__('The temporary password is disabled' + self._fmt_request(self.request)) + + def __reduce__(self): + return type(self), (self.request,) + + +class TmpPasswordInvalidError(BadRequestError): + def __init__(self, request): + self.request = request + super(Exception, self).__init__('Password auth needs to be regenerated' + self._fmt_request(self.request)) + + def __reduce__(self): + return type(self), (self.request,) + + +class TokenInvalidError(BadRequestError): + def __init__(self, request): + self.request = request + super(Exception, self).__init__('The provided token is invalid' + self._fmt_request(self.request)) + + def __reduce__(self): + return type(self), (self.request,) + + +class TopicDeletedError(BadRequestError): + def __init__(self, request): + self.request = request + super(Exception, self).__init__('The topic was deleted' + self._fmt_request(self.request)) + + def __reduce__(self): + return type(self), (self.request,) + + +class ToLangInvalidError(BadRequestError): + def __init__(self, request): + self.request = request + super(Exception, self).__init__('The specified destination language is invalid' + self._fmt_request(self.request)) + + def __reduce__(self): + return type(self), (self.request,) + + +class TtlDaysInvalidError(BadRequestError): + def __init__(self, request): + self.request = request + super(Exception, self).__init__('The provided TTL is invalid' + self._fmt_request(self.request)) + + def __reduce__(self): + return type(self), (self.request,) + + +class TtlMediaInvalidError(BadRequestError): + def __init__(self, request): + self.request = request + super(Exception, self).__init__('The provided media cannot be used with a TTL' + self._fmt_request(self.request)) + + def __reduce__(self): + return type(self), (self.request,) + + +class TtlPeriodInvalidError(BadRequestError): + def __init__(self, request): + self.request = request + super(Exception, self).__init__('The provided TTL Period is invalid' + self._fmt_request(self.request)) + + def __reduce__(self): + return type(self), (self.request,) + + +class TypesEmptyError(BadRequestError): + def __init__(self, request): + self.request = request + super(Exception, self).__init__('The types field is empty' + self._fmt_request(self.request)) + + def __reduce__(self): + return type(self), (self.request,) + + +class TypeConstructorInvalidError(BadRequestError): + def __init__(self, request): + self.request = request + super(Exception, self).__init__('The type constructor is invalid' + self._fmt_request(self.request)) + + def __reduce__(self): + return type(self), (self.request,) + + +class TimedoutError(TimedOutError): + def __init__(self, request): + self.request = request + super(Exception, self).__init__('Timeout while fetching data' + self._fmt_request(self.request)) + + def __reduce__(self): + return type(self), (self.request,) + + +class TimeoutError(TimedOutError): + def __init__(self, request): + self.request = request + super(Exception, self).__init__('Timeout while fetching data' + self._fmt_request(self.request)) + + def __reduce__(self): + return type(self), (self.request,) + + +class UnknownErrorError(BadRequestError): + def __init__(self, request): + self.request = request + super(Exception, self).__init__('' + self._fmt_request(self.request)) + + def __reduce__(self): + return type(self), (self.request,) + + +class UnknownMethodError(ServerError): + def __init__(self, request): + self.request = request + super(Exception, self).__init__('The method you tried to call cannot be called on non-CDN DCs' + self._fmt_request(self.request)) + + def __reduce__(self): + return type(self), (self.request,) + + +class UntilDateInvalidError(BadRequestError): + def __init__(self, request): + self.request = request + super(Exception, self).__init__('That date cannot be specified in this request (try using None)' + self._fmt_request(self.request)) + + def __reduce__(self): + return type(self), (self.request,) + + +class UpdateAppToLoginError(AuthKeyError): + def __init__(self, request): + self.request = request + super(Exception, self).__init__('' + self._fmt_request(self.request)) + + def __reduce__(self): + return type(self), (self.request,) + + +class UrlInvalidError(BadRequestError): + def __init__(self, request): + self.request = request + super(Exception, self).__init__("The URL used was invalid (e.g. when answering a callback with a URL that's not t.me/yourbot or your game's URL)" + self._fmt_request(self.request)) + + def __reduce__(self): + return type(self), (self.request,) + + +class UsageLimitInvalidError(BadRequestError): + def __init__(self, request): + self.request = request + super(Exception, self).__init__('The specified usage limit is invalid' + self._fmt_request(self.request)) + + def __reduce__(self): + return type(self), (self.request,) + + +class UsernameInvalidError(BadRequestError): + def __init__(self, request): + self.request = request + super(Exception, self).__init__('Nobody is using this username or the username is unacceptable' + self._fmt_request(self.request)) + + def __reduce__(self): + return type(self), (self.request,) + + +class UsernameNotModifiedError(BadRequestError): + def __init__(self, request): + self.request = request + super(Exception, self).__init__('The username is not different from the current username' + self._fmt_request(self.request)) + + def __reduce__(self): + return type(self), (self.request,) + + +class UsernameNotOccupiedError(BadRequestError): + def __init__(self, request): + self.request = request + super(Exception, self).__init__('The username is not in use by anyone else yet' + self._fmt_request(self.request)) + + def __reduce__(self): + return type(self), (self.request,) + + +class UsernameOccupiedError(BadRequestError): + def __init__(self, request): + self.request = request + super(Exception, self).__init__('The username is already taken' + self._fmt_request(self.request)) + + def __reduce__(self): + return type(self), (self.request,) + + +class UsernamePurchaseAvailableError(BadRequestError): + def __init__(self, request): + self.request = request + super(Exception, self).__init__('' + self._fmt_request(self.request)) + + def __reduce__(self): + return type(self), (self.request,) + + +class UserpicPrivacyRequiredError(AuthKeyError): + def __init__(self, request): + self.request = request + super(Exception, self).__init__('You need to disable privacy settings for your profile picture in order to make your geolocation public' + self._fmt_request(self.request)) + + def __reduce__(self): + return type(self), (self.request,) + + +class UserpicUploadRequiredError(BadRequestError): + def __init__(self, request): + self.request = request + super(Exception, self).__init__('You must have a profile picture before using this method' + self._fmt_request(self.request)) + + def __reduce__(self): + return type(self), (self.request,) + + +class UsersTooFewError(BadRequestError): + def __init__(self, request): + self.request = request + super(Exception, self).__init__('Not enough users (to create a chat, for example)' + self._fmt_request(self.request)) + + def __reduce__(self): + return type(self), (self.request,) + + +class UsersTooMuchError(BadRequestError): + def __init__(self, request): + self.request = request + super(Exception, self).__init__('The maximum number of users has been exceeded (to create a chat, for example)' + self._fmt_request(self.request)) + + def __reduce__(self): + return type(self), (self.request,) + + +class UserAdminInvalidError(BadRequestError): + def __init__(self, request): + self.request = request + super(Exception, self).__init__("Either you're not an admin or you tried to ban an admin that you didn't promote" + self._fmt_request(self.request)) + + def __reduce__(self): + return type(self), (self.request,) + + +class UserAlreadyInvitedError(BadRequestError): + def __init__(self, request): + self.request = request + super(Exception, self).__init__('You have already invited this user' + self._fmt_request(self.request)) + + def __reduce__(self): + return type(self), (self.request,) + + +class UserAlreadyParticipantError(BadRequestError): + def __init__(self, request): + self.request = request + super(Exception, self).__init__('The authenticated user is already a participant of the chat' + self._fmt_request(self.request)) + + def __reduce__(self): + return type(self), (self.request,) + + +class UserBannedInChannelError(BadRequestError): + def __init__(self, request): + self.request = request + super(Exception, self).__init__("You're banned from sending messages in supergroups/channels" + self._fmt_request(self.request)) + + def __reduce__(self): + return type(self), (self.request,) + + +class UserBlockedError(BadRequestError): + def __init__(self, request): + self.request = request + super(Exception, self).__init__('User blocked' + self._fmt_request(self.request)) + + def __reduce__(self): + return type(self), (self.request,) + + +class UserBotError(BadRequestError): + def __init__(self, request): + self.request = request + super(Exception, self).__init__('Bots can only be admins in channels.' + self._fmt_request(self.request)) + + def __reduce__(self): + return type(self), (self.request,) + + +class UserBotInvalidError(BadRequestError): + def __init__(self, request): + self.request = request + super(Exception, self).__init__('This method can only be called by a bot' + self._fmt_request(self.request)) + + def __reduce__(self): + return type(self), (self.request,) + + +class UserBotRequiredError(BadRequestError): + def __init__(self, request): + self.request = request + super(Exception, self).__init__('This method can only be called by a bot' + self._fmt_request(self.request)) + + def __reduce__(self): + return type(self), (self.request,) + + +class UserChannelsTooMuchError(BadRequestError): + def __init__(self, request): + self.request = request + super(Exception, self).__init__('One of the users you tried to add is already in too many channels/supergroups' + self._fmt_request(self.request)) + + def __reduce__(self): + return type(self), (self.request,) + + +class UserCreatorError(BadRequestError): + def __init__(self, request): + self.request = request + super(Exception, self).__init__("You can't leave this channel, because you're its creator" + self._fmt_request(self.request)) + + def __reduce__(self): + return type(self), (self.request,) + + +class UserDeactivatedError(UnauthorizedError): + def __init__(self, request): + self.request = request + super(Exception, self).__init__('The user has been deleted/deactivated' + self._fmt_request(self.request)) + + def __reduce__(self): + return type(self), (self.request,) + + +class UserDeactivatedBanError(UnauthorizedError): + def __init__(self, request): + self.request = request + super(Exception, self).__init__('The user has been deleted/deactivated' + self._fmt_request(self.request)) + + def __reduce__(self): + return type(self), (self.request,) + + +class UserDeletedError(ForbiddenError): + def __init__(self, request): + self.request = request + super(Exception, self).__init__("You can't send this secret message because the other participant deleted their account" + self._fmt_request(self.request)) + + def __reduce__(self): + return type(self), (self.request,) + + +class UserIdInvalidError(BadRequestError): + def __init__(self, request): + self.request = request + super(Exception, self).__init__('Invalid object ID for a user. Make sure to pass the right types, for instance making sure that the request is designed for users or otherwise look for a different one more suited' + self._fmt_request(self.request)) + + def __reduce__(self): + return type(self), (self.request,) + + +class UserInvalidError(BadRequestError): + def __init__(self, request): + self.request = request + super(Exception, self).__init__('The given user was invalid' + self._fmt_request(self.request)) + + def __reduce__(self): + return type(self), (self.request,) + + +class UserIsBlockedError(BadRequestError): + def __init__(self, request): + self.request = request + super(Exception, self).__init__('User is blocked' + self._fmt_request(self.request)) + + def __reduce__(self): + return type(self), (self.request,) + + +class UserIsBotError(BadRequestError): + def __init__(self, request): + self.request = request + super(Exception, self).__init__("Bots can't send messages to other bots" + self._fmt_request(self.request)) + + def __reduce__(self): + return type(self), (self.request,) + + +class UserKickedError(BadRequestError): + def __init__(self, request): + self.request = request + super(Exception, self).__init__('This user was kicked from this supergroup/channel' + self._fmt_request(self.request)) + + def __reduce__(self): + return type(self), (self.request,) + + +class UserMigrateError(InvalidDCError): + def __init__(self, request, capture=0): + self.request = request + self.new_dc = int(capture) + super(Exception, self).__init__('The user whose identity is being used to execute queries is associated with DC {new_dc}'.format(new_dc=self.new_dc) + self._fmt_request(self.request)) + + def __reduce__(self): + return type(self), (self.request, self.new_dc) + + +class UserNotMutualContactError(BadRequestError): + def __init__(self, request): + self.request = request + super(Exception, self).__init__('The provided user is not a mutual contact' + self._fmt_request(self.request)) + + def __reduce__(self): + return type(self), (self.request,) + + +class UserNotParticipantError(BadRequestError): + def __init__(self, request): + self.request = request + super(Exception, self).__init__('The target user is not a member of the specified megagroup or channel' + self._fmt_request(self.request)) + + def __reduce__(self): + return type(self), (self.request,) + + +class UserPrivacyRestrictedError(ForbiddenError): + def __init__(self, request): + self.request = request + super(Exception, self).__init__("The user's privacy settings do not allow you to do this" + self._fmt_request(self.request)) + + def __reduce__(self): + return type(self), (self.request,) + + +class UserRestrictedError(ForbiddenError): + def __init__(self, request): + self.request = request + super(Exception, self).__init__("You're spamreported, you can't create channels or chats." + self._fmt_request(self.request)) + + def __reduce__(self): + return type(self), (self.request,) + + +class UserVolumeInvalidError(BadRequestError): + def __init__(self, request): + self.request = request + super(Exception, self).__init__('The specified user volume is invalid' + self._fmt_request(self.request)) + + def __reduce__(self): + return type(self), (self.request,) + + +class VideoContentTypeInvalidError(BadRequestError): + def __init__(self, request): + self.request = request + super(Exception, self).__init__('The video content type is not supported with the given parameters (i.e. supports_streaming)' + self._fmt_request(self.request)) + + def __reduce__(self): + return type(self), (self.request,) + + +class VideoFileInvalidError(BadRequestError): + def __init__(self, request): + self.request = request + super(Exception, self).__init__('The given video cannot be used' + self._fmt_request(self.request)) + + def __reduce__(self): + return type(self), (self.request,) + + +class VideoTitleEmptyError(BadRequestError): + def __init__(self, request): + self.request = request + super(Exception, self).__init__('The specified video title is empty' + self._fmt_request(self.request)) + + def __reduce__(self): + return type(self), (self.request,) + + +class VoiceMessagesForbiddenError(BadRequestError): + def __init__(self, request): + self.request = request + super(Exception, self).__init__("This user's privacy settings forbid you from sending voice messages" + self._fmt_request(self.request)) + + def __reduce__(self): + return type(self), (self.request,) + + +class WallpaperFileInvalidError(BadRequestError): + def __init__(self, request): + self.request = request + super(Exception, self).__init__('The given file cannot be used as a wallpaper' + self._fmt_request(self.request)) + + def __reduce__(self): + return type(self), (self.request,) + + +class WallpaperInvalidError(BadRequestError): + def __init__(self, request): + self.request = request + super(Exception, self).__init__('The input wallpaper was not valid' + self._fmt_request(self.request)) + + def __reduce__(self): + return type(self), (self.request,) + + +class WallpaperMimeInvalidError(BadRequestError): + def __init__(self, request): + self.request = request + super(Exception, self).__init__('The specified wallpaper MIME type is invalid' + self._fmt_request(self.request)) + + def __reduce__(self): + return type(self), (self.request,) + + +class WcConvertUrlInvalidError(BadRequestError): + def __init__(self, request): + self.request = request + super(Exception, self).__init__('WC convert URL invalid' + self._fmt_request(self.request)) + + def __reduce__(self): + return type(self), (self.request,) + + +class WebdocumentInvalidError(BadRequestError): + def __init__(self, request): + self.request = request + super(Exception, self).__init__('Invalid webdocument URL provided' + self._fmt_request(self.request)) + + def __reduce__(self): + return type(self), (self.request,) + + +class WebdocumentMimeInvalidError(BadRequestError): + def __init__(self, request): + self.request = request + super(Exception, self).__init__('Invalid webdocument mime type provided' + self._fmt_request(self.request)) + + def __reduce__(self): + return type(self), (self.request,) + + +class WebdocumentSizeTooBigError(BadRequestError): + def __init__(self, request): + self.request = request + super(Exception, self).__init__('Webdocument is too big!' + self._fmt_request(self.request)) + + def __reduce__(self): + return type(self), (self.request,) + + +class WebdocumentUrlInvalidError(BadRequestError): + def __init__(self, request): + self.request = request + super(Exception, self).__init__('The given URL cannot be used' + self._fmt_request(self.request)) + + def __reduce__(self): + return type(self), (self.request,) + + +class WebpageCurlFailedError(BadRequestError): + def __init__(self, request): + self.request = request + super(Exception, self).__init__('Failure while fetching the webpage with cURL' + self._fmt_request(self.request)) + + def __reduce__(self): + return type(self), (self.request,) + + +class WebpageMediaEmptyError(BadRequestError): + def __init__(self, request): + self.request = request + super(Exception, self).__init__('Webpage media empty' + self._fmt_request(self.request)) + + def __reduce__(self): + return type(self), (self.request,) + + +class WebpushAuthInvalidError(BadRequestError): + def __init__(self, request): + self.request = request + super(Exception, self).__init__('The specified web push authentication secret is invalid' + self._fmt_request(self.request)) + + def __reduce__(self): + return type(self), (self.request,) + + +class WebpushKeyInvalidError(BadRequestError): + def __init__(self, request): + self.request = request + super(Exception, self).__init__('The specified web push elliptic curve Diffie-Hellman public key is invalid' + self._fmt_request(self.request)) + + def __reduce__(self): + return type(self), (self.request,) + + +class WebpushTokenInvalidError(BadRequestError): + def __init__(self, request): + self.request = request + super(Exception, self).__init__('The specified web push token is invalid' + self._fmt_request(self.request)) + + def __reduce__(self): + return type(self), (self.request,) + + +class WorkerBusyTooLongRetryError(ServerError): + def __init__(self, request): + self.request = request + super(Exception, self).__init__('Telegram workers are too busy to respond immediately' + self._fmt_request(self.request)) + + def __reduce__(self): + return type(self), (self.request,) + + +class YouBlockedUserError(BadRequestError): + def __init__(self, request): + self.request = request + super(Exception, self).__init__('You blocked this user' + self._fmt_request(self.request)) + + def __reduce__(self): + return type(self), (self.request,) + + +class FrozenMethodInvalidError(FloodError): + def __init__(self, request): + self.request = request + super(Exception, self).__init__('You tried to use a method that is not available for frozen accounts' + self._fmt_request(self.request)) + + def __reduce__(self): + return type(self), (self.request,) + + +class FrozenParticipantMissingError(BadRequestError): + def __init__(self, request): + self.request = request + super(Exception, self).__init__("Your account is frozen and can't access the chat" + self._fmt_request(self.request)) + + def __reduce__(self): + return type(self), (self.request,) + + +class ChatSendVoicesForbiddenError(ForbiddenError): + def __init__(self, request): + self.request = request + super(Exception, self).__init__('You cannot send voices results in this chat' + self._fmt_request(self.request)) + + def __reduce__(self): + return type(self), (self.request,) + + +class ChatSendPhotosForbiddenError(ForbiddenError): + def __init__(self, request): + self.request = request + super(Exception, self).__init__('You cannot send photos results in this chat' + self._fmt_request(self.request)) + + def __reduce__(self): + return type(self), (self.request,) + + +class ChatSendVideosForbiddenError(ForbiddenError): + def __init__(self, request): + self.request = request + super(Exception, self).__init__('You cannot send videos results in this Chat' + self._fmt_request(self.request)) + + def __reduce__(self): + return type(self), (self.request,) + + +class ChatSendPlainForbiddenError(ForbiddenError): + def __init__(self, request): + self.request = request + super(Exception, self).__init__('You cannot send plain results in this chat' + self._fmt_request(self.request)) + + def __reduce__(self): + return type(self), (self.request,) + + +rpc_errors_dict = { + 'ABOUT_TOO_LONG': AboutTooLongError, + 'ACCESS_TOKEN_EXPIRED': AccessTokenExpiredError, + 'ACCESS_TOKEN_INVALID': AccessTokenInvalidError, + 'ACTIVE_USER_REQUIRED': ActiveUserRequiredError, + 'ADMINS_TOO_MUCH': AdminsTooMuchError, + 'ADMIN_ID_INVALID': AdminIdInvalidError, + 'ADMIN_RANK_EMOJI_NOT_ALLOWED': AdminRankEmojiNotAllowedError, + 'ADMIN_RANK_INVALID': AdminRankInvalidError, + 'ALBUM_PHOTOS_TOO_MANY': AlbumPhotosTooManyError, + 'API_ID_INVALID': ApiIdInvalidError, + 'API_ID_PUBLISHED_FLOOD': ApiIdPublishedFloodError, + 'ARTICLE_TITLE_EMPTY': ArticleTitleEmptyError, + 'AUDIO_CONTENT_URL_EMPTY': AudioContentUrlEmptyError, + 'AUDIO_TITLE_EMPTY': AudioTitleEmptyError, + 'AUTH_BYTES_INVALID': AuthBytesInvalidError, + 'AUTH_KEY_DUPLICATED': AuthKeyDuplicatedError, + 'AUTH_KEY_INVALID': AuthKeyInvalidError, + 'AUTH_KEY_PERM_EMPTY': AuthKeyPermEmptyError, + 'AUTH_KEY_UNREGISTERED': AuthKeyUnregisteredError, + 'AUTH_RESTART': AuthRestartError, + 'AUTH_TOKEN_ALREADY_ACCEPTED': AuthTokenAlreadyAcceptedError, + 'AUTH_TOKEN_EXCEPTION': AuthTokenExceptionError, + 'AUTH_TOKEN_EXPIRED': AuthTokenExpiredError, + 'AUTH_TOKEN_INVALID': AuthTokenInvalidError, + 'AUTH_TOKEN_INVALID2': AuthTokenInvalid2Error, + 'AUTH_TOKEN_INVALIDX': AuthTokenInvalidxError, + 'AUTOARCHIVE_NOT_AVAILABLE': AutoarchiveNotAvailableError, + 'BANK_CARD_NUMBER_INVALID': BankCardNumberInvalidError, + 'BANNED_RIGHTS_INVALID': BannedRightsInvalidError, + 'BASE_PORT_LOC_INVALID': BasePortLocInvalidError, + 'BOTS_TOO_MUCH': BotsTooMuchError, + 'BOT_CHANNELS_NA': BotChannelsNaError, + 'BOT_COMMAND_DESCRIPTION_INVALID': BotCommandDescriptionInvalidError, + 'BOT_COMMAND_INVALID': BotCommandInvalidError, + 'BOT_COMMANDS_TOO_MUCH': BotCommandsTooMuchError, + 'BOT_DOMAIN_INVALID': BotDomainInvalidError, + 'BOT_GAMES_DISABLED': BotGamesDisabledError, + 'BOT_GROUPS_BLOCKED': BotGroupsBlockedError, + 'BOT_INLINE_DISABLED': BotInlineDisabledError, + 'BOT_INVALID': BotInvalidError, + 'BOT_METHOD_INVALID': BotMethodInvalidError, + 'BOT_MISSING': BotMissingError, + 'BOT_ONESIDE_NOT_AVAIL': BotOnesideNotAvailError, + 'BOT_PAYMENTS_DISABLED': BotPaymentsDisabledError, + 'BOT_POLLS_DISABLED': BotPollsDisabledError, + 'BOT_RESPONSE_TIMEOUT': BotResponseTimeoutError, + 'BOT_SCORE_NOT_MODIFIED': BotScoreNotModifiedError, + 'BROADCAST_CALLS_DISABLED': BroadcastCallsDisabledError, + 'BROADCAST_FORBIDDEN': BroadcastForbiddenError, + 'BROADCAST_ID_INVALID': BroadcastIdInvalidError, + 'BROADCAST_PUBLIC_VOTERS_FORBIDDEN': BroadcastPublicVotersForbiddenError, + 'BROADCAST_REQUIRED': BroadcastRequiredError, + 'BUTTON_DATA_INVALID': ButtonDataInvalidError, + 'BUTTON_TEXT_INVALID': ButtonTextInvalidError, + 'BUTTON_TYPE_INVALID': ButtonTypeInvalidError, + 'BUTTON_URL_INVALID': ButtonUrlInvalidError, + 'BUTTON_USER_PRIVACY_RESTRICTED': ButtonUserPrivacyRestrictedError, + 'CALL_ALREADY_ACCEPTED': CallAlreadyAcceptedError, + 'CALL_ALREADY_DECLINED': CallAlreadyDeclinedError, + 'CALL_OCCUPY_FAILED': CallOccupyFailedError, + 'CALL_PEER_INVALID': CallPeerInvalidError, + 'CALL_PROTOCOL_FLAGS_INVALID': CallProtocolFlagsInvalidError, + 'CDN_METHOD_INVALID': CdnMethodInvalidError, + 'CDN_UPLOAD_TIMEOUT': CdnUploadTimeoutError, + 'CHANNELS_ADMIN_LOCATED_TOO_MUCH': ChannelsAdminLocatedTooMuchError, + 'CHANNELS_ADMIN_PUBLIC_TOO_MUCH': ChannelsAdminPublicTooMuchError, + 'CHANNELS_TOO_MUCH': ChannelsTooMuchError, + 'CHANNEL_BANNED': ChannelBannedError, + 'CHANNEL_FORUM_MISSING': ChannelForumMissingError, + 'CHANNEL_ID_INVALID': ChannelIdInvalidError, + 'CHANNEL_INVALID': ChannelInvalidError, + 'CHANNEL_PARICIPANT_MISSING': ChannelParicipantMissingError, + 'CHANNEL_PRIVATE': ChannelPrivateError, + 'CHANNEL_PUBLIC_GROUP_NA': ChannelPublicGroupNaError, + 'CHANNEL_TOO_BIG': ChannelTooBigError, + 'CHANNEL_TOO_LARGE': ChannelTooLargeError, + 'CHAT_ABOUT_NOT_MODIFIED': ChatAboutNotModifiedError, + 'CHAT_ABOUT_TOO_LONG': ChatAboutTooLongError, + 'CHAT_ADMIN_INVITE_REQUIRED': ChatAdminInviteRequiredError, + 'CHAT_ADMIN_REQUIRED': ChatAdminRequiredError, + 'CHAT_DISCUSSION_UNALLOWED': ChatDiscussionUnallowedError, + 'CHAT_FORBIDDEN': ChatForbiddenError, + 'CHAT_FORWARDS_RESTRICTED': ChatForwardsRestrictedError, + 'CHAT_GET_FAILED': ChatGetFailedError, + 'CHAT_GUEST_SEND_FORBIDDEN': ChatGuestSendForbiddenError, + 'CHAT_ID_EMPTY': ChatIdEmptyError, + 'CHAT_ID_GENERATE_FAILED': ChatIdGenerateFailedError, + 'CHAT_ID_INVALID': ChatIdInvalidError, + 'CHAT_INVALID': ChatInvalidError, + 'CHAT_INVITE_PERMANENT': ChatInvitePermanentError, + 'CHAT_LINK_EXISTS': ChatLinkExistsError, + 'CHAT_NOT_MODIFIED': ChatNotModifiedError, + 'CHAT_RESTRICTED': ChatRestrictedError, + 'CHAT_REVOKE_DATE_UNSUPPORTED': ChatRevokeDateUnsupportedError, + 'CHAT_SEND_GAME_FORBIDDEN': ChatSendGameForbiddenError, + 'CHAT_SEND_GIFS_FORBIDDEN': ChatSendGifsForbiddenError, + 'CHAT_SEND_INLINE_FORBIDDEN': ChatSendInlineForbiddenError, + 'CHAT_SEND_MEDIA_FORBIDDEN': ChatSendMediaForbiddenError, + 'CHAT_SEND_POLL_FORBIDDEN': ChatSendPollForbiddenError, + 'CHAT_SEND_STICKERS_FORBIDDEN': ChatSendStickersForbiddenError, + 'CHAT_TITLE_EMPTY': ChatTitleEmptyError, + 'CHAT_TOO_BIG': ChatTooBigError, + 'CHAT_WRITE_FORBIDDEN': ChatWriteForbiddenError, + 'CHP_CALL_FAIL': ChpCallFailError, + 'CODE_EMPTY': CodeEmptyError, + 'CODE_HASH_INVALID': CodeHashInvalidError, + 'CODE_INVALID': CodeInvalidError, + 'CONNECTION_API_ID_INVALID': ConnectionApiIdInvalidError, + 'CONNECTION_APP_VERSION_EMPTY': ConnectionAppVersionEmptyError, + 'CONNECTION_DEVICE_MODEL_EMPTY': ConnectionDeviceModelEmptyError, + 'CONNECTION_LANG_PACK_INVALID': ConnectionLangPackInvalidError, + 'CONNECTION_LAYER_INVALID': ConnectionLayerInvalidError, + 'CONNECTION_NOT_INITED': ConnectionNotInitedError, + 'CONNECTION_SYSTEM_EMPTY': ConnectionSystemEmptyError, + 'CONNECTION_SYSTEM_LANG_CODE_EMPTY': ConnectionSystemLangCodeEmptyError, + 'CONTACT_ADD_MISSING': ContactAddMissingError, + 'CONTACT_ID_INVALID': ContactIdInvalidError, + 'CONTACT_NAME_EMPTY': ContactNameEmptyError, + 'CONTACT_REQ_MISSING': ContactReqMissingError, + 'CREATE_CALL_FAILED': CreateCallFailedError, + 'CURRENCY_TOTAL_AMOUNT_INVALID': CurrencyTotalAmountInvalidError, + 'DATA_INVALID': DataInvalidError, + 'DATA_JSON_INVALID': DataJsonInvalidError, + 'DATA_TOO_LONG': DataTooLongError, + 'DATE_EMPTY': DateEmptyError, + 'DC_ID_INVALID': DcIdInvalidError, + 'DH_G_A_INVALID': DhGAInvalidError, + 'DOCUMENT_INVALID': DocumentInvalidError, + 'EDIT_BOT_INVITE_FORBIDDEN': EditBotInviteForbiddenError, + 'EMAIL_HASH_EXPIRED': EmailHashExpiredError, + 'EMAIL_INVALID': EmailInvalidError, + 'EMAIL_UNCONFIRMED': EmailUnconfirmedError, + 'EMAIL_VERIFY_EXPIRED': EmailVerifyExpiredError, + 'EMOJI_INVALID': EmojiInvalidError, + 'EMOJI_NOT_MODIFIED': EmojiNotModifiedError, + 'EMOTICON_EMPTY': EmoticonEmptyError, + 'EMOTICON_INVALID': EmoticonInvalidError, + 'EMOTICON_STICKERPACK_MISSING': EmoticonStickerpackMissingError, + 'ENCRYPTED_MESSAGE_INVALID': EncryptedMessageInvalidError, + 'ENCRYPTION_ALREADY_ACCEPTED': EncryptionAlreadyAcceptedError, + 'ENCRYPTION_ALREADY_DECLINED': EncryptionAlreadyDeclinedError, + 'ENCRYPTION_DECLINED': EncryptionDeclinedError, + 'ENCRYPTION_ID_INVALID': EncryptionIdInvalidError, + 'ENCRYPTION_OCCUPY_FAILED': EncryptionOccupyFailedError, + 'ENTITIES_TOO_LONG': EntitiesTooLongError, + 'ENTITY_BOUNDS_INVALID': EntityBoundsInvalidError, + 'ENTITY_MENTION_USER_INVALID': EntityMentionUserInvalidError, + 'ERROR_TEXT_EMPTY': ErrorTextEmptyError, + 'EXPIRE_DATE_INVALID': ExpireDateInvalidError, + 'EXPIRE_FORBIDDEN': ExpireForbiddenError, + 'EXPORT_CARD_INVALID': ExportCardInvalidError, + 'EXTERNAL_URL_INVALID': ExternalUrlInvalidError, + 'FIELD_NAME_EMPTY': FieldNameEmptyError, + 'FIELD_NAME_INVALID': FieldNameInvalidError, + 'FILEREF_UPGRADE_NEEDED': FilerefUpgradeNeededError, + 'FILE_CONTENT_TYPE_INVALID': FileContentTypeInvalidError, + 'FILE_EMTPY': FileEmtpyError, + 'FILE_ID_INVALID': FileIdInvalidError, + 'FILE_PARTS_INVALID': FilePartsInvalidError, + 'FILE_PART_0_MISSING': FilePart0MissingError, + 'FILE_PART_EMPTY': FilePartEmptyError, + 'FILE_PART_INVALID': FilePartInvalidError, + 'FILE_PART_LENGTH_INVALID': FilePartLengthInvalidError, + 'FILE_PART_SIZE_CHANGED': FilePartSizeChangedError, + 'FILE_PART_SIZE_INVALID': FilePartSizeInvalidError, + 'FILE_PART_TOO_BIG': FilePartTooBigError, + 'FILE_REFERENCE_EMPTY': FileReferenceEmptyError, + 'FILE_REFERENCE_EXPIRED': FileReferenceExpiredError, + 'FILE_REFERENCE_INVALID': FileReferenceInvalidError, + 'FILE_TITLE_EMPTY': FileTitleEmptyError, + 'FILTER_ID_INVALID': FilterIdInvalidError, + 'FILTER_INCLUDE_EMPTY': FilterIncludeEmptyError, + 'FILTER_NOT_SUPPORTED': FilterNotSupportedError, + 'FILTER_TITLE_EMPTY': FilterTitleEmptyError, + 'FIRSTNAME_INVALID': FirstNameInvalidError, + 'FOLDER_ID_EMPTY': FolderIdEmptyError, + 'FOLDER_ID_INVALID': FolderIdInvalidError, + 'FRESH_CHANGE_ADMINS_FORBIDDEN': FreshChangeAdminsForbiddenError, + 'FRESH_CHANGE_PHONE_FORBIDDEN': FreshChangePhoneForbiddenError, + 'FRESH_RESET_AUTHORISATION_FORBIDDEN': FreshResetAuthorisationForbiddenError, + 'FROM_MESSAGE_BOT_DISABLED': FromMessageBotDisabledError, + 'FROM_PEER_INVALID': FromPeerInvalidError, + 'GAME_BOT_INVALID': GameBotInvalidError, + 'GEO_POINT_INVALID': GeoPointInvalidError, + 'GIF_CONTENT_TYPE_INVALID': GifContentTypeInvalidError, + 'GIF_ID_INVALID': GifIdInvalidError, + 'GRAPH_EXPIRED_RELOAD': GraphExpiredReloadError, + 'GRAPH_INVALID_RELOAD': GraphInvalidReloadError, + 'GRAPH_OUTDATED_RELOAD': GraphOutdatedReloadError, + 'GROUPCALL_ADD_PARTICIPANTS_FAILED': GroupcallAddParticipantsFailedError, + 'GROUPCALL_ALREADY_DISCARDED': GroupcallAlreadyDiscardedError, + 'GROUPCALL_ALREADY_STARTED': GroupcallAlreadyStartedError, + 'GROUPCALL_FORBIDDEN': GroupcallForbiddenError, + 'GROUPCALL_INVALID': GroupcallInvalidError, + 'GROUPCALL_JOIN_MISSING': GroupcallJoinMissingError, + 'GROUPCALL_NOT_MODIFIED': GroupcallNotModifiedError, + 'GROUPCALL_SSRC_DUPLICATE_MUCH': GroupcallSsrcDuplicateMuchError, + 'GROUPED_MEDIA_INVALID': GroupedMediaInvalidError, + 'GROUP_CALL_INVALID': GroupCallInvalidError, + 'HASH_INVALID': HashInvalidError, + 'HIDE_REQUESTER_MISSING': HideRequesterMissingError, + 'HISTORY_GET_FAILED': HistoryGetFailedError, + 'IMAGE_PROCESS_FAILED': ImageProcessFailedError, + 'IMPORT_FILE_INVALID': ImportFileInvalidError, + 'IMPORT_FORMAT_UNRECOGNIZED': ImportFormatUnrecognizedError, + 'IMPORT_ID_INVALID': ImportIdInvalidError, + 'INLINE_BOT_REQUIRED': InlineBotRequiredError, + 'INLINE_RESULT_EXPIRED': InlineResultExpiredError, + 'INPUT_CONSTRUCTOR_INVALID': InputConstructorInvalidError, + 'INPUT_FETCH_ERROR': InputFetchErrorError, + 'INPUT_FETCH_FAIL': InputFetchFailError, + 'INPUT_FILTER_INVALID': InputFilterInvalidError, + 'INPUT_LAYER_INVALID': InputLayerInvalidError, + 'INPUT_METHOD_INVALID': InputMethodInvalidError, + 'INPUT_REQUEST_TOO_LONG': InputRequestTooLongError, + 'INPUT_TEXT_EMPTY': InputTextEmptyError, + 'INPUT_USER_DEACTIVATED': InputUserDeactivatedError, + 'INVITE_FORBIDDEN_WITH_JOINAS': InviteForbiddenWithJoinasError, + 'INVITE_HASH_EMPTY': InviteHashEmptyError, + 'INVITE_HASH_EXPIRED': InviteHashExpiredError, + 'INVITE_HASH_INVALID': InviteHashInvalidError, + 'INVITE_REQUEST_SENT': InviteRequestSentError, + 'INVITE_REVOKED_MISSING': InviteRevokedMissingError, + 'INVOICE_PAYLOAD_INVALID': InvoicePayloadInvalidError, + 'JOIN_AS_PEER_INVALID': JoinAsPeerInvalidError, + 'LANG_CODE_INVALID': LangCodeInvalidError, + 'LANG_CODE_NOT_SUPPORTED': LangCodeNotSupportedError, + 'LANG_PACK_INVALID': LangPackInvalidError, + 'LASTNAME_INVALID': LastnameInvalidError, + 'LIMIT_INVALID': LimitInvalidError, + 'LINK_NOT_MODIFIED': LinkNotModifiedError, + 'LOCATION_INVALID': LocationInvalidError, + 'MAX_DATE_INVALID': MaxDateInvalidError, + 'MAX_ID_INVALID': MaxIdInvalidError, + 'MAX_QTS_INVALID': MaxQtsInvalidError, + 'MD5_CHECKSUM_INVALID': Md5ChecksumInvalidError, + 'MEDIA_CAPTION_TOO_LONG': MediaCaptionTooLongError, + 'MEDIA_EMPTY': MediaEmptyError, + 'MEDIA_GROUPED_INVALID': MediaGroupedInvalidError, + 'MEDIA_INVALID': MediaInvalidError, + 'MEDIA_NEW_INVALID': MediaNewInvalidError, + 'MEDIA_PREV_INVALID': MediaPrevInvalidError, + 'MEDIA_TTL_INVALID': MediaTtlInvalidError, + 'MEGAGROUP_ID_INVALID': MegagroupIdInvalidError, + 'MEGAGROUP_PREHISTORY_HIDDEN': MegagroupPrehistoryHiddenError, + 'MEGAGROUP_REQUIRED': MegagroupRequiredError, + 'MEMBER_NO_LOCATION': MemberNoLocationError, + 'MEMBER_OCCUPY_PRIMARY_LOC_FAILED': MemberOccupyPrimaryLocFailedError, + 'MESSAGE_AUTHOR_REQUIRED': MessageAuthorRequiredError, + 'MESSAGE_DELETE_FORBIDDEN': MessageDeleteForbiddenError, + 'MESSAGE_EDIT_TIME_EXPIRED': MessageEditTimeExpiredError, + 'MESSAGE_EMPTY': MessageEmptyError, + 'MESSAGE_IDS_EMPTY': MessageIdsEmptyError, + 'MESSAGE_ID_INVALID': MessageIdInvalidError, + 'MESSAGE_NOT_MODIFIED': MessageNotModifiedError, + 'MESSAGE_POLL_CLOSED': MessagePollClosedError, + 'MESSAGE_TOO_LONG': MessageTooLongError, + 'METHOD_INVALID': MethodInvalidError, + 'MIN_DATE_INVALID': MinDateInvalidError, + 'MSGID_DECREASE_RETRY': MsgidDecreaseRetryError, + 'MSG_ID_INVALID': MsgIdInvalidError, + 'MSG_TOO_OLD': MsgTooOldError, + 'MSG_WAIT_FAILED': MsgWaitFailedError, + 'MT_SEND_QUEUE_TOO_LONG': MtSendQueueTooLongError, + 'MULTI_MEDIA_TOO_LONG': MultiMediaTooLongError, + 'NEED_CHAT_INVALID': NeedChatInvalidError, + 'NEED_MEMBER_INVALID': NeedMemberInvalidError, + 'NEW_SALT_INVALID': NewSaltInvalidError, + 'NEW_SETTINGS_EMPTY': NewSettingsEmptyError, + 'NEW_SETTINGS_INVALID': NewSettingsInvalidError, + 'NEXT_OFFSET_INVALID': NextOffsetInvalidError, + 'NOT_ALLOWED': NotAllowedError, + 'OFFSET_INVALID': OffsetInvalidError, + 'OFFSET_PEER_ID_INVALID': OffsetPeerIdInvalidError, + 'OPTIONS_TOO_MUCH': OptionsTooMuchError, + 'OPTION_INVALID': OptionInvalidError, + 'PACK_SHORT_NAME_INVALID': PackShortNameInvalidError, + 'PACK_SHORT_NAME_OCCUPIED': PackShortNameOccupiedError, + 'PACK_TITLE_INVALID': PackTitleInvalidError, + 'PARTICIPANTS_TOO_FEW': ParticipantsTooFewError, + 'PARTICIPANT_CALL_FAILED': ParticipantCallFailedError, + 'PARTICIPANT_ID_INVALID': ParticipantIdInvalidError, + 'PARTICIPANT_JOIN_MISSING': ParticipantJoinMissingError, + 'PARTICIPANT_VERSION_OUTDATED': ParticipantVersionOutdatedError, + 'PASSWORD_EMPTY': PasswordEmptyError, + 'PASSWORD_HASH_INVALID': PasswordHashInvalidError, + 'PASSWORD_MISSING': PasswordMissingError, + 'PASSWORD_RECOVERY_EXPIRED': PasswordRecoveryExpiredError, + 'PASSWORD_RECOVERY_NA': PasswordRecoveryNaError, + 'PASSWORD_REQUIRED': PasswordRequiredError, + 'PAYMENT_PROVIDER_INVALID': PaymentProviderInvalidError, + 'PEER_FLOOD': PeerFloodError, + 'PEER_HISTORY_EMPTY': PeerHistoryEmptyError, + 'PEER_ID_INVALID': PeerIdInvalidError, + 'PEER_ID_NOT_SUPPORTED': PeerIdNotSupportedError, + 'PERSISTENT_TIMESTAMP_EMPTY': PersistentTimestampEmptyError, + 'PERSISTENT_TIMESTAMP_INVALID': PersistentTimestampInvalidError, + 'PERSISTENT_TIMESTAMP_OUTDATED': PersistentTimestampOutdatedError, + 'PHONE_CODE_EMPTY': PhoneCodeEmptyError, + 'PHONE_CODE_EXPIRED': PhoneCodeExpiredError, + 'PHONE_CODE_HASH_EMPTY': PhoneCodeHashEmptyError, + 'PHONE_CODE_INVALID': PhoneCodeInvalidError, + 'PHONE_HASH_EXPIRED': PhoneHashExpiredError, + 'PHONE_NOT_OCCUPIED': PhoneNotOccupiedError, + 'PHONE_NUMBER_APP_SIGNUP_FORBIDDEN': PhoneNumberAppSignupForbiddenError, + 'PHONE_NUMBER_BANNED': PhoneNumberBannedError, + 'PHONE_NUMBER_FLOOD': PhoneNumberFloodError, + 'PHONE_NUMBER_INVALID': PhoneNumberInvalidError, + 'PHONE_NUMBER_OCCUPIED': PhoneNumberOccupiedError, + 'PHONE_NUMBER_UNOCCUPIED': PhoneNumberUnoccupiedError, + 'PHONE_PASSWORD_FLOOD': PhonePasswordFloodError, + 'PHONE_PASSWORD_PROTECTED': PhonePasswordProtectedError, + 'PHOTO_CONTENT_TYPE_INVALID': PhotoContentTypeInvalidError, + 'PHOTO_CONTENT_URL_EMPTY': PhotoContentUrlEmptyError, + 'PHOTO_CROP_FILE_MISSING': PhotoCropFileMissingError, + 'PHOTO_CROP_SIZE_SMALL': PhotoCropSizeSmallError, + 'PHOTO_EXT_INVALID': PhotoExtInvalidError, + 'PHOTO_FILE_MISSING': PhotoFileMissingError, + 'PHOTO_ID_INVALID': PhotoIdInvalidError, + 'PHOTO_INVALID': PhotoInvalidError, + 'PHOTO_INVALID_DIMENSIONS': PhotoInvalidDimensionsError, + 'PHOTO_SAVE_FILE_INVALID': PhotoSaveFileInvalidError, + 'PHOTO_THUMB_URL_EMPTY': PhotoThumbUrlEmptyError, + 'PINNED_DIALOGS_TOO_MUCH': PinnedDialogsTooMuchError, + 'PIN_RESTRICTED': PinRestrictedError, + 'POLL_ANSWERS_INVALID': PollAnswersInvalidError, + 'POLL_ANSWER_INVALID': PollAnswerInvalidError, + 'POLL_OPTION_DUPLICATE': PollOptionDuplicateError, + 'POLL_OPTION_INVALID': PollOptionInvalidError, + 'POLL_QUESTION_INVALID': PollQuestionInvalidError, + 'POLL_UNSUPPORTED': PollUnsupportedError, + 'POLL_VOTE_REQUIRED': PollVoteRequiredError, + 'POSTPONED_TIMEOUT': PostponedTimeoutError, + 'PREMIUM_ACCOUNT_REQUIRED': PremiumAccountRequiredError, + 'PREMIUM_CURRENTLY_UNAVAILABLE': PremiumCurrentlyUnavailableError, + 'PRIVACY_KEY_INVALID': PrivacyKeyInvalidError, + 'PRIVACY_TOO_LONG': PrivacyTooLongError, + 'PRIVACY_VALUE_INVALID': PrivacyValueInvalidError, + 'PTS_CHANGE_EMPTY': PtsChangeEmptyError, + 'PUBLIC_CHANNEL_MISSING': PublicChannelMissingError, + 'PUBLIC_KEY_REQUIRED': PublicKeyRequiredError, + 'QUERY_ID_EMPTY': QueryIdEmptyError, + 'QUERY_ID_INVALID': QueryIdInvalidError, + 'QUERY_TOO_SHORT': QueryTooShortError, + 'QUIZ_ANSWER_MISSING': QuizAnswerMissingError, + 'QUIZ_CORRECT_ANSWERS_EMPTY': QuizCorrectAnswersEmptyError, + 'QUIZ_CORRECT_ANSWERS_TOO_MUCH': QuizCorrectAnswersTooMuchError, + 'QUIZ_CORRECT_ANSWER_INVALID': QuizCorrectAnswerInvalidError, + 'QUIZ_MULTIPLE_INVALID': QuizMultipleInvalidError, + 'RANDOM_ID_DUPLICATE': RandomIdDuplicateError, + 'RANDOM_ID_EMPTY': RandomIdEmptyError, + 'RANDOM_ID_INVALID': RandomIdInvalidError, + 'RANDOM_LENGTH_INVALID': RandomLengthInvalidError, + 'RANGES_INVALID': RangesInvalidError, + 'REACTIONS_TOO_MANY': ReactionsTooManyError, + 'REACTION_EMPTY': ReactionEmptyError, + 'REACTION_INVALID': ReactionInvalidError, + 'REFLECTOR_NOT_AVAILABLE': ReflectorNotAvailableError, + 'REG_ID_GENERATE_FAILED': RegIdGenerateFailedError, + 'REPLY_MARKUP_BUY_EMPTY': ReplyMarkupBuyEmptyError, + 'REPLY_MARKUP_GAME_EMPTY': ReplyMarkupGameEmptyError, + 'REPLY_MARKUP_INVALID': ReplyMarkupInvalidError, + 'REPLY_MARKUP_TOO_LONG': ReplyMarkupTooLongError, + 'RESET_REQUEST_MISSING': ResetRequestMissingError, + 'RESULTS_TOO_MUCH': ResultsTooMuchError, + 'RESULT_ID_DUPLICATE': ResultIdDuplicateError, + 'RESULT_ID_EMPTY': ResultIdEmptyError, + 'RESULT_ID_INVALID': ResultIdInvalidError, + 'RESULT_TYPE_INVALID': ResultTypeInvalidError, + 'REVOTE_NOT_ALLOWED': RevoteNotAllowedError, + 'RIGHTS_NOT_MODIFIED': RightsNotModifiedError, + 'RIGHT_FORBIDDEN': RightForbiddenError, + 'RPC_CALL_FAIL': RpcCallFailError, + 'RPC_MCGET_FAIL': RpcMcgetFailError, + 'RSA_DECRYPT_FAILED': RsaDecryptFailedError, + 'SCHEDULE_BOT_NOT_ALLOWED': ScheduleBotNotAllowedError, + 'SCHEDULE_DATE_INVALID': ScheduleDateInvalidError, + 'SCHEDULE_DATE_TOO_LATE': ScheduleDateTooLateError, + 'SCHEDULE_STATUS_PRIVATE': ScheduleStatusPrivateError, + 'SCHEDULE_TOO_MUCH': ScheduleTooMuchError, + 'SCORE_INVALID': ScoreInvalidError, + 'SEARCH_QUERY_EMPTY': SearchQueryEmptyError, + 'SEARCH_WITH_LINK_NOT_SUPPORTED': SearchWithLinkNotSupportedError, + 'SECONDS_INVALID': SecondsInvalidError, + 'SEND_AS_PEER_INVALID': SendAsPeerInvalidError, + 'SEND_CODE_UNAVAILABLE': SendCodeUnavailableError, + 'SEND_MESSAGE_MEDIA_INVALID': SendMessageMediaInvalidError, + 'SEND_MESSAGE_TYPE_INVALID': SendMessageTypeInvalidError, + 'SENSITIVE_CHANGE_FORBIDDEN': SensitiveChangeForbiddenError, + 'SESSION_EXPIRED': SessionExpiredError, + 'SESSION_PASSWORD_NEEDED': SessionPasswordNeededError, + 'SESSION_REVOKED': SessionRevokedError, + 'SETTINGS_INVALID': SettingsInvalidError, + 'SHA256_HASH_INVALID': Sha256HashInvalidError, + 'SHORTNAME_OCCUPY_FAILED': ShortnameOccupyFailedError, + 'SHORT_NAME_INVALID': ShortNameInvalidError, + 'SHORT_NAME_OCCUPIED': ShortNameOccupiedError, + 'SIGN_IN_FAILED': SignInFailedError, + 'SLOWMODE_MULTI_MSGS_DISABLED': SlowModeMultiMsgsDisabledError, + 'SMS_CODE_CREATE_FAILED': SmsCodeCreateFailedError, + 'SRP_ID_INVALID': SrpIdInvalidError, + 'SRP_PASSWORD_CHANGED': SrpPasswordChangedError, + 'START_PARAM_EMPTY': StartParamEmptyError, + 'START_PARAM_INVALID': StartParamInvalidError, + 'START_PARAM_TOO_LONG': StartParamTooLongError, + 'STICKERPACK_STICKERS_TOO_MUCH': StickerpackStickersTooMuchError, + 'STICKERSET_INVALID': StickersetInvalidError, + 'STICKERSET_OWNER_ANONYMOUS': StickersetOwnerAnonymousError, + 'STICKERS_EMPTY': StickersEmptyError, + 'STICKERS_TOO_MUCH': StickersTooMuchError, + 'STICKER_DOCUMENT_INVALID': StickerDocumentInvalidError, + 'STICKER_EMOJI_INVALID': StickerEmojiInvalidError, + 'STICKER_FILE_INVALID': StickerFileInvalidError, + 'STICKER_GIF_DIMENSIONS': StickerGifDimensionsError, + 'STICKER_ID_INVALID': StickerIdInvalidError, + 'STICKER_INVALID': StickerInvalidError, + 'STICKER_MIME_INVALID': StickerMimeInvalidError, + 'STICKER_PNG_DIMENSIONS': StickerPngDimensionsError, + 'STICKER_PNG_NOPNG': StickerPngNopngError, + 'STICKER_TGS_NODOC': StickerTgsNodocError, + 'STICKER_TGS_NOTGS': StickerTgsNotgsError, + 'STICKER_THUMB_PNG_NOPNG': StickerThumbPngNopngError, + 'STICKER_THUMB_TGS_NOTGS': StickerThumbTgsNotgsError, + 'STICKER_VIDEO_BIG': StickerVideoBigError, + 'STICKER_VIDEO_NODOC': StickerVideoNodocError, + 'STICKER_VIDEO_NOWEBM': StickerVideoNowebmError, + 'STORAGE_CHECK_FAILED': StorageCheckFailedError, + 'STORE_INVALID_SCALAR_TYPE': StoreInvalidScalarTypeError, + 'SWITCH_PM_TEXT_EMPTY': SwitchPmTextEmptyError, + 'TAKEOUT_INVALID': TakeoutInvalidError, + 'TAKEOUT_REQUIRED': TakeoutRequiredError, + 'TEMP_AUTH_KEY_ALREADY_BOUND': TempAuthKeyAlreadyBoundError, + 'TEMP_AUTH_KEY_EMPTY': TempAuthKeyEmptyError, + 'THEME_FILE_INVALID': ThemeFileInvalidError, + 'THEME_FORMAT_INVALID': ThemeFormatInvalidError, + 'THEME_INVALID': ThemeInvalidError, + 'THEME_MIME_INVALID': ThemeMimeInvalidError, + 'THEME_TITLE_INVALID': ThemeTitleInvalidError, + 'TIMEOUT': TimeoutError, + 'TITLE_INVALID': TitleInvalidError, + 'TMP_PASSWORD_DISABLED': TmpPasswordDisabledError, + 'TMP_PASSWORD_INVALID': TmpPasswordInvalidError, + 'TOKEN_INVALID': TokenInvalidError, + 'TOPIC_DELETED': TopicDeletedError, + 'TO_LANG_INVALID': ToLangInvalidError, + 'TTL_DAYS_INVALID': TtlDaysInvalidError, + 'TTL_MEDIA_INVALID': TtlMediaInvalidError, + 'TTL_PERIOD_INVALID': TtlPeriodInvalidError, + 'TYPES_EMPTY': TypesEmptyError, + 'TYPE_CONSTRUCTOR_INVALID': TypeConstructorInvalidError, + 'Timedout': TimedoutError, + 'Timeout': TimeoutError, + 'UNKNOWN_ERROR': UnknownErrorError, + 'UNKNOWN_METHOD': UnknownMethodError, + 'UNTIL_DATE_INVALID': UntilDateInvalidError, + 'UPDATE_APP_TO_LOGIN': UpdateAppToLoginError, + 'URL_INVALID': UrlInvalidError, + 'USAGE_LIMIT_INVALID': UsageLimitInvalidError, + 'USERNAME_INVALID': UsernameInvalidError, + 'USERNAME_NOT_MODIFIED': UsernameNotModifiedError, + 'USERNAME_NOT_OCCUPIED': UsernameNotOccupiedError, + 'USERNAME_OCCUPIED': UsernameOccupiedError, + 'USERNAME_PURCHASE_AVAILABLE': UsernamePurchaseAvailableError, + 'USERPIC_PRIVACY_REQUIRED': UserpicPrivacyRequiredError, + 'USERPIC_UPLOAD_REQUIRED': UserpicUploadRequiredError, + 'USERS_TOO_FEW': UsersTooFewError, + 'USERS_TOO_MUCH': UsersTooMuchError, + 'USER_ADMIN_INVALID': UserAdminInvalidError, + 'USER_ALREADY_INVITED': UserAlreadyInvitedError, + 'USER_ALREADY_PARTICIPANT': UserAlreadyParticipantError, + 'USER_BANNED_IN_CHANNEL': UserBannedInChannelError, + 'USER_BLOCKED': UserBlockedError, + 'USER_BOT': UserBotError, + 'USER_BOT_INVALID': UserBotInvalidError, + 'USER_BOT_REQUIRED': UserBotRequiredError, + 'USER_CHANNELS_TOO_MUCH': UserChannelsTooMuchError, + 'USER_CREATOR': UserCreatorError, + 'USER_DEACTIVATED': UserDeactivatedError, + 'USER_DEACTIVATED_BAN': UserDeactivatedBanError, + 'USER_DELETED': UserDeletedError, + 'USER_ID_INVALID': UserIdInvalidError, + 'USER_INVALID': UserInvalidError, + 'USER_IS_BLOCKED': UserIsBlockedError, + 'USER_IS_BOT': UserIsBotError, + 'USER_KICKED': UserKickedError, + 'USER_NOT_MUTUAL_CONTACT': UserNotMutualContactError, + 'USER_NOT_PARTICIPANT': UserNotParticipantError, + 'USER_PRIVACY_RESTRICTED': UserPrivacyRestrictedError, + 'USER_RESTRICTED': UserRestrictedError, + 'USER_VOLUME_INVALID': UserVolumeInvalidError, + 'VIDEO_CONTENT_TYPE_INVALID': VideoContentTypeInvalidError, + 'VIDEO_FILE_INVALID': VideoFileInvalidError, + 'VIDEO_TITLE_EMPTY': VideoTitleEmptyError, + 'VOICE_MESSAGES_FORBIDDEN': VoiceMessagesForbiddenError, + 'WALLPAPER_FILE_INVALID': WallpaperFileInvalidError, + 'WALLPAPER_INVALID': WallpaperInvalidError, + 'WALLPAPER_MIME_INVALID': WallpaperMimeInvalidError, + 'WC_CONVERT_URL_INVALID': WcConvertUrlInvalidError, + 'WEBDOCUMENT_INVALID': WebdocumentInvalidError, + 'WEBDOCUMENT_MIME_INVALID': WebdocumentMimeInvalidError, + 'WEBDOCUMENT_SIZE_TOO_BIG': WebdocumentSizeTooBigError, + 'WEBDOCUMENT_URL_INVALID': WebdocumentUrlInvalidError, + 'WEBPAGE_CURL_FAILED': WebpageCurlFailedError, + 'WEBPAGE_MEDIA_EMPTY': WebpageMediaEmptyError, + 'WEBPUSH_AUTH_INVALID': WebpushAuthInvalidError, + 'WEBPUSH_KEY_INVALID': WebpushKeyInvalidError, + 'WEBPUSH_TOKEN_INVALID': WebpushTokenInvalidError, + 'WORKER_BUSY_TOO_LONG_RETRY': WorkerBusyTooLongRetryError, + 'YOU_BLOCKED_USER': YouBlockedUserError, + 'FROZEN_METHOD_INVALID': FrozenMethodInvalidError, + 'FROZEN_PARTICIPANT_MISSING': FrozenParticipantMissingError, + 'CHAT_SEND_VOICES_FORBIDDEN': ChatSendVoicesForbiddenError, + 'CHAT_SEND_PHOTOS_FORBIDDEN': ChatSendPhotosForbiddenError, + 'CHAT_SEND_VIDEOS_FORBIDDEN': ChatSendVideosForbiddenError, + 'CHAT_SEND_PLAIN_FORBIDDEN': ChatSendPlainForbiddenError, +} + +rpc_errors_re = ( + ('2FA_CONFIRM_WAIT_(\\d+)', TwoFaConfirmWaitError), + ('EMAIL_UNCONFIRMED_(\\d+)', EmailUnconfirmedError), + ('FILE_MIGRATE_(\\d+)', FileMigrateError), + ('FILE_PART_(\\d+)_MISSING', FilePartMissingError), + ('FLOOD_TEST_PHONE_WAIT_(\\d+)', FloodTestPhoneWaitError), + ('FLOOD_WAIT_(\\d+)', FloodWaitError), + ('FLOOD_PREMIUM_WAIT_(\\d+)', FloodPremiumWaitError), + ('INTERDC_(\\d+)_CALL_ERROR', InterdcCallErrorError), + ('INTERDC_(\\d+)_CALL_RICH_ERROR', InterdcCallRichErrorError), + ('NETWORK_MIGRATE_(\\d+)', NetworkMigrateError), + ('PASSWORD_TOO_FRESH_(\\d+)', PasswordTooFreshError), + ('PHONE_MIGRATE_(\\d+)', PhoneMigrateError), + ('PREVIOUS_CHAT_IMPORT_ACTIVE_WAIT_(\\d+)MIN', PreviousChatImportActiveWaitMinError), + ('SESSION_TOO_FRESH_(\\d+)', SessionTooFreshError), + ('SLOWMODE_WAIT_(\\d+)', SlowModeWaitError), + ('STATS_MIGRATE_(\\d+)', StatsMigrateError), + ('TAKEOUT_INIT_DELAY_(\\d+)', TakeoutInitDelayError), + ('USER_MIGRATE_(\\d+)', UserMigrateError), +) diff --git a/module/scripts/lib/telethon/events/__init__.py b/module/scripts/lib/telethon/events/__init__.py new file mode 100644 index 0000000..28f85b1 --- /dev/null +++ b/module/scripts/lib/telethon/events/__init__.py @@ -0,0 +1,140 @@ +from .raw import Raw +from .album import Album +from .chataction import ChatAction +from .messagedeleted import MessageDeleted +from .messageedited import MessageEdited +from .messageread import MessageRead +from .newmessage import NewMessage +from .userupdate import UserUpdate +from .callbackquery import CallbackQuery +from .inlinequery import InlineQuery + + +_HANDLERS_ATTRIBUTE = '__tl.handlers' + + +class StopPropagation(Exception): + """ + If this exception is raised in any of the handlers for a given event, + it will stop the execution of all other registered event handlers. + It can be seen as the ``StopIteration`` in a for loop but for events. + + Example usage: + + >>> from telethon import TelegramClient, events + >>> client = TelegramClient(...) + >>> + >>> @client.on(events.NewMessage) + ... async def delete(event): + ... await event.delete() + ... # No other event handler will have a chance to handle this event + ... raise StopPropagation + ... + >>> @client.on(events.NewMessage) + ... async def _(event): + ... # Will never be reached, because it is the second handler + ... pass + """ + # For some reason Sphinx wants the silly >>> or + # it will show warnings and look bad when generated. + pass + + +def register(event=None): + """ + Decorator method to *register* event handlers. This is the client-less + `add_event_handler() + ` variant. + + Note that this method only registers callbacks as handlers, + and does not attach them to any client. This is useful for + external modules that don't have access to the client, but + still want to define themselves as a handler. Example: + + >>> from telethon import events + >>> @events.register(events.NewMessage) + ... async def handler(event): + ... ... + ... + >>> # (somewhere else) + ... + >>> from telethon import TelegramClient + >>> client = TelegramClient(...) + >>> client.add_event_handler(handler) + + Remember that you can use this as a non-decorator + through ``register(event)(callback)``. + + Args: + event (`_EventBuilder` | `type`): + The event builder class or instance to be used, + for instance ``events.NewMessage``. + """ + if isinstance(event, type): + event = event() + elif not event: + event = Raw() + + def decorator(callback): + handlers = getattr(callback, _HANDLERS_ATTRIBUTE, []) + handlers.append(event) + setattr(callback, _HANDLERS_ATTRIBUTE, handlers) + return callback + + return decorator + + +def unregister(callback, event=None): + """ + Inverse operation of `register` (though not a decorator). Client-less + `remove_event_handler + ` + variant. **Note that this won't remove handlers from the client**, + because it simply can't, so you would generally use this before + adding the handlers to the client. + + This method is here for symmetry. You will rarely need to + unregister events, since you can simply just not add them + to any client. + + If no event is given, all events for this callback are removed. + Returns how many callbacks were removed. + """ + found = 0 + if event and not isinstance(event, type): + event = type(event) + + handlers = getattr(callback, _HANDLERS_ATTRIBUTE, []) + handlers.append((event, callback)) + i = len(handlers) + while i: + i -= 1 + ev = handlers[i] + if not event or isinstance(ev, event): + del handlers[i] + found += 1 + + return found + + +def is_handler(callback): + """ + Returns `True` if the given callback is an + event handler (i.e. you used `register` on it). + """ + return hasattr(callback, _HANDLERS_ATTRIBUTE) + + +def list(callback): + """ + Returns a list containing the registered event + builders inside the specified callback handler. + """ + return getattr(callback, _HANDLERS_ATTRIBUTE, [])[:] + + +def _get_handlers(callback): + """ + Like ``list`` but returns `None` if the callback was never registered. + """ + return getattr(callback, _HANDLERS_ATTRIBUTE, None) diff --git a/module/scripts/lib/telethon/events/__pycache__/__init__.cpython-314.pyc b/module/scripts/lib/telethon/events/__pycache__/__init__.cpython-314.pyc new file mode 100644 index 0000000000000000000000000000000000000000..594b4ef0bbbe503820bd9c05989b7a41abca9e94 GIT binary patch literal 5120 zcmb7IOKcm*8J^`sqL!j4TT-Mrjy--Uq9QHxP#VW@gvho4vPqPfmgPR2ELYqSwaIdq znHh>w+Jky&l@>t-q=y`G$f1Y&W}ug%hu(WLVii^wHe8^&^i(%ifgbz+v%55<*hM

Srngr$}Ms9~!JGXhE`Kwc(YXbC+8UXy_MLEjd?{j_<+Oz}rj+tIjSlmq}(r zPcG`2x1F#f8+BBeh2#1HYezl!byYBawP{n>Zh4==YE-++#6*>fF_sn3sd zrYT)x)$(kYaZ!tetzJkgmWM~6+9dB0sa1$fygw+D6-}lp37M`WfvZ%IzastSyWDSD zbqjg0{+J*{FRxI!>Ill#YpjXg6h64+2xe2qqn5WpeVoE*d^}z8IkjlrSz{h$P{}FX zk+ki&E){Tun#|T&t);eu)xa9{VGB+1t(j+=-YO0&Qeqi2&VHkyYQv9ZGgtHCbn}~?%Qn#7~%Ydy7uSRXQ z0uS0vIJa=FGXd$f+7<{@?P5gJ!t-zkET`y#A4znd7(*qjT5AjxR0D<87?pk`l;8i$p7~hLPSI;XBN;+nQJQD@|d@MI3^0t-& zP+^2X`nEby7~fhtTcv9jcR*DD5evQqw*V{;JtCsg_uP#RJ_*305RU~SQ4D=kIzTYB zeI}^qOA1B<=8M(}(BdnsjL7P61(vhYauK3*whk!fSuPFD0j(TJuzl}22?wCd)@r~w z?u7Oq?gXCElDd%SHEkdtM*%&+lGU+-xobcSkZ8q38Z?4H0KX>!Uubn|{Ee_}#2R`W zdhJK9SRHC1Kv#@P@W!^<^%R{DeuK3E^ciKYV1>@mcR*sbMJRwLpf7S3$@*K$o`?u6 z=z4`USYsKmM#*}^Z>boAK%;`5lAGsy({^mLx?JUctG-IB)%^k~OvDyZs8+#<=6GF% zsIA06C#vQ=LhFT=M^{vcqX7JbfPx7CqcQVKBX7byaog>q~Ti?l?-OZ0~ zne8$KmMoz4IhBy3!*_a* zebsyHuY)6>^?s7RpF4baaOku2R{GzmRC??|Kj|O2l^Ry3*E(1Y+n`S1b|}6VK?fj^tAEU5FD0%eBFm zyX^xaMOc=SoxM{_sSdU0kbgOP>6+T7iYx^zNrgi24?=FmXU~riM!Z00x)bZ*n_{2WAKNtb*sc0QV zObFTv-VvU}Mgw~JhCzRbCt${4`h-KZDgXSRKXcS*U`f%RCrAjvXhkBfa9cJeyN&Yfg}0YXV(qd>j!x zM2RjL@F^I(29Nj&RGZ`hCAopk`TM>7zxmOZ>EFsbBU4|EOx+zE-oE--b}RjL{;BQt zof9)Rmv84^+br#-`hW4x&)(U-dTaR1PHOyaCc9PoRc>?czMkF9^le@Kv}a4(&5eBe z!J{At&;=;# zf|st)5j!Hf*2mX7BCRg+7K1xlcw-E&h_Q4r$+J8?41ke=(Fw<3hB5&RdEKsvw=`KW z^g<%&U9|*86D9r|ni9G|lE)*tIwH;yIl}HbJmRpTeorU;L)&L}o_*nV_QhMN7r)JJ z2Vq}Q3m(tzBP^_F-Drhb@5j{+`xbTKdQ0KrA}z@0BWH^3KECwoQS^tj*AtGdGC3-m ztmdpZFsCE0NJfT!Zed~JMxx6Ci^yP?08)4?e_1gABTpY-{pQKr*;j5_f61P`l{))B zB!I^LB!yQxabEFU(%*7EWRNA{$#5&jdpM8$s=DBNkBp$hP8+hV@Z$GKAcnrcQSSU8 zM>!KRh*+tDzXEf%;1LiJfW*B_tks*RZ)abhzK%a7I4V6DXoP=o!Yj7T(OrXF)XCIO9F93pkd6A_wrcZ?+RJ0@xDx|opo97H z@G`>jH(8L2`Thuh=x~gULDo_Kbiy9I*C~Sz`sU+*FX7b>QwW6*%9xc}l>pSeG-Ooe zLYed7O=6bM!3s17QGx0~Qq#11WcVH#yGKsmBa`>Y#65Cawc!)n4|>Sx_$@ND*E6Q| mZ997eUVGmwXd`<+(2i+`zPX^KwU@pbOlz<04W_ivWc~|YbZ{pC literal 0 HcmV?d00001 diff --git a/module/scripts/lib/telethon/events/__pycache__/album.cpython-314.pyc b/module/scripts/lib/telethon/events/__pycache__/album.cpython-314.pyc new file mode 100644 index 0000000000000000000000000000000000000000..f71ff33e2a84a386a50349d0b9a2ddfb72bf1f60 GIT binary patch literal 16409 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 F{{_jNcxM0r literal 0 HcmV?d00001 diff --git a/module/scripts/lib/telethon/events/__pycache__/chataction.cpython-314.pyc b/module/scripts/lib/telethon/events/__pycache__/chataction.cpython-314.pyc new file mode 100644 index 0000000000000000000000000000000000000000..bae7e0d3969b663569f20c41bf0279d37c919c2b GIT binary patch literal 22291 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 literal 0 HcmV?d00001 diff --git a/module/scripts/lib/telethon/events/__pycache__/common.cpython-314.pyc b/module/scripts/lib/telethon/events/__pycache__/common.cpython-314.pyc new file mode 100644 index 0000000000000000000000000000000000000000..07f0448fe4d43ae8446c37b4170aee15ee10c86c GIT binary patch literal 9357 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 literal 0 HcmV?d00001 diff --git a/module/scripts/lib/telethon/events/__pycache__/inlinequery.cpython-314.pyc b/module/scripts/lib/telethon/events/__pycache__/inlinequery.cpython-314.pyc new file mode 100644 index 0000000000000000000000000000000000000000..dc66cac54d346c0c675ddbba18d33262a1824ce5 GIT binary patch literal 10903 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 Sz+;Jiabn+^FZm}9|9=2eS*SMv literal 0 HcmV?d00001 diff --git a/module/scripts/lib/telethon/events/__pycache__/messagedeleted.cpython-314.pyc b/module/scripts/lib/telethon/events/__pycache__/messagedeleted.cpython-314.pyc new file mode 100644 index 0000000000000000000000000000000000000000..84000c0bdbf4306b95d5e2c7916f261d89461882 GIT binary patch literal 3213 zcmb7GO>7&-6`tKCDVmg+IJF$tmF01aSRy5pZf&HN>?;0|8yje3gH2f!u)G>Aht$-| zU3z9Hl{fX&6zIV{wE+q=hhBQi(U$wB}i6lJ4_4#45= z%$xVV_r0H;xza>|!1&^?|N8m8JR$$YmvrQkhrtiwVVgX#>ed}{+tSu;TidrC?VKhL z?7Dp?b33cEr^%`_NzRbE^E#<#R;^1;eEL$NMCa<+v!tB+(t?Gu6OG^6kl@~s0P24)6eW>?2jm^xq2=_3uI3w0Y{=A+WPTq)M(%iQOhH<_Cuq;S{s zdQ#D?4ITh6Ng3_LQdB6~j3ujdCDfei4W?m&b>jjE1T&g`_rirY3zGYSt^0hQDiL^` zz6S@}l67b^i)Q&nU6{>j9q#|gAvct3p1IpIJo~9LWO!CTsTSmo*5h_|#Xd1>eh=6vb*ks_X z7Y4wGGRlA-p*LsVnxM7%p0XDL}maUFial?;TC$kwIRF>OI=PK!F=m*79^R1N7mL1&p%jO zqg@7B2H7luFo*&oHO7*J?8lz;0w0pv&?W+nnF7q9oCw-6(`lG# z$Gacg-JAUOewG|NH6YHh(mrt}O3(77bmI59KV^66^&NVBcjCs=i5rIxeLi_|ck4nKWakyvZ3(HgF9IHft{X$11WzR2IJbbm=bJ6atfx4@KdkA^q*frfjqafN9O)bU^1Ra6#Fx_qLp${V#MVv zPGy`bFhygo+YCKGP&CxT2YMrH$`iPceZeS@bNDn5Q`wSL_*2;O5+K;YB~F;8ss__H z2;{5GbT+@gL~=(5b|G6DIPkQOPtQ)|vU87%&k6i}jZ+L1HLR4!q8zjxxQ0tiwip5G zjco0kPH8Mjv=Se~^oa6TI>?AN}=1*7V}yj4Ms literal 0 HcmV?d00001 diff --git a/module/scripts/lib/telethon/events/__pycache__/messageedited.cpython-314.pyc b/module/scripts/lib/telethon/events/__pycache__/messageedited.cpython-314.pyc new file mode 100644 index 0000000000000000000000000000000000000000..5263dda38f7315e41216959c20f47c3af0b70cad GIT binary patch literal 2666 zcma)8&2Jk;6rZ(YJKZLbHcDEc$kVit*f=&)qe_k1Qd)wV2-3)us6|cIlijhsWIbbM zX6yRa6I{y)kScNJPvJ(zq0&MnBzoaQT33Q2@9nM=w|t0|{Pua~ef-|n%)a%~q)p&? z{Qd8rZWjpo75@yMe98>gfVoSyt-7^Ju3E+_BA;2!T+Ny+@R@pcb?j=+9hkrTK z?{e*?`td85{u%55!vNP7F;<VBQMK)AmG+ntzGy8k+ct&gnxJl*K?wp~*|%4fq4|F{V;em`2KOTE(>WuGSA&OYtm7NX~o~Ba7W;6tfp?xGfobo%Y(+#+$O&J8TmquNtH!-(dcq5gY zdv^{}(7@QieH;tq3@s(%2$6yiHImL<{&!jQD+Ra?G!RhIC?0EF`MVZfD3>8jmnq&1 zgOlZQY3Ork2VsJV8>71KGcexRhc^^Wz1>K>-TV&?j6VtJW+ME_KME&_^o7f@l!{Y; zNkffL0kVd?!U(_;mQZjGjS2;aI=4-d$wni#aGM1ouf&^yzoVGyV|lR!JV}}8Lt~2; zWg+w}Gro&Gg86Fmpo^xHjI0W6IP)shl|uU-OkJo^tM1s(ii4Ce7@133s9<_ z6!S>cF7-tWV@FBm0c^A1^OV6TLHB~8B*eHWz%+CKhLX+(VDw@O2pIrDjqH`Av*Kl# zD_44aaVL=O?(#D2LPSy|ZHNto0O@eO-REG5i)(m6`o@G*+_L-La7q6wl-=A>)Jlz}zLb@O+P-P3u->$Zw2lq>CfF!`o&k z4(~IWrBr)9UG+{7a)NA0GdG`H4E`N;O6+}bcGxkBjC zk&n9=7UnrkLv;LL=}K}w4tH`ZcmPL*PI^m?MOin{Q9g`qM0tsWqN9T5LDTU)jbXnD zPjsgSPFM~5wDxulPOU0y^=nNZPN!P6#XG+6YfKowrPyuX^cPvTThp#$#%qsvQIBxh|qiZluwP9n`lD4U($Z)*5SMBzr+)n5RUNoJCkn~eEUR(rp`ZAEa z5sm$i!T{6VaVOV-nv{xUuL)wwUveZBeZ_BY!< zPFH@Is65Hx_5iF97s>D$_Q7BlRz_~>GbW3-bbP;NUxXE>_dBv)%0=Uj6MrH&k?lB1 z;EoebIL>Dw3(}pJ9jEClZ316#Az^#QaiF$via5*|&;`y#|c@Q`beA!HnECOK6)TIO&e1;K)1SdkD1@h+?kh~{jr*q?bYZl2L z8Dz4##X%OhJ>;I*R&Mg1{cwstn4(ar+$0n#H<^TjhSp1kD37`~n*zXIeipI|;slTY+0A)m~OF*D8Eb9?D{fNvxBC~sWk~#V;vuI7;TN@B~0lv)9 Jr??OU`3q1p*aH9n literal 0 HcmV?d00001 diff --git a/module/scripts/lib/telethon/events/__pycache__/messageread.cpython-314.pyc b/module/scripts/lib/telethon/events/__pycache__/messageread.cpython-314.pyc new file mode 100644 index 0000000000000000000000000000000000000000..62bfa51961c9c114135eaaa74ca3aac536cf3f86 GIT binary patch literal 7816 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%0A+M zllnOIiT3+ffBWjasvp3h2RIlqwz9TIZqgK;o%pqyZ|q)d?4I!~gKeFjJB=d_;)sLG zwM}ihA^ZS6_{Djh$dCwUe8awMgA(WXnr335i5L}Kah`NSjsm-j)Z!?kmH?=ve7d>=k$ya$IK7JhJ5vX%i**FXf-!r~m)} literal 0 HcmV?d00001 diff --git a/module/scripts/lib/telethon/events/__pycache__/newmessage.cpython-314.pyc b/module/scripts/lib/telethon/events/__pycache__/newmessage.cpython-314.pyc new file mode 100644 index 0000000000000000000000000000000000000000..69d0f109874e36ee069a97eb7f314a9531be9267 GIT binary patch literal 12509 zcmdryZEO@rmeuoN&)75eU>j_(joTj?dj=R2AOwi*0O7+B+mOK-UoIvy?U}aS$;|Ym zy9eVO>{{6$XYcI>kXDDu zbnjJl_t?Y0=Ht)Zm1e5z^VO?YuU@@+ueMZp%Ls&fzx%J(f7eRLe_+N8B}QW9HAqa6 z*GQNQm>0|AHbIebP;YX`GZG9fB@InBKwr&4lS_oaoD z7?sj#QHhG!O7}oclcQ21s)|~G)16sON~)O9CNrWMHwK}fN+YxK9wY=Ja8u+o(YP?t zY_GXA`vxL&VNP>|tHSoMBkT;@!X@x8g})2_ZupmlZJnhNvXcv!*OH_wX-jgNljc2` z*Gf*8hO5I~NORWoT(rJ|*1IVoDuEEb(`68og{loah%- zRTvY6r(A@ToruS?ipo!ni)om-!V7#_oZvO_tj4EUJ6^_8GczG+ibf$!Q@OU=6U*u~8nV%4;$o8&t9)HcBTa z`C!oO9SoY|0cKJ%$&ZNqxR6dH#YBh>gHZajT2@FxKk;llnNd?X-AD8nZ!=2$Wyd}=CWB(%;n=>hBgL1yfn&B%2|FwNNaosm`TvY)e8@c zi3oR6z!oGrK!}g!i4fzHqNY+%Y(|KUg&)j|q8FsAuUK$WvLXf)hRpOjS1WSmi1nGlo&uEFDZeP~<+^nui@F#yRUHZEtAiB-If zJ1s41A^zYbpAbidY*O25p*tc2Ir30oH$Yn@B_#6z1x3USn9L?%pWrT&w070lV-_4B zLvk``LV9$6<@+5GlByVEyT?S@0;3@_;DABE_zB?H9?J+CFj^WYrD_WBVJr?iR~Sj+ z-jAJRmBl%j$%;arO9@(h9L0>m&^F{b_vPp4ptS)pBEhlI^FFZ z&r5-EF`1M@TiOEwYXmwXGOi)36$WFK;79aY`LL*FWME(G%u(f(h?`RtCRM(r7a_Os z$lzE3D-5e60xMbrAsAsokTjkm8B)YdahF zMrL57FxESyCPgvR@l3HM#^SQkqvpVDl|m@*K;PNQzsiV;$y7*6oDP%?=#?W$A^rvm zzbKW9M|1~ryzVm3E!|}jR4>a5X5D4VNWIkHp194>mP+AU4u3l#u9crU2~UIeT+ld!G7%8g%pe+4L^kM!Hcu_3I! z0H=Wr0|XYr4ztBLHw?39;gmZL_5tjuU zMg`bFR~GV6tQMetHjtsIRsD|-H|5)hOCG^DR0y*eMkcn_IMvuI;wsj54ff^3&F1Lg zQWFZSj#+5(%G(GJc*RvqBIIO?Rc$4p6y{bKpp;&T)|Nx*rnM!QgHqs^!T_Zeg=J4@ z+xBSNovu}!4Ro<+w1%^RDp1Eln6whc=Y^%f_5a5yV6JiD%mVI#xUqdH5pWysg#XGB zh+1Ijr^p~VQcK92>%!zX_Z(-Dk+O6XiC85;a+XZN9rneIgpm=*`HWrSZg2xP$PJrr z3vJiAvnp;(Kd)yy6M`mmqK4bKy%V&7kT5pc2|BH)c8118xL8S@pesvbif{&W{%t}g z)2YT4RE9buhqfIA8J*2^rsPC6DPo0`(bUeQG}39F`*RUEFcx`b)EI7aNz?bu6sw zn1A#AXYQ=)SlV&q8x?oL+ZD~+C0{dlWbmzlLo%wVWFEeN`M>%I%;G>tzuknH3K!imd2PHJbq&h09z41x2iadtX6ggW`ioFOyVM zNriM=)a{_e>NZ8xOAQrLX~1@P3g|9VN!3fo1r=0EMRyMZMU0{7dCutPj;3We-^KVF zfim5pipfzWfM6^N7sG=M;*Lh+NkLVkQ5A)p-`~c<6r&8Xie6oWx1NIv^`Bs~eLY3K zDj}YlIajX5zu@st_g*}k^RB<`ZJr;$?cbfNZ~eu&nchcBKJ)ti?$o01=*>OAgG;`n z7fxL`_0!b_%ig26y+`j>HQXoM=40Fn;i`{wkoF$u9+nVy#Z2j+iHGSykeV?tX zd#`tXXt`nA?S^f+`j(&f-SyU79GV?l_WEyo{U7YPdGuEQNBzrN4&L5!5bEc;uXJO- zdsX!d4SSZV_AYq#!u&Ti{zEmeN@ID>2*&U?i>uU$KT_597mt>i~ZtT-`q zVs>!u^p(@s4}Eyz#)*#yfBnj@UU}dIwgTR2ET5}>uX@_?PQ|-4p8eqf0qUl5gt!8$ zMJx>tGswk1p&A?{x;VCr@+amg8|X8jtwcCe)*b~N?sX8TXSpeRpR){v>2O|~@tw4^ z83fLPsRb1F1gVXJnFc(iN=aV_{Jkh%=`Ia86dr0}8!4hHC&9Xf(CH2e*p)3*Kn)>1ShAWcK9`J?zStn`h4~Rrr@({sou+F2D79F_t|X6X2n>H~J}@44q4mm47QFWdpn@vk08{f6Tpjw4CU zfX6VPFt&f{=5jCiA^u&MT=z~Q^e)2p(<>gMIh%E?E zYN^usDk!xS=ryH9lxb*NM1@|3O%YX)VuRAWAeeOe7QKti#aef|MuYbLa=`5gqd+Yl zsxo)7t!P_+(Na9A3qqKC*5C${B2VO;U_p1t{=ep&Z@cy= z0Skgj&s7{2Drf>(%AY7L3xe7Ee=+CSigON1tblzWP@&twsG+-4XkiEYR6_Sq$44}q z2JJPWm#CnagRjUYCBVEPg6{|u)cS!5L&4S^pwo+B37JSlu}&{LBMDJ3{a|YsHC;tD zS$E+enk1@vnWiP9p#Dj6B2b~=NuxZ2(Qb^M#b^&k`!L#z5$aG%7e?I}^A%cT>5VO_z^fI-c9a zFK-GiZVKiaTbCQRFE(z^)iz!}f9ZU#dgJB2m-gPRYn-ciw_^UxwI5ym(Nf)RBY)F> zD|jn-GkA646aQ~RzX>hXy_BolJXdk0BDcQ&@~f9#&24O&Yr4{ut8ZSe-?~`8)yx+Z zKWwaQ0)?+~eI+2@Yak!6aPL~=yOzD(x4qpz0QVxe7S8I2b>PV}Z?yE2f$i({ZhSt5v2V2d>rbd%|C% zk`+8pm}QYxrf6T)uJ}xgscD9axo3iq#Om1=IjBT9?jK9VVyYM^rnc>c z5!Z}VL|wD!y5CS5@q%9v9?zdgahWE;>4oC~POuK`HcdWz)AD=+k1=$Y=x zRo6}TeGa+q3*FP*voGVtb-6JFzlFxor=BWoNUP@8Uu(SDxZJ!0ehbYz%&M90+vRO{ zE2}Ray4-)Mf2p$N{lxWWKJ32Hy}aeX;+6wT{)2Za4}#3aA)cRk{&u-PS6Or6{LJ~~ zO8;V||9av>`G$O_vfF6Vb)jpzD_2u@x$jb+IpXfc=H1K9T?@@!AGa*kbWb1tthx@R zcuV^oPt(21nwg22AI*wOm2C^IHYP)0j#e<`z{&s=32Wbf{4)RkgTB|W(f1ncZHJw` z?Fc(+iB^iwSTt9-M03N7jWYUTqa2^FtU06T0W7S*T9k#w3|vS-X<%M!W<>+fJ$t`l z=pN{^2R|dLL8P>4Ov)J24D^+}vmbowv@^cLF`lWQx9^k)ZzsMJ zDbUYvVa4VOUTox(GlE1t@Zip-6sMCwcdMMhCp|da8X*CTAdkErTSHqUoeHlM&_fHA zz(_oGGo!^3%W%L9SOvkO9g7*z;Dw9nEYH8?>4hbMrwV8eW}a?%mw^BT-tf+M81Erq zC1t}qPDK}>V@ZQj`krj37y46yFvfZ8tw{>DRMT`@$^~>S(qT@*XV7fAZqPs82e{Ppxzcn zjm#3s`9zpQOG5q%OD&S%GltW=h+(YwWp&s(o1q>(4F$nG+rzfJ$2JUScita6!bjPz9%-!$wJ*s-+m5eUoFCc+SI1FDFsNw_ldBq2OYDMjws7{?kW+(_lzGC*O zFfkuTqDod=xeeQpVGJ9j$J3S7^Tp^Mp#lrvF8?Kp4ppt@X1p3uZXJNlkL-sw zkX3{=JRS~w>a07#L7Ei}k4K)sMu*1`*s4VhpSnPRz}2;vyDxRm*DhBF?o0(6x$|QAR|Gx}F=B*5d;;YPQWuFQ!!)uX%(Sq}c{4v6mCIga}y pasMajUn2c~A~k;^8{pisRXwyt?A*?a4J!n{fU~XY3tB|6`yW$Kw5$LC literal 0 HcmV?d00001 diff --git a/module/scripts/lib/telethon/events/__pycache__/raw.cpython-314.pyc b/module/scripts/lib/telethon/events/__pycache__/raw.cpython-314.pyc new file mode 100644 index 0000000000000000000000000000000000000000..075caa2f7aeef7d94acb7eae86116e377519a230 GIT binary patch literal 3008 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 _IGNORE_MAX_SIZE: + for i in [i for i, t in _IGNORE_DICT.items() if now - t > _IGNORE_MAX_AGE]: + del _IGNORE_DICT[i] + + # Add the other updates to the ignore list + for u in others: + if u is not update: + _IGNORE_DICT[id(u)] = now + + # Figure out which updates share the same group and use those + return cls.Event([ + u.message for u in others + if (isinstance(u, (types.UpdateNewMessage, types.UpdateNewChannelMessage)) + and isinstance(u.message, types.Message) + and u.message.grouped_id == group) + ]) + + def filter(self, event): + # Albums with less than two messages require a few hacks to work. + if len(event.messages) > 1: + return super().filter(event) + + class Event(EventCommon, SenderGetter): + """ + Represents the event of a new album. + + Members: + messages (Sequence[`Message `]): + The list of messages belonging to the same album. + """ + def __init__(self, messages): + message = messages[0] + super().__init__(chat_peer=message.peer_id, + msg_id=message.id, broadcast=bool(message.post)) + SenderGetter.__init__(self, message.sender_id) + self.messages = messages + + def _set_client(self, client): + super()._set_client(client) + self._sender, self._input_sender = utils._get_entity_pair( + self.sender_id, self._entities, client._mb_entity_cache) + + for msg in self.messages: + msg._finish_init(client, self._entities, None) + + if len(self.messages) == 1: + # This will require hacks to be a proper album event + hack = client._albums.get(self.grouped_id) + if hack is None: + client._albums[self.grouped_id] = AlbumHack(client, self) + else: + hack.extend(self.messages) + + @property + def grouped_id(self): + """ + The shared ``grouped_id`` between all the messages. + """ + return self.messages[0].grouped_id + + @property + def text(self): + """ + The message text of the first photo with a caption, + formatted using the client's default parse mode. + """ + return next((m.text for m in self.messages if m.text), '') + + @property + def raw_text(self): + """ + The raw message text of the first photo + with a caption, ignoring any formatting. + """ + return next((m.raw_text for m in self.messages if m.raw_text), '') + + @property + def is_reply(self): + """ + `True` if the album is a reply to some other message. + + Remember that you can access the ID of the message + this one is replying to through `reply_to_msg_id`, + and the `Message` object with `get_reply_message()`. + """ + # Each individual message in an album all reply to the same message + return self.messages[0].is_reply + + @property + def forward(self): + """ + The `Forward ` + information for the first message in the album if it was forwarded. + """ + # Each individual message in an album all reply to the same message + return self.messages[0].forward + + # endregion Public Properties + + # region Public Methods + + async def get_reply_message(self): + """ + The `Message ` + that this album is replying to, or `None`. + + The result will be cached after its first use. + """ + return await self.messages[0].get_reply_message() + + async def respond(self, *args, **kwargs): + """ + Responds to the album (not as a reply). Shorthand for + `telethon.client.messages.MessageMethods.send_message` + with ``entity`` already set. + """ + return await self.messages[0].respond(*args, **kwargs) + + async def reply(self, *args, **kwargs): + """ + Replies to the first photo in the album (as a reply). Shorthand + for `telethon.client.messages.MessageMethods.send_message` + with both ``entity`` and ``reply_to`` already set. + """ + return await self.messages[0].reply(*args, **kwargs) + + async def forward_to(self, *args, **kwargs): + """ + Forwards the entire album. Shorthand for + `telethon.client.messages.MessageMethods.forward_messages` + with both ``messages`` and ``from_peer`` already set. + """ + if self._client: + kwargs['messages'] = self.messages + kwargs['from_peer'] = await self.get_input_chat() + return await self._client.forward_messages(*args, **kwargs) + + async def edit(self, *args, **kwargs): + """ + Edits the first caption or the message, or the first messages' + caption if no caption is set, iff it's outgoing. Shorthand for + `telethon.client.messages.MessageMethods.edit_message` + with both ``entity`` and ``message`` already set. + + Returns `None` if the message was incoming, + or the edited `Message` otherwise. + + .. note:: + + This is different from `client.edit_message + ` + and **will respect** the previous state of the message. + For example, if the message didn't have a link preview, + the edit won't add one by default, and you should force + it by setting it to `True` if you want it. + + This is generally the most desired and convenient behaviour, + and will work for link previews and message buttons. + """ + for msg in self.messages: + if msg.raw_text: + return await msg.edit(*args, **kwargs) + + return await self.messages[0].edit(*args, **kwargs) + + async def delete(self, *args, **kwargs): + """ + Deletes the entire album. You're responsible for checking whether + you have the permission to do so, or to except the error otherwise. + Shorthand for + `telethon.client.messages.MessageMethods.delete_messages` with + ``entity`` and ``message_ids`` already set. + """ + if self._client: + return await self._client.delete_messages( + await self.get_input_chat(), self.messages, + *args, **kwargs + ) + + async def mark_read(self): + """ + Marks the entire album as read. Shorthand for + `client.send_read_acknowledge() + ` + with both ``entity`` and ``message`` already set. + """ + if self._client: + await self._client.send_read_acknowledge( + await self.get_input_chat(), max_id=self.messages[-1].id) + + async def pin(self, *, notify=False): + """ + Pins the first photo in the album. Shorthand for + `telethon.client.messages.MessageMethods.pin_message` + with both ``entity`` and ``message`` already set. + """ + return await self.messages[0].pin(notify=notify) + + def __len__(self): + """ + Return the amount of messages in the album. + + Equivalent to ``len(self.messages)``. + """ + return len(self.messages) + + def __iter__(self): + """ + Iterate over the messages in the album. + + Equivalent to ``iter(self.messages)``. + """ + return iter(self.messages) + + def __getitem__(self, n): + """ + Access the n'th message in the album. + + Equivalent to ``event.messages[n]``. + """ + return self.messages[n] diff --git a/module/scripts/lib/telethon/events/callbackquery.py b/module/scripts/lib/telethon/events/callbackquery.py new file mode 100644 index 0000000..29ffd05 --- /dev/null +++ b/module/scripts/lib/telethon/events/callbackquery.py @@ -0,0 +1,346 @@ +import re +import struct + +from .common import EventBuilder, EventCommon, name_inner_event +from .. import utils +from ..tl import types, functions +from ..tl.custom.sendergetter import SenderGetter + + +@name_inner_event +class CallbackQuery(EventBuilder): + """ + Occurs whenever you sign in as a bot and a user + clicks one of the inline buttons on your messages. + + Note that the `chats` parameter will **not** work with normal + IDs or peers if the clicked inline button comes from a "via bot" + message. The `chats` parameter also supports checking against the + `chat_instance` which should be used for inline callbacks. + + Args: + data (`bytes`, `str`, `callable`, optional): + If set, the inline button payload data must match this data. + A UTF-8 string can also be given, a regex or a callable. For + instance, to check against ``'data_1'`` and ``'data_2'`` you + can use ``re.compile(b'data_')``. + + pattern (`bytes`, `str`, `callable`, `Pattern`, optional): + If set, only buttons with payload matching this pattern will be handled. + You can specify a regex-like string which will be matched + against the payload data, a callable function that returns `True` + if a the payload data is acceptable, or a compiled regex pattern. + + Example + .. code-block:: python + + from telethon import events, Button + + # Handle all callback queries and check data inside the handler + @client.on(events.CallbackQuery) + async def handler(event): + if event.data == b'yes': + await event.answer('Correct answer!') + + # Handle only callback queries with data being b'no' + @client.on(events.CallbackQuery(data=b'no')) + async def handler(event): + # Pop-up message with alert + await event.answer('Wrong answer!', alert=True) + + # Send a message with buttons users can click + async def main(): + await client.send_message(user, 'Yes or no?', buttons=[ + Button.inline('Yes!', b'yes'), + Button.inline('Nope', b'no') + ]) + """ + def __init__( + self, chats=None, *, blacklist_chats=False, func=None, data=None, pattern=None): + super().__init__(chats, blacklist_chats=blacklist_chats, func=func) + + if data and pattern: + raise ValueError("Only pass either data or pattern not both.") + + if isinstance(data, str): + data = data.encode('utf-8') + if isinstance(pattern, str): + pattern = pattern.encode('utf-8') + + match = data if data else pattern + + if isinstance(match, bytes): + self.match = data if data else re.compile(pattern).match + elif not match or callable(match): + self.match = match + elif hasattr(match, 'match') and callable(match.match): + if not isinstance(getattr(match, 'pattern', b''), bytes): + match = re.compile(match.pattern.encode('utf-8'), + match.flags & (~re.UNICODE)) + + self.match = match.match + else: + raise TypeError('Invalid data or pattern type given') + + self._no_check = all(x is None for x in ( + self.chats, self.func, self.match, + )) + + @classmethod + def build(cls, update, others=None, self_id=None): + if isinstance(update, types.UpdateBotCallbackQuery): + return cls.Event(update, update.peer, update.msg_id) + elif isinstance(update, types.UpdateInlineBotCallbackQuery): + # See https://github.com/LonamiWebs/Telethon/pull/1005 + # The long message ID is actually just msg_id + peer_id + mid, pid = struct.unpack('`, + since the message object is normally not present. + """ + self._client.loop.create_task(self.answer()) + if isinstance(self.query.msg_id, (types.InputBotInlineMessageID, types.InputBotInlineMessageID64)): + return await self._client.edit_message( + self.query.msg_id, *args, **kwargs + ) + else: + return await self._client.edit_message( + await self.get_input_chat(), self.query.msg_id, + *args, **kwargs + ) + + async def delete(self, *args, **kwargs): + """ + Deletes the message. Shorthand for + `telethon.client.messages.MessageMethods.delete_messages` with + ``entity`` and ``message_ids`` already set. + + If you need to delete more than one message at once, don't use + this `delete` method. Use a + `telethon.client.telegramclient.TelegramClient` instance directly. + + This method also creates a task to `answer` the callback. + + This method will likely fail if `via_inline` is `True`. + """ + self._client.loop.create_task(self.answer()) + if isinstance(self.query.msg_id, (types.InputBotInlineMessageID, types.InputBotInlineMessageID64)): + raise TypeError('Inline messages cannot be deleted as there is no API request available to do so') + return await self._client.delete_messages( + await self.get_input_chat(), [self.query.msg_id], + *args, **kwargs + ) diff --git a/module/scripts/lib/telethon/events/chataction.py b/module/scripts/lib/telethon/events/chataction.py new file mode 100644 index 0000000..b8fc3dc --- /dev/null +++ b/module/scripts/lib/telethon/events/chataction.py @@ -0,0 +1,458 @@ +from .common import EventBuilder, EventCommon, name_inner_event +from .. import utils +from ..tl import types + + +@name_inner_event +class ChatAction(EventBuilder): + """ + Occurs on certain chat actions: + + * Whenever a new chat is created. + * Whenever a chat's title or photo is changed or removed. + * Whenever a new message is pinned. + * Whenever a user scores in a game. + * Whenever a user joins or is added to the group. + * Whenever a user is removed or leaves a group if it has + less than 50 members or the removed user was a bot. + + Note that "chat" refers to "small group, megagroup and broadcast + channel", whereas "group" refers to "small group and megagroup" only. + + Example + .. code-block:: python + + from telethon import events + + @client.on(events.ChatAction) + async def handler(event): + # Welcome every new user + if event.user_joined: + await event.reply('Welcome to the group!') + """ + + @classmethod + def build(cls, update, others=None, self_id=None): + # Rely on specific pin updates for unpins, but otherwise ignore them + # for new pins (we'd rather handle the new service message with pin, + # so that we can act on that message'). + if isinstance(update, types.UpdatePinnedChannelMessages) and not update.pinned: + return cls.Event(types.PeerChannel(update.channel_id), + pin_ids=update.messages, + pin=update.pinned) + + elif isinstance(update, types.UpdatePinnedMessages) and not update.pinned: + return cls.Event(update.peer, + pin_ids=update.messages, + pin=update.pinned) + + elif isinstance(update, types.UpdateChatParticipantAdd): + return cls.Event(types.PeerChat(update.chat_id), + added_by=update.inviter_id or True, + users=update.user_id) + + elif isinstance(update, types.UpdateChatParticipantDelete): + return cls.Event(types.PeerChat(update.chat_id), + kicked_by=True, + users=update.user_id) + + # UpdateChannel is sent if we leave a channel, and the update._entities + # set by _process_update would let us make some guesses. However it's + # better not to rely on this. Rely only in MessageActionChatDeleteUser. + + elif (isinstance(update, ( + types.UpdateNewMessage, types.UpdateNewChannelMessage)) + and isinstance(update.message, types.MessageService)): + msg = update.message + action = update.message.action + if isinstance(action, types.MessageActionChatJoinedByLink): + return cls.Event(msg, + added_by=True, + users=msg.from_id) + elif isinstance(action, types.MessageActionChatAddUser): + # If a user adds itself, it means they joined via the public chat username + added_by = ([msg.sender_id] == action.users) or msg.from_id + return cls.Event(msg, + added_by=added_by, + users=action.users) + elif isinstance(action, types.MessageActionChatDeleteUser): + return cls.Event(msg, + kicked_by=utils.get_peer_id(msg.from_id) if msg.from_id else True, + users=action.user_id) + elif isinstance(action, types.MessageActionChatCreate): + return cls.Event(msg, + users=action.users, + created=True, + new_title=action.title) + elif isinstance(action, types.MessageActionChannelCreate): + return cls.Event(msg, + created=True, + users=msg.from_id, + new_title=action.title) + elif isinstance(action, types.MessageActionChatEditTitle): + return cls.Event(msg, + users=msg.from_id, + new_title=action.title) + elif isinstance(action, types.MessageActionChatEditPhoto): + return cls.Event(msg, + users=msg.from_id, + new_photo=action.photo) + elif isinstance(action, types.MessageActionChatDeletePhoto): + return cls.Event(msg, + users=msg.from_id, + new_photo=True) + elif isinstance(action, types.MessageActionPinMessage) and msg.reply_to: + return cls.Event(msg, + pin_ids=[msg.reply_to_msg_id]) + elif isinstance(action, types.MessageActionGameScore): + return cls.Event(msg, + new_score=action.score) + + elif isinstance(update, types.UpdateChannelParticipant) \ + and bool(update.new_participant) != bool(update.prev_participant): + # If members are hidden, bots will receive this update instead, + # as there won't be a service message. Promotions and demotions + # seem to have both new and prev participant, which are ignored + # by this event. + return cls.Event(types.PeerChannel(update.channel_id), + users=update.user_id, + added_by=update.actor_id if update.new_participant else None, + kicked_by=update.actor_id if update.prev_participant else None) + + class Event(EventCommon): + """ + Represents the event of a new chat action. + + Members: + action_message (`MessageAction `_): + The message invoked by this Chat Action. + + new_pin (`bool`): + `True` if there is a new pin. + + new_photo (`bool`): + `True` if there's a new chat photo (or it was removed). + + photo (:tl:`Photo`, optional): + The new photo (or `None` if it was removed). + + user_added (`bool`): + `True` if the user was added by some other. + + user_joined (`bool`): + `True` if the user joined on their own. + + user_left (`bool`): + `True` if the user left on their own. + + user_kicked (`bool`): + `True` if the user was kicked by some other. + + created (`bool`, optional): + `True` if this chat was just created. + + new_title (`str`, optional): + The new title string for the chat, if applicable. + + new_score (`str`, optional): + The new score string for the game, if applicable. + + unpin (`bool`): + `True` if the existing pin gets unpinned. + """ + + def __init__(self, where, new_photo=None, + added_by=None, kicked_by=None, created=None, + users=None, new_title=None, pin_ids=None, pin=None, new_score=None): + if isinstance(where, types.MessageService): + self.action_message = where + where = where.peer_id + else: + self.action_message = None + + # TODO needs some testing (can there be more than one id, and do they follow pin order?) + # same in get_pinned_message + super().__init__(chat_peer=where, msg_id=pin_ids[0] if pin_ids else None) + + self.new_pin = pin_ids is not None + self._pin_ids = pin_ids + self._pinned_messages = None + + self.new_photo = new_photo is not None + self.photo = \ + new_photo if isinstance(new_photo, types.Photo) else None + + self._added_by = None + self._kicked_by = None + self.user_added = self.user_joined = self.user_left = \ + self.user_kicked = self.unpin = False + + if added_by is True: + self.user_joined = True + elif added_by: + self.user_added = True + self._added_by = added_by + + # If `from_id` was not present (it's `True`) or the affected + # user was "kicked by itself", then it left. Else it was kicked. + if kicked_by is True or (users is not None and kicked_by == users): + self.user_left = True + elif kicked_by: + self.user_kicked = True + self._kicked_by = kicked_by + + self.created = bool(created) + + if isinstance(users, list): + self._user_ids = [utils.get_peer_id(u) for u in users] + elif users: + self._user_ids = [utils.get_peer_id(users)] + else: + self._user_ids = [] + + self._users = None + self._input_users = None + self.new_title = new_title + self.new_score = new_score + self.unpin = not pin + + def _set_client(self, client): + super()._set_client(client) + if self.action_message: + self.action_message._finish_init(client, self._entities, None) + + async def respond(self, *args, **kwargs): + """ + Responds to the chat action message (not as a reply). Shorthand for + `telethon.client.messages.MessageMethods.send_message` with + ``entity`` already set. + """ + return await self._client.send_message( + await self.get_input_chat(), *args, **kwargs) + + async def reply(self, *args, **kwargs): + """ + Replies to the chat action message (as a reply). Shorthand for + `telethon.client.messages.MessageMethods.send_message` with + both ``entity`` and ``reply_to`` already set. + + Has the same effect as `respond` if there is no message. + """ + if not self.action_message: + return await self.respond(*args, **kwargs) + + kwargs['reply_to'] = self.action_message.id + return await self._client.send_message( + await self.get_input_chat(), *args, **kwargs) + + async def delete(self, *args, **kwargs): + """ + Deletes the chat action message. You're responsible for checking + whether you have the permission to do so, or to except the error + otherwise. Shorthand for + `telethon.client.messages.MessageMethods.delete_messages` with + ``entity`` and ``message_ids`` already set. + + Does nothing if no message action triggered this event. + """ + if not self.action_message: + return + + return await self._client.delete_messages( + await self.get_input_chat(), [self.action_message], + *args, **kwargs + ) + + async def get_pinned_message(self): + """ + If ``new_pin`` is `True`, this returns the `Message + ` object that was pinned. + """ + if self._pinned_messages is None: + await self.get_pinned_messages() + + if self._pinned_messages: + return self._pinned_messages[0] + + async def get_pinned_messages(self): + """ + If ``new_pin`` is `True`, this returns a `list` of `Message + ` objects that were pinned. + """ + if not self._pin_ids: + return self._pin_ids # either None or empty list + + chat = await self.get_input_chat() + if chat: + self._pinned_messages = await self._client.get_messages( + self._input_chat, ids=self._pin_ids) + + return self._pinned_messages + + @property + def added_by(self): + """ + The user who added ``users``, if applicable (`None` otherwise). + """ + if self._added_by and not isinstance(self._added_by, types.User): + aby = self._entities.get(utils.get_peer_id(self._added_by)) + if aby: + self._added_by = aby + + return self._added_by + + async def get_added_by(self): + """ + Returns `added_by` but will make an API call if necessary. + """ + if not self.added_by and self._added_by: + self._added_by = await self._client.get_entity(self._added_by) + + return self._added_by + + @property + def kicked_by(self): + """ + The user who kicked ``users``, if applicable (`None` otherwise). + """ + if self._kicked_by and not isinstance(self._kicked_by, types.User): + kby = self._entities.get(utils.get_peer_id(self._kicked_by)) + if kby: + self._kicked_by = kby + + return self._kicked_by + + async def get_kicked_by(self): + """ + Returns `kicked_by` but will make an API call if necessary. + """ + if not self.kicked_by and self._kicked_by: + self._kicked_by = await self._client.get_entity(self._kicked_by) + + return self._kicked_by + + @property + def user(self): + """ + The first user that takes part in this action. For example, who joined. + + Might be `None` if the information can't be retrieved or + there is no user taking part. + """ + if self.users: + return self._users[0] + + async def get_user(self): + """ + Returns `user` but will make an API call if necessary. + """ + if self.users or await self.get_users(): + return self._users[0] + + @property + def input_user(self): + """ + Input version of the ``self.user`` property. + """ + if self.input_users: + return self._input_users[0] + + async def get_input_user(self): + """ + Returns `input_user` but will make an API call if necessary. + """ + if self.input_users or await self.get_input_users(): + return self._input_users[0] + + @property + def user_id(self): + """ + Returns the marked signed ID of the first user, if any. + """ + if self._user_ids: + return self._user_ids[0] + + @property + def users(self): + """ + A list of users that take part in this action. For example, who joined. + + Might be empty if the information can't be retrieved or there + are no users taking part. + """ + if not self._user_ids: + return [] + + if self._users is None: + self._users = [ + self._entities[user_id] + for user_id in self._user_ids + if user_id in self._entities + ] + + return self._users + + async def get_users(self): + """ + Returns `users` but will make an API call if necessary. + """ + if not self._user_ids: + return [] + + # Note: we access the property first so that it fills if needed + if (self.users is None or len(self._users) != len(self._user_ids)) and self.action_message: + await self.action_message._reload_message() + self._users = [ + u for u in self.action_message.action_entities + if isinstance(u, (types.User, types.UserEmpty))] + + return self._users + + @property + def input_users(self): + """ + Input version of the ``self.users`` property. + """ + if self._input_users is None and self._user_ids: + self._input_users = [] + for user_id in self._user_ids: + # First try to get it from our entities + try: + self._input_users.append(utils.get_input_peer(self._entities[user_id])) + continue + except (KeyError, TypeError): + pass + + # If missing, try from the entity cache + try: + self._input_users.append(self._client._mb_entity_cache.get( + utils.resolve_id(user_id)[0])._as_input_peer()) + continue + except AttributeError: + pass + + return self._input_users or [] + + async def get_input_users(self): + """ + Returns `input_users` but will make an API call if necessary. + """ + if not self._user_ids: + return [] + + # Note: we access the property first so that it fills if needed + if (self.input_users is None or len(self._input_users) != len(self._user_ids)) and self.action_message: + self._input_users = [ + utils.get_input_peer(u) + for u in self.action_message.action_entities + if isinstance(u, (types.User, types.UserEmpty))] + + return self._input_users or [] + + @property + def user_ids(self): + """ + Returns the marked signed ID of the users, if any. + """ + if self._user_ids: + return self._user_ids[:] diff --git a/module/scripts/lib/telethon/events/common.py b/module/scripts/lib/telethon/events/common.py new file mode 100644 index 0000000..e295adb --- /dev/null +++ b/module/scripts/lib/telethon/events/common.py @@ -0,0 +1,186 @@ +import abc +import asyncio +import warnings + +from .. import utils +from ..tl import TLObject, types +from ..tl.custom.chatgetter import ChatGetter + + +async def _into_id_set(client, chats): + """Helper util to turn the input chat or chats into a set of IDs.""" + if chats is None: + return None + + if not utils.is_list_like(chats): + chats = (chats,) + + result = set() + for chat in chats: + if isinstance(chat, int): + if chat < 0: + result.add(chat) # Explicitly marked IDs are negative + else: + result.update({ # Support all valid types of peers + utils.get_peer_id(types.PeerUser(chat)), + utils.get_peer_id(types.PeerChat(chat)), + utils.get_peer_id(types.PeerChannel(chat)), + }) + elif isinstance(chat, TLObject) and chat.SUBCLASS_OF_ID == 0x2d45687: + # 0x2d45687 == crc32(b'Peer') + result.add(utils.get_peer_id(chat)) + else: + chat = await client.get_input_entity(chat) + if isinstance(chat, types.InputPeerSelf): + chat = await client.get_me(input_peer=True) + result.add(utils.get_peer_id(chat)) + + return result + + +class EventBuilder(abc.ABC): + """ + The common event builder, with builtin support to filter per chat. + + Args: + chats (`entity`, optional): + May be one or more entities (username/peer/etc.), preferably IDs. + By default, only matching chats will be handled. + + blacklist_chats (`bool`, optional): + Whether to treat the chats as a blacklist instead of + as a whitelist (default). This means that every chat + will be handled *except* those specified in ``chats`` + which will be ignored if ``blacklist_chats=True``. + + func (`callable`, optional): + A callable (async or not) function that should accept the event as input + parameter, and return a value indicating whether the event + should be dispatched or not (any truthy value will do, it + does not need to be a `bool`). It works like a custom filter: + + .. code-block:: python + + @client.on(events.NewMessage(func=lambda e: e.is_private)) + async def handler(event): + pass # code here + """ + def __init__(self, chats=None, *, blacklist_chats=False, func=None): + self.chats = chats + self.blacklist_chats = bool(blacklist_chats) + self.resolved = False + self.func = func + self._resolve_lock = None + + @classmethod + @abc.abstractmethod + def build(cls, update, others=None, self_id=None): + """ + Builds an event for the given update if possible, or returns None. + + `others` are the rest of updates that came in the same container + as the current `update`. + + `self_id` should be the current user's ID, since it is required + for some events which lack this information but still need it. + """ + # TODO So many parameters specific to only some update types seems dirty + + async def resolve(self, client): + """Helper method to allow event builders to be resolved before usage""" + if self.resolved: + return + + if not self._resolve_lock: + self._resolve_lock = asyncio.Lock() + + async with self._resolve_lock: + if not self.resolved: + await self._resolve(client) + self.resolved = True + + async def _resolve(self, client): + self.chats = await _into_id_set(client, self.chats) + + def filter(self, event): + """ + Returns a truthy value if the event passed the filter and should be + used, or falsy otherwise. The return value may need to be awaited. + + The events must have been resolved before this can be called. + """ + if not self.resolved: + return + + if self.chats is not None: + # Note: the `event.chat_id` property checks if it's `None` for us + inside = event.chat_id in self.chats + if inside == self.blacklist_chats: + # If this chat matches but it's a blacklist ignore. + # If it doesn't match but it's a whitelist ignore. + return + + if not self.func: + return True + + # Return the result of func directly as it may need to be awaited + return self.func(event) + + +class EventCommon(ChatGetter, abc.ABC): + """ + Intermediate class with common things to all events. + + Remember that this class implements `ChatGetter + ` which + means you have access to all chat properties and methods. + + In addition, you can access the `original_update` + field which contains the original :tl:`Update`. + """ + _event_name = 'Event' + + def __init__(self, chat_peer=None, msg_id=None, broadcast=None): + super().__init__(chat_peer, broadcast=broadcast) + self._entities = {} + self._client = None + self._message_id = msg_id + self.original_update = None + + def _set_client(self, client): + """ + Setter so subclasses can act accordingly when the client is set. + """ + self._client = client + if self._chat_peer: + self._chat, self._input_chat = utils._get_entity_pair( + self.chat_id, self._entities, client._mb_entity_cache) + else: + self._chat = self._input_chat = None + + @property + def client(self): + """ + The `telethon.TelegramClient` that created this event. + """ + return self._client + + def __str__(self): + return TLObject.pretty_format(self.to_dict()) + + def stringify(self): + return TLObject.pretty_format(self.to_dict(), indent=0) + + def to_dict(self): + d = {k: v for k, v in self.__dict__.items() if k[0] != '_'} + d['_'] = self._event_name + return d + + +def name_inner_event(cls): + """Decorator to rename cls.Event 'Event' as 'cls.Event'""" + if hasattr(cls, 'Event'): + cls.Event._event_name = '{}.Event'.format(cls.__name__) + else: + warnings.warn('Class {} does not have a inner Event'.format(cls)) + return cls diff --git a/module/scripts/lib/telethon/events/inlinequery.py b/module/scripts/lib/telethon/events/inlinequery.py new file mode 100644 index 0000000..7a065e5 --- /dev/null +++ b/module/scripts/lib/telethon/events/inlinequery.py @@ -0,0 +1,247 @@ +import inspect +import re + +import asyncio + +from .common import EventBuilder, EventCommon, name_inner_event +from .. import utils, helpers +from ..tl import types, functions, custom +from ..tl.custom.sendergetter import SenderGetter + + +@name_inner_event +class InlineQuery(EventBuilder): + """ + Occurs whenever you sign in as a bot and a user + sends an inline query such as ``@bot query``. + + Args: + users (`entity`, optional): + May be one or more entities (username/peer/etc.), preferably IDs. + By default, only inline queries from these users will be handled. + + blacklist_users (`bool`, optional): + Whether to treat the users as a blacklist instead of + as a whitelist (default). This means that every chat + will be handled *except* those specified in ``users`` + which will be ignored if ``blacklist_users=True``. + + pattern (`str`, `callable`, `Pattern`, optional): + If set, only queries matching this pattern will be handled. + You can specify a regex-like string which will be matched + against the message, a callable function that returns `True` + if a message is acceptable, or a compiled regex pattern. + + Example + .. code-block:: python + + from telethon import events + + @client.on(events.InlineQuery) + async def handler(event): + builder = event.builder + + # Two options (convert user text to UPPERCASE or lowercase) + await event.answer([ + builder.article('UPPERCASE', text=event.text.upper()), + builder.article('lowercase', text=event.text.lower()), + ]) + """ + def __init__( + self, users=None, *, blacklist_users=False, func=None, pattern=None): + super().__init__(users, blacklist_chats=blacklist_users, func=func) + + if isinstance(pattern, str): + self.pattern = re.compile(pattern).match + elif not pattern or callable(pattern): + self.pattern = pattern + elif hasattr(pattern, 'match') and callable(pattern.match): + self.pattern = pattern.match + else: + raise TypeError('Invalid pattern type given') + + @classmethod + def build(cls, update, others=None, self_id=None): + if isinstance(update, types.UpdateBotInlineQuery): + return cls.Event(update) + + def filter(self, event): + if self.pattern: + match = self.pattern(event.text) + if not match: + return + event.pattern_match = match + + return super().filter(event) + + class Event(EventCommon, SenderGetter): + """ + Represents the event of a new callback query. + + Members: + query (:tl:`UpdateBotInlineQuery`): + The original :tl:`UpdateBotInlineQuery`. + + Make sure to access the `text` property of the query if + you want the text rather than the actual query object. + + pattern_match (`obj`, optional): + The resulting object from calling the passed ``pattern`` + function, which is ``re.compile(...).match`` by default. + """ + def __init__(self, query): + super().__init__(chat_peer=types.PeerUser(query.user_id)) + SenderGetter.__init__(self, query.user_id) + self.query = query + self.pattern_match = None + self._answered = False + + def _set_client(self, client): + super()._set_client(client) + self._sender, self._input_sender = utils._get_entity_pair( + self.sender_id, self._entities, client._mb_entity_cache) + + @property + def id(self): + """ + Returns the unique identifier for the query ID. + """ + return self.query.query_id + + @property + def text(self): + """ + Returns the text the user used to make the inline query. + """ + return self.query.query + + @property + def offset(self): + """ + The string the user's client used as an offset for the query. + This will either be empty or equal to offsets passed to `answer`. + """ + return self.query.offset + + @property + def geo(self): + """ + If the user location is requested when using inline mode + and the user's device is able to send it, this will return + the :tl:`GeoPoint` with the position of the user. + """ + return self.query.geo + + @property + def builder(self): + """ + Returns a new `InlineBuilder + ` instance. + """ + return custom.InlineBuilder(self._client) + + async def answer( + self, results=None, cache_time=0, *, + gallery=False, next_offset=None, private=False, + switch_pm=None, switch_pm_param=''): + """ + Answers the inline query with the given results. + + See the documentation for `builder` to know what kind of answers + can be given. + + Args: + results (`list`, optional): + A list of :tl:`InputBotInlineResult` to use. + You should use `builder` to create these: + + .. code-block:: python + + builder = inline.builder + r1 = builder.article('Be nice', text='Have a nice day') + r2 = builder.article('Be bad', text="I don't like you") + await inline.answer([r1, r2]) + + You can send up to 50 results as documented in + https://core.telegram.org/bots/api#answerinlinequery. + Sending more will raise ``ResultsTooMuchError``, + and you should consider using `next_offset` to + paginate them. + + cache_time (`int`, optional): + For how long this result should be cached on + the user's client. Defaults to 0 for no cache. + + gallery (`bool`, optional): + Whether the results should show as a gallery (grid) or not. + + next_offset (`str`, optional): + The offset the client will send when the user scrolls the + results and it repeats the request. + + private (`bool`, optional): + Whether the results should be cached by Telegram + (not private) or by the user's client (private). + + switch_pm (`str`, optional): + If set, this text will be shown in the results + to allow the user to switch to private messages. + + switch_pm_param (`str`, optional): + Optional parameter to start the bot with if + `switch_pm` was used. + + Example: + + .. code-block:: python + + @bot.on(events.InlineQuery) + async def handler(event): + builder = event.builder + + rev_text = event.text[::-1] + await event.answer([ + builder.article('Reverse text', text=rev_text), + builder.photo('/path/to/photo.jpg') + ]) + """ + if self._answered: + return + + if results: + futures = [self._as_future(x) for x in results] + + await asyncio.wait(futures) + + # All futures will be in the `done` *set* that `wait` returns. + # + # Precisely because it's a `set` and not a `list`, it + # will not preserve the order, but since all futures + # completed we can use our original, ordered `list`. + results = [x.result() for x in futures] + else: + results = [] + + if switch_pm: + switch_pm = types.InlineBotSwitchPM(switch_pm, switch_pm_param) + + return await self._client( + functions.messages.SetInlineBotResultsRequest( + query_id=self.query.query_id, + results=results, + cache_time=cache_time, + gallery=gallery, + next_offset=next_offset, + private=private, + switch_pm=switch_pm + ) + ) + + @staticmethod + def _as_future(obj): + if inspect.isawaitable(obj): + return asyncio.ensure_future(obj) + + f = helpers.get_running_loop().create_future() + f.set_result(obj) + return f diff --git a/module/scripts/lib/telethon/events/messagedeleted.py b/module/scripts/lib/telethon/events/messagedeleted.py new file mode 100644 index 0000000..f631fd4 --- /dev/null +++ b/module/scripts/lib/telethon/events/messagedeleted.py @@ -0,0 +1,57 @@ +from .common import EventBuilder, EventCommon, name_inner_event +from ..tl import types + + +@name_inner_event +class MessageDeleted(EventBuilder): + """ + Occurs whenever a message is deleted. Note that this event isn't 100% + reliable, since Telegram doesn't always notify the clients that a message + was deleted. + + .. important:: + + Telegram **does not** send information about *where* a message + was deleted if it occurs in private conversations with other users + or in small group chats, because message IDs are *unique* and you + can identify the chat with the message ID alone if you saved it + previously. + + Telethon **does not** save information of where messages occur, + so it cannot know in which chat a message was deleted (this will + only work in channels, where the channel ID *is* present). + + This means that the ``chats=`` parameter will not work reliably, + unless you intend on working with channels and super-groups only. + + Example + .. code-block:: python + + from telethon import events + + @client.on(events.MessageDeleted) + async def handler(event): + # Log all deleted message IDs + for msg_id in event.deleted_ids: + print('Message', msg_id, 'was deleted in', event.chat_id) + """ + @classmethod + def build(cls, update, others=None, self_id=None): + if isinstance(update, types.UpdateDeleteMessages): + return cls.Event( + deleted_ids=update.messages, + peer=None + ) + elif isinstance(update, types.UpdateDeleteChannelMessages): + return cls.Event( + deleted_ids=update.messages, + peer=types.PeerChannel(update.channel_id) + ) + + class Event(EventCommon): + def __init__(self, deleted_ids, peer): + super().__init__( + chat_peer=peer, msg_id=(deleted_ids or [0])[0] + ) + self.deleted_id = None if not deleted_ids else deleted_ids[0] + self.deleted_ids = deleted_ids diff --git a/module/scripts/lib/telethon/events/messageedited.py b/module/scripts/lib/telethon/events/messageedited.py new file mode 100644 index 0000000..c4a2b4a --- /dev/null +++ b/module/scripts/lib/telethon/events/messageedited.py @@ -0,0 +1,52 @@ +from .common import name_inner_event +from .newmessage import NewMessage +from ..tl import types + + +@name_inner_event +class MessageEdited(NewMessage): + """ + Occurs whenever a message is edited. Just like `NewMessage + `, you should treat + this event as a `Message `. + + .. warning:: + + On channels, `Message.out ` + will be `True` if you sent the message originally, **not if + you edited it**! This can be dangerous if you run outgoing + commands on edits. + + Some examples follow: + + * You send a message "A", ``out is True``. + * You edit "A" to "B", ``out is True``. + * Someone else edits "B" to "C", ``out is True`` (**be careful!**). + * Someone sends "X", ``out is False``. + * Someone edits "X" to "Y", ``out is False``. + * You edit "Y" to "Z", ``out is False``. + + Since there are useful cases where you need the right ``out`` + value, the library cannot do anything automatically to help you. + Instead, consider using ``from_users='me'`` (it won't work in + broadcast channels at all since the sender is the channel and + not you). + + Example + .. code-block:: python + + from telethon import events + + @client.on(events.MessageEdited) + async def handler(event): + # Log the date of new edits + print('Message', event.id, 'changed at', event.date) + """ + @classmethod + def build(cls, update, others=None, self_id=None): + if isinstance(update, (types.UpdateEditMessage, + types.UpdateEditChannelMessage)): + return cls.Event(update.message) + + class Event(NewMessage.Event): + pass # Required if we want a different name for it diff --git a/module/scripts/lib/telethon/events/messageread.py b/module/scripts/lib/telethon/events/messageread.py new file mode 100644 index 0000000..29f17ab --- /dev/null +++ b/module/scripts/lib/telethon/events/messageread.py @@ -0,0 +1,143 @@ +from .common import EventBuilder, EventCommon, name_inner_event +from .. import utils +from ..tl import types + + +@name_inner_event +class MessageRead(EventBuilder): + """ + Occurs whenever one or more messages are read in a chat. + + Args: + inbox (`bool`, optional): + If this argument is `True`, then when you read someone else's + messages the event will be fired. By default (`False`) only + when messages you sent are read by someone else will fire it. + + Example + .. code-block:: python + + from telethon import events + + @client.on(events.MessageRead) + async def handler(event): + # Log when someone reads your messages + print('Someone has read all your messages until', event.max_id) + + @client.on(events.MessageRead(inbox=True)) + async def handler(event): + # Log when you read message in a chat (from your "inbox") + print('You have read messages until', event.max_id) + """ + def __init__( + self, chats=None, *, blacklist_chats=False, func=None, inbox=False): + super().__init__(chats, blacklist_chats=blacklist_chats, func=func) + self.inbox = inbox + + @classmethod + def build(cls, update, others=None, self_id=None): + if isinstance(update, types.UpdateReadHistoryInbox): + return cls.Event(update.peer, update.max_id, False) + elif isinstance(update, types.UpdateReadHistoryOutbox): + return cls.Event(update.peer, update.max_id, True) + elif isinstance(update, types.UpdateReadChannelInbox): + return cls.Event(types.PeerChannel(update.channel_id), + update.max_id, False) + elif isinstance(update, types.UpdateReadChannelOutbox): + return cls.Event(types.PeerChannel(update.channel_id), + update.max_id, True) + elif isinstance(update, types.UpdateReadMessagesContents): + return cls.Event(message_ids=update.messages, + contents=True) + elif isinstance(update, types.UpdateChannelReadMessagesContents): + return cls.Event(types.PeerChannel(update.channel_id), + message_ids=update.messages, + contents=True) + + def filter(self, event): + if self.inbox == event.outbox: + return + + return super().filter(event) + + class Event(EventCommon): + """ + Represents the event of one or more messages being read. + + Members: + max_id (`int`): + Up to which message ID has been read. Every message + with an ID equal or lower to it have been read. + + outbox (`bool`): + `True` if someone else has read your messages. + + contents (`bool`): + `True` if what was read were the contents of a message. + This will be the case when e.g. you play a voice note. + It may only be set on ``inbox`` events. + """ + def __init__(self, peer=None, max_id=None, out=False, contents=False, + message_ids=None): + self.outbox = out + self.contents = contents + self._message_ids = message_ids or [] + self._messages = None + self.max_id = max_id or max(message_ids or [], default=None) + super().__init__(peer, self.max_id) + + @property + def inbox(self): + """ + `True` if you have read someone else's messages. + """ + return not self.outbox + + @property + def message_ids(self): + """ + The IDs of the messages **which contents'** were read. + + Use :meth:`is_read` if you need to check whether a message + was read instead checking if it's in here. + """ + return self._message_ids + + async def get_messages(self): + """ + Returns the list of `Message ` + **which contents'** were read. + + Use :meth:`is_read` if you need to check whether a message + was read instead checking if it's in here. + """ + if self._messages is None: + chat = await self.get_input_chat() + if not chat: + self._messages = [] + else: + self._messages = await self._client.get_messages( + chat, ids=self._message_ids) + + return self._messages + + def is_read(self, message): + """ + Returns `True` if the given message (or its ID) has been read. + + If a list-like argument is provided, this method will return a + list of booleans indicating which messages have been read. + """ + if utils.is_list_like(message): + return [(m if isinstance(m, int) else m.id) <= self.max_id + for m in message] + else: + return (message if isinstance(message, int) + else message.id) <= self.max_id + + def __contains__(self, message): + """`True` if the message(s) are read message.""" + if utils.is_list_like(message): + return all(self.is_read(message)) + else: + return self.is_read(message) diff --git a/module/scripts/lib/telethon/events/newmessage.py b/module/scripts/lib/telethon/events/newmessage.py new file mode 100644 index 0000000..d2077a7 --- /dev/null +++ b/module/scripts/lib/telethon/events/newmessage.py @@ -0,0 +1,223 @@ +import re + +from .common import EventBuilder, EventCommon, name_inner_event, _into_id_set +from .. import utils +from ..tl import types + + +@name_inner_event +class NewMessage(EventBuilder): + """ + Occurs whenever a new text message or a message with media arrives. + + Args: + incoming (`bool`, optional): + If set to `True`, only **incoming** messages will be handled. + Mutually exclusive with ``outgoing`` (can only set one of either). + + outgoing (`bool`, optional): + If set to `True`, only **outgoing** messages will be handled. + Mutually exclusive with ``incoming`` (can only set one of either). + + from_users (`entity`, optional): + Unlike `chats`, this parameter filters the *senders* of the + message. That is, only messages *sent by these users* will be + handled. Use `chats` if you want private messages with this/these + users. `from_users` lets you filter by messages sent by *one or + more* users across the desired chats (doesn't need a list). + + forwards (`bool`, optional): + Whether forwarded messages should be handled or not. By default, + both forwarded and normal messages are included. If it's `True` + *only* forwards will be handled. If it's `False` only messages + that are *not* forwards will be handled. + + pattern (`str`, `callable`, `Pattern`, optional): + If set, only messages matching this pattern will be handled. + You can specify a regex-like string which will be matched + against the message, a callable function that returns `True` + if a message is acceptable, or a compiled regex pattern. + + Example + .. code-block:: python + + import asyncio + from telethon import events + + @client.on(events.NewMessage(pattern='(?i)hello.+')) + async def handler(event): + # Respond whenever someone says "Hello" and something else + await event.reply('Hey!') + + @client.on(events.NewMessage(outgoing=True, pattern='!ping')) + async def handler(event): + # Say "!pong" whenever you send "!ping", then delete both messages + m = await event.respond('!pong') + await asyncio.sleep(5) + await client.delete_messages(event.chat_id, [event.id, m.id]) + """ + def __init__(self, chats=None, *, blacklist_chats=False, func=None, + incoming=None, outgoing=None, + from_users=None, forwards=None, pattern=None): + if incoming and outgoing: + incoming = outgoing = None # Same as no filter + elif incoming is not None and outgoing is None: + outgoing = not incoming + elif outgoing is not None and incoming is None: + incoming = not outgoing + elif all(x is not None and not x for x in (incoming, outgoing)): + raise ValueError("Don't create an event handler if you " + "don't want neither incoming nor outgoing!") + + super().__init__(chats, blacklist_chats=blacklist_chats, func=func) + self.incoming = incoming + self.outgoing = outgoing + self.from_users = from_users + self.forwards = forwards + if isinstance(pattern, str): + self.pattern = re.compile(pattern).match + elif not pattern or callable(pattern): + self.pattern = pattern + elif hasattr(pattern, 'match') and callable(pattern.match): + self.pattern = pattern.match + else: + raise TypeError('Invalid pattern type given') + + # Should we short-circuit? E.g. perform no check at all + self._no_check = all(x is None for x in ( + self.chats, self.incoming, self.outgoing, self.pattern, + self.from_users, self.forwards, self.from_users, self.func + )) + + async def _resolve(self, client): + await super()._resolve(client) + self.from_users = await _into_id_set(client, self.from_users) + + @classmethod + def build(cls, update, others=None, self_id=None): + if isinstance(update, + (types.UpdateNewMessage, types.UpdateNewChannelMessage)): + if not isinstance(update.message, types.Message): + return # We don't care about MessageService's here + event = cls.Event(update.message) + elif isinstance(update, types.UpdateShortMessage): + event = cls.Event(types.Message( + out=update.out, + mentioned=update.mentioned, + media_unread=update.media_unread, + silent=update.silent, + id=update.id, + peer_id=types.PeerUser(update.user_id), + from_id=types.PeerUser(self_id if update.out else update.user_id), + message=update.message, + date=update.date, + fwd_from=update.fwd_from, + via_bot_id=update.via_bot_id, + reply_to=update.reply_to, + entities=update.entities, + ttl_period=update.ttl_period + )) + elif isinstance(update, types.UpdateShortChatMessage): + event = cls.Event(types.Message( + out=update.out, + mentioned=update.mentioned, + media_unread=update.media_unread, + silent=update.silent, + id=update.id, + from_id=types.PeerUser(self_id if update.out else update.from_id), + peer_id=types.PeerChat(update.chat_id), + message=update.message, + date=update.date, + fwd_from=update.fwd_from, + via_bot_id=update.via_bot_id, + reply_to=update.reply_to, + entities=update.entities, + ttl_period=update.ttl_period + )) + else: + return + + return event + + def filter(self, event): + if self._no_check: + return event + + if self.incoming and event.message.out: + return + if self.outgoing and not event.message.out: + return + if self.forwards is not None: + if bool(self.forwards) != bool(event.message.fwd_from): + return + + if self.from_users is not None: + if event.message.sender_id not in self.from_users: + return + + if self.pattern: + match = self.pattern(event.message.message or '') + if not match: + return + event.pattern_match = match + + return super().filter(event) + + class Event(EventCommon): + """ + Represents the event of a new message. This event can be treated + to all effects as a `Message `, + so please **refer to its documentation** to know what you can do + with this event. + + Members: + message (`Message `): + This is the only difference with the received + `Message `, and will + return the `telethon.tl.custom.message.Message` itself, + not the text. + + See `Message ` for + the rest of available members and methods. + + pattern_match (`obj`): + The resulting object from calling the passed ``pattern`` function. + Here's an example using a string (defaults to regex match): + + >>> from telethon import TelegramClient, events + >>> client = TelegramClient(...) + >>> + >>> @client.on(events.NewMessage(pattern=r'hi (\\w+)!')) + ... async def handler(event): + ... # In this case, the result is a ``Match`` object + ... # since the `str` pattern was converted into + ... # the ``re.compile(pattern).match`` function. + ... print('Welcomed', event.pattern_match.group(1)) + ... + >>> + """ + def __init__(self, message): + self.__dict__['_init'] = False + super().__init__(chat_peer=message.peer_id, + msg_id=message.id, broadcast=bool(message.post)) + + self.pattern_match = None + self.message = message + + def _set_client(self, client): + super()._set_client(client) + m = self.message + m._finish_init(client, self._entities, None) + self.__dict__['_init'] = True # No new attributes can be set + + def __getattr__(self, item): + if item in self.__dict__: + return self.__dict__[item] + else: + return getattr(self.message, item) + + def __setattr__(self, name, value): + if not self.__dict__['_init'] or name in self.__dict__: + self.__dict__[name] = value + else: + setattr(self.message, name, value) diff --git a/module/scripts/lib/telethon/events/raw.py b/module/scripts/lib/telethon/events/raw.py new file mode 100644 index 0000000..8491077 --- /dev/null +++ b/module/scripts/lib/telethon/events/raw.py @@ -0,0 +1,53 @@ +from .common import EventBuilder +from .. import utils + + +class Raw(EventBuilder): + """ + Raw events are not actual events. Instead, they are the raw + :tl:`Update` object that Telegram sends. You normally shouldn't + need these. + + Args: + types (`list` | `tuple` | `type`, optional): + The type or types that the :tl:`Update` instance must be. + Equivalent to ``if not isinstance(update, types): return``. + + Example + .. code-block:: python + + from telethon import events + + @client.on(events.Raw) + async def handler(update): + # Print all incoming updates + print(update.stringify()) + """ + def __init__(self, types=None, *, func=None): + super().__init__(func=func) + if not types: + self.types = None + elif not utils.is_list_like(types): + if not isinstance(types, type): + raise TypeError('Invalid input type given: {}'.format(types)) + + self.types = types + else: + if not all(isinstance(x, type) for x in types): + raise TypeError('Invalid input types given: {}'.format(types)) + + self.types = tuple(types) + + async def resolve(self, client): + self.resolved = True + + @classmethod + def build(cls, update, others=None, self_id=None): + return update + + def filter(self, event): + if not self.types or isinstance(event, self.types): + if self.func: + # Return the result of func directly as it may need to be awaited + return self.func(event) + return event diff --git a/module/scripts/lib/telethon/events/userupdate.py b/module/scripts/lib/telethon/events/userupdate.py new file mode 100644 index 0000000..450172e --- /dev/null +++ b/module/scripts/lib/telethon/events/userupdate.py @@ -0,0 +1,310 @@ +import datetime +import functools + +from .common import EventBuilder, EventCommon, name_inner_event +from .. import utils +from ..tl import types +from ..tl.custom.sendergetter import SenderGetter + + +# TODO Either the properties are poorly named or they should be +# different events, but that would be a breaking change. +# +# TODO There are more "user updates", but bundling them all up +# in a single place will make it annoying to use (since +# the user needs to check for the existence of `None`). +# +# TODO Handle UpdateUserBlocked, UpdateUserName, UpdateUserPhone, UpdateUser + +def _requires_action(function): + @functools.wraps(function) + def wrapped(self): + return None if self.action is None else function(self) + + return wrapped + + +def _requires_status(function): + @functools.wraps(function) + def wrapped(self): + return None if self.status is None else function(self) + + return wrapped + + +@name_inner_event +class UserUpdate(EventBuilder): + """ + Occurs whenever a user goes online, starts typing, etc. + + Example + .. code-block:: python + + from telethon import events + + @client.on(events.UserUpdate) + async def handler(event): + # If someone is uploading, say something + if event.uploading: + await client.send_message(event.user_id, 'What are you sending?') + """ + @classmethod + def build(cls, update, others=None, self_id=None): + if isinstance(update, types.UpdateUserStatus): + return cls.Event(types.PeerUser(update.user_id), + status=update.status) + elif isinstance(update, types.UpdateChannelUserTyping): + return cls.Event(update.from_id, + chat_peer=types.PeerChannel(update.channel_id), + typing=update.action) + elif isinstance(update, types.UpdateChatUserTyping): + return cls.Event(update.from_id, + chat_peer=types.PeerChat(update.chat_id), + typing=update.action) + elif isinstance(update, types.UpdateUserTyping): + return cls.Event(update.user_id, + typing=update.action) + + class Event(EventCommon, SenderGetter): + """ + Represents the event of a user update + such as gone online, started typing, etc. + + Members: + status (:tl:`UserStatus`, optional): + The user status if the update is about going online or offline. + + You should check this attribute first before checking any + of the seen within properties, since they will all be `None` + if the status is not set. + + action (:tl:`SendMessageAction`, optional): + The "typing" action if any the user is performing if any. + + You should check this attribute first before checking any + of the typing properties, since they will all be `None` + if the action is not set. + """ + def __init__(self, peer, *, status=None, chat_peer=None, typing=None): + super().__init__(chat_peer or peer) + SenderGetter.__init__(self, utils.get_peer_id(peer)) + + self.status = status + self.action = typing + + def _set_client(self, client): + super()._set_client(client) + self._sender, self._input_sender = utils._get_entity_pair( + self.sender_id, self._entities, client._mb_entity_cache) + + @property + def user(self): + """Alias for `sender `.""" + return self.sender + + async def get_user(self): + """Alias for `get_sender `.""" + return await self.get_sender() + + @property + def input_user(self): + """Alias for `input_sender `.""" + return self.input_sender + + async def get_input_user(self): + """Alias for `get_input_sender `.""" + return await self.get_input_sender() + + @property + def user_id(self): + """Alias for `sender_id `.""" + return self.sender_id + + @property + @_requires_action + def typing(self): + """ + `True` if the action is typing a message. + """ + return isinstance(self.action, types.SendMessageTypingAction) + + @property + @_requires_action + def uploading(self): + """ + `True` if the action is uploading something. + """ + return isinstance(self.action, ( + types.SendMessageChooseContactAction, + types.SendMessageChooseStickerAction, + types.SendMessageUploadAudioAction, + types.SendMessageUploadDocumentAction, + types.SendMessageUploadPhotoAction, + types.SendMessageUploadRoundAction, + types.SendMessageUploadVideoAction + )) + + @property + @_requires_action + def recording(self): + """ + `True` if the action is recording something. + """ + return isinstance(self.action, ( + types.SendMessageRecordAudioAction, + types.SendMessageRecordRoundAction, + types.SendMessageRecordVideoAction + )) + + @property + @_requires_action + def playing(self): + """ + `True` if the action is playing a game. + """ + return isinstance(self.action, types.SendMessageGamePlayAction) + + @property + @_requires_action + def cancel(self): + """ + `True` if the action was cancelling other actions. + """ + return isinstance(self.action, types.SendMessageCancelAction) + + @property + @_requires_action + def geo(self): + """ + `True` if what's being uploaded is a geo. + """ + return isinstance(self.action, types.SendMessageGeoLocationAction) + + @property + @_requires_action + def audio(self): + """ + `True` if what's being recorded/uploaded is an audio. + """ + return isinstance(self.action, ( + types.SendMessageRecordAudioAction, + types.SendMessageUploadAudioAction + )) + + @property + @_requires_action + def round(self): + """ + `True` if what's being recorded/uploaded is a round video. + """ + return isinstance(self.action, ( + types.SendMessageRecordRoundAction, + types.SendMessageUploadRoundAction + )) + + @property + @_requires_action + def video(self): + """ + `True` if what's being recorded/uploaded is an video. + """ + return isinstance(self.action, ( + types.SendMessageRecordVideoAction, + types.SendMessageUploadVideoAction + )) + + @property + @_requires_action + def contact(self): + """ + `True` if what's being uploaded (selected) is a contact. + """ + return isinstance(self.action, types.SendMessageChooseContactAction) + + @property + @_requires_action + def document(self): + """ + `True` if what's being uploaded is document. + """ + return isinstance(self.action, types.SendMessageUploadDocumentAction) + + @property + @_requires_action + def sticker(self): + """ + `True` if what's being uploaded is a sticker. + """ + return isinstance(self.action, types.SendMessageChooseStickerAction) + + @property + @_requires_action + def photo(self): + """ + `True` if what's being uploaded is a photo. + """ + return isinstance(self.action, types.SendMessageUploadPhotoAction) + + @property + @_requires_status + def last_seen(self): + """ + Exact `datetime.datetime` when the user was last seen if known. + """ + if isinstance(self.status, types.UserStatusOffline): + return self.status.was_online + + @property + @_requires_status + def until(self): + """ + The `datetime.datetime` until when the user should appear online. + """ + if isinstance(self.status, types.UserStatusOnline): + return self.status.expires + + def _last_seen_delta(self): + if isinstance(self.status, types.UserStatusOffline): + return datetime.datetime.now(tz=datetime.timezone.utc) - self.status.was_online + elif isinstance(self.status, types.UserStatusOnline): + return datetime.timedelta(days=0) + elif isinstance(self.status, types.UserStatusRecently): + return datetime.timedelta(days=1) + elif isinstance(self.status, types.UserStatusLastWeek): + return datetime.timedelta(days=7) + elif isinstance(self.status, types.UserStatusLastMonth): + return datetime.timedelta(days=30) + else: + return datetime.timedelta(days=365) + + @property + @_requires_status + def online(self): + """ + `True` if the user is currently online, + """ + return self._last_seen_delta() <= datetime.timedelta(days=0) + + @property + @_requires_status + def recently(self): + """ + `True` if the user was seen within a day. + """ + return self._last_seen_delta() <= datetime.timedelta(days=1) + + @property + @_requires_status + def within_weeks(self): + """ + `True` if the user was seen within 7 days. + """ + return self._last_seen_delta() <= datetime.timedelta(days=7) + + @property + @_requires_status + def within_months(self): + """ + `True` if the user was seen within 30 days. + """ + return self._last_seen_delta() <= datetime.timedelta(days=30) diff --git a/module/scripts/lib/telethon/extensions/__init__.py b/module/scripts/lib/telethon/extensions/__init__.py new file mode 100644 index 0000000..a3c7729 --- /dev/null +++ b/module/scripts/lib/telethon/extensions/__init__.py @@ -0,0 +1,6 @@ +""" +Several extensions Python is missing, such as a proper class to handle a TCP +communication with support for cancelling the operation, and a utility class +to read arbitrary binary data in a more comfortable way, with int/strings/etc. +""" +from .binaryreader import BinaryReader diff --git a/module/scripts/lib/telethon/extensions/__pycache__/__init__.cpython-314.pyc b/module/scripts/lib/telethon/extensions/__pycache__/__init__.cpython-314.pyc new file mode 100644 index 0000000000000000000000000000000000000000..5018412b28d739541918ae426375437489586e8e GIT binary patch literal 521 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 literal 0 HcmV?d00001 diff --git a/module/scripts/lib/telethon/extensions/__pycache__/html.cpython-314.pyc b/module/scripts/lib/telethon/extensions/__pycache__/html.cpython-314.pyc new file mode 100644 index 0000000000000000000000000000000000000000..90f6f80900985420852196e16b504258b6ee3fb2 GIT binary patch literal 10655 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 QJi07#eSQ!uG`g1m1svFrqW}N^ literal 0 HcmV?d00001 diff --git a/module/scripts/lib/telethon/extensions/__pycache__/markdown.cpython-314.pyc b/module/scripts/lib/telethon/extensions/__pycache__/markdown.cpython-314.pyc new file mode 100644 index 0000000000000000000000000000000000000000..8535b0798ce647e516e2c307fb231bcb69ba93d8 GIT binary patch literal 7715 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 I(O&2CKXQCz`Tzg` literal 0 HcmV?d00001 diff --git a/module/scripts/lib/telethon/extensions/binaryreader.py b/module/scripts/lib/telethon/extensions/binaryreader.py new file mode 100644 index 0000000..84f3b5b --- /dev/null +++ b/module/scripts/lib/telethon/extensions/binaryreader.py @@ -0,0 +1,201 @@ +""" +This module contains the BinaryReader utility class. +""" +import struct +import time +from datetime import datetime, timedelta, timezone + +from ..errors import TypeNotFoundError +from ..tl.alltlobjects import tlobjects +from ..tl.core import core_objects + +_EPOCH_NAIVE = datetime(*time.gmtime(0)[:6]) +_EPOCH = _EPOCH_NAIVE.replace(tzinfo=timezone.utc) + + +class BinaryReader: + """ + Small utility class to read binary data. + """ + + def __init__(self, data): + self.stream = data or b'' + self.position = 0 + self._last = None # Should come in handy to spot -404 errors + + # region Reading + + # "All numbers are written as little endian." + # https://core.telegram.org/mtproto + def read_byte(self): + """Reads a single byte value.""" + value, = struct.unpack_from("= 0: + result = self.stream[self.position:self.position + length] + self.position += length + else: + result = self.stream[self.position:] + self.position += len(result) + if (length >= 0) and (len(result) != length): + raise BufferError( + 'No more data left to read (need {}, got {}: {}); last read {}' + .format(length, len(result), repr(result), repr(self._last)) + ) + + self._last = result + return result + + def get_bytes(self): + """Gets the byte array representing the current buffer as a whole.""" + return self.stream + + # endregion + + # region Telegram custom reading + + def tgread_bytes(self): + """ + Reads a Telegram-encoded byte array, without the need of + specifying its length. + """ + first_byte = self.read_byte() + if first_byte == 254: + length = self.read_byte() | (self.read_byte() << 8) | ( + self.read_byte() << 16) + padding = length % 4 + else: + length = first_byte + padding = (length + 1) % 4 + + data = self.read(length) + if padding > 0: + padding = 4 - padding + self.read(padding) + + return data + + def tgread_string(self): + """Reads a Telegram-encoded string.""" + return str(self.tgread_bytes(), encoding='utf-8', errors='replace') + + def tgread_bool(self): + """Reads a Telegram boolean value.""" + value = self.read_int(signed=False) + if value == 0x997275b5: # boolTrue + return True + elif value == 0xbc799737: # boolFalse + return False + else: + raise RuntimeError('Invalid boolean code {}'.format(hex(value))) + + def tgread_date(self): + """Reads and converts Unix time (used by Telegram) + into a Python datetime object. + """ + value = self.read_int() + return _EPOCH + timedelta(seconds=value) + + def tgread_object(self): + """Reads a Telegram object.""" + constructor_id = self.read_int(signed=False) + clazz = tlobjects.get(constructor_id, None) + if clazz is None: + # The class was None, but there's still a + # chance of it being a manually parsed value like bool! + value = constructor_id + if value == 0x997275b5: # boolTrue + return True + elif value == 0xbc799737: # boolFalse + return False + elif value == 0x1cb5c415: # Vector + return [self.tgread_object() for _ in range(self.read_int())] + + clazz = core_objects.get(constructor_id, None) + if clazz is None: + # If there was still no luck, give up + self.seek(-4) # Go back + pos = self.tell_position() + error = TypeNotFoundError(constructor_id, self.read()) + self.set_position(pos) + raise error + + return clazz.from_reader(self) + + def tgread_vector(self): + """Reads a vector (a list) of Telegram objects.""" + if 0x1cb5c415 != self.read_int(signed=False): + raise RuntimeError('Invalid constructor code, vector was expected') + + count = self.read_int() + return [self.tgread_object() for _ in range(count)] + + # endregion + + def close(self): + """Closes the reader, freeing the BytesIO stream.""" + self.stream = b'' + + # region Position related + + def tell_position(self): + """Tells the current position on the stream.""" + return self.position + + def set_position(self, position): + """Sets the current position on the stream.""" + self.position = position + + def seek(self, offset): + """ + Seeks the stream position given an offset from the current position. + The offset may be negative. + """ + self.position += offset + + # endregion + + # region with block + + def __enter__(self): + return self + + def __exit__(self, exc_type, exc_val, exc_tb): + self.close() + + # endregion diff --git a/module/scripts/lib/telethon/extensions/html.py b/module/scripts/lib/telethon/extensions/html.py new file mode 100644 index 0000000..8a04035 --- /dev/null +++ b/module/scripts/lib/telethon/extensions/html.py @@ -0,0 +1,208 @@ +""" +Simple HTML -> Telegram entity parser. +""" +from collections import deque +from html import escape +from html.parser import HTMLParser +from typing import Iterable, Tuple, List + +from ..helpers import add_surrogate, del_surrogate, within_surrogate, strip_text +from ..tl import TLObject +from ..tl.types import ( + MessageEntityBold, MessageEntityItalic, MessageEntityCode, + MessageEntityPre, MessageEntityEmail, MessageEntityUrl, + MessageEntityTextUrl, MessageEntityMentionName, + MessageEntityUnderline, MessageEntityStrike, MessageEntityBlockquote, + MessageEntityCustomEmoji, TypeMessageEntity +) + + +class HTMLToTelegramParser(HTMLParser): + def __init__(self): + super().__init__() + self.text = '' + self.entities = [] + self._building_entities = {} + self._open_tags = deque() + self._open_tags_meta = deque() + + def handle_starttag(self, tag, attrs): + self._open_tags.appendleft(tag) + self._open_tags_meta.appendleft(None) + + attrs = dict(attrs) + EntityType = None + args = {} + if tag == 'strong' or tag == 'b': + EntityType = MessageEntityBold + elif tag == 'em' or tag == 'i': + EntityType = MessageEntityItalic + elif tag == 'u': + EntityType = MessageEntityUnderline + elif tag == 'del' or tag == 's': + EntityType = MessageEntityStrike + elif tag == 'blockquote': + EntityType = MessageEntityBlockquote + if 'expandable' in attrs: + args['collapsed'] = True + elif tag == 'code': + try: + # If we're in the middle of a

 tag, this  tag is
+                # probably intended for syntax highlighting.
+                #
+                # Syntax highlighting is set with
+                #     codeblock
+                # inside 
 tags
+                pre = self._building_entities['pre']
+                try:
+                    pre.language = attrs['class'][len('language-'):]
+                except KeyError:
+                    pass
+            except KeyError:
+                EntityType = MessageEntityCode
+        elif tag == 'pre':
+            EntityType = MessageEntityPre
+            args['language'] = ''
+        elif tag == 'a':
+            try:
+                url = attrs['href']
+            except KeyError:
+                return
+            if url.startswith('mailto:'):
+                url = url[len('mailto:'):]
+                EntityType = MessageEntityEmail
+            else:
+                if self.get_starttag_text() == url:
+                    EntityType = MessageEntityUrl
+                else:
+                    EntityType = MessageEntityTextUrl
+                    args['url'] = del_surrogate(url)
+                    url = None
+            self._open_tags_meta.popleft()
+            self._open_tags_meta.appendleft(url)
+        elif tag == 'tg-emoji':
+            try:
+                emoji_id = int(attrs['emoji-id'])
+            except (KeyError, ValueError):
+                return
+
+            EntityType = MessageEntityCustomEmoji
+            args['document_id'] = emoji_id
+            
+        if EntityType and tag not in self._building_entities:
+            self._building_entities[tag] = EntityType(
+                offset=len(self.text),
+                # The length will be determined when closing the tag.
+                length=0,
+                **args)
+
+    def handle_data(self, text):
+        previous_tag = self._open_tags[0] if len(self._open_tags) > 0 else ''
+        if previous_tag == 'a':
+            url = self._open_tags_meta[0]
+            if url:
+                text = url
+
+        for tag, entity in self._building_entities.items():
+            entity.length += len(text)
+
+        self.text += text
+
+    def handle_endtag(self, tag):
+        try:
+            self._open_tags.popleft()
+            self._open_tags_meta.popleft()
+        except IndexError:
+            pass
+        entity = self._building_entities.pop(tag, None)
+        if entity:
+            self.entities.append(entity)
+
+
+def parse(html: str) -> Tuple[str, List[TypeMessageEntity]]:
+    """
+    Parses the given HTML message and returns its stripped representation
+    plus a list of the MessageEntity's that were found.
+
+    :param html: the message with HTML to be parsed.
+    :return: a tuple consisting of (clean message, [message entities]).
+    """
+    if not html:
+        return html, []
+
+    parser = HTMLToTelegramParser()
+    parser.feed(add_surrogate(html))
+    text = strip_text(parser.text, parser.entities)
+    parser.entities.reverse()
+    parser.entities.sort(key=lambda entity: entity.offset)
+    return del_surrogate(text), parser.entities
+
+
+ENTITY_TO_FORMATTER = {
+    MessageEntityBold: ('', ''),
+    MessageEntityItalic: ('', ''),
+    MessageEntityCode: ('', ''),
+    MessageEntityUnderline: ('', ''),
+    MessageEntityStrike: ('', ''),
+    MessageEntityBlockquote: lambda e, _: (
+        '
' if e.collapsed else '
', + '
' + ), + MessageEntityPre: lambda e, _: ( + "
\n"
+        "    \n"
+        "        ".format(e.language), "{}\n"
+        "    \n"
+        "
" + ), + MessageEntityEmail: lambda _, t: ('
'.format(t), ''), + MessageEntityUrl: lambda _, t: (''.format(t), ''), + MessageEntityTextUrl: lambda e, _: (''.format(escape(e.url)), ''), + MessageEntityMentionName: lambda e, _: (''.format(e.user_id), ''), + MessageEntityCustomEmoji: lambda e, _: (''.format(e.document_id), ''), +} + + +def unparse(text: str, entities: Iterable[TypeMessageEntity]) -> str: + """ + Performs the reverse operation to .parse(), effectively returning HTML + given a normal text and its MessageEntity's. + + :param text: the text to be reconverted into HTML. + :param entities: the MessageEntity's applied to the text. + :return: a HTML representation of the combination of both inputs. + """ + if not text: + return text + elif not entities: + return escape(text) + + if isinstance(entities, TLObject): + entities = (entities,) + + text = add_surrogate(text) + insert_at = [] + for i, entity in enumerate(entities): + s = entity.offset + e = entity.offset + entity.length + delimiter = ENTITY_TO_FORMATTER.get(type(entity), None) + if delimiter: + if callable(delimiter): + delimiter = delimiter(entity, text[s:e]) + insert_at.append((s, i, delimiter[0])) + insert_at.append((e, -i, delimiter[1])) + + insert_at.sort(key=lambda t: (t[0], t[1])) + next_escape_bound = len(text) + while insert_at: + # Same logic as markdown.py + at, _, what = insert_at.pop() + while within_surrogate(text, at): + at += 1 + + text = text[:at] + what + escape(text[at:next_escape_bound]) + text[next_escape_bound:] + next_escape_bound = at + + text = escape(text[:next_escape_bound]) + text[next_escape_bound:] + + return del_surrogate(text) diff --git a/module/scripts/lib/telethon/extensions/markdown.py b/module/scripts/lib/telethon/extensions/markdown.py new file mode 100644 index 0000000..4dbbe39 --- /dev/null +++ b/module/scripts/lib/telethon/extensions/markdown.py @@ -0,0 +1,193 @@ +""" +Simple markdown parser which does not support nesting. Intended primarily +for use within the library, which attempts to handle emojies correctly, +since they seem to count as two characters and it's a bit strange. +""" +import re +import warnings + +from ..helpers import add_surrogate, del_surrogate, within_surrogate, strip_text +from ..tl import TLObject +from ..tl.types import ( + MessageEntityBold, MessageEntityItalic, MessageEntityCode, + MessageEntityPre, MessageEntityTextUrl, MessageEntityMentionName, + MessageEntityStrike +) + +DEFAULT_DELIMITERS = { + '**': MessageEntityBold, + '__': MessageEntityItalic, + '~~': MessageEntityStrike, + '`': MessageEntityCode, + '```': MessageEntityPre +} + +DEFAULT_URL_RE = re.compile(r'\[([^]]*?)\]\(([\s\S]*?)\)') +DEFAULT_URL_FORMAT = '[{0}]({1})' + + +def parse(message, delimiters=None, url_re=None): + """ + Parses the given markdown message and returns its stripped representation + plus a list of the MessageEntity's that were found. + + :param message: the message with markdown-like syntax to be parsed. + :param delimiters: the delimiters to be used, {delimiter: type}. + :param url_re: the URL bytes regex to be used. Must have two groups. + :return: a tuple consisting of (clean message, [message entities]). + """ + if not message: + return message, [] + + if url_re is None: + url_re = DEFAULT_URL_RE + elif isinstance(url_re, str): + url_re = re.compile(url_re) + + if not delimiters: + if delimiters is not None: + return message, [] + delimiters = DEFAULT_DELIMITERS + + # Build a regex to efficiently test all delimiters at once. + # Note that the largest delimiter should go first, we don't + # want ``` to be interpreted as a single back-tick in a code block. + delim_re = re.compile('|'.join('({})'.format(re.escape(k)) + for k in sorted(delimiters, key=len, reverse=True))) + + # Cannot use a for loop because we need to skip some indices + i = 0 + result = [] + + # Work on byte level with the utf-16le encoding to get the offsets right. + # The offset will just be half the index we're at. + message = add_surrogate(message) + while i < len(message): + m = delim_re.match(message, pos=i) + + # Did we find some delimiter here at `i`? + if m: + delim = next(filter(None, m.groups())) + + # +1 to avoid matching right after (e.g. "****") + end = message.find(delim, i + len(delim) + 1) + + # Did we find the earliest closing tag? + if end != -1: + + # Remove the delimiter from the string + message = ''.join(( + message[:i], + message[i + len(delim):end], + message[end + len(delim):] + )) + + # Check other affected entities + for ent in result: + # If the end is after our start, it is affected + if ent.offset + ent.length > i: + # If the old start is before ours and the old end is after ours, we are fully enclosed + if ent.offset <= i and ent.offset + ent.length >= end + len(delim): + ent.length -= len(delim) * 2 + else: + ent.length -= len(delim) + + # Append the found entity + ent = delimiters[delim] + if ent == MessageEntityPre: + result.append(ent(i, end - i - len(delim), '')) # has 'lang' + else: + result.append(ent(i, end - i - len(delim))) + + # No nested entities inside code blocks + if ent in (MessageEntityCode, MessageEntityPre): + i = end - len(delim) + + continue + + elif url_re: + m = url_re.match(message, pos=i) + if m: + # Replace the whole match with only the inline URL text. + message = ''.join(( + message[:m.start()], + m.group(1), + message[m.end():] + )) + + delim_size = m.end() - m.start() - len(m.group(1)) + for ent in result: + # If the end is after our start, it is affected + if ent.offset + ent.length > m.start(): + ent.length -= delim_size + + result.append(MessageEntityTextUrl( + offset=m.start(), length=len(m.group(1)), + url=del_surrogate(m.group(2)) + )) + i += len(m.group(1)) + continue + + i += 1 + + message = strip_text(message, result) + return del_surrogate(message), result + + +def unparse(text, entities, delimiters=None, url_fmt=None): + """ + Performs the reverse operation to .parse(), effectively returning + markdown-like syntax given a normal text and its MessageEntity's. + + :param text: the text to be reconverted into markdown. + :param entities: the MessageEntity's applied to the text. + :return: a markdown-like text representing the combination of both inputs. + """ + if not text or not entities: + return text + + if not delimiters: + if delimiters is not None: + return text + delimiters = DEFAULT_DELIMITERS + + if url_fmt is not None: + warnings.warn('url_fmt is deprecated') # since it complicates everything *a lot* + + if isinstance(entities, TLObject): + entities = (entities,) + + text = add_surrogate(text) + delimiters = {v: k for k, v in delimiters.items()} + insert_at = [] + for i, entity in enumerate(entities): + s = entity.offset + e = entity.offset + entity.length + delimiter = delimiters.get(type(entity), None) + if delimiter: + insert_at.append((s, i, delimiter)) + insert_at.append((e, -i, delimiter)) + else: + url = None + if isinstance(entity, MessageEntityTextUrl): + url = entity.url + elif isinstance(entity, MessageEntityMentionName): + url = 'tg://user?id={}'.format(entity.user_id) + if url: + insert_at.append((s, i, '[')) + insert_at.append((e, -i, ']({})'.format(url))) + + insert_at.sort(key=lambda t: (t[0], t[1])) + while insert_at: + at, _, what = insert_at.pop() + + # If we are in the middle of a surrogate nudge the position by -1. + # Otherwise we would end up with malformed text and fail to encode. + # For example of bad input: "Hi \ud83d\ude1c" + # https://en.wikipedia.org/wiki/UTF-16#U+010000_to_U+10FFFF + while within_surrogate(text, at): + at += 1 + + text = text[:at] + what + text[at:] + + return del_surrogate(text) diff --git a/module/scripts/lib/telethon/extensions/messagepacker.py b/module/scripts/lib/telethon/extensions/messagepacker.py new file mode 100644 index 0000000..c0f46f4 --- /dev/null +++ b/module/scripts/lib/telethon/extensions/messagepacker.py @@ -0,0 +1,111 @@ +import asyncio +import collections +import io +import struct + +from ..tl import TLRequest +from ..tl.core.messagecontainer import MessageContainer +from ..tl.core.tlmessage import TLMessage + + +class MessagePacker: + """ + This class packs `RequestState` as outgoing `TLMessages`. + + The purpose of this class is to support putting N `RequestState` into a + queue, and then awaiting for "packed" `TLMessage` in the other end. The + simplest case would be ``State -> TLMessage`` (1-to-1 relationship) but + for efficiency purposes it's ``States -> Container`` (N-to-1). + + This addresses several needs: outgoing messages will be smaller, so the + encryption and network overhead also is smaller. It's also a central + point where outgoing requests are put, and where ready-messages are get. + """ + + def __init__(self, state, loggers): + self._state = state + self._deque = collections.deque() + self._ready = asyncio.Event() + self._log = loggers[__name__] + + def append(self, state): + self._deque.append(state) + self._ready.set() + + def extend(self, states): + self._deque.extend(states) + self._ready.set() + + async def get(self): + """ + Returns (batch, data) if one or more items could be retrieved. + + If the cancellation occurs or only invalid items were in the + queue, (None, None) will be returned instead. + """ + if not self._deque: + self._ready.clear() + await self._ready.wait() + + buffer = io.BytesIO() + batch = [] + size = 0 + + # Fill a new batch to return while the size is small enough, + # as long as we don't exceed the maximum length of messages. + while self._deque and len(batch) <= MessageContainer.MAXIMUM_LENGTH: + state = self._deque.popleft() + size += len(state.data) + TLMessage.SIZE_OVERHEAD + + if size <= MessageContainer.MAXIMUM_SIZE: + state.msg_id = self._state.write_data_as_message( + buffer, state.data, isinstance(state.request, TLRequest), + after_id=state.after.msg_id if state.after else None + ) + batch.append(state) + self._log.debug('Assigned msg_id = %d to %s (%x)', + state.msg_id, state.request.__class__.__name__, + id(state.request)) + continue + + if batch: + # Put the item back since it can't be sent in this batch + self._deque.appendleft(state) + break + + # If a single message exceeds the maximum size, then the + # message payload cannot be sent. Telegram would forcibly + # close the connection; message would never be confirmed. + # + # We don't put the item back because it can never be sent. + # If we did, we would loop again and reach this same path. + # Setting the exception twice results in `InvalidStateError` + # and this method should never return with error, which we + # really want to avoid. + self._log.warning( + 'Message payload for %s is too long (%d) and cannot be sent', + state.request.__class__.__name__, len(state.data) + ) + state.future.set_exception( + ValueError('Request payload is too big')) + + size = 0 + continue + + if not batch: + return None, None + + if len(batch) > 1: + # Inlined code to pack several messages into a container + data = struct.pack( + ' Surrogate Pairs (Telegram offsets are calculated with these). + # See https://en.wikipedia.org/wiki/Plane_(Unicode)#Overview for more. + ''.join(chr(y) for y in struct.unpack(' left_offset: + if e.offset >= left_offset: + # 0 1|2 3 4 5 | 0 1|2 3 4 5 + # ^ ^ | ^ + # lo(2) o(5) | o(2)/lo(2) + e.offset -= left_offset + # |0 1 2 3 | |0 1 2 3 + # ^ | ^ + # o=o-lo(3=5-2) | o=o-lo(0=2-2) + else: + # e.offset < left_offset and e.offset + e.length > left_offset + # 0 1 2 3|4 5 6 7 8 9 10 + # ^ ^ ^ + # o(1) lo(4) o+l(1+9) + e.length = e.offset + e.length - left_offset + e.offset = 0 + # |0 1 2 3 4 5 6 + # ^ ^ + # o(0) o+l=0+o+l-lo(6=0+6=0+1+9-4) + else: + # e.offset + e.length <= left_offset + # 0 1 2 3|4 5 + # ^ ^ + # o(0) o+l(4) + # lo(4) + del entities[i] + continue + + if e.offset + e.length <= len_final: + # |0 1 2 3 4 5 6 7 8 9 + # ^ ^ + # o(1) o+l(1+9)/lf(10) + continue + if e.offset >= len_final: + # |0 1 2 3 4 + # ^ + # o(5)/lf(5) + del entities[i] + else: + # e.offset < len_final and e.offset + e.length > len_final + # |0 1 2 3 4 5 (6) (7) (8) (9) + # ^ ^ ^ + # o(1) lf(6) o+l(1+8) + e.length = len_final - e.offset + # |0 1 2 3 4 5 + # ^ ^ + # o(1) o+l=o+lf-o=lf(6=1+5=1+6-1) + + return text + + +def retry_range(retries, force_retry=True): + """ + Generates an integer sequence starting from 1. If `retries` is + not a zero or a positive integer value, the sequence will be + infinite, otherwise it will end at `retries + 1`. + """ + + # We need at least one iteration even if the retries are 0 + # when force_retry is True. + if force_retry and not (retries is None or retries < 0): + retries += 1 + + attempt = 0 + while attempt != retries: + attempt += 1 + yield attempt + + + +async def _maybe_await(value): + if inspect.isawaitable(value): + return await value + else: + return value + + +async def _cancel(log, **tasks): + """ + Helper to cancel one or more tasks gracefully, logging exceptions. + """ + for name, task in tasks.items(): + if not task: + continue + + task.cancel() + try: + await task + except asyncio.CancelledError: + pass + except RuntimeError: + # Probably: RuntimeError: await wasn't used with future + # + # See: https://github.com/python/cpython/blob/12d3061c7819a73d891dcce44327410eaf0e1bc2/Lib/asyncio/futures.py#L265 + # + # Happens with _asyncio.Task instances (in "Task cancelling" state) + # trying to SIGINT the program right during initial connection, on + # _recv_loop coroutine (but we're creating its task explicitly with + # a loop, so how can it bug out like this?). + # + # Since we're aware of this error there's no point in logging it. + # *May* be https://bugs.python.org/issue37172 + pass + except AssertionError as e: + # In Python 3.6, the above RuntimeError is an AssertionError + # See https://github.com/python/cpython/blob/7df32f844efed33ca781a016017eab7050263b90/Lib/asyncio/futures.py#L328 + if e.args != ("yield from wasn't used with future",): + log.exception('Unhandled exception from %s after cancelling ' + '%s (%s)', name, type(task), task) + except Exception: + log.exception('Unhandled exception from %s after cancelling ' + '%s (%s)', name, type(task), task) + + +def _sync_enter(self): + """ + Helps to cut boilerplate on async context + managers that offer synchronous variants. + """ + if hasattr(self, 'loop'): + loop = self.loop + else: + loop = self._client.loop + + if loop.is_running(): + raise RuntimeError( + 'You must use "async with" if the event loop ' + 'is running (i.e. you are inside an "async def")' + ) + + return loop.run_until_complete(self.__aenter__()) + + +def _sync_exit(self, *args): + if hasattr(self, 'loop'): + loop = self.loop + else: + loop = self._client.loop + + return loop.run_until_complete(self.__aexit__(*args)) + + +def _entity_type(entity): + # This could be a `utils` method that just ran a few `isinstance` on + # `utils.get_peer(...)`'s result. However, there are *a lot* of auto + # casts going on, plenty of calls and temporary short-lived objects. + # + # So we just check if a string is in the class name. + # Still, assert that it's the right type to not return false results. + try: + if entity.SUBCLASS_OF_ID not in ( + 0x2d45687, # crc32(b'Peer') + 0xc91c90b6, # crc32(b'InputPeer') + 0xe669bf46, # crc32(b'InputUser') + 0x40f202fd, # crc32(b'InputChannel') + 0x2da17977, # crc32(b'User') + 0xc5af5d94, # crc32(b'Chat') + 0x1f4661b9, # crc32(b'UserFull') + 0xd49a2697, # crc32(b'ChatFull') + ): + raise TypeError('{} does not have any entity type'.format(entity)) + except AttributeError: + raise TypeError('{} is not a TLObject, cannot determine entity type'.format(entity)) + + name = entity.__class__.__name__ + if 'User' in name: + return _EntityType.USER + elif 'Chat' in name: + return _EntityType.CHAT + elif 'Channel' in name: + return _EntityType.CHANNEL + elif 'Self' in name: + return _EntityType.USER + + # 'Empty' in name or not found, we don't care, not a valid entity. + raise TypeError('{} does not have any entity type'.format(entity)) + +# endregion + +# region Cryptographic related utils + + +def generate_key_data_from_nonce(server_nonce, new_nonce): + """Generates the key data corresponding to the given nonce""" + server_nonce = server_nonce.to_bytes(16, 'little', signed=True) + new_nonce = new_nonce.to_bytes(32, 'little', signed=True) + hash1 = sha1(new_nonce + server_nonce).digest() + hash2 = sha1(server_nonce + new_nonce).digest() + hash3 = sha1(new_nonce + new_nonce).digest() + + key = hash1 + hash2[:12] + iv = hash2[12:20] + hash3 + new_nonce[:4] + return key, iv + + +# endregion + +# region Custom Classes + + +class TotalList(list): + """ + A list with an extra `total` property, which may not match its `len` + since the total represents the total amount of items *available* + somewhere else, not the items *in this list*. + + Examples: + + .. code-block:: python + + # Telethon returns these lists in some cases (for example, + # only when a chunk is returned, but the "total" count + # is available). + result = await client.get_messages(chat, limit=10) + + print(result.total) # large number + print(len(result)) # 10 + print(result[0]) # latest message + + for x in result: # show the 10 messages + print(x.text) + + """ + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + self.total = 0 + + def __str__(self): + return '[{}, total={}]'.format( + ', '.join(str(x) for x in self), self.total) + + def __repr__(self): + return '[{}, total={}]'.format( + ', '.join(repr(x) for x in self), self.total) + + +class _FileStream(io.IOBase): + """ + Proxy around things that represent a file and need to be used as streams + which may or not need to be closed. + + This will handle `pathlib.Path`, `str` paths, in-memory `bytes`, and + anything IO-like (including `aiofiles`). + + It also provides access to the name and file size (also necessary). + """ + def __init__(self, file, *, file_size=None): + if isinstance(file, Path): + file = str(file.absolute()) + + self._file = file + self._name = None + self._size = file_size + self._stream = None + self._close_stream = None + + async def __aenter__(self): + if isinstance(self._file, str): + self._name = os.path.basename(self._file) + self._size = os.path.getsize(self._file) + self._stream = open(self._file, 'rb') + self._close_stream = True + return self + + if isinstance(self._file, bytes): + self._size = len(self._file) + self._stream = io.BytesIO(self._file) + self._close_stream = True + return self + + if not callable(getattr(self._file, 'read', None)): + raise TypeError('file description should have a `read` method') + + self._name = getattr(self._file, 'name', None) + self._stream = self._file + self._close_stream = False + + if self._size is None: + if callable(getattr(self._file, 'seekable', None)): + seekable = await _maybe_await(self._file.seekable()) + else: + seekable = False + + if seekable: + pos = await _maybe_await(self._file.tell()) + await _maybe_await(self._file.seek(0, os.SEEK_END)) + self._size = await _maybe_await(self._file.tell()) + await _maybe_await(self._file.seek(pos, os.SEEK_SET)) + else: + _log.warning( + 'Could not determine file size beforehand so the entire ' + 'file will be read in-memory') + + data = await _maybe_await(self._file.read()) + self._size = len(data) + self._stream = io.BytesIO(data) + self._close_stream = True + + return self + + async def __aexit__(self, exc_type, exc_val, exc_tb): + if self._close_stream and self._stream: + await _maybe_await(self._stream.close()) + + @property + def file_size(self): + return self._size + + @property + def name(self): + return self._name + + # Proxy all the methods. Doesn't need to be readable (makes multiline edits easier) + def read(self, *args, **kwargs): return self._stream.read(*args, **kwargs) + def readinto(self, *args, **kwargs): return self._stream.readinto(*args, **kwargs) + def write(self, *args, **kwargs): return self._stream.write(*args, **kwargs) + def fileno(self, *args, **kwargs): return self._stream.fileno(*args, **kwargs) + def flush(self, *args, **kwargs): return self._stream.flush(*args, **kwargs) + def isatty(self, *args, **kwargs): return self._stream.isatty(*args, **kwargs) + def readable(self, *args, **kwargs): return self._stream.readable(*args, **kwargs) + def readline(self, *args, **kwargs): return self._stream.readline(*args, **kwargs) + def readlines(self, *args, **kwargs): return self._stream.readlines(*args, **kwargs) + def seek(self, *args, **kwargs): return self._stream.seek(*args, **kwargs) + def seekable(self, *args, **kwargs): return self._stream.seekable(*args, **kwargs) + def tell(self, *args, **kwargs): return self._stream.tell(*args, **kwargs) + def truncate(self, *args, **kwargs): return self._stream.truncate(*args, **kwargs) + def writable(self, *args, **kwargs): return self._stream.writable(*args, **kwargs) + def writelines(self, *args, **kwargs): return self._stream.writelines(*args, **kwargs) + + # close is special because it will be called by __del__ but we do NOT + # want to close the file unless we have to (we're just a wrapper). + # Instead, we do nothing (we should be used through the decorator which + # has its own mechanism to close the file correctly). + def close(self, *args, **kwargs): + pass + +# endregion + +def get_running_loop(): + try: + return asyncio.get_running_loop() + except RuntimeError: + try: + return asyncio.get_event_loop() + except RuntimeError: + loop = asyncio.new_event_loop() + asyncio.set_event_loop(loop) + return loop diff --git a/module/scripts/lib/telethon/hints.py b/module/scripts/lib/telethon/hints.py new file mode 100644 index 0000000..18337ac --- /dev/null +++ b/module/scripts/lib/telethon/hints.py @@ -0,0 +1,72 @@ +import datetime +import typing + +from . import helpers +from .tl import types, custom + +Phone = str +Username = str +PeerID = int +Entity = typing.Union[types.User, types.Chat, types.Channel, types.Community] +FullEntity = typing.Union[types.UserFull, types.messages.ChatFull, types.ChatFull, types.ChannelFull, types.CommunityFull] + +EntityLike = typing.Union[ + Phone, + Username, + PeerID, + types.TypePeer, + types.TypeInputPeer, + Entity, + FullEntity +] +EntitiesLike = typing.Union[EntityLike, typing.Sequence[EntityLike]] + +ButtonLike = typing.Union[types.TypeKeyboardButton, custom.Button] +MarkupLike = typing.Union[ + types.TypeReplyMarkup, + ButtonLike, + typing.Sequence[ButtonLike], + typing.Sequence[typing.Sequence[ButtonLike]] +] + +TotalList = helpers.TotalList + +DateLike = typing.Optional[typing.Union[float, datetime.datetime, datetime.date, datetime.timedelta]] + +LocalPath = str +ExternalUrl = str +BotFileID = str +FileLike = typing.Union[ + LocalPath, + ExternalUrl, + BotFileID, + bytes, + typing.BinaryIO, + types.TypeMessageMedia, + types.TypeInputFile, + types.TypeInputFileLocation, + types.TypeInputMedia, + types.TypePhoto, + types.TypeInputPhoto, + types.TypeDocument, + types.TypeInputDocument +] + +# Can't use `typing.Type` in Python 3.5.2 +# See https://github.com/python/typing/issues/266 +try: + OutFileLike = typing.Union[ + str, + typing.Type[bytes], + typing.BinaryIO + ] +except TypeError: + OutFileLike = typing.Union[ + str, + typing.BinaryIO + ] + +MessageLike = typing.Union[str, types.Message] +MessageIDLike = typing.Union[int, types.Message, types.TypeInputMessage] + +ProgressCallback = typing.Callable[[int, int], None] diff --git a/module/scripts/lib/telethon/network/__init__.py b/module/scripts/lib/telethon/network/__init__.py new file mode 100644 index 0000000..0b985d5 --- /dev/null +++ b/module/scripts/lib/telethon/network/__init__.py @@ -0,0 +1,14 @@ +""" +This module contains several classes regarding network, low level connection +with Telegram's servers and the protocol used (TCP full, abridged, etc.). +""" +from .mtprotoplainsender import MTProtoPlainSender +from .authenticator import do_authentication +from .mtprotosender import MTProtoSender +from .connection import ( + Connection, + ConnectionTcpFull, ConnectionTcpIntermediate, ConnectionTcpAbridged, + ConnectionTcpObfuscated, ConnectionTcpMTProxyAbridged, + ConnectionTcpMTProxyIntermediate, + ConnectionTcpMTProxyRandomizedIntermediate, ConnectionHttp, TcpMTProxy +) diff --git a/module/scripts/lib/telethon/network/__pycache__/__init__.cpython-314.pyc b/module/scripts/lib/telethon/network/__pycache__/__init__.cpython-314.pyc new file mode 100644 index 0000000000000000000000000000000000000000..ae61ff62778f5304c15c3f90c575c960c65b3cc8 GIT binary patch literal 930 zcmZuv!EO^V5cO`-B;7V?Q>m(ukZOqo4Wuy=SA+yb$TWXJ*Iqw9}4|Tfct%{yYp3`soih%bm-0dEmCM zXoLVA1fyUW&=A6HZ->JgtwX)EYr`elfJSN8hfUgo7F~vA8bL%?V1=&2Ds4lXcA!Jo zV2!TBI_*Mt8;zD8p3DaEW6Pyb; zRrPmEFc;522n4t8}G*ZUcaBZnm&r03+>o0S23HpU^&Qy`K*p?SJFxq z*=X;!DBHcW$4T+l<+BeKs9_F5XM#%x!fr0OUR8D3tp)nS;nB>v?1I~e3y!jmFZ0`L z_&)DKz9;u7My_EhlV^f2XghaW3Q*X{{ZEzgeQ(1yWh2hmM2q=+YswKsWu*sYp$y69Jn21c~k}&s*ZdOht%y(H)5R+&I$&`G0v-_GQ$FqW5kEVCX0t`vz zAptjFFjYCJhLOaviv@8!DCV~H;`+u*z2%As+-G_Y{?vjXxCm=O{cj5e&(Bf+9PM0% QjUc>2H*n!Cz3J5d0Qm_iF8}}l literal 0 HcmV?d00001 diff --git a/module/scripts/lib/telethon/network/__pycache__/authenticator.cpython-314.pyc b/module/scripts/lib/telethon/network/__pycache__/authenticator.cpython-314.pyc new file mode 100644 index 0000000000000000000000000000000000000000..d3c6549ea9cc9435993b98493d5373f9fb188636 GIT binary patch literal 9735 zcmb_CZEPDycC-9ilDi@)iW2ofQmYTkqAiQoha+3^wtm5D&%by#6XP#`4z`GJQ^(QqazunKy6Vn|bfeo432yP@_lid-}zWNynSv0%r#5%V{w7BcVdbwRQ(crdgJbc2s9av^~ais9%S7mUgTV?uZd8e&oID!yjN8O$vOp+K6h9i<`d)xV|QY~pI53D^UGdt=oH_@Z> zr}-$L>t(>pi^8I$lVMC3IHBkHNE}3PswCx!v5OLY0+!$mzbMs=2SZ|sbxA2Cf`DFg zkq<2hVR3Ox5Mlx}uqVUOps?uWgEPF~R7$m8{<`P-xrK-r255m#h>{rqA`NM4yvU1$ z-GZFOo)ATJ4@wsP{|q73mc@inJ8A_L^q0Cl$`V)t{3-%^Gz2P_ZD`6gSfuZzyr8T@ zMH+F+hdjD6CV~76fqWDhNy^j6tGWW?Z&69*S+yR;75{xx6g?GO4i@|8DWAdv^6vcksDn2Vnt?U3A%7j|^=b$|^OV1yv;u_&0+&mI8CUun zNvo%sK;^Ox`IH3rCU7r|`E8`v11@6c5Aan4|BaYxf+<}3k$cGzN?v-~UTXYpghPKv z33`(RC5hh|d(HYQJe`DEx%6qg+M(hu5`N;eSLIa+D?q2Kj7cC98VCO3`ihn6CVl0y z4ZZUz(VV!}r=0~&v!OGs2wl?0b*<=}LWdj(+Cgn>=(RI}{NHCLSU+9K7Rs{$pF3td zw?q5%p1ozB3FKuw`v_bvU&rH5m<2^IT2PFG#l#uEi;VCwWIZPpXTTn?ybFqWsg?AS z66~hB4fmBOR=2CF`wCj1^J%^8&b@ub>e&jbXZ`(TE;xC(D1kkR4#$LsLItP=#ey<`$R5ROeYGC&B0GOm7Q#y_0*v!J`%#&Kxnf7m zeh-371S$yWowIH0#Q!Q|U<3A!pD|*-bBkiu9NCDo+{3GqIp-ijdWH)=fMB;Mk zwL&aYj5FeOF!QByAA#bQZB%@uy2v&zlan) z@-?G?5pO8-@}`*0+ej#EL{f4b z=Bt~cDmlYFb8r&b&^fBLGR|ruT+CBIrT+hgO5^{LN^=D-3h%BSEm;Y$!)9M|k-Oot zjK1b=GD1&P8C%M9fl7h9A5O(qrcx1!1qhGe=c+}wRlVO7s)&-RN1kQvun!fZe=>i( z#a}*UO0xaF6U5ijL)e5eZmaN$mn%AoAb3Svg$K01CjJPKkv~LopWW*q5gC!^mDt1K zpCPpUvn6PrKm*EnYlW|L_*!4ORuv@LVDE%B3e#h6xns0Jr+iR%w;bqm~`f zBv&n(^p)a#vJT)|9Y4^w?wqOZ`_AN#5&7Pzi6sv~_>8wrQNC8sbwZ(B`V$29IS4i> zw`pLzug$YSaLc7n<7+2-8Sn7Jy|D1}IlS#vC2J?tOQSk{oxYB$Q9%d6-4^>)=o+C> zI%(lH3U|^)!iBeUhc*?@4A{3eU&DZ+1JP)VKKo!n(JDvuVO&?A{ggv{QHds;W;PTC z3_INW@!y#8Y-&VlM_~%Esk6xg$}r{ZTKaqa*e%FOh&)c=@Rb=HoNrZhNCxvk?;%`jYUH|H!H+2pE&0vX(C}!jPOp4q=;Xa zsJKL3m(((v#1>Ejg_H3QBq_`X0&uf2 z0XNR^LR<((#l$f8{s&8n9?q!|?6B1W_QwJk783B?T55|@01ORO43i9eG$bs> zVL1ackl$KrD9qejUF3f5wIVO=F7iw;3Sz#xHpAcGgWwwrJSLaAcMKYJbF+~8dUaHZ zvHl%<;gBf|aF8D3rX#V?RW1=;;<;<##I>LpnlJ9)z>ZOcc?Y1NE0JaM>Zy(p^#7NU`L>j6>sekJI z{{4do_q{dv!8V!|?an0__zLw3gx3yqs>&K9X~Bww#XxvQQUW=rvfL%K!wwd=W#AQk z=_oE6(T4XwsIsSfUPaorycHI;WGWd-U_O|b2W7YK7!3%;IWR(rvmFW({)|S*c|=lz z(MWncLtuIl{F9`JCBOiMV00#S4N}11P6V9Cll0*P3Q2sqa(=>maJB$!A90+Kcm zhz76mfq;N>xsn?EBYz7hF2;GOE)mJ$;R$LFCU^0y$7E(Qg>*_GavsjN`5lBp;? zUKaWNfuxK^=Orb0Wl0gfA=Sq5Q6|$Xt~q80C+{U=d8o{Ra2yyCPTB&GEg|s zBx+i~M>Q;Ya2gJr;4CjL20*pvc*$6JX%YYyWI9sAjt1Gh_!(G4`Go~WEGdGdff{Iz z#K?LQz^k2*RCpsKEk0ziHc4!uGrDkXnim9|K9Xn{hpjTP3u;~5_xMSRF07Rj=vU;hFw8;8w4+VZon&#eZhF#l?Z`Khvr?r(S0X_p{omV zSK-wX?g+=lgewxBc8TzY2*QD=E6R&EW5QKe#e1cg&>LTrYG-1Bibl{B0f*T*X`lE{ zu(xke61^~?8q-IUcPBsTe>nVLcy;>cN7oKKw%nP_ut$?)`MRe2BlkwKt@|INy2dg9GM!K8nm@n&;qB#xEN$B~w4`iD zGlpX+?Xjmi({jW8rh82pT}$%h3k_nMa!mU=)4tlDVY)sXO`h0f3^~TW&e&Ii8HW3t zj$HfTdi&s`3z_zzKc-F?G7!zaSSr3XuE?LYtHwOgO5mJJ!xSaRa2rRkHs<exNiBvUo`-jHl5y5QuPzm7E%}tTWYmY{|u>h`ez2@ z=zRFzU%!`ij=UnIp2@fN<*n^67|k9KRpX#vB26QmQdwZD*@nEfG25~?yYE=4`S?pE z#h#>|Bm8`Z6zahzS}xTwu%R8;QbX-op+k$bjXBz$rtPZ@pW1$5%hLNQcl8Nv&C#7{ zx^qpF>mEsWk7Vf)xqmEU7*A=(pVGA{b9=V)tsFhHP7iGwYCnqJjc&50<)&P1N4mCS z_2ioNu?4mvQ+qVS9?P-gX?8rrx<9vUu#Lt%Z)ePBQtX+hOns`ME8BfA#~fN`4gvq_!F)+1CDc;Pp%+?YY{(h<*A4x7 zts$pvNo!kH{$NX~Fls(Loje6=Zr_zjYoi4-GMyYz8ctI2VvKB2hzqPFKMKAV#R?awxwy? zs^(Mn7i^a9kr#M8V>q4Co_<0%3I zSK8`&tj-#bCf#|Ur)^4Wo514qwac!Yc~{!JYmLw92a~7r=C+)9U)sFy*UDdy{^P{2 zCjQ|}X8-XG^YP@Fr&{C3iu?MX>Q@H-(zvG1YI`w(nmfVe+GTe}Z%>}Wwa{|k@_a_$ znmh#-R%2MEzGT~%-(97C<_AD#>_ozC zBY8H@Hm;odl0CfYUc0@~IRcPOdM&rbM`}N`=N||%)Sly?b!3{~ zPN}TVG|19iaG^u5%wyg2f>v=rS!DquRT=2y_zMKY2#>vny9Y-Pf5t zDb=}D&**12HhRuw0A%*$nc5uFlE%8)nPxhZqxquay>b8cz1vy3qp+Sin>%d-Ys%Or zGlugi?RoIeIl4JbH?Pd*9DQj=UzY9z)I57I#~w?w$39bK*fCH+<7w~;lj4n{4I?M9=Ics;?+Wkw*+U(!iGS;^?>fVA)ski6q`_lD&kDQPF znfkYrlTQtBV5A)TbB>|3W9TuPabWL|Je{Wv%e`5elQ|v97!Iejhrx>BaGA?7UF%HO zrooo7A9$?Uuon-JCrsV)dntxXskrAC)yQz@ch5Z(s&!M(wczDmdMU6NFujzn`+uGb z6s+m*5;$q~rziB6wbXA|D$u3+P0eUORQ_2dKSu@t{+C`$z-jtiaY7B1FK8;ztNKDa z-U5|>>o}na^r=&OCU8wsQw=p#|-pC+M@h{N-z?O@;w|R0m5*rgV3Rvx`3VVJu)4{-{xi!&pIUN*vY4l@=LAjA~#X9rG&&<%y)EcEKjV#wVIyir|U zLKx%plE=RSkZ6Jjghgh=8b3OE_vp%=N9xD3caCP5F$i?Fpc6T%OHNLr1A&=X2;%lV z&?Jn)13q(s&=5j1a)XKV%7JC!x~0Ns^8H4>>EL^Xd|TLgH$a7pjYhtl#N@k*d^M2o zWrfR_284_W2|>QR=^<$6qX`J^5`}9FD@J-tUmVGIzrqDezKG!XPZs!Zkr2f&UI9sa zxDa0+6>dQlC(;sU;qgpKQPg(|C8gTZB1-@7i1`XNzEmkG#Y+T_|B@fjWBnQ($e;sX gq4uxPu9pf0rT7lP!Gynhq literal 0 HcmV?d00001 diff --git a/module/scripts/lib/telethon/network/__pycache__/mtprotoplainsender.cpython-314.pyc b/module/scripts/lib/telethon/network/__pycache__/mtprotoplainsender.cpython-314.pyc new file mode 100644 index 0000000000000000000000000000000000000000..e9683f2c1402633034d04ab4e33db2077edfd43f GIT binary patch literal 2719 zcmai0&2JmW6`%d!l3J}OOA4h}a?G`9Ng+*URPMi ziAhPcA{L}Y`C5cUd|F;qu0>fii4y1>Dnv$5L75hVl~}AGq)J5rZI&*hIqcU;|Uu!>9UmdR|_(Qo6XUNNb@ih*kp3=5X@CT2PHMq2Q+*KRHmm$`2+ zn1}J;qO)OE?b3W>Z4HyFgt&xD&D)Mingwi@FtNfY@vqPw96Sev39gvqzia;sf$ zsV}BJTVbqDXY+Z>B{;{r;mNtAoUgGucPd}P)FO7B$05~luw#*C9o(O)VM@(1rfJpc zCdhXr0zOCxX$Lbv+(FBsbKpc#EQl|5WsAr;;=Fmv30D9qa7k(v)kVioOSmJw8d1T& zpRkFC?nDdyIJrh${4qOLVlNFMR08YIpyi=(sbf1U!gAt=vvYzWG{lBvpm4XkT$Gna zLdoOr5kFFlte}^Ehfq1j_j(-_q>xt;enwX+N}#*>gs%KO-P6bWg;dC|D@6r#??0g% z33>j$W1+_JH{>^@22=(9o8|`!S`wvD6utU2{6E#vMKl>qqcULZ46?-}dIn_puOOz- zob(K`g%>1SNGq)wl~+Hin@&k5*uwTkw|Edt8&#&Sxx}vyWnefUvEINGYIsY&yuNPV zX!XyVCBN|V>R{NVmdYn;w0uNYu~TN1bli)qHW{XbL(}Uw%9t4r-ZTPH(#vO|YB(iN zp^P*v=E-$9=AH!lo(z_45biAD`1E?~n}-Nbk&k=adT+KFtZKf5JA z>Wy!)yO-{*zyI>1!3*0r+k=@ME%RO9=?51&eaVM?$?ciFQN1&o*&WTaN3&bk_lC}H zpL;lz+R2 z69N0m{n;HYyREgg?7i9j7t7G}#_XAD%;T{V#pV#{*#dV+h97VGL%#n4P}l z|LDsQt3O=1c(q^nd`eh&QT{x44(KoXg@sG<7pI;F`b+UjW??S!RaS$+SC<6d&-Gr_ zdj72R0G$?nsQ4(lk?!G@+%anyD!FPH!G!>{W*F-Yv)bkK8^)SVD686V6}!NW8b-;r z41@3j7$i4g_!pUN875=IUTpwWL2|$_{O3{0wwQ&hRl|T+N#7HbpvG(ZuL7L#@=SQg zp-1`g|L?m1ipQc9iTyhkjZ7R4o{ePo5p;x?NMAPHL!RaIFm#^QeM0!RYWIr8{53>< z_tada=tv7=B6k(}){K&qKE_34#EI2=YM;3GsiRiEq&4 RH)#A&$qJbR1f9Re{{c*KkU{_e literal 0 HcmV?d00001 diff --git a/module/scripts/lib/telethon/network/__pycache__/mtprotosender.cpython-314.pyc b/module/scripts/lib/telethon/network/__pycache__/mtprotosender.cpython-314.pyc new file mode 100644 index 0000000000000000000000000000000000000000..7ab9b7765d07af493e6e4dca554d2cc16ac84f1d GIT binary patch literal 50377 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 eVmCVfD__9QU78F_J?sJlH_tDmTSNEF+wv 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 K=v}>!b^icnSmqo6 literal 0 HcmV?d00001 diff --git a/module/scripts/lib/telethon/network/connection/__pycache__/connection.cpython-314.pyc b/module/scripts/lib/telethon/network/connection/__pycache__/connection.cpython-314.pyc new file mode 100644 index 0000000000000000000000000000000000000000..9221fa32717f61534f2f15d95a020bcccd594d0a GIT binary patch literal 20211 zcmdUXdvH@%n&-XJ)mygwke@R6`U&z&JPZUd7-I}J5aS5PWIHB8NWvD#l5!=7U?v@g zp5BF?R0Ca`!SwbnWIMZo>1>7R>77Z}_D+WY38~%Qv20J|R@^nIUAAibkF7P>)k(U$ zYWMe@t1HQhAY^xEtM`NQ8r z)m#Zz!#RaA&Z#-f4OA+Prl0L<8II;NOf4{Vhx1dZ7M7|aDM(vxjN*!lw7;<*VL7MyDDMr3%=wI_`ThuKF^8B_*jo5 zg``ALGIi(pv12}orP=v}ZqyeD`=p3R9)}zY$(O|+18`$n4|7r8$zd9;QFSR7qOJ)} zRaE1|)Hwk;R8CD97f?mDhjmU&LVL1;s2=YnP95_2RGPtA>co@;_^1((fk7rfMh2Ok zrZO&?=QJbEcjh6sIP(z~I4y_^odt-CoP~&sokfVP&SJzRPHVJuh>MmvRdt-bEMe>p z1%p0M)E^4E|0BHTN8FK9L61Mw z77F`EhaBS+ed#)IQWSy5j^Jn0VvME_Vg;2ZM=qoUVO6H8E3at9;fkQ5ciLS7)c z@fQt^`8^_Pl-$&-NQ?30sNXXxMn};Hxida@1jycyKR6;r{6UWonLg3?ayS(6c}0IP z>XVMSks6^kPl@62DC!M*jUzq)o)C2;>Jz>0s9OxXPX$75Z$ylS2t_<5g~r4e$9?0z z2*yB@5elLTAMFgZ;*QFLk=*`>PezXTP>hO7K9A4;l207-MMp!Z(H}7$$(TvDk&YZ` z5#`~qIgzV5IqC~y+)>|HI7&l8S6HR~V@4X6C*Y^K!sxyJNOF=f%+(lpV4P-6@{NUP zJgHooHhY_~KcNXrp_fl3jDe8H9dNn5Udf|uSvrr&fBj-RR{jiks34f!jvRML3?fYq zht>pD3OpnXZhSVEjGFnC{ z4AZnM&%Ep{Pk9@zQlNpA3Y31W-WMjP3Q33~Pbo^?4FzSLo=m@rZ|=lY}mL7!#UjX!|Bia%r}g@xqF;7rQPY zM0|l`2^D%Ruuf~_KI_oY5zsh^mPMk~h|qF1BGlK{C;r$$N5(N8@kx#~4xFPl_sA*7 zF@L}pakP#2#{5CQ!ySzJMV=88*R z)6bPuyk?v$wY_GVv)X>p`*+rcbHZx|IlMQ1LwO6-&f7|uK8Cx?YlrK;CSYN(~_Bl`L@Zg2!5JyeF_%eVq+%RG}9K$xQ z;)dxMvfxZ@&rv>&HIO=33LXb$JH$Cv891p!mc{|k5jG{uVykNUC;GbG!C)vVVg*IE z8)b?n`Cc6NOCTf%5AFwf!%B>UG=uWcyS!ZDQ6Etz_e*YnzlMPd^yltryQs1@yIhh=?b^^7Qe6_(YSqbzEz z98Obb%|LN>?L_qz`ma1k`9m)rsy}pay|#|#QrlG#O0hb9IGmB zP>(h{fRStbGKK*d&A`Yt78%3%f6(u`Lp{`QyH27#1tHZiK>U92RM?l$vQx4l`D{-J zqtR%1qWan3iD2ktFnRnz1fn5NC?G~rWQ%zE6&z#j!2{j<4x%7?qG0df;DFtfF!~{C zMWSwqTM2a}DkU_5&`EGCbNCd|ZgyZJ(t316+C3{HOFktm-KUmws^H8 zR_&NAbllwXUg1wV-#b1vI6S+1csbJ5Z(jN4Y+*({&&}?74k%Z*U)g?1f4@|ms@WN@ z>5J9$-D{eud3L6BXsTdnLCaaIuC?EMYO1(v%G`y{<*kpI*L`ZKn0)57SFbhQ)X&yF zHD{|~-I#pwpH^Sndvo<{{g$kx#@TvcRex18*I$;Kkttt_ZDHPs#eIud+&VzD(8-ol zzkw9>170!=WBIa}6hOVfE9;GwE+Gyx|nJ{8Fos z9vMx}*{ajzAbOo+Ognkja_ewPB6V^DpuO24O{a3#qfn>pC&Ypxr7ndJ^zh!>)LW)O z^U$DG!&U`H(XYb-`JViZh)pHD-5a97{gnX%z+LC zNnG$?TP+M=KT7xvDO&S_M{>FgH5 zIveJKNf1I`ZPLG4D?*ual=wUr%7RVt!svY9C z@=2J%}zG zfku~*SO}- z4luFD&a<9KPXo)Ymx!goI74bItQxROm|dq5mgZh+_6Izb*7>oyo5^(q>^qRZ6nZ^IhqZYE%LAn1j zqAgQ(X=C;?m;CUDr01zYtp^6Ws1H^P*&-#8ej$;pq$n}sFsvkmqyC^*s-SxHC*3e`;qXg1J}_>;Xzh~5 zgF&FdbQlUJ%ru+I#+G)lzA$Y{wwM@DZ&K4S0!%KL-NcR)TM%a1OK6$u<7F$)1}j_r z*pdp^&OYe~s%G^oS`|~NQd(91@*|e0NY8lI={t^!A_;8MUt{OZL$~r?K5^;9n>}y0 zU)vTpt(`Wlg{3ZD(l}kxcwKnMbi))YX+OVvKCf(2xN5p$dUG(I*EF5iltPM?Y`AWH z$9BU8;Qa3UMYgG`&GD)ov8o++tnYb0JaOy9OqX+}>flV#;FNigX>6}`Eaj{7*1q}b z5-dDxp}IM``PcTx&0@?f&YGL&pW2gt+c3XjSNd(;e>HFZ)LL2dtZ@wuwH1s%7td65%V7JbGEhKFw8(ddb z-3Lbh8~rz*jc@3QZRmM#Jig~pY|o*o_U9&7Eor&(m6u=n-YeGz-`*H+-WF@#HdD6! zqA*urjTeZq0`XeOY{4qLT{c`YOg7CHG@wM;%6MtxbZO(f)fTt5Oj}!KtZi}Y)|hqc zd)6uI)*0&|j9^KlE<@+lg+(xkAu8Zj*5w>3%%TehHd+mhR#RSdNm$s$v(YYY;PNU+ zNiM#4Z1S1c{j-+pd9m>=%Nv%P^?$wkFIK<1>*sray7wM`Z|&6Dr!S4%M_&FL`8Tb9 zUH%v4@3#MZ%TKpVt=WBXgjC#$cyV2>rNXUcT4$xvv9YpyA>}V34Fgr_(-#c;0=6#k?_&R?(KN_MLXYLBK%@Q zIpF_f;VCX+aS6rCxGmY^zeU{k`{28>jtxBriP^#T&zPn{+?j>kSZV!Rk@yvj(hJ=Q znvU-AJ69$)5VqUWIUlksZe|4Fp*^zocusa zFICc*KA3(e&JEOLRe{$bL7Bs_@Reg1poJ$bZSrP~?!in0s+ zf*YvH$Vh?%_AjR?WwLt;uLDT1kl%lX|JU*F!oQix^4k6hQ8BYJQCdt2NMc%|hXgK= zb-)*two)3Y&rD2WG8K~`q*H`q z$-0wIOD+;klSI~W>5nN|L+Iu(Zs#C4u$x7xjFODMXl(u1{5*2@g??Vh-0 z-Lz%hPa^NO$G7f_ZQVDsY5&x~GqanXnOjjKTV5|2=L>C46;T{1G`X?MJ4Z>(nTz4rLN=VSYxpQ#z1DLp(@aQM@T73cPXvR^oT z;q1+x3|lY!TOv@v@d! zSG&&2J|q^kTu%crH4@zPbX(pA@mnbKBvBdIrEu$;Hx zPI4~4=tAIpAf7MA^2M3_+PJ=UT3`F16`guegytR;aZOJCfu1weeGwVL)SunivB$)} z-`Sg&Jk1DV%u$tu&vA`o%) z0By~zfMcIx6zMGGh$|!vEv9OJWId!vWrZB|q?j6c+kikEX9Pl+Xg2mh0Xz!LXOz5N z;FF@n{tfJHT)#MQ!QNSY6keF3wE~Iu7d-nHXBN12Zbq&sELb{Cz(@)7HxjVOymZI#m_%nAH73Iu+a-ZBqb8;g@fAlt@|OWgLo?K6}TKa!?MOTz3r0cC5~n zMp7IyUB6TJ)X!ku?x5OjT-uhOOG06iORn}wbIl4$`!5)m$ro8;W@|v}fPWm2KOrtE zX7-{ot6IFm^G~GSR&#?~uJsro!%y)>_l;sjoa&_fa!h9#pa5OrC{Ji3W$jE?x;u+(vl+RW~_9i2@3;E=?FsCw4z=x|# zBmC>Uq|jkBb*uh|x{A!1kknuh&r9U^;C znl`~au}e$IN4m$(vwfy5otG4APcP=E0*P{}VMZ z>IUuAdg0XzubzALm*%2RW&fc2`6U-Z-wBb%*>}EguC(%U=u#;8Skeg=BeZ^lkz7QK z3uX>j9Tz&zbrAag^RPIqjG0%?n#FlbQQT4;vs7Q(IBT&lsLX}?`1#5j5W?xoCQ!(Z z8y)fH?pSm8OmojYX|~cCw>qb-&IcN)vVW z-rp^l2$_{X+eLhLwQ##wAh=A2^xM@u z!8L1oEb2RjYNXt;>Iq)KQ@paV$Edz5Xpw%`s6onI3*mUkfT;x9NccA|N&wCf6;*OF zo(fGI0WErz0~JaN8AX!d0PWGNkhW2iN8iMA#)CLN#Fk~5m~8UQS2Q{bzt<7P6E)4U z$ymmmMd6|;JAc9@GdeB>W!8>DaO(udZ}a-$e3j&)J}L2srBw2cyKX}^-u4!jsbWpcO97u8&I%oMGgB42=Fd85TGbukNUC}74@ zdb@mwpDB>h%zuk!9_-+WxTqHz2-D0n*AHbWJkFaboAZ(0DivjDn&=7YRgLmPd}^gx z+P=NeH9=3)wh3qIu5pwH?^wkst9s&jqf_?A{Rddae(7IhfXeET9+^`+&!&j+tbt(1p;s(41Hw7uUqZHF2>$ zCbrLr8{?H5&+j|8`|mA9bEOp*4S#PbIsejJ*@}yK_w!dwhGz2Hru1zO@;KY3FCxS` z&lYu6@vpBy^kcrOQuxafK)3Sp5igUVNRni0UzC!daHILyv*(2-0Us!CfQFY9p8V`7 zQbP8gNONn{l|#CEoFJcFS2ee91YX3lgwcjwVuXZ|l45dMdms^DMo7X)$_gZcvVa^A zCXy9~NrPr#kWWiZU$S077GbkQd@m~u7R&c7*Glo(*B%oz8 z4az8cKIk}Oa+HsiRfLIdIKzX!k)A-V=%8Zc3l=5!`cODbVgme^$08t>$C&!UT#Zw% z>=0|lAe~IK4W{H9h!xCTpCq0nO#}-k@Av?TKuq45Xm~aVw|;z|0O`c1uz3hRNeu}? z5Ov6IVU!du*i??uQt97df@CAdcL<`XPUOS?m2T2}UUvT@U8NklkV@$K4#-JNP`O4) zhE#jf4+-xLM47ULbb+ukg$UW1D@d$3+L3rHz-2*R1iH zwXvGD@tO^>nhi5Go8q=jvjtC)7!kM>h?lO8m9CyCg@w9ln(_PM+F#l>{j#9x^Lf&# z8e*1)C5|@~Cd>u%mhvS|Yp9=x&sWV44k0sb{JxxjDSxW^z~2=Nd@A1p#0%iI5* zaH~N}aI*?nw_16E9V}j3vP)3EuT>-ceS@ChBA(*nF0s2)eS1CMy-|C+T|@9@Es$<^ zYLI7mK`0(J#1qCM$7d0#{g&btYNe0*f z(z&z3qgtnrmzd(y1F}>LvXmlqxs=CDvb1j_mqElb%B4)c=`O5Sp(LbYkS!I6LyDNC zF>*P}I#q)ycRZ$QJ)t<|8ec|lpU>!R%DDf()m!2JL2rMY(c6^bjoyNNK4$OaU^bcRHdgx&Vdtp!)uczK zDB{cw=Z;8X%-7tJN)58ff1>Bn>~S1-@M-oavo4uC;&V4}zf-Fu9~1_+ml&jYkeU5* zGe^?3CsrphAYq2Vgni|Ga2Pu!5-ribkh$<*0ZIzb(hW-emxz{0 z&eBDsEa$h>3+5BiPyF_;5Xt&DAu#RryBc53Ay~){DCq>N#7rY;lZN zw#6#jZVETO*Z*jyvLkNon6`E-V~n_4eqiUS+8)$%CEGq7k;Wp5x(|@GV5^PcBX6#~F>;vFr*<<($knT=em?9v5J1KveiwfCYr4QsZ^Bd#{ zzG@42#W8%F#=d`pQHNFNXX?r+G3Y}dl3(5$WphtR9K?r-_?mHyGT@-)$ETLQYD{_ej#*$$AMVIbxR?pRY;h@xGj{u}>VSfdJ99HEeNl zrw9w;*7I90bYAE@*SXA`I2pZq`pW66uU>ieW3%IdnlsyE>rSR!hBSQa75yUJc06*g z(2mLqUt@Qq?BE%zk6UutS5lC*vA;p}^3>${z$Lu+Du>-XOV^Q+Y44Iqe@YZ|r|fKP zOuu@Wrlby8xH!fQsQJ7(uCI*gD<|8pZoRTKZflR(+HVfd*tUGE-}*=!mwpk_@MCRU zPj1yv#tL;{nJ)tq>je@I#ryJc6Q4Ux*u{e0b zU4H)3^YN0FSP5>A-x{rH>T-&p44pP`gL%}-4j$~nI83-dr0CBnx=9gPFql}yXb?Ma674?e zKTtG8(IG^jS^o$Rwnuj`D<)eI7Fj9dbwr<&SMdXtQKMg|k=KUx(}OOig0kHg zY3<$UxAL;AMA+gow}$2KcHcBsw-Hq zI{;)dm52i*w4)${Fpg&vKgJBJbIoa6+8yb;bZ*zPvBLp1Uey+>YP%ktscL7R=x4Eo zxUD5-Yl+*|$876A*0=vbaz-{eDb6UpPt%l}A6Dj`8nSzSk7lJoo)tZ~Vns|}5!YA8 z^wrmdw@h!C;^O9*xcS}s8L{(Y{f<9C-YDIrakII|-WH4}K4R>|9bc!hH==kB?ig>~ z9&6n`Q_>l?bWZ6zW0uY@qz{2==UH3uj3<*>7Lk>HL1{A-(U#1hjVZDy{Vg&wiDHZp zDWcOKX$6oZiYgFAEsJpTdgkeef=aXDp`O=t@t=WS?cf(UirI>=^2>;o!iCwdM?49! zI;_iO;Uf~JSRtc^N8n`sB9Re<&ICedP7*rP0F%?YA+mY=S1`hfmD%(yd0)zW2qU0b zO4l(H9KmYRRY>Mp{#!Fi?O+XliwJcy!5=~rnf%!=i+Ao&_tmUO-1(Np-+r_G3d7vn0pPx(b81)1B-e}^#MH5ewhHOb61|)}3p4qSXi9uYL z($63*9|QX}q$4D;;%g8wbo4mZ0csO|1u3acjk;g*8D)f*-5%Tqtq$6cBrn0+jM9Hb zE6Up7N|dTuwk~ww6J?p>Bc!Bs{9_s&$x?KtmvARaQ%$X-zFW^`kIg{l~_d`L@h`4^& z8seI=n5OK3O3>7jI8(kr(6Ss&GUqocK=PTV7VL%6f1}naD3U1pK1JWAs0mTRgzbVK z7?O=?+bBf~{u#or_25_~O=%wyq@g@@*UoNPN@G-s4a7sa4HVT9ioOGiD?VD=F8z1B z(k74mV?+yTp65T|jGu7%pKy5q76dh4{E1MppjY$dp9p22={dghS6tbWHmKo^7mF{K zT`F7R2wpmBD&%+YlUtTJdM>T5=4&t3U0!u*)o(a}pD%dKoW2-_Grkx;t7rF6Q^mi- x9LkC1bH!z6_ful|ZzI&j?+INZcUkCiaJNK5SBv^qGf%O@(6v>4YYUI~e*yV${EGkp literal 0 HcmV?d00001 diff --git a/module/scripts/lib/telethon/network/connection/__pycache__/http.cpython-314.pyc b/module/scripts/lib/telethon/network/connection/__pycache__/http.cpython-314.pyc new file mode 100644 index 0000000000000000000000000000000000000000..d475c87bb4da4ed6c13040acfb081aca4da80ed1 GIT binary patch literal 2703 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 S49{MZxbDT{eV{SEfZ;rHM~V3ft0bQg-gM4J2jj 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 F{tKkp$0z^* literal 0 HcmV?d00001 diff --git a/module/scripts/lib/telethon/network/connection/__pycache__/tcpmtproxy.cpython-314.pyc b/module/scripts/lib/telethon/network/connection/__pycache__/tcpmtproxy.cpython-314.pyc new file mode 100644 index 0000000000000000000000000000000000000000..e39abcf28005309cd957689de552418d6e978cc0 GIT binary patch literal 8471 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 literal 0 HcmV?d00001 diff --git a/module/scripts/lib/telethon/network/connection/__pycache__/tcpobfuscated.cpython-314.pyc b/module/scripts/lib/telethon/network/connection/__pycache__/tcpobfuscated.cpython-314.pyc new file mode 100644 index 0000000000000000000000000000000000000000..aa38cdcd9f085bbea216f2c63986a270a3013544 GIT binary patch literal 3437 zcmcInO>7&-6`oy^%OyowKNdw+vZIw`E1_kHvaHyOV%L^sNmk;>!iEqK!_sE8Tv2ON zTxND?g)IUIZIIG{(G)F=st2Py6*gL+Iki3Jnwts2Cb4mW7IhA~Bytoq?WujUKh)Co zwF7YWoA=(#ym@cty~p80tziW1*%#m3tOgPK2OYRA;DG%b0JhO85}5@=&?+Oc3(OUk zuzeK!7JOIy#1Ggf`WFIMIKuUzDC$RIAcjP4g0)v6L5mgy8o%P7@HtdoWCl?pv`dGJ z3C27;y@s`PMollt$}N@5=xJ2}a@V!B^@5?uL`~1=xtyvHP0s;6JU#pFJ0LTY5*2$z zyLUE4`*SdsLHQL#7!eV6HAH;fNQa{kzsSZA39N9UFNP+Z*(50Xfyz_mNl4^Kco~rv zOUa-aInZL^+M?oaLZ|xS2WR5UMSe4T%2C#)S0-#09X++wvsgb

23Fs6hSkrH)*>y9lG#q5m6AlfTA1Bi?k&^{O#mTXxqHm7M znPPHX%c@3lG^1|9hDkX`v<#MSYos_L=krNJ!7@=clFOGzE`bV#d~#Dy7qTj4XnA5J zv)Wpcs9BY4=(%K0C0jba1#7#PBT1CJ-ncZQ`68$)StpassMn99E;$0q8ne)Cqk62r z7E4rOiIV?rq-n6nqu#z+&xuOUi3d});Y*d_OVys4=K(epFLmARt%q9g9)<4bNB61y zm4-86KpkOSk*|Tejl^)C=|%Ku+3jW5p&zYYbX}Wgm)TWxvd5K2X!V=PErc?#<`MMv ztKfK6WW4-k<_*}%;h(kmtd}qPJoD!f0&Nn_^ebt_H=HQ$FwPyzd4C47;UR z(eLFo=K7mt0xy*bcw~Gc_fi?INhbJGnc$6(tIvD0ZrI859%`mR4W-bM75Lm4jdSor z08{)@Ycq$AJUTS2h@3Hl&bv>9F*T6M-%^Y4 z@Y06KIqS69Ux+HVRcxqfv&ESrfv6dF2ik43#fcm?!yN!4^jSyG&Y5b*K#8mO2<6W5ba|~DyPqtzeaE5L zk)65n#Qm$)?nJ5OX%qy@$M2u7MiZs*)9&7#YY%1~9~rB5pR$=39=!gz_f$1HUJ5_! zdS&Ow)vmLp(9;I`Tl`opK3<8Bm$*{lPHR2hUyCOy@x&jFf5!cOq#B?6OZ0L4g2Qa@ zF2C`pZR8O@g1h0AP5FpA@24~$eF9(r^??l_p8$TbEkXErv4Ny}BR#C8#&bU)E;H@} zm`5VI32S}GZ2PVT!pNl$q9D4;zwXl*OrIy*&UW}<2v-PGmzphB{`pW=NVb?@aR;R_ zIVW}4Zaz(E&2IKgN&?rQ-7Xxm-gfxYkM4X_YdcnHJN97WkLMnqs}0XohG#yXt`5)D zj?Y!w<{$C%mE&{&HV(m@pG-_M%%7MG{GUSr{>OxHAby`F?f$nP0zuaI}UVX5IV3AANe$LCsN~kDtyn*#Bb04=6tPp ztkOI7Sy#39%oF~sb(InU+e_A-)JvFZf~mn05t>MI@}??DW>}K!tPD6JNgovCtix%O zq;(A&B&+4roGwY$Tufi288L`VG-Z=!?zHuUF?BGuw2w-X&6Z?Bu(no!NCb;_NRpD3 z4I`~7L{YO@NrK|9)kB17G}m6VFATk@n0EBa8MLN*(B5M)CbXhh%i_`s+zo1NUL^T8&m5airY8Y}x6}B|85taozWM_*) zM$M^EcByFrg0)aEv|Ps8465uAD6s`_wkjC9M*&b1tKkh9LkTGmT}W$&qT<|W_#tGX zFrESv;WueV^AtW09Tb{UT36soPzQMuOj^=(3=a-jsRdu4=rZkSaI^h7LlN!Hz|Wv5 z;1l$m4FveT2#WS^zf(Uv^zl_&o=PP`nEJ?Ufm%>@{#V#LbOIUsDUY+NSKZ2XmPXe4 zFKle`=j?m_L%aZ!=nXIu(CzschWQeOzCtJeicWrogzvc%%;a+f-FMd+o_Y1rE93t} I@U?XMFE@7g#{d8T literal 0 HcmV?d00001 diff --git a/module/scripts/lib/telethon/network/connection/connection.py b/module/scripts/lib/telethon/network/connection/connection.py new file mode 100644 index 0000000..8236bc0 --- /dev/null +++ b/module/scripts/lib/telethon/network/connection/connection.py @@ -0,0 +1,440 @@ +import abc +import asyncio +import socket +import sys + +try: + import ssl as ssl_mod +except ImportError: + ssl_mod = None + +try: + import python_socks +except ImportError: + python_socks = None + +from ...errors import InvalidChecksumError, InvalidBufferError +from ... import helpers + + +class Connection(abc.ABC): + """ + The `Connection` class is a wrapper around ``asyncio.open_connection``. + + Subclasses will implement different transport modes as atomic operations, + which this class eases doing since the exposed interface simply puts and + gets complete data payloads to and from queues. + + The only error that will raise from send and receive methods is + ``ConnectionError``, which will raise when attempting to send if + the client is disconnected (includes remote disconnections). + """ + # this static attribute should be redefined by `Connection` subclasses and + # should be one of `PacketCodec` implementations + packet_codec = None + + def __init__(self, ip, port, dc_id, *, loggers, proxy=None, local_addr=None): + self._ip = ip + self._port = port + self._dc_id = dc_id # only for MTProxy, it's an abstraction leak + self._log = loggers[__name__] + self._proxy = proxy + self._local_addr = local_addr + self._reader = None + self._writer = None + self._connected = False + self._send_task = None + self._recv_task = None + self._codec = None + self._obfuscation = None # TcpObfuscated and MTProxy + self._send_queue = asyncio.Queue(1) + self._recv_queue = asyncio.Queue(1) + + @staticmethod + def _wrap_socket_ssl(sock): + if ssl_mod is None: + raise RuntimeError( + 'Cannot use proxy that requires SSL ' + 'without the SSL module being available' + ) + + return ssl_mod.wrap_socket( + sock, + do_handshake_on_connect=True, + ssl_version=ssl_mod.PROTOCOL_SSLv23, + ciphers='ADH-AES256-SHA') + + @staticmethod + def _parse_proxy(proxy_type, addr, port, rdns=True, username=None, password=None): + if isinstance(proxy_type, str): + proxy_type = proxy_type.lower() + + # Always prefer `python_socks` when available + if python_socks: + from python_socks import ProxyType + + # We do the check for numerical values here + # to be backwards compatible with PySocks proxy format, + # (since socks.SOCKS5 == 2, socks.SOCKS4 == 1, socks.HTTP == 3) + if proxy_type == ProxyType.SOCKS5 or proxy_type == 2 or proxy_type == "socks5": + protocol = ProxyType.SOCKS5 + elif proxy_type == ProxyType.SOCKS4 or proxy_type == 1 or proxy_type == "socks4": + protocol = ProxyType.SOCKS4 + elif proxy_type == ProxyType.HTTP or proxy_type == 3 or proxy_type == "http": + protocol = ProxyType.HTTP + else: + raise ValueError("Unknown proxy protocol type: {}".format(proxy_type)) + + # This tuple must be compatible with `python_socks`' `Proxy.create()` signature + return protocol, addr, port, username, password, rdns + + else: + from socks import SOCKS5, SOCKS4, HTTP + + if proxy_type == 2 or proxy_type == "socks5": + protocol = SOCKS5 + elif proxy_type == 1 or proxy_type == "socks4": + protocol = SOCKS4 + elif proxy_type == 3 or proxy_type == "http": + protocol = HTTP + else: + raise ValueError("Unknown proxy protocol type: {}".format(proxy_type)) + + # This tuple must be compatible with `PySocks`' `socksocket.set_proxy()` signature + return protocol, addr, port, rdns, username, password + + async def _proxy_connect(self, timeout=None, local_addr=None): + if isinstance(self._proxy, (tuple, list)): + parsed = self._parse_proxy(*self._proxy) + elif isinstance(self._proxy, dict): + parsed = self._parse_proxy(**self._proxy) + else: + raise TypeError("Proxy of unknown format: {}".format(type(self._proxy))) + + # Always prefer `python_socks` when available + if python_socks: + # python_socks internal errors are not inherited from + # builtin IOError (just from Exception). Instead of adding those + # in exceptions clauses everywhere through the code, we + # rather monkey-patch them in place. Keep in mind that + # ProxyError takes error_code as keyword argument. + + class ConnectionErrorExtra(ConnectionError): + def __init__(self, message, error_code=None): + super().__init__(message) + self.error_code = error_code + + python_socks._errors.ProxyError = ConnectionErrorExtra + python_socks._errors.ProxyConnectionError = ConnectionError + python_socks._errors.ProxyTimeoutError = ConnectionError + + from python_socks.async_.asyncio import Proxy + + proxy = Proxy.create(*parsed) + + # WARNING: If `local_addr` is set we use manual socket creation, because, + # unfortunately, `Proxy.connect()` does not expose `local_addr` + # argument, so if we want to bind socket locally, we need to manually + # create, bind and connect socket, and then pass to `Proxy.connect()` method. + + if local_addr is None: + sock = await proxy.connect( + dest_host=self._ip, + dest_port=self._port, + timeout=timeout + ) + else: + # Here we start manual setup of the socket. + # The `address` represents the proxy ip and proxy port, + # not the destination one (!), because the socket + # connects to the proxy server, not destination server. + # IPv family is also checked on proxy address. + if ':' in proxy.proxy_host: + mode, address = socket.AF_INET6, (proxy.proxy_host, proxy.proxy_port, 0, 0) + else: + mode, address = socket.AF_INET, (proxy.proxy_host, proxy.proxy_port) + + # Create a non-blocking socket and bind it (if local address is specified). + sock = socket.socket(mode, socket.SOCK_STREAM) + sock.setblocking(False) + sock.bind(local_addr) + + # Actual TCP connection is performed here. + await asyncio.wait_for( + helpers.get_running_loop().sock_connect(sock=sock, address=address), + timeout=timeout + ) + + # As our socket is already created and connected, + # this call sets the destination host/port and + # starts protocol negotiations with the proxy server. + sock = await proxy.connect( + dest_host=self._ip, + dest_port=self._port, + timeout=timeout, + _socket=sock + ) + + else: + import socks + + # Here `address` represents destination address (not proxy), because of + # the `PySocks` implementation of the connection routine. + # IPv family is checked on proxy address, not destination address. + if ':' in parsed[1]: + mode, address = socket.AF_INET6, (self._ip, self._port, 0, 0) + else: + mode, address = socket.AF_INET, (self._ip, self._port) + + # Setup socket, proxy, timeout and bind it (if necessary). + sock = socks.socksocket(mode, socket.SOCK_STREAM) + sock.set_proxy(*parsed) + sock.settimeout(timeout) + + if local_addr is not None: + sock.bind(local_addr) + + # Actual TCP connection and negotiation performed here. + await asyncio.wait_for( + helpers.get_running_loop().sock_connect(sock=sock, address=address), + timeout=timeout + ) + + sock.setblocking(False) + + return sock + + async def _connect(self, timeout=None, ssl=None): + if self._local_addr is not None: + # NOTE: If port is not specified, we use 0 port + # to notify the OS that port should be chosen randomly + # from the available ones. + if isinstance(self._local_addr, tuple) and len(self._local_addr) == 2: + local_addr = self._local_addr + elif isinstance(self._local_addr, str): + local_addr = (self._local_addr, 0) + else: + raise ValueError("Unknown local address format: {}".format(self._local_addr)) + else: + local_addr = None + + if not self._proxy: + self._reader, self._writer = await asyncio.wait_for( + asyncio.open_connection( + host=self._ip, + port=self._port, + ssl=ssl, + local_addr=local_addr + ), timeout=timeout) + else: + # Proxy setup, connection and negotiation is performed here. + sock = await self._proxy_connect( + timeout=timeout, + local_addr=local_addr + ) + + # Wrap socket in SSL context (if provided) + if ssl: + sock = self._wrap_socket_ssl(sock) + + self._reader, self._writer = await asyncio.open_connection(sock=sock) + + self._codec = self.packet_codec(self) + self._init_conn() + await self._writer.drain() + + async def connect(self, timeout=None, ssl=None): + """ + Establishes a connection with the server. + """ + await self._connect(timeout=timeout, ssl=ssl) + self._connected = True + + loop = helpers.get_running_loop() + self._send_task = loop.create_task(self._send_loop()) + self._recv_task = loop.create_task(self._recv_loop()) + + async def disconnect(self): + """ + Disconnects from the server, and clears + pending outgoing and incoming messages. + """ + if not self._connected: + return + + self._connected = False + + await helpers._cancel( + self._log, + send_task=self._send_task, + recv_task=self._recv_task + ) + + if self._writer: + self._writer.close() + if sys.version_info >= (3, 7): + try: + await asyncio.wait_for(self._writer.wait_closed(), timeout=10) + except asyncio.TimeoutError: + # See issue #3917. For some users, this line was hanging indefinitely. + # The hard timeout is not ideal (connection won't be properly closed), + # but the code will at least be able to procceed. + self._log.warning('Graceful disconnection timed out, forcibly ignoring cleanup') + except Exception as e: + # Disconnecting should never raise. Seen: + # * OSError: No route to host and + # * OSError: [Errno 32] Broken pipe + # * ConnectionResetError + self._log.info('%s during disconnect: %s', type(e), e) + + def send(self, data): + """ + Sends a packet of data through this connection mode. + + This method returns a coroutine. + """ + if not self._connected: + raise ConnectionError('Not connected') + + return self._send_queue.put(data) + + async def recv(self): + """ + Receives a packet of data through this connection mode. + + This method returns a coroutine. + """ + while self._connected: + result, err = await self._recv_queue.get() + if err: + raise err + if result: + return result + + raise ConnectionError('Not connected') + + async def _send_loop(self): + """ + This loop is constantly popping items off the queue to send them. + """ + try: + while self._connected: + self._send(await self._send_queue.get()) + await self._writer.drain() + except asyncio.CancelledError: + pass + except Exception as e: + if isinstance(e, IOError): + self._log.info('The server closed the connection while sending') + else: + self._log.exception('Unexpected exception in the send loop') + + await self.disconnect() + + async def _recv_loop(self): + """ + This loop is constantly putting items on the queue as they're read. + """ + try: + while self._connected: + try: + data = await self._recv() + except asyncio.CancelledError: + break + except (IOError, asyncio.IncompleteReadError) as e: + self._log.warning('Server closed the connection: %s', e) + await self._recv_queue.put((None, e)) + await self.disconnect() + except InvalidChecksumError as e: + self._log.warning('Server response had invalid checksum: %s', e) + await self._recv_queue.put((None, e)) + except InvalidBufferError as e: + self._log.warning('Server response had invalid buffer: %s', e) + await self._recv_queue.put((None, e)) + except Exception as e: + self._log.exception('Unexpected exception in the receive loop') + await self._recv_queue.put((None, e)) + await self.disconnect() + else: + await self._recv_queue.put((data, None)) + finally: + await self.disconnect() + + + def _init_conn(self): + """ + This method will be called after `connect` is called. + After this method finishes, the writer will be drained. + + Subclasses should make use of this if they need to send + data to Telegram to indicate which connection mode will + be used. + """ + if self._codec.tag: + self._writer.write(self._codec.tag) + + def _send(self, data): + self._writer.write(self._codec.encode_packet(data)) + + async def _recv(self): + return await self._codec.read_packet(self._reader) + + def __str__(self): + return '{}:{}/{}'.format( + self._ip, self._port, + self.__class__.__name__.replace('Connection', '') + ) + + +class ObfuscatedConnection(Connection): + """ + Base class for "obfuscated" connections ("obfuscated2", "mtproto proxy") + """ + """ + This attribute should be redefined by subclasses + """ + obfuscated_io = None + + def _init_conn(self): + self._obfuscation = self.obfuscated_io(self) + self._writer.write(self._obfuscation.header) + + def _send(self, data): + self._obfuscation.write(self._codec.encode_packet(data)) + + async def _recv(self): + return await self._codec.read_packet(self._obfuscation) + + +class PacketCodec(abc.ABC): + """ + Base class for packet codecs + """ + + """ + This attribute should be re-defined by subclass to define if some + "magic bytes" should be sent to server right after connection is made to + signal which protocol will be used + """ + tag = None + + def __init__(self, connection): + """ + Codec is created when connection is just made. + """ + self._conn = connection + + @abc.abstractmethod + def encode_packet(self, data): + """ + Encodes single packet and returns encoded bytes. + """ + raise NotImplementedError + + @abc.abstractmethod + async def read_packet(self, reader): + """ + Reads single packet from `reader` object that should have + `readexactly(n)` method. + """ + raise NotImplementedError diff --git a/module/scripts/lib/telethon/network/connection/http.py b/module/scripts/lib/telethon/network/connection/http.py new file mode 100644 index 0000000..e2d976f --- /dev/null +++ b/module/scripts/lib/telethon/network/connection/http.py @@ -0,0 +1,39 @@ +import asyncio + +from .connection import Connection, PacketCodec + + +SSL_PORT = 443 + + +class HttpPacketCodec(PacketCodec): + tag = None + obfuscate_tag = None + + def encode_packet(self, data): + return ('POST /api HTTP/1.1\r\n' + 'Host: {}:{}\r\n' + 'Content-Type: application/x-www-form-urlencoded\r\n' + 'Connection: keep-alive\r\n' + 'Keep-Alive: timeout=100000, max=10000000\r\n' + 'Content-Length: {}\r\n\r\n' + .format(self._conn._ip, self._conn._port, len(data)) + .encode('ascii') + data) + + async def read_packet(self, reader): + while True: + line = await reader.readline() + if not line or line[-1] != b'\n': + raise asyncio.IncompleteReadError(line, None) + + if line.lower().startswith(b'content-length: '): + await reader.readexactly(2) + length = int(line[16:-2]) + return await reader.readexactly(length) + + +class ConnectionHttp(Connection): + packet_codec = HttpPacketCodec + + async def connect(self, timeout=None, ssl=None): + await super().connect(timeout=timeout, ssl=self._port == SSL_PORT) diff --git a/module/scripts/lib/telethon/network/connection/tcpabridged.py b/module/scripts/lib/telethon/network/connection/tcpabridged.py new file mode 100644 index 0000000..171b1d8 --- /dev/null +++ b/module/scripts/lib/telethon/network/connection/tcpabridged.py @@ -0,0 +1,33 @@ +import struct + +from .connection import Connection, PacketCodec + + +class AbridgedPacketCodec(PacketCodec): + tag = b'\xef' + obfuscate_tag = b'\xef\xef\xef\xef' + + def encode_packet(self, data): + length = len(data) >> 2 + if length < 127: + length = struct.pack('B', length) + else: + length = b'\x7f' + int.to_bytes(length, 3, 'little') + return length + data + + async def read_packet(self, reader): + length = struct.unpack('= 127: + length = struct.unpack( + ' 0: + return packet_with_padding[:-pad_size] + return packet_with_padding + + +class ConnectionTcpIntermediate(Connection): + """ + Intermediate mode between `ConnectionTcpFull` and `ConnectionTcpAbridged`. + Always sends 4 extra bytes for the packet length. + """ + packet_codec = IntermediatePacketCodec diff --git a/module/scripts/lib/telethon/network/connection/tcpmtproxy.py b/module/scripts/lib/telethon/network/connection/tcpmtproxy.py new file mode 100644 index 0000000..e3ef98a --- /dev/null +++ b/module/scripts/lib/telethon/network/connection/tcpmtproxy.py @@ -0,0 +1,165 @@ +import asyncio +import hashlib +import base64 +import os + +from .connection import ObfuscatedConnection +from .tcpabridged import AbridgedPacketCodec +from .tcpintermediate import ( + IntermediatePacketCodec, + RandomizedIntermediatePacketCodec +) + +from ...crypto import AESModeCTR + + +class MTProxyIO: + """ + It's very similar to tcpobfuscated.ObfuscatedIO, but the way + encryption keys, protocol tag and dc_id are encoded is different. + """ + header = None + + def __init__(self, connection): + self._reader = connection._reader + self._writer = connection._writer + + (self.header, + self._encrypt, + self._decrypt) = self.init_header( + connection._secret, connection._dc_id, connection.packet_codec) + + @staticmethod + def init_header(secret, dc_id, packet_codec): + # Validate + is_dd = (len(secret) == 17) and (secret[0] == 0xDD) + is_rand_codec = issubclass( + packet_codec, RandomizedIntermediatePacketCodec) + if is_dd and not is_rand_codec: + raise ValueError( + "Only RandomizedIntermediate can be used with dd-secrets") + secret = secret[1:] if is_dd else secret + if len(secret) != 16: + raise ValueError( + "MTProxy secret must be a hex-string representing 16 bytes") + + # Obfuscated messages secrets cannot start with any of these + keywords = (b'PVrG', b'GET ', b'POST', b'\xee\xee\xee\xee') + while True: + random = os.urandom(64) + if (random[0] != 0xef and + random[:4] not in keywords and + random[4:4] != b'\0\0\0\0'): + break + + random = bytearray(random) + random_reversed = random[55:7:-1] # Reversed (8, len=48) + + # Encryption has "continuous buffer" enabled + encrypt_key = hashlib.sha256( + bytes(random[8:40]) + secret).digest() + encrypt_iv = bytes(random[40:56]) + decrypt_key = hashlib.sha256( + bytes(random_reversed[:32]) + secret).digest() + decrypt_iv = bytes(random_reversed[32:48]) + + encryptor = AESModeCTR(encrypt_key, encrypt_iv) + decryptor = AESModeCTR(decrypt_key, decrypt_iv) + + random[56:60] = packet_codec.obfuscate_tag + + dc_id_bytes = dc_id.to_bytes(2, "little", signed=True) + random = random[:60] + dc_id_bytes + random[62:] + random[56:64] = encryptor.encrypt(bytes(random))[56:64] + return (random, encryptor, decryptor) + + async def readexactly(self, n): + return self._decrypt.encrypt(await self._reader.readexactly(n)) + + def write(self, data): + self._writer.write(self._encrypt.encrypt(data)) + + +class TcpMTProxy(ObfuscatedConnection): + """ + Connector which allows user to connect to the Telegram via proxy servers + commonly known as MTProxy. + Implemented very ugly due to the leaky abstractions in Telethon networking + classes that should be refactored later (TODO). + + .. warning:: + + The support for TcpMTProxy classes is **EXPERIMENTAL** and prone to + be changed. You shouldn't be using this class yet. + """ + packet_codec = None + obfuscated_io = MTProxyIO + + # noinspection PyUnusedLocal + def __init__(self, ip, port, dc_id, *, loggers, proxy=None, local_addr=None): + # connect to proxy's host and port instead of telegram's ones + proxy_host, proxy_port = self.address_info(proxy) + self._secret = self.normalize_secret(proxy[2]) + super().__init__( + proxy_host, proxy_port, dc_id, loggers=loggers) + + async def _connect(self, timeout=None, ssl=None): + await super()._connect(timeout=timeout, ssl=ssl) + + # Wait for EOF for 2 seconds (or if _wait_for_data's definition + # is missing or different, just sleep for 2 seconds). This way + # we give the proxy a chance to close the connection if the current + # codec (which the proxy detects with the data we sent) cannot + # be used for this proxy. This is a work around for #1134. + # TODO Sleeping for N seconds may not be the best solution + # TODO This fix could be welcome for HTTP proxies as well + try: + await asyncio.wait_for(self._reader._wait_for_data('proxy'), 2) + except asyncio.TimeoutError: + pass + except Exception: + await asyncio.sleep(2) + + if self._reader.at_eof(): + await self.disconnect() + raise ConnectionError( + 'Proxy closed the connection after sending initial payload') + + @staticmethod + def address_info(proxy_info): + if proxy_info is None: + raise ValueError("No proxy info specified for MTProxy connection") + return proxy_info[:2] + + @staticmethod + def normalize_secret(secret): + if secret[:2] in ("ee", "dd"): # Remove extra bytes + secret = secret[2:] + + try: + secret_bytes = bytes.fromhex(secret) + except ValueError: + secret = secret + '=' * (-len(secret) % 4) + secret_bytes = base64.b64decode(secret.encode()) + + return secret_bytes[:16] # Remove the domain from the secret (until domain support is added) + +class ConnectionTcpMTProxyAbridged(TcpMTProxy): + """ + Connect to proxy using abridged protocol + """ + packet_codec = AbridgedPacketCodec + + +class ConnectionTcpMTProxyIntermediate(TcpMTProxy): + """ + Connect to proxy using intermediate protocol + """ + packet_codec = IntermediatePacketCodec + + +class ConnectionTcpMTProxyRandomizedIntermediate(TcpMTProxy): + """ + Connect to proxy using randomized intermediate protocol (dd-secrets) + """ + packet_codec = RandomizedIntermediatePacketCodec diff --git a/module/scripts/lib/telethon/network/connection/tcpobfuscated.py b/module/scripts/lib/telethon/network/connection/tcpobfuscated.py new file mode 100644 index 0000000..cf2e6af --- /dev/null +++ b/module/scripts/lib/telethon/network/connection/tcpobfuscated.py @@ -0,0 +1,62 @@ +import os + +from .tcpabridged import AbridgedPacketCodec +from .connection import ObfuscatedConnection + +from ...crypto import AESModeCTR + + +class ObfuscatedIO: + header = None + + def __init__(self, connection): + self._reader = connection._reader + self._writer = connection._writer + + (self.header, + self._encrypt, + self._decrypt) = self.init_header(connection.packet_codec) + + @staticmethod + def init_header(packet_codec): + # Obfuscated messages secrets cannot start with any of these + keywords = (b'PVrG', b'GET ', b'POST', b'\xee\xee\xee\xee') + while True: + random = os.urandom(64) + if (random[0] != 0xef and + random[:4] not in keywords and + random[4:8] != b'\0\0\0\0'): + break + + random = bytearray(random) + random_reversed = random[55:7:-1] # Reversed (8, len=48) + + # Encryption has "continuous buffer" enabled + encrypt_key = bytes(random[8:40]) + encrypt_iv = bytes(random[40:56]) + decrypt_key = bytes(random_reversed[:32]) + decrypt_iv = bytes(random_reversed[32:48]) + + encryptor = AESModeCTR(encrypt_key, encrypt_iv) + decryptor = AESModeCTR(decrypt_key, decrypt_iv) + + random[56:60] = packet_codec.obfuscate_tag + random[56:64] = encryptor.encrypt(bytes(random))[56:64] + return (random, encryptor, decryptor) + + async def readexactly(self, n): + return self._decrypt.encrypt(await self._reader.readexactly(n)) + + def write(self, data): + self._writer.write(self._encrypt.encrypt(data)) + + +class ConnectionTcpObfuscated(ObfuscatedConnection): + """ + Mode that Telegram defines as "obfuscated2". Encodes the packet + just like `ConnectionTcpAbridged`, but encrypts every message with + a randomly generated key using the AES-CTR mode so the packets are + harder to discern. + """ + obfuscated_io = ObfuscatedIO + packet_codec = AbridgedPacketCodec diff --git a/module/scripts/lib/telethon/network/mtprotoplainsender.py b/module/scripts/lib/telethon/network/mtprotoplainsender.py new file mode 100644 index 0000000..563affd --- /dev/null +++ b/module/scripts/lib/telethon/network/mtprotoplainsender.py @@ -0,0 +1,56 @@ +""" +This module contains the class used to communicate with Telegram's servers +in plain text, when no authorization key has been created yet. +""" +import struct + +from .mtprotostate import MTProtoState +from ..errors import InvalidBufferError +from ..extensions import BinaryReader + + +class MTProtoPlainSender: + """ + MTProto Mobile Protocol plain sender + (https://core.telegram.org/mtproto/description#unencrypted-messages) + """ + def __init__(self, connection, *, loggers): + """ + Initializes the MTProto plain sender. + + :param connection: the Connection to be used. + """ + self._state = MTProtoState(auth_key=None, loggers=loggers) + self._connection = connection + + async def send(self, request): + """ + Sends and receives the result for the given request. + """ + body = bytes(request) + msg_id = self._state._get_new_msg_id() + await self._connection.send( + struct.pack(' 0, 'Bad length' + # We could read length bytes and use those in a new reader to read + # the next TLObject without including the padding, but since the + # reader isn't used for anything else after this, it's unnecessary. + return reader.tgread_object() diff --git a/module/scripts/lib/telethon/network/mtprotosender.py b/module/scripts/lib/telethon/network/mtprotosender.py new file mode 100644 index 0000000..0a96877 --- /dev/null +++ b/module/scripts/lib/telethon/network/mtprotosender.py @@ -0,0 +1,942 @@ +import asyncio +from asyncio.tasks import Task +import collections +import contextlib +import struct +import datetime +import time +from typing import Any + +from . import authenticator +from ..extensions.messagepacker import MessagePacker +from .mtprotoplainsender import MTProtoPlainSender +from .requeststate import RequestState +from .mtprotostate import MTProtoState +from ..tl.tlobject import TLRequest +from .. import helpers, utils +from ..errors import ( + BadMessageError, InvalidBufferError, AuthKeyNotFound, SecurityError, + TypeNotFoundError, rpc_message_to_error +) +from ..extensions import BinaryReader +from ..tl.core import RpcResult, MessageContainer, GzipPacked +from ..tl.functions.auth import LogOutRequest +from ..tl.functions import PingRequest, DestroySessionRequest, DestroyAuthKeyRequest +from ..tl.types import ( + MsgsAck, Pong, BadServerSalt, BadMsgNotification, FutureSalts, + MsgNewDetailedInfo, NewSessionCreated, MsgDetailedInfo, MsgsStateReq, + MsgsStateInfo, MsgsAllInfo, MsgResendReq, upload, DestroySessionOk, DestroySessionNone, + DestroyAuthKeyOk, DestroyAuthKeyNone, DestroyAuthKeyFail +) +from ..tl import types as _tl +from ..crypto import AuthKey +from ..helpers import retry_range + + +class MTProtoSender: + """ + MTProto Mobile Protocol sender + (https://core.telegram.org/mtproto/description). + + This class is responsible for wrapping requests into `TLMessage`'s, + sending them over the network and receiving them in a safe manner. + + Automatic reconnection due to temporary network issues is a concern + for this class as well, including retry of messages that could not + be sent successfully. + + A new authorization key will be generated on connection if no other + key exists yet. + """ + def __init__(self, auth_key, *, loggers, + retries=5, delay=1, auto_reconnect=True, connect_timeout=None, + auth_key_callback=None, + updates_queue=None, auto_reconnect_callback=None): + self._connection = None + self._loggers = loggers + self._log = loggers[__name__] + self._retries = retries + self._delay = delay + self._auto_reconnect = auto_reconnect + self._connect_timeout = connect_timeout + self._auth_key_callback = auth_key_callback + self._updates_queue = updates_queue + self._auto_reconnect_callback = auto_reconnect_callback + self._connect_lock = asyncio.Lock() + self._ping = None + + # Whether the user has explicitly connected or disconnected. + # + # If a disconnection happens for any other reason and it + # was *not* user action then the pending messages won't + # be cleared but on explicit user disconnection all the + # pending futures should be cancelled. + self._user_connected = False + self._reconnecting = False + self.__disconnected = None + + # We need to join the loops upon disconnection + self._send_loop_handle = None + self._recv_loop_handle = None + + # Preserving the references of the AuthKey and state is important + self.auth_key = auth_key or AuthKey(None) + self._state = MTProtoState(self.auth_key, loggers=self._loggers) + + # Outgoing messages are put in a queue and sent in a batch. + # Note that here we're also storing their ``_RequestState``. + self._send_queue = MessagePacker(self._state, loggers=self._loggers) + + # Sent states are remembered until a response is received. + self._pending_state = {} + + # Responses must be acknowledged, and we can also batch these. + self._pending_ack = set() + + # Similar to pending_messages but only for the last acknowledges. + # These can't go in pending_messages because no acknowledge for them + # is received, but we may still need to resend their state on bad salts. + self._last_acks = collections.deque(maxlen=10) + + # Jump table from response ID to method that handles it + self._handlers = { + RpcResult.CONSTRUCTOR_ID: self._handle_rpc_result, + MessageContainer.CONSTRUCTOR_ID: self._handle_container, + GzipPacked.CONSTRUCTOR_ID: self._handle_gzip_packed, + Pong.CONSTRUCTOR_ID: self._handle_pong, + BadServerSalt.CONSTRUCTOR_ID: self._handle_bad_server_salt, + BadMsgNotification.CONSTRUCTOR_ID: self._handle_bad_notification, + MsgDetailedInfo.CONSTRUCTOR_ID: self._handle_detailed_info, + MsgNewDetailedInfo.CONSTRUCTOR_ID: self._handle_new_detailed_info, + NewSessionCreated.CONSTRUCTOR_ID: self._handle_new_session_created, + MsgsAck.CONSTRUCTOR_ID: self._handle_ack, + FutureSalts.CONSTRUCTOR_ID: self._handle_future_salts, + MsgsStateReq.CONSTRUCTOR_ID: self._handle_state_forgotten, + MsgResendReq.CONSTRUCTOR_ID: self._handle_state_forgotten, + MsgsAllInfo.CONSTRUCTOR_ID: self._handle_msg_all, + DestroySessionOk.CONSTRUCTOR_ID: self._handle_destroy_session, + DestroySessionNone.CONSTRUCTOR_ID: self._handle_destroy_session, + DestroyAuthKeyOk.CONSTRUCTOR_ID: self._handle_destroy_auth_key, + DestroyAuthKeyNone.CONSTRUCTOR_ID: self._handle_destroy_auth_key, + DestroyAuthKeyFail.CONSTRUCTOR_ID: self._handle_destroy_auth_key, + } + + self._reconnect_task: Task[Any] | None = None + + # Public API + + async def connect(self, connection): + """ + Connects to the specified given connection using the given auth key. + """ + async with self._connect_lock: + if self._user_connected: + self._log.info('User is already connected!') + return False + + self._connection = connection + await self._connect() + self._user_connected = True + return True + + def is_connected(self): + return self._user_connected + + def _transport_connected(self): + return ( + not self._reconnecting + and self._connection is not None + and self._connection._connected + ) + + async def disconnect(self): + """ + Cleanly disconnects the instance from the network, cancels + all pending requests, and closes the send and receive loops. + """ + if self._reconnect_task: + _ = self._reconnect_task.cancel() + + with contextlib.suppress(asyncio.CancelledError): + await self._reconnect_task + + await self._disconnect() + + def send(self, request, ordered=False): + """ + This method enqueues the given request to be sent. Its send + state will be saved until a response arrives, and a ``Future`` + that will be resolved when the response arrives will be returned: + + .. code-block:: python + + async def method(): + # Sending (enqueued for the send loop) + future = sender.send(request) + # Receiving (waits for the receive loop to read the result) + result = await future + + Designed like this because Telegram may send the response at + any point, and it can send other items while one waits for it. + Once the response for this future arrives, it is set with the + received result, quite similar to how a ``receive()`` call + would otherwise work. + + Since the receiving part is "built in" the future, it's + impossible to await receive a result that was never sent. + """ + if not self._user_connected: + raise ConnectionError('Cannot send requests while disconnected') + + if not utils.is_list_like(request): + try: + state = RequestState(request) + except struct.error as e: + # "struct.error: required argument is not an integer" is not + # very helpful; log the request to find out what wasn't int. + self._log.error('Request caused struct.error: %s: %s', e, request) + raise + + self._send_queue.append(state) + return state.future + else: + states = [] + futures = [] + state = None + for req in request: + try: + state = RequestState(req, after=ordered and state) + except struct.error as e: + self._log.error('Request caused struct.error: %s: %s', e, request) + raise + + states.append(state) + futures.append(state.future) + + self._send_queue.extend(states) + return futures + + @property + def disconnected(self): + """ + Future that resolves when the connection to Telegram + ends, either by user action or in the background. + + Note that it may resolve in either a ``ConnectionError`` + or any other unexpected error that could not be handled. + """ + return asyncio.shield(self._disconnected) + + @property + def _disconnected(self): + if self.__disconnected is None: + self.__disconnected = helpers.get_running_loop().create_future() + self.__disconnected.set_result(None) + return self.__disconnected + + # Private methods + + async def _connect(self): + """ + Performs the actual connection, retrying, generating the + authorization key if necessary, and starting the send and + receive loops. + """ + self._log.info('Connecting to %s...', self._connection) + + connected = False + + for attempt in retry_range(self._retries): + if not connected: + connected = await self._try_connect(attempt) + if not connected: + continue # skip auth key generation until we're connected + + if not self.auth_key: + try: + if not await self._try_gen_auth_key(attempt): + continue # keep retrying until we have the auth key + except (IOError, asyncio.TimeoutError) as e: + # Sometimes, specially during user-DC migrations, + # Telegram may close the connection during auth_key + # generation. If that's the case, we will need to + # connect again. + self._log.warning('Connection error %d during auth_key gen: %s: %s', + attempt, type(e).__name__, e) + + # Whatever the IOError was, make sure to disconnect so we can + # reconnect cleanly after. + await self._connection.disconnect() + connected = False + await asyncio.sleep(self._delay) + continue # next iteration we will try to reconnect + + break # all steps done, break retry loop + else: + if not connected: + raise ConnectionError('Connection to Telegram failed {} time(s)'.format(self._retries)) + + e = ConnectionError('auth_key generation failed {} time(s)'.format(self._retries)) + await self._disconnect(error=e) + raise e + + loop = helpers.get_running_loop() + self._log.debug('Starting send loop') + self._send_loop_handle = loop.create_task(self._send_loop()) + + self._log.debug('Starting receive loop') + self._recv_loop_handle = loop.create_task(self._recv_loop()) + + # _disconnected only completes after manual disconnection + # or errors after which the sender cannot continue such + # as failing to reconnect or any unexpected error. + if self._disconnected.done(): + self.__disconnected = loop.create_future() + + self._log.info('Connection to %s complete!', self._connection) + + async def _try_connect(self, attempt): + try: + self._log.debug('Connection attempt %d...', attempt) + await self._connection.connect(timeout=self._connect_timeout) + self._log.debug('Connection success!') + return True + except (IOError, asyncio.TimeoutError) as e: + self._log.warning('Attempt %d at connecting failed: %s: %s', + attempt, type(e).__name__, e) + await asyncio.sleep(self._delay) + return False + + async def _try_gen_auth_key(self, attempt): + plain = MTProtoPlainSender(self._connection, loggers=self._loggers) + try: + self._log.debug('New auth_key attempt %d...', attempt) + self.auth_key.key, self._state.time_offset = \ + await authenticator.do_authentication(plain) + + # This is *EXTREMELY* important since we don't control + # external references to the authorization key, we must + # notify whenever we change it. This is crucial when we + # switch to different data centers. + if self._auth_key_callback: + await self._auth_key_callback(self.auth_key) + + self._log.debug('auth_key generation success!') + return True + except (SecurityError, AssertionError) as e: + self._log.warning('Attempt %d at new auth_key failed: %s', attempt, e) + await asyncio.sleep(self._delay) + return False + + async def _disconnect(self, error=None): + if self._connection is None: + self._log.info('Not disconnecting (already have no connection)') + return + + self._log.info('Disconnecting from %s...', self._connection) + self._user_connected = False + try: + self._log.debug('Closing current connection...') + await self._connection.disconnect() + finally: + self._log.debug('Cancelling %d pending message(s)...', len(self._pending_state)) + for state in self._pending_state.values(): + if error and not state.future.done(): + state.future.set_exception(error) + else: + state.future.cancel() + + self._pending_state.clear() + await helpers._cancel( + self._log, + send_loop_handle=self._send_loop_handle, + recv_loop_handle=self._recv_loop_handle + ) + + self._log.info('Disconnection from %s complete!', self._connection) + self._connection = None + + if self._disconnected and not self._disconnected.done(): + if error: + self._disconnected.set_exception(error) + else: + self._disconnected.set_result(None) + + async def _reconnect(self, last_error): + """ + Cleanly disconnects and then reconnects. + """ + self._log.info('Closing current connection to begin reconnect...') + if self._connection is not None: + await self._connection.disconnect() + + await helpers._cancel( + self._log, + send_loop_handle=self._send_loop_handle, + recv_loop_handle=self._recv_loop_handle + ) + + # TODO See comment in `_start_reconnect` + # Perhaps this should be the last thing to do? + # But _connect() creates tasks which may run and, + # if they see that reconnecting is True, they will end. + # Perhaps that task creation should not belong in connect? + self._reconnecting = False + + # Start with a clean state (and thus session ID) to avoid old msgs + self._state.reset() + + retries = self._retries if self._auto_reconnect else 0 + + attempt = 0 + ok = True + # We're already "retrying" to connect, so we don't want to force retries + for attempt in retry_range(retries, force_retry=False): + if not self._user_connected: + ok = False + break + try: + await self._connect() + except (IOError, asyncio.TimeoutError) as e: + last_error = e + self._log.info('Failed reconnection attempt %d with %s', + attempt, e.__class__.__name__) + await asyncio.sleep(self._delay) + except BufferError as e: + # TODO there should probably only be one place to except all these errors + if isinstance(e, InvalidBufferError) and e.code == 404: + self._log.info('Server does not know about the current auth key; the session may need to be recreated') + last_error = AuthKeyNotFound() + ok = False + break + else: + self._log.warning('Invalid buffer %s', e) + + except Exception as e: + last_error = e + self._log.exception('Unexpected exception reconnecting on ' + 'attempt %d', attempt) + + await asyncio.sleep(self._delay) + else: + self._send_queue.extend(self._pending_state.values()) + self._pending_state.clear() + + if self._auto_reconnect_callback: + helpers.get_running_loop().create_task(self._auto_reconnect_callback()) + + break + else: + ok = False + + if not ok: + self._log.error('Automatic reconnection failed %d time(s)', attempt) + # There may be no error (e.g. automatic reconnection was turned off). + error = last_error.with_traceback(None) if last_error else None + await self._disconnect(error=error) + + def _start_reconnect(self, error): + """Starts a reconnection in the background.""" + if self._user_connected and not self._reconnecting: + # We set reconnecting to True here and not inside the new task + # because it may happen that send/recv loop calls this again + # while the new task hasn't had a chance to run yet. This race + # condition puts `self.connection` in a bad state with two calls + # to its `connect` without disconnecting, so it creates a second + # receive loop. There can't be two tasks receiving data from + # the reader, since that causes an error, and the library just + # gets stuck. + # TODO It still gets stuck? Investigate where and why. + self._reconnecting = True + self._reconnect_task = helpers.get_running_loop().create_task(self._reconnect(error)) + + def _keepalive_ping(self, rnd_id): + """ + Send a keep-alive ping. If a pong for the last ping was not received + yet, this means we're probably not connected. + """ + # TODO this is ugly, update loop shouldn't worry about this, sender should + if self._ping is None: + self._ping = rnd_id + self.send(PingRequest(rnd_id)) + else: + self._start_reconnect(None) + + # Loops + + async def _send_loop(self): + """ + This loop is responsible for popping items off the send + queue, encrypting them, and sending them over the network. + + Besides `connect`, only this method ever sends data. + """ + while self._user_connected and not self._reconnecting: + if self._pending_ack: + ack = RequestState(MsgsAck(list(self._pending_ack))) + self._send_queue.append(ack) + self._last_acks.append(ack) + self._pending_ack.clear() + + self._log.debug('Waiting for messages to send...') + # TODO Wait for the connection send queue to be empty? + # This means that while it's not empty we can wait for + # more messages to be added to the send queue. + batch, data = await self._send_queue.get() + + if not data: + continue + + self._log.debug('Encrypting %d message(s) in %d bytes for sending', + len(batch), len(data)) + + data = self._state.encrypt_message_data(data) + + # Whether sending succeeds or not, the popped requests are now + # pending because they're removed from the queue. If a reconnect + # occurs, they will be removed from pending state and re-enqueued + # so even if the network fails they won't be lost. If they were + # never re-enqueued, the future waiting for a response "locks". + for state in batch: + if not isinstance(state, list): + if isinstance(state.request, TLRequest): + self._pending_state[state.msg_id] = state + else: + for s in state: + if isinstance(s.request, TLRequest): + self._pending_state[s.msg_id] = s + + try: + await self._connection.send(data) + except IOError as e: + self._log.info('Connection closed while sending data') + self._start_reconnect(e) + return + + self._log.debug('Encrypted messages put in a queue to be sent') + + async def _recv_loop(self): + """ + This loop is responsible for reading all incoming responses + from the network, decrypting and handling or dispatching them. + + Besides `connect`, only this method ever receives data. + """ + while self._user_connected and not self._reconnecting: + self._log.debug('Receiving items from the network...') + try: + body = await self._connection.recv() + except asyncio.CancelledError: + raise # bypass except Exception + except (IOError, asyncio.IncompleteReadError) as e: + self._log.info('Connection closed while receiving data: %s', e) + self._start_reconnect(e) + return + except InvalidBufferError as e: + if e.code == 429: + self._log.warning('Server indicated flood error at transport level: %s', e) + await self._disconnect(error=e) + else: + self._log.exception('Server sent invalid buffer') + self._start_reconnect(e) + return + except Exception as e: + self._log.exception('Unhandled error while receiving data') + self._start_reconnect(e) + return + + try: + message = self._state.decrypt_message_data(body) + if message is None: + continue # this message is to be ignored + except TypeNotFoundError as e: + # Received object which we don't know how to deserialize + self._log.info('Type %08x not found, remaining data %r', + e.invalid_constructor_id, e.remaining) + continue + except SecurityError as e: + # A step while decoding had the incorrect data. This message + # should not be considered safe and it should be ignored. + self._log.warning('Security error while unpacking a ' + 'received message: %s', e) + continue + except BufferError as e: + if isinstance(e, InvalidBufferError) and e.code == 404: + self._log.info('Server does not know about the current auth key; the session may need to be recreated') + await self._disconnect(error=AuthKeyNotFound()) + else: + self._log.warning('Invalid buffer %s', e) + self._start_reconnect(e) + return + except Exception as e: + self._log.exception('Unhandled error while decrypting data') + self._start_reconnect(e) + return + + try: + await self._process_message(message) + except Exception: + self._log.exception('Unhandled error while processing msgs') + + # Response Handlers + + async def _process_message(self, message): + """ + Adds the given message to the list of messages that must be + acknowledged and dispatches control to different ``_handle_*`` + method based on its type. + """ + self._pending_ack.add(message.msg_id) + handler = self._handlers.get(message.obj.CONSTRUCTOR_ID, + self._handle_update) + await handler(message) + + def _pop_states(self, msg_id): + """ + Pops the states known to match the given ID from pending messages. + + This method should be used when the response isn't specific. + """ + state = self._pending_state.pop(msg_id, None) + if state: + return [state] + + to_pop = [] + for state in self._pending_state.values(): + if state.container_id == msg_id: + to_pop.append(state.msg_id) + + if to_pop: + return [self._pending_state.pop(x) for x in to_pop] + + for ack in self._last_acks: + if ack.msg_id == msg_id: + return [ack] + + return [] + + async def _handle_rpc_result(self, message): + """ + Handles the result for Remote Procedure Calls: + + rpc_result#f35c6d01 req_msg_id:long result:bytes = RpcResult; + + This is where the future results for sent requests are set. + """ + rpc_result = message.obj + state = self._pending_state.pop(rpc_result.req_msg_id, None) + self._log.debug('Handling RPC result for message %d', + rpc_result.req_msg_id) + + if not state: + # TODO We should not get responses to things we never sent + # However receiving a File() with empty bytes is "common". + # See #658, #759 and #958. They seem to happen in a container + # which contain the real response right after. + # + # But, it might also happen that we get an *error* for no parent request. + # If that's the case attempting to read from body which is None would fail with: + # "BufferError: No more data left to read (need 4, got 0: b''); last read None". + # This seems to be particularly common for "RpcError(error_code=-500, error_message='No workers running')". + if rpc_result.error: + self._log.info('Received error without parent request: %s', rpc_result.error) + else: + try: + with BinaryReader(rpc_result.body) as reader: + if not isinstance(reader.tgread_object(), upload.File): + raise ValueError('Not an upload.File') + except (TypeNotFoundError, ValueError): + self._log.info('Received response without parent request: %s', rpc_result.body) + return + + if rpc_result.error: + error = rpc_message_to_error(rpc_result.error, state.request) + self._send_queue.append( + RequestState(MsgsAck([state.msg_id]))) + + if not state.future.cancelled(): + state.future.set_exception(error) + else: + try: + with BinaryReader(rpc_result.body) as reader: + result = state.request.read_result(reader) + except Exception as e: + # e.g. TypeNotFoundError, should be propagated to caller + if not state.future.cancelled(): + state.future.set_exception(e) + else: + self._store_own_updates(result) + if not state.future.cancelled(): + state.future.set_result(result) + + async def _handle_container(self, message): + """ + Processes the inner messages of a container with many of them: + + msg_container#73f1f8dc messages:vector<%Message> = MessageContainer; + """ + self._log.debug('Handling container') + for inner_message in message.obj.messages: + await self._process_message(inner_message) + + async def _handle_gzip_packed(self, message): + """ + Unpacks the data from a gzipped object and processes it: + + gzip_packed#3072cfa1 packed_data:bytes = Object; + """ + self._log.debug('Handling gzipped data') + with BinaryReader(message.obj.data) as reader: + message.obj = reader.tgread_object() + await self._process_message(message) + + async def _handle_update(self, message): + try: + assert message.obj.SUBCLASS_OF_ID == 0x8af52aac # crc32(b'Updates') + except AssertionError: + self._log.warning( + 'Note: %s is not an update, not dispatching it %s', + message.obj.__class__.__name__, + message.obj + ) + return + + self._log.debug('Handling update %s', message.obj.__class__.__name__) + self._updates_queue.put_nowait(message.obj) + + def _store_own_updates(self, obj, *, _update_ids=frozenset(( + _tl.UpdateShortMessage.CONSTRUCTOR_ID, + _tl.UpdateShortChatMessage.CONSTRUCTOR_ID, + _tl.UpdateShort.CONSTRUCTOR_ID, + _tl.UpdatesCombined.CONSTRUCTOR_ID, + _tl.Updates.CONSTRUCTOR_ID, + _tl.UpdateShortSentMessage.CONSTRUCTOR_ID, + )), _update_like_ids=frozenset(( + _tl.messages.AffectedHistory.CONSTRUCTOR_ID, + _tl.messages.AffectedMessages.CONSTRUCTOR_ID, + _tl.messages.AffectedFoundMessages.CONSTRUCTOR_ID, + ))): + try: + if obj.CONSTRUCTOR_ID in _update_ids: + obj._self_outgoing = True # flag to only process, but not dispatch these + self._updates_queue.put_nowait(obj) + elif obj.CONSTRUCTOR_ID in _update_like_ids: + # Ugly "hack" (?) - otherwise bots reliably detect gaps when deleting messages. + # + # Note: the `date` being `None` is used to check for `updatesTooLong`, so epoch + # is used instead. It is still not read, because `updateShort` has no `seq`. + # + # Some requests, such as `readHistory`, also return these types. But the `pts_count` + # seems to be zero, so while this will produce some bogus `updateDeleteMessages`, + # it's still one of the "cleaner" approaches to handling the new `pts`. + # `updateDeleteMessages` is probably the "least-invasive" update that can be used. + upd = _tl.UpdateShort( + _tl.UpdateDeleteMessages([], obj.pts, obj.pts_count), + datetime.datetime(*time.gmtime(0)[:6]).replace(tzinfo=datetime.timezone.utc) + ) + upd._self_outgoing = True + self._updates_queue.put_nowait(upd) + elif obj.CONSTRUCTOR_ID == _tl.messages.InvitedUsers.CONSTRUCTOR_ID: + obj.updates._self_outgoing = True + self._updates_queue.put_nowait(obj.updates) + elif obj.CONSTRUCTOR_ID == _tl.messages.ChatInviteJoinResultOk.CONSTRUCTOR_ID: + obj.updates._self_outgoing = True + self._updates_queue.put_nowait(obj.updates) + + except AttributeError: + pass + + async def _handle_pong(self, message): + """ + Handles pong results, which don't come inside a ``rpc_result`` + but are still sent through a request: + + pong#347773c5 msg_id:long ping_id:long = Pong; + """ + pong = message.obj + self._log.debug('Handling pong for message %d', pong.msg_id) + if self._ping == pong.ping_id: + self._ping = None + + state = self._pending_state.pop(pong.msg_id, None) + if state: + state.future.set_result(pong) + + async def _handle_bad_server_salt(self, message): + """ + Corrects the currently used server salt to use the right value + before enqueuing the rejected message to be re-sent: + + bad_server_salt#edab447b bad_msg_id:long bad_msg_seqno:int + error_code:int new_server_salt:long = BadMsgNotification; + """ + bad_salt = message.obj + self._log.debug('Handling bad salt for message %d', bad_salt.bad_msg_id) + self._state.salt = bad_salt.new_server_salt + states = self._pop_states(bad_salt.bad_msg_id) + self._send_queue.extend(states) + + self._log.debug('%d message(s) will be resent', len(states)) + + async def _handle_bad_notification(self, message): + """ + Adjusts the current state to be correct based on the + received bad message notification whenever possible: + + bad_msg_notification#a7eff811 bad_msg_id:long bad_msg_seqno:int + error_code:int = BadMsgNotification; + """ + bad_msg = message.obj + states = self._pop_states(bad_msg.bad_msg_id) + + self._log.debug('Handling bad msg %s', bad_msg) + if bad_msg.error_code in (16, 17): + # Sent msg_id too low or too high (respectively). + # Use the current msg_id to determine the right time offset. + to = self._state.update_time_offset( + correct_msg_id=message.msg_id) + self._log.info('System clock is wrong, set time offset to %ds', to) + elif bad_msg.error_code == 32: + # msg_seqno too low, so just pump it up by some "large" amount + # TODO A better fix would be to start with a new fresh session ID + self._state._sequence += 64 + elif bad_msg.error_code == 33: + # msg_seqno too high never seems to happen but just in case + self._state._sequence -= 16 + else: + for state in states: + state.future.set_exception( + BadMessageError(state.request, bad_msg.error_code)) + return + + # Messages are to be re-sent once we've corrected the issue + self._send_queue.extend(states) + self._log.debug('%d messages will be resent due to bad msg', + len(states)) + + async def _handle_detailed_info(self, message): + """ + Updates the current status with the received detailed information: + + msg_detailed_info#276d3ec6 msg_id:long answer_msg_id:long + bytes:int status:int = MsgDetailedInfo; + """ + # TODO https://goo.gl/VvpCC6 + msg_id = message.obj.answer_msg_id + self._log.debug('Handling detailed info for message %d', msg_id) + self._pending_ack.add(msg_id) + + async def _handle_new_detailed_info(self, message): + """ + Updates the current status with the received detailed information: + + msg_new_detailed_info#809db6df answer_msg_id:long + bytes:int status:int = MsgDetailedInfo; + """ + # TODO https://goo.gl/G7DPsR + msg_id = message.obj.answer_msg_id + self._log.debug('Handling new detailed info for message %d', msg_id) + self._pending_ack.add(msg_id) + + async def _handle_new_session_created(self, message): + """ + Updates the current status with the received session information: + + new_session_created#9ec20908 first_msg_id:long unique_id:long + server_salt:long = NewSession; + """ + # TODO https://goo.gl/LMyN7A + self._log.debug('Handling new session created') + self._state.salt = message.obj.server_salt + + async def _handle_ack(self, message): + """ + Handles a server acknowledge about our messages. Normally + these can be ignored except in the case of ``auth.logOut``: + + auth.logOut#5717da40 = Bool; + + Telegram doesn't seem to send its result so we need to confirm + it manually. No other request is known to have this behaviour. + + Since the ID of sent messages consisting of a container is + never returned (unless on a bad notification), this method + also removes containers messages when any of their inner + messages are acknowledged. + """ + ack = message.obj + self._log.debug('Handling acknowledge for %s', str(ack.msg_ids)) + for msg_id in ack.msg_ids: + state = self._pending_state.get(msg_id) + if state and isinstance(state.request, LogOutRequest): + del self._pending_state[msg_id] + if not state.future.cancelled(): + state.future.set_result(True) + + async def _handle_future_salts(self, message): + """ + Handles future salt results, which don't come inside a + ``rpc_result`` but are still sent through a request: + + future_salts#ae500895 req_msg_id:long now:int + salts:vector = FutureSalts; + """ + # TODO save these salts and automatically adjust to the + # correct one whenever the salt in use expires. + self._log.debug('Handling future salts for message %d', message.msg_id) + state = self._pending_state.pop(message.obj.req_msg_id, None) + if state: + state.future.set_result(message.obj) + + async def _handle_state_forgotten(self, message): + """ + Handles both :tl:`MsgsStateReq` and :tl:`MsgResendReq` by + enqueuing a :tl:`MsgsStateInfo` to be sent at a later point. + """ + self._send_queue.append(RequestState(MsgsStateInfo( + req_msg_id=message.msg_id, info=chr(1) * len(message.obj.msg_ids) + ))) + + async def _handle_msg_all(self, message): + """ + Handles :tl:`MsgsAllInfo` by doing nothing (yet). + """ + + async def _handle_destroy_session(self, message): + """ + Handles both :tl:`DestroySessionOk` and :tl:`DestroySessionNone`. + It behaves pretty much like handling an RPC result. + """ + for msg_id, state in self._pending_state.items(): + if isinstance(state.request, DestroySessionRequest)\ + and state.request.session_id == message.obj.session_id: + break + else: + return + + del self._pending_state[msg_id] + if not state.future.cancelled(): + state.future.set_result(message.obj) + + async def _handle_destroy_auth_key(self, message): + """ + Handles :tl:`DestroyAuthKeyFail`, :tl:`DestroyAuthKeyNone`, and :tl:`DestroyAuthKeyOk`. + + :tl:`DestroyAuthKey` is not intended for users to use, but they still + might, and the response won't come in `rpc_result`, so thhat's worked + around here. + """ + self._log.debug('Handling destroy auth key %s', message.obj) + for msg_id, state in list(self._pending_state.items()): + if isinstance(state.request, DestroyAuthKeyRequest): + del self._pending_state[msg_id] + if not state.future.cancelled(): + state.future.set_result(message.obj) + + # If the auth key has been destroyed, that pretty much means the + # library can't continue as our auth key will no longer be found + # on the server. + # Even if the library didn't disconnect, the server would (and then + # the library would reconnect and learn about auth key being invalid). + if isinstance(message.obj, DestroyAuthKeyOk): + await self._disconnect(error=AuthKeyNotFound()) diff --git a/module/scripts/lib/telethon/network/mtprotostate.py b/module/scripts/lib/telethon/network/mtprotostate.py new file mode 100644 index 0000000..49fb747 --- /dev/null +++ b/module/scripts/lib/telethon/network/mtprotostate.py @@ -0,0 +1,285 @@ +import os +import struct +import time +from hashlib import sha256 +from collections import deque + +from ..crypto import AES +from ..errors import SecurityError, InvalidBufferError +from ..extensions import BinaryReader +from ..tl.core import TLMessage +from ..tl.tlobject import TLRequest +from ..tl.functions import InvokeAfterMsgRequest +from ..tl.core.gzippacked import GzipPacked +from ..tl.types import BadServerSalt, BadMsgNotification + + +# N is not specified in https://core.telegram.org/mtproto/security_guidelines#checking-msg-id, but 500 is reasonable +MAX_RECENT_MSG_IDS = 500 + +MSG_TOO_NEW_DELTA = 30 +MSG_TOO_OLD_DELTA = 300 + +# Something must be wrong if we ignore too many messages at the same time +MAX_CONSECUTIVE_IGNORED = 10 + + +class _OpaqueRequest(TLRequest): + """ + Wraps a serialized request into a type that can be serialized again. + """ + def __init__(self, data: bytes): + self.data = data + + def _bytes(self): + return self.data + + + +class MTProtoState: + """ + `telethon.network.mtprotosender.MTProtoSender` needs to hold a state + in order to be able to encrypt and decrypt incoming/outgoing messages, + as well as generating the message IDs. Instances of this class hold + together all the required information. + + It doesn't make sense to use `telethon.sessions.abstract.Session` for + the sender because the sender should *not* be concerned about storing + this information to disk, as one may create as many senders as they + desire to any other data center, or some CDN. Using the same session + for all these is not a good idea as each need their own authkey, and + the concept of "copying" sessions with the unnecessary entities or + updates state for these connections doesn't make sense. + + While it would be possible to have a `MTProtoPlainState` that does no + encryption so that it was usable through the `MTProtoLayer` and thus + avoid the need for a `MTProtoPlainSender`, the `MTProtoLayer` is more + focused to efficiency and this state is also more advanced (since it + supports gzipping and invoking after other message IDs). There are too + many methods that would be needed to make it convenient to use for the + authentication process, at which point the `MTProtoPlainSender` is better. + """ + def __init__(self, auth_key, loggers): + self.auth_key = auth_key + self._log = loggers[__name__] + self.time_offset = 0 + self.salt = 0 + + self.id = self._sequence = self._last_msg_id = None + self._recent_remote_ids = deque(maxlen=MAX_RECENT_MSG_IDS) + self._highest_remote_id = 0 + self._ignore_count = 0 + self.reset() + + def reset(self): + """ + Resets the state. + """ + # Session IDs can be random on every connection + self.id = struct.unpack('q', os.urandom(8))[0] + self._sequence = 0 + self._last_msg_id = 0 + self._recent_remote_ids.clear() + self._highest_remote_id = 0 + self._ignore_count = 0 + + def update_message_id(self, message): + """ + Updates the message ID to a new one, + used when the time offset changed. + """ + message.msg_id = self._get_new_msg_id() + + @staticmethod + def _calc_key(auth_key, msg_key, client): + """ + Calculate the key based on Telegram guidelines for MTProto 2, + specifying whether it's the client or not. See + https://core.telegram.org/mtproto/description#defining-aes-key-and-initialization-vector + """ + x = 0 if client else 8 + sha256a = sha256(msg_key + auth_key[x: x + 36]).digest() + sha256b = sha256(auth_key[x + 40:x + 76] + msg_key).digest() + + aes_key = sha256a[:8] + sha256b[8:24] + sha256a[24:32] + aes_iv = sha256b[:8] + sha256a[8:24] + sha256b[24:32] + + return aes_key, aes_iv + + def write_data_as_message(self, buffer, data, content_related, + *, after_id=None): + """ + Writes a message containing the given data into buffer. + + Returns the message id. + """ + msg_id = self._get_new_msg_id() + seq_no = self._get_seq_no(content_related) + if after_id is None: + body = GzipPacked.gzip_if_smaller(content_related, data) + else: + # The `RequestState` stores `bytes(request)`, not the request itself. + # `invokeAfterMsg` wants a `TLRequest` though, hence the wrapping. + body = GzipPacked.gzip_if_smaller(content_related, + bytes(InvokeAfterMsgRequest(after_id, _OpaqueRequest(data)))) + + buffer.write(struct.pack('> 32 + time_delta = (now + self.time_offset) - remote_msg_time + + if time_delta > MSG_TOO_OLD_DELTA: + self._log.warning('Server sent a very old message with ID %d, ignoring (see FAQ for details)', remote_msg_id) + self._count_ignored() + return None + + if -time_delta > MSG_TOO_NEW_DELTA: + self._log.warning('Server sent a very new message with ID %d, ignoring (see FAQ for details)', remote_msg_id) + self._count_ignored() + return None + + self._recent_remote_ids.append(remote_msg_id) + self._highest_remote_id = remote_msg_id + self._ignore_count = 0 + + return TLMessage(remote_msg_id, remote_sequence, obj) + + def _count_ignored(self): + # It's possible that ignoring a message "bricks" the connection, + # but this should not happen unless there's something else wrong. + self._ignore_count += 1 + if self._ignore_count >= MAX_CONSECUTIVE_IGNORED: + raise SecurityError('Too many messages had to be ignored consecutively') + + def _get_new_msg_id(self): + """ + Generates a new unique message ID based on the current + time (in ms) since epoch, applying a known time offset. + """ + now = time.time() + self.time_offset + nanoseconds = int((now - int(now)) * 1e+9) + new_msg_id = (int(now) << 32) | (nanoseconds << 2) + + if self._last_msg_id >= new_msg_id: + new_msg_id = self._last_msg_id + 4 + + self._last_msg_id = new_msg_id + return new_msg_id + + def update_time_offset(self, correct_msg_id): + """ + Updates the time offset to the correct + one given a known valid message ID. + """ + bad = self._get_new_msg_id() + old = self.time_offset + + now = int(time.time()) + correct = correct_msg_id >> 32 + self.time_offset = correct - now + + if self.time_offset != old: + self._last_msg_id = 0 + self._log.debug( + 'Updated time offset (old offset %d, bad %d, good %d, new %d)', + old, bad, correct_msg_id, self.time_offset + ) + + return self.time_offset + + def _get_seq_no(self, content_related): + """ + Generates the next sequence number depending on whether + it should be for a content-related query or not. + """ + if content_related: + result = self._sequence * 2 + 1 + self._sequence += 1 + return result + else: + return self._sequence * 2 diff --git a/module/scripts/lib/telethon/network/requeststate.py b/module/scripts/lib/telethon/network/requeststate.py new file mode 100644 index 0000000..21b5efd --- /dev/null +++ b/module/scripts/lib/telethon/network/requeststate.py @@ -0,0 +1,19 @@ +import asyncio + + +class RequestState: + """ + This request state holds several information relevant to sent messages, + in particular the message ID assigned to the request, the container ID + it belongs to, the request itself, the request as bytes, and the future + result that will eventually be resolved. + """ + __slots__ = ('container_id', 'msg_id', 'request', 'data', 'future', 'after') + + def __init__(self, request, after=None): + self.container_id = None + self.msg_id = None + self.request = request + self.data = bytes(request) + self.future = asyncio.Future() + self.after = after diff --git a/module/scripts/lib/telethon/password.py b/module/scripts/lib/telethon/password.py new file mode 100644 index 0000000..0f95025 --- /dev/null +++ b/module/scripts/lib/telethon/password.py @@ -0,0 +1,194 @@ +import hashlib +import os + +from .crypto import factorization +from .tl import types + + +def check_prime_and_good_check(prime: int, g: int): + good_prime_bits_count = 2048 + if prime < 0 or prime.bit_length() != good_prime_bits_count: + raise ValueError('bad prime count {}, expected {}' + .format(prime.bit_length(), good_prime_bits_count)) + + # TODO This is awfully slow + if factorization.Factorization.factorize(prime)[0] != 1: + raise ValueError('given "prime" is not prime') + + if g == 2: + if prime % 8 != 7: + raise ValueError('bad g {}, mod8 {}'.format(g, prime % 8)) + elif g == 3: + if prime % 3 != 2: + raise ValueError('bad g {}, mod3 {}'.format(g, prime % 3)) + elif g == 4: + pass + elif g == 5: + if prime % 5 not in (1, 4): + raise ValueError('bad g {}, mod5 {}'.format(g, prime % 5)) + elif g == 6: + if prime % 24 not in (19, 23): + raise ValueError('bad g {}, mod24 {}'.format(g, prime % 24)) + elif g == 7: + if prime % 7 not in (3, 5, 6): + raise ValueError('bad g {}, mod7 {}'.format(g, prime % 7)) + else: + raise ValueError('bad g {}'.format(g)) + + prime_sub1_div2 = (prime - 1) // 2 + if factorization.Factorization.factorize(prime_sub1_div2)[0] != 1: + raise ValueError('(prime - 1) // 2 is not prime') + + # Else it's good + + +def check_prime_and_good(prime_bytes: bytes, g: int): + good_prime = bytes(( + 0xC7, 0x1C, 0xAE, 0xB9, 0xC6, 0xB1, 0xC9, 0x04, 0x8E, 0x6C, 0x52, 0x2F, 0x70, 0xF1, 0x3F, 0x73, + 0x98, 0x0D, 0x40, 0x23, 0x8E, 0x3E, 0x21, 0xC1, 0x49, 0x34, 0xD0, 0x37, 0x56, 0x3D, 0x93, 0x0F, + 0x48, 0x19, 0x8A, 0x0A, 0xA7, 0xC1, 0x40, 0x58, 0x22, 0x94, 0x93, 0xD2, 0x25, 0x30, 0xF4, 0xDB, + 0xFA, 0x33, 0x6F, 0x6E, 0x0A, 0xC9, 0x25, 0x13, 0x95, 0x43, 0xAE, 0xD4, 0x4C, 0xCE, 0x7C, 0x37, + 0x20, 0xFD, 0x51, 0xF6, 0x94, 0x58, 0x70, 0x5A, 0xC6, 0x8C, 0xD4, 0xFE, 0x6B, 0x6B, 0x13, 0xAB, + 0xDC, 0x97, 0x46, 0x51, 0x29, 0x69, 0x32, 0x84, 0x54, 0xF1, 0x8F, 0xAF, 0x8C, 0x59, 0x5F, 0x64, + 0x24, 0x77, 0xFE, 0x96, 0xBB, 0x2A, 0x94, 0x1D, 0x5B, 0xCD, 0x1D, 0x4A, 0xC8, 0xCC, 0x49, 0x88, + 0x07, 0x08, 0xFA, 0x9B, 0x37, 0x8E, 0x3C, 0x4F, 0x3A, 0x90, 0x60, 0xBE, 0xE6, 0x7C, 0xF9, 0xA4, + 0xA4, 0xA6, 0x95, 0x81, 0x10, 0x51, 0x90, 0x7E, 0x16, 0x27, 0x53, 0xB5, 0x6B, 0x0F, 0x6B, 0x41, + 0x0D, 0xBA, 0x74, 0xD8, 0xA8, 0x4B, 0x2A, 0x14, 0xB3, 0x14, 0x4E, 0x0E, 0xF1, 0x28, 0x47, 0x54, + 0xFD, 0x17, 0xED, 0x95, 0x0D, 0x59, 0x65, 0xB4, 0xB9, 0xDD, 0x46, 0x58, 0x2D, 0xB1, 0x17, 0x8D, + 0x16, 0x9C, 0x6B, 0xC4, 0x65, 0xB0, 0xD6, 0xFF, 0x9C, 0xA3, 0x92, 0x8F, 0xEF, 0x5B, 0x9A, 0xE4, + 0xE4, 0x18, 0xFC, 0x15, 0xE8, 0x3E, 0xBE, 0xA0, 0xF8, 0x7F, 0xA9, 0xFF, 0x5E, 0xED, 0x70, 0x05, + 0x0D, 0xED, 0x28, 0x49, 0xF4, 0x7B, 0xF9, 0x59, 0xD9, 0x56, 0x85, 0x0C, 0xE9, 0x29, 0x85, 0x1F, + 0x0D, 0x81, 0x15, 0xF6, 0x35, 0xB1, 0x05, 0xEE, 0x2E, 0x4E, 0x15, 0xD0, 0x4B, 0x24, 0x54, 0xBF, + 0x6F, 0x4F, 0xAD, 0xF0, 0x34, 0xB1, 0x04, 0x03, 0x11, 0x9C, 0xD8, 0xE3, 0xB9, 0x2F, 0xCC, 0x5B)) + + if good_prime == prime_bytes: + if g in (3, 4, 5, 7): + return # It's good + + check_prime_and_good_check(int.from_bytes(prime_bytes, 'big'), g) + + +def is_good_large(number: int, p: int) -> bool: + return number > 0 and p - number > 0 + + +SIZE_FOR_HASH = 256 + + +def num_bytes_for_hash(number: bytes) -> bytes: + return bytes(SIZE_FOR_HASH - len(number)) + number + + +def big_num_for_hash(g: int) -> bytes: + return g.to_bytes(SIZE_FOR_HASH, 'big') + + +def sha256(*p: bytes) -> bytes: + hash = hashlib.sha256() + for q in p: + hash.update(q) + return hash.digest() + + +def is_good_mod_exp_first(modexp, prime) -> bool: + diff = prime - modexp + min_diff_bits_count = 2048 - 64 + max_mod_exp_size = 256 + if diff < 0 or \ + diff.bit_length() < min_diff_bits_count or \ + modexp.bit_length() < min_diff_bits_count or \ + (modexp.bit_length() + 7) // 8 > max_mod_exp_size: + return False + return True + + +def xor(a: bytes, b: bytes) -> bytes: + return bytes(x ^ y for x, y in zip(a, b)) + + +def pbkdf2sha512(password: bytes, salt: bytes, iterations: int): + return hashlib.pbkdf2_hmac('sha512', password, salt, iterations) + + +def compute_hash(algo: types.PasswordKdfAlgoSHA256SHA256PBKDF2HMACSHA512iter100000SHA256ModPow, + password: str): + hash1 = sha256(algo.salt1, password.encode('utf-8'), algo.salt1) + hash2 = sha256(algo.salt2, hash1, algo.salt2) + hash3 = pbkdf2sha512(hash2, algo.salt1, 100000) + return sha256(algo.salt2, hash3, algo.salt2) + + +def compute_digest(algo: types.PasswordKdfAlgoSHA256SHA256PBKDF2HMACSHA512iter100000SHA256ModPow, + password: str): + try: + check_prime_and_good(algo.p, algo.g) + except ValueError: + raise ValueError('bad p/g in password') + + value = pow(algo.g, + int.from_bytes(compute_hash(algo, password), 'big'), + int.from_bytes(algo.p, 'big')) + + return big_num_for_hash(value) + + +# https://github.com/telegramdesktop/tdesktop/blob/18b74b90451a7db2379a9d753c9cbaf8734b4d5d/Telegram/SourceFiles/core/core_cloud_password.cpp +def compute_check(request: types.account.Password, password: str): + algo = request.current_algo + if not isinstance(algo, types.PasswordKdfAlgoSHA256SHA256PBKDF2HMACSHA512iter100000SHA256ModPow): + raise ValueError('unsupported password algorithm {}' + .format(algo.__class__.__name__)) + + pw_hash = compute_hash(algo, password) + + p = int.from_bytes(algo.p, 'big') + g = algo.g + B = int.from_bytes(request.srp_B, 'big') + try: + check_prime_and_good(algo.p, g) + except ValueError: + raise ValueError('bad p/g in password') + + if not is_good_large(B, p): + raise ValueError('bad b in check') + + x = int.from_bytes(pw_hash, 'big') + p_for_hash = num_bytes_for_hash(algo.p) + g_for_hash = big_num_for_hash(g) + b_for_hash = num_bytes_for_hash(request.srp_B) + g_x = pow(g, x, p) + k = int.from_bytes(sha256(p_for_hash, g_for_hash), 'big') + kg_x = (k * g_x) % p + + def generate_and_check_random(): + random_size = 256 + while True: + random = os.urandom(random_size) + a = int.from_bytes(random, 'big') + A = pow(g, a, p) + if is_good_mod_exp_first(A, p): + a_for_hash = big_num_for_hash(A) + u = int.from_bytes(sha256(a_for_hash, b_for_hash), 'big') + if u > 0: + return (a, a_for_hash, u) + + a, a_for_hash, u = generate_and_check_random() + g_b = (B - kg_x) % p + if not is_good_mod_exp_first(g_b, p): + raise ValueError('bad g_b') + + ux = u * x + a_ux = a + ux + S = pow(g_b, a_ux, p) + K = sha256(big_num_for_hash(S)) + M1 = sha256( + xor(sha256(p_for_hash), sha256(g_for_hash)), + sha256(algo.salt1), + sha256(algo.salt2), + a_for_hash, + b_for_hash, + K + ) + + return types.InputCheckPasswordSRP( + request.srp_id, bytes(a_for_hash), bytes(M1)) diff --git a/module/scripts/lib/telethon/requestiter.py b/module/scripts/lib/telethon/requestiter.py new file mode 100644 index 0000000..fd28419 --- /dev/null +++ b/module/scripts/lib/telethon/requestiter.py @@ -0,0 +1,134 @@ +import abc +import asyncio +import time + +from . import helpers + + +class RequestIter(abc.ABC): + """ + Helper class to deal with requests that need offsets to iterate. + + It has some facilities, such as automatically sleeping a desired + amount of time between requests if needed (but not more). + + Can be used synchronously if the event loop is not running and + as an asynchronous iterator otherwise. + + `limit` is the total amount of items that the iterator should return. + This is handled on this base class, and will be always ``>= 0``. + + `left` will be reset every time the iterator is used and will indicate + the amount of items that should be emitted left, so that subclasses can + be more efficient and fetch only as many items as they need. + + Iterators may be used with ``reversed``, and their `reverse` flag will + be set to `True` if that's the case. Note that if this flag is set, + `buffer` should be filled in reverse too. + """ + def __init__(self, client, limit, *, reverse=False, wait_time=None, **kwargs): + self.client = client + self.reverse = reverse + self.wait_time = wait_time + self.kwargs = kwargs + self.limit = max(float('inf') if limit is None else limit, 0) + self.left = self.limit + self.buffer = None + self.index = 0 + self.total = None + self.last_load = 0 + + async def _init(self, **kwargs): + """ + Called when asynchronous initialization is necessary. All keyword + arguments passed to `__init__` will be forwarded here, and it's + preferable to use named arguments in the subclasses without defaults + to avoid forgetting or misspelling any of them. + + This method may ``raise StopAsyncIteration`` if it cannot continue. + + This method may actually fill the initial buffer if it needs to, + and similarly to `_load_next_chunk`, ``return True`` to indicate + that this is the last iteration (just the initial load). + """ + + async def __anext__(self): + if self.buffer is None: + self.buffer = [] + if await self._init(**self.kwargs): + self.left = len(self.buffer) + + if self.left <= 0: # <= 0 because subclasses may change it + raise StopAsyncIteration + + if self.index == len(self.buffer): + # asyncio will handle times <= 0 to sleep 0 seconds + if self.wait_time: + await asyncio.sleep( + self.wait_time - (time.time() - self.last_load) + ) + self.last_load = time.time() + + self.index = 0 + self.buffer = [] + if await self._load_next_chunk(): + self.left = len(self.buffer) + + if not self.buffer: + raise StopAsyncIteration + + result = self.buffer[self.index] + self.left -= 1 + self.index += 1 + return result + + def __next__(self): + try: + return self.client.loop.run_until_complete(self.__anext__()) + except StopAsyncIteration: + raise StopIteration + + def __aiter__(self): + self.buffer = None + self.index = 0 + self.last_load = 0 + self.left = self.limit + return self + + def __iter__(self): + if self.client.loop.is_running(): + raise RuntimeError( + 'You must use "async for" if the event loop ' + 'is running (i.e. you are inside an "async def")' + ) + + return self.__aiter__() + + async def collect(self): + """ + Create a `self` iterator and collect it into a `TotalList` + (a normal list with a `.total` attribute). + """ + result = helpers.TotalList() + async for message in self: + result.append(message) + + result.total = self.total + return result + + @abc.abstractmethod + async def _load_next_chunk(self): + """ + Called when the next chunk is necessary. + + It should extend the `buffer` with new items. + + It should return `True` if it's the last chunk, + after which moment the method won't be called again + during the same iteration. + """ + raise NotImplementedError + + def __reversed__(self): + self.reverse = not self.reverse + return self # __aiter__ will be called after, too diff --git a/module/scripts/lib/telethon/sessions/__init__.py b/module/scripts/lib/telethon/sessions/__init__.py new file mode 100644 index 0000000..d10c76e --- /dev/null +++ b/module/scripts/lib/telethon/sessions/__init__.py @@ -0,0 +1,4 @@ +from .abstract import Session +from .memory import MemorySession +from .sqlite import SQLiteSession +from .string import StringSession diff --git a/module/scripts/lib/telethon/sessions/__pycache__/__init__.cpython-314.pyc b/module/scripts/lib/telethon/sessions/__pycache__/__init__.cpython-314.pyc new file mode 100644 index 0000000000000000000000000000000000000000..fc3df45d1293e426d2a561703c3ed638720efdf7 GIT binary patch literal 394 zcmYk2&q~8U5XN_#CM{wMK7f?sDM&k>Jc$SKkbnxd;L)&6*K{@i+?@!y_2A3+D853V zc+iU{g}#83v_gkv_V<0uH|*|lzqiMzzTSTxUuplW#ecQ7us$VNvU@(^KJz&8LCr0{ zT4A{gzVy^SeDeUs9GnK}uEG6t+%Oum~jxoKxQYd!~)GCd( z#*N#px?*Nfix9O%^C~B4M(B1U%yUd}ZEr*q)eea_wx-Ui0_Fk*6h>+|fiO$PA}ZlQ z#nQkyl8H(ch!j<%#f!pnB=Q_gs0D@#xVs!(i14Y%A<5=NEb9Z6qk&k>0A(z3k)>cZ rk24SiGz^1aoR?MiY!f-xeY!>c59WZ{uf;iEvBMP`()wUSy-)5RKdx|K literal 0 HcmV?d00001 diff --git a/module/scripts/lib/telethon/sessions/__pycache__/abstract.cpython-314.pyc b/module/scripts/lib/telethon/sessions/__pycache__/abstract.cpython-314.pyc new file mode 100644 index 0000000000000000000000000000000000000000..9d3e6811ef42117268f6ce7d75d0931eb781ab55 GIT binary patch literal 6523 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 literal 0 HcmV?d00001 diff --git a/module/scripts/lib/telethon/sessions/__pycache__/sqlite.cpython-314.pyc b/module/scripts/lib/telethon/sessions/__pycache__/sqlite.cpython-314.pyc new file mode 100644 index 0000000000000000000000000000000000000000..7a2d68dda580a76aa7298979255e401f9a6f5af6 GIT binary patch literal 19014 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 literal 0 HcmV?d00001 diff --git a/module/scripts/lib/telethon/sessions/__pycache__/string.cpython-314.pyc b/module/scripts/lib/telethon/sessions/__pycache__/string.cpython-314.pyc new file mode 100644 index 0000000000000000000000000000000000000000..4e93a51589ccb33b1fba7cfe2c196d235830bc9d GIT binary patch literal 4331 zcmbVPO>7&-6`tkKisFw*NtWz7QoNM>N9%{SBguxGxbdHA#g)vg9JWnquU5M&X{*&P zGrN?{6hd_@6;zaflG3+#oyc2CBX(Bf4CSv3&KM?fv zJ28>#CYi=(_&NT7a~mc!+tw{J!?}quZQi2g`b5K-k~4crE59E-efEco_Qm6aL$=Eu zQ#@5(AF6(A-2p--GPgvo5{DBB*xKX-+lj@AoD&jbdG0l^eK#>8c>+`rDI;ZYPWUR% zcp;n-!_cP&s!E-PDvxJ8ILkI(gsqA&P9t*Q)KMpz=bTucoZ_4~oHOnwCLF4pGu1=M z-wY5Ey)vuYg6*AKnAQzVP-Ih>(gaPmb)ziU@`9!cvZ)G&C96oqMF(tjgB(i&a73M znRNs(4o+0E!Tjs98r6idRT5OoJmd)1Ws~t0s>zN9JC(uCK2^8pqUa46uhD?_;`TG7s3`$!VcTH5*(o9E~2S%d6Tnb2_+b9#r8$Ct>}x;3+)3 zAR8rZSePn1nw=5)PUGDTPfqru4u6}k)Zq#EI$$N!z;rPk3J1}NKWq?A1XF8EfbMk& z{ye1O$sZG>4cR}~hCGwmk!u^}r-M87W1@lC~bNG!10$dZ>dI&B=hZTGdnF!~}B;cT* z{rgCsJ5=SWE9lNV;9&3G+5%GW(TAM84la+QOaYM9Ya%yLy{1_%57@JPWsxg1`f~Z2 zoESX$UH|H?g~qsRsbZkOFdJoapnqpelR}d}muvQ^`j`*+HWXU?*<71X)yKLG166Nb zhqL908S`b{J8IVUFNO*=FXTFWiTao(`JlgV?$!FdPl4W)PZzpe$Z7{@ca+^}6LYSfKL5+*Z5 zS@PUFqIhBaij=<~PK;h2ccXbsV~DFMIE!08BS2L9Q)m zw8jhqMp6u2w^2e+mgcl_hId1@W=vDO7MBNAXkkDvx{)LYR?>rO?L#Z6 zp(kyx+&lY=cW;iY@9n%d{i~kk)<=7L@4o$U7tCY_f4lgb#r0IWk~+MaI=tMzmOA=r zU*-77>hY03EUX*;eFSmZ!aVPZ4rp+Kp?b&Q2@fL~^@d7$xdP1_3t%J~g z9Xg5uz(_6|=q3+>6hq$SZn!+voDwwwAHxiNX#yCTfOo@5psN7sITe$SVvhA8=N0G} zAaUVSn*_SM2R3lHe~x|CO$9mD&`kJ6w!RI-c2vr3M=wtn-c@Ho>zSa-Psz46G~hOusA0>~nlv>u;FTpzC|?cH>3#+&{p+>b8L}Yqg8O4gZ&mfY{;2!9pw_FL>C40XhWp+g!^$ zzSFOs=uwo!VBYE1mB^90!M-ywK3I?oKzv=0drtrs-V_m*;;R21&v>4CfsKV@n*_SM zC)iLo2c%fTa>W990Sv_VNUqA*KKKAX1Ckv?B;BM?3-cUC$Bu>ppFHYI#Ci;z~JjtZoh+ipa8hEq|I=5*@Vd1fiX)4`=5?Hn93G5KQ z+z|3876cFS@FZ^7t7Rc9W%LNlKmf5|$^gv(@u1*AM+l$jZp(|nfpaXNegxu6?uXGKrb`7p2hAN3SRugaB46G+#{iw5&9$ZZiemecT zxliWSdHL6bXCv02$}R-(7A0;of%t_9eAX`{~pZ- z4b9u$;fiJ$Fy%kZy-#p@4=SmF-$J+I&}H}bmY;?pz_pO`{6g78L=Ep1E5FZg3z?9SLf2n 1: + results.sort(key=lambda t: t[2] or 0) + c.executemany('update entities set username = null where id = ?', + [(t[0],) for t in results[:-1]]) + + return results[-1][0], results[-1][1] + finally: + c.close() + + def get_entity_rows_by_name(self, name): + return self._execute( + 'select id, hash from entities where name = ?', name) + + def get_entity_rows_by_id(self, id, exact=True): + if exact: + return self._execute( + 'select id, hash from entities where id = ?', id) + else: + return self._execute( + 'select id, hash from entities where id in (?,?,?)', + utils.get_peer_id(PeerUser(id)), + utils.get_peer_id(PeerChat(id)), + utils.get_peer_id(PeerChannel(id)) + ) + + # File processing + + def get_file(self, md5_digest, file_size, cls): + row = self._execute( + 'select id, hash from sent_files ' + 'where md5_digest = ? and file_size = ? and type = ?', + md5_digest, file_size, _SentFileType.from_type(cls).value + ) + if row: + # Both allowed classes have (id, access_hash) as parameters + return cls(row[0], row[1]) + + def cache_file(self, md5_digest, file_size, instance): + if not isinstance(instance, (InputDocument, InputPhoto)): + raise TypeError('Cannot cache %s instance' % type(instance)) + + self._execute( + 'insert or replace into sent_files values (?,?,?,?,?)', + md5_digest, file_size, + _SentFileType.from_type(type(instance)).value, + instance.id, instance.access_hash + ) diff --git a/module/scripts/lib/telethon/sessions/string.py b/module/scripts/lib/telethon/sessions/string.py new file mode 100644 index 0000000..fb971d8 --- /dev/null +++ b/module/scripts/lib/telethon/sessions/string.py @@ -0,0 +1,63 @@ +import base64 +import ipaddress +import struct + +from .abstract import Session +from .memory import MemorySession +from ..crypto import AuthKey + +_STRUCT_PREFORMAT = '>B{}sH256s' + +CURRENT_VERSION = '1' + + +class StringSession(MemorySession): + """ + This session file can be easily saved and loaded as a string. According + to the initial design, it contains only the data that is necessary for + successful connection and authentication, so takeout ID is not stored. + + It is thought to be used where you don't want to create any on-disk + files but would still like to be able to save and load existing sessions + by other means. + + You can use custom `encode` and `decode` functions, if present: + + * `encode` definition must be ``def encode(value: bytes) -> str:``. + * `decode` definition must be ``def decode(value: str) -> bytes:``. + """ + def __init__(self, string: str = None): + super().__init__() + if string: + if string[0] != CURRENT_VERSION: + raise ValueError('Not a valid string') + + string = string[1:] + ip_len = 4 if len(string) == 352 else 16 + self._dc_id, ip, self._port, key = struct.unpack( + _STRUCT_PREFORMAT.format(ip_len), StringSession.decode(string)) + + self._server_address = ipaddress.ip_address(ip).compressed + if any(key): + self._auth_key = AuthKey(key) + + @staticmethod + def encode(x: bytes) -> str: + return base64.urlsafe_b64encode(x).decode('ascii') + + @staticmethod + def decode(x: str) -> bytes: + return base64.urlsafe_b64decode(x) + + def save(self: Session): + if not self.auth_key: + return '' + + ip = ipaddress.ip_address(self.server_address).packed + return CURRENT_VERSION + StringSession.encode(struct.pack( + _STRUCT_PREFORMAT.format(len(ip)), + self.dc_id, + ip, + self.port, + self.auth_key.key + )) diff --git a/module/scripts/lib/telethon/sync.py b/module/scripts/lib/telethon/sync.py new file mode 100644 index 0000000..f647670 --- /dev/null +++ b/module/scripts/lib/telethon/sync.py @@ -0,0 +1,74 @@ +""" +This magical module will rewrite all public methods in the public interface +of the library so they can run the loop on their own if it's not already +running. This rewrite may not be desirable if the end user always uses the +methods they way they should be ran, but it's incredibly useful for quick +scripts and the runtime overhead is relatively low. + +Some really common methods which are hardly used offer this ability by +default, such as ``.start()`` and ``.run_until_disconnected()`` (since +you may want to start, and then run until disconnected while using async +event handlers). +""" +import asyncio +import functools +import inspect + +from . import events, errors, utils, connection, helpers +from .client.account import _TakeoutClient +from .client.telegramclient import TelegramClient +from .tl import types, functions, custom +from .tl.custom import ( + Draft, Dialog, MessageButton, Forward, Button, + Message, InlineResult, Conversation +) +from .tl.custom.chatgetter import ChatGetter +from .tl.custom.sendergetter import SenderGetter + + +def _syncify_wrap(t, method_name): + method = getattr(t, method_name) + + @functools.wraps(method) + def syncified(*args, **kwargs): + coro = method(*args, **kwargs) + loop = helpers.get_running_loop() + if loop.is_running(): + return coro + else: + return loop.run_until_complete(coro) + + # Save an accessible reference to the original method + setattr(syncified, '__tl.sync', method) + setattr(t, method_name, syncified) + + +def syncify(*types): + """ + Converts all the methods in the given types (class definitions) + into synchronous, which return either the coroutine or the result + based on whether ``asyncio's`` event loop is running. + """ + # Our asynchronous generators all are `RequestIter`, which already + # provide a synchronous iterator variant, so we don't need to worry + # about asyncgenfunction's here. + for t in types: + for name in dir(t): + if not name.startswith('_') or name == '__call__': + if inspect.iscoroutinefunction(getattr(t, name)): + _syncify_wrap(t, name) + + +syncify(TelegramClient, _TakeoutClient, Draft, Dialog, MessageButton, + ChatGetter, SenderGetter, Forward, Message, InlineResult, Conversation) + + +# Private special case, since a conversation's methods return +# futures (but the public function themselves are synchronous). +_syncify_wrap(Conversation, '_get_result') + +__all__ = [ + 'TelegramClient', 'Button', + 'types', 'functions', 'custom', 'errors', + 'events', 'utils', 'connection' +] diff --git a/module/scripts/lib/telethon/tl/__init__.py b/module/scripts/lib/telethon/tl/__init__.py new file mode 100644 index 0000000..e187537 --- /dev/null +++ b/module/scripts/lib/telethon/tl/__init__.py @@ -0,0 +1 @@ +from .tlobject import TLObject, TLRequest diff --git a/module/scripts/lib/telethon/tl/__pycache__/__init__.cpython-314.pyc b/module/scripts/lib/telethon/tl/__pycache__/__init__.cpython-314.pyc new file mode 100644 index 0000000000000000000000000000000000000000..0195b26ac3febc355376e5f47c6824e0dc01cfe5 GIT binary patch literal 277 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 DOn3@P literal 0 HcmV?d00001 diff --git a/module/scripts/lib/telethon/tl/__pycache__/tlobject.cpython-314.pyc b/module/scripts/lib/telethon/tl/__pycache__/tlobject.cpython-314.pyc new file mode 100644 index 0000000000000000000000000000000000000000..69e66a711689c4325b3d000a6e43c07966d53ab0 GIT binary patch literal 11263 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 and ``Vector``). +3. Those bytes may be gzipped data, which needs to be treated early. +""" +from .tlmessage import TLMessage +from .gzippacked import GzipPacked +from .messagecontainer import MessageContainer +from .rpcresult import RpcResult + +core_objects = {x.CONSTRUCTOR_ID: x for x in ( + GzipPacked, MessageContainer, RpcResult +)} diff --git a/module/scripts/lib/telethon/tl/core/__pycache__/__init__.cpython-314.pyc b/module/scripts/lib/telethon/tl/core/__pycache__/__init__.cpython-314.pyc new file mode 100644 index 0000000000000000000000000000000000000000..956055dd40ffd1b6d2c2beeb681dda240ea03b95 GIT binary patch literal 1449 zcmY*Z&2Jk;6rZ)7G+9dPB5@_J2@_YTa^2j*dOSO}r&-TzXWrJB z6Y5`rIPtG=11?CGa)LVz6$fs-S${;Vw7YNSef{2hznKSj-VGT&fByW}i&l-XKm20x ztjvYCuSxixwHdN+f_BghPAgd1@N%VDJ*{D_l&j5^(^Xt8c!eY zwa%Wiq7u{l#a+f;vetWxBsc$a)~kGVkFj<30ommY!}iJ+NgL%#vBoMtuQC>PhRVUn zq(vrSXtLCSFjm5*%cW2}+XS5C((S-_sKgLBsYYHejD97xk{V&mCoV(-sR4&YzYj_S zhc3!Y;?U?wOB@?}8I3Tv291+Cl{R|PY5CQ&a@8dthg=I8h2fdbCNNJAf;;KTw1e+d zzMFG#DN{%}@*TjT1S?%`v?El~IX;lEtqlSXMOI>)?>1(}3mXEDTxn_NRax1WR#JB7 zW8MUFA=fF4O_8NwNMpx}l;qvdl%T>D4i_MB2fS@xX`T^$53QYxwsb{?hhYfx*jyw8 zmo~Qg2cL*X>E1(v|1uf5L88*b%;-j_1r{Z~ZujH;^x%=$7u{|Y?t6P049xQSA^R>` z$y3PGrO#S+8o~7EHrtuh11kRk`XFWAR9R9}D|=G#g0Ly18grtH1EV3Ac9)u0NQcCozOf_jYlm&8XpTOL zGb3sN*Axy5tD)OHr+~&DDUFZ2-Ev&ZJ>PuVq#_Rf--{9V2*8ssUlMNySxZS+XcDAH z!i=Ps+-8#~Yy{Kw&eu<7l%z%YCHc2X?$WA0R~#>B=*^dGdD?hqdMim%Ba&pgj@g{F zwnArn8kRuHK$cD~o7U&*C8aIhJ8Zg6WSeox`g>=3=lJaDvrhZ@ap$a^G*70t{UJ|g zBXE>H_|^qvw>QPupRJf0ZsWaJm{G(Blen)k>EdW0N5rw1Yg7ZvUn!jI@;r~7upGrO zK0n$0oS+qXJiAZ4Lgnbk4nH)mtCgtomR0K2Yev#f!S8kWl@#}Xt8bM% V^r=^0-~RV+|98GOKfZrd`wt2s&58g3 literal 0 HcmV?d00001 diff --git a/module/scripts/lib/telethon/tl/core/__pycache__/gzippacked.cpython-314.pyc b/module/scripts/lib/telethon/tl/core/__pycache__/gzippacked.cpython-314.pyc new file mode 100644 index 0000000000000000000000000000000000000000..a2e53ade504e000fe54f217803c837e4350b7032 GIT binary patch literal 2868 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 SfXq{2lpDQy^%>yDRs08J0m>Z! literal 0 HcmV?d00001 diff --git a/module/scripts/lib/telethon/tl/core/__pycache__/rpcresult.cpython-314.pyc b/module/scripts/lib/telethon/tl/core/__pycache__/rpcresult.cpython-314.pyc new file mode 100644 index 0000000000000000000000000000000000000000..5281a525c3727be1fb610e5115e9a8d06283215c GIT binary patch literal 1941 zcmah~O=ufO6rR=oSX#+HimUh^+p!%`U3*>Qw1Jc+#QBk+$YSft#l#-gtF^MpYNgDM z3hv35QWs2XUuqW$ZE2wi^w49DJ+?PH3Ro;P1lmhZb?eafP;lRjBwGkdAL#9ynfK@wQeKf&BW@Z}&cs5c-7;>LS;S<7;57qh&!Ck|-fiVGu0`^guF@5UI#*F)1Zv zDi5L<8b-R*g>-pIoDr+>RMC|Y6mNMhz&I}W>h1UKV%jk8TUi(d=ii!Jxo4S_*?Q5u zNl1a1)yS$2)CRl^0}5F}RL~I(EGtwzj|xZ^sH6wF5S5n{UF>9*6$<&st0FR?`BPyTVJUe0G8pS zdL;TPu^!;Om&10}msSedHD9(^PjShYJj+=n3~&-=Dgw%5i7$fqB`s@ELo0a}(WW%B zke@ViYuc*qSe`bSv+}lUYlchh95LRr>Dq)*ENY%f3~JuhGB+l!1EQs(mM>&Wj>Q)2 zBK0)KUeT!KSoChe)u;p85=$dRlW+hgi)(%mW7wHu?4LUYX0kpPViiU~t)ois=w^H6 zrS8p8CD!|~dJL1uJnbI_E%4*0Z2&^+C?lj#kz&48sh>?T0P76?Ceh(EU$4a##f*4| z!NVo3)UC7c<#!RvUO=cH-C>n-$pAGQqByC)HYMgQ77pLf?Vu8xcq0XSDJG* z#QaaDbX$G*v#%a#$S>=fLbALOR(Qo^pg-!?m!dV5}fXLf#0$H^On9jYH*C51d@j=*GgXuia# z%J^*E_a(Lf>=sSO^OdTXuhedUYP+sQuvy4jHKq>@5rzcmX8Xj>DA$mKt$Uw>T1U^K zT?f(O{pfHxIe$VvN@HF@PX7OO`%9F7xk8hR7W)H>^`(ue8N6Vda zdy%{NGp6iR?tO`t=~cmIBT2K zkDlRMn;KxQKx2k&q-2Z(vx;je%N#j z&&#vtDa#o_M%Z)=V~-ltHnBk|u~$me@?hI2#@wH^W?GJeF>t%JVG_a^3fHO(W8CGa zv6sV}0QL8W&~_y@xSsk$66L-l6)Dl*0}=VPV-c()X8qN#$j~$>-ZYO-#anze2me|{ z{;yTN0*J8F@@2YKv^;)!xbGuWjbj}4gKtzJd6T>fLspvC3+hM|1mO@}I7D5CsQ-mB NAgEixKM{O*?7vlsnC1Wg literal 0 HcmV?d00001 diff --git a/module/scripts/lib/telethon/tl/core/__pycache__/tlmessage.cpython-314.pyc b/module/scripts/lib/telethon/tl/core/__pycache__/tlmessage.cpython-314.pyc new file mode 100644 index 0000000000000000000000000000000000000000..3d6789bfd78229d5637b97dd79a4cc9bee78f9ba GIT binary patch literal 1688 zcmZ8hPj4JG6!+}xZZ^||G^I%nRMe8YQgABAsFjf8{*7fu9%fFsW{lWmC)Z*0Hk-+S-(-kXo!S_}}ZUw{1l zS=dJC7k9W=on~|X0c@t|pcQ)$(0$K(H_-}u55=uD6#GxTO~1CYt+;&)^*g_{;N17h z-tNQ4gJ+ys*5FhhuQ{A=!iECuQ)Imu+15eZ`m0Ex*t6|}1>14(wj28(=i;{Q?IAlK zcfdinoZo%;h#NzPoEG3LNUa^4ttetj^U(5)4>iq0rH4^&$68qx8Lp27<0P-2nOjEW zo5o!NQ0^7_#~zFeS?e4ikt%E1RE|bwhHey_*(&n)d1q$42ej#G9`y# zc_h`A0Q14ba#QbAGTEw*_*2|?`Zx%-DNAP%h=gxYA6`43{4X# zgK3#9*YPj~z4H%YGe!GX^_U>(bN!h3vN4ZLhj5~!GTN7ns5V`TOsgjY+s(FHM)8pZIQuYV| zWJ^eSZ8n49#od~Va2doDy;{BT{gqn0z4}A%94--?@c;*xSH)Nin<+gwzFq{Q@CJ1 512: + gzipped = bytes(GzipPacked(data)) + return gzipped if len(gzipped) < len(data) else data + else: + return data + + def __bytes__(self): + return struct.pack(' zy+AS%V6fwOPR@xM);!nbyr^T{^Slg2gpudEY=|Z{J%@5Zw6Ntll5G)V?71O3qKjS6 zO}QwRaLMz6ye5`$*>g)?7c02pxh+@48m@Vc<+|9w4bL69DYkHHhxNL**zV#_vSGWy zBz~L@P5!JHJRR#CDX1TUI&}~p8kH3TqK?eV5xz1Xb$Vnn?RGua)~bO%7$Fup1<_Ff z9~7hxXFbC3Uq6<4W^$T0S*n3r?JDSLJIdT?vw^C%Zs19gk2P3@raIwek8Ww)VXMaq z&?Kf*IUZ{m8f$6BzS!NK#LDT=wYAfM8c9y-sWXCk$6@NFpzex}abDJWost)XE{unj z+KE?#$Ys%KWd{vk%_gAfs&F>wzLQBzXOrehr3o#UwW|!gDYX9^t0D`rA4~r8g^r7& z_dq5lhpwwu%PZ^h${H4_`I%kN$|hr;DpY)AHSqg9DTe!MaL%2a<@*Couf}kCX=VUw zZ18+fl_j?cC```y>%+YRmAo63d|0HTOuL3D(elikaMYQ`vm)o1@nmGN81jCf{tf&6 z{qlU$cv$Uu1UKm#?$ffImfy7?2&U}Ll&w$M=9Jy_ygg;~qmuI5m+SU7_5g0sp1(gD B2OaP0a~X8gN5%``lr9DL;@4b1u$9s}-0e=7XPyg{|=@miv1OfKzuru4g z2AO-p8|_PxIbk+pM2=v-ZLV#$-Dp1|>=TX(OYMWgQpc$_+QR5qijD}G&VO$QI1{m= z#zsxnA=6Wqjl5E=)s1{jmbIe&0P@dnD{S9^oCME%T`(d`g3Sta&SMuDGnY0crOR<1GtZa_XFI|!Giz~aPR?u_i^w+fcJB78sI?= zeh%OR9DE4igB&~raGHY;1Nq>QA zEnmDNR}D#qxfoxPYnrSBsEd-gu4=Nlswr}{RM`-fsv&EuQW1(&O_Z>$SW(NOjIH%- zJiZ{WugIEyIxfOb)s(VQl`7)IX`^zwaA8fVR^^K8iG>VBMfBx0x!EMV1v~|@;em>w z$htUDC@DpQ@94aUC5l>=v<-1xs@0TgS*+`_CeF?1qM?eU^P5mmusg#JUtN={VnMBx z3ZhgkiG`|s7jiIqo|K%V8A?&9NmZk8Lfla6Vo|CRdaw(4hG?uQI{$ol9FPFTvREh} z5qYIlfVPFgZKZfyF6CD?0Cc)26qM>61-MMmC?$oOrmm}ojmriBJt~@iC}fNTR1sEo(t0Lh5aJ`6ICHr9>{$ktrAahT6HC1Ib!mDC6!hlzg=GYd^is1i=bQ8EG zRGofb>t^M*gj^@fH~*iq`-L`9R=OmN2oEBQnNBOlhHu3fN31TJ6IKUiG^E9fH+hk1 zw>osWvZ~>OH9Tf?BxZbk-1^2;Niw7~ANWUNqXhCWqMR;CQOA{%RJb;_tr73;d8 zu1}Fg32Txp$!u-Iisj)5QjC1w+V5I=Syqk=)K5TiPx$zN_+FAG1BV(zljhLmzYb*I z>3FaEQF`Lf2Yz<&!NHHxC*SG#!`^Mc7mAK-0Khy9II*3D%sm0P4*$Ks96R>73fPDu zUmuL)@}HZ*NkWzor9Yl&BmA`5l+uHU)&tFSe9*Yo3#m2$iIs8>ODTy^s$zXE)vd$Wq?LMktHjr6?r;q2yX3q2Ba>T8~^YcA{&AS&ohyaF;8|^>OFSOrN9z*~` z`~3|+ZfL(R`f)@1J?+QkB=K^~+Oz)J068%(p7UdeQZ?RcG|}B^1KxX)9g;3Fk)wtb8D)h z+D&RCzvH8BfOT_Q)^5m#)k_|w(*hj|T5$4WNih~=U6;zTl_2fbZRmyY!Ev_KvRW=z zWOA%p*(Yc*Emf@)d0wq)m|4fdd!P@RBPNz^wIGfn<}}%6{;X0e$t`6?Tij5}RmrG> z0c~}I1!;?_1w}Kc!pn?#U5sy&;bWMQSEYKzfU#HQ(vnhMGjywuFkyuh>yWRIH97rY%^1@vWMwfeA`kM~QxX{3ZF0dRw->5huPE zkkv`eVe3SAQ#WZi6|Fsl9kl6#Hexnd?VtwgXnu@~#CU*$O3B*m*5#lG_e=c~P|u0= z(A)q1`n&6mo?)|R_?`Agd-i=$e3aVPNWEmHUUFy7nyIs-#L1j9Q|Gq!bRLR-E_5dL ze1^$Zk1#O!;o19VHxox6B?o`d{-c|Xp;>ciwvn8Bn4J5#f4{eK!5ms>Bo`kh7ooDz zKW_GqZzjfr+cx?~%>I$h#0aS-#9T5{mq=#DOwDXX+YbXV?L9yYBwH~crqO@Q>_4`d zIM!@)#!Q`YHzp(L>cGYw*ccM9tPVW);oJA${+IsI&BUlf&=qs&N+WsoVe%>t=B4{D zZ6*#ol{s@L*GOJ}n7mE~lQH`|;zJGc%G30c6%^bScNah|Ub4b!F_g~pe41d}#^jwel^>Vaah+h8f|ATrtrxgU~ z;aqKi=FQax=-MY^p=W!5wjHW71Zdl#{RU{;q5TGE+oAo6Fatcl(0&7S@6dj~RL#4E z#vh=GhmI#e59j0_pl`Q$?*B85J5=rg`gbV*1GI6D|K~i@62Pa#|6f{H53%4c`6&+N z)2tuYu>r=24FLL-Z3DpZju#YgAK|tH#+sIZCPIJ21kjFR4wtGnf(cGc?JJm|p{L=x z)W$G5i3!@BRCOaNvz9@)2UWSIDy$vH!aZ1M>ucI1w#5Zv^xjIgQtr?JR*C^vJ68Nn&=~XBxTN6phM)(Ab7E-k6iKW6riZ6?M- z+EQ(YRJv8VaD9+hF^R%;#!Q@XGC?||ty4K9c+|WJvwvbUG2v8SG>0xWl9wJPF9oQG zSIwcTjpY2pFR$qjSpMkBfWFu9DqdGv0#UPvp$^ui99 z9z-vE12Xr7*U)`WesIqLlg9fCx~bxJ@&I2g+ZV}gNI4F-k>KOd$o5vG7pKSIJ{Ely z;$2A>v8)U(dANqHss>!l&B5i`vR0SzYSD9vc~_}a#1&a&yCyU*=muVXW}%x#L|D(c4JTLSA45;u=pVc%{AZ7lO8;Q=$CE#tY^1YhI{PSnq>;X8 zrY~-{M^cv~TSBD!a)2KGZ15Ydm@9Cv^JVj!oK2zpCPaksn^gWOO75$epjz*`-1^_; zUZkGuVBX4|CY(NTLecye|OY=WMv7&^qu*s+UR1?#CJmmXcll zbLd02kI>Yw!V&u3V@jw*{N1e-7r(mlv;>2fb$-Fv6fslID;Q7p$2$q;vYEb&OJ|NO zow;3KI`2Dz!ArB6tgS1$4);zvjp3`>VSL1tZmy=dJzc+`f*B7BnhVuAi2?0BxSKjB zh8l+HKoVAdf^xw<5_HZ!STG+aQtvOkyU<98CKy)eH=Z^V)7#Ou)C>rwt$SwI1@lcu zFkj}L_QZlfCx&m446xk`94`J|(R^JKE z1Fe8v?XtOc(cw0Y%N5a)1{2y?gNxzvn%nshuhmy7N|6Kx5?hYB9)g_+kLe%8%U<7Z z(@E*@2HMfm6mXBt=|qrbG-syg@BqF_4&bZ1zM;J1a9I+;(MTgJJ)D-R_D&P8DAn5- zqmG*o+ie)9T}~e8sv!Eel_x)W=Yl46k`icpQN1UxtMVWx5IlK(-kz&w`YH-!fe2(_ z*9DSs1Y%G7io9wBGCX8zw}`CZT;KDubjKI$YjxnmywSc`Hpa0LqL%$^eAaBT4;?nH zDJ9&UiGB(3Ns3z69V-o9hrr+^qHN;qU#D+~K;XQKa{+x)ZL{O2dfx-yqvf&~s(Lg+r z-nWW0>`uIkrhz@kV4(#poVfdXhH0g)Air?66t8}LG8kfvw#oEd3m*@bN+9y$PI6i> z(+eo4YeY`hc3n;z&SocxHMrm=>j=)FT5We0lC}^ep@dCQ9!a#)Zh`Q1U)_oS(`I@a z`H!xR$TQ&DkU9RBF~}6&Hgu!_p#)DT1^VI+@svB4|KY1)F^JcF5EG8i%@cC+B;3_= ztoSiX!1rguQ2V0%Bs-1YMtCxQ;;RV0-DRLeLx|lR^*DrraPWOjt&Cmnvhn--+#KcN zXvkJUVo8`rq*lvdq?Sc)B0tGlOquP>Th2fTpTgIISRTjQNgC#29dK_J&6>Bkts?O1 z{aLn-y<(`7h@evQm5*HzMJ`AbNy2bK9_%Sz>)2KKEf7e{tK?_RI=<_y3)FGANpCyh z^vBR}Txux|OA(b-$NK?+ETALNMW$AX*D3T~;Cc}M_c7fD_i?=QMDL((jks4uAkup~ zEs$r-aFhChV3$ai6$qqQR>gVc4ty{HUzcv$bIjW2JfjdBv_Nl6b?~D?(4@2q?;t}f z`?9ADQ5hH@vza811n-+iDa z|AwfqsdacgtAh04cOh0}NM-nks8ZcfC+$15pfDwNI^ihbxDw$lrxV-<7_b=K2Bf%2 zOz3nWft+5O(zzwK*^`GMhTW2LDBtI%%D}IJN5*i?6WM-c2aOR7mBr6WfRuF*47{iQ zm?E*!=pn}5w7CEs&XNAYk1%{G(Wf;GvddIP8}l!YxmRuo+D~@cOBc=bBI?08q6g=8 z-41D*`8bc+po0ZMLl=UUi2oLeEf%bS)j@r}0n#}k!d+ZVhU>aJa%CfQ(O}T5PgeUW zL3u_s6V<3&RKZw3<(5vMdu-lJ&!eOkiKG^HT~bf7bil<4Z?nN?9bEo+m#&QhveBM= z>AVyqjzPOSpFBKECj{+pKp@&*V8^?xcK?yHM^Voguk+#NWM7QjRSbIn1j-a1a?S+_ z95dD1tym9Hu9Nax;C05+X9A>>36%h9)NtFSw$?!K1rFcpNZD5)D;sQGctQ%j_5ZNb zinwm3uj7iiK~}_#U0)Hu%L$4T5=0^2A#Wd`Y9LX&-Z@lkjmsk*mo|tiqfUH>r7vO` zT?rC~bG{SJo01oHzH=x)$2IXMfoq~2O5rcIO}~q?Q@A(#n9zuzVQ6=z&1#1*3HmaM zR~hNb!h{9}&T^cz*1w z1rEK0RI3|fJE~=6b;CQ~wZDfRtU>qI?_gUG#qD;;ZDGzS8+OWuow6~f zY|JSeb;?HVvg6Say9{z$zZD@5$wpa+klQNI!mjA$h>eQbtv-SpR(hq03*pwSap#-3 z5V}fnW6h^w7GAi)(Z`~53>UL@X*@c^N-?{2fZ%pVr&%dxxAxogIz4n@4~()PdOM?6 zSr3@qO4XW@i*t&3RtkfAr7mk9K#+&_O*5-OwG{@-0 ztc@FsF0xY0I`jtP-e}^&xyr`oqU^K2NObt~&PX%^zVm2?>{ev?EM}rsn^B>k5sB6f zrJ|FMzLClYRFR@z5{SJ__pP(qKf{v4rKk@=veh1mM1CO*{z4e}yz^F9B(p7GN}ljP Djvq88 literal 0 HcmV?d00001 diff --git a/module/scripts/lib/telethon/tl/custom/__pycache__/button.cpython-314.pyc b/module/scripts/lib/telethon/tl/custom/__pycache__/button.cpython-314.pyc new file mode 100644 index 0000000000000000000000000000000000000000..0be9437d92bfe2b6f9819b531ebe8270d138b2a9 GIT binary patch literal 15868 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 literal 0 HcmV?d00001 diff --git a/module/scripts/lib/telethon/tl/custom/__pycache__/draft.cpython-314.pyc b/module/scripts/lib/telethon/tl/custom/__pycache__/draft.cpython-314.pyc new file mode 100644 index 0000000000000000000000000000000000000000..b54f43f2faee7d65b2f8dad0a830d6fa1c94db75 GIT binary patch literal 8642 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 literal 0 HcmV?d00001 diff --git a/module/scripts/lib/telethon/tl/custom/__pycache__/file.cpython-314.pyc b/module/scripts/lib/telethon/tl/custom/__pycache__/file.cpython-314.pyc new file mode 100644 index 0000000000000000000000000000000000000000..107ff8327bc1bf7dd326d067022b3307e9af0cc6 GIT binary patch literal 7437 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 OcG@F6w??q$%l`#lhWsS} literal 0 HcmV?d00001 diff --git a/module/scripts/lib/telethon/tl/custom/__pycache__/forward.cpython-314.pyc b/module/scripts/lib/telethon/tl/custom/__pycache__/forward.cpython-314.pyc new file mode 100644 index 0000000000000000000000000000000000000000..bd4ac7d6bdaf45163bd74769093ec254baf88676 GIT binary patch literal 2815 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 b;#Z8vYMZCY@~ZdK-g$?3659xSu&(?+p}W0B literal 0 HcmV?d00001 diff --git a/module/scripts/lib/telethon/tl/custom/__pycache__/inlineresult.cpython-314.pyc b/module/scripts/lib/telethon/tl/custom/__pycache__/inlineresult.cpython-314.pyc new file mode 100644 index 0000000000000000000000000000000000000000..d43b4ccd8d6fb6059255e388885851d75fd25a43 GIT binary patch literal 8125 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 literal 0 HcmV?d00001 diff --git a/module/scripts/lib/telethon/tl/custom/__pycache__/inlineresults.cpython-314.pyc b/module/scripts/lib/telethon/tl/custom/__pycache__/inlineresults.cpython-314.pyc new file mode 100644 index 0000000000000000000000000000000000000000..34c908ef9878e35bae05b2c452bd70671563e605 GIT binary patch literal 4548 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 literal 0 HcmV?d00001 diff --git a/module/scripts/lib/telethon/tl/custom/__pycache__/message.cpython-314.pyc b/module/scripts/lib/telethon/tl/custom/__pycache__/message.cpython-314.pyc new file mode 100644 index 0000000000000000000000000000000000000000..f8043ecbbfe0b48708bd5d7addf42bd80b6a2645 GIT binary patch literal 57555 zcmd_T3wT_|btZT(x*KRT8V}E?rM*|?iHvkGG0KO?0gupnW1T@i2utlI7zTFT- zTCx>+CZ;m-h>WeJ=-3gNu~uY8-%y^-L{bvlqT;;vF+l^Q8tS1xG2i;zOm=3!0ZHx1 z%Fgb$|Ea3`=*9&}$%>Qx_L8Wss#{gJ9;Z&7bL!Noww!F2gy-jf^DIKvdq zd?ItC(A*^MA9W_B$r#Nvg|nW>vdn{F*1wa5?_`<6ZiL;Ya5lo(rf?3zIi_$f!nvk! z-e?|LvktNAOtJY0=bOR>2p5>bg$Ng#!bJ!dnZm^g7n{N*2$z_`r3jar!et1TnZo4= zmz%;B2v?ZGl?Ydw!c_=YnZnfwSDV5$2-ldx3c`vhT#In6DO`teohe+8aJ?zqfN+B; z+=y_aDcpo`lPTPcaI-1gGCDZg0(g?6o>33_zjd?~Zrf-Z-1gCSxE-S%aJ{2mxSgY& zaJxo_M!Nv<-S}INzYX~7!QV#wZ5mDM%oyE_64Ov-_vjX+Uq8Babld3m(QTb+qrLPd zx?!{r@javca5s+bfV*k*5xAR22jFf&op!Q1Z5`be-8L>ow~wX)l?*2`#KiXay7q8tlHPbk}>GutLbo=L?Q^1IA2hXd-UnHRTrlx>cf}v?8dLj^1mD#}Tv49%!x?H0H?#ByX6b^lueK9yYH-mSfXhfMb zaF(k#8kh-0PlQ9>=!}a4CD0dDC44Hx zTj*2K6EmlLr|BIpyu=OX&)TiIUbHEE`zL`iEt(gx+^_;$l_89^x8nM}cKj@p752*p)gKEiOn5E+YhKw}2=Bnx+ z;#F=}0^Vsa`YjR-P%oh8DeKc=%yDWGH5#Reszg9004&$;D*jpCCy^83`58YI56KGKV3-J6jG;^Evk^7$bTB+0nK`Y@M^F`19OEAf0g~Glf;u1>RBEd-Fh5OW zy}qk^YdcMmDPKr$73)!gMy-fBHTX1e8v|?u5_(TWqjQmMot=bord8jpH>^%~9-9eI zck(uNXk*k7nF*ijm<{^_{gV@-8|K3J{QWusQ(EMw+3izU2DHbg98<$S|CBFc47g~c z-W{wdH0viKQzruc`I-B|rg>&|*JB9rSU%Upw>?_5RYTsSO5ufiCe) zoqkXYS<6IH%VGdH(TNk7sP`=is0!&&-C3V{5_2~RTB-Z^YrVz-tT4tX+8+ohEewLu zySpFTt4(=^8Ikf-NE@;s0O@|yC#fUx)v0+^1*`)Y=Xlnn9twp8s*2pVZ74_U82~9^ zNs$dzNh&WGdKyd01ec4~$ac*aNTc)NnMNaMqLL5=b$ASb|*U^P27k130#O6!Dn_MRq$ z4v5+-F-$=kqF^X$UNgslkM<6xLWWlvBZA_1FaR6_Pb*)rS_#~#=0@`eLZ}xIlSZ1b z1y`_Dd<93v31u9@Ixs0J!X_ZqhS5GFfqpHoEUmN1M$z4^s9i8V!U7i)|lH!uuh`~Y54fjQ7W1ZlH#(bK8zI)2JO zL8Byw=pd)2hEMtTV4Yk&Tmmo7Xfseb!IBZZlGI`hnomn(^!=BKBID~B$iHL0V|DD3(R#*e33f49vj=$)}1dez}0Mq+d^tU#FL=csYL|0(}M6u=to{4e{W#CYPT#b*?%eLZi#0R`$ zT)>+U{P;lZL=3W^Oc98d`6;~`<3R14@k5n zG7pZ)XHZddC&JM%h_k1$#?!AqJT=eQx^`tccpMRtXmIKzm_bmP(J8M6j)cr;Ld^p2 zp7Md71Y~k9lFuhrEU@FGR^FuEeN>D#&4{W_o@i9?UYgI^DoKCgd-{WPStD`CEVLhYK)03d6AqR|bU^9>v zkbRKxwk;%ZeB9hmL-gwO=4T$JTk6w}<#~E;=4LX?6 zLc@sf3GnxkFxVbrevSt8w*@ijK+pr6^d%q)b! zsw_W}b5glIIWIvYAzKFL=0aFiqS2WN%{HjUa8nyLQ4Ow`vB384P|NS~;j*XRMyq{e1& zNJxf{0XEF33x(E1kwcI}DHDu($qZ_muvV|XL0AZC|1%R55r|3s7|GR4h3n9ijBOJ7 zqh}g_HvHZD6YOpE%Fj#V*d5#Q&m_r@*h=Y3@(FoVJ}f_uoj17hh@CPeKchChp+9?j zx$$PyE@B-?u?`WNo)nvI8Jl4l>$Hr`w2XCG#%5W@x-Da~En{;mV{US<(505@DRYcb*L+D|Dzp54xn*pHUaAG8C6!vK zr_9s7YWyaptkP5F8!0qjQp##QrNy>lQpy^Ul3+<5Db-3de$A=P{!vBbOdgF|%h);* zE036FmiCRLQtL&^eWlFGfLBZNu_pL=I;`*$^G1|m)f9}W&9?U zx?WG&WTeo1Nhvpolu7ura|5;zd;TsBITC3>eIpkhkBOXQ;dUaO%a9Y9j94XkaUz||ToW!s&YEzWWu%D=Ao5LmPs`@~SU%GLfCM4knzMG%DdTq&XOfTT7Ze@{hZ`6Nfc|+VESAD{ldw=L{VgZ7Cms9_0hzPFEl+5{T1{L zHf|v*XFNqBowq1atha3f8-_I4k>D{XwG<|7U>enIf?($um`f73szjKm=@e%Jv74~d zK5e3qULQ=6BPNPXv7?5_ERko5XA*=&F2%4UoYz-rp-HSWrQ;lQBHt86>A?UdeH=u* z7^Wg#WD0o?H6VOx7>RbV0brjvtO5E6O|q;}bdqRGQWXYR$npi(q3!FJ^# zSBp+eL};qlJ)fBG#PIn#v5Tg5c6DM8*z21<-FY1Qf00h_bO0O9!A=Mkg4jEI8bW~% zOp(sWlp?DS1R{Lu-qB=)c9MQ|<}>6{53V;gR?$KM&9>EwI-onk1xbEgyT zi3y0k!%-lYiHSs^Xn@xodkqzg{2m7I7tc$dNC(p#jdz&)H}2dO2MeZ4`6U;s&sVQx zu%J`QEjjh{e ztk&Mvu%IFpRb3o=<M zS3E4(D%G`KZFs$UrHuvKDR1Lz-izLw>pUwREY2%cH(j3k{u3*mEZ9X=+x1G{N;eCx zmr9k(jyG~H=iDsmT-m_ldZdDiiyPw=8)Fq4Z{}}W*~nryQLPWZa&Toc3vSVh-O7U7 zq^ictkG=8a)!K&EkzK12T)>DN`XmmmP6sb4=NMvtrB5{H-gy_{%|=%Cddyxs@RvG0akL zc%@@yH;>pOvmS|;uaA|lzqxM1%3dD7Po{3zbF-jvWj~KOAWOy77pGo%V&x!@I3#Ng zew0UyuslQY%1yD#O*acRuY8HekIK~cv8#>o)}dJI(9Oo-o0Yp)4)d2|dMS_bh;f;Q ztp4??l_Naj*JOf@f>*LvzRV+jUHeXFtg`cFLD$OTJpQOGHFjM6wVMr_R-WK7PqHr` z{{F$02_7*i6Id3-Yq!K|x7;kF8!zc8A4_H zyK;ndqJF#rp$h$7B|v$bPjrzNKgmiLsya6HY!_s&oLM?jACWJhCs2QPFJ=B6w zn;!BY)UJnG5$e!GZ3ubwP&-1MdZ+`TEmsgMIS>J>Pt$gk1Bh&BkA2ENudEFG}kj; z7yz7AN=N^h7CZ8rn$P&HmBtIT`8d~e@X&agHemkUlzTi=3r}OT5O(Ncv>c`D?HErp zLE>5C3z}~gBw|iHMD5ay+8zBZt*nukG@tQXD;-H2&m5IE3!GCU9sQTuTkG`W(s++n zs~U;*?j9`%3++t`?K47Ptc6;`#G~-p8=qk-`m8P|NfE=3AD$Qh-Ca_vX z4spn84LJ%qH1kxNa4O9+m8O?Uvr47Oq*9lwG<8&(F)B?ImF9;^(?X@$pwc8z3HDV2 zd6ghN;R-^DgKtN2Dteuu${5khw#`(U1S-M4N+7Qiq^ktrD#5i%;H(l9s|36%jkQXk zsuF~%1ehwpqxuLr1bymGa(0n3NX`&B!{h*(B$c{1kpZfewEGf{i6}N-@k7LEB1f<+ zf~HJ3C&WG%=v?8OI6eciH%AXL{W~NSj2qsNFBqBw!-M^_MBW6a#wTf^|%&d~_u8GHpFm}`iQ9Ks^^ytJZ% zTr7GiW4WaICHHb+=}Vc+cdMY}Rol0>p0&T^TrR77DQCH|<%Y9txw!JBES{*k{f4uG zhuM4fro7>$T#>S*8hPEzEsvbF-*7fAdv?-q({gh^{Wjnm>~;Mn3M%*nf;r1otrRR@ zE>#fBUM_FIPwujosIG_J)`+*as+zAj-kgtj4#zr&<5jyCt9G9s`^(3W!-XnP$}+8- zqH-21N)?|LpR8!2q@~)oM6C8HQ3I{k#g!;1ORH_`AZ2Y{))H5@Q@CQesh581mm9ay zZ=F_~+akAkz2cNgsW|s?Q5mbA_^C!$P$}$X_HtD%1axc8&%!cSaMUg2xIez5P#sXuZ2BS`z4)Mj4ejl>3iAGB4bRE-=+ZQ_a`C%Ez%+Dhb1%Co91=nu5Tx<;0L zIYOjS%A?Iq^^u@9ih?YIte|dkfO1+Uf0+mPp3MCF6BaqZ1Q2Y5*JIHC|EEqDj>`NhIRHB zXNylyO!=@I$eH213F3xXnWrbP0jR3;lsZFjwP3|Lcg?xFL17O8Rg=YS?UNTs>v5QleQ^!k-{9;*-};dx{F8C z?lTt$Vb)2WbTvvZ(>WY5VGcElBq1~y2?rbGgdB_zn?PK+!ue`!+#P3N%OoA9jXP6p zKL~`#32*^mvfWZC*lVzS?!1?KU+n$f#@G74+kd6$TIbcyrHXAgvbMd~^A~-8(f3~8 za!JM6!I$>j&MS`RHOKOr&pLkI))j9XSZo_uM&3g&9(r}pJ)7L#5p#maeaU^!f1|hs z3|mV_yt!|&xsMVZc=5oiElAYT7IP8@?K(Gjqp;zVbje-3T;sXnuDqR<`|^&LcbwgE zt9w(td-r1Z?&Z9~3%TcWFO?$O`t>ol7nL~pYR`=d?+UWzz1;V5-`T!fmGzef-#Bpj zz*`M(cl@y9z0#%1eUy%h+y3(Q7q{QaEqk@$wYH0GZ#v>F!?Bj(Tc}pcP^@KW1-02N zeHTk?{Nc&?roFLEdzU=>7FzbR zOt;LL4#YeM;_D7Bt~;2L=|F7LfhEtug_c9->^JiolQQj%dG^NF?OR;8Ph|RoKz!5g z#Z9|!6_&*d8y5>3FZtg%dHH0#sW;ZtyHwbR)@NhPFZl;2_Qo-A(-0kP= zH}V?!2=%_)d$t#ybKv5Ec;yECU#Q%$thBu0zU+SUv3N)SVn@HZwlkM!;!SZ|sXT_QkXM7qj}8a|>R6?#1WgxgGew zklV4`ZS93hKc_FuXxuE1i z-}%0HL0hb#Enctz{}&22EVuVuJ8<>DpA^Nn?pxft?=O4)y6><0;`^s!`={djPsaA2 zT-rLb)INJ|@Pk6n9h+3x11GQLPKM;(@#)=0f*v+W|Gchz%x-6znV7QdAwr!AAqa<) zUZ%x5FF`mh`|6Ms$M~c3Dh|ohS~58%&dcs%!dJyO&cacKS36HDg2~DIC$UOlX=|C` z4s7#mSVW%=pA5KE-zjBaWUrP5=TAcO(Cvl}Ff`ElNei6J4e;~p{ID0R2S1zQ;^*CQ zEPjo5z5Fn>^2ldqVFklb8mH|8cA^#9Fmw)?^tG5V3qejn zXWSn+?wg;9y5^ufhKx9<;#H2UK7(ncdJXVMSN&)egOLO;VuFfbdpn7Y`4B6W@0?sf zLK6Tb>}+qttw9ilehbhLp=Fvaqgac!y|`^LtLjq28|`njUuq|0(tEx)UeJpF3k9tZ zn7(%O;?Z;V@3>Y--1+jE7tbs>%T_X^>Nn#L(B2RkIfl&qQr zj`%)atG^59A`L7tgU?I1i^?Ij2N_|xinV&LwO?(Iw+_Tw2jZ=}W39XY(y`QfDCTYk z^>N_UhTrYD-P-w9{(=!+ja&%PJk%O+=3O>f2zSnM)i{MunBa5>$3QVt=x89 z>w4&k;X2G&7sV&e&fwe+JB`8*b+e-h>>LMSbJwKCqu^xVJWjpB&<7FCFm}Kv>!hgO zBLsl|e5af=o&I4gho z#=*_H#P+Z1nOwaKgoH+uqZpHpzxem7dZ!_l+pyqlSlv5zZ5EUw z&sr$z_~hDayG}P85^1Q!lt)W54Ma`*ne|cyAwvrafUqz; zHIOVQhBeqJMLMeQbEZpvP#Kj{ja0>I8I-o$s-&RoNjvk;b$-jIXva0_7Y9?1>zHjP zJG*UZ@=O2%*LBfM3h?y(z>xqCtk=Nak7A0cr-F3O5l7-x!Q=6RorD!L)M+%HEA^a* zFOfeH3gBd;UmR^`sxfXz^q?fePm-%&r(~{ivQRQaMj<_d=&?bIhOcu%Sd<}h7-&XB zd_eGY3dMl7&1Z6!7yA~oluKjr#!dLY(71`n+Z`8noZInXUhztrBYW#|ae2JBBUaq8 zRNQ&a28q(TqIh0yEU)%bL%eQ%tZw~VyKdxdxy>1Mpsce|!q)Ko;^Qo-u=-<3&Mrxy zEE|6RKC%wq{=>Y1ZPN8^Svxar?>l9^23UK|5{Jiae!$puQ>)&{Icn2*zLPFh?=SY4MWD}}SdFrJM=|C@_Sl2fI z6tkVf8fi=#$x2C%U{cM|Z@lIik_)|t9QT`*X;spJ$Fy0o(Z*)RkUldE+Xdp-x?z zpo#j2^or&L2)QVwu+y@kz5{Or-7o2)P^Jo|L3n|-s?di-VJ9l^PimeXfn ze)jCMm+Wz+8~+!S?&Y$ocv*X_tbM7>8!v$L@uPwg2J!ant`vxON!jb-?y8s@$|yJ7 zEe53DDUk|0fAO)4L3x_U*BUEsT`F#m=e5W3+RY{Xwr$0MME@;9+k-DO?A#!~Q2^(U zhdPW+#uDpS*()IFc@^7u)*=2jbMBZ7R{(fl@`8z1h`<*iRyKL|$%^iYn zbd~#F)BV3QkSv^@Je$o>=b^a->WBd zAb+G5Ww7Z2S??bC!>-{g1>f2As_nI`i&^o)hK0h0rLN&mBhKbIs@gTfxdx$S# zG_;9zqoiKBmzhlBN3;?JENXw?^sz8ZA&W>GIy7OYs;?rA+~&#}`yA%j{~J9Ofmlw$ zrFo7PsnjWM1#pdF_ zVKudeKYe}Ky7n5j-B^Xt$wbpw&m8l5<;OaUIj*k&hCJq>=7j9~J@m0YEqux(*{?PU zqfPu0UU=y(S$tJ-N==9hT$9|&5SjZh>ez~Xqwdu_8PefRM_tbV!aYvGAI5; zAvh)olS;w{(ucC57xtl4A{GR7$kd6GP=uW+a_cfr z3b_ddY-51BQcCOPnU~L;JwxKkedqVZ3p*DIJGs#E?9fNKg}16|FE#$o<0QMJ)sPE3 zSI+=K7HkH%Q2O(d2?1KltxXym`r3mM#G_G9+KllZWDqWprbKgs&|u1Axpq;#*mFNg zxBi`ywP&VP>Cilo>yz86b0yk$C9|fK1tqwuty&!{O0up4dV_fClqU&8FpOgw6H7f; z)0kMS^Q-E~@RPO1EbkZU8?3m0ZSdzKWa5lOiWa7H8MDo~DK?qe6lXT1I6K8A2e&N* zF^Z*O7Gl?7#?Qr17!YnT?;#ip4MQ-I%B#YJ1B}Txq-LR;n!!omH!$-i2KL9a$43tz*bd-gR=xNSq6c_#A` zwR;D=tTHt;lV3;a&r2VJ!=qi*r_Mk1gT{E>)>z%vcow!Yw()KA){Cw2^43^+>zhN@ z_Fvr}&)u?^y9Me17tWkN6EA6wl{7EpHPfEw)}LpWylN*(fHr4`VvR%b?BT`iVPX}x zpWhxYXowXwEVvtP=NHHG>tgwJXLo&+oBy3Nm+If}T=u;A*tN&6J{~XGx>&MxxupEU z>GP)-@)VR?y6wfteUJui!~FjXX<*V?Oz?j)lXinKX$QqR^T2saVh$h831ebtXU^Hg z&aYjn(zWvmoH;GeLYuti0pR z`Dx4)LwAlGcU1R)Y8)P;n?INd@Ni%k6*Qcsu8ZIq{MmizPb_T2W0rMmBeWCd-tDgzoS}s>6bo>ALB~YOH^#;@&2^<@NeTjzvn)Fnl(YSmDG@_hD z2GHRg8LY?Bt5W=5=FW_zq|dpxUmF{{@EGwreR_BR99X0Oi1 zb9)zadv%!c#7aC1d88ha*9XVlcOQf{$UcWjuG1`t5YJJacCq4#iCfmzTDE@fG&)tA zGWY9MW|ob1hUWdjFf2NQu1#iJsYbNja<2i-gXC?k+J@Uz8Rk;Rjy*hfB23L2>(1U8 z-|R8JuU}WU^RmyI_)n35j)g^tx_DlCXYeQc-re`39M+3hZHeLuZIVwMJ3XP(zI=r( z)|x(`(ZmDPzEmJcIM{a+`ZCFad+4LQwSv8Cjpeq!xiQ|hJJz=Q!#3gqp*jYor_4R_ zuVzpOKF^?*1 z7?62huj{H!{XbBRwV-pHQl<6@85}~isaGiYYt$=rAcaXdXj9A#olCiU7My$jNdqM{ zg4DRyL^KZplsLGZjXmj*AtAN|&<0J)#qCtFj~X&Afnx&Hp#sO)<6HJEZrR8BPyMen zU@34)CZ^_3o0}N&l{0d<9?>-mI*1>>u9i$|6yYb7`1Q^gg zt!Vm$3SChH{gYYJ-%}uKn4RmHoD_yzX$668Q!c!4vCBqC8Ur(}xNHR(uoGVA3k9L> z74X+_*3PT!37?|vR<;{PhN-y@A>=So=m@${8=>REH&}6z13p6hS~MJ1W_;>2KnWWI z#sn){jsC?aSB<9)r6;m9I6+|AG`~ni(3oHQVr~1Da`!Jd_phE`Uo^7m|6BtnOn#^I zM(L{FILu9huj-5pQGl^GC?Zr-r;s9>(J6alZF`q;_boX2q**2LWf>#z zCpGa|41Rz51@y6rf73T{QLK1OZ)qt?c{bQI691t@j~0>weMXol$=!kQ2u{#o8qBUK zVS{=CnU;n+1ne`@DJUANReZaqvBbv;B#XvZX%S>tI+0|%WIPa>56~H)Rb-D^L?ddT z+wTdA7ysw_!82zV{O;&E0pG95Cz`q%pmVK3kcBq(D9R)+GGcyLpq0sNFiP2YK9D&g- zu+2X{{Oz#rOg2^T+nczBV;(fv>XlM0B)CbXp&cx}iLasT|B5EL<*H~1r;}I3?f(=3 z(j%%51@c_{YVW##I8uLs)OOi1VY^2xaeS z-#4P^(haa;p{p6?XGt~fP0`iHYvTZ^R?ey<=F_8-Yt%(QHaf`^oGmMyazd1D7ud`7 zv3VyDqY~nGk@0iCVbc5KO12RkY+1ECjoeK6Tkm5iXYjimi7~?V7LH%x9sphj8xZ1| z7;za4f#rhUI9Daa?p6SoYG6!)pktwmJA+|g1v$CAnudifUEwt4#s5Gvl(r5bd8lMe zz?urD4-H>uUSc?huF+wg%T#hTHg8s(dC;zuVt4LDU|5>2+R=9L*~Xv6k+n%-QjFBo z*%06pqFbP=&Q5F#yt zhpl_G!w&4=g5X!W30!DXSN#^8grU664@RU;Hnkgopr4FSE%^qG3HIIrO&q*;b;JYIJ?i3YBJO* z)PvcBB;$4eB%GfPY?%XdN&$2onRkkedZ>)0jVk)pJ7F5`jY`4V`%;! zp0##v=F$v%ps)@VO=pwx_VCJNpglBw7G{g|U zuZ`13KqWBwkB`tT@3?o(rQf^8b-lF1Q^x11K*Il%us%F3L z4R)K$<@K_w+&XZZ^3=&~+myM}Wbd6{cLrDuMO&yYxPOVAD8q?ywA7<0^$J}E7TKmW z?+MJzgq7CTQ(<++-`ZN+%=3~C4L%8)19vTgpwJ2+&1zN^=*O{ReJ!R7>i7ro+Wdus z{L@3Me|DlbT5y{LPBQduYKJ0TXgYeLZ=-R67B2!HH5b9^=5h_F(-D5V`xGgiPZFhQ zzPU*|JFnFbLxz*@TnCZ5mlO(#aPTUoJ~VF1qwYTrf;@bRvM@4()rO4=bsQsRncnXt zU7rULxb`Isgd(2){(gQJG#$>TvG77alF_tuu`h0JP?9=i7YZ`xHT#uWrBCr9cLAJ+q_H7uZgbjU^kf#z3>cL2aw9?6tiO^79x2lqfGEvLb^}j=? za{>hbb@ob7Hk^HN_N)!+Mau<6XZQYXR?dgjwM+S1E>69=?gzjAmK<-~e4+bX_r
ua8ZM2!arE-hw>B*0Z9eO`mCH1nzy0VEJ4yBF-3+NO<|d9zCmzrGA;VvC~06 z#0jB)j=wT?V#ju7hOJVQV)jh(#UqIhN~Mi~HnbyXEH9QUGkeO+5BU|}aTr-5y-DoC zskosMv=byhQ1h_CK(HrUJ&u0hM3RquDFm{cCb&Ra5k^7bAP;fS>YpHWqF^uCI}&vs zr2_-7*hGEH&bKk@IhCYNFr2}Oby09K&PaMy21+188@ud0gIhl#u$0wwrRQ4jwce|} zw+e8W=e603vvfRQDAq9)FBo1d82+%NV#O(WIHr$tTV_O`+OX|p^e+ke_pO#U4*+&`1aO~QtRwO%G{_=oV^ z4;yN;QP{|{WJ;_s6~hoZZp25#vlh;QT}1g&d1HDf?1Ao-?W=GS-xk5FDaM~uoOSya zNX5t;T^~sd1d#y(u9OzCKEt*{7*OH1-@|k}6CjvflIfb%7kOcVM}u@tesz#Y0Dr_w zCwR4KFzI5KK2LHR3#UOw_-0@z&CjgMkxq7&!?1@bK7F>>R$%X900UC=?t|oP;0=) zUKq$4XpygH%L5J0>p8jPuaXCv?ANO^$loNxwT8A2ko%#a?LN{G6SUnQJtaX1a@p?cOWeijYM7W?}CbjDD zlOlv(y$YnrxLSA>HYV&0Gmx{E_R$!Z9rfWG*o#qdyb6q=E8*1Ib4Xl3NG(a49*l0u z(`un5x0ov~Ee&oO)HTP9xP3Hhj%u@}kPhH8QP6VotZ8YFw``BKY+q{Wi`VsGE^Uvw zYxKFaX|Zh6JNAXL-S4Hx%67*Kb}zVhlU2IXzp3l{uwb`9${Pnf^4s!2qy3#G_^wyW z1I_m9H5ue@mf>1YlN>-2pC+Jd)3zeSxqt^Lu4~l0Hbq7uabmf_J2G2Rx=aX-7Z1{= ziHTt`3Dy)R>aroNoTPK`HZ$_ZvS1-d^ro$%7m{)@bY6q$mCk4dyRR0r-9;yJlI9ja zSp?f8<8H;pf_Ji#c+(ufv1 zlSrZf7=D86M$wiMW)MaQy%^cPGaMDid%1{b?;wl-iKAC*B-qr$_LZ5CmVJZS02Evb z&1KysDxM&CmLXWIj-S!3ncdJ~G)s9iV^gP&j%pjDx61Z2A%*SK5Wc$S{K; zCS;5AVJiO`QIIe|$MPybYLFkLmcG{y9SxWrPAz-2#U|iCC}~hqRzKKX2q;JeXoR(3 zh`X330Z^IxE~_anEyc$o zG;`Ru0L4(zpc|e540HfFJg@S~QYW!4f(-T~6M&i<1R*%ynE zKuTl>fTh_n9Bw6mD9(i2JTRQm6#6nPp}&b@7~wfAOYZVZ#W%8= zZ#8g3JM;?t(acr%i(##)0{o!DkFe%m>I+^$4mf! z0=zRMPXKBcn@a?_INm=Sgn|jouL1J?WHpbS(M2He%v4~G<9v>5*l~U`y5x0zPOcqy zEW;|fLHIHZ>RQEH3HZle9}eOtGxMo5FE8=4lDs?@scnA2wlPJ*z@=qZzKkhWo^yL$5vCY^%W5#i%1LZin1d;n zonuHE5Ti>P8cEVn>-is)zuZ4CU2a{?xNr z*7MfbLfQUz1{cfrzc(B!+aE93zu?}__!)5jg$LwMneNRCn+9b09eIcS-3)kCvgwKo zwu3W}Qzz_nlXfC2a)RAUj_tQVB9jN8d7N<0`63alWd4K`mO?@k^XkkLYQ`QC1ko%bF2LBRsO+uz5AenQGyH=O^ zpndewG*b$FugLfu#0dHwzS`dTT$Zu7Zr!#ce>5uNjRoJ-BZM9!xyCm%-;wDfFOJ$a z3ob8p=LYsL^gX0edx>;dK5RQ|-(Xv9*XhZ$E#S5N_&TiD(1}#`jOy(;L3JzC;*H;c zpG{r+ICf*kZ5!;P=_S&PbIv{PuGMWv&1H{gAIGRvN?vI$8|46YQ=g+mh_J_M|7JwL zBT7pp(MVSsA{vG zrhdpB&((7IQFoxc+|M(nwAYv+4ao<5So`ncZvuaN@t4c^=FIu$!Th^+fdUmqHx5G} zJ`TbkhJtW(jM2|Tf^#;BRIHWo9BGug2*GiKPh`@4>g4?xSdmVkpq4;F6cq>lpnzRyv&`Ds&E1p#*X*1lc zo3M-O&ar&384c$7iCs1|7RJS*P^u;Bhooq%zCm0~`i#;AEfnHe=po)?78`Xz@Ms-V zC+MgED5n_HI942l|*-KwCvA1N52nvYD=-6e% zs|<0jFh$itEE{VoDlpAfsG8`tCq09Dg>?sGD={P+R490|{!B^rIFIdqLJ_|8Po$<` zJ;IZ1VE$Dp_{$!>jE%`De5ZV1qeL?}u(Yga$|tPnIUDtq7+I6PglRze7l^r^3rBE+ z5Fo83*seU~VH&&W*<%6GRK51vz|i_lP}u_eXYE+|XXa-^9*rHv85&YnvF`YkK3oqz zOw>aFdN+7Hlxhq%@A-eMKJ98(x>>g-HPz6Y(Kyv)1x=kU@$$w+9`AbM@$M(YIC)4# zkM${2&uQVxQPXX^#cy*q)8!FBPb=VnTDNxLz9URZ33bXtc=dSQ`!`% zIV3%hROkpmph>3m&5@L$&_o4lC;<~$gE@SFlYBuCdr;!{fvq45?yPN`i~_i8%~}JA z-LDG?s5LcANXSJ0=v~L{H9@X8rp^Z@sc(%$8m1z}TrhJ!g|3H~F*Kw!>EBLnlF<T!^g2 zs2n>@*4|8Lh)o-9@0Ed>b2*h$zClF@b&XH6}!$xdAF&%ztf>$|&MPJX& zgpnyLu3xOiT!4l}8PY)H)a#{Z*4C{-yiFFMicvB#0n~L6Cr>Is4ot)HStUBNZBl5T zfEU^&P6(U2_Q;7qh_{EZH?XQtIVnt6YK;>eLhmv8h}%#?^K>7AUda}A%7KI57!(jm zV2v(>;ja*|6D<&EpFV!G^XT1UM*qNeC;Bspttkuw6ZZLVG!VTzHHA^ed1X3~Pt6qT zi}<=3N--qZSp%jLVnWP*h{VG{NkN*KJUi{bVG_43>LO?le;`U0gv}M^!c-2Hq!mk=w1gK4 zkccFq9&1-74a?V%`jh2hAyB3o>kJ=;Jvt9EiH$oW$p8S!1Fs*7{Ut^otfhWlR&Vbb zBaiPL5JzjR$6qgi#PXkY{Ebt)dNF%xv0z|@0n@sJ4wf8;-~nfWF?FFAJrl;|9?^L} zr2aDi4f0`iHP#Z~OY_!`-fLJ@K(o>&DgoAN;AHpv=yf3|JEh`NpiFQl&lf!G8gq|P zr#ZgnfZ{YApbnFQ)9_@{mylva!G_kb4|N3elN1JP98D82Ap}(P2&kkLRP}`-bY=ju z7F=$40$Q(3J_BhbdTNT9*JZrI`r!en2@^2_HE>>vlC%vDd!uAdgyQs^_`cQ{ukt9> zh#8O9^OMLY8X)CjChClMM}%Bsk1ui}2^X5@27R3sp~*fpthi=R5z?ch#t`QR4CMFN z=mD)K7-y5_J*C5$y#mfMWtiqRP|5 zJgaH&`zhY@jHI^Bh)z~TGr^>nO$n_SkS>ow7*iUpAa>RdZS3Tn!UM}Oa~0MlVmFD1 z&O0mv=Q~Y;&$UWp%dMDht5?|vegxcxY4zve z<2sa(pvf@}QIC3poFF-Lvj9mh^@k)|%xrfrJY4bzv)#~MPBz;;D(%znwGw8!A&XjV zrdw9`q8vz3e~O=k{7jM&!#}4uMaU~SQ)9BKr5W#5AWDnSWkV!bU&!f;we?=|y%agu z{p#SkzIbl!LT>F+Tdy$4U0x}lX~*6QTlU#8NunH#V79kvG!GXWmjz#l91_FAOVgkZWjs-E&4w&Ng@EDj>! zlBTcZjmzzHh*>;Vsv$Ung!5S9@_D8Nf7t$@JYJ+boF2f5E(jZSwUWc>t7-+%pnZWn zX&7hM=h6rL~6(i1$}PLov;&)?T!-21l_p5MtjzAJBm5g0hb#54Eg*I_&;o_Ez(y@dZKo&~u<&r@l5Ki%1WOKzJGe5`D)d1j11lwXtO&oZ z*o{MUM2W#U8AzmaDPO{=afwWtkGaIJD{%c2ywFIZevO%BPSi+~>mJJa#q-iK_98qNJ+FB$dgJ9?vGT5S8OsHQzukAM zq%vOOSuF8fsg0MkFP5}_xZ{zfnt^w}@{a9_?R9rt*>FSI@a|V)H3JK!1IxImcHsN_ z;*}k-%8rE!??Ry$W(*b!>Ti|QT(V#I+HZgDKQwi`I{aZx{iWeI_Fvu~uj#r`)3w~< zy_S77`_k7Jl}&eGRo;6iM=Gj-B(>T5>hN++<7;2N_|+@^4{Fx0PMD5_w+qS^3mX4J zo#)l`4=ZXfHNDpVz5eBf);A7bK6q)zV&!@iS>K916l`8+N`+0I-mQ?7o&3(y>**DP zrFM@kVJB6$L^?mnn#d5Hn1mi^5(6Zpdsi?Ry$(VTfmt?X5a!QeT|Tb?zJEQR$W{mf zKnVFZ(1Zg(Xn6<38h}aDhyk22VGi5Z_yV0@T%+{Ej+9B1TBaOmxo_=(l2Ch@YQIKJ zX$qy5n#T7Yo|M9Ahi%8PZV;lgs{P?KGvWe@vRk3glQJO%crpx9I zzg;CgkGd88!SsTC_{H3sTLrjA znGUxu*EPlKdT!M95X{zGN`E8ka#p;e{YFLma=quf2MJ_1Viv3`!S$K-&05msirUw< zU)+AB;e!gVDIJu9OK#^DE#}rPSJ%a>yKYo>ox>51s#lL+_}X{Ah6(iDY!q12O#bL6ZU%H%pqY$TyiOV0O~llgIU> z|Acyx4pAivwc9MrL~x?i93r-V7nt2+zNwRHCgr4!C0y@KB(C~DQpDTj5VBP143;_u zUm{oAA`v13ej?^4c#m2{DH%hCU!z>K12R7vR7l{9FY^Q$`25-fsIv=|8|28Yv@;;rC zil67higNp6e*2ZtKPZZKKf2ic=)%a@Qukv^`QvAI-70Ik6n*2_%g?@b_&xi3{=b;M zKD|_SC|))eD;ry`Y<}+<9E2}_OvY6l<$k)lqtH)A6|?;^-4~r3%jvr^a3%WIIB27# zoW7;3zW3JM$l7@;zjC2!|5E;e1@{4LHABUA>x-}krhJlClwGkR!MWp=O6tEII=g#0 zxA?cdzFb}jl+X1QZsnE5^Xg)Gb@9CQvAp%HQJz?tXSt&0waphd1I+_2*KN2b+1KUY zv!#=|LOQ7{q-W>AGz2cnDr$@sH7*s=z0)7$Hlu4kv6p7H-I2)Yk@Bm)U3E77qpWqy zWi_+`uD7vibrVVbA^+~?_(ZNuyg4W=jwjsqKj%Y1r2^NCu9Y5VrP|>FQx|xiP^y&N-%*b z9}1rW?>`-A@tCfV6A6W^S~qv2>1kLN0O>X3YpoA>h@v3UPdQB_V@dZt1LY*4#hxg# zEZwB>a{>Kpvi+OLq$B9Kq08sjO}b%ALqr7$!5>Brh6{2Gt)Kkj3+?CI1yIOaUSgCGh$(x`crjyQYwzlF*dOx?^SCDc}Vo8b_`kd1stfjP-;~ zkFo{hC|fX&$|e{qJPOQNi_%}%K2S?QR^|M0DSX$9<$)Ue^^y$o*T`^nc%tDUG{oTP zeRv)Uo`!z)@T95vtO-k1IxMA~`IhUk5PJKIfhJtC{>#D>KaZBkPAbZPqE%;t9W0?i zr?B8}5bWT@V5-uYetKp@tZaiN?7(Pq`6ePVHW7`n$qX@@bbzTLfT?i)xOkvOzFr8} zv0pFBAU}?w)vN(9y+B6~12CNZrb*B4R3#*q^61=^b~67ntc`S19ao81>a$th6nP}i zN?@_}S~{0DmZsIuXvvtR7-LKd5s$$hq#*h?!08$X; z%^sux$CdgZg-eS=3SIY0i<5ARW=IzdAqBX~)CVbaT{)yMiK(=dNI`4^djv}VOH9M> zD$-H?>ucRtE9jA9I8fSg9Cl<*ux(!KeF^MOJ23njO|~Y`Di2XO?f=rIVZ{a3OO@yG?dRd5QrBz%GVf-A0dfhAuHt~(3fcn$B}}ef3jP~Z|?*(y<)d# z7A&vd^mgaloo{u%T4=t)bD_ZdUi9oBot}4JbjM4(W2N11HT-1vySo=kyO&A_stLMLpJKx>QRZ6_d8>{l3wcl`-FBg?w`11KL2hfWmCEjjudW5J8LRH17Zv6gN_|}Ei77p2?H@R+?o@$t|1f{>7q~_Qci?6gBv5@D ze*ar!9Bq4{Xs}X#b={y#`uzepKavO2?LY2>?|O$k=(Jz=+R48@1F5cW+vPt#>Y z{nW`^SJq&e?WaXHiZ7MPUq=3<70ZUtV8wFY+X;`3fH0h?kBOX?oXd6xPW9om^$VkO7=Z&Cnjg6bQTT{YNz8`8D_m4Ga1InigQgh81uhKA!rRZZg&J ziIK+iR-dcJ+YP$SD*c<_&l!JX@g*yPE?L}q%sy}PLRvK354neJpU8Qa99?9?L}pBa z=1J#NETJ@ZDY5!zr}pBBzO{8%z5D(84EX;vEq|9c{ZH#1 z@F(FZTIkuN{?8zJ9yqD{e)W^uAZM&mi1oxCUcp3#X%g${Jp41fO*0`UOCraFek=$a zjoR0ckJ!KIgz(w2G^>fM=Y`0cAubCdNKXTl!O~ceL4>&UbU+9|yt=adsJ_#5LLP^K z!5SeNXJjr0KlQOvsJBvyKdNXsN9r`6@yj+BELx+9?4Oj(B6BTvq|qor^J(j&QFDgg z?sD`-hzz70`i4Is<$(C;nqI3O7?n??Y1giMY(6rBuNy5ia2M^$=z0>+;Uk?n=!ft- z*4c?nO3Ms!h!eL^t6^$^APqwSQfBa}xTzf*zLwe=haps&g3<`-cubPMIK`AXf^K^jhd*)2|BEtMG+d<3G?;vB! z^tAIr`HU6f9R%6!*-g5X{ohfc+7+PQ3EuJvZu)Toz4$h*5`_SeB&(r6a9!z%xAj8i zZM915V5>weTP5KBO+m?R++)Zt=!_S5ZWMUV?piLddadnyZRgU-2G;S{PF_4ox1Jr0 zwH=HX9J*0(=ypjJk;d5c#Q%kob_nhsef!b39%Xwhow4f9S8X5F)ZMCUU+Cyxs@t(p zxnsGe?t3nf=4Jcj<@)9~dN23J>mP~LKN7DWjMWd`QX1n**P_z(R^i(aJ}#FOFX(2JsekIdv4zULSPBvM z_Yq>L{&e#|$xwy;&nilXHe@6bio_VRHRTWBS!+!(<1GIm!)-RJ4LOiDuy6*Vi%MnB zk%2XEFYt|FYSoW}QoxsKz(lJx9Z(kDF>F{5TtQfbX#wDu{2+Q>NG5tXt49-uv3lCY zcu-P>LMKoNGCr2Ewb~DYbzdg`D-|RFXq0C^NLF)|0Q191<|7 z#L=m=+^Q|)5I?2T5~x;_vy~h$6Oy`}oIT`hCT9aV0A)#~PF8!#878NloI!G`$k|2C z5ILZ>B!(YVP!*C&i=4WVoJ(*#X@?GZdK1ox3A)vMVj|(1n1I=f`5E%NCnmmvi@n5~ z+=&UM3`Yx2D2&$`69fVi6Y3n5_8mAls41>;SI<%0_bDzNR6Y(WsIODZRdU`W=NdWx zmK;W{|Ac)1GdVGG{+gTxa^mFt7jpiVoL`Xh|B#agw5q0)<02=Q99oOjGIH3iY%Td3 z$!Q^{jT|pI-Q;W}X9qdE$vH&Mm&iFn4w=hQC&?j7m+hyjbf2+$j-2n1^CPMTos?&M zf=Y`4pCd&R6PT*d)Hd$BjG)8TO-xM9V3Oi+S9A*YdnYDf8vfUji|yJx!Y3JsI7uak z80AQn9b53P^~;_P@~~ISjUI}xYg*p0 zdAVl`L@mqhUCSNa!0Y7S+DFL{+`N5x`woiP(!1RI2o-`Xo3_E4A(enM+xnOH9U@Qn zMi^h9(vTNLA#qm^j4Gn=+bF+#<0rH7!8AwXU5U9*+P6AF^2*LMDLv;-n$uB<)gV1* zg?uZ8EY9sH7jf|6ih^|4Hy{6v$M2*$96c1jZiRd+Icwzq=EQGI+)1ow5Yj&2lj8IC%U4!)Iq z@pY@bpFb|!p$!mxE7j~98TtUhC%$Xf2Nyokj!cJ3)DJ!Z7%m+!;rm!=0{o81<&OHh zTbz#myM>u>inB2WcdOeSuDeI&JV(ymhD`L>o#I_KhhJWTyDvS{(YI2ZL4c_PQpdvWSDg& zT%17>%urI@-0H~EN$K`LF{!0;Bc0mlv@<&ovOzQfXRP{5d?q^cd&!m>0NtHj7ia(Mz dFG-s}lBzy-jTFer*{5D@zvBF_68Txf|6i874<7&k literal 0 HcmV?d00001 diff --git a/module/scripts/lib/telethon/tl/custom/__pycache__/messagebutton.cpython-314.pyc b/module/scripts/lib/telethon/tl/custom/__pycache__/messagebutton.cpython-314.pyc new file mode 100644 index 0000000000000000000000000000000000000000..aae4966304db19645b1e282c848606ae57e843da GIT binary patch literal 9809 zcmds7Yit`wexKz_)RL5JQP#txrIqE#qAZh&AF)qTtcPVwvSlBSR_seEhg^{(X=8Gi z*qt~!^tzOICyw2|; z9VF##CMn+$mo>H?NO?MmKrZ`zBnR_FNjup?ruIl=z`ch^T&(6d*fQoaz4~I2YG&`S?u$q~DQUIS&%rm`Q(z{>jfS}6NAm3b)FY$^v* zp;T?kpW;*ggew(jCV5w$(}Ss?9=b^Mx|F|@#Oln@IjU*mJRL3Rx+06sz&sz13$miq z{(hbpAZ6?(@mbbJ69#pfr}}~-$Mt+XTher;5HDE6#4FmK$gm+ql?sxq>7tyanxM=H z`huhhDB!p-IVUVCCE=PV>w>NbJv})(C&{#@M-T;-7W0d2go3DEDHVk|MHOg#J}zif z&PnpTV9%@dLfbj2;s_aw7uKyBsTLb?tJR4NBaDiiQ$Z@Is7d)nLCgZjnxH8KDrA*HL6M7fa@AiJYj5iIPUu zL|+1w5f|qd6LV6YYKizfEl9GI03(#x50>*=)FWG@j*zHz6%4G7gD@VHJ^ zo7&zCtTr~kS-ZNc^-X@YdC!|cc>HDXAxbQ}E?p>K9;6z4xEnIp$wiWccV$BPzB1C+r^Vi+|R zUK-NYrS5{Kx&t0&<5tG4nV!Oy*y}o33z8j;fAzgLzPIF$s4Y-cozpmLT%FV8y0t+g zJ2CFt(9U)GiY~w+#RbNEkh8|(F_((FlIg)FD#~&*^?UGB!&d6C;X!QI2oIp$hqS-; z#%o4>d%3=S$=|-60}r%hy7~d@GPABn;H3)PNo-${X4cnW(qHijiq0Ol-~N>A!2Rv< zM^CbydF!~Hut9+YU9S$J1T(XX;C(UzE+poQnUC4*Tc*LMpA_@?Sy(L@+jnU3^RQ@n zNpnoa5jM0{(I=QKj~c@C;8fc*(utiaG=dEeLA~}YJg$>3eI(d)Blp&on^%mcu5we? zhhC%mbh-Pq5g1<%jIV~9-x|0%V1zr%;Z7smT@H6I1-tLNNpOPuLsQ)ls_x|yRTqT} zn1ANcn!9jK0&iz2V~h4w0ffrbrh-usOKZ_et!?gZdfAMXsxyNU+lkWbtGRpYs@nYR zIL!0Aba|%SeZ~l!T@IYB&}FO~jx7aasLQE8I9(n=U6$1RH)--YHUG^D3|A@;dYNt* zy;5sxFDS#@xm8)v?!-6fXr$adVgyE)1EUo>+FuUuUkdI=9i6mvbY8`yGHm8WDyL!S zEII~E=o-=4{RwV!uYil%3rF_r?kO)1N2Dj@x1nM(C~n&>K4$#7>6FcPV0)h#-0oOPHaWSCcM>ji zmC3oU9I>~%O3sSj8QgDKN=BXB#=C=Dy5LY+bZ?p;4@?y)iN2{KJyI)sD;h%a5xT0f z5hwRm>FrLrkJ)XDz6g$1MS>T7kCMa_>$rP6O^)Q&j|BP0(rq=_9@suQaEOkfB0W;# zk9o*R;t8)Jr995c_+OJ=U&WMA+$KGhNzbz`%`;d|d1vt4!BWyOcc%j0--2K6D7Kt= z#;H!??pBdHejoemujmX#M>3sHIL;H>;b0ZoY~8O*?mJ8#8{VXv#XaF_P5~a`Sk3>- zdc@taB5kv%e`OZMIdYi~GP#U6-BNx>s|(ykF6BG^T|(xYSsUA*g4g0^MxFNC#(#y4 zPpTd5ld!*4CdYiYdFh%>SC7+`y$Au@wW-U&&Ip%|n&iaxR_C~B4!*#usZ0YK5`bL~ zLAg{dw1l0gGF58{I2XU}c{%Vu)pqP}SB}K|eYG8%bEH@)g(qGaRc`0bT#g0g{({NR z<&?KsL}0@rHk}DvGbx9^nQ13?*tKnR@N%FX*ELv=>KmJMdeP~#@$Mj-zJuLWCfJ$Z zP6dzIE8C%}=z3!LuT#BU{&*R!A*Vc3f6kr)Jl#`&Cy<#ghdO|D;UH@<;NWZ){)6}BF*+xf@_I&q+t&g^DSDx8!9rA_GM|TOctAXLsFx9T zp@@I#>&WcmAhG~-stf?4h;*)#Bm?}3$e@M&Ro(|SZu0A-F$(am-OEe5L!7Xeu}*V) z*9m0E06^r<5F@g7jl$jY_a|cWtukXvwHJlJvpFsRKAWY3DwpTar=1}rcHA__K&RV?Y}XigGODll~xEJ<5nRDoV$)-B8C5wabp%9H}K zgmBSj2{R)QdA(H3V;p3i{&}iA+nrbBc}XwjXs-Za29}FuGI8NNgkUU{IAAO+dRowU z;1hffxB!ePEYOlFX}XjJTvritRHrHc*)U6?sA!rr3lrvTiXcV;@&kw*=7W$018=X2 z`4R3+UjbT3#KkoCfq+eUe8i8sdy@(7NeCD~KJe3bXCpSi<&B z!C-bXh%vCVy-lcaa3m@s)#_e+;62o=frX@$WZi5Sr+U&3UD|HItSMfD-b&64K_~=F zpF+HaX0MojdqmBQK(B2c-0rR-0Ai^MF=-X|U$ef#7pr*xRq-UF;_cCNYm1s$V?|Ij z^)Oa>(5YGQQe)-rGmc_!+Teb(12g%HXu2%o3ghak>6@oINW)Vs%b2#f7 zKKzhyyNA9aT;tFh;X*@Sgqq*>+z#9d7%ko9mhKOqGI}P;JrhP~ayc})THm}}-?f!vfwSd-v&KNOJdj*%?O5}Y@Cf&ixWjdS zc5=<{4nPw%AUB<9ADZI2fV+orIOOAa?g|zI=37;XEXnF_?BlaPK5Gn4l?SJc!BlxL1>)F03Ddgv*TG|V{a3`* zSieSGq57(mohRDr;p))nz3p-zEBA~Up;ODDQ}=$S1c7cQE*BG*@A1WTy4-Wx2#qg? z#y=0Y-D?t-I=;Ko^!(C}=YL(l`|aG_`n~VI^ikv|S)*%oxodPa(tdmJ)}RrImm~3& zNT0E*?}qDMTc^=>bh+*5>cQi8#_k>*ey8ERmcMWLrz@-N9k*Y<^}5l1XsP|sN3PX2 z;dcI3-e~JvYU_J{c6E33%UW-H!;QdN9ogMxnQ5Zjn)t}`BmN)wJ7-r~CyemKQgEV* zST>aWc{s|brYlWzOFQPisBe8c_AdQ!`kl*0{o&>M!>iG!Zr0xL+>3S?(Id;zBmelE zadcw&=)|WN?jD_4iC(@@`)g;7Y?^#>rS-HCKD`t?eSasZJI_71%)#P#;3v)B;U4TD zEs3x1g?qk)wGuU(BWrGW|Mr0oEak!O>fh=})6@5Nkj6>wegkP7=l)AugH`;;BS)tD zxt~76O%u;gkB#&~;b)!PwA=HueIp4deA3uBT}wXc=B9m~PhxJ&_l`#3!>2Xebj0(i ze+cqF_n!>I>$7O%bO-sYkDG4ueD<^(^G61u{-2*7@xtpDFv(%hFOKy?{$CC?!gHzS z5I(=fO+VvV>c^4F{Ui8VfvV%4mHryY-*t2N^l>ov-P*u(i~DY)8*7?5%(r+j-^Rgg zcRK>peeS!x>}!I{J?6GNNh_>6h2-GS{}cni076!d>F*ZK3YdJLK4#tR{HK8+g{ zgxw(@Ohf2M%-dh;(`mLv;}u3$;B!qnol~;uw2JUF{9i|f{YBLmSp?eJ9@+5%p;ET% zs4rlNFddcAF6+`hW|F0VKG&<(jY%-MA=uVB|fZq3iyxxF#F4svU} zUDvZ-*Rx*Ny;i z>P#P;#uQak+2+o6>hL+H0yvhQhBHNu?Yt^NNTy5C*mxfhYitbpoBlydWhYb#iYO&* z7#^@bIPP=O{wvb?D{^qn<>qRC>Dl$rN4UT*N$pp@^Db^2TrPWky%r+=2&^ow_TM~y Y>{o?G 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 W?`-tneLJM7$CFKo>O5)%>i+;d+4X7w literal 0 HcmV?d00001 diff --git a/module/scripts/lib/telethon/tl/custom/__pycache__/qrlogin.cpython-314.pyc b/module/scripts/lib/telethon/tl/custom/__pycache__/qrlogin.cpython-314.pyc new file mode 100644 index 0000000000000000000000000000000000000000..bc927ff67a6c9ab189430775af0ec2896ed7c505 GIT binary patch literal 7209 zcmbtZU2GiJb-uHITn=|hilj)&rph5L(pqY{E5i~kQ$IEZu@)U#UacrPmT4`AyF+rE z*_rjstSB-FAk+my2SFS)53Y&=w%P)=+5jnv0?k8EG_Vm{PvM#lRvo86iW&+06v|8? zyHEYjof$5*q~bKa;NH3aKj)l#&UX$EHOEDU^y%&2zEW&u?7yhur;u0JJcq(Hc9~^` z31+)r!4`NuU~b(0L_K0hdf8sq$FiX=mJJ^XxU1O>S>X|u zhFwGtpXm>4|4I^(o;w@oEy}k(VYq+nk(d&!yzQ+6d#B4KP zvx)b`s818@rfl4fO*1>56>+l`r{R2d(pXUSl~wV4R??^_srtNO7G=6%N)(?nEL$?> zC3{hkY_+H)rDdZmEvcF&l}yFL-Feq++0K(ww5|3zD^HEa^!pFJrOUC0Vs+Q)1pH9uXjnAL`M~pP+D!z0j&tD^D{XlNihR z%=f$S%`|MQc*SX!+1bc4(VdMC7|F%&{?XHcBi?zZnI7@`vJADY6J*&V?3QpY5p^1J zc})eyOgga>lFRm@(=~pzWSBPR!j$n1Mb9d)mKDo(B63O1sRbuW-$mJ4bfP)a>kFe~ zl@dWGWGULb6LFVyn$!i|FqHzPSr(mZaB$Emrwg(zr-=>e!|A+HOvww&>3OiiN~ac- zqN=NDS+~^%Q@*0w%foW1l(zDwZ08r#(`SZH%lU7VOX;FfC~FFJs3qG>C$WL*I@6YVW?)(D%em ztikdEa}I@m){8B`%9ZBi_ho@Iqg~T33Z@!O5^l?A#hn zI-!A0HXRMBLL#uDiq9yz0&*!N`?|6Oxd#^|68MYRNeOSsYxmwoQUWXH%bKLRkBZ)k z2>wdS)shP4;cZ|Jy%T8(t!k0PLn)-hbGL#fiJ;j+84>Z8VD7=kqEio=9mt)ispL%s zr>T+MZ|S6=eaJooRW?M{BfWLv#))@BKZ@UquRlCe-8(`wt?V87(u!f!pY^7No5FDD zZ(?bI)7YmXQ3AAKn-a1U0#W#K;MkutRzPbHJ1;B>S>b~45=(e}dr_UHyw| z>>i7TrMr|}FY}Mtg^v3Kgf4+&#x_$ZaN)qWKJ6wNP?y5Wzlu{(gLA4bo68bCE6G$m zElB!|>qz20^yY*Oy{3bi#BMNk8B&0uS5%{HX?SIL`-Z8mpv!%{P`enxRiSN&qqa6W zOJhuK8Q;vCYA?#r(WVOR4OO}0OKV)~NeOiH1q6gFpl=+r-KGQ8OQ* z{kBPfd4O66Xdv|vJ2{_Q!?tjjQGWe``>h=YV|en6M{M&SQN4y2y_+8R{br<(w8xfO z_twQ{H>2U}j!aW+oL|ZYwtC}!TkV?-`n~=a!WNPTKnE^wv$OWXXgbYR>+CGF$^`7M zy|_)>q!$*Ua*Pr*ktb?XP|nckMG@*;Q}f)K!vHH#EF=?T4J6efL0Z5R6;_z^4Ow+p z;P@!MqsRq;BzoR8{V*0Jg9X(pX)=lRZU!Iou{{20H`0S7qaqA`StAW6X)-X>BDThv zbUw&m1#>LC9J1Sh zntVLP$DI7;w^qVsdw%$sBb-b$@VDhe+f{3pH`D+K1!bXx2X><{q$0G{9kHhiutcp(sE7hn4@02tf10}bwKRy~)^5nRe?@wx1cI*vx) z7Bp|!g1$;i-&(YfljPhVj}vgDUfQ&Qm}Hb~0t)k*A=_WgurqY7Ez9tDI0MT-`ON|M z>vD^Xil0cMzeXV7*CF_4VGkz?^K*n>9svHg&-d1u!E87Ep)Sa_8N0Y0nOz)=Y~Ts+ zF(kM3W_Gb6^h79?gqdcy1Up42u`cy7f20wf4#Vq&2dYI&0o=Q-ZYZjmy*t~c12cpr zdCGMArP{ZT&CvVdDbwSZYG2>V`F?gUdzfuIGr@wo)WJr>JkJ@}zdtEoGSnI&fR)1Fg-lgCSfhwM+gWIS=TC*-?hUywn^(o{p zQh@)`6SYW1 z)ukQOf+`2Kf|+A=15wS4?rbZC9v~?vcaEJ7J?IvoV(1Yz^OQd)8_taR<=S_82j*K8 z8rZln`J%+8{$P2X+9t0mJ09H6RCkqA@K8U)?;r4cmV2rZ`7iN6#1{}{ad8XbCJq+J zL;zIzj^tbQTgNT@v5a>_{ND@9aWsH<&Wouc_t43qQ8P<=pvantTbjWl#3KM6eT3j=1B)#i#N&}sbh+G`z!kFGp?5FvJdRe*=M@V_gVU?(mM!afg&;8ZWG}+qm<6GAX>-YFym^GOs}9-ZW2RF|q0rvrpnz%KX|I*5$7pcF=>Z+jbm%k{P5@ z>qUNjezGZY{+%dYG6x672~o-EFqNWlMX6hc?(2xE<#W^|=hkVjt@9kk4Q*x0aKxMi zuQb1yE99MUA&(fO8|Lhq;1R6a6feN(`Wt&{?H61La^ik$ggdQHhfs{x;c%a`7|<%;NIXMp3~Djs$@Qgx)a;Z>@zg%?_v5m zZ#R-G*8ZKR?sPr;=F0VzYFDDtmH6S=YVu?yd9s=ut0c$X|9W+Nx-vdp9lul=zqFPd zTTj09i>{Zdov&0nUs>9JU&MdVvKs#HkkHt@5n+w-H)5~Hs?otpbnxw^ ze~$c%_)p?bj#iJKuN*(WemMJa^uk6c-1x-rBCP$PYFqzDZT)W_eP{IM=-;0Bao5Lf zW7XK$T5RlBet2}a5*vPZ;p5l{R_=W0&6(>nZy$JP_~!8X-ow?F!>gfRHtqd>;jhQv zdiKV%)t=Ex&uF#j_(x60@3eKknY^B?wk0ZUiFXhDXz13^dfU-z?C4tT=$(E0-|D&1 z^LOey`c1t$I9VB-{BY{$mu_ELAN>0Iz890+I=g^yKwj#ZnU{HW>4JMCT7 z_JKv-2or}wKMT-=4^SQseGnMyN9DrT<*Q{` z^Lkoxxp@`tw1$Xfm~T%j#Z)MG*s4 z^EuguS3Xxp02S-(*%mm>E1H(eK?M=2 Wmfx`6|6rZ>BDw&Ru*vv`ukpWbLuzpV literal 0 HcmV?d00001 diff --git a/module/scripts/lib/telethon/tl/custom/__pycache__/sendergetter.cpython-314.pyc b/module/scripts/lib/telethon/tl/custom/__pycache__/sendergetter.cpython-314.pyc new file mode 100644 index 0000000000000000000000000000000000000000..2817b73600c6f3e924bbbd4af2f310298860e62f GIT binary patch literal 5060 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. + """ + return isinstance(self.original.action, + types.ChannelAdminLogEventActionEditMessage) + + @property + def deleted_message(self): + """ + Whether a message in this channel was deleted or not. + + If `True`, `old` will be present as + `Message `. + """ + return isinstance(self.original.action, + types.ChannelAdminLogEventActionDeleteMessage) + + @property + def changed_admin(self): + """ + Whether the permissions for an admin in this channel + changed or not. + + If `True`, `old` and `new` will be present as + :tl:`ChannelParticipant`. + """ + return isinstance( + self.original.action, + types.ChannelAdminLogEventActionParticipantToggleAdmin) + + @property + def changed_restrictions(self): + """ + Whether a message in this channel was edited or not. + + If `True`, `old` and `new` will be present as + :tl:`ChannelParticipant`. + """ + return isinstance( + self.original.action, + types.ChannelAdminLogEventActionParticipantToggleBan) + + @property + def changed_invites(self): + """ + Whether the invites in the channel were toggled or not. + + If `True`, `old` and `new` will be present as `bool`. + """ + return isinstance(self.original.action, + types.ChannelAdminLogEventActionToggleInvites) + + @property + def changed_location(self): + """ + Whether the location setting of the channel has changed or not. + + If `True`, `old` and `new` will be present as :tl:`ChannelLocation`. + """ + return isinstance(self.original.action, + types.ChannelAdminLogEventActionChangeLocation) + + @property + def joined(self): + """ + Whether `user` joined through the channel's + public username or not. + """ + return isinstance(self.original.action, + types.ChannelAdminLogEventActionParticipantJoin) + + @property + def joined_invite(self): + """ + Whether a new user joined through an invite + link to the channel or not. + + If `True`, `new` will be present as + :tl:`ChannelParticipant`. + """ + return isinstance(self.original.action, + types.ChannelAdminLogEventActionParticipantInvite) + + @property + def left(self): + """ + Whether `user` left the channel or not. + """ + return isinstance(self.original.action, + types.ChannelAdminLogEventActionParticipantLeave) + + @property + def changed_hide_history(self): + """ + Whether hiding the previous message history for new members + in the channel was toggled or not. + + If `True`, `old` and `new` will be present as `bool`. + """ + return isinstance(self.original.action, + types.ChannelAdminLogEventActionTogglePreHistoryHidden) + + @property + def changed_signatures(self): + """ + Whether the message signatures in the channel were toggled + or not. + + If `True`, `old` and `new` will be present as `bool`. + """ + return isinstance(self.original.action, + types.ChannelAdminLogEventActionToggleSignatures) + + @property + def changed_pin(self): + """ + Whether a new message in this channel was pinned or not. + + If `True`, `new` will be present as + `Message `. + """ + return isinstance(self.original.action, + types.ChannelAdminLogEventActionUpdatePinned) + + @property + def changed_default_banned_rights(self): + """ + Whether the default banned rights were changed or not. + + If `True`, `old` and `new` will + be present as :tl:`ChatBannedRights`. + """ + return isinstance(self.original.action, + types.ChannelAdminLogEventActionDefaultBannedRights) + + @property + def stopped_poll(self): + """ + Whether a poll was stopped or not. + + If `True`, `new` will be present as + `Message `. + """ + return isinstance(self.original.action, + types.ChannelAdminLogEventActionStopPoll) + + @property + def started_group_call(self): + """ + Whether a group call was started or not. + + If `True`, `new` will be present as :tl:`InputGroupCall`. + """ + return isinstance(self.original.action, + types.ChannelAdminLogEventActionStartGroupCall) + + @property + def discarded_group_call(self): + """ + Whether a group call was started or not. + + If `True`, `old` will be present as :tl:`InputGroupCall`. + """ + return isinstance(self.original.action, + types.ChannelAdminLogEventActionDiscardGroupCall) + + @property + def user_muted(self): + """ + Whether a participant was muted in the ongoing group call or not. + + If `True`, `new` will be present as :tl:`GroupCallParticipant`. + """ + return isinstance(self.original.action, + types.ChannelAdminLogEventActionParticipantMute) + + @property + def user_unmutted(self): + """ + Whether a participant was unmuted from the ongoing group call or not. + + If `True`, `new` will be present as :tl:`GroupCallParticipant`. + """ + return isinstance(self.original.action, + types.ChannelAdminLogEventActionParticipantUnmute) + + @property + def changed_call_settings(self): + """ + Whether the group call settings were changed or not. + + If `True`, `new` will be `True` if new users are muted on join. + """ + return isinstance(self.original.action, + types.ChannelAdminLogEventActionToggleGroupCallSetting) + + @property + def changed_history_ttl(self): + """ + Whether the Time To Live of the message history has changed. + + Messages sent after this change will have a ``ttl_period`` in seconds + indicating how long they should live for before being auto-deleted. + + If `True`, `old` will be the old TTL, and `new` the new TTL, in seconds. + """ + return isinstance(self.original.action, + types.ChannelAdminLogEventActionChangeHistoryTTL) + + @property + def deleted_exported_invite(self): + """ + Whether the exported chat invite has been deleted. + + If `True`, `old` will be the deleted :tl:`ExportedChatInvite`. + """ + return isinstance(self.original.action, + types.ChannelAdminLogEventActionExportedInviteDelete) + + @property + def edited_exported_invite(self): + """ + Whether the exported chat invite has been edited. + + If `True`, `old` and `new` will be the old and new + :tl:`ExportedChatInvite`, respectively. + """ + return isinstance(self.original.action, + types.ChannelAdminLogEventActionExportedInviteEdit) + + @property + def revoked_exported_invite(self): + """ + Whether the exported chat invite has been revoked. + + If `True`, `old` will be the revoked :tl:`ExportedChatInvite`. + """ + return isinstance(self.original.action, + types.ChannelAdminLogEventActionExportedInviteRevoke) + + @property + def joined_by_invite(self): + """ + Whether a new participant has joined with the use of an invite link. + + If `True`, `old` will be pre-existing (old) :tl:`ExportedChatInvite` + used to join. + """ + return isinstance(self.original.action, + types.ChannelAdminLogEventActionParticipantJoinByInvite) + + @property + def changed_user_volume(self): + """ + Whether a participant's volume in a call has been changed. + + If `True`, `new` will be the updated :tl:`GroupCallParticipant`. + """ + return isinstance(self.original.action, + types.ChannelAdminLogEventActionParticipantVolume) + + def __str__(self): + return str(self.original) + + def stringify(self): + return self.original.stringify() diff --git a/module/scripts/lib/telethon/tl/custom/button.py b/module/scripts/lib/telethon/tl/custom/button.py new file mode 100644 index 0000000..7f8759d --- /dev/null +++ b/module/scripts/lib/telethon/tl/custom/button.py @@ -0,0 +1,347 @@ +from .. import types +from ... import utils + + +class Button: + """ + .. note:: + + This class is used to **define** reply markups, e.g. when + sending a message or replying to events. When you access + `Message.buttons ` + they are actually `MessageButton + `, + so you might want to refer to that class instead. + + Helper class to allow defining ``reply_markup`` when + sending a message with inline or keyboard buttons. + + You should make use of the defined class methods to create button + instances instead making them yourself (i.e. don't do ``Button(...)`` + but instead use methods line `Button.inline(...) ` etc. + + You can use `inline`, `switch_inline`, `url`, `auth`, `buy` and `game` + together to create inline buttons (under the message). + + You can use `text`, `request_location`, `request_phone` and `request_poll` + together to create a reply markup (replaces the user keyboard). + You can also configure the aspect of the reply with these. + The latest message with a reply markup will be the one shown to the user + (messages contain the buttons, not the chat itself). + + You **cannot** mix the two type of buttons together, + and it will error if you try to do so. + + The text for all buttons may be at most 142 characters. + If more characters are given, Telegram will cut the text + to 128 characters and add the ellipsis (…) character as + the 129. + """ + def __init__(self, button, *, resize, single_use, selective, + persistent, placeholder): + self.button = button + self.resize = resize + self.single_use = single_use + self.selective = selective + self.persistent = persistent + self.placeholder = placeholder + @staticmethod + def _get_style(style=None, icon=None): + """ + Returns `KeyboardButtonStyle` by an appropriate mapping. + """ + if style is not None and (not isinstance(style,str) or (style := style.lower()) not in {'primary','danger','success'}): + raise ValueError("Invalid value. Must be either one of 'primary' (blue) , 'danger' (red) and 'success' (green) , or None") + return types.KeyboardButtonStyle(**({f'bg_{style}': True,'icon':icon} if style else {'icon': icon})) + @staticmethod + def _is_inline(button): + """ + Returns `True` if the button belongs to an inline keyboard. + """ + return isinstance(button, types.KeyboardInlineButton) + + @staticmethod + def inline(text, data=None, style=None, icon=None): + """ + Creates a new inline button with some payload data in it. + + If `data` is omitted, the given `text` will be used as `data`. + In any case `data` should be either `bytes` or `str`. + + Note that the given `data` must be less or equal to 64 bytes. + If more than 64 bytes are passed as data, ``ValueError`` is raised. + If you need to store more than 64 bytes, consider saving the real + data in a database and a reference to that data inside the button. + + When the user clicks this button, `events.CallbackQuery + ` will trigger with the + same data that the button contained, so that you can determine which + button was pressed. + """ + if not data: + data = text.encode('utf-8') + elif not isinstance(data, (bytes, bytearray, memoryview)): + data = str(data).encode('utf-8') + + if len(data) > 64: + raise ValueError('Too many bytes for the data') + + return types.KeyboardInlineButton(text, types.InlineButtonTypeCallback(data), style=Button._get_style(style, icon)) + + @staticmethod + def switch_inline(text, query='', same_peer=False, style=None, icon=None): + """ + Creates a new inline button to switch to inline query. + + If `query` is given, it will be the default text to be used + when making the inline query. + + If ``same_peer is True`` the inline query will directly be + set under the currently opened chat. Otherwise, the user will + have to select a different dialog to make the query. + + When the user clicks this button, after a chat is selected, their + input field will be filled with the username of your bot followed + by the query text, ready to make inline queries. + """ + return types.KeyboardInlineButton(text, types.InlineButtonTypeSwitchInline(query, same_peer), style=Button._get_style(style, icon)) + + @staticmethod + def url(text, url=None, style=None, icon=None): + """ + Creates a new inline button to open the desired URL on click. + + If no `url` is given, the `text` will be used as said URL instead. + + You cannot detect that the user clicked this button directly. + + When the user clicks this button, a confirmation box will be shown + to the user asking whether they want to open the displayed URL unless + the domain is trusted, and once confirmed the URL will open in their + device. + """ + return types.KeyboardInlineButton(text, types.InlineButtonTypeUrl(url or text), style=Button._get_style(style, icon)) + + @staticmethod + def auth(text, url=None, style=None, icon=None, *, bot=None, write_access=False, fwd_text=None): + """ + Creates a new inline button to authorize the user at the given URL. + + You should set the `url` to be on the same domain as the one configured + for the desired `bot` via `@BotFather `_ using + the ``/setdomain`` command. + + For more information about letting the user login via Telegram to + a certain domain, see https://core.telegram.org/widgets/login. + + If no `url` is specified, it will default to `text`. + + Args: + bot (`hints.EntityLike`): + The bot that requires this authorization. By default, this + is the bot that is currently logged in (itself), although + you may pass a different input peer. + + .. note:: + + For now, you cannot use ID or username for this argument. + If you want to use a different bot than the one currently + logged in, you must manually use `client.get_input_entity() + `. + + write_access (`bool`): + Whether write access is required or not. + This is `False` by default (read-only access). + + fwd_text (`str`): + The new text to show in the button if the message is + forwarded. By default, the button text will be the same. + + When the user clicks this button, a confirmation box will be shown + to the user asking whether they want to login to the specified domain. + """ + return types.KeyboardInlineButton( + text, + types.InputInlineButtonTypeUrlAuth( + url=url or text, + bot=utils.get_input_user(bot or types.InputUserSelf()), + request_write_access=write_access, + fwd_text=fwd_text + ), + style=Button._get_style(style, icon) + ) + + @classmethod + def text(cls, text, *, resize=None, single_use=None, selective=None, + persistent=None, placeholder=None, style=None, icon=None): + """ + Creates a new keyboard button with the given text. + + Args: + text (`str`): + The title of the button. + + resize (`bool`): + If present, the entire keyboard will be reconfigured to + be resized and be smaller if there are not many buttons. + + single_use (`bool`): + If present, the entire keyboard will be reconfigured to + be usable only once before it hides itself. + + selective (`bool`): + If present, the entire keyboard will be reconfigured to + be "selective". The keyboard will be shown only to specific + users. It will target users that are @mentioned in the text + of the message or to the sender of the message you reply to. + + persistent (`bool`): + If present, always show the keyboard when the regular keyboard + is hidden. Defaults to false, in which case the custom keyboard + can be hidden and revealed via the keyboard icon. + + placeholder (`str`): + The placeholder to be shown in the input field when the keyboard is active; + 1-64 characters + + When the user clicks this button, a text message with the same text + as the button will be sent, and can be handled with `events.NewMessage + `. You cannot distinguish + between a button press and the user typing and sending exactly the + same text on their own. + """ + return cls( + types.KeyboardButton(text, types.ButtonTypeDefault(), style=cls._get_style(style, icon)), + resize=resize, + single_use=single_use, + selective=selective, + persistent=persistent, + placeholder=placeholder + ) + + @classmethod + def request_location(cls, text, style=None, icon=None, *, resize=None, single_use=None, selective=None, + persistent=None, placeholder=None): + """ + Creates a new keyboard button to request the user's location on click. + + ``resize``, ``single_use``, ``selective``, ``persistent`` and ``placeholder`` + are documented in `text`. + + When the user clicks this button, a confirmation box will be shown + to the user asking whether they want to share their location with the + bot, and if confirmed a message with geo media will be sent. + """ + return cls( + types.KeyboardButton(text, types.ButtonTypeRequestGeoLocation(), style=cls._get_style(style, icon)), + resize=resize, + single_use=single_use, + selective=selective, + persistent=persistent, + placeholder=placeholder + ) + + @classmethod + def request_phone(cls, text, style=None, icon=None, *, resize=None, single_use=None, + selective=None, persistent=None, placeholder=None): + """ + Creates a new keyboard button to request the user's phone on click. + + ``resize``, ``single_use``, ``selective``, ``persistent`` and ``placeholder`` + are documented in `text`. + + When the user clicks this button, a confirmation box will be shown + to the user asking whether they want to share their phone with the + bot, and if confirmed a message with contact media will be sent. + """ + return cls( + types.KeyboardButton(text, types.ButtonTypeRequestPhone(), style=cls._get_style(style, icon)), + resize=resize, + single_use=single_use, + selective=selective, + placeholder=placeholder, + persistent=persistent + ) + + @classmethod + def request_poll(cls, text, style=None, icon=None, *, force_quiz=False, resize=None, single_use=None, + selective=None, persistent=None, placeholder=None): + """ + Creates a new keyboard button to request the user to create a poll. + + If `force_quiz` is `False`, the user will be allowed to choose whether + they want their poll to be a quiz or not. Otherwise, the user will be + forced to create a quiz when creating the poll. + + If a poll is a quiz, there will be only one answer that is valid, and + the votes cannot be retracted. Otherwise, users can vote and retract + the vote, and the pol might be multiple choice. + + ``resize``, ``single_use``, ``selective``, ``persistent`` and ``placeholder`` + are documented in `text`. + + When the user clicks this button, a screen letting the user create a + poll will be shown, and if they do create one, the poll will be sent. + """ + return cls( + types.KeyboardButton(text, types.ButtonTypeRequestPoll(force_quiz), style=cls._get_style(style, icon)), + resize=resize, + single_use=single_use, + selective=selective, + persistent=persistent, + placeholder=placeholder + ) + + @staticmethod + def clear(selective=None): + """ + Clears all keyboard buttons after sending a message with this markup. + When used, no other button should be present or it will be ignored. + + ``selective`` is as documented in `text`. + + """ + return types.ReplyKeyboardHide(selective=selective) + + @staticmethod + def force_reply(single_use=None, selective=None, placeholder=None): + """ + Forces a reply to the message with this markup. If used, + no other button should be present or it will be ignored. + + ``single_use``, ``selective`` and ``placeholder`` are as documented in `text`. + + """ + return types.ReplyKeyboardForceReply( + single_use=single_use, + selective=selective, + placeholder=placeholder) + + @staticmethod + def buy(text, style=None, icon=None): + """ + Creates a new inline button to buy a product. + + This can only be used when sending files of type + :tl:`InputMediaInvoice`, and must be the first button. + + If the button is not specified, Telegram will automatically + add the button to the message. See the + `Payments API `__ + documentation for more information. + """ + return types.KeyboardInlineButton(text, types.InlineButtonTypeBuy(), style=Button._get_style(style, icon)) + + @staticmethod + def game(text, style=None, icon=None): + """ + Creates a new inline button to start playing a game. + + This should be used when sending files of type + :tl:`InputMediaGame`, and must be the first button. + + See the + `Games `__ + documentation for more information on using games. + """ + return types.KeyboardInlineButton(text, types.InlineButtonTypeGame(), style=Button._get_style(style, icon)) diff --git a/module/scripts/lib/telethon/tl/custom/chatgetter.py b/module/scripts/lib/telethon/tl/custom/chatgetter.py new file mode 100644 index 0000000..240d9b8 --- /dev/null +++ b/module/scripts/lib/telethon/tl/custom/chatgetter.py @@ -0,0 +1,150 @@ +import abc + +from ... import errors, utils +from ...tl import types + + +class ChatGetter(abc.ABC): + """ + Helper base class that introduces the `chat`, `input_chat` + and `chat_id` properties and `get_chat` and `get_input_chat` + methods. + """ + def __init__(self, chat_peer=None, *, input_chat=None, chat=None, broadcast=None): + self._chat_peer = chat_peer + self._input_chat = input_chat + self._chat = chat + self._broadcast = broadcast + self._client = None + + @property + def chat(self): + """ + Returns the :tl:`User`, :tl:`Chat` or :tl:`Channel` where this object + belongs to. It may be `None` if Telegram didn't send the chat. + + If you only need the ID, use `chat_id` instead. + + If you need to call a method which needs + this chat, use `input_chat` instead. + + If you're using `telethon.events`, use `get_chat()` instead. + """ + return self._chat + + async def get_chat(self): + """ + Returns `chat`, but will make an API call to find the + chat unless it's already cached. + + If you only need the ID, use `chat_id` instead. + + If you need to call a method which needs + this chat, use `get_input_chat()` instead. + """ + # See `get_sender` for information about 'min'. + if (self._chat is None or getattr(self._chat, 'min', None))\ + and await self.get_input_chat(): + try: + self._chat =\ + await self._client.get_entity(self._input_chat) + except ValueError: + await self._refetch_chat() + return self._chat + + @property + def input_chat(self): + """ + This :tl:`InputPeer` is the input version of the chat where the + message was sent. Similarly to `input_sender + `, this + doesn't have things like username or similar, but still useful in + some cases. + + Note that this might not be available if the library doesn't + have enough information available. + """ + if self._input_chat is None and self._chat_peer and self._client: + try: + self._input_chat = self._client._mb_entity_cache.get( + utils.get_peer_id(self._chat_peer, add_mark=False))._as_input_peer() + except AttributeError: + pass + + return self._input_chat + + async def get_input_chat(self): + """ + Returns `input_chat`, but will make an API call to find the + input chat unless it's already cached. + """ + if self.input_chat is None and self.chat_id and self._client: + try: + # The chat may be recent, look in dialogs + target = self.chat_id + async for d in self._client.iter_dialogs(100): + if d.id == target: + self._chat = d.entity + self._input_chat = d.input_entity + break + except errors.RPCError: + pass + + return self._input_chat + + @property + def chat_id(self): + """ + Returns the marked chat integer ID. Note that this value **will + be different** from ``peer_id`` for incoming private messages, since + the chat *to* which the messages go is to your own person, but + the *chat* itself is with the one who sent the message. + + TL;DR; this gets the ID that you expect. + + If there is a chat in the object, `chat_id` will *always* be set, + which is why you should use it instead of `chat.id `. + """ + return utils.get_peer_id(self._chat_peer) if self._chat_peer else None + + @property + def is_private(self): + """ + `True` if the message was sent as a private message. + + Returns `None` if there isn't enough information + (e.g. on `events.MessageDeleted `). + """ + return isinstance(self._chat_peer, types.PeerUser) if self._chat_peer else None + + @property + def is_group(self): + """ + True if the message was sent on a group or megagroup. + + Returns `None` if there isn't enough information + (e.g. on `events.MessageDeleted `). + """ + # TODO Cache could tell us more in the future + if self._broadcast is None and hasattr(self.chat, 'broadcast'): + self._broadcast = bool(self.chat.broadcast) + + if isinstance(self._chat_peer, types.PeerChannel): + if self._broadcast is None: + return None + else: + return not self._broadcast + + return isinstance(self._chat_peer, types.PeerChat) + + @property + def is_channel(self): + """`True` if the message was sent on a megagroup or channel.""" + # The only case where chat peer could be none is in MessageDeleted, + # however those always have the peer in channels. + return isinstance(self._chat_peer, types.PeerChannel) + + async def _refetch_chat(self): + """ + Re-fetches chat information through other means. + """ diff --git a/module/scripts/lib/telethon/tl/custom/conversation.py b/module/scripts/lib/telethon/tl/custom/conversation.py new file mode 100644 index 0000000..6cb973d --- /dev/null +++ b/module/scripts/lib/telethon/tl/custom/conversation.py @@ -0,0 +1,529 @@ +import asyncio +import functools +import inspect +import itertools +import time + +from .chatgetter import ChatGetter +from ... import helpers, utils, errors + +# Sometimes the edits arrive very fast (within the same second). +# In that case we add a small delta so that the age is older, for +# comparision purposes. This value is enough for up to 1000 messages. +_EDIT_COLLISION_DELTA = 0.001 + + +def _checks_cancelled(f): + @functools.wraps(f) + def wrapper(self, *args, **kwargs): + if self._cancelled: + raise asyncio.CancelledError('The conversation was cancelled before') + + return f(self, *args, **kwargs) + return wrapper + + +class Conversation(ChatGetter): + """ + Represents a conversation inside an specific chat. + + A conversation keeps track of new messages since it was + created until its exit and easily lets you query the + current state. + + If you need a conversation across two or more chats, + you should use two conversations and synchronize them + as you better see fit. + """ + _id_counter = 0 + _custom_counter = 0 + + def __init__(self, client, input_chat, + *, timeout, total_timeout, max_messages, + exclusive, replies_are_responses): + # This call resets the client + ChatGetter.__init__(self, input_chat=input_chat) + + self._id = Conversation._id_counter + Conversation._id_counter += 1 + + self._client = client + self._timeout = timeout + self._total_timeout = total_timeout + self._total_due = None + + self._outgoing = set() + self._last_outgoing = 0 + self._incoming = [] + self._last_incoming = 0 + self._max_incoming = max_messages + self._last_read = None + self._custom = {} + + self._pending_responses = {} + self._pending_replies = {} + self._pending_edits = {} + self._pending_reads = {} + + self._exclusive = exclusive + self._cancelled = False + + # The user is able to expect two responses for the same message. + # {desired message ID: next incoming index} + self._response_indices = {} + if replies_are_responses: + self._reply_indices = self._response_indices + else: + self._reply_indices = {} + + self._edit_dates = {} + + @_checks_cancelled + async def send_message(self, *args, **kwargs): + """ + Sends a message in the context of this conversation. Shorthand + for `telethon.client.messages.MessageMethods.send_message` with + ``entity`` already set. + """ + sent = await self._client.send_message( + self._input_chat, *args, **kwargs) + + # Albums will be lists, so handle that + ms = sent if isinstance(sent, list) else (sent,) + self._outgoing.update(m.id for m in ms) + self._last_outgoing = ms[-1].id + return sent + + @_checks_cancelled + async def send_file(self, *args, **kwargs): + """ + Sends a file in the context of this conversation. Shorthand + for `telethon.client.uploads.UploadMethods.send_file` with + ``entity`` already set. + """ + sent = await self._client.send_file( + self._input_chat, *args, **kwargs) + + # Albums will be lists, so handle that + ms = sent if isinstance(sent, list) else (sent,) + self._outgoing.update(m.id for m in ms) + self._last_outgoing = ms[-1].id + return sent + + @_checks_cancelled + def mark_read(self, message=None): + """ + Marks as read the latest received message if ``message is None``. + Otherwise, marks as read until the given message (or message ID). + + This is equivalent to calling `client.send_read_acknowledge + `. + """ + if message is None: + if self._incoming: + message = self._incoming[-1].id + else: + message = 0 + elif not isinstance(message, int): + message = message.id + + return self._client.send_read_acknowledge( + self._input_chat, max_id=message) + + def get_response(self, message=None, *, timeout=None): + """ + Gets the next message that responds to a previous one. This is + the method you need most of the time, along with `get_edit`. + + Args: + message (`Message ` | `int`, optional): + The message (or the message ID) for which a response + is expected. By default this is the last sent message. + + timeout (`int` | `float`, optional): + If present, this `timeout` (in seconds) will override the + per-action timeout defined for the conversation. + + .. code-block:: python + + async with client.conversation(...) as conv: + await conv.send_message('Hey, what is your name?') + + response = await conv.get_response() + name = response.text + + await conv.send_message('Nice to meet you, {}!'.format(name)) + """ + return self._get_message( + message, self._response_indices, self._pending_responses, timeout, + lambda x, y: True + ) + + def get_reply(self, message=None, *, timeout=None): + """ + Gets the next message that explicitly replies to a previous one. + """ + return self._get_message( + message, self._reply_indices, self._pending_replies, timeout, + lambda x, y: x.reply_to and x.reply_to.reply_to_msg_id == y + ) + + def _get_message( + self, target_message, indices, pending, timeout, condition): + """ + Gets the next desired message under the desired condition. + + Args: + target_message (`object`): + The target message for which we want to find another + response that applies based on `condition`. + + indices (`dict`): + This dictionary remembers the last ID chosen for the + input `target_message`. + + pending (`dict`): + This dictionary remembers {msg_id: Future} to be set + once `condition` is met. + + timeout (`int`): + The timeout (in seconds) override to use for this operation. + + condition (`callable`): + The condition callable that checks if an incoming + message is a valid response. + """ + start_time = time.time() + target_id = self._get_message_id(target_message) + + # If there is no last-chosen ID, make sure to pick one *after* + # the input message, since we don't want responses back in time + if target_id not in indices: + for i, incoming in enumerate(self._incoming): + if incoming.id > target_id: + indices[target_id] = i + break + else: + indices[target_id] = len(self._incoming) + + # We will always return a future from here, even if the result + # can be set immediately. Otherwise, needing to await only + # sometimes is an annoying edge case (i.e. we would return + # a `Message` but `get_response()` always `await`'s). + future = self._client.loop.create_future() + + # If there are enough responses saved return the next one + last_idx = indices[target_id] + if last_idx < len(self._incoming): + incoming = self._incoming[last_idx] + if condition(incoming, target_id): + indices[target_id] += 1 + future.set_result(incoming) + return future + + # Otherwise the next incoming response will be the one to use + # + # Note how we fill "pending" before giving control back to the + # event loop through "await". We want to register it as soon as + # possible, since any other task switch may arrive with the result. + pending[target_id] = future + return self._get_result(future, start_time, timeout, pending, target_id) + + def get_edit(self, message=None, *, timeout=None): + """ + Awaits for an edit after the last message to arrive. + The arguments are the same as those for `get_response`. + """ + start_time = time.time() + target_id = self._get_message_id(message) + + target_date = self._edit_dates.get(target_id, 0) + earliest_edit = min( + (x for x in self._incoming + if x.edit_date + and x.id > target_id + and x.edit_date.timestamp() > target_date + ), + key=lambda x: x.edit_date.timestamp(), + default=None + ) + + future = self._client.loop.create_future() + if earliest_edit and earliest_edit.edit_date.timestamp() > target_date: + self._edit_dates[target_id] = earliest_edit.edit_date.timestamp() + future.set_result(earliest_edit) + return future # we should always return something we can await + + # Otherwise the next incoming response will be the one to use + self._pending_edits[target_id] = future + return self._get_result(future, start_time, timeout, self._pending_edits, target_id) + + def wait_read(self, message=None, *, timeout=None): + """ + Awaits for the sent message to be marked as read. Note that + receiving a response doesn't imply the message was read, and + this action will also trigger even without a response. + """ + start_time = time.time() + future = self._client.loop.create_future() + target_id = self._get_message_id(message) + + if self._last_read is None: + self._last_read = target_id - 1 + + if self._last_read >= target_id: + return + + self._pending_reads[target_id] = future + return self._get_result(future, start_time, timeout, self._pending_reads, target_id) + + async def wait_event(self, event, *, timeout=None): + """ + Waits for a custom event to occur. Timeouts still apply. + + .. note:: + + **Only use this if there isn't another method available!** + For example, don't use `wait_event` for new messages, + since `get_response` already exists, etc. + + Unless you're certain that your code will run fast enough, + generally you should get a "handle" of this special coroutine + before acting. In this example you will see how to wait for a user + to join a group with proper use of `wait_event`: + + .. code-block:: python + + from telethon import TelegramClient, events + + client = TelegramClient(...) + group_id = ... + + async def main(): + # Could also get the user id from an event; this is just an example + user_id = ... + + async with client.conversation(user_id) as conv: + # Get a handle to the future event we'll wait for + handle = conv.wait_event(events.ChatAction( + group_id, + func=lambda e: e.user_joined and e.user_id == user_id + )) + + # Perform whatever action in between + await conv.send_message('Please join this group before speaking to me!') + + # Wait for the event we registered above to fire + event = await handle + + # Continue with the conversation + await conv.send_message('Thanks!') + + This way your event can be registered before acting, + since the response may arrive before your event was + registered. It depends on your use case since this + also means the event can arrive before you send + a previous action. + """ + start_time = time.time() + if isinstance(event, type): + event = event() + + await event.resolve(self._client) + + counter = Conversation._custom_counter + Conversation._custom_counter += 1 + + future = self._client.loop.create_future() + self._custom[counter] = (event, future) + try: + return await self._get_result(future, start_time, timeout, self._custom, counter) + finally: + # Need to remove it from the dict if it times out, else we may + # try and fail to set the result later (#1618). + self._custom.pop(counter, None) + + async def _check_custom(self, built): + for key, (ev, fut) in list(self._custom.items()): + ev_type = type(ev) + inst = built[ev_type] + + if inst: + filter = ev.filter(inst) + if inspect.isawaitable(filter): + filter = await filter + + if filter: + fut.set_result(inst) + del self._custom[key] + + def _on_new_message(self, response): + response = response.message + if response.chat_id != self.chat_id or response.out: + return + + if len(self._incoming) == self._max_incoming: + self._cancel_all(ValueError('Too many incoming messages')) + return + + self._incoming.append(response) + + # Most of the time, these dictionaries will contain just one item + # TODO In fact, why not make it be that way? Force one item only. + # How often will people want to wait for two responses at + # the same time? It's impossible, first one will arrive + # and then another, so they can do that. + for msg_id, future in list(self._pending_responses.items()): + self._response_indices[msg_id] = len(self._incoming) + future.set_result(response) + del self._pending_responses[msg_id] + + for msg_id, future in list(self._pending_replies.items()): + if response.reply_to and msg_id == response.reply_to.reply_to_msg_id: + self._reply_indices[msg_id] = len(self._incoming) + future.set_result(response) + del self._pending_replies[msg_id] + + def _on_edit(self, message): + message = message.message + if message.chat_id != self.chat_id or message.out: + return + + # We have to update our incoming messages with the new edit date + for i, m in enumerate(self._incoming): + if m.id == message.id: + self._incoming[i] = message + break + + for msg_id, future in list(self._pending_edits.items()): + if msg_id < message.id: + edit_ts = message.edit_date.timestamp() + + # We compare <= because edit_ts resolution is always to + # seconds, but we may have increased _edit_dates before. + # Since the dates are ever growing this is not a problem. + if edit_ts <= self._edit_dates.get(msg_id, 0): + self._edit_dates[msg_id] += _EDIT_COLLISION_DELTA + else: + self._edit_dates[msg_id] = message.edit_date.timestamp() + + future.set_result(message) + del self._pending_edits[msg_id] + + def _on_read(self, event): + if event.chat_id != self.chat_id or event.inbox: + return + + self._last_read = event.max_id + + for msg_id, pending in list(self._pending_reads.items()): + if msg_id >= self._last_read: + pending.set_result(True) + del self._pending_reads[msg_id] + + def _get_message_id(self, message): + if message is not None: # 0 is valid but false-y, check for None + return message if isinstance(message, int) else message.id + elif self._last_outgoing: + return self._last_outgoing + else: + raise ValueError('No message was sent previously') + + @_checks_cancelled + def _get_result(self, future, start_time, timeout, pending, target_id): + due = self._total_due + if timeout is None: + timeout = self._timeout + + if timeout is not None: + due = min(due, start_time + timeout) + + # NOTE: We can't try/finally to pop from pending here because + # the event loop needs to get back to us, but it might + # dispatch another update before, and in that case a + # response could be set twice. So responses must be + # cleared when their futures are set to a result. + return asyncio.wait_for( + future, + timeout=None if due == float('inf') else due - time.time() + ) + + def _cancel_all(self, exception=None): + self._cancelled = True + for pending in itertools.chain( + self._pending_responses.values(), + self._pending_replies.values(), + self._pending_edits.values()): + if exception: + pending.set_exception(exception) + else: + pending.cancel() + + for _, fut in self._custom.values(): + if exception: + fut.set_exception(exception) + else: + fut.cancel() + + async def __aenter__(self): + self._input_chat = \ + await self._client.get_input_entity(self._input_chat) + + self._chat_peer = utils.get_peer(self._input_chat) + + # Make sure we're the only conversation in this chat if it's exclusive + chat_id = utils.get_peer_id(self._chat_peer) + conv_set = self._client._conversations[chat_id] + if self._exclusive and conv_set: + raise errors.AlreadyInConversationError() + + conv_set.add(self) + self._cancelled = False + + self._last_outgoing = 0 + self._last_incoming = 0 + for d in ( + self._outgoing, self._incoming, + self._pending_responses, self._pending_replies, + self._pending_edits, self._response_indices, + self._reply_indices, self._edit_dates, self._custom): + d.clear() + + if self._total_timeout: + self._total_due = time.time() + self._total_timeout + else: + self._total_due = float('inf') + + return self + + def cancel(self): + """ + Cancels the current conversation. Pending responses and subsequent + calls to get a response will raise ``asyncio.CancelledError``. + + This method is synchronous and should not be awaited. + """ + self._cancel_all() + + async def cancel_all(self): + """ + Calls `cancel` on *all* conversations in this chat. + + Note that you should ``await`` this method, since it's meant to be + used outside of a context manager, and it needs to resolve the chat. + """ + chat_id = await self._client.get_peer_id(self._input_chat) + for conv in self._client._conversations[chat_id]: + conv.cancel() + + async def __aexit__(self, exc_type, exc_val, exc_tb): + chat_id = utils.get_peer_id(self._chat_peer) + conv_set = self._client._conversations[chat_id] + conv_set.discard(self) + if not conv_set: + del self._client._conversations[chat_id] + + self._cancel_all() + + __enter__ = helpers._sync_enter + __exit__ = helpers._sync_exit diff --git a/module/scripts/lib/telethon/tl/custom/dialog.py b/module/scripts/lib/telethon/tl/custom/dialog.py new file mode 100644 index 0000000..5f5ba33 --- /dev/null +++ b/module/scripts/lib/telethon/tl/custom/dialog.py @@ -0,0 +1,172 @@ +from . import Draft +from .. import TLObject, types, functions +from ... import utils + + +class Dialog: + """ + Custom class that encapsulates a dialog (an open "conversation" with + someone, a group or a channel) providing an abstraction to easily + access the input version/normal entity/message etc. The library will + return instances of this class when calling :meth:`.get_dialogs()`. + + Args: + dialog (:tl:`Dialog`): + The original ``Dialog`` instance. + + pinned (`bool`): + Whether this dialog is pinned to the top or not. + + folder_id (`folder_id`): + The folder ID that this dialog belongs to. + + archived (`bool`): + Whether this dialog is archived or not (``folder_id is None``). + + message (`Message `): + The last message sent on this dialog. Note that this member + will not be updated when new messages arrive, it's only set + on creation of the instance. + + date (`datetime`): + The date of the last message sent on this dialog. + + entity (`entity`): + The entity that belongs to this dialog (user, chat or channel). + + input_entity (:tl:`InputPeer`): + Input version of the entity. + + id (`int`): + The marked ID of the entity, which is guaranteed to be unique. + + name (`str`): + Display name for this dialog. For chats and channels this is + their title, and for users it's "First-Name Last-Name". + + title (`str`): + Alias for `name`. + + unread_count (`int`): + How many messages are currently unread in this dialog. Note that + this value won't update when new messages arrive. + + unread_mentions_count (`int`): + How many mentions are currently unread in this dialog. Note that + this value won't update when new messages arrive. + + unread_reactions_count (`int`): + How many reactions are currently unread in this dialog. Note that + this value won't update when new messages arrive. + + unread_poll_votes_count (`int`): + How many poll votes are currently unread in this dialog. Note that + this value won't update when new messages arrive. + + draft (`Draft `): + The draft object in this dialog. It will not be `None`, + so you can call ``draft.set_message(...)``. + + is_user (`bool`): + `True` if the `entity` is a :tl:`User`. + + is_group (`bool`): + `True` if the `entity` is a :tl:`Chat` + or a :tl:`Channel` megagroup. + + is_channel (`bool`): + `True` if the `entity` is a :tl:`Channel`. + """ + def __init__(self, client, dialog, entities, message): + # Both entities and messages being dicts {ID: item} + self._client = client + self.dialog = dialog + self.pinned = bool(dialog.pinned) + self.folder_id = dialog.folder_id + self.archived = dialog.folder_id is not None + self.message = message + self.date = getattr(self.message, 'date', None) + + self.entity = entities[utils.get_peer_id(dialog.peer)] + self.input_entity = utils.get_input_peer(self.entity) + self.id = utils.get_peer_id(self.entity) # ^ May be InputPeerSelf() + self.name = self.title = utils.get_display_name(self.entity) + + self.unread_count = dialog.unread_count + self.unread_mentions_count = dialog.unread_mentions_count + self.unread_reactions_count = dialog.unread_reactions_count + self.unread_poll_votes_count = dialog.unread_poll_votes_count + + self.draft = Draft(client, self.entity, self.dialog.draft) + + self.is_user = isinstance(self.entity, types.User) + self.is_group = ( + isinstance(self.entity, (types.Chat, types.ChatForbidden)) or + (isinstance(self.entity, types.Channel) and self.entity.megagroup) + ) + self.is_channel = isinstance(self.entity, types.Channel) + self.is_community = isinstance(self.entity, types.Community) + + async def send_message(self, *args, **kwargs): + """ + Sends a message to this dialog. This is just a wrapper around + ``client.send_message(dialog.input_entity, *args, **kwargs)``. + """ + return await self._client.send_message( + self.input_entity, *args, **kwargs) + + async def delete(self, revoke=False): + """ + Deletes the dialog from your dialog list. If you own the + channel this won't destroy it, only delete it from the list. + + Shorthand for `telethon.client.dialogs.DialogMethods.delete_dialog` + with ``entity`` already set. + """ + # Pass the entire entity so the method can determine whether + # the `Chat` is deactivated (in which case we don't kick ourselves, + # or it would raise `PEER_ID_INVALID`). + await self._client.delete_dialog(self.entity, revoke=revoke) + + async def archive(self, folder=1): + """ + Archives (or un-archives) this dialog. + + Args: + folder (`int`, optional): + The folder to which the dialog should be archived to. + + If you want to "un-archive" it, use ``folder=0``. + + Returns: + The :tl:`Updates` object that the request produces. + + Example: + + .. code-block:: python + + # Archiving + dialog.archive() + + # Un-archiving + dialog.archive(0) + """ + return await self._client(functions.folders.EditPeerFoldersRequest([ + types.InputFolderPeer(self.input_entity, folder_id=folder) + ])) + + def to_dict(self): + return { + '_': 'Dialog', + 'name': self.name, + 'date': self.date, + 'draft': self.draft, + 'message': self.message, + 'entity': self.entity, + } + + def __str__(self): + return TLObject.pretty_format(self.to_dict()) + + def stringify(self): + return TLObject.pretty_format(self.to_dict(), indent=0) diff --git a/module/scripts/lib/telethon/tl/custom/draft.py b/module/scripts/lib/telethon/tl/custom/draft.py new file mode 100644 index 0000000..bde965e --- /dev/null +++ b/module/scripts/lib/telethon/tl/custom/draft.py @@ -0,0 +1,191 @@ +import datetime + +from .. import TLObject, types +from ..functions.messages import SaveDraftRequest +from ..types import DraftMessage +from ...errors import RPCError +from ...extensions import markdown +from ...utils import get_input_peer, get_peer, get_peer_id + + +class Draft: + """ + Custom class that encapsulates a draft on the Telegram servers, providing + an abstraction to change the message conveniently. The library will return + instances of this class when calling :meth:`get_drafts()`. + + Args: + date (`datetime`): + The date of the draft. + + link_preview (`bool`): + Whether the link preview is enabled or not. + + reply_to_msg_id (`int`): + The message ID that the draft will reply to. + """ + def __init__(self, client, entity, draft): + self._client = client + self._peer = get_peer(entity) + self._entity = entity + self._input_entity = get_input_peer(entity) if entity else None + + if not draft or not isinstance(draft, DraftMessage): + draft = DraftMessage('', None, None, None, None) + + self._text = markdown.unparse(draft.message, draft.entities) + self._raw_text = draft.message + self.date = draft.date + self.link_preview = not draft.no_webpage + self.reply_to_msg_id = draft.reply_to.reply_to_msg_id if isinstance(draft.reply_to, types.InputReplyToMessage) else None + + @property + def entity(self): + """ + The entity that belongs to this dialog (user, chat or channel). + """ + return self._entity + + @property + def input_entity(self): + """ + Input version of the entity. + """ + if not self._input_entity: + try: + self._input_entity = self._client._mb_entity_cache.get( + get_peer_id(self._peer, add_mark=False))._as_input_peer() + except AttributeError: + pass + + return self._input_entity + + async def get_entity(self): + """ + Returns `entity` but will make an API call if necessary. + """ + if not self.entity and await self.get_input_entity(): + try: + self._entity =\ + await self._client.get_entity(self._input_entity) + except ValueError: + pass + + return self._entity + + async def get_input_entity(self): + """ + Returns `input_entity` but will make an API call if necessary. + """ + # We don't actually have an API call we can make yet + # to get more info, but keep this method for consistency. + return self.input_entity + + @property + def text(self): + """ + The markdown text contained in the draft. It will be + empty if there is no text (and hence no draft is set). + """ + return self._text + + @property + def raw_text(self): + """ + The raw (text without formatting) contained in the draft. + It will be empty if there is no text (thus draft not set). + """ + return self._raw_text + + @property + def is_empty(self): + """ + Convenience bool to determine if the draft is empty or not. + """ + return not self._text + + async def set_message( + self, text=None, reply_to=0, parse_mode=(), + link_preview=None): + """ + Changes the draft message on the Telegram servers. The changes are + reflected in this object. + + :param str text: New text of the draft. + Preserved if left as None. + + :param int reply_to: Message ID to reply to. + Preserved if left as 0, erased if set to None. + + :param bool link_preview: Whether to attach a web page preview. + Preserved if left as None. + + :param str parse_mode: The parse mode to be used for the text. + :return bool: `True` on success. + """ + if text is None: + text = self._text + + if reply_to == 0: + reply_to = self.reply_to_msg_id + + if link_preview is None: + link_preview = self.link_preview + + raw_text, entities =\ + await self._client._parse_message_text(text, parse_mode) + + result = await self._client(SaveDraftRequest( + peer=self._peer, + message=raw_text, + no_webpage=not link_preview, + reply_to=None if reply_to is None else types.InputReplyToMessage(reply_to), + entities=entities + )) + + if result: + self._text = text + self._raw_text = raw_text + self.link_preview = link_preview + self.reply_to_msg_id = reply_to + self.date = datetime.datetime.now(tz=datetime.timezone.utc) + + return result + + async def send(self, clear=True, parse_mode=()): + """ + Sends the contents of this draft to the dialog. This is just a + wrapper around ``send_message(dialog.input_entity, *args, **kwargs)``. + """ + await self._client.send_message( + self._peer, self.text, reply_to=self.reply_to_msg_id, + link_preview=self.link_preview, parse_mode=parse_mode, + clear_draft=clear + ) + + async def delete(self): + """ + Deletes this draft, and returns `True` on success. + """ + return await self.set_message(text='') + + def to_dict(self): + try: + entity = self.entity + except RPCError as e: + entity = e + + return { + '_': 'Draft', + 'text': self.text, + 'entity': entity, + 'date': self.date, + 'link_preview': self.link_preview, + 'reply_to_msg_id': self.reply_to_msg_id + } + + def __str__(self): + return TLObject.pretty_format(self.to_dict()) + + def stringify(self): + return TLObject.pretty_format(self.to_dict(), indent=0) diff --git a/module/scripts/lib/telethon/tl/custom/file.py b/module/scripts/lib/telethon/tl/custom/file.py new file mode 100644 index 0000000..aa71482 --- /dev/null +++ b/module/scripts/lib/telethon/tl/custom/file.py @@ -0,0 +1,146 @@ +import mimetypes +import os + +from ... import utils +from ...tl import types + + +class File: + """ + Convenience class over media like photos or documents, which + supports accessing the attributes in a more convenient way. + + If any of the attributes are not present in the current media, + the properties will be `None`. + + The original media is available through the ``media`` attribute. + """ + def __init__(self, media): + self.media = media + + @property + def id(self): + """ + The old bot-API style ``file_id`` representing this file. + + .. warning:: + + This feature has not been maintained for a long time and + may not work. It will be removed in future versions. + + .. note:: + + This file ID may not work under user accounts, + but should still be usable by bot accounts. + + You can, however, still use it to identify + a file in for example a database. + """ + return utils.pack_bot_file_id(self.media) + + @property + def name(self): + """ + The file name of this document. + """ + return self._from_attr(types.DocumentAttributeFilename, 'file_name') + + @property + def ext(self): + """ + The extension from the mime type of this file. + + If the mime type is unknown, the extension + from the file name (if any) will be used. + """ + return ( + mimetypes.guess_extension(self.mime_type) + or os.path.splitext(self.name or '')[-1] + or None + ) + + @property + def mime_type(self): + """ + The mime-type of this file. + """ + if isinstance(self.media, types.Photo): + return 'image/jpeg' + elif isinstance(self.media, types.Document): + return self.media.mime_type + + @property + def width(self): + """ + The width in pixels of this media if it's a photo or a video. + """ + if isinstance(self.media, types.Photo): + return max(getattr(s, 'w', 0) for s in self.media.sizes) + + return self._from_attr(( + types.DocumentAttributeImageSize, types.DocumentAttributeVideo), 'w') + + @property + def height(self): + """ + The height in pixels of this media if it's a photo or a video. + """ + if isinstance(self.media, types.Photo): + return max(getattr(s, 'h', 0) for s in self.media.sizes) + + return self._from_attr(( + types.DocumentAttributeImageSize, types.DocumentAttributeVideo), 'h') + + @property + def duration(self): + """ + The duration in seconds of the audio or video. + """ + return self._from_attr(( + types.DocumentAttributeAudio, types.DocumentAttributeVideo), 'duration') + + @property + def title(self): + """ + The title of the song. + """ + return self._from_attr(types.DocumentAttributeAudio, 'title') + + @property + def performer(self): + """ + The performer of the song. + """ + return self._from_attr(types.DocumentAttributeAudio, 'performer') + + @property + def emoji(self): + """ + A string with all emoji that represent the current sticker. + """ + return self._from_attr(types.DocumentAttributeSticker, 'alt') + + @property + def sticker_set(self): + """ + The :tl:`InputStickerSet` to which the sticker file belongs. + """ + return self._from_attr(types.DocumentAttributeSticker, 'stickerset') + + @property + def size(self): + """ + The size in bytes of this file. + + For photos, this is the heaviest thumbnail, as it often repressents the largest dimensions. + """ + if isinstance(self.media, types.Photo): + return max(filter(None, map(utils._photo_size_byte_count, self.media.sizes)), default=None) + elif isinstance(self.media, types.Document): + return self.media.size + + def _from_attr(self, cls, field): + if isinstance(self.media, types.Document): + for attr in self.media.attributes: + if isinstance(attr, cls): + return getattr(attr, field, None) diff --git a/module/scripts/lib/telethon/tl/custom/forward.py b/module/scripts/lib/telethon/tl/custom/forward.py new file mode 100644 index 0000000..afe6b3a --- /dev/null +++ b/module/scripts/lib/telethon/tl/custom/forward.py @@ -0,0 +1,51 @@ +from .chatgetter import ChatGetter +from .sendergetter import SenderGetter +from ... import utils, helpers +from ...tl import types + + +class Forward(ChatGetter, SenderGetter): + """ + Custom class that encapsulates a :tl:`MessageFwdHeader` providing an + abstraction to easily access information like the original sender. + + Remember that this class implements `ChatGetter + ` and `SenderGetter + ` which means you + have access to all their sender and chat properties and methods. + + Attributes: + + original_fwd (:tl:`MessageFwdHeader`): + The original :tl:`MessageFwdHeader` instance. + + Any other attribute: + Attributes not described here are the same as those available + in the original :tl:`MessageFwdHeader`. + """ + def __init__(self, client, original, entities): + # Copy all the fields, not reference! It would cause memory cycles: + # self.original_fwd.original_fwd.original_fwd.original_fwd + # ...would be valid if we referenced. + self.__dict__.update(original.__dict__) + self.original_fwd = original + + sender_id = sender = input_sender = peer = chat = input_chat = None + if original.from_id: + ty = helpers._entity_type(original.from_id) + if ty == helpers._EntityType.USER: + sender_id = utils.get_peer_id(original.from_id) + sender, input_sender = utils._get_entity_pair( + sender_id, entities, client._mb_entity_cache) + + elif ty in (helpers._EntityType.CHAT, helpers._EntityType.CHANNEL): + peer = original.from_id + chat, input_chat = utils._get_entity_pair( + utils.get_peer_id(peer), entities, client._mb_entity_cache) + + # This call resets the client + ChatGetter.__init__(self, peer, chat=chat, input_chat=input_chat) + SenderGetter.__init__(self, sender_id, sender=sender, input_sender=input_sender) + self._client = client + + # TODO We could reload the message diff --git a/module/scripts/lib/telethon/tl/custom/inlinebuilder.py b/module/scripts/lib/telethon/tl/custom/inlinebuilder.py new file mode 100644 index 0000000..1bea134 --- /dev/null +++ b/module/scripts/lib/telethon/tl/custom/inlinebuilder.py @@ -0,0 +1,450 @@ +import hashlib + +from .. import functions, types +from ... import utils + +_TYPE_TO_MIMES = { + 'gif': ['image/gif'], # 'video/mp4' too, but that's used for video + 'article': ['text/html'], + 'audio': ['audio/mpeg'], + 'contact': [], + 'file': ['application/pdf', 'application/zip'], # actually any + 'geo': [], + 'photo': ['image/jpeg'], + 'sticker': ['image/webp', 'application/x-tgsticker'], + 'venue': [], + 'video': ['video/mp4'], # tdlib includes text/html for some reason + 'voice': ['audio/ogg'], +} + + +class InlineBuilder: + """ + Helper class to allow defining `InlineQuery + ` ``results``. + + Common arguments to all methods are + explained here to avoid repetition: + + text (`str`, optional): + If present, the user will send a text + message with this text upon being clicked. + + link_preview (`bool`, optional): + Whether to show a link preview in the sent + text message or not. + + geo (:tl:`InputGeoPoint`, :tl:`GeoPoint`, :tl:`InputMediaVenue`, :tl:`MessageMediaVenue`, optional): + If present, it may either be a geo point or a venue. + + period (int, optional): + The period in seconds to be used for geo points. + + contact (:tl:`InputMediaContact`, :tl:`MessageMediaContact`, optional): + If present, it must be the contact information to send. + + game (`bool`, optional): + May be `True` to indicate that the game will be sent. + + buttons (`list`, `custom.Button `, :tl:`KeyboardInlineButton`, optional): + Same as ``buttons`` for `client.send_message() + `. + + parse_mode (`str`, optional): + Same as ``parse_mode`` for `client.send_message() + `. + + id (`str`, optional): + The string ID to use for this result. If not present, it + will be the SHA256 hexadecimal digest of converting the + created :tl:`InputBotInlineResult` with empty ID to ``bytes()``, + so that the ID will be deterministic for the same input. + + .. note:: + + If two inputs are exactly the same, their IDs will be the same + too. If you send two articles with the same ID, it will raise + ``ResultIdDuplicateError``. Consider giving them an explicit + ID if you need to send two results that are the same. + """ + def __init__(self, client): + self._client = client + + # noinspection PyIncorrectDocstring + async def article( + self, title, description=None, + *, url=None, thumb=None, content=None, + id=None, text=None, parse_mode=(), link_preview=True, + geo=None, period=60, contact=None, game=False, buttons=None + ): + """ + Creates new inline result of article type. + + Args: + title (`str`): + The title to be shown for this result. + + description (`str`, optional): + Further explanation of what this result means. + + url (`str`, optional): + The URL to be shown for this result. + + thumb (:tl:`InputWebDocument`, optional): + The thumbnail to be shown for this result. + For now it has to be a :tl:`InputWebDocument` if present. + + content (:tl:`InputWebDocument`, optional): + The content to be shown for this result. + For now it has to be a :tl:`InputWebDocument` if present. + + Example: + .. code-block:: python + + results = [ + # Option with title and description sending a message. + builder.article( + title='First option', + description='This is the first option', + text='Text sent after clicking this option', + ), + # Option with title URL to be opened when clicked. + builder.article( + title='Second option', + url='https://example.com', + text='Text sent if the user clicks the option and not the URL', + ), + # Sending a message with buttons. + # You can use a list or a list of lists to include more buttons. + builder.article( + title='Third option', + text='Text sent with buttons below', + buttons=Button.url('https://example.com'), + ), + ] + """ + # TODO Does 'article' work always? + # article, photo, gif, mpeg4_gif, video, audio, + # voice, document, location, venue, contact, game + result = types.InputBotInlineResult( + id=id or '', + type='article', + send_message=await self._message( + text=text, parse_mode=parse_mode, link_preview=link_preview, + geo=geo, period=period, + contact=contact, + game=game, + buttons=buttons + ), + title=title, + description=description, + url=url, + thumb=thumb, + content=content + ) + if id is None: + result.id = hashlib.sha256(bytes(result)).hexdigest() + + return result + + # noinspection PyIncorrectDocstring + async def photo( + self, file, *, id=None, include_media=True, + text=None, parse_mode=(), link_preview=True, + geo=None, period=60, contact=None, game=False, buttons=None + ): + """ + Creates a new inline result of photo type. + + Args: + include_media (`bool`, optional): + Whether the photo file used to display the result should be + included in the message itself or not. By default, the photo + is included, and the text parameter alters the caption. + + file (`obj`, optional): + Same as ``file`` for `client.send_file() + `. + + Example: + .. code-block:: python + + results = [ + # Sending just the photo when the user selects it. + builder.photo('/path/to/photo.jpg'), + + # Including a caption with some in-memory photo. + photo_bytesio = ... + builder.photo( + photo_bytesio, + text='This will be the caption of the sent photo', + ), + + # Sending just the message without including the photo. + builder.photo( + photo, + text='This will be a normal text message', + include_media=False, + ), + ] + """ + try: + fh = utils.get_input_photo(file) + except TypeError: + _, media, _ = await self._client._file_to_media( + file, allow_cache=True, as_image=True + ) + if isinstance(media, types.InputPhoto): + fh = media + else: + r = await self._client(functions.messages.UploadMediaRequest( + types.InputPeerSelf(), media=media + )) + fh = utils.get_input_photo(r.photo) + + result = types.InputBotInlineResultPhoto( + id=id or '', + type='photo', + photo=fh, + send_message=await self._message( + text=text or '', + parse_mode=parse_mode, + link_preview=link_preview, + media=include_media, + geo=geo, + period=period, + contact=contact, + game=game, + buttons=buttons + ) + ) + if id is None: + result.id = hashlib.sha256(bytes(result)).hexdigest() + + return result + + # noinspection PyIncorrectDocstring + async def document( + self, file, title=None, *, description=None, type=None, + mime_type=None, attributes=None, force_document=False, + voice_note=False, video_note=False, use_cache=True, id=None, + text=None, parse_mode=(), link_preview=True, + geo=None, period=60, contact=None, game=False, buttons=None, + include_media=True + ): + """ + Creates a new inline result of document type. + + `use_cache`, `mime_type`, `attributes`, `force_document`, + `voice_note` and `video_note` are described in `client.send_file + `. + + Args: + file (`obj`): + Same as ``file`` for `client.send_file() + `. + + title (`str`, optional): + The title to be shown for this result. + + description (`str`, optional): + Further explanation of what this result means. + + type (`str`, optional): + The type of the document. May be one of: article, audio, + contact, file, geo, gif, photo, sticker, venue, video, voice. + It will be automatically set if ``mime_type`` is specified, + and default to ``'file'`` if no matching mime type is found. + you may need to pass ``attributes`` in order to use ``type`` + effectively. + + attributes (`list`, optional): + Optional attributes that override the inferred ones, like + :tl:`DocumentAttributeFilename` and so on. + + include_media (`bool`, optional): + Whether the document file used to display the result should be + included in the message itself or not. By default, the document + is included, and the text parameter alters the caption. + + Example: + .. code-block:: python + + results = [ + # Sending just the file when the user selects it. + builder.document('/path/to/file.pdf'), + + # Including a caption with some in-memory file. + file_bytesio = ... + builder.document( + file_bytesio, + text='This will be the caption of the sent file', + ), + + # Sending just the message without including the file. + builder.document( + photo, + text='This will be a normal text message', + include_media=False, + ), + ] + """ + if type is None: + if voice_note: + type = 'voice' + elif mime_type: + for ty, mimes in _TYPE_TO_MIMES.items(): + for mime in mimes: + if mime_type == mime: + type = ty + break + + if type is None: + type = 'file' + + try: + fh = utils.get_input_document(file) + except TypeError: + _, media, _ = await self._client._file_to_media( + file, + mime_type=mime_type, + attributes=attributes, + force_document=force_document, + voice_note=voice_note, + video_note=video_note, + allow_cache=use_cache + ) + if isinstance(media, types.InputDocument): + fh = media + else: + r = await self._client(functions.messages.UploadMediaRequest( + types.InputPeerSelf(), media=media + )) + fh = utils.get_input_document(r.document) + + result = types.InputBotInlineResultDocument( + id=id or '', + type=type, + document=fh, + send_message=await self._message( + # Empty string for text if there's media but text is None. + # We may want to display a document but send text; however + # default to sending the media (without text, i.e. stickers). + text=text or '', + parse_mode=parse_mode, + link_preview=link_preview, + media=include_media, + geo=geo, + period=period, + contact=contact, + game=game, + buttons=buttons + ), + title=title, + description=description + ) + if id is None: + result.id = hashlib.sha256(bytes(result)).hexdigest() + + return result + + # noinspection PyIncorrectDocstring + async def game( + self, short_name, *, id=None, + text=None, parse_mode=(), link_preview=True, + geo=None, period=60, contact=None, game=False, buttons=None + ): + """ + Creates a new inline result of game type. + + Args: + short_name (`str`): + The short name of the game to use. + """ + result = types.InputBotInlineResultGame( + id=id or '', + short_name=short_name, + send_message=await self._message( + text=text, parse_mode=parse_mode, link_preview=link_preview, + geo=geo, period=period, + contact=contact, + game=game, + buttons=buttons + ) + ) + if id is None: + result.id = hashlib.sha256(bytes(result)).hexdigest() + + return result + + async def _message( + self, *, + text=None, parse_mode=(), link_preview=True, media=False, + geo=None, period=60, contact=None, game=False, buttons=None + ): + # Empty strings are valid but false-y; if they're empty use dummy '\0' + args = ('\0' if text == '' else text, geo, contact, game) + if sum(1 for x in args if x is not None and x is not False) != 1: + raise ValueError( + 'Must set exactly one of text, geo, contact or game (set {})' + .format(', '.join(x[0] for x in zip( + 'text geo contact game'.split(), args) if x[1]) or 'none') + ) + + markup = self._client.build_reply_markup(buttons) + if text is not None: + text, msg_entities = await self._client._parse_message_text( + text, parse_mode + ) + if media: + # "MediaAuto" means it will use whatever media the inline + # result itself has (stickers, photos, or documents), while + # respecting the user's text (caption) and formatting. + return types.InputBotInlineMessageMediaAuto( + message=text, + entities=msg_entities, + reply_markup=markup + ) + else: + return types.InputBotInlineMessageText( + message=text, + no_webpage=not link_preview, + entities=msg_entities, + reply_markup=markup + ) + elif isinstance(geo, (types.InputGeoPoint, types.GeoPoint)): + return types.InputBotInlineMessageMediaGeo( + geo_point=utils.get_input_geo(geo), + period=period, + reply_markup=markup + ) + elif isinstance(geo, (types.InputMediaVenue, types.MessageMediaVenue)): + if isinstance(geo, types.InputMediaVenue): + geo_point = geo.geo_point + else: + geo_point = geo.geo + + return types.InputBotInlineMessageMediaVenue( + geo_point=geo_point, + title=geo.title, + address=geo.address, + provider=geo.provider, + venue_id=geo.venue_id, + venue_type=geo.venue_type, + reply_markup=markup + ) + elif isinstance(contact, ( + types.InputMediaContact, types.MessageMediaContact)): + return types.InputBotInlineMessageMediaContact( + phone_number=contact.phone_number, + first_name=contact.first_name, + last_name=contact.last_name, + vcard=contact.vcard, + reply_markup=markup + ) + elif game: + return types.InputBotInlineMessageGame( + reply_markup=markup + ) + else: + raise ValueError('No text, game or valid geo or contact given') diff --git a/module/scripts/lib/telethon/tl/custom/inlineresult.py b/module/scripts/lib/telethon/tl/custom/inlineresult.py new file mode 100644 index 0000000..d56b2fe --- /dev/null +++ b/module/scripts/lib/telethon/tl/custom/inlineresult.py @@ -0,0 +1,176 @@ +from .. import types, functions +from ... import utils + + +class InlineResult: + """ + Custom class that encapsulates a bot inline result providing + an abstraction to easily access some commonly needed features + (such as clicking a result to select it). + + Attributes: + + result (:tl:`BotInlineResult`): + The original :tl:`BotInlineResult` object. + """ + # tdlib types are the following (InlineQueriesManager::answer_inline_query @ 1a4a834): + # gif, article, audio, contact, file, geo, photo, sticker, venue, video, voice + # + # However, those documented in https://core.telegram.org/bots/api#inline-mode are different. + ARTICLE = 'article' + PHOTO = 'photo' + GIF = 'gif' + VIDEO = 'video' + VIDEO_GIF = 'mpeg4_gif' + AUDIO = 'audio' + DOCUMENT = 'document' + LOCATION = 'location' + VENUE = 'venue' + CONTACT = 'contact' + GAME = 'game' + + def __init__(self, client, original, query_id=None, *, entity=None): + self._client = client + self.result = original + self._query_id = query_id + self._entity = entity + + @property + def type(self): + """ + The always-present type of this result. It will be one of: + ``'article'``, ``'photo'``, ``'gif'``, ``'mpeg4_gif'``, ``'video'``, + ``'audio'``, ``'voice'``, ``'document'``, ``'location'``, ``'venue'``, + ``'contact'``, ``'game'``. + + You can access all of these constants through `InlineResult`, + such as `InlineResult.ARTICLE`, `InlineResult.VIDEO_GIF`, etc. + """ + return self.result.type + + @property + def message(self): + """ + The always-present :tl:`BotInlineMessage` that + will be sent if `click` is called on this result. + """ + return self.result.send_message + + @property + def title(self): + """ + The title for this inline result. It may be `None`. + """ + return self.result.title + + @property + def description(self): + """ + The description for this inline result. It may be `None`. + """ + return self.result.description + + @property + def url(self): + """ + The URL present in this inline results. If you want to "click" + this URL to open it in your browser, you should use Python's + `webbrowser.open(url)` for such task. + """ + if isinstance(self.result, types.BotInlineResult): + return self.result.url + + @property + def photo(self): + """ + Returns either the :tl:`WebDocument` thumbnail for + normal results or the :tl:`Photo` for media results. + """ + if isinstance(self.result, types.BotInlineResult): + return self.result.thumb + elif isinstance(self.result, types.BotInlineMediaResult): + return self.result.photo + + @property + def document(self): + """ + Returns either the :tl:`WebDocument` content for + normal results or the :tl:`Document` for media results. + """ + if isinstance(self.result, types.BotInlineResult): + return self.result.content + elif isinstance(self.result, types.BotInlineMediaResult): + return self.result.document + + async def click(self, entity=None, reply_to=None, comment_to=None, + silent=False, clear_draft=False, hide_via=False, + background=None): + """ + Clicks this result and sends the associated `message`. + + Args: + entity (`entity`): + The entity to which the message of this result should be sent. + + reply_to (`int` | `Message `, optional): + If present, the sent message will reply to this ID or message. + + comment_to (`int` | `Message `, optional): + Similar to ``reply_to``, but replies in the linked group of a + broadcast channel instead (effectively leaving a "comment to" + the specified message). + + silent (`bool`, optional): + Whether the message should notify people with sound or not. + Defaults to `False` (send with a notification sound unless + the person has the chat muted). Set it to `True` to alter + this behaviour. + + clear_draft (`bool`, optional): + Whether the draft should be removed after sending the + message from this result or not. Defaults to `False`. + + hide_via (`bool`, optional): + Whether the "via @bot" should be hidden or not. + Only works with certain bots (like @bing or @gif). + + background (`bool`, optional): + Whether the message should be send in background. + + """ + if entity: + entity = await self._client.get_input_entity(entity) + elif self._entity: + entity = self._entity + else: + raise ValueError('You must provide the entity where the result should be sent to') + + if comment_to: + entity, reply_id = await self._client._get_comment_data(entity, comment_to) + else: + reply_id = None if reply_to is None else utils.get_message_id(reply_to) + + req = functions.messages.SendInlineBotResultRequest( + peer=entity, + query_id=self._query_id, + id=self.result.id, + silent=silent, + background=background, + clear_draft=clear_draft, + hide_via=hide_via, + reply_to=None if reply_id is None else types.InputReplyToMessage(reply_id) + ) + return self._client._get_response_message( + req, await self._client(req), entity) + + async def download_media(self, *args, **kwargs): + """ + Downloads the media in this result (if there is a document, the + document will be downloaded; otherwise, the photo will if present). + + This is a wrapper around `client.download_media + `. + """ + if self.document or self.photo: + return await self._client.download_media( + self.document or self.photo, *args, **kwargs) diff --git a/module/scripts/lib/telethon/tl/custom/inlineresults.py b/module/scripts/lib/telethon/tl/custom/inlineresults.py new file mode 100644 index 0000000..9ab0c0a --- /dev/null +++ b/module/scripts/lib/telethon/tl/custom/inlineresults.py @@ -0,0 +1,83 @@ +import time + +from .inlineresult import InlineResult + + +class InlineResults(list): + """ + Custom class that encapsulates :tl:`BotResults` providing + an abstraction to easily access some commonly needed features + (such as clicking one of the results to select it) + + Note that this is a list of `InlineResult + ` + so you can iterate over it or use indices to + access its elements. In addition, it has some + attributes. + + Attributes: + result (:tl:`BotResults`): + The original :tl:`BotResults` object. + + query_id (`int`): + The random ID that identifies this query. + + cache_time (`int`): + For how long the results should be considered + valid. You can call `results_valid` at any + moment to determine if the results are still + valid or not. + + users (:tl:`User`): + The users present in this inline query. + + gallery (`bool`): + Whether these results should be presented + in a grid (as a gallery of images) or not. + + next_offset (`str`, optional): + The string to be used as an offset to get + the next chunk of results, if any. + + switch_pm (:tl:`InlineBotSwitchPM`, optional): + If presents, the results should show a button to + switch to a private conversation with the bot using + the text in this object. + """ + def __init__(self, client, original, *, entity=None): + super().__init__(InlineResult(client, x, original.query_id, entity=entity) + for x in original.results) + + self.result = original + self.query_id = original.query_id + self.cache_time = original.cache_time + self._valid_until = time.time() + self.cache_time + self.users = original.users + self.gallery = bool(original.gallery) + self.next_offset = original.next_offset + self.switch_pm = original.switch_pm + + def results_valid(self): + """ + Returns `True` if the cache time has not expired + yet and the results can still be considered valid. + """ + return time.time() < self._valid_until + + def _to_str(self, item_function): + return ('[{}, query_id={}, cache_time={}, users={}, gallery={}, ' + 'next_offset={}, switch_pm={}]'.format( + ', '.join(item_function(x) for x in self), + self.query_id, + self.cache_time, + self.users, + self.gallery, + self.next_offset, + self.switch_pm + )) + + def __str__(self): + return self._to_str(str) + + def __repr__(self): + return self._to_str(repr) diff --git a/module/scripts/lib/telethon/tl/custom/inputsizedfile.py b/module/scripts/lib/telethon/tl/custom/inputsizedfile.py new file mode 100644 index 0000000..fcb743f --- /dev/null +++ b/module/scripts/lib/telethon/tl/custom/inputsizedfile.py @@ -0,0 +1,9 @@ +from ..types import InputFile + + +class InputSizedFile(InputFile): + """InputFile class with two extra parameters: md5 (digest) and size""" + def __init__(self, id_, parts, name, md5, size): + super().__init__(id_, parts, name, md5.hexdigest()) + self.md5 = md5.digest() + self.size = size diff --git a/module/scripts/lib/telethon/tl/custom/message.py b/module/scripts/lib/telethon/tl/custom/message.py new file mode 100644 index 0000000..b490df9 --- /dev/null +++ b/module/scripts/lib/telethon/tl/custom/message.py @@ -0,0 +1,1248 @@ +from typing import Optional, List, TYPE_CHECKING +from datetime import datetime +from .chatgetter import ChatGetter +from .sendergetter import SenderGetter +from .messagebutton import MessageButton +from .forward import Forward +from .file import File +from .. import TLObject, types, functions, alltlobjects +from ... import utils, errors + + +# TODO Figure out a way to have the code generator error on missing fields +# Maybe parsing the init function alone if that's possible. +class Message(ChatGetter, SenderGetter, TLObject): + """ + This custom class aggregates both :tl:`Message` and + :tl:`MessageService` to ease accessing their members. + + Remember that this class implements `ChatGetter + ` and `SenderGetter + ` which means you + have access to all their sender and chat properties and methods. + + Members: + out (`bool`): + Whether the message is outgoing (i.e. you sent it from + another session) or incoming (i.e. someone else sent it). + + Note that messages in your own chat are always incoming, + but this member will be `True` if you send a message + to your own chat. Messages you forward to your chat are + *not* considered outgoing, just like official clients + display them. + + mentioned (`bool`): + Whether you were mentioned in this message or not. + Note that replies to your own messages also count + as mentions. + + media_unread (`bool`): + Whether you have read the media in this message + or not, e.g. listened to the voice note media. + + silent (`bool`): + Whether the message should notify people with sound or not. + Previously used in channels, but since 9 August 2019, it can + also be `used in private chats + `_. + + post (`bool`): + Whether this message is a post in a broadcast + channel or not. + + from_scheduled (`bool`): + Whether this message was originated from a previously-scheduled + message or not. + + legacy (`bool`): + Whether this is a legacy message or not. + + edit_hide (`bool`): + Whether the edited mark of this message is edited + should be hidden (e.g. in GUI clients) or shown. + + pinned (`bool`): + Whether this message is currently pinned or not. + + noforwards (`bool`): + Whether this message can be forwarded or not. + + invert_media (`bool`): + Whether the media in this message should be inverted. + + offline (`bool`): + Whether the message was sent by an implicit action, for example, as an away or a greeting business message, or as a scheduled message. + + id (`int`): + The ID of this message. This field is *always* present. + Any other member is optional and may be `None`. + + from_id (:tl:`Peer`): + The peer who sent this message, which is either + :tl:`PeerUser`, :tl:`PeerChat` or :tl:`PeerChannel`. + This value will be `None` for anonymous messages. + + peer_id (:tl:`Peer`): + The peer to which this message was sent, which is either + :tl:`PeerUser`, :tl:`PeerChat` or :tl:`PeerChannel`. This + will always be present except for empty messages. + + fwd_from (:tl:`MessageFwdHeader`): + The original forward header if this message is a forward. + You should probably use the `forward` property instead. + + via_bot_id (`int`): + The ID of the bot used to send this message + through its inline mode (e.g. "via @like"). + + reply_to (:tl:`MessageReplyHeader` | :tl:`MessageReplyStoryHeader`): + The original reply header if this message is replying to another. + + date (`datetime`): + The UTC+0 `datetime` object indicating when this message + was sent. This will always be present except for empty + messages. + + message (`str`): + The string text of the message for `Message + ` instances, + which will be `None` for other types of messages. + + media (:tl:`MessageMedia`): + The media sent with this message if any (such as + photos, videos, documents, gifs, stickers, etc.). + + You may want to access the `photo`, `document` + etc. properties instead. + + If the media was not present or it was :tl:`MessageMediaEmpty`, + this member will instead be `None` for convenience. + + reply_markup (:tl:`ReplyMarkup`): + The reply markup for this message (which was sent + either via a bot or by a bot). You probably want + to access `buttons` instead. + + entities (List[:tl:`MessageEntity`]): + The list of markup entities in this message, + such as bold, italics, code, hyperlinks, etc. + + views (`int`): + The number of views this message from a broadcast + channel has. This is also present in forwards. + + forwards (`int`): + The number of times this message has been forwarded. + + replies (`int`): + The number of times another message has replied to this message. + + edit_date (`datetime`): + The date when this message was last edited. + + post_author (`str`): + The display name of the message sender to + show in messages sent to broadcast channels. + + grouped_id (`int`): + If this message belongs to a group of messages + (photo albums or video albums), all of them will + have the same value here. + + reactions (:tl:`MessageReactions`) + Reactions to this message. + + restriction_reason (List[:tl:`RestrictionReason`]) + An optional list of reasons why this message was restricted. + If the list is `None`, this message has not been restricted. + + ttl_period (`int`): + The Time To Live period configured for this message. + The message should be erased from wherever it's stored (memory, a + local database, etc.) when + ``datetime.now() > message.date + timedelta(seconds=message.ttl_period)``. + + action (:tl:`MessageAction`): + The message action object of the message for :tl:`MessageService` + instances, which will be `None` for other types of messages. + + saved_peer_id (:tl:`Peer`) + """ + + # region Initialization + + def __init__( + self, + id: int, + peer_id: types.TypePeer, + date: Optional[datetime] = None, + message: Optional[str] = None, + # Copied from Message.__init__ signature + out: Optional[bool] = None, + mentioned: Optional[bool] = None, + media_unread: Optional[bool] = None, + silent: Optional[bool] = None, + post: Optional[bool] = None, + from_scheduled: Optional[bool] = None, + legacy: Optional[bool] = None, + edit_hide: Optional[bool] = None, + pinned: Optional[bool] = None, + noforwards: Optional[bool] = None, + invert_media: Optional[bool] = None, + offline: Optional[bool] = None, + video_processing_pending: Optional[bool] = None, + paid_suggested_post_stars: Optional[bool] = None, + paid_suggested_post_ton: Optional[bool] = None, + from_id: Optional[types.TypePeer] = None, + from_boosts_applied: Optional[int] = None, + from_rank: Optional[str] = None, + saved_peer_id: Optional[types.TypePeer] = None, + fwd_from: Optional[types.TypeMessageFwdHeader] = None, + via_bot_id: Optional[int] = None, + via_business_bot_id: Optional[int] = None, + guestchat_via_from: Optional[types.TypePeer] = None, + reply_to: Optional[types.TypeMessageReplyHeader] = None, + media: Optional[types.TypeMessageMedia] = None, + reply_markup: Optional[types.TypeReplyMarkup] = None, + entities: Optional[List[types.TypeMessageEntity]] = None, + views: Optional[int] = None, + forwards: Optional[int] = None, + replies: Optional[types.TypeMessageReplies] = None, + edit_date: Optional[datetime] = None, + post_author: Optional[str] = None, + grouped_id: Optional[int] = None, + reactions: Optional[types.TypeMessageReactions] = None, + restriction_reason: Optional[List[types.TypeRestrictionReason]] = None, + ttl_period: Optional[int] = None, + quick_reply_shortcut_id: Optional[int] = None, + effect: Optional[int] = None, + factcheck: Optional[types.TypeFactCheck] = None, + report_delivery_until_date: Optional[datetime] = None, + paid_message_stars: Optional[int] = None, + suggested_post: Optional[types.TypeSuggestedPost] = None, + schedule_repeat_period: Optional[int] = None, + summary_from_language: Optional[str] = None, + rich_message: Optional[types.RichMessage] = None, + # Copied from MessageService.__init__ signature + action: Optional[types.TypeMessageAction] = None, + reactions_are_possible: Optional[bool] = None, + ): + # Copied from Message.__init__ body + self.id = id + self.peer_id = peer_id + self.date = date + self.message = message + self.out = bool(out) + self.mentioned = mentioned + self.media_unread = media_unread + self.silent = silent + self.post = post + self.from_scheduled = from_scheduled + self.legacy = legacy + self.edit_hide = edit_hide + self.pinned = pinned + self.noforwards = noforwards + self.invert_media = invert_media + self.offline = offline + self.video_processing_pending = video_processing_pending + self.paid_suggested_post_stars = paid_suggested_post_stars + self.paid_suggested_post_ton = paid_suggested_post_ton + self.from_id = from_id + self.from_boosts_applied = from_boosts_applied + self.from_rank = from_rank + self.saved_peer_id = saved_peer_id + self.fwd_from = fwd_from + self.via_bot_id = via_bot_id + self.via_business_bot_id = via_business_bot_id + self.guestchat_via_from = guestchat_via_from + self.reply_to = reply_to + self.media = None if isinstance(media, types.MessageMediaEmpty) else media + self.reply_markup = reply_markup + self.entities = entities + self.views = views + self.forwards = forwards + self.replies = replies + self.edit_date = edit_date + self.post_author = post_author + self.grouped_id = grouped_id + self.reactions = reactions + self.restriction_reason = restriction_reason + self.ttl_period = ttl_period + self.quick_reply_shortcut_id = quick_reply_shortcut_id + self.effect = effect + self.factcheck = factcheck + self.report_delivery_until_date = report_delivery_until_date + self.paid_message_stars = paid_message_stars + self.suggested_post = suggested_post + self.schedule_repeat_period = schedule_repeat_period + self.summary_from_language = summary_from_language + self.rich_message = rich_message + # Copied from MessageService.__init__ body + self.action = action + self.reactions_are_possible = reactions_are_possible + + # Convenient storage for custom functions + # TODO This is becoming a bit of bloat + self._client = None + self._text = None + self._file = None + self._reply_message = None + self._buttons = None + self._buttons_flat = None + self._buttons_count = None + self._via_bot = None + self._via_input_bot = None + self._action_entities = None + self._linked_chat = None + + sender_id = None + if from_id is not None: + sender_id = utils.get_peer_id(from_id) + elif peer_id: + # If the message comes from a Channel, let the sender be it + # ...or... + # incoming messages in private conversations no longer have from_id + # (layer 119+), but the sender can only be the chat we're in. + if post or (not out and isinstance(peer_id, types.PeerUser)): + sender_id = utils.get_peer_id(peer_id) + + # Note that these calls would reset the client + ChatGetter.__init__(self, peer_id, broadcast=post) + SenderGetter.__init__(self, sender_id) + + self._forward = None + self._reply_to_chat = None + self._reply_to_sender = None + + def _finish_init(self, client, entities, input_chat): + """ + Finishes the initialization of this message by setting + the client that sent the message and making use of the + known entities. + """ + self._client = client + + # Make messages sent to ourselves outgoing unless they're forwarded. + # This makes it consistent with official client's appearance. + if self.peer_id == types.PeerUser(client._self_id) and not self.fwd_from: + self.out = True + + cache = client._mb_entity_cache + + self._sender, self._input_sender = utils._get_entity_pair( + self.sender_id, entities, cache) + + self._chat, self._input_chat = utils._get_entity_pair( + self.chat_id, entities, cache) + + if input_chat: # This has priority + self._input_chat = input_chat + + if self.via_bot_id: + self._via_bot, self._via_input_bot = utils._get_entity_pair( + self.via_bot_id, entities, cache) + + if self.fwd_from: + self._forward = Forward(self._client, self.fwd_from, entities) + + if self.action: + if isinstance(self.action, (types.MessageActionChatAddUser, + types.MessageActionChatCreate)): + self._action_entities = [entities.get(i) + for i in self.action.users] + elif isinstance(self.action, types.MessageActionChatDeleteUser): + self._action_entities = [entities.get(self.action.user_id)] + elif isinstance(self.action, types.MessageActionChatJoinedByLink): + self._action_entities = [entities.get(self.action.inviter_id)] + elif isinstance(self.action, types.MessageActionChatMigrateTo): + self._action_entities = [entities.get(utils.get_peer_id( + types.PeerChannel(self.action.channel_id)))] + elif isinstance( + self.action, types.MessageActionChannelMigrateFrom): + self._action_entities = [entities.get(utils.get_peer_id( + types.PeerChat(self.action.chat_id)))] + + if self.replies and self.replies.channel_id: + self._linked_chat = entities.get(utils.get_peer_id( + types.PeerChannel(self.replies.channel_id))) + + if isinstance(self.reply_to, types.MessageReplyHeader): + if self.reply_to.reply_to_peer_id: + self._reply_to_chat = entities.get(utils.get_peer_id(self.reply_to.reply_to_peer_id)) + if self.reply_to.reply_from: + if self.reply_to.reply_from.from_id: + self._reply_to_sender = entities.get(utils.get_peer_id(self.reply_to.reply_from.from_id)) + + + + # endregion Initialization + + # region Public Properties + + @property + def client(self): + """ + Returns the `TelegramClient ` + that *patched* this message. This will only be present if you + **use the friendly methods**, it won't be there if you invoke + raw API methods manually, in which case you should only access + members, not properties. + """ + return self._client + + @property + def text(self): + """ + The message text, formatted using the client's default + parse mode. Will be `None` for :tl:`MessageService`. + """ + if self._text is None and self._client: + if not self._client.parse_mode: + self._text = self.message + else: + self._text = self._client.parse_mode.unparse( + self.message, self.entities) + + return self._text + + @text.setter + def text(self, value): + self._text = value + if self._client and self._client.parse_mode: + self.message, self.entities = self._client.parse_mode.parse(value) + else: + self.message, self.entities = value, [] + + @property + def raw_text(self): + """ + The raw message text, ignoring any formatting. + Will be `None` for :tl:`MessageService`. + + Setting a value to this field will erase the + `entities`, unlike changing the `message` member. + """ + return self.message + + @raw_text.setter + def raw_text(self, value): + self.message = value + self.entities = [] + self._text = None + + @property + def is_reply(self): + """ + `True` if the message is a reply to some other message or story. + + Remember that if the replied-to is a message, + you can access the ID of the message this one is + replying to through `reply_to.reply_to_msg_id`, + and the `Message` object with `get_reply_message()`. + """ + return self.reply_to is not None + + @property + def forward(self): + """ + The `Forward ` + information if this message is a forwarded message. + """ + return self._forward + + @property + def reply_to_chat(self): + """ + The :tl:`Channel` in which the replied-to message was sent, + if this message is a reply in another chat + """ + return self._reply_to_chat + + @property + def reply_to_sender(self): + """ + The :tl:`User`, :tl:`Channel`, or whatever other entity that + sent the replied-to message, if this message is a reply in another chat. + """ + return self._reply_to_sender + + @property + def buttons(self): + """ + Returns a list of lists of `MessageButton + `, + if any. + + Otherwise, it returns `None`. + """ + if self._buttons is None and self.reply_markup: + if not self.input_chat: + return + try: + bot = self._needed_markup_bot() + except ValueError: + return + else: + self._set_buttons(self._input_chat, bot) + + return self._buttons + + async def get_buttons(self): + """ + Returns `buttons` when that property fails (this is rarely needed). + """ + if not self.buttons and self.reply_markup: + chat = await self.get_input_chat() + if not chat: + return + try: + bot = self._needed_markup_bot() + except ValueError: + await self._reload_message() + bot = self._needed_markup_bot() # TODO use via_input_bot + + self._set_buttons(chat, bot) + + return self._buttons + + @property + def button_count(self): + """ + Returns the total button count (sum of all `buttons` rows). + """ + if self._buttons_count is None: + if isinstance(self.reply_markup, ( + types.ReplyInlineMarkup, types.ReplyKeyboardMarkup)): + self._buttons_count = sum( + len(row.buttons) for row in self.reply_markup.rows) + else: + self._buttons_count = 0 + + return self._buttons_count + + @property + def file(self): + """ + Returns a `File ` wrapping the + `photo` or `document` in this message. If the media type is different + (polls, games, none, etc.), this property will be `None`. + + This instance lets you easily access other properties, such as + `file.id `, + `file.name `, + etc., without having to manually inspect the ``document.attributes``. + """ + if not self._file: + media = self.photo or self.document + if media: + self._file = File(media) + + return self._file + + @property + def photo(self): + """ + The :tl:`Photo` media in this message, if any. + + This will also return the photo for :tl:`MessageService` if its + action is :tl:`MessageActionChatEditPhoto`, or if the message has + a web preview with a photo. + """ + if isinstance(self.media, types.MessageMediaPhoto): + if isinstance(self.media.photo, types.Photo): + return self.media.photo + elif isinstance(self.action, types.MessageActionChatEditPhoto): + return self.action.photo + else: + web = self.web_preview + if web and isinstance(web.photo, types.Photo): + return web.photo + + @property + def document(self): + """ + The :tl:`Document` media in this message, if any. + """ + if isinstance(self.media, types.MessageMediaDocument): + if isinstance(self.media.document, types.Document): + return self.media.document + else: + web = self.web_preview + if web and isinstance(web.document, types.Document): + return web.document + + @property + def web_preview(self): + """ + The :tl:`WebPage` media in this message, if any. + """ + if isinstance(self.media, types.MessageMediaWebPage): + if isinstance(self.media.webpage, types.WebPage): + return self.media.webpage + + @property + def audio(self): + """ + The :tl:`Document` media in this message, if it's an audio file. + """ + return self._document_by_attribute(types.DocumentAttributeAudio, + lambda attr: not attr.voice) + + @property + def voice(self): + """ + The :tl:`Document` media in this message, if it's a voice note. + """ + return self._document_by_attribute(types.DocumentAttributeAudio, + lambda attr: attr.voice) + + @property + def video(self): + """ + The :tl:`Document` media in this message, if it's a video. + """ + return self._document_by_attribute(types.DocumentAttributeVideo) + + @property + def video_note(self): + """ + The :tl:`Document` media in this message, if it's a video note. + """ + return self._document_by_attribute(types.DocumentAttributeVideo, + lambda attr: attr.round_message) + + @property + def gif(self): + """ + The :tl:`Document` media in this message, if it's a "gif". + + "Gif" files by Telegram are normally ``.mp4`` video files without + sound, the so called "animated" media. However, it may be the actual + gif format if the file is too large. + """ + return self._document_by_attribute(types.DocumentAttributeAnimated) + + @property + def sticker(self): + """ + The :tl:`Document` media in this message, if it's a sticker. + """ + return self._document_by_attribute(types.DocumentAttributeSticker) + + @property + def contact(self): + """ + The :tl:`MessageMediaContact` in this message, if it's a contact. + """ + if isinstance(self.media, types.MessageMediaContact): + return self.media + + @property + def game(self): + """ + The :tl:`Game` media in this message, if it's a game. + """ + if isinstance(self.media, types.MessageMediaGame): + return self.media.game + + @property + def geo(self): + """ + The :tl:`GeoPoint` media in this message, if it has a location. + """ + if isinstance(self.media, (types.MessageMediaGeo, + types.MessageMediaGeoLive, + types.MessageMediaVenue)): + return self.media.geo + + @property + def invoice(self): + """ + The :tl:`MessageMediaInvoice` in this message, if it's an invoice. + """ + if isinstance(self.media, types.MessageMediaInvoice): + return self.media + + @property + def poll(self): + """ + The :tl:`MessageMediaPoll` in this message, if it's a poll. + """ + if isinstance(self.media, types.MessageMediaPoll): + return self.media + + @property + def venue(self): + """ + The :tl:`MessageMediaVenue` in this message, if it's a venue. + """ + if isinstance(self.media, types.MessageMediaVenue): + return self.media + + @property + def dice(self): + """ + The :tl:`MessageMediaDice` in this message, if it's a dice roll. + """ + if isinstance(self.media, types.MessageMediaDice): + return self.media + + @property + def action_entities(self): + """ + Returns a list of entities that took part in this action. + + Possible cases for this are :tl:`MessageActionChatAddUser`, + :tl:`types.MessageActionChatCreate`, :tl:`MessageActionChatDeleteUser`, + :tl:`MessageActionChatJoinedByLink` :tl:`MessageActionChatMigrateTo` + and :tl:`MessageActionChannelMigrateFrom`. + + If the action is neither of those, the result will be `None`. + If some entities could not be retrieved, the list may contain + some `None` items in it. + """ + return self._action_entities + + @property + def via_bot(self): + """ + The bot :tl:`User` if the message was sent via said bot. + + This will only be present if `via_bot_id` is not `None` and + the entity is known. + """ + return self._via_bot + + @property + def via_input_bot(self): + """ + Returns the input variant of `via_bot`. + """ + return self._via_input_bot + + @property + def reply_to_msg_id(self): + """ + Returns the message ID this message is replying to, if any. + This is equivalent to accessing ``.reply_to.reply_to_msg_id``. + """ + return ( + self.reply_to.reply_to_msg_id + if isinstance(self.reply_to, types.MessageReplyHeader) + else None + ) + + @property + def to_id(self): + """ + Returns the peer to which this message was sent to. This used to exist + to infer the ``.peer_id``. + """ + # If the client wasn't set we can't emulate the behaviour correctly, + # so as a best-effort simply return the chat peer. + if self._client and not self.out and self.is_private: + return types.PeerUser(self._client._self_id) + + return self.peer_id + + # endregion Public Properties + + # region Public Methods + + def get_entities_text(self, cls=None): + """ + Returns a list of ``(markup entity, inner text)`` + (like bold or italics). + + The markup entity is a :tl:`MessageEntity` that represents bold, + italics, etc., and the inner text is the `str` inside that markup + entity. + + For example: + + .. code-block:: python + + print(repr(message.text)) # shows: 'Hello **world**!' + + for ent, txt in message.get_entities_text(): + print(ent) # shows: MessageEntityBold(offset=6, length=5) + print(txt) # shows: world + + Args: + cls (`type`): + Returns entities matching this type only. For example, + the following will print the text for all ``code`` entities: + + >>> from telethon.tl.types import MessageEntityCode + >>> + >>> m = ... # get the message + >>> for _, inner_text in m.get_entities_text(MessageEntityCode): + >>> print(inner_text) + """ + ent = self.entities + if not ent: + return [] + + if cls: + ent = [c for c in ent if isinstance(c, cls)] + + texts = utils.get_inner_text(self.message, ent) + return list(zip(ent, texts)) + + async def get_reply_message(self): + """ + The `Message` that this message is replying to, or `None`. + + The result will be cached after its first use. + """ + if self._reply_message is None and self._client: + if not isinstance(self.reply_to, types.MessageReplyHeader): + return None + + # Bots cannot access other bots' messages by their ID. + # However they can access them through replies... + self._reply_message = await self._client.get_messages( + await self.get_input_chat() if self.is_channel else None, + ids=types.InputMessageReplyTo(self.id) + ) + if not self._reply_message: + # ...unless the current message got deleted. + # + # If that's the case, give it a second chance accessing + # directly by its ID. + self._reply_message = await self._client.get_messages( + self._input_chat if self.is_channel else None, + ids=self.reply_to.reply_to_msg_id + ) + + return self._reply_message + + async def respond(self, *args, **kwargs): + """ + Responds to the message (not as a reply). Shorthand for + `telethon.client.messages.MessageMethods.send_message` + with ``entity`` already set. + """ + if self._client: + return await self._client.send_message( + await self.get_input_chat(), *args, **kwargs) + + async def reply(self, *args, **kwargs): + """ + Replies to the message (as a reply). Shorthand for + `telethon.client.messages.MessageMethods.send_message` + with both ``entity`` and ``reply_to`` already set. + """ + if self._client: + kwargs['reply_to'] = self.id + return await self._client.send_message( + await self.get_input_chat(), *args, **kwargs) + + async def forward_to(self, *args, **kwargs): + """ + Forwards the message. Shorthand for + `telethon.client.messages.MessageMethods.forward_messages` + with both ``messages`` and ``from_peer`` already set. + + If you need to forward more than one message at once, don't use + this `forward_to` method. Use a + `telethon.client.telegramclient.TelegramClient` instance directly. + """ + if self._client: + kwargs['messages'] = self.id + kwargs['from_peer'] = await self.get_input_chat() + return await self._client.forward_messages(*args, **kwargs) + + async def edit(self, *args, **kwargs): + """ + Edits the message if it's outgoing. Shorthand for + `telethon.client.messages.MessageMethods.edit_message` + with both ``entity`` and ``message`` already set. + + Returns + The edited `Message `, + unless `entity` was a :tl:`InputBotInlineMessageID` or :tl:`InputBotInlineMessageID64` in which + case this method returns a boolean. + + Raises + ``MessageAuthorRequiredError`` if you're not the author of the + message but tried editing it anyway. + + ``MessageNotModifiedError`` if the contents of the message were + not modified at all. + + ``MessageIdInvalidError`` if the ID of the message is invalid + (the ID itself may be correct, but the message with that ID + cannot be edited). For example, when trying to edit messages + with a reply markup (or clear markup) this error will be raised. + + .. note:: + + This is different from `client.edit_message + ` + and **will respect** the previous state of the message. + For example, if the message didn't have a link preview, + the edit won't add one by default, and you should force + it by setting it to `True` if you want it. + + This is generally the most desired and convenient behaviour, + and will work for link previews and message buttons. + """ + if 'link_preview' not in kwargs: + kwargs['link_preview'] = bool(self.web_preview) + + if 'buttons' not in kwargs: + kwargs['buttons'] = self.reply_markup + + return await self._client.edit_message( + await self.get_input_chat(), self.id, + *args, **kwargs + ) + + async def delete(self, *args, **kwargs): + """ + Deletes the message. You're responsible for checking whether you + have the permission to do so, or to except the error otherwise. + Shorthand for + `telethon.client.messages.MessageMethods.delete_messages` with + ``entity`` and ``message_ids`` already set. + + If you need to delete more than one message at once, don't use + this `delete` method. Use a + `telethon.client.telegramclient.TelegramClient` instance directly. + """ + if self._client: + return await self._client.delete_messages( + await self.get_input_chat(), [self.id], + *args, **kwargs + ) + + async def download_media(self, *args, **kwargs): + """ + Downloads the media contained in the message, if any. Shorthand + for `telethon.client.downloads.DownloadMethods.download_media` + with the ``message`` already set. + """ + if self._client: + # Passing the entire message is important, in case it has to be + # refetched for a fresh file reference. + return await self._client.download_media(self, *args, **kwargs) + + async def click(self, i=None, j=None, + *, text=None, filter=None, data=None, share_phone=None, + share_geo=None, password=None, open_url=None): + """ + Calls :tl:`SendVote` with the specified poll option + or `button.click ` + on the specified button. + + Does nothing if the message is not a poll or has no buttons. + + Args: + i (`int` | `list`): + Clicks the i'th button or poll option (starting from the index 0). + For multiple-choice polls, a list with the indices should be used. + Will ``raise IndexError`` if out of bounds. Example: + + >>> message = ... # get the message somehow + >>> # Clicking the 3rd button + >>> # [button1] [button2] + >>> # [ button3 ] + >>> # [button4] [button5] + >>> await message.click(2) # index + + j (`int`): + Clicks the button at position (i, j), these being the + indices for the (row, column) respectively. Example: + + >>> # Clicking the 2nd button on the 1st row. + >>> # [button1] [button2] + >>> # [ button3 ] + >>> # [button4] [button5] + >>> await message.click(0, 1) # (row, column) + + This is equivalent to ``message.buttons[0][1].click()``. + + text (`str` | `callable`): + Clicks the first button or poll option with the text "text". This may + also be a callable, like a ``re.compile(...).match``, + and the text will be passed to it. + + If you need to select multiple options in a poll, + pass a list of indices to the ``i`` parameter. + + filter (`callable`): + Clicks the first button or poll option for which the callable + returns `True`. The callable should accept a single + `MessageButton ` + or `PollAnswer ` argument. + + If you need to select multiple options in a poll, + pass a list of indices to the ``i`` parameter. + + data (`bytes`): + This argument overrides the rest and will not search any + buttons. Instead, it will directly send the request to + behave as if it clicked a button with said data. Note + that if the message does not have this data, it will + ``raise DataInvalidError``. + + share_phone (`bool` | `str` | tl:`InputMediaContact`): + When clicking on a keyboard button requesting a phone number + (:tl:`ButtonTypeRequestPhone`), this argument must be + explicitly set to avoid accidentally sharing the number. + + It can be `True` to automatically share the current user's + phone, a string to share a specific phone number, or a contact + media to specify all details. + + If the button is pressed without this, `ValueError` is raised. + + share_geo (`tuple` | `list` | tl:`InputMediaGeoPoint`): + When clicking on a keyboard button requesting a geo location + (:tl:`ButtonTypeRequestGeoLocation`), this argument must + be explicitly set to avoid accidentally sharing the location. + + It must be a `tuple` of `float` as ``(longitude, latitude)``, + or a :tl:`InputGeoPoint` instance to avoid accidentally using + the wrong roder. + + If the button is pressed without this, `ValueError` is raised. + + password (`str`): + When clicking certain buttons (such as BotFather's confirmation + button to transfer ownership), if your account has 2FA enabled, + you need to provide your account's password. Otherwise, + `teltehon.errors.PasswordHashInvalidError` is raised. + + open_url (`bool`): + When clicking on an inline keyboard URL button :tl:`InlineButtonTypeUrl` + By default it will return URL of the button, passing ``click(open_url=True)`` + will lunch the default browser with given URL of the button and + return `True` on success. + + Example: + + .. code-block:: python + + # Click the first button + await message.click(0) + + # Click some row/column + await message.click(row, column) + + # Click by text + await message.click(text='👍') + + # Click by data + await message.click(data=b'payload') + + # Click on a button requesting a phone + await message.click(0, share_phone=True) + """ + if not self._client: + return + + if data: + chat = await self.get_input_chat() + if not chat: + return None + + but = types.KeyboardInlineButton('', types.InlineButtonTypeCallback(data)) + return await MessageButton(self._client, but, chat, None, self.id).click( + share_phone=share_phone, share_geo=share_geo, password=password, open_url=open_url) + + if sum(int(x is not None) for x in (i, text, filter)) >= 2: + raise ValueError('You can only set either of i, text or filter') + + # Finding the desired poll options and sending them + if self.poll is not None: + def find_options(): + answers = self.poll.poll.answers + if i is not None: + if utils.is_list_like(i): + return [answers[idx].option for idx in i] + return [answers[i].option] + if text is not None: + if callable(text): + for answer in answers: + if text(answer.text): + return [answer.option] + else: + for answer in answers: + if answer.text == text: + return [answer.option] + return + + if filter is not None: + for answer in answers: + if filter(answer): + return [answer.option] + return + + options = find_options() + if options is None: + options = [] + return await self._client( + functions.messages.SendVoteRequest( + peer=self._input_chat, + msg_id=self.id, + options=options + ) + ) + + if not await self.get_buttons(): + return # Accessing the property sets self._buttons[_flat] + + def find_button(): + nonlocal i + if text is not None: + if callable(text): + for button in self._buttons_flat: + if text(button.text): + return button + else: + for button in self._buttons_flat: + if button.text == text: + return button + return + + if filter is not None: + for button in self._buttons_flat: + if filter(button): + return button + return + + if i is None: + i = 0 + if j is None: + return self._buttons_flat[i] + else: + return self._buttons[i][j] + + button = find_button() + if button: + return await button.click( + share_phone=share_phone, share_geo=share_geo, password=password, open_url=open_url) + + async def mark_read(self): + """ + Marks the message as read. Shorthand for + `client.send_read_acknowledge() + ` + with both ``entity`` and ``message`` already set. + """ + if self._client: + await self._client.send_read_acknowledge( + await self.get_input_chat(), max_id=self.id) + + async def pin(self, *, notify=False, pm_oneside=False): + """ + Pins the message. Shorthand for + `telethon.client.messages.MessageMethods.pin_message` + with both ``entity`` and ``message`` already set. + """ + # TODO Constantly checking if client is a bit annoying, + # maybe just make it illegal to call messages from raw API? + # That or figure out a way to always set it directly. + if self._client: + return await self._client.pin_message( + await self.get_input_chat(), self.id, notify=notify, pm_oneside=pm_oneside) + + async def unpin(self): + """ + Unpins the message. Shorthand for + `telethon.client.messages.MessageMethods.unpin_message` + with both ``entity`` and ``message`` already set. + """ + if self._client: + return await self._client.unpin_message( + await self.get_input_chat(), self.id) + + # endregion Public Methods + + # region Private Methods + + async def _reload_message(self): + """ + Re-fetches this message to reload the sender and chat entities, + along with their input versions. + """ + if not self._client: + return + + try: + chat = await self.get_input_chat() if self.is_channel else None + msg = await self._client.get_messages(chat, ids=self.id) + except ValueError: + return # We may not have the input chat/get message failed + if not msg: + return # The message may be deleted and it will be None + + self._sender = msg._sender + self._input_sender = msg._input_sender + self._chat = msg._chat + self._input_chat = msg._input_chat + self._via_bot = msg._via_bot + self._via_input_bot = msg._via_input_bot + self._forward = msg._forward + self._action_entities = msg._action_entities + + async def _refetch_sender(self): + await self._reload_message() + + def _set_buttons(self, chat, bot): + """ + Helper methods to set the buttons given the input sender and chat. + """ + if self._client and isinstance(self.reply_markup, ( + types.ReplyInlineMarkup, types.ReplyKeyboardMarkup)): + self._buttons = [[ + MessageButton(self._client, button, chat, bot, self.id) + for button in row.buttons + ] for row in self.reply_markup.rows] + self._buttons_flat = [x for row in self._buttons for x in row] + + def _needed_markup_bot(self): + """ + Returns the input peer of the bot that's needed for the reply markup. + + This is necessary for :tl:`InlineButtonTypeSwitchInline` since we need + to know what bot we want to start. Raises ``ValueError`` if the bot + cannot be found but is needed. Returns `None` if it's not needed. + """ + if self._client and not isinstance(self.reply_markup, ( + types.ReplyInlineMarkup, types.ReplyKeyboardMarkup)): + return None + + for row in self.reply_markup.rows: + for button in row.buttons: + if isinstance(button.type, types.InlineButtonTypeSwitchInline): + # no via_bot_id means the bot sent the message itself (#1619) + if button.type.same_peer or not self.via_bot_id: + bot = self.input_sender + if not bot: + raise ValueError('No input sender') + return bot + else: + try: + return self._client._mb_entity_cache.get( + utils.resolve_id(self.via_bot_id)[0])._as_input_peer() + except AttributeError: + raise ValueError('No input sender') from None + + def _document_by_attribute(self, kind, condition=None): + """ + Helper method to return the document only if it has an attribute + that's an instance of the given kind, and passes the condition. + """ + doc = self.document + if doc: + for attr in doc.attributes: + if isinstance(attr, kind): + if not condition or condition(attr): + return doc + return None + + # endregion Private Methods diff --git a/module/scripts/lib/telethon/tl/custom/messagebutton.py b/module/scripts/lib/telethon/tl/custom/messagebutton.py new file mode 100644 index 0000000..17b07a7 --- /dev/null +++ b/module/scripts/lib/telethon/tl/custom/messagebutton.py @@ -0,0 +1,154 @@ +from .. import types, functions +from ... import password as pwd_mod +from ...errors import BotResponseTimeoutError +try: + import webbrowser +except ModuleNotFoundError: + pass +import sys +import os + + +class MessageButton: + """ + .. note:: + + `Message.buttons ` + are instances of this type. If you want to **define** a reply + markup for e.g. sending messages, refer to `Button + ` instead. + + Custom class that encapsulates a message button providing + an abstraction to easily access some commonly needed features + (such as clicking the button itself). + + Attributes: + + button (:tl:`KeyboardButton`, :tl:`KeyboardInlineButton`): + The original :tl:`KeyboardButton` object. + """ + def __init__(self, client, original, chat, bot, msg_id): + self.button = original + self._bot = bot + self._chat = chat + self._msg_id = msg_id + self._client = client + + @property + def client(self): + """ + Returns the `telethon.client.telegramclient.TelegramClient` + instance that created this instance. + """ + return self._client + + @property + def text(self): + """The text string of the button.""" + return self.button.text + + @property + def data(self): + """The `bytes` data for :tl:`InlineButtonTypeCallback` objects.""" + if isinstance(self.button.type, types.InlineButtonTypeCallback): + return self.button.type.data + + @property + def inline_query(self): + """The query `str` for :tl:`InlineButtonTypeSwitchInline` objects.""" + if isinstance(self.button.type, types.InlineButtonTypeSwitchInline): + return self.button.type.query + + @property + def url(self): + """The url `str` for :tl:`InlineButtonTypeUrl` objects.""" + if isinstance(self.button.type, types.InlineButtonTypeUrl): + return self.button.type.url + + async def click(self, share_phone=None, share_geo=None, *, password=None, open_url=None): + """ + Emulates the behaviour of clicking this button. + + If it's a normal :tl:`ButtonTypeDefault` with text, a message will be + sent, and the sent `Message ` returned. + + If it's an inline :tl:`InlineButtonTypeCallback` with text and data, + it will be "clicked" and the :tl:`BotCallbackAnswer` returned. + + If it's an inline :tl:`InlineButtonTypeSwitchInline` button, the + :tl:`StartBotRequest` will be invoked and the resulting updates + returned. + + If it's a :tl:`InlineButtonTypeUrl`, the ``URL`` of the button will + be returned. If you pass ``open_url=True`` the URL of the button will + be passed to ``webbrowser.open`` and return `True` on success. + + If it's a :tl:`ButtonTypeRequestPhone`, you must indicate that you + want to ``share_phone=True`` in order to share it. Sharing it is not a + default because it is a privacy concern and could happen accidentally. + + You may also use ``share_phone=phone`` to share a specific number, in + which case either `str` or :tl:`InputMediaContact` should be used. + + If it's a :tl:`ButtonTypeRequestGeoLocation`, you must pass a + tuple in ``share_geo=(longitude, latitude)``. Note that Telegram seems + to have some heuristics to determine impossible locations, so changing + this value a lot quickly may not work as expected. You may also pass a + :tl:`InputGeoPoint` if you find the order confusing. + """ + if isinstance(self.button.type, types.ButtonTypeDefault): + return await self._client.send_message( + self._chat, self.button.text, parse_mode=None) + elif isinstance(self.button.type, types.InlineButtonTypeCallback): + if password is not None: + pwd = await self._client(functions.account.GetPasswordRequest()) + password = pwd_mod.compute_check(pwd, password) + + req = functions.messages.GetBotCallbackAnswerRequest( + peer=self._chat, msg_id=self._msg_id, data=self.button.type.data, + password=password + ) + try: + return await self._client(req) + except BotResponseTimeoutError: + return None + elif isinstance(self.button.type, types.InlineButtonTypeSwitchInline): + return await self._client(functions.messages.StartBotRequest( + bot=self._bot, peer=self._chat, start_param=self.button.type.query + )) + elif isinstance(self.button.type, types.InlineButtonTypeUrl): + if open_url: + if "webbrowser" in sys.modules: + return webbrowser.open(self.button.type.url) + return self.button.type.url + elif isinstance(self.button.type, types.InlineButtonTypeGame): + req = functions.messages.GetBotCallbackAnswerRequest( + peer=self._chat, msg_id=self._msg_id, game=True + ) + try: + return await self._client(req) + except BotResponseTimeoutError: + return None + elif isinstance(self.button.type, types.ButtonTypeRequestPhone): + if not share_phone: + raise ValueError('cannot click on phone buttons unless share_phone=True') + + if share_phone == True or isinstance(share_phone, str): + me = await self._client.get_me() + share_phone = types.InputMediaContact( + phone_number=me.phone if share_phone == True else share_phone, + first_name=me.first_name or '', + last_name=me.last_name or '', + vcard='' + ) + + return await self._client.send_file(self._chat, share_phone) + elif isinstance(self.button.type, types.ButtonTypeRequestGeoLocation): + if not share_geo: + raise ValueError('cannot click on geo buttons unless share_geo=(longitude, latitude)') + + if isinstance(share_geo, (tuple, list)): + long, lat = share_geo + share_geo = types.InputMediaGeoPoint(types.InputGeoPoint(lat=lat, long=long)) + + return await self._client.send_file(self._chat, share_geo) diff --git a/module/scripts/lib/telethon/tl/custom/participantpermissions.py b/module/scripts/lib/telethon/tl/custom/participantpermissions.py new file mode 100644 index 0000000..d371977 --- /dev/null +++ b/module/scripts/lib/telethon/tl/custom/participantpermissions.py @@ -0,0 +1,138 @@ +from .. import types + + +def _admin_prop(field_name, doc): + """ + Helper method to build properties that return `True` if the user is an + administrator of a normal chat, or otherwise return `True` if the user + has a specific permission being an admin of a channel. + """ + def fget(self): + if not self.is_admin: + return False + if self.is_chat: + return True + + return getattr(self.participant.admin_rights, field_name) + + return {'fget': fget, 'doc': doc} + + +class ParticipantPermissions: + """ + Participant permissions information. + + The properties in this objects are boolean values indicating whether the + user has the permission or not. + + Example + .. code-block:: python + + permissions = ... + + if permissions.is_banned: + "this user is banned" + elif permissions.is_admin: + "this user is an administrator" + """ + def __init__(self, participant, chat: bool): + self.participant = participant + self.is_chat = chat + + @property + def is_admin(self): + """ + Whether the user is an administrator of the chat or not. The creator + also counts as begin an administrator, since they have all permissions. + """ + return self.is_creator or isinstance(self.participant, ( + types.ChannelParticipantAdmin, + types.ChatParticipantAdmin + )) + + @property + def is_creator(self): + """ + Whether the user is the creator of the chat or not. + """ + return isinstance(self.participant, ( + types.ChannelParticipantCreator, + types.ChatParticipantCreator + )) + + @property + def has_default_permissions(self): + """ + Whether the user is a normal user of the chat (not administrator, but + not banned either, and has no restrictions applied). + """ + return isinstance(self.participant, ( + types.ChannelParticipant, + types.ChatParticipant, + types.ChannelParticipantSelf + )) + + @property + def is_banned(self): + """ + Whether the user is banned in the chat. + """ + return isinstance(self.participant, types.ChannelParticipantBanned) + + @property + def has_left(self): + """ + Whether the user left the chat. + """ + return isinstance(self.participant, types.ChannelParticipantLeft) + + @property + def add_admins(self): + """ + Whether the administrator can add new administrators with the same or + less permissions than them. + """ + if not self.is_admin: + return False + + if self.is_chat: + return self.is_creator + + return self.participant.admin_rights.add_admins + + ban_users = property(**_admin_prop('ban_users', """ + Whether the administrator can ban other users or not. + """)) + + pin_messages = property(**_admin_prop('pin_messages', """ + Whether the administrator can pin messages or not. + """)) + + invite_users = property(**_admin_prop('invite_users', """ + Whether the administrator can add new users to the chat. + """)) + + delete_messages = property(**_admin_prop('delete_messages', """ + Whether the administrator can delete messages from other participants. + """)) + + edit_messages = property(**_admin_prop('edit_messages', """ + Whether the administrator can edit messages. + """)) + + post_messages = property(**_admin_prop('post_messages', """ + Whether the administrator can post messages in the broadcast channel. + """)) + + change_info = property(**_admin_prop('change_info', """ + Whether the administrator can change the information about the chat, + such as title or description. + """)) + + anonymous = property(**_admin_prop('anonymous', """ + Whether the administrator will remain anonymous when sending messages. + """)) + + manage_call = property(**_admin_prop('manage_call', """ + Whether the user will be able to manage group calls. + """)) diff --git a/module/scripts/lib/telethon/tl/custom/qrlogin.py b/module/scripts/lib/telethon/tl/custom/qrlogin.py new file mode 100644 index 0000000..cd54027 --- /dev/null +++ b/module/scripts/lib/telethon/tl/custom/qrlogin.py @@ -0,0 +1,119 @@ +import asyncio +import base64 +import datetime + +from .. import types, functions +from ... import events + + +class QRLogin: + """ + QR login information. + + Most of the time, you will present the `url` as a QR code to the user, + and while it's being shown, call `wait`. + """ + def __init__(self, client, ignored_ids): + self._client = client + self._request = functions.auth.ExportLoginTokenRequest( + self._client.api_id, self._client.api_hash, ignored_ids) + self._resp = None + + async def recreate(self): + """ + Generates a new token and URL for a new QR code, useful if the code + has expired before it was imported. + """ + self._resp = await self._client(self._request) + + @property + def token(self) -> bytes: + """ + The binary data representing the token. + + It can be used by a previously-authorized client in a call to + :tl:`auth.importLoginToken` to log the client that originally + requested the QR login. + """ + return self._resp.token + + @property + def url(self) -> str: + """ + The ``tg://login`` URI with the token. When opened by a Telegram + application where the user is logged in, it will import the login + token. + + If you want to display a QR code to the user, this is the URL that + should be launched when the QR code is scanned (the URL that should + be contained in the QR code image you generate). + + Whether you generate the QR code image or not is up to you, and the + library can't do this for you due to the vast ways of generating and + displaying the QR code that exist. + + The URL simply consists of `token` base64-encoded. + """ + return 'tg://login?token={}'.format(base64.urlsafe_b64encode(self._resp.token).decode('utf-8').rstrip('=')) + + @property + def expires(self) -> datetime.datetime: + """ + The `datetime` at which the QR code will expire. + + If you want to try again, you will need to call `recreate`. + """ + return self._resp.expires + + async def wait(self, timeout: float = None): + """ + Waits for the token to be imported by a previously-authorized client, + either by scanning the QR, launching the URL directly, or calling the + import method. + + This method **must** be called before the QR code is scanned, and + must be executing while the QR code is being scanned. Otherwise, the + login will not complete. + + Will raise `asyncio.TimeoutError` if the login doesn't complete on + time. + + Arguments + timeout (float): + The timeout, in seconds, to wait before giving up. By default + the library will wait until the token expires, which is often + what you want. + + Returns + On success, an instance of :tl:`User`. On failure it will raise. + """ + if timeout is None: + timeout = (self._resp.expires - datetime.datetime.now(tz=datetime.timezone.utc)).total_seconds() + + event = asyncio.Event() + + async def handler(_update): + event.set() + + self._client.add_event_handler(handler, events.Raw(types.UpdateLoginToken)) + + try: + # Will raise timeout error if it doesn't complete quick enough, + # which we want to let propagate + await asyncio.wait_for(event.wait(), timeout=timeout) + finally: + self._client.remove_event_handler(handler) + + # We got here without it raising timeout error, so we can proceed + resp = await self._client(self._request) + if isinstance(resp, types.auth.LoginTokenMigrateTo): + await self._client._switch_dc(resp.dc_id) + resp = await self._client(functions.auth.ImportLoginTokenRequest(resp.token)) + # resp should now be auth.loginTokenSuccess + + if isinstance(resp, types.auth.LoginTokenSuccess): + user = resp.authorization.user + await self._client._on_login(user) + return user + + raise TypeError('Login token response was unexpected: {}'.format(resp)) diff --git a/module/scripts/lib/telethon/tl/custom/sendergetter.py b/module/scripts/lib/telethon/tl/custom/sendergetter.py new file mode 100644 index 0000000..7d389ec --- /dev/null +++ b/module/scripts/lib/telethon/tl/custom/sendergetter.py @@ -0,0 +1,102 @@ +import abc + +from ... import utils + + +class SenderGetter(abc.ABC): + """ + Helper base class that introduces the `sender`, `input_sender` + and `sender_id` properties and `get_sender` and `get_input_sender` + methods. + """ + def __init__(self, sender_id=None, *, sender=None, input_sender=None): + self._sender_id = sender_id + self._sender = sender + self._input_sender = input_sender + self._client = None + + @property + def sender(self): + """ + Returns the :tl:`User` or :tl:`Channel` that sent this object. + It may be `None` if Telegram didn't send the sender. + + If you only need the ID, use `sender_id` instead. + + If you need to call a method which needs + this chat, use `input_sender` instead. + + If you're using `telethon.events`, use `get_sender()` instead. + """ + return self._sender + + async def get_sender(self): + """ + Returns `sender`, but will make an API call to find the + sender unless it's already cached. + + If you only need the ID, use `sender_id` instead. + + If you need to call a method which needs + this sender, use `get_input_sender()` instead. + """ + # ``sender.min`` is present both in :tl:`User` and :tl:`Channel`. + # It's a flag that will be set if only minimal information is + # available (such as display name, but username may be missing), + # in which case we want to force fetch the entire thing because + # the user explicitly called a method. If the user is okay with + # cached information, they may use the property instead. + if (self._sender is None or getattr(self._sender, 'min', None)) \ + and await self.get_input_sender(): + # self.get_input_sender may refresh in which case the sender may no longer be min + # However it could still incur a cost so the cheap check is done twice instead. + if self._sender is None or getattr(self._sender, 'min', None): + try: + self._sender =\ + await self._client.get_entity(self._input_sender) + except ValueError: + await self._refetch_sender() + return self._sender + + @property + def input_sender(self): + """ + This :tl:`InputPeer` is the input version of the user/channel who + sent the message. Similarly to `input_chat + `, this doesn't + have things like username or similar, but still useful in some cases. + + Note that this might not be available if the library can't + find the input chat, or if the message a broadcast on a channel. + """ + if self._input_sender is None and self._sender_id and self._client: + try: + self._input_sender = self._client._mb_entity_cache.get( + utils.resolve_id(self._sender_id)[0])._as_input_peer() + except AttributeError: + pass + return self._input_sender + + async def get_input_sender(self): + """ + Returns `input_sender`, but will make an API call to find the + input sender unless it's already cached. + """ + if self.input_sender is None and self._sender_id and self._client: + await self._refetch_sender() + return self._input_sender + + @property + def sender_id(self): + """ + Returns the marked sender integer ID, if present. + + If there is a sender in the object, `sender_id` will *always* be set, + which is why you should use it instead of `sender.id `. + """ + return self._sender_id + + async def _refetch_sender(self): + """ + Re-fetches sender information through other means. + """ diff --git a/module/scripts/lib/telethon/tl/functions/__init__.py b/module/scripts/lib/telethon/tl/functions/__init__.py new file mode 100644 index 0000000..78b7cdb --- /dev/null +++ b/module/scripts/lib/telethon/tl/functions/__init__.py @@ -0,0 +1,717 @@ +"""File generated by TLObjects' generator. All changes will be ERASED""" +from ...tl.tlobject import TLObject +from ...tl.tlobject import TLRequest +from typing import Optional, List, Union, TYPE_CHECKING +from . import account, aicompose, auth, bots, channels, chatlists, communities, contacts, ephemeral, folders, fragment, help, langpack, messages, payments, phone, photos, premium, smsjobs, stats, stickers, stories, updates, upload, users +import os +import struct +from datetime import datetime +if TYPE_CHECKING: + from ...tl.types import TypeInputClientProxy, TypeJSONValue, TypeMessageRange, TypeType, TypeX + + + +class DestroyAuthKeyRequest(TLRequest): + CONSTRUCTOR_ID = 0xd1435160 + SUBCLASS_OF_ID = 0x8291e68e + + def to_dict(self): + return { + '_': 'DestroyAuthKeyRequest' + } + + def _bytes(self): + return b''.join(( + b'`QC\xd1', + )) + + @classmethod + def from_reader(cls, reader): + return cls() + + +class DestroySessionRequest(TLRequest): + CONSTRUCTOR_ID = 0xe7512126 + SUBCLASS_OF_ID = 0xaf0ce7bd + + def __init__(self, session_id: int): + """ + :returns DestroySessionRes: Instance of either DestroySessionOk, DestroySessionNone. + """ + self.session_id = session_id + + def to_dict(self): + return { + '_': 'DestroySessionRequest', + 'session_id': self.session_id + } + + def _bytes(self): + return b''.join(( + b'&!Q\xe7', + struct.pack('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 IZCX$Ne?c#b`~Uy| literal 0 HcmV?d00001 diff --git a/module/scripts/lib/telethon/tl/functions/__pycache__/account.cpython-314.pyc b/module/scripts/lib/telethon/tl/functions/__pycache__/account.cpython-314.pyc new file mode 100644 index 0000000000000000000000000000000000000000..e0459f1827938d2572bf3f1557d24e67c4979d56 GIT binary patch literal 214450 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

f%AT%ZI})o^AdA|@VEX}Grv5)@W$>TB|7*3u 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 literal 0 HcmV?d00001 diff --git a/module/scripts/lib/telethon/tl/functions/__pycache__/aicompose.cpython-314.pyc b/module/scripts/lib/telethon/tl/functions/__pycache__/aicompose.cpython-314.pyc new file mode 100644 index 0000000000000000000000000000000000000000..137e9a3128ad373aaa8307ca52d7aaac85d37c09 GIT binary patch literal 14101 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 literal 0 HcmV?d00001 diff --git a/module/scripts/lib/telethon/tl/functions/__pycache__/bots.cpython-314.pyc b/module/scripts/lib/telethon/tl/functions/__pycache__/bots.cpython-314.pyc new file mode 100644 index 0000000000000000000000000000000000000000..307234e2468762f76eb7afdcd0751bfa0ee063ff GIT binary patch literal 83604 zcmeIb3wTu5eJ?ssX(Wx1KoUYiZ;6L^NW9<10tqbf5b{VC1_MGG4KSb=dqx;s$0l)l zB5K!Cnleh;3hq`+oZ8`lE%L6ccRf?k1|bD+N6&(%?U;{vD=f=`}_ab z-h1}mv-e0mY~!A+@0+#vti9LT>-m4L|5`jFJJW{nqqlx`{AXXX*?vfWl;D`fGn;3H zXEo=A^P2O+`OO94g67%b+0BLF!g5=ItOA6sRoSK2uOigMLr)DA5 zt4S^9)NG{A(4@}g)EuPdYEnx$btY10X;Mo$H4mxzn$$8*EkNpQO=>x(79w?yCbfc7 zi;!BZNv-76xkxS1q*ifiDN@TcsnwiXj?@ZGY7M7WBDG4BI*(JUky@ikozJQBkUC$J zx`0y`Aa$W8bs?wLB6X1_wU$#CBXx-;brGj7MQWWUbup*bBXyZ3bqS{~N9qbq>QYW! ziPTk^)H+UGjnp-o)Ot=`i_~?R)McEy9;q8NsmnQaBT_eMQde;5JxJZGNnOdQ_ae1H zle&siw;*+^CUrHZHX^l2le&gew;^@ACUq^R-iOp?P3k&MZ9(b|P3n41-HFs)n$!)P zx*Msjn$(S)x(BIkn$%64dOuQqn$&xIu4-Ft`y*So2m1mQM*{-^sXH9#t>`&Z(Xne! z&+&ji99p2H4@z|vTl)Gc{KvWnjs`*%4+rt5Cs47?x21hsQ>{JjQS#tEqhpsZaB?^h z3fH>go;^e1;NU=aU);4T7z)ST4-DXMJiB9G+qSO8=538TTUzgHr4oAa`Eam5P@5H> z(Q#%duyrupIN0CcJusRN9 zhn=%+gEpT%?D9Fn?oQiEyU$r@>+|?rh;zefK6jxlobF3Q>>-dB&N!Iq^Ay@v$wh{< zd>LVHr!AcA%fx_Y#beeQZ^we3>ckEum*XU||*nn`yV zeV7oNG<^~31l`epV0!T!4Lqi5=mU}A>q>y8F6mcjb&fpGAs)O{)#KC`rYXsABq$9(l4 ztM6=Dy0zPXVtA;&f3SDBFF+51L*Y<;U$Cb>9Ow&#j|~pghx_V}3=jBeDu(J&>rma$ znYg#Bt9xKz5c43=)fKNc@3%VbOVrj-HKO;<+J0e6bI*O3qR~Cq=<=GAFJny|oH~iu zYPTS8qceyidN_wX)(V?A= zwsb>w{6)E*3B{eSu7A945w6!@5a;oeL=h&cMpeyeW4JwTpfyZDG(a$I~9nRS&W4Iya<^>+YtR2 zojQ>>CziJ$lDFW+BeA+|k-BZK_kQQt8^@w`dtwXsMDyB4To|W<;#k3=NWr2PSABWI zr47-7H6yO4Gv9UEYTNAZhiLtNYSET5`%CsMC7v%X!yN+-By-3@jCy(|H=*~5N+d9G zKpjc+s5*(|9Jm}%`*D^0R8I?<=Q0~H1^n&KGSB04nvbc^rK=-T!HN3OU5Ywl$VzCg znB+=T$9iB|^IY@f6NQm@6+!-HJ}KsAfn&hscl-7i+K zw2|Jlnlf?$rFbXwD5A5rtKOWknY9z%nJ*PzZhEEp<>s#)i`CvYUVGoOtIn@`Ze1*| zI+9m?C2!uF9p8QMtp_K(g`@e;mt82E@aDa=|5evFGGEL5R W?(wC&pY1rm|GE9K zf(4O+1<``qYZ>m`%xgAxMkc^Rhy6i&to>l5{a_+l010*Cpfi7yxa&B`2LTh(x_Zup1EF{h z;6|{!FZf76BqRn_+JrV}hqVF!`=?x%%8WMSfQ&IKI8roi3kkEkQ>FAV#}H;Ny}bo*h0a z1-g5?2AOVE|AO`i4jjb-bNc&2@ick`0NT|B0wA8*l^|Ktx)>yfXi(?Ruc)XHNHFV& zG}zxI=c=;kbIm(Tkv+5?5tdU^ybHPa3_i|=E8-8{6m7In#PXl)|H>&m03_`*K-Tg$HWOj84>IV0qT22mk_sLIk zy1I@8rBJvpI1m^Z#N!!_ds^E&d=E5s?D2KAG{tAMKd`lN*OvD7u07lFUn1H=T1^o_ z2&n;4Jd0_vej=56rQMWL0OAaEQPAJj9Y!bi3}c$2@R=I&%^&FN>*~T7R3HOS(FX1| z0m=xTWz&Prh~E1Q&BcQ9v#lKc!UbxHX{GDA4)@yYo-}vn_1rXf*0o$)+RS$yF83Du zq!YJ4=UdS!RG=R}H7JuYnWkQS$7$+9Fh7G2w+9A#cLzeD?xO*jsP3b=_%nZ?$7$o+ zmlpkVfy1UFu&J8#)?^tXYXZBT2tB7nNL)aAqN^?u{3xfS3PdusEIPRg3Go_>YUyZJ znvj$Eh~6Wb)#WaHm!i=v-ga1Tp%+5OHlGJrNoRhK~iL z3i(;b;NY&oful?0|Lw;HC2(5YiMljmsLaxpmqK6$doam+)db~+i^SL{*@^14b0=PM zFk0k$?md~jelIMiJBgD{!ZyDS7>avbSizOi|P zE@^{uFHt4|I*q8ADK5)Sl}WCx$UeCZkxX(enu6qt=FPk2b`k~U%*edD`+lOn5(#2H zY7sBZN6b&pDfDiPMZKaUnZ5pN1O3jH##+P!*jH&=hhPGELEgtNEe{AR#FQ^I}pjs zNDv4DRaRTIN<$g6BI_hgB_n0P*#D_djCo1~mZNXCN_!AX`w(f-Do;m>2zgK5gD45D z0p;C|g}ci(Tnp5_8x^}2vpy^L2K?ot`8;!b>z%Jbu=2AbX!COZ-7BvlXwC*x=v^63pE{9FWdvvFh%7x9)P<{sn_+ zd#UK?38x;b%HwRCtif|DtjZ!Qh^}~1yBN2|JrGPvXS#yDaTm#)B%)DBOcfjmr^dC5 zQdU`qacSv=<|1N@>vk8tYeV!x9w=#d(TlD8FBy|i&!e?oCKU9DS1TQoB|`NHB)oNS zux~>}$FX3jA`CHP#j);C#lT>NAEK_XH0%!#N+Cjt4(Ty`L3$h!ARMi;M1meW5Q!L{ zKUI4Lj^wt^Kn5276jeTzJ&`}>=?u|4Enp%%$}96SzbxbhV94;#7<|qHH0&Jk2932L z93bw4Ly{apK?x?g+NchOpiEH`Ynp$Hp47SoO87P8PV`!>Tii2ppnjbSIm}Dlr!tD@ zLP?^?de)>hvbD;awVL|QXpbQY6 znuyriNQ`6%%8;ydYA}wID;}az-F7yZtxri|a3X{hNlhrQ$I;st}ID?tuK9Jvx%`l5qY!=S| z7b!zRcNc*|?pxx11%ttCU~TG-(qOoL>Ma5)n6&xDvHY4yeoZug{z%3|?(A4@O(eG_ znmd2Q4SYA+awTWQm}dnKC}1ux90E){M4YV ziexS{d^7lRTAH1L#_oY+9yIt5H}>U8kD^Am%6Cc!ky$!~C^ZVKGX1^|^_`}Y4pUTv z$glJc?rn6>_|2TLls=8;QV&Jc!O84enbp??BH}LxC_8^yMIcC_75Btl#iL0_siGEq zS*dwLWv+~oc#yImN2C+UCqeSt6v->7R6C0JCWYJQu6`G#2!^4L?lvJ}YF8`1w&)2N zPkxfJY(pgD$%P+xR*Dz$kXdguATeHIJ}(5e`V3{0C`wv*E~H{PcqR#H+zL-5g{j$e;#gS|)?u-@fCZx2*A4 zz<;7?@Zo{J!R}r%nP}^ly8AfSw=^HfbWHNA(GM2@+r`fnKE^z(6**hj;bT}I)CcU3HZ>X zL_ipVIoqZ%W|nzO#zx6wr5+^^DzkKA*cwn3X$wWx14Qq{=S{H;1cv+qUJVgfqv7X@ zrlA&96{T}tm=pCb`Un)GghC8~P&tJmQ^hDaKx(Z-#1VuPD$~azOehl**u>QZbYjqZ z3bE#*DLPey+y4lknU3C5rxRt#=jHH~oK0h%O#;JHoh^t0Ud$BzHhs%f_z7Ia2ch$x^7taC z0;Tux(zKX8$k<=QtEeyIe7aBO`bsToCa^o#Dgzm1>EqMcqAFU!qYIB2#Z~cpsRWN& z?awQCo9icijqXhOlM>U)$@t(~$i`A&86PZA#dxXoH)+VlwcU>!LdfRQH~?M~sDM;z zts$s@*FuBydzIHe1Bvjx$R@xojYLv|8;}^EYuaD|U;jV65L${z17FGPM1`NFFTk** z{)1QH0Q%98;c_2cG%6ejy_7{^!o*aVHckWQ#;Ap$aL_`QaV?DGFO23d8p)8QTJy&| z^95v~@3%_7gE&bb#q)}yKf%vjAcciRYMe^a-53-&3uGAPa4!&FqLOqlOqmvwhw((G zzUY-I&Hy+&%Cg-RVS9B#Y$D2Y(SI>l=&R2zHySxG4Aa6IhGK&AO|M( z(gdh;F_@!i8_$HnIP5bdXn2HWviDT5yQ?=47=ne&2~37Eyd}{piMx8BI7!V#l^PqT zgNbpJ5?Y4nJ>r|v-L>yhbfNtkT`o29<<%~}L=N;u0>QTr9OqXAkRq{^5@ZPPom_(y zYvqsDGCJf7rRSBgKp%-(r3_RG@KNLO#^9kINX2>PIiLVM^_ZHpJQU=|? zVAQu#dVS5y33uh)w?3Acfqi->#eTn>VvWR zgVEwcFvZP@)E|65S<0sx-u{5QG!;H$BUmk3Mh*%vf>kFUa0cNVLpVTe1_wodvq1hY zHf@>bN~O(V7SH5MKJ0mDBcl;RsCNUrqzP8;&+*K3j;3o;H_%HwFw|mcBSkcklQ0=Q zi6@VejJE#)PeTtOlF@cGlx{-FrsD4j@0_nb@_Nm8>fflp(*96v?Sb*N2S#h2ufI?q zD_$NcUVf!`9wph(#@r1$ae8sB89pe^WQVM1gX|& z|5lZ70a%l<<+9lTICsdKJr3D++Yi~50mrappjL)})J#JX{wgGfWge3O*5o1mu_3gf z#>SxFpp|YVFQ$l#fk%iS`r^l5{(C3}TejC`2x{g@BuY2KpI!DzDUTlihtd7*I9eN~fq%uR9Le zY04=<=~(C$?WruO!FQ&9?#6Qyae^|0LustH+_u;6v#+JGLm<}W$5a>Te;}g0W30Y} zWrp+@luFc`&|{`2SlYPzNMHBS5H=QR@qFCP!cytKBTGE1D?t;4s7`F+tJ8CqB#pI( z?0XMcLOw)*2)VP*=RB7a&8-}9|7cFh=mQt;|Lnfs%NfbmYdI%K1FBMo6SGTVvlm5X zFN)4yGLl8KqWjeoS8|Swd5-Yh#S~_+m6(}4TkG&eb4wrwhZMa-H6yw`Nv8<{6WV@QHg&1)SzIFBmp~itBGA}4(wWU%DdJy zgyc<;ky%Ve7B$gUt$=oGlOkJpVon6Vvf92hnK72ICd|pIb*of{2yo zAoC@MpbQBwrltw!Th>%Z9Hyhzys3_fL-Va-Z#0dy?0dUuKkpagLAVq9#e^nx0U9DL zM8u%cAU&^2)doSeQT-*NRz$r3UDQBG=qdvT&=@Hxt@M?|3E;p-vxv<-rl)p%Po2&3e05uI&N)u%pg4?AX2-v&^p9N@h>7 zZA=hfDe>WMdxyQ%u@mAGnsvjDAv@J#815G6K1kdcfjH?ZMLg9=4#D$HT8}$`uUcH< z_R~UyCL8fEsheU>$uw^v#xHaM`DL0HMol-r4{UVR^BXQ~7@xcJSzkhb0fw*7K4g!b z7#=?{99?-Ta$mJ*xhwYODbKY3JeCwiFx+zF>B~QUBc?w#J%M@gW3`$5v-~hZ3axny?5~Xek3fRE@ zN0l}sis?ZYRUMdS_ zX^y&O(!03TPStqECh|dWdPv$FUug{l1qoiasc+EV-51(ir)H`lut-7%0*hliPecwM zAA9J)cbng8{_c)9cSH{#f1lg>sgg3g!X5B3qACfgCod?KV~9KZ0s~@wnQ{jeQ`NDY z*3=18Vw!?e$5{Xy1?O<%2sq37|JkAI~;TCw%@mET$S#=6MP6Jv*aV~3BA zA3h%4c_P|)V*1Lc|HU<#iZQ@*72P3&9F)PGiYX`u@Emd-P}far7D1VnK-o0_ro=Fy z%Zx$&c3P*)6s4=P8`EySn097+1*ImG3~am|7r{w<4|j35L=G*af5LsOhO1yjnY5GM z-9-_@-k;#kf`6E2JsUKP!~&Y5eJBNBR;LRy><{$JcqZpu&J|D5#LT>rwC~TH{p_jp zk3aW#v~b}&Giyh-==Ki+q0f3|_qpApRquF9-*ww&Eqd2u%gr<0QfSUiT%`t5M0ED# zX51uk6N&A5Y3^d$si)hnmyy`6N4E@NxIjP3QiC$Dn0C?1rH#yl56&zgmnMm{hosT6 zTpHU6AeSbIw8sZ|G|Qzkb#mz}SuRa`sCTb*QA=26ZsHq|91S1ENs;z>9;dbUBYr-=Hi=2z9 zo}MAvq0C9VW21q8f{0myLLUWF1AI z&2;YRSQkg%gB85{M8(xSRjWR;pZQ_@Ys0y>d{#V_6Yn{*nZmn3s}qAw>qwi z%OqrBjSf)Qlr}an_?H{TI`>}L*B#s0GrqIu)h*v>d9CGZJI9U%$2SG#w{St0M7hLv zAwfb>dK+_frp`hKeYA=S08A5ROjUK5JoD*Q9gZa`sR|W_%pjsvm5D>+<bQ!-=Q^!vtst9RQtGfY^6!d6_Bm8(&9@Xx6E5(!d9xgv$QP$5nY2GVvNlc z*4y~EUHVtV5=jenP>;|r(IJx!i0%lFDZPi1r1uf&mg&DEA->XjnObs{owRVt+Kg}u zR{w*JM`>O4JATc9f5#j_2UgxbM>i$Xh5z~K**=HgfyF~_zAyc6lu+w2>ZB5ICBcdQ z2i`PSM^#K!{rjn|O^CD%r$#UJ`8_Y}8L!#+YWN$EzxMc;Z*TP8z0gS=JT!K!FLvxy z`P z=I1t#=heR0@#RC84!t@YUD`C#Xy8@>yLQ@J?cZJa*21xlqgRd%#Eyl=kA=p{Kn z34G&mdn484ar~3k?#U^*c_|Ga3QFMYQK2 z!#p~1qk=E@NX_)9g`zkjDD)ECl48=T@=O|Do#3FX>(g#(oM*j~G|c)wd_VLVlwi>M z%`P5YeR26`*Z=n5NY;d&i_gk^cKI{wpIT2WIn=jT;Ag35?#Pbs7Zi`KdH&uD_eSR~ zd8c6Mvvw`JPjqK-EVnX}TY0hSo!oiX-L~0v*D`IzWg|P-ME2E3uH@_(^Xy=_iaukS zS*2TQ9jz7?vrOMmL}Oy*g{S&DF?H8-)7@3qXBpV~8Sb5mj*xEo`w|uCN2jPk#+tSd zLq}Lh181foJb_xV|DD0ZJ2GKF3-P0VGbGIf!Zz&AWGb$nEY5uJyU7-2>4vR18rNJT z$&F_W1RfUNqo!eITVv4x9eX}V$oC0PwuHKY1e;i{q;4>Kq;PS1S3A}X7rZ(*dJc+A@anP`$)_^m zPtuHGr`A3}3_JNU{YV)0+jJ#q>59^X{!{=fTv@84EHl}A5)5{dK5{t?QVB&gYE~Og z%dJL7=CDuGjC=^COq;_t@hnN>SQaa)ixkzp82<8OmmZ52ZHAHC49G0m-()VzhqjR% z2W^4ppe?z$)lrK?fn!E{Cf`JLK2F+3jh4`qw@9Hm#Hza$hm&k;XEKX?Ze3kSd1eSH z>Vj#+bur0hrYciN3$o(_PhpZvYvET))6)#lq?@wjdT5kHFmFVw;+C^oX39T7v-(B6 zDKq6$d15l#ua(mI<&}ckvG%>f-6i<}S%(&9_U~Kii@zwi;2hCVfeWa*9 zTC_Z29tiY**xqZ8b@fNO`V+|l$fMS3kkOiF667u~2{R4K-RmgRd|IT^%IYltG_ExV z56}B`u%5hyfoD+zIz*$^OMt*lc%a>9PA|sGtN@^34F3zw6oP*m&T6)S60@N&aiS+JbpIMrQ zOy7aW3H2FvoT*e&h-BP?DA)KIskRyb4MF}4jlzA%J%#p6=Ur1vYL8?Jf*QUyJI6eo zf}o}PTTsvplPDtc(wu?@DaLfz^;u5$z1K4hR(5XpUWJ0DTk%0E(2rKA;lre$3$~pe zg8xNq*60{K5g1S`>_$;1rk{UOH1`n)wr%vaoMzmMbPhD)z`d#6Z7Ftk8h2X)q|Zc7 z^1gZs--H#wU*KPor$*Wc=QOUTx3=izM2!KiTR)R_u z(=HI}Z5Fbl%X<^sDc~_SR3~Z`=vBH;zrVuxRQTLtXtWHK1?!d|nJGcufh7C8ScD1z z4w(%P+AWA=ZZipB6GZatlBZ?gyxL}}r14q2{85m`GHIG{N||ySd#QP|W$N>D=qroe z>mMjCUEE)4G;r@`Qeim4o z7Z62+#LRbw0~Ih@xIRPd3{P_NwN&0!>2xm@-=p&UsHPf}0r@b*TyyRVSQxJSDXJCx zT>H98M%LfV*t*Z)7E{J=({VOEU8i63tZJh}$KOEBq{Ha05Q6%lFY$>6W&Q_ya>Z=<;<>wY`&;W?l3$ehcn>=Im_n(=bRBQptjIC zWz0~h#))x!o5A}NrGIxAyx8t|noGA>Wq9hDBmtjTtaNw)II4Bj03S8w5ZxI?9V`_q zHT`Oxyexiz%pn1%1;KdD>7YHU4zoS01T69Mun%TyQxL-46u62sBDMCA3zsKKPhN%q zf$1<~RU7a;USeKVF=2m5H5Cd2dBUQWq``I2CP|0cCP}s9NYo5Jcp!w7!NDf5IwUGS zFo~dJeWS1H^su=5SA?QYrG*6S`OjCC`e*!h>qJOAwN0-4ysov0Y)H=pfPg;cZM z=s-fh?+&I{$x)+VK2;Cwp|ee5^)o!T7L&(xDkd z6ELL%aIHiFimW2MA9si7^gxLgkI>~!?Mtu9w5B@x=Uvo#5@=5mM?h)khMKNyB@v$0 zZ#elccekHvtps^WPRHjr+&kQppp!arJma9WDbatMr{w|sAgal&hUG0mdr3_&lSWuI zTaZrNWK|k5d#+AVhwrFCr7k|cT>5_MH?^;@jbMY?c{GH=*#JqStbuM~sFbO$0NwXc zc!*dpSrGv){buwELXD7$QCA1x17Orm$T@QhG6fCj7|1g@-eVhqraG7(_pWH#c7*$BuCR|zdeANLG1 zTs4}LHK!CvXwUj+#WBr%Hk6tu7B4EU1`qCdDApm>nqiyL+T7x#j{tzwEjSr65%o{6 zB%f_}5hjvJelz1#h1EljEVW0}8>E_8s3>bxXW_1KIP}Z>?|oll3wOo%;7ODyJPz^N zY@okeDB=lYoE-ExPU2n}2Fi%0PS7vaG&hFviiUnE*BW#;ztB8hvkIrLdS3IqzAd`0 z<-*A~Pe#4I37B8w%+~R|>WksepMK%=<<-#z>&`X8^855_r^)&|dT*Nv-7}R1w2am~ zlR`>3r6R)MT1W+;kS}1GOtI6L?S`jzLd15(uqvhnhyc{7SQVea90!PGt70OqdDAae zu1Za@Eio%K*iu2ZTdYvraWJ<}(h|eiXo#uBG(e3g8-%;YG0;`j4mht!9KsyUt3B6r zHLno6&2%S$k}shBn$u8#;d$EdWT~S{@1bZPBDPs+5ICNp5k*9hSgAY;uM<+gKEvUD zz<%AsDv!hon;!LK6Wj@ci^}ukLu!z_^uw|275%(BFfhDzI2;~SEfZ*9ndDk{;*E8X zhHd)#$z6%KL|s>&3aez}1nvMK_PXY=%_)Cch`n^;j=N8#0tk_y zpQz=QHnLXjmr1ED7}3OGnI;s}lDC#bSL}@~*&8i<5O%NL$dbJ#&K*@&UBKAewrD7k zQL=XK(fmWWeND_c>Mx!;@&TFLf%GXM5HX@VriDloVLFjyil_sHrs> z1d_n4*C@9kD5Qne5t$2ra`^WFd|0%>UDSXAk|afxsBmtHWvebTJ}G<$bH4xiBO=C~Q2^yxNRGe5X!&aFMsnFW^Sq;1fqf(EvRTnU- z26>Kn2ve@Rl2bG0sS)UkzHgz`;b}<`ArEu(2+&L(mdoj&%zb7DWzLi35f1k}@jWW< zBSVj*3Bj821^k2k{ctUYgSm0Iqav2*L^Byj7I+349pd)&q4T{4i8^M0hq1wf6=6fhjBm$hyI@XVC&7P2k!G(0>A$ z5WS&nWi??XmzHql^(n3p9xX*O9WlW{u2}sGfrxPUIxqT6PXR@ z3sDI>8OLywRf%$iEkmcX)SN{8TlV|E$!;y+IfkkGEhs>AWufha}Kr;!j;6CaS zlEOi$m0%RoyJYy0*2MrMM5AZWW)f;P?;Z`0Zts zl&B;>YN-a4%HhK>C?-oAdE2~Jo%w&E4#6*eWy4m+5vw2nbhm+HpfRDfHfWY?5oJLkSmX97r(A0ye0U2?_AFcwAn)GX^%R6Ee3R(pPG#WWWb7f)55B zrrsn5<1;kXwflRho+em;^s<^VjVOSdsG@-ULilc;V(a@^eCK1ZzsJ1UGPiLN13Q&n z@|Vy~1=SruXNzEt+)`{|#;~x0N?CkLLAa)phTZJxlv?!4mZhptANq$4_?S^?NYf<= zMQlW3oD>iyGe*X^pQAO=i1%cSdlUP63jdRs4H5i8uQK-KZf>By+(MCQuhL^S8#3ut zfpxz?ol2dr!MamA?!7B8E`cGj7gvL7H5x90=LQ_hbZG*ZDG*76U|**05PV)Lr?v8y zofTuA6$0Z?Wh@vdUK11%1~M1WB}|1Zw(P7_F|4e=qVhg63|p93rn>{Z!ETiWdlfBY z^tyZBj%~7R?(`xraD$E!!&?H=5*KB~i1{33=RIB798*tXWOY^2A=R(p!?!G!+nT_Z z33F6=S@9>QVUx<%`4WwrxyngfoYJYPSdMG|pF%80N)gkh$~f|yG)_VE(#L%7&RYbb z7))DBYF!S}a^9EPkz#|A{q4?N9#3gq$e zWw2tHAOfZE+%adc=#e>tHFYn_Xy@-~^=#vL#Fm;Xr_cdud@0Nw?TD4uM@s8ci9c1# zkGpx3#^fvWzG1;_nRaml`YXO*%84o%uYaK)Htg3Rai`XoLSnZ@ta4?fa%I%J>aJkD zzE2a3W`|bFDLf|3=+c4pkO>#IWXc}WpxR05CR2v2s61A*G*YxQT2wzWLuSY}jCnRN zsFSFQ77Q7$A&LmgB{5{!Mo4=%7L%}Zv84}%jVeR7QGAce`{)pQ%K&+)kWqQ#a40<3 zzdH~e0QD zN{C+GweS-ZDE$i}EEt;1qQ}%!d6np#h}KS{FND1Y_X8PYYH|J*Pccs7{7>L6Y2lRp zAS3=X%xnw9PbTnq>Y3bxA^7-6R&j8(IuRlLHBs56kp7m)(qEHYsieQw#{sf~VEJG^ z+VyRi!xqO$sB_$cAenM>Wr8FF;}?32C;2TCB+$^l^vJ6<-&p+G;%}Xf)whk;w+X*b zvDvke*|pKxixWpez^ZpI&O3^Ac1Jq96UhRQsH1}VsFci~aph7datY)ZLK{)xhM?G2 zV#KLzLqMvDQdd#^n$MC{U(s#$&}>j8T%qWB4U%T-A#Ffq;|r!7Qb7RCq!Fck2O7z< z!K6mBZ9^r1h(yAXo-uD(#9J2iR!rZO{VD#|lp8sw*s3u_ewW#CY|sy1qZPv>lXg?a z4(hNOU3#RM-X*Z6=^l+GQ}(Wgcq*bUUeC=oNV9ds(}Yfy%%s%oP#~***tzl+#1umU ziipC{p3V$9YNCKQ!U7F5F20^Cq}vA1>#l_7b-ESg6qV>l#nqrJo)1HVJl7W(lzIb_ znHVToi9Th~$QMF;`rv*2QB+Y!Nyz}=_D*TW(j&v81ZBngow%FpB@u%!{T-sz)<+9d zv_nTp%|n}{A5oz-i0=4-XRMl{{pmXDM@==Oeri%zT@XH8?Imip(FipVIr{`*{Ga3I zqtF@OwTCv-IEYiomZNK|k0Ps1H3BlpwUp+~PpO#yiAX1$o`D2B{ncs7Fr565&iMKz zjV$A8b!k};W$JT_Grm%ey-bj;#^`EKQLdZ5gh z=cNdOmr7>)`_#m*LO~jU>3S~l=HOTu}8zRv;W6X|FOiqK8j zH(_48gx5&(QcOrNw^Fj61pDU88RsoXFw7uvw&-@;Y_iL%+_rr-4#~E+JK8WejG@!6 zeC4$5tQ$ud&Jl-O(`r?{E&*e1K}Ogp{2MOPyA+ZBLIA9avtq;Zzau5S+-lug(qt!S zVpSvWdvwy)*v`|D{)cf)@0(4rU56vP4v%&B#CG||cKM_I55Lbcf+xxL=v6HQRzrpAd$DcwhZ{|ON|Rdv6NFybY5FPh^mNuopM zygH9H3Zt5Y2r22w;Xo*CYC!e~ zS{Xd_xj$GQ-0-iT&C$t630C2$ZL$hcDowM`#dM%sNJ5{a>B2hZv>tYvBjq9gG#uJM z3TjzT9WnSAjZ6s&m5D)d$j-$avIHm{Tfdtq!P`D%N3*-1H{GZjx@0xMgp33goT~9cj!F%mR7uG>mRbq0oryPl=sZ5aaG$jBf z2vT}vt`kl0?U2Iadmojw#-9rlAJb(yAIYu$nCRK}-uZ5PIUt z@*B9Zrc^=SQx_OQDg`ZaP}zo%Du-_Wu12Lx;P{e!6c7(a0}ACKN*V`xLIN)9c$aTHT%yqpiJMc_)aLLmkinJqvJQP&I#hFZwT(V2^ zY5I{zAM{9q@VJ12;^|!i!7$Bf*g*ujk9D7Dh>qW-jv?p_qC@A}CrQ^zj(%2+c~u{{M2hoP$ubQCsI=R zA5O40K6jz5FZZUL;zPUyuL^MDptx(3dpG!5v@Q7l)!)1)S-t8)h46Ixmgf?I$!Qp$ z0acn-Mn@yOfKN+ZRA3G2;HQcxfw~O;r!5f@UaxylmI>a5_{XX;0%vnYdew^HMh&VM z$h}^|1HE*F+PMj>(F)-e;e~lW$#~*LjEL6TJcJkj2YQ?5l@v7V z#0GRKF~;VdD1iKzsSnL8mG5@X@=_l)c^jH2_2bSAo`cpfZ@LDS|2MQ>8ls|T%}Xb7 zXW>SHr!7SUza&Y`LP>t)RRh5a3Lh+Zozo8QX~G&<2CNEv{W|K$;`^TlTRR}W|1qj+ z&ZJS3t(Y|7)ZORQitB$ujV4Ri>Dv6xv#O*H?!JKvYj^Vq-s}ZG^JbSf-#%+5{S0*m z`c><%?8{LTOrUgSinmHse#greNBn+a?8FJQ2)Es)l{T`v{%j#u{OxO^W?or z-zt4FWJ2CU+PD)l4f;VAFOtX+mS+hn|9t{a|Axp6Bh*;-iv}gUL!Bd&_o+=OFP2@& zX&Cb~2;4!{vcMg@bSNU&%to6gCyFR^eG)~#MLBxg>E0rnP*PcbG+hn&&*6hbIt$yG zqTebFcK7uK7FC-pz!z9h=yKPR+w0n~c^ml3Bkfl@8*j139$Bp1Ukm-P zUD}I0z(6#u_&(W3!19}@uE0NBRoVi;TH?~$cRWR+ep=L{G7O{q;%)0qOKweXN@N6p zqPFKy9|}xhIE69*`dg?nM6`@&#RJ- z2l-Rx8pK;URNBfpGc7l)7)BM`aShaQrU;^b+I=Pv#!sosmU25(^%=>6V&2k-w>0Lh ziFj+G-uWN=9)C!89-=wH(jBe8=IkyljF9O$t<&aJ({8c`7eJO`o1Dw-zB;EYHfKp> z&XVYyx{+)c(qGKJl2bS4sT0tjYGncac~(DS#OA;#_;(>%t+2h_bGE`H(}0 zyB>8&oB4AG?!=d8rmOn^%oq(M% zmiLhV=tsHeE_Rn4O*d+TYd+g+6<0l`B zu75mo^0BeL{=Y09>v-VG-mX~7;qjKkuU3Ae_O;rtEgm~^bbP~6yb)Rdc%p>&uX}78 z8XUhe?#2jF@Zk(*P))hwpEfqJEV3}}X4w3%$jk0sbxKeGDq&gFfE(4WDBor~IWv{1 zifmH^`=bn78K35++7wCe(TCngq??)lj)b^Oa~Kead_t6oS@$fOt3>{@1ySqCh<|A8 zP}g@)zjgY%kG%Ov)IapT&fc4WWCLCoY%+sOgZU6I8&i6y#C+8V;wPIbGJujb+%j8w zr+U~l3P%jj6x_PicBkG&~DbOWE$pru3?*IMq-;L-3mIB%JL^hTClqG z|AVD7A)}$uxKA1!9Dks#on~sK(IP=h+(q0VCi(a36&89AXns&S`n4LL9ISk0s{Ce69NM1 zFx?qpIa5GOrqwetL9~Qcwni4P_CJX%;Gf?*^ji{5xTHR6z?jDuPpgjv`ddL=WZ?aU z@AeOw_m_@Pdx-Owj^fUQ$CpJxQ>;)8f4`0v=y58R7AUVb3-HP_k0OFR(g`&SCF!7~ zf^sJhqr_C!6?f`%XEBsocj%i@#bEXF@8&X3u0PRPw!61Yl7UrfCI0mfM^2r2yZ;gX z2mAOvj=c_C9&|k9071RSaRC2Nf&lJXLoUn-AqCiqo9Rgb_BN0$vj}_J22f)W$T5+S z4#-FKa)(i0Nl(!v{7po#uVy(xqC@r1LsU1C80cgM#Yq!KMHk#*Wyr+x0{P5o_;Amk z^Hc`T@^~t9V($ERJaa_D)oSB?q7a`Tv&t87BRC51FSs{`1#-e&a|X5aD3Oj9#->Hd zT_puVgMFt0yD(7FW0duAL^|$@)D-9&;if<_)iLjl+d3{9qs}{-)G3qb>r|%)vnzO$ zJU)0iF>^=cb0eMFWC*@Aq?5?V5bD!-r8DvU8#Gg`W@oKOJYs=A&u+Xe3;gx-I0=_+ z#EWs0!I6_P5ci4JS?-JV~+iv?I&k~ghgu?^V z$)T1;5?Vtrlx8=SW-_JJ(oCKiN^@vSTSXHG0d>?uW07c6OEr0xC^f}V++^xu2nsWe zFA@$j1f=4GXk$=VWjrMo9gp$+KyG(61+5Y2npqzomOHb3O)H5iQ$z=Dm*qqlLhy*5**%o4&P6BH{o;8l%e z$;f%o<2Y=xh;`MGA?zM|KfW0HXCMfT1gS_p?C4_C=S|N)AKQN@w%;Gw?;qcPVr(!J z8$2BuJble(`-FX~W0E%iZFNK*=tM%ZqlE~P@*NKRX$O(=3%VS1fL5W)b-D!TS