WPyrogram 2.0.149__py3-none-any.whl → 2.0.150__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: WPyrogram
3
- Version: 2.0.149
3
+ Version: 2.0.150
4
4
  Summary: Elegant, modern and asynchronous Telegram MTProto API framework in Python for users and bots - Woto's experimental fork
5
5
  Home-page: https://github.com/ALiwoto/WPyrogram
6
6
  Download-URL: https://github.com/ALiwoto/WPyrogram
@@ -1,13 +1,13 @@
1
- pyrogram/__init__.py,sha256=L-6OARe-oYpXLMQHueWSGqR9-I2LOxiaFGRcxtcbFkM,1401
2
- pyrogram/client.py,sha256=cVEv_0pVQVSooQI0fvvfTSOSFRfRwa-rEpK39665RA4,46022
3
- pyrogram/dispatcher.py,sha256=Xf1G216PcJX2rZFYujNGAcPuBKKvnGRNWPNIzJkYwUA,15465
1
+ pyrogram/__init__.py,sha256=u9UQDjfd1cxA6-EUvI2Db2oi4v9yNdlVJUxGJuFcWG4,1401
2
+ pyrogram/client.py,sha256=Tgtlw85sYEjgzI8cVPL7FwhiCvUNow7FsQcbn5Gx4nY,50088
3
+ pyrogram/dispatcher.py,sha256=cJV2tUDAfgNAh0aSjRHqiWNUBQ7ipGzBNTyJHunxLxc,13947
4
4
  pyrogram/emoji.py,sha256=ixb3OXR1XpLf3BQpw8kWIpmbQRowRIaFedjdnuzljac,208021
5
5
  pyrogram/file_id.py,sha256=jmMBAt2O_XhLE_AXKffZ4F5gccXJB8GHV7MlFCpiklA,15154
6
- pyrogram/filters.py,sha256=eR_dAl8qwF-h3SZHLhwNtskh1AZIhp_khaNthK3TwxE,28225
6
+ pyrogram/filters.py,sha256=5d7GL7W2OLwg9E8RUHLudQnbhdK7OrtFs2mSFTmoR1s,28802
7
7
  pyrogram/mime_types.py,sha256=JbYF6B-JrxZQNga8dPz2C8Dwp8-2pOrE1uv3KIBbjA0,61915
8
8
  pyrogram/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
9
9
  pyrogram/sync.py,sha256=cxKmLz9NAJPX2N2n2CogTLXyNo-dWgf6etBRXnqq1XA,3739
10
- pyrogram/utils.py,sha256=5oyowGt07WMsDWvWPMpFal-dIsh1af5Qv9KzduB_KPQ,16372
10
+ pyrogram/utils.py,sha256=HHlPe3CxShGPUidDdvCSXVgye5OeqTvmj-QUu5zyNh0,20125
11
11
  pyrogram/connection/__init__.py,sha256=q7KiWEV6VNDQGoj5s13GPyhRRlafXVPvX1h1VpAsVf8,854
12
12
  pyrogram/connection/connection.py,sha256=X5T7jd_lAXsH86f9gaNd4GDg4XIVnll7wrlVvSfgBJ0,2765
13
13
  pyrogram/connection/transport/__init__.py,sha256=nRpFdumPinBtuLOZkR8hH_vXlN7ChzW1oyJ6na4a8qw,838
@@ -3316,10 +3316,10 @@ pyrogram/types/user_and_chats/video_chat_ended.py,sha256=J5xTIAMSGDz8_z-EGrKwkoJ
3316
3316
  pyrogram/types/user_and_chats/video_chat_members_invited.py,sha256=9QhonApJd1XjOeJu9fqx2_u6nIOTZhfXZw_tX9XXxd8,1610
3317
3317
  pyrogram/types/user_and_chats/video_chat_scheduled.py,sha256=rUvDXF8r-2MfsAZKAcaWb_UZt5VoHebPjeJXWRDHGSo,1531
3318
3318
  pyrogram/types/user_and_chats/video_chat_started.py,sha256=WOAuM_CnkRA-W_Iedx0fIHB9RJ-AsvJIiSF05HzLRdA,1043
3319
- WPyrogram-2.0.149.dist-info/COPYING,sha256=ixuiBLtpoK3iv89l7ylKkg9rs2GzF9ukPH7ynZYzK5s,35148
3320
- WPyrogram-2.0.149.dist-info/COPYING.lesser,sha256=pWgb-bBdsU2Gd2kwAXxketnm5W_2u8_fIeWEgojfrxs,7651
3321
- WPyrogram-2.0.149.dist-info/METADATA,sha256=RMMZapyMNYHNe1aDLXUJJgfB0FWgopZpk_HRwfpeKek,4922
3322
- WPyrogram-2.0.149.dist-info/NOTICE,sha256=QL3iJdeVh_ZqRZZchyruQic5bYUWdx3yl0OkSUC2PQc,774
3323
- WPyrogram-2.0.149.dist-info/WHEEL,sha256=PZUExdf71Ui_so67QXpySuHtCi3-J3wvF4ORK6k_S8U,91
3324
- WPyrogram-2.0.149.dist-info/top_level.txt,sha256=vS2-tKX7dJ1uKjistE5CoOVbv4yqglA95SK0KFEDwl0,9
3325
- WPyrogram-2.0.149.dist-info/RECORD,,
3319
+ WPyrogram-2.0.150.dist-info/COPYING,sha256=ixuiBLtpoK3iv89l7ylKkg9rs2GzF9ukPH7ynZYzK5s,35148
3320
+ WPyrogram-2.0.150.dist-info/COPYING.lesser,sha256=pWgb-bBdsU2Gd2kwAXxketnm5W_2u8_fIeWEgojfrxs,7651
3321
+ WPyrogram-2.0.150.dist-info/METADATA,sha256=AnGagkc8BFEJyL0oohTeuZo7eCyZeofShG0E-m1cEHE,4922
3322
+ WPyrogram-2.0.150.dist-info/NOTICE,sha256=QL3iJdeVh_ZqRZZchyruQic5bYUWdx3yl0OkSUC2PQc,774
3323
+ WPyrogram-2.0.150.dist-info/WHEEL,sha256=PZUExdf71Ui_so67QXpySuHtCi3-J3wvF4ORK6k_S8U,91
3324
+ WPyrogram-2.0.150.dist-info/top_level.txt,sha256=vS2-tKX7dJ1uKjistE5CoOVbv4yqglA95SK0KFEDwl0,9
3325
+ WPyrogram-2.0.150.dist-info/RECORD,,
pyrogram/__init__.py CHANGED
@@ -16,7 +16,7 @@
16
16
  # You should have received a copy of the GNU Lesser General Public License
17
17
  # along with Pyrogram. If not, see <http://www.gnu.org/licenses/>.
18
18
 
19
- __version__ = "2.0.149"
19
+ __version__ = "2.0.150"
20
20
  __license__ = "GNU Lesser General Public License v3.0 (LGPL-3.0)"
21
21
  __copyright__ = "Copyright (C) 2017-present Dan <https://github.com/delivrance>"
22
22
 
pyrogram/client.py CHANGED
@@ -32,7 +32,7 @@ from importlib import import_module
32
32
  from io import StringIO, BytesIO
33
33
  from mimetypes import MimeTypes
34
34
  from pathlib import Path
35
- from typing import Union, List, Optional, Callable, AsyncGenerator
35
+ from typing import Union, List, Optional, Callable, AsyncGenerator, Type, Tuple
36
36
 
37
37
  import pyrogram
38
38
  from pyrogram import __version__, __license__
@@ -45,7 +45,8 @@ from pyrogram.errors import (
45
45
  SessionPasswordNeeded,
46
46
  VolumeLocNotFound, ChannelPrivate,
47
47
  BadRequest, AuthBytesInvalid,
48
- FloodWait, FloodPremiumWait
48
+ FloodWait, FloodPremiumWait,
49
+ ChannelInvalid, PersistentTimestampInvalid, PersistentTimestampOutdated
49
50
  )
50
51
  from pyrogram.handlers.handler import Handler
51
52
  from pyrogram.methods import Methods
@@ -53,6 +54,8 @@ from pyrogram.session import Auth, Session
53
54
  from pyrogram.storage import Storage, FileStorage, MemoryStorage
54
55
  from pyrogram.types import User, TermsOfService
55
56
  from pyrogram.utils import ainput
57
+ from .connection import Connection
58
+ from .connection.transport import TCP, TCPAbridged
56
59
  from .dispatcher import Dispatcher
57
60
  from .file_id import FileId, FileType, ThumbnailSource
58
61
  from .mime_types import mime_types
@@ -264,7 +267,9 @@ class Client(Methods):
264
267
  max_message_cache_size: int = MAX_MESSAGE_CACHE_SIZE,
265
268
  storage_engine: Optional[Storage] = None,
266
269
  client_platform: "enums.ClientPlatform" = enums.ClientPlatform.OTHER,
267
- init_connection_params: Optional["raw.base.JSONValue"] = None
270
+ init_connection_params: Optional["raw.base.JSONValue"] = None,
271
+ connection_factory: Type[Connection] = Connection,
272
+ protocol_factory: Type[TCP] = TCPAbridged
268
273
  ):
269
274
  super().__init__()
270
275
 
@@ -299,6 +304,8 @@ class Client(Methods):
299
304
  self.max_message_cache_size = max_message_cache_size
300
305
  self.client_platform = client_platform
301
306
  self.init_connection_params = init_connection_params
307
+ self.connection_factory = connection_factory
308
+ self.protocol_factory = protocol_factory
302
309
 
303
310
  self.executor = ThreadPoolExecutor(self.workers, thread_name_prefix="Handler")
304
311
 
@@ -541,25 +548,26 @@ class Client(Methods):
541
548
  async def fetch_peers(self, peers: List[Union[raw.types.User, raw.types.Chat, raw.types.Channel]]) -> bool:
542
549
  is_min = False
543
550
  parsed_peers = []
551
+ parsed_usernames = []
544
552
 
545
553
  for peer in peers:
546
554
  if getattr(peer, "min", False):
547
555
  is_min = True
548
556
  continue
549
557
 
550
- usernames = None
558
+ usernames = []
551
559
  phone_number = None
552
560
 
553
561
  if isinstance(peer, raw.types.User):
554
562
  peer_id = peer.id
555
563
  access_hash = peer.access_hash
556
- usernames = (
557
- [peer.username.lower()] if peer.username
558
- else [username.username.lower() for username in peer.usernames] if peer.usernames
559
- else None
560
- )
561
564
  phone_number = peer.phone
562
565
  peer_type = "bot" if peer.bot else "user"
566
+
567
+ if peer.username:
568
+ usernames.append(peer.username.lower())
569
+ elif peer.usernames:
570
+ usernames.extend(username.username.lower() for username in peer.usernames)
563
571
  elif isinstance(peer, (raw.types.Chat, raw.types.ChatForbidden)):
564
572
  peer_id = -peer.id
565
573
  access_hash = 0
@@ -567,12 +575,12 @@ class Client(Methods):
567
575
  elif isinstance(peer, raw.types.Channel):
568
576
  peer_id = utils.get_channel_id(peer.id)
569
577
  access_hash = peer.access_hash
570
- usernames = (
571
- [peer.username.lower()] if peer.username
572
- else [username.username.lower() for username in peer.usernames] if peer.usernames
573
- else None
574
- )
575
578
  peer_type = "channel" if peer.broadcast else "supergroup"
579
+
580
+ if peer.username:
581
+ usernames.append(peer.username.lower())
582
+ elif peer.usernames:
583
+ usernames.extend(username.username.lower() for username in peer.usernames)
576
584
  elif isinstance(peer, raw.types.ChannelForbidden):
577
585
  peer_id = utils.get_channel_id(peer.id)
578
586
  access_hash = peer.access_hash
@@ -580,9 +588,11 @@ class Client(Methods):
580
588
  else:
581
589
  continue
582
590
 
583
- parsed_peers.append((peer_id, access_hash, peer_type, usernames, phone_number))
591
+ parsed_peers.append((peer_id, access_hash, peer_type, phone_number))
592
+ parsed_usernames.append((peer_id, usernames))
584
593
 
585
594
  await self.storage.update_peers(parsed_peers)
595
+ await self.storage.update_usernames(parsed_usernames)
586
596
 
587
597
  return is_min
588
598
 
@@ -639,10 +649,11 @@ class Client(Methods):
639
649
  )]
640
650
  ),
641
651
  pts=pts - pts_count,
642
- limit=pts
652
+ limit=pts,
653
+ force=False
643
654
  )
644
655
  )
645
- except ChannelPrivate:
656
+ except (ChannelPrivate, PersistentTimestampOutdated, PersistentTimestampInvalid):
646
657
  pass
647
658
  else:
648
659
  if not isinstance(diff, raw.types.updates.ChannelDifferenceEmpty):
@@ -688,6 +699,92 @@ class Client(Methods):
688
699
  elif isinstance(updates, raw.types.UpdatesTooLong):
689
700
  log.info(updates)
690
701
 
702
+ async def recover_gaps(self) -> Tuple[int, int]:
703
+ states = await self.storage.update_state()
704
+
705
+ message_updates_counter = 0
706
+ other_updates_counter = 0
707
+
708
+ if not states:
709
+ log.info("No states found, skipping recovery.")
710
+ return (message_updates_counter, other_updates_counter)
711
+
712
+ for state in states:
713
+ id, local_pts, _, local_date, _ = state
714
+
715
+ prev_pts = 0
716
+
717
+ while True:
718
+ try:
719
+ diff = await self.invoke(
720
+ raw.functions.updates.GetChannelDifference(
721
+ channel=await self.resolve_peer(id),
722
+ filter=raw.types.ChannelMessagesFilterEmpty(),
723
+ pts=local_pts,
724
+ limit=10000,
725
+ force=False
726
+ ) if id < 0 else
727
+ raw.functions.updates.GetDifference(
728
+ pts=local_pts,
729
+ date=local_date,
730
+ qts=0
731
+ )
732
+ )
733
+ except (ChannelPrivate, ChannelInvalid, PersistentTimestampOutdated, PersistentTimestampInvalid):
734
+ break
735
+
736
+ if isinstance(diff, raw.types.updates.DifferenceEmpty):
737
+ break
738
+ elif isinstance(diff, raw.types.updates.DifferenceTooLong):
739
+ break
740
+ elif isinstance(diff, raw.types.updates.Difference):
741
+ local_pts = diff.state.pts
742
+ elif isinstance(diff, raw.types.updates.DifferenceSlice):
743
+ local_pts = diff.intermediate_state.pts
744
+ local_date = diff.intermediate_state.date
745
+
746
+ if prev_pts == local_pts:
747
+ break
748
+
749
+ prev_pts = local_pts
750
+ elif isinstance(diff, raw.types.updates.ChannelDifferenceEmpty):
751
+ break
752
+ elif isinstance(diff, raw.types.updates.ChannelDifferenceTooLong):
753
+ break
754
+ elif isinstance(diff, raw.types.updates.ChannelDifference):
755
+ local_pts = diff.pts
756
+
757
+ users = {i.id: i for i in diff.users}
758
+ chats = {i.id: i for i in diff.chats}
759
+
760
+ for message in diff.new_messages:
761
+ message_updates_counter += 1
762
+ self.dispatcher.updates_queue.put_nowait(
763
+ (
764
+ raw.types.UpdateNewMessage(
765
+ message=message,
766
+ pts=local_pts,
767
+ pts_count=-1
768
+ ),
769
+ users,
770
+ chats
771
+ )
772
+ )
773
+
774
+ for update in diff.other_updates:
775
+ other_updates_counter += 1
776
+ self.dispatcher.updates_queue.put_nowait(
777
+ (update, users, chats)
778
+ )
779
+
780
+ if isinstance(diff, (raw.types.updates.Difference, raw.types.updates.ChannelDifference)):
781
+ break
782
+
783
+ await self.storage.update_state(id)
784
+
785
+ log.info("Recovered %s messages and %s updates.", message_updates_counter, other_updates_counter)
786
+ return (message_updates_counter, other_updates_counter)
787
+
691
788
  async def load_session(self):
692
789
  await self.storage.open()
693
790
 
pyrogram/dispatcher.py CHANGED
@@ -22,13 +22,12 @@ import logging
22
22
  from collections import OrderedDict
23
23
 
24
24
  import pyrogram
25
- from pyrogram import errors
26
25
  from pyrogram import utils
27
- from pyrogram import raw
28
26
  from pyrogram.handlers import (
29
27
  CallbackQueryHandler, MessageHandler, EditedMessageHandler, DeletedMessagesHandler,
30
28
  UserStatusHandler, RawUpdateHandler, InlineQueryHandler, PollHandler, PreCheckoutQueryHandler,
31
- ChosenInlineResultHandler, ChatMemberUpdatedHandler, ChatJoinRequestHandler, StoryHandler
29
+ ChosenInlineResultHandler, ChatMemberUpdatedHandler, ChatJoinRequestHandler, StoryHandler,
30
+ ShippingQueryHandler, MessageReactionHandler, MessageReactionCountHandler, ChatBoostHandler
32
31
  )
33
32
  from pyrogram.raw.types import (
34
33
  UpdateNewMessage, UpdateNewChannelMessage, UpdateNewScheduledMessage,
@@ -38,7 +37,9 @@ from pyrogram.raw.types import (
38
37
  UpdateBotCallbackQuery, UpdateInlineBotCallbackQuery, UpdateBotPrecheckoutQuery,
39
38
  UpdateUserStatus, UpdateBotInlineQuery, UpdateMessagePoll,
40
39
  UpdateBotInlineSend, UpdateChatParticipant, UpdateChannelParticipant,
41
- UpdateBotChatInviteRequester, UpdateStory
40
+ UpdateBotChatInviteRequester, UpdateStory, UpdateBotShippingQuery, UpdateBotMessageReaction,
41
+ UpdateBotMessageReactions, UpdateBotChatBoost, UpdateBusinessBotCallbackQuery,
42
+ UpdateBotPurchasedPaidMedia, UpdateMessagePollVote
42
43
  )
43
44
 
44
45
  log = logging.getLogger(__name__)
@@ -48,15 +49,20 @@ class Dispatcher:
48
49
  NEW_MESSAGE_UPDATES = (UpdateNewMessage, UpdateNewChannelMessage, UpdateNewScheduledMessage, UpdateBotNewBusinessMessage)
49
50
  EDIT_MESSAGE_UPDATES = (UpdateEditMessage, UpdateEditChannelMessage, UpdateBotEditBusinessMessage)
50
51
  DELETE_MESSAGES_UPDATES = (UpdateDeleteMessages, UpdateDeleteChannelMessages, UpdateBotDeleteBusinessMessage)
51
- CALLBACK_QUERY_UPDATES = (UpdateBotCallbackQuery, UpdateInlineBotCallbackQuery)
52
+ CALLBACK_QUERY_UPDATES = (UpdateBotCallbackQuery, UpdateInlineBotCallbackQuery, UpdateBusinessBotCallbackQuery)
52
53
  CHAT_MEMBER_UPDATES = (UpdateChatParticipant, UpdateChannelParticipant)
53
54
  USER_STATUS_UPDATES = (UpdateUserStatus,)
54
55
  BOT_INLINE_QUERY_UPDATES = (UpdateBotInlineQuery,)
55
- POLL_UPDATES = (UpdateMessagePoll,)
56
+ POLL_UPDATES = (UpdateMessagePoll, UpdateMessagePollVote)
56
57
  CHOSEN_INLINE_RESULT_UPDATES = (UpdateBotInlineSend,)
57
58
  CHAT_JOIN_REQUEST_UPDATES = (UpdateBotChatInviteRequester,)
58
59
  NEW_STORY_UPDATES = (UpdateStory,)
59
60
  PRE_CHECKOUT_QUERY_UPDATES = (UpdateBotPrecheckoutQuery,)
61
+ SHIPPING_QUERY_UPDATES = (UpdateBotShippingQuery,)
62
+ MESSAGE_REACTION_UPDATES = (UpdateBotMessageReaction,)
63
+ MESSAGE_REACTION_COUNT_UPDATES = (UpdateBotMessageReactions,)
64
+ CHAT_BOOST_UPDATES = (UpdateBotChatBoost,)
65
+ PURCHASED_PAID_MEDIA_UPDATES = (UpdateBotPurchasedPaidMedia,)
60
66
 
61
67
  def __init__(self, client: "pyrogram.Client"):
62
68
  self.client = client
@@ -69,6 +75,8 @@ class Dispatcher:
69
75
  self.groups = OrderedDict()
70
76
 
71
77
  async def message_parser(update, users, chats):
78
+ connection_id = getattr(update, "connection_id", None)
79
+
72
80
  return (
73
81
  await pyrogram.types.Message._parse(
74
82
  self.client,
@@ -76,8 +84,9 @@ class Dispatcher:
76
84
  users,
77
85
  chats,
78
86
  is_scheduled=isinstance(update, UpdateNewScheduledMessage),
79
- business_connection_id=getattr(update, "connection_id", None),
80
- reply_to_message=getattr(update, "reply_to_message", None)
87
+ replies=0 if getattr(update, "connection_id", None) else 1,
88
+ business_connection_id=connection_id,
89
+ raw_reply_to_message=getattr(update, "reply_to_message", None)
81
90
  ),
82
91
  MessageHandler
83
92
  )
@@ -99,7 +108,7 @@ class Dispatcher:
99
108
 
100
109
  async def callback_query_parser(update, users, chats):
101
110
  return (
102
- await pyrogram.types.CallbackQuery._parse(self.client, update, users),
111
+ await pyrogram.types.CallbackQuery._parse(self.client, update, users, chats),
103
112
  CallbackQueryHandler
104
113
  )
105
114
 
@@ -117,7 +126,7 @@ class Dispatcher:
117
126
 
118
127
  async def poll_parser(update, users, chats):
119
128
  return (
120
- pyrogram.types.Poll._parse_update(self.client, update),
129
+ pyrogram.types.Poll._parse_update(self.client, update, users),
121
130
  PollHandler
122
131
  )
123
132
 
@@ -151,6 +160,36 @@ class Dispatcher:
151
160
  PreCheckoutQueryHandler
152
161
  )
153
162
 
163
+ async def shipping_query_parser(update, users, chats):
164
+ return (
165
+ await pyrogram.types.ShippingQuery._parse(self.client, update, users),
166
+ ShippingQueryHandler
167
+ )
168
+
169
+ async def message_reaction_parser(update, users, chats):
170
+ return (
171
+ pyrogram.types.MessageReactionUpdated._parse(self.client, update, users, chats),
172
+ MessageReactionHandler
173
+ )
174
+
175
+ async def message_reaction_count_parser(update, users, chats):
176
+ return (
177
+ pyrogram.types.MessageReactionCountUpdated._parse(self.client, update, users, chats),
178
+ MessageReactionCountHandler
179
+ )
180
+
181
+ async def chat_boost_parser(update, users, chats):
182
+ return (
183
+ pyrogram.types.ChatBoostUpdated._parse(self.client, update, users, chats),
184
+ ChatBoostHandler
185
+ )
186
+
187
+ async def purchased_paid_media_parser(update, users, chats):
188
+ return (
189
+ pyrogram.types.PurchasedPaidMedia._parse(self.client, update, users),
190
+ ChatBoostHandler
191
+ )
192
+
154
193
  self.update_parsers = {
155
194
  Dispatcher.NEW_MESSAGE_UPDATES: message_parser,
156
195
  Dispatcher.EDIT_MESSAGE_UPDATES: edited_message_parser,
@@ -163,7 +202,12 @@ class Dispatcher:
163
202
  Dispatcher.CHAT_MEMBER_UPDATES: chat_member_updated_parser,
164
203
  Dispatcher.CHAT_JOIN_REQUEST_UPDATES: chat_join_request_parser,
165
204
  Dispatcher.NEW_STORY_UPDATES: story_parser,
166
- Dispatcher.PRE_CHECKOUT_QUERY_UPDATES: pre_checkout_query_parser
205
+ Dispatcher.PRE_CHECKOUT_QUERY_UPDATES: pre_checkout_query_parser,
206
+ Dispatcher.SHIPPING_QUERY_UPDATES: shipping_query_parser,
207
+ Dispatcher.MESSAGE_REACTION_UPDATES: message_reaction_parser,
208
+ Dispatcher.MESSAGE_REACTION_COUNT_UPDATES: message_reaction_count_parser,
209
+ Dispatcher.CHAT_BOOST_UPDATES: chat_boost_parser,
210
+ Dispatcher.PURCHASED_PAID_MEDIA_UPDATES: purchased_paid_media_parser,
167
211
  }
168
212
 
169
213
  self.update_parsers = {key: value for key_tuple, value in self.update_parsers.items() for key in key_tuple}
@@ -180,93 +224,7 @@ class Dispatcher:
180
224
  log.info("Started %s HandlerTasks", self.client.workers)
181
225
 
182
226
  if not self.client.skip_updates:
183
- states = await self.client.storage.update_state()
184
-
185
- if not states:
186
- log.info("No states found, skipping recovery.")
187
- return
188
-
189
- message_updates_counter = 0
190
- other_updates_counter = 0
191
-
192
- for state in states:
193
- id, local_pts, _, local_date, _ = state
194
-
195
- prev_pts = 0
196
-
197
- while True:
198
- try:
199
- diff = await self.client.invoke(
200
- raw.functions.updates.GetChannelDifference(
201
- channel=await self.client.resolve_peer(id),
202
- filter=raw.types.ChannelMessagesFilterEmpty(),
203
- pts=local_pts,
204
- limit=10000
205
- ) if id < 0 else
206
- raw.functions.updates.GetDifference(
207
- pts=local_pts,
208
- date=local_date,
209
- qts=0
210
- )
211
- )
212
- except (errors.ChannelPrivate, errors.ChannelInvalid):
213
- break
214
-
215
- if isinstance(diff, raw.types.updates.DifferenceEmpty):
216
- break
217
- elif isinstance(diff, raw.types.updates.DifferenceTooLong):
218
- break
219
- elif isinstance(diff, raw.types.updates.Difference):
220
- local_pts = diff.state.pts
221
- elif isinstance(diff, raw.types.updates.DifferenceSlice):
222
- local_pts = diff.intermediate_state.pts
223
- local_date = diff.intermediate_state.date
224
-
225
- if prev_pts == local_pts:
226
- break
227
-
228
- prev_pts = local_pts
229
- elif isinstance(diff, raw.types.updates.ChannelDifferenceEmpty):
230
- break
231
- elif isinstance(diff, raw.types.updates.ChannelDifferenceTooLong):
232
- break
233
- elif isinstance(diff, raw.types.updates.ChannelDifference):
234
- local_pts = diff.pts
235
-
236
- users = {i.id: i for i in getattr(diff, "users", [])}
237
- chats = {i.id: i for i in getattr(diff, "chats", [])}
238
-
239
- for message in diff.new_messages:
240
- message_updates_counter += 1
241
- self.updates_queue.put_nowait(
242
- (
243
- raw.types.UpdateNewMessage(
244
- message=message,
245
- pts=local_pts,
246
- pts_count=-1
247
- ) if id == self.client.me.id else
248
- raw.types.UpdateNewChannelMessage(
249
- message=message,
250
- pts=local_pts,
251
- pts_count=-1
252
- ),
253
- users,
254
- chats
255
- )
256
- )
257
-
258
- for update in diff.other_updates:
259
- other_updates_counter += 1
260
- self.updates_queue.put_nowait(
261
- (update, users, chats)
262
- )
263
-
264
- if isinstance(diff, (raw.types.updates.Difference, raw.types.updates.ChannelDifference)):
265
- break
266
-
267
- await self.client.storage.update_state(id)
268
-
269
- log.info("Recovered %s messages and %s updates.", message_updates_counter, other_updates_counter)
227
+ await self.client.recover_gaps()
270
228
 
271
229
  async def stop(self):
272
230
  if not self.client.no_updates:
@@ -345,7 +303,12 @@ class Dispatcher:
345
303
  continue
346
304
 
347
305
  elif isinstance(handler, RawUpdateHandler):
348
- args = (update, users, chats)
306
+ try:
307
+ if await handler.check(self.client, update):
308
+ args = (update, users, chats)
309
+ except Exception as e:
310
+ log.exception(e)
311
+ continue
349
312
 
350
313
  if args is None:
351
314
  continue
pyrogram/filters.py CHANGED
@@ -18,7 +18,7 @@
18
18
 
19
19
  import inspect
20
20
  import re
21
- from typing import Callable, Union, List, Pattern
21
+ from typing import Callable, Union, List, Pattern, Optional
22
22
 
23
23
  import pyrogram
24
24
  from pyrogram import enums
@@ -261,6 +261,16 @@ caption = create(caption_filter)
261
261
 
262
262
  # endregion
263
263
 
264
+ # region self_destruction_filter
265
+ async def self_destruction_filter(_, __, m: Message):
266
+ return bool(m.media and getattr(getattr(m, m.media.value, None), "ttl_seconds", None))
267
+
268
+
269
+ self_destruction = create(self_destruction_filter)
270
+ """Filter self-destruction media messages."""
271
+
272
+
273
+ # endregion
264
274
 
265
275
  # region audio_filter
266
276
  async def audio_filter(_, __, m: Message):
@@ -339,13 +349,13 @@ giveaway = create(giveaway_filter)
339
349
 
340
350
  # endregion
341
351
 
342
- # region giveaway_result_filter
343
- async def giveaway_result_filter(_, __, m: Message):
344
- return bool(m.giveaway_result)
352
+ # region giveaway_winners_filter
353
+ async def giveaway_winners_filter(_, __, m: Message):
354
+ return bool(m.giveaway_winners)
345
355
 
346
356
 
347
- giveaway_result = create(giveaway_result_filter)
348
- """Filter messages that contain :obj:`~pyrogram.types.GiveawayResult` objects."""
357
+ giveaway_winners = create(giveaway_winners_filter)
358
+ """Filter messages that contain :obj:`~pyrogram.types.GiveawayWinners` objects."""
349
359
 
350
360
 
351
361
  # endregion
@@ -359,6 +369,17 @@ gift_code = create(gift_code_filter)
359
369
  """Filter messages that contain :obj:`~pyrogram.types.GiftCode` objects."""
360
370
 
361
371
 
372
+ # endregion
373
+
374
+ # region star_gift_filter
375
+ async def star_gift_filter(_, __, m: Message):
376
+ return bool(m.star_gift)
377
+
378
+
379
+ star_gift = create(star_gift_filter)
380
+ """Filter messages that contain :obj:`~pyrogram.types.StarGift` objects."""
381
+
382
+
362
383
  # endregion
363
384
 
364
385
  # region requested_chats_filter
@@ -971,7 +992,7 @@ def regex(pattern: Union[str, Pattern], flags: int = 0):
971
992
  elif isinstance(update, InlineQuery):
972
993
  value = update.query
973
994
  elif isinstance(update, PreCheckoutQuery):
974
- value = update.payload
995
+ value = update.invoice_payload
975
996
  else:
976
997
  raise ValueError(f"Regex filter doesn't work with {type(update)}")
977
998
 
@@ -1001,7 +1022,7 @@ class user(Filter, set):
1001
1022
  Defaults to None (no users).
1002
1023
  """
1003
1024
 
1004
- def __init__(self, users: Union[int, str, List[Union[int, str]]] = None):
1025
+ def __init__(self, users: Optional[Union[int, str, List[Union[int, str]]]] = None):
1005
1026
  users = [] if users is None else users if isinstance(users, list) else [users]
1006
1027
 
1007
1028
  super().__init__(
@@ -1033,7 +1054,7 @@ class chat(Filter, set):
1033
1054
  Defaults to None (no chats).
1034
1055
  """
1035
1056
 
1036
- def __init__(self, chats: Union[int, str, List[Union[int, str]]] = None):
1057
+ def __init__(self, chats: Optional[Union[int, str, List[Union[int, str]]]] = None):
1037
1058
  chats = [] if chats is None else chats if isinstance(chats, list) else [chats]
1038
1059
 
1039
1060
  super().__init__(
@@ -1066,7 +1087,7 @@ class topic(Filter, set):
1066
1087
  Defaults to None (no topics).
1067
1088
  """
1068
1089
 
1069
- def __init__(self, topics: Union[int, List[int]] = None):
1090
+ def __init__(self, topics: Optional[Union[int, List[int]]] = None):
1070
1091
  topics = [] if topics is None else topics if isinstance(topics, list) else [topics]
1071
1092
 
1072
1093
  super().__init__(
pyrogram/utils.py CHANGED
@@ -16,21 +16,23 @@
16
16
  # You should have received a copy of the GNU Lesser General Public License
17
17
  # along with Pyrogram. If not, see <http://www.gnu.org/licenses/>.
18
18
 
19
+ from concurrent.futures.thread import ThreadPoolExecutor
20
+ from datetime import datetime, timezone
21
+ from getpass import getpass
22
+ from io import BytesIO
23
+ from typing import Union, List, Dict, Optional
19
24
  import asyncio
20
25
  import base64
21
26
  import functools
22
27
  import hashlib
23
28
  import os
24
- import struct
25
- from concurrent.futures.thread import ThreadPoolExecutor
26
- from datetime import datetime, timezone
27
- from getpass import getpass
28
29
  import re
29
- from typing import Union, List, Dict, Optional
30
+ import struct
30
31
 
31
32
  import pyrogram
32
33
  from pyrogram import raw, enums
33
34
  from pyrogram import types
35
+ from pyrogram.types.messages_and_media.message import Str
34
36
  from pyrogram.file_id import FileId, FileType, PHOTO_TYPES, DOCUMENT_TYPES
35
37
 
36
38
 
@@ -89,122 +91,112 @@ def get_input_media_from_file_id(
89
91
 
90
92
 
91
93
  async def parse_messages(
92
- client,
93
- messages: "raw.types.messages.Messages",
94
+ client: "pyrogram.Client",
95
+ messages: Union["raw.base.messages.Messages", "raw.base.Updates"],
94
96
  replies: int = 1,
95
97
  business_connection_id: str = None
96
98
  ) -> List["types.Message"]:
97
- users = {i.id: i for i in messages.users if hasattr(i, "id")}
98
- chats = {i.id: i for i in messages.chats if hasattr(i, "id")}
99
- topics = {i.id: i for i in messages.topics if hasattr(i, "id")} \
100
- if hasattr(messages, "topics") else None
101
-
102
- if not messages.messages:
103
- return types.List()
99
+ users = {i.id: i for i in getattr(messages, "users", [])}
100
+ chats = {i.id: i for i in getattr(messages, "chats", [])}
101
+ topics = {i.id: i for i in getattr(messages, "topics", [])}
104
102
 
105
103
  parsed_messages = []
106
104
 
107
- for message in messages.messages:
108
- parsed_messages.append(
109
- await types.Message._parse(
110
- client,
111
- message,
112
- users,
113
- chats,
114
- topics,
115
- replies=0,
116
- business_connection_id=business_connection_id
117
- )
105
+ if isinstance(
106
+ messages,
107
+ (
108
+ raw.types.messages.ChannelMessages,
109
+ raw.types.messages.Messages,
110
+ raw.types.messages.MessagesNotModified,
111
+ raw.types.messages.MessagesSlice
118
112
  )
113
+ ):
114
+ if not messages.messages:
115
+ return types.List()
116
+
117
+ for message in messages.messages:
118
+ parsed_messages.append(
119
+ await types.Message._parse(
120
+ client=client,
121
+ message=message,
122
+ users=users,
123
+ chats=chats,
124
+ topics=topics,
125
+ replies=0,
126
+ business_connection_id=business_connection_id
127
+ )
128
+ )
119
129
 
120
- if replies:
121
- messages_with_replies = {
122
- i.id: i.reply_to
123
- for i in messages.messages
124
- if not isinstance(i, raw.types.MessageEmpty) and i.reply_to and isinstance(i.reply_to, raw.types.MessageReplyHeader)
125
- }
126
-
127
- message_reply_to_story = {
128
- i.id: {'user_id': i.reply_to.user_id, 'story_id': i.reply_to.story_id}
129
- for i in messages.messages
130
- if not isinstance(i, raw.types.MessageEmpty) and i.reply_to and isinstance(i.reply_to, raw.types.MessageReplyStoryHeader)
131
- }
132
-
133
- if messages_with_replies:
134
- # We need a chat id, but some messages might be empty (no chat attribute available)
135
- # Scan until we find a message with a chat available (there must be one, because we are fetching replies)
136
- for m in parsed_messages:
137
- if not isinstance(m, types.Message):
138
- continue
139
-
140
- if m.chat:
141
- chat_id = m.chat.id
142
- break
143
- else:
144
- chat_id = 0
130
+ if replies:
131
+ messages_with_replies = {}
132
+ messages_with_story_replies = {}
145
133
 
146
- is_all_within_chat = not any(
147
- value.reply_to_peer_id
148
- for value in messages_with_replies.values()
149
- )
150
- reply_messages: List[pyrogram.types.Message] = []
151
- if is_all_within_chat:
152
- # fast path: fetch all messages within the same chat
153
- reply_messages = await client.get_messages(
154
- chat_id,
155
- reply_to_message_ids=messages_with_replies.keys(),
156
- replies=replies - 1
157
- )
158
- else:
159
- # slow path: fetch all messages individually
160
- for target_reply_to in messages_with_replies.values():
161
- to_be_added_msg = None
162
- the_chat_id = chat_id
163
- if target_reply_to.reply_to_peer_id:
164
- the_chat_id = get_channel_id(target_reply_to.reply_to_peer_id.channel_id)
165
- to_be_added_msg = await client.get_messages(
166
- chat_id=the_chat_id,
167
- message_ids=target_reply_to.reply_to_msg_id,
168
- replies=replies - 1
169
- )
170
- if isinstance(to_be_added_msg, list):
171
- for current_to_be_added in to_be_added_msg:
172
- reply_messages.append(current_to_be_added)
173
- elif to_be_added_msg:
174
- reply_messages.append(to_be_added_msg)
175
-
176
- for message in parsed_messages:
177
- reply_to = messages_with_replies.get(message.id, None)
178
- if not reply_to:
134
+ for m in messages.messages:
135
+ if isinstance(m, raw.types.MessageEmpty):
179
136
  continue
180
137
 
181
- reply_id = reply_to.reply_to_msg_id
138
+ if m.reply_to and isinstance(m.reply_to, raw.types.MessageReplyHeader):
139
+ messages_with_replies[m.id] = m.reply_to
182
140
 
183
- for reply in reply_messages:
184
- if reply.id == reply_id and not reply.forum_topic_created:
185
- message.reply_to_message = reply
141
+ if m.reply_to and isinstance(m.reply_to, raw.types.MessageReplyStoryHeader):
142
+ messages_with_story_replies[m.id] = m.reply_to
186
143
 
187
- if message_reply_to_story:
188
- for m in parsed_messages:
189
- if not isinstance(m, types.Message):
190
- continue
144
+ if messages_with_replies:
145
+ # We need a chat id, but some messages might be empty (no chat attribute available)
146
+ # Scan until we find a message with a chat available (there must be one, because we are fetching replies)
147
+ chat_id = next((m.chat.id for m in parsed_messages if m.chat), 0)
191
148
 
192
- if m.chat:
193
- chat_id = m.chat.id
194
- break
195
- else:
196
- chat_id = 0
149
+ is_all_replies_in_same_chat = not any(m.reply_to_peer_id for m in messages_with_replies.values())
150
+ reply_messages: List["types.Message"] = []
197
151
 
198
- reply_messages = {}
199
- for msg_id in message_reply_to_story:
200
- reply_messages[msg_id] = await client.get_stories(
201
- message_reply_to_story[msg_id]['user_id'],
202
- message_reply_to_story[msg_id]['story_id']
152
+ if is_all_replies_in_same_chat:
153
+ reply_messages = await client.get_messages(
154
+ chat_id,
155
+ reply_to_message_ids=list(messages_with_replies.keys()),
156
+ replies=replies - 1
157
+ )
158
+ else:
159
+ for reply_header in messages_with_replies.values():
160
+ reply_messages.append(
161
+ await client.get_messages(
162
+ chat_id=get_peer_id(reply_header.reply_to_peer_id) if getattr(reply_header, "reply_to_peer_id", None) else chat_id,
163
+ message_ids=reply_header.reply_to_msg_id,
164
+ replies=replies - 1
165
+ )
166
+ )
167
+
168
+ for message in parsed_messages:
169
+ reply_to = messages_with_replies.get(message.id, None)
170
+
171
+ if not reply_to:
172
+ continue
173
+
174
+ for reply in reply_messages:
175
+ if reply.id == reply_to.reply_to_msg_id:
176
+ message.reply_to_message = reply
177
+ else:
178
+ for u in getattr(messages, "updates", []):
179
+ if isinstance(
180
+ u,
181
+ (
182
+ raw.types.UpdateNewMessage,
183
+ raw.types.UpdateNewChannelMessage,
184
+ raw.types.UpdateNewScheduledMessage,
185
+ raw.types.UpdateBotNewBusinessMessage,
186
+ )
187
+ ):
188
+ parsed_messages.append(
189
+ await types.Message._parse(
190
+ client,
191
+ u.message,
192
+ users,
193
+ chats,
194
+ is_scheduled=isinstance(u, raw.types.UpdateNewScheduledMessage),
195
+ business_connection_id=getattr(u, "connection_id", business_connection_id),
196
+ raw_reply_to_message=getattr(u, "reply_to_message", None),
197
+ replies=0
198
+ )
203
199
  )
204
-
205
- for message in parsed_messages:
206
- if message.id in reply_messages:
207
- message.reply_to_story = reply_messages[message.id]
208
200
 
209
201
  return types.List(parsed_messages)
210
202
 
@@ -212,7 +204,6 @@ async def parse_messages(
212
204
  def parse_deleted_messages(client, update, users, chats) -> List["types.Message"]:
213
205
  messages = update.messages
214
206
  channel_id = getattr(update, "channel_id", None)
215
- business_connection_id = getattr(update, "connection_id", None)
216
207
  peer = getattr(update, "peer", None)
217
208
 
218
209
  chat = None
@@ -244,7 +235,7 @@ def parse_deleted_messages(client, update, users, chats) -> List["types.Message"
244
235
  types.Message(
245
236
  id=message,
246
237
  chat=chat,
247
- business_connection_id=business_connection_id,
238
+ business_connection_id=getattr(update, "connection_id", None),
248
239
  client=client
249
240
  )
250
241
  )
@@ -465,8 +456,8 @@ def compute_password_check(
465
456
  async def parse_text_entities(
466
457
  client: "pyrogram.Client",
467
458
  text: str,
468
- parse_mode: enums.ParseMode,
469
- entities: List["types.MessageEntity"]
459
+ parse_mode: Optional[enums.ParseMode],
460
+ entities: Optional[List["types.MessageEntity"]]
470
461
  ) -> Dict[str, Union[str, List[raw.base.MessageEntity]]]:
471
462
  if entities:
472
463
  # Inject the client instance because parsing user mentions requires it
@@ -506,3 +497,80 @@ def get_first_url(text):
506
497
  matches = re.findall(r"(https?):\/\/([\w_-]+(?:(?:\.[\w_-]+)+))([\w.,@?^=%&:\/~+#-]*[\w@?^=%&\/~+#-])", text)
507
498
 
508
499
  return f"{matches[0][0]}://{matches[0][1]}{matches[0][2]}" if matches else None
500
+
501
+
502
+ def parse_text_with_entities(client, message: "raw.types.TextWithEntities", users):
503
+ entities = types.List(
504
+ filter(
505
+ lambda x: x is not None,
506
+ [
507
+ types.MessageEntity._parse(client, entity, users)
508
+ for entity in getattr(message, "entities", [])
509
+ ]
510
+ )
511
+ )
512
+
513
+ return {
514
+ "text": Str(getattr(message, "text", "")).init(entities) or None,
515
+ "entities": entities or None
516
+ }
517
+
518
+
519
+ def expand_inline_bytes(bytes_data: bytes):
520
+ if len(bytes_data) < 3 or bytes_data[0] != 0x01:
521
+ return bytearray()
522
+
523
+ header = bytearray(
524
+ b"\xff\xd8\xff\xe0\x00\x10\x4a\x46\x49"
525
+ b"\x46\x00\x01\x01\x00\x00\x01\x00\x01\x00\x00\xff\xdb\x00\x43\x00\x28\x1c"
526
+ b"\x1e\x23\x1e\x19\x28\x23\x21\x23\x2d\x2b\x28\x30\x3c\x64\x41\x3c\x37\x37"
527
+ b"\x3c\x7b\x58\x5d\x49\x64\x91\x80\x99\x96\x8f\x80\x8c\x8a\xa0\xb4\xe6\xc3"
528
+ b"\xa0\xaa\xda\xad\x8a\x8c\xc8\xff\xcb\xda\xee\xf5\xff\xff\xff\x9b\xc1\xff"
529
+ b"\xff\xff\xfa\xff\xe6\xfd\xff\xf8\xff\xdb\x00\x43\x01\x2b\x2d\x2d\x3c\x35"
530
+ b"\x3c\x76\x41\x41\x76\xf8\xa5\x8c\xa5\xf8\xf8\xf8\xf8\xf8\xf8\xf8\xf8\xf8"
531
+ b"\xf8\xf8\xf8\xf8\xf8\xf8\xf8\xf8\xf8\xf8\xf8\xf8\xf8\xf8\xf8\xf8\xf8\xf8"
532
+ b"\xf8\xf8\xf8\xf8\xf8\xf8\xf8\xf8\xf8\xf8\xf8\xf8\xf8\xf8\xf8\xf8\xf8\xf8"
533
+ b"\xf8\xf8\xf8\xf8\xf8\xff\xc0\x00\x11\x08\x00\x00\x00\x00\x03\x01\x22\x00"
534
+ b"\x02\x11\x01\x03\x11\x01\xff\xc4\x00\x1f\x00\x00\x01\x05\x01\x01\x01\x01"
535
+ b"\x01\x01\x00\x00\x00\x00\x00\x00\x00\x00\x01\x02\x03\x04\x05\x06\x07\x08"
536
+ b"\x09\x0a\x0b\xff\xc4\x00\xb5\x10\x00\x02\x01\x03\x03\x02\x04\x03\x05\x05"
537
+ b"\x04\x04\x00\x00\x01\x7d\x01\x02\x03\x00\x04\x11\x05\x12\x21\x31\x41\x06"
538
+ b"\x13\x51\x61\x07\x22\x71\x14\x32\x81\x91\xa1\x08\x23\x42\xb1\xc1\x15\x52"
539
+ b"\xd1\xf0\x24\x33\x62\x72\x82\x09\x0a\x16\x17\x18\x19\x1a\x25\x26\x27\x28"
540
+ b"\x29\x2a\x34\x35\x36\x37\x38\x39\x3a\x43\x44\x45\x46\x47\x48\x49\x4a\x53"
541
+ b"\x54\x55\x56\x57\x58\x59\x5a\x63\x64\x65\x66\x67\x68\x69\x6a\x73\x74\x75"
542
+ b"\x76\x77\x78\x79\x7a\x83\x84\x85\x86\x87\x88\x89\x8a\x92\x93\x94\x95\x96"
543
+ b"\x97\x98\x99\x9a\xa2\xa3\xa4\xa5\xa6\xa7\xa8\xa9\xaa\xb2\xb3\xb4\xb5\xb6"
544
+ b"\xb7\xb8\xb9\xba\xc2\xc3\xc4\xc5\xc6\xc7\xc8\xc9\xca\xd2\xd3\xd4\xd5\xd6"
545
+ b"\xd7\xd8\xd9\xda\xe1\xe2\xe3\xe4\xe5\xe6\xe7\xe8\xe9\xea\xf1\xf2\xf3\xf4"
546
+ b"\xf5\xf6\xf7\xf8\xf9\xfa\xff\xc4\x00\x1f\x01\x00\x03\x01\x01\x01\x01\x01"
547
+ b"\x01\x01\x01\x01\x00\x00\x00\x00\x00\x00\x01\x02\x03\x04\x05\x06\x07\x08"
548
+ b"\x09\x0a\x0b\xff\xc4\x00\xb5\x11\x00\x02\x01\x02\x04\x04\x03\x04\x07\x05"
549
+ b"\x04\x04\x00\x01\x02\x77\x00\x01\x02\x03\x11\x04\x05\x21\x31\x06\x12\x41"
550
+ b"\x51\x07\x61\x71\x13\x22\x32\x81\x08\x14\x42\x91\xa1\xb1\xc1\x09\x23\x33"
551
+ b"\x52\xf0\x15\x62\x72\xd1\x0a\x16\x24\x34\xe1\x25\xf1\x17\x18\x19\x1a\x26"
552
+ b"\x27\x28\x29\x2a\x35\x36\x37\x38\x39\x3a\x43\x44\x45\x46\x47\x48\x49\x4a"
553
+ b"\x53\x54\x55\x56\x57\x58\x59\x5a\x63\x64\x65\x66\x67\x68\x69\x6a\x73\x74"
554
+ b"\x75\x76\x77\x78\x79\x7a\x82\x83\x84\x85\x86\x87\x88\x89\x8a\x92\x93\x94"
555
+ b"\x95\x96\x97\x98\x99\x9a\xa2\xa3\xa4\xa5\xa6\xa7\xa8\xa9\xaa\xb2\xb3\xb4"
556
+ b"\xb5\xb6\xb7\xb8\xb9\xba\xc2\xc3\xc4\xc5\xc6\xc7\xc8\xc9\xca\xd2\xd3\xd4"
557
+ b"\xd5\xd6\xd7\xd8\xd9\xda\xe2\xe3\xe4\xe5\xe6\xe7\xe8\xe9\xea\xf2\xf3\xf4"
558
+ b"\xf5\xf6\xf7\xf8\xf9\xfa\xff\xda\x00\x0c\x03\x01\x00\x02\x11\x03\x11\x00"
559
+ b"\x3f\x00"
560
+ )
561
+
562
+ footer = bytearray(b"\xff\xd9")
563
+
564
+ header[164] = bytes_data[1]
565
+ header[166] = bytes_data[2]
566
+
567
+ return header + bytes_data[3:] + footer
568
+
569
+
570
+ def from_inline_bytes(data: bytes, file_name: str = None) -> BytesIO:
571
+ b = BytesIO()
572
+
573
+ b.write(data)
574
+ b.name = file_name or f"photo_{datetime.now().strftime('%Y-%m-%d_%H-%M-%S')}.jpg"
575
+
576
+ return b