mtproto 0.0.14 → 0.0.15

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.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: e4d643a3e49ecb3e3d2744952aa42edb40237c2307a0d7d316c1ed350443bab7
4
- data.tar.gz: f3f21cb5f49140c69d1d8dd8d34b7b12f2cf0aaaae48efedc9bce6b954cdf1ae
3
+ metadata.gz: a52707d10eeb5e41cf6e1dcb01374d92092031ceba5f5f92dde31ce4965d174c
4
+ data.tar.gz: ddf47dbcf9612d686806ec83fd16470de46595300b76c733fc8af37be851f1ff
5
5
  SHA512:
6
- metadata.gz: ba8f0adc18f691fa85b9722dc994ac1784a30d9465f972f9992da6a95027722655467952c94b31877531c08fb5fa45effef9fc77558ff6b7e2a8cccc69d9310e
7
- data.tar.gz: a95133f8932df90e61b39de4cd6342b6d5f5b790bd803eacb3cde964b6d2fbd1e6983fc6f2bfb0543c25568287225e094f10e95bdc52a68fbbc5ff9ebbdc8484
6
+ metadata.gz: c97f61c1f21c9b2221b90333dd12156b77617e7debd04424a0a4996dd7c50c7b570ba0777d043b966032ad2827db4d70498cc362cd9966c1342bd2b4c711d91c
7
+ data.tar.gz: 5f235da4dac488f6c9853affca03de93bfecf2b35eacaedf13e933a4400a099e55d1831e2002c63a0687fc326e74c38e86c1e82da9bd9c95c5f07bd41ee846f7
@@ -1,11 +1,16 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require 'securerandom'
3
4
  require_relative '../message_id'
4
5
  require_relative 'rpc/response'
5
6
 
6
7
  module MTProto
7
8
  class Client
8
9
  class RPC
10
+ MSGS_ACK = 0x62d6b459
11
+ VECTOR = 0x1cb5c415
12
+ PING_DELAY_DISCONNECT = 0xf3427b8c
13
+
9
14
  attr_reader :pending_requests
10
15
 
11
16
  def initialize(client)
@@ -66,6 +71,34 @@ module MTProto
66
71
  @pending_requests.clear
67
72
  end
68
73
 
74
+ # Acknowledge received server messages (msgs_ack). Without this the server
75
+ # keeps the unacknowledged messages, resends them, and drops the connection
76
+ # after a fixed window. Fire-and-forget — no rpc_result is expected.
77
+ def send_ack(msg_ids)
78
+ return if msg_ids.nil? || msg_ids.empty?
79
+
80
+ send_encrypted(self.class.ack_body(msg_ids), content_related: false)
81
+ end
82
+
83
+ # Keepalive: ping_delay_disconnect asks the server to disconnect us only if
84
+ # we stop pinging within disconnect_delay seconds. Fire-and-forget (the pong
85
+ # is ignored by process_message).
86
+ def send_ping(disconnect_delay = 75)
87
+ send_encrypted(self.class.ping_body(SecureRandom.random_number(2**63), disconnect_delay),
88
+ content_related: false)
89
+ end
90
+
91
+ # Serialized msgs_ack body (no envelope) — extracted for unit testing.
92
+ def self.ack_body(msg_ids)
93
+ [MSGS_ACK].pack('L<') + [VECTOR].pack('L<') + [msg_ids.length].pack('L<') +
94
+ msg_ids.map { |id| [id].pack('Q<') }.join
95
+ end
96
+
97
+ # Serialized ping_delay_disconnect body (no envelope) — extracted for testing.
98
+ def self.ping_body(ping_id, disconnect_delay)
99
+ [PING_DELAY_DISCONNECT].pack('L<') + [ping_id].pack('Q<') + [disconnect_delay].pack('l<')
100
+ end
101
+
69
102
  private
70
103
 
71
104
  def serialize_request(request)
@@ -77,10 +77,17 @@ module MTProto
77
77
  @lang_code = 'en'
78
78
 
79
79
  @receiver_task = nil
80
+ @keepalive_task = nil
81
+ @ack_ids = []
80
82
  @running = false
81
83
  @on_update_callbacks = []
82
84
  end
83
85
 
86
+ # Keepalive cadence: flush pending acks every ACK_INTERVAL seconds, send a
87
+ # ping every PING_INTERVAL seconds.
88
+ ACK_INTERVAL = 5
89
+ PING_INTERVAL = 30
90
+
84
91
  def on_update(&block)
85
92
  @on_update_callbacks << block
86
93
  end
@@ -111,6 +118,7 @@ module MTProto
111
118
  raise 'Mainloop already running' if @running
112
119
 
113
120
  @running = true
121
+ @ack_ids = []
114
122
  @receiver_task = Async do
115
123
  @connection.receive do |packet, error|
116
124
  if error
@@ -124,15 +132,20 @@ module MTProto
124
132
  sender: :server
125
133
  )
126
134
 
135
+ collect_ack(decrypted[:msg_id], decrypted[:seq_no], decrypted[:body])
127
136
  process_message(decrypted[:body])
128
137
  end
129
138
  end
139
+ @keepalive_task = Async { keepalive_loop }
130
140
  end
131
141
 
132
142
  def disconnect!
133
143
  @running = false
144
+ @keepalive_task&.stop
145
+ @keepalive_task = nil
134
146
  @receiver_task&.stop
135
147
  @receiver_task = nil
148
+ @ack_ids = []
136
149
 
137
150
  rpc.signal_all_error(Transport::ConnectionError.new('Client shutting down'))
138
151
 
@@ -232,6 +245,46 @@ module MTProto
232
245
 
233
246
  private
234
247
 
248
+ # Periodically acknowledge received messages and ping, so the server doesn't
249
+ # drop a long-lived connection. Ends when the connection dies (the wrapper
250
+ # reconnects) or when the task is stopped on disconnect!.
251
+ def keepalive_loop
252
+ elapsed = 0
253
+ loop do
254
+ sleep ACK_INTERVAL
255
+ flush_acks
256
+ elapsed += ACK_INTERVAL
257
+ next if elapsed < PING_INTERVAL
258
+
259
+ elapsed = 0
260
+ rpc.send_ping
261
+ end
262
+ rescue StandardError
263
+ nil
264
+ end
265
+
266
+ # Queue msg_ids of received content-related server messages for msgs_ack.
267
+ # A container carries its own inner msg_ids; content-related messages have an
268
+ # odd seq_no and must be acknowledged (acks/pongs are even and need none).
269
+ def collect_ack(msg_id, seq_no, body)
270
+ constructor = body[0, 4].unpack1('L<')
271
+ if constructor == TL::Constructors::MSG_CONTAINER
272
+ TL::MsgContainer.deserialize(body).messages.each do |msg|
273
+ collect_ack(msg[:msg_id], msg[:seqno], msg[:body])
274
+ end
275
+ elsif seq_no.odd?
276
+ @ack_ids << msg_id
277
+ end
278
+ end
279
+
280
+ def flush_acks
281
+ return if @ack_ids.empty?
282
+
283
+ ids = @ack_ids
284
+ @ack_ids = []
285
+ rpc.send_ack(ids)
286
+ end
287
+
235
288
  def process_message(response_body)
236
289
  constructor = response_body[0, 4].unpack1('L<')
237
290
 
@@ -192,6 +192,7 @@ module MTProto
192
192
  0x16115a96 => 'pageBlockRelatedArticles',
193
193
  0x161d9628 => 'topPeerCategoryChannels',
194
194
  0x16484857 => 'account.chatThemes',
195
+ 0x16605e3e => 'messageActionManagedBotCreated',
195
196
  0x1662af0b => 'messages.historyImport',
196
197
  0x167bd90b => 'payments.starGiftUpgradePreview',
197
198
  0x167fc0a1 => 'channels.toggleAutotranslation',
@@ -304,6 +305,7 @@ module MTProto
304
305
  0x21108ff7 => 'businessRecipients',
305
306
  0x211a1788 => 'webPageEmpty',
306
307
  0x21202222 => 'messages.getDialogUnreadMarks',
308
+ 0x213853a3 => 'bots.getAccessSettings',
307
309
  0x2144ca19 => 'rpc_error',
308
310
  0x21461b5d => 'privacyValueAllowBots',
309
311
  0x219c34e6 => 'phone.toggleGroupCallStartSubscription',
@@ -428,6 +430,8 @@ module MTProto
428
430
  0x31518e9b => 'messageActionRequestedPeer',
429
431
  0x315a4974 => 'users.usersSlice',
430
432
  0x316ce548 => 'account.setReactionsNotifySettings',
433
+ 0x31774388 => 'user',
434
+ 0x31813cd8 => 'bots.editAccessSettings',
431
435
  0x31bb5d52 => 'channelAdminLogEventActionChangeWallpaper',
432
436
  0x31bd492d => 'messages.messageReactionsList',
433
437
  0x31c1c44f => 'messages.getMessageReadParticipants',
@@ -525,6 +529,7 @@ module MTProto
525
529
  0x3c479971 => 'phone.declineConferenceCallInvite',
526
530
  0x3c4f04d8 => 'botCommandScopeUsers',
527
531
  0x3c5693e9 => 'inputTheme',
532
+ 0x3c60b621 => 'bots.exportedBotToken',
528
533
  0x3cbc93f8 => 'chatParticipants',
529
534
  0x3cc04740 => 'messages.deleteQuickReplyShortcut',
530
535
  0x3cd930b7 => 'channels.setEmojiStickers',
@@ -616,6 +621,7 @@ module MTProto
616
621
  0x47dd8079 => 'messageActionWebViewDataSentMe',
617
622
  0x481eadfa => 'emojiListNotModified',
618
623
  0x48222faf => 'inputGeoPoint',
624
+ 0x4880ed9a => 'updateManagedBot',
619
625
  0x48870999 => 'pageBlockFooter',
620
626
  0x4899484e => 'messages.votesList',
621
627
  0x48a30254 => 'replyInlineMarkup',
@@ -918,6 +924,7 @@ module MTProto
918
924
  0x6c47ac9f => 'langPackStringPluralized',
919
925
  0x6c50051c => 'messages.importChatInvite',
920
926
  0x6c5a5b37 => 'account.saveWallPaper',
927
+ 0x6c5cf2a7 => 'messages.sendBotRequestedPeer',
921
928
  0x6c6274fa => 'messageActionGiftPremium',
922
929
  0x6c750de1 => 'messages.sendQuickReplyMessages',
923
930
  0x6c8e1e06 => 'birthday',
@@ -1651,6 +1658,7 @@ module MTProto
1651
1658
  0xbc799737 => 'boolFalse',
1652
1659
  0xbcf22685 => 'phone.inviteConferenceCallParticipant',
1653
1660
  0xbd0415c4 => 'stories.togglePeerStoriesHidden',
1661
+ 0xbd0d99eb => 'bots.exportBotToken',
1654
1662
  0xbd17a14a => 'topPeerCategoryGroups',
1655
1663
  0xbd1efd3e => 'payments.getStarsGiveawayOptions',
1656
1664
  0xbd2a0840 => 'inputPeerChannelFromMessage',
@@ -1927,6 +1935,7 @@ module MTProto
1927
1935
  0xdcdf8607 => 'stats.getMegagroupStats',
1928
1936
  0xdd0c66f2 => 'starRefProgram',
1929
1937
  0xdd18782e => 'help.appConfig',
1938
+ 0xdd1fbf93 => 'bots.accessSettings',
1930
1939
  0xdd289f8e => 'invokeWithBusinessConnection',
1931
1940
  0xdd6a8f48 => 'sendMessageGamePlayAction',
1932
1941
  0xdde8a54c => 'inputPeerUser',
@@ -1999,6 +2008,7 @@ module MTProto
1999
2008
  0xe56dbf05 => 'dialogPeer',
2000
2009
  0xe581e4e9 => 'requirementToContactPremium',
2001
2010
  0xe58e95d2 => 'messages.deleteMessages',
2011
+ 0xe5b17f2b => 'bots.createBot',
2002
2012
  0xe5bbfe1a => 'inputMediaPhotoExternal',
2003
2013
  0xe5bdf8de => 'updateUserStatus',
2004
2014
  0xe5bfffcd => 'auth.exportAuthorization',
@@ -0,0 +1,54 @@
1
+ # frozen_string_literal: true
2
+
3
+ module MTProto
4
+ module TL
5
+ class CreateBot
6
+ include Binary
7
+
8
+ CONSTRUCTOR = 0xe5b17f2b
9
+ INPUT_USER = 0xf21158c6
10
+ INPUT_USER_SELF = 0xf7c1b13f
11
+
12
+ # manager: { id:, access_hash: } or { type: :self }
13
+ def initialize(name:, username:, manager:, via_deeplink: false)
14
+ @name = name
15
+ @username = username
16
+ @manager = manager
17
+ @via_deeplink = via_deeplink
18
+ end
19
+
20
+ def serialize
21
+ flags = 0
22
+ flags |= (1 << 0) if @via_deeplink
23
+
24
+ result = u32_b(CONSTRUCTOR)
25
+ result += u32_b(flags)
26
+ result += serialize_tl_string(@name)
27
+ result += serialize_tl_string(@username)
28
+ result += if @manager[:type] == :self
29
+ u32_b(INPUT_USER_SELF)
30
+ else
31
+ u32_b(INPUT_USER) + u64_b(@manager[:id]) + u64_b(@manager[:access_hash] || 0)
32
+ end
33
+ result
34
+ end
35
+
36
+ private
37
+
38
+ def serialize_tl_string(str)
39
+ bytes = str.encode('UTF-8').bytes
40
+ length = bytes.length
41
+ if length <= 253
42
+ [length] + bytes + padding(length + 1)
43
+ else
44
+ [254] + u32_b(length)[0, 3] + bytes + padding(length + 4)
45
+ end
46
+ end
47
+
48
+ def padding(current_length)
49
+ pad_length = (4 - (current_length % 4)) % 4
50
+ [0] * pad_length
51
+ end
52
+ end
53
+ end
54
+ end
@@ -51,6 +51,16 @@ module MTProto
51
51
  new(dialogs: dialogs, chats: chats, users: users, count: count, message_dates: message_dates)
52
52
  end
53
53
 
54
+ # Parse a chats:Vector<Chat> / users:Vector<User> at an arbitrary offset.
55
+ # Reused by UpdatesDifference — the same vectors appear in updates.difference.
56
+ def self.chats_at(data, offset)
57
+ parse_chats_vector(data, offset)
58
+ end
59
+
60
+ def self.users_at(data, offset)
61
+ parse_users_vector(data, offset)
62
+ end
63
+
54
64
  class << self
55
65
  private
56
66
 
@@ -0,0 +1,46 @@
1
+ # frozen_string_literal: true
2
+
3
+ module MTProto
4
+ module TL
5
+ class EditAccessSettings
6
+ include Binary
7
+
8
+ CONSTRUCTOR = 0x31813cd8
9
+ INPUT_USER = 0xf21158c6
10
+ INPUT_USER_SELF = 0xf7c1b13f
11
+
12
+ # bot / add_users entries: { id:, access_hash: } or { type: :self }
13
+ def initialize(bot:, restricted: false, add_users: [])
14
+ @bot = bot
15
+ @restricted = restricted
16
+ @add_users = add_users
17
+ end
18
+
19
+ def serialize
20
+ flags = 0
21
+ flags |= (1 << 0) if @restricted
22
+ flags |= (1 << 1) unless @add_users.empty?
23
+
24
+ result = u32_b(CONSTRUCTOR)
25
+ result += u32_b(flags)
26
+ result += serialize_input_user(@bot)
27
+ unless @add_users.empty?
28
+ result += u32_b(Constructors::VECTOR)
29
+ result += u32_b(@add_users.size)
30
+ @add_users.each { |u| result += serialize_input_user(u) }
31
+ end
32
+ result
33
+ end
34
+
35
+ private
36
+
37
+ def serialize_input_user(user)
38
+ if user[:type] == :self
39
+ u32_b(INPUT_USER_SELF)
40
+ else
41
+ u32_b(INPUT_USER) + u64_b(user[:id]) + u64_b(user[:access_hash] || 0)
42
+ end
43
+ end
44
+ end
45
+ end
46
+ end
@@ -0,0 +1,32 @@
1
+ # frozen_string_literal: true
2
+
3
+ module MTProto
4
+ module TL
5
+ class ExportBotToken
6
+ include Binary
7
+
8
+ CONSTRUCTOR = 0xbd0d99eb
9
+ INPUT_USER = 0xf21158c6
10
+ INPUT_USER_SELF = 0xf7c1b13f
11
+ BOOL_TRUE = 0x997275b5
12
+ BOOL_FALSE = 0xbc799737
13
+
14
+ # bot: { id:, access_hash: } or { type: :self }
15
+ def initialize(bot:, revoke: false)
16
+ @bot = bot
17
+ @revoke = revoke
18
+ end
19
+
20
+ def serialize
21
+ result = u32_b(CONSTRUCTOR)
22
+ result += if @bot[:type] == :self
23
+ u32_b(INPUT_USER_SELF)
24
+ else
25
+ u32_b(INPUT_USER) + u64_b(@bot[:id]) + u64_b(@bot[:access_hash] || 0)
26
+ end
27
+ result += u32_b(@revoke ? BOOL_TRUE : BOOL_FALSE)
28
+ result
29
+ end
30
+ end
31
+ end
32
+ end
@@ -0,0 +1,27 @@
1
+ # frozen_string_literal: true
2
+
3
+ module MTProto
4
+ module TL
5
+ class ExportedBotToken
6
+ CONSTRUCTOR = 0x3c60b621
7
+
8
+ attr_reader :token
9
+
10
+ def initialize(token:)
11
+ @token = token
12
+ end
13
+
14
+ def self.deserialize(bytes)
15
+ body = bytes[4..]
16
+ first = body.getbyte(0)
17
+ token = if first == 254
18
+ len = body.getbyte(1) | (body.getbyte(2) << 8) | (body.getbyte(3) << 16)
19
+ body[4, len]
20
+ else
21
+ body[1, first]
22
+ end
23
+ new(token: token.force_encoding('UTF-8'))
24
+ end
25
+ end
26
+ end
27
+ end
@@ -9,16 +9,22 @@ module MTProto
9
9
 
10
10
  CONSTRUCTOR = 0x978928ca
11
11
 
12
- def initialize(from_peer:, to_peer:, ids:, random_ids: nil)
12
+ def initialize(from_peer:, to_peer:, ids:, random_ids: nil, drop_author: false, drop_media_captions: false)
13
13
  @from_peer = from_peer
14
14
  @to_peer = to_peer
15
15
  @ids = ids
16
16
  @random_ids = random_ids || Array.new(ids.length) { SecureRandom.random_number(2**63) }
17
+ @drop_author = drop_author
18
+ @drop_media_captions = drop_media_captions
17
19
  end
18
20
 
19
21
  def serialize
22
+ flags = 0
23
+ flags |= (1 << 11) if @drop_author
24
+ flags |= (1 << 12) if @drop_media_captions
25
+
20
26
  result = u32_b(CONSTRUCTOR)
21
- result += u32_b(0) # flags
27
+ result += u32_b(flags)
22
28
  result += serialize_input_peer(@from_peer)
23
29
  result += u32_b(Constructors::VECTOR) + u32_b(@ids.length)
24
30
  @ids.each { |id| result += u32_b(id) }
@@ -0,0 +1,28 @@
1
+ # frozen_string_literal: true
2
+
3
+ module MTProto
4
+ module TL
5
+ class GetAccessSettings
6
+ include Binary
7
+
8
+ CONSTRUCTOR = 0x213853a3
9
+ INPUT_USER = 0xf21158c6
10
+ INPUT_USER_SELF = 0xf7c1b13f
11
+
12
+ # bot: { id:, access_hash: } or { type: :self }
13
+ def initialize(bot:)
14
+ @bot = bot
15
+ end
16
+
17
+ def serialize
18
+ result = u32_b(CONSTRUCTOR)
19
+ result += if @bot[:type] == :self
20
+ u32_b(INPUT_USER_SELF)
21
+ else
22
+ u32_b(INPUT_USER) + u64_b(@bot[:id]) + u64_b(@bot[:access_hash] || 0)
23
+ end
24
+ result
25
+ end
26
+ end
27
+ end
28
+ end
@@ -0,0 +1,54 @@
1
+ # frozen_string_literal: true
2
+
3
+ module MTProto
4
+ module TL
5
+ class InputKeyboardButtonRequestPeer
6
+ include Binary
7
+
8
+ CONSTRUCTOR = 0x02b78156
9
+
10
+ def initialize(text:, button_id:, peer_type:, max_quantity: 1,
11
+ name_requested: false, username_requested: false, photo_requested: false)
12
+ @text = text
13
+ @button_id = button_id
14
+ @peer_type = peer_type
15
+ @max_quantity = max_quantity
16
+ @name_requested = name_requested
17
+ @username_requested = username_requested
18
+ @photo_requested = photo_requested
19
+ end
20
+
21
+ def serialize
22
+ flags = 0
23
+ flags |= (1 << 0) if @name_requested
24
+ flags |= (1 << 1) if @username_requested
25
+ flags |= (1 << 2) if @photo_requested
26
+
27
+ result = u32_b(CONSTRUCTOR)
28
+ result += u32_b(flags)
29
+ result += serialize_tl_string(@text)
30
+ result += u32_b(@button_id)
31
+ result += @peer_type.serialize
32
+ result += u32_b(@max_quantity)
33
+ result
34
+ end
35
+
36
+ private
37
+
38
+ def serialize_tl_string(str)
39
+ bytes = str.encode('UTF-8').bytes
40
+ length = bytes.length
41
+ if length <= 253
42
+ [length] + bytes + padding(length + 1)
43
+ else
44
+ [254] + u32_b(length)[0, 3] + bytes + padding(length + 4)
45
+ end
46
+ end
47
+
48
+ def padding(current_length)
49
+ pad_length = (4 - (current_length % 4)) % 4
50
+ [0] * pad_length
51
+ end
52
+ end
53
+ end
54
+ end
@@ -0,0 +1,26 @@
1
+ # frozen_string_literal: true
2
+
3
+ module MTProto
4
+ module TL
5
+ # messages.getMessages#63c66506 id:Vector<InputMessage> — fetch messages by id
6
+ # from a basic group or private chat (the non-channel counterpart of
7
+ # channels.getMessages). Returns messages.messages / messagesSlice.
8
+ class MessagesGetMessages
9
+ include Binary
10
+
11
+ CONSTRUCTOR = 0x63c66506
12
+ INPUT_MESSAGE_ID = 0xa676a322
13
+
14
+ def initialize(ids:)
15
+ @ids = ids
16
+ end
17
+
18
+ def serialize
19
+ result = u32_b(CONSTRUCTOR)
20
+ result += u32_b(Constructors::VECTOR) + u32_b(@ids.length)
21
+ @ids.each { |id| result += u32_b(INPUT_MESSAGE_ID) + u32_b(id) }
22
+ result
23
+ end
24
+ end
25
+ end
26
+ end
@@ -0,0 +1,35 @@
1
+ # frozen_string_literal: true
2
+
3
+ module MTProto
4
+ module TL
5
+ class ReplyKeyboardMarkup
6
+ include Binary
7
+
8
+ CONSTRUCTOR = 0x85dd99d1
9
+ KEYBOARD_BUTTON_ROW = 0x77608b83
10
+
11
+ # buttons: a single row of button objects (each responds to #serialize)
12
+ def initialize(buttons:, resize: true, single_use: false)
13
+ @buttons = buttons
14
+ @resize = resize
15
+ @single_use = single_use
16
+ end
17
+
18
+ def serialize
19
+ flags = 0
20
+ flags |= (1 << 0) if @resize
21
+ flags |= (1 << 1) if @single_use
22
+
23
+ result = u32_b(CONSTRUCTOR)
24
+ result += u32_b(flags)
25
+ result += u32_b(Constructors::VECTOR)
26
+ result += u32_b(1)
27
+ result += u32_b(KEYBOARD_BUTTON_ROW)
28
+ result += u32_b(Constructors::VECTOR)
29
+ result += u32_b(@buttons.size)
30
+ @buttons.each { |button| result += button.serialize }
31
+ result
32
+ end
33
+ end
34
+ end
35
+ end
@@ -0,0 +1,47 @@
1
+ # frozen_string_literal: true
2
+
3
+ module MTProto
4
+ module TL
5
+ class RequestPeerTypeCreateBot
6
+ include Binary
7
+
8
+ CONSTRUCTOR = 0x3e81e078
9
+
10
+ def initialize(bot_managed: true, suggested_name: nil, suggested_username: nil)
11
+ @bot_managed = bot_managed
12
+ @suggested_name = suggested_name
13
+ @suggested_username = suggested_username
14
+ end
15
+
16
+ def serialize
17
+ flags = 0
18
+ flags |= (1 << 0) if @bot_managed
19
+ flags |= (1 << 1) if @suggested_name
20
+ flags |= (1 << 2) if @suggested_username
21
+
22
+ result = u32_b(CONSTRUCTOR)
23
+ result += u32_b(flags)
24
+ result += serialize_tl_string(@suggested_name) if @suggested_name
25
+ result += serialize_tl_string(@suggested_username) if @suggested_username
26
+ result
27
+ end
28
+
29
+ private
30
+
31
+ def serialize_tl_string(str)
32
+ bytes = str.encode('UTF-8').bytes
33
+ length = bytes.length
34
+ if length <= 253
35
+ [length] + bytes + padding(length + 1)
36
+ else
37
+ [254] + u32_b(length)[0, 3] + bytes + padding(length + 4)
38
+ end
39
+ end
40
+
41
+ def padding(current_length)
42
+ pad_length = (4 - (current_length % 4)) % 4
43
+ [0] * pad_length
44
+ end
45
+ end
46
+ end
47
+ end
@@ -0,0 +1,51 @@
1
+ # frozen_string_literal: true
2
+
3
+ module MTProto
4
+ module TL
5
+ class SendBotRequestedPeer
6
+ include Binary
7
+
8
+ CONSTRUCTOR = 0x6c5cf2a7
9
+
10
+ # peer / requested_peers entries: { type:, id:, access_hash: } or { type: :self }
11
+ def initialize(peer:, button_id:, msg_id: nil, requested_peers: [])
12
+ @peer = peer
13
+ @button_id = button_id
14
+ @msg_id = msg_id
15
+ @requested_peers = requested_peers
16
+ end
17
+
18
+ def serialize
19
+ flags = 0
20
+ flags |= (1 << 0) if @msg_id
21
+
22
+ result = u32_b(CONSTRUCTOR)
23
+ result += u32_b(flags)
24
+ result += serialize_input_peer(@peer)
25
+ result += u32_b(@msg_id) if @msg_id
26
+ result += u32_b(@button_id)
27
+ result += u32_b(Constructors::VECTOR)
28
+ result += u32_b(@requested_peers.size)
29
+ @requested_peers.each { |peer| result += serialize_input_peer(peer) }
30
+ result
31
+ end
32
+
33
+ private
34
+
35
+ def serialize_input_peer(peer)
36
+ case peer[:type]
37
+ when :self
38
+ u32_b(Constructors::INPUT_PEER_SELF)
39
+ when :user
40
+ u32_b(Constructors::INPUT_PEER_USER) + u64_b(peer[:id]) + u64_b(peer[:access_hash] || 0)
41
+ when :chat
42
+ u32_b(Constructors::INPUT_PEER_CHAT) + u64_b(peer[:id])
43
+ when :channel
44
+ u32_b(Constructors::INPUT_PEER_CHANNEL) + u64_b(peer[:id]) + u64_b(peer[:access_hash] || 0)
45
+ else
46
+ raise "Unknown peer type: #{peer[:type]}"
47
+ end
48
+ end
49
+ end
50
+ end
51
+ end
@@ -7,17 +7,24 @@ module MTProto
7
7
  class SendMedia
8
8
  include Binary
9
9
 
10
- def initialize(peer:, media:, message: '', random_id: nil)
10
+ INPUT_REPLY_TO_MESSAGE = 0x869fbe10
11
+
12
+ def initialize(peer:, media:, message: '', random_id: nil, reply_to: nil)
11
13
  @peer = peer
12
14
  @media = media
13
15
  @message = message
14
16
  @random_id = random_id || SecureRandom.random_number(2**63)
17
+ @reply_to = reply_to
15
18
  end
16
19
 
17
20
  def serialize
21
+ flags = 0
22
+ flags |= (1 << 0) if @reply_to
23
+
18
24
  result = u32_b(Constructors::MESSAGES_SEND_MEDIA)
19
- result += u32_b(0) # flags
25
+ result += u32_b(flags)
20
26
  result += serialize_input_peer
27
+ result += serialize_reply_to if @reply_to
21
28
  result += serialize_input_media
22
29
  result += serialize_tl_string(@message)
23
30
  result += u64_b(@random_id)
@@ -26,6 +33,10 @@ module MTProto
26
33
 
27
34
  private
28
35
 
36
+ def serialize_reply_to
37
+ u32_b(INPUT_REPLY_TO_MESSAGE) + u32_b(0) + u32_b(@reply_to)
38
+ end
39
+
29
40
  def serialize_input_peer
30
41
  case @peer[:type]
31
42
  when :self
@@ -9,16 +9,18 @@ module MTProto
9
9
 
10
10
  INPUT_REPLY_TO_MESSAGE = 0x869fbe10
11
11
 
12
- def initialize(peer:, message:, random_id: nil, reply_to: nil)
12
+ def initialize(peer:, message:, random_id: nil, reply_to: nil, reply_markup: nil)
13
13
  @peer = peer
14
14
  @message = message
15
15
  @random_id = random_id || SecureRandom.random_number(2**63)
16
16
  @reply_to = reply_to
17
+ @reply_markup = reply_markup
17
18
  end
18
19
 
19
20
  def serialize
20
21
  flags = 0
21
22
  flags |= (1 << 0) if @reply_to
23
+ flags |= (1 << 2) if @reply_markup
22
24
 
23
25
  result = u32_b(Constructors::MESSAGES_SEND_MESSAGE)
24
26
  result += u32_b(flags)
@@ -26,6 +28,7 @@ module MTProto
26
28
  result += serialize_reply_to if @reply_to
27
29
  result += serialize_tl_string(@message)
28
30
  result += u64_b(@random_id)
31
+ result += @reply_markup.serialize if @reply_markup
29
32
  result
30
33
  end
31
34
 
@@ -0,0 +1,50 @@
1
+ # frozen_string_literal: true
2
+
3
+ module MTProto
4
+ module TL
5
+ class SetBotCommands
6
+ include Binary
7
+
8
+ CONSTRUCTOR = 0x0517165a
9
+ BOT_COMMAND_SCOPE_DEFAULT = 0x2f6cb2ab
10
+ BOT_COMMAND = 0xc27ac8c7
11
+
12
+ # commands: array of { command:, description: }
13
+ def initialize(commands:, lang_code: '')
14
+ @commands = commands
15
+ @lang_code = lang_code
16
+ end
17
+
18
+ def serialize
19
+ result = u32_b(CONSTRUCTOR)
20
+ result += u32_b(BOT_COMMAND_SCOPE_DEFAULT)
21
+ result += serialize_tl_string(@lang_code)
22
+ result += u32_b(Constructors::VECTOR)
23
+ result += u32_b(@commands.size)
24
+ @commands.each do |cmd|
25
+ result += u32_b(BOT_COMMAND)
26
+ result += serialize_tl_string(cmd[:command])
27
+ result += serialize_tl_string(cmd[:description])
28
+ end
29
+ result
30
+ end
31
+
32
+ private
33
+
34
+ def serialize_tl_string(str)
35
+ bytes = str.to_s.encode('UTF-8').bytes
36
+ length = bytes.length
37
+ if length <= 253
38
+ [length] + bytes + padding(length + 1)
39
+ else
40
+ [254] + u32_b(length)[0, 3] + bytes + padding(length + 4)
41
+ end
42
+ end
43
+
44
+ def padding(current_length)
45
+ pad_length = (4 - (current_length % 4)) % 4
46
+ [0] * pad_length
47
+ end
48
+ end
49
+ end
50
+ end
@@ -0,0 +1,64 @@
1
+ # frozen_string_literal: true
2
+
3
+ module MTProto
4
+ module TL
5
+ class SetBotInfo
6
+ include Binary
7
+
8
+ CONSTRUCTOR = 0x10cf3123
9
+ INPUT_USER = 0xf21158c6
10
+ INPUT_USER_SELF = 0xf7c1b13f
11
+
12
+ # bot: { id:, access_hash: } or { type: :self } (nil = the calling bot)
13
+ def initialize(bot: nil, lang_code: '', name: nil, about: nil, description: nil)
14
+ @bot = bot
15
+ @lang_code = lang_code
16
+ @name = name
17
+ @about = about
18
+ @description = description
19
+ end
20
+
21
+ def serialize
22
+ flags = 0
23
+ flags |= (1 << 2) if @bot
24
+ flags |= (1 << 3) if @name
25
+ flags |= (1 << 0) if @about
26
+ flags |= (1 << 1) if @description
27
+
28
+ result = u32_b(CONSTRUCTOR)
29
+ result += u32_b(flags)
30
+ result += serialize_input_user(@bot) if @bot
31
+ result += serialize_tl_string(@lang_code)
32
+ result += serialize_tl_string(@name) if @name
33
+ result += serialize_tl_string(@about) if @about
34
+ result += serialize_tl_string(@description) if @description
35
+ result
36
+ end
37
+
38
+ private
39
+
40
+ def serialize_input_user(user)
41
+ if user[:type] == :self
42
+ u32_b(INPUT_USER_SELF)
43
+ else
44
+ u32_b(INPUT_USER) + u64_b(user[:id]) + u64_b(user[:access_hash] || 0)
45
+ end
46
+ end
47
+
48
+ def serialize_tl_string(str)
49
+ bytes = str.to_s.encode('UTF-8').bytes
50
+ length = bytes.length
51
+ if length <= 253
52
+ [length] + bytes + padding(length + 1)
53
+ else
54
+ [254] + u32_b(length)[0, 3] + bytes + padding(length + 4)
55
+ end
56
+ end
57
+
58
+ def padding(current_length)
59
+ pad_length = (4 - (current_length % 4)) % 4
60
+ [0] * pad_length
61
+ end
62
+ end
63
+ end
64
+ end
@@ -1,21 +1,24 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require_relative '../schema'
4
+ require_relative 'dialogs'
4
5
 
5
6
  module MTProto
6
7
  module TL
7
8
  class UpdatesDifference
8
- attr_reader :type, :date, :seq, :new_messages, :pts, :state
9
+ attr_reader :type, :date, :seq, :new_messages, :pts, :state, :chats, :users
9
10
 
10
11
  SCHEMA_PATH = File.expand_path('../../../../data/tl-schema.json', __dir__)
11
12
 
12
- def initialize(type:, date: nil, seq: nil, new_messages: [], pts: nil, state: nil)
13
+ def initialize(type:, date: nil, seq: nil, new_messages: [], pts: nil, state: nil, chats: [], users: [])
13
14
  @type = type
14
15
  @date = date
15
16
  @seq = seq
16
17
  @new_messages = new_messages
17
18
  @pts = pts
18
19
  @state = state
20
+ @chats = chats
21
+ @users = users
19
22
  end
20
23
 
21
24
  def self.schema
@@ -41,6 +44,11 @@ module MTProto
41
44
  MESSAGE_CONSTRUCTOR = Constructors::MESSAGE
42
45
  MESSAGE_SERVICE = 0x7a800e0a
43
46
  MESSAGE_EMPTY = 0x90a6ca84
47
+ MESSAGE_REPLY_HEADER = 0x6917560b
48
+ MEDIA_PHOTO = 0x695150d7
49
+ MEDIA_DOCUMENT = 0x52d8ccd9
50
+ UPDATE_NEW_MESSAGE = 0x1f2b0afd
51
+ UPDATE_NEW_CHANNEL_MESSAGE = 0x62ba04d9
44
52
 
45
53
  class << self
46
54
  private
@@ -53,24 +61,48 @@ module MTProto
53
61
  )
54
62
  end
55
63
 
56
- # updates.difference: new_messages new_encrypted_messages other_updates chats users state
64
+ # updates.difference: new_messages new_encrypted_messages other_updates chats users state.
65
+ # Channel/supergroup messages arrive in other_updates as
66
+ # updateNewChannelMessage (not in new_messages), so we mine those too; and
67
+ # chats/users carry the peers' access_hashes for the bot's peer cache.
57
68
  def parse_difference(data, constructor)
58
69
  offset = 4
59
70
 
60
71
  messages, offset = parse_messages_vector(data, offset)
61
72
  offset = schema.skip_vector(data, offset) # new_encrypted_messages
62
- offset = schema.skip_vector(data, offset) # other_updates
63
- offset = schema.skip_vector(data, offset) # chats
64
- offset = schema.skip_vector(data, offset) # users
73
+ other_messages, offset = parse_other_updates(data, offset)
74
+ chats, offset = Dialogs.chats_at(data, offset)
75
+ users, offset = Dialogs.users_at(data, offset)
65
76
  state = UpdatesState.deserialize(data[offset..])
66
77
 
67
78
  new(
68
79
  type: constructor == Constructors::UPDATES_DIFFERENCE ? :difference : :slice,
69
- new_messages: messages,
70
- state: state
80
+ new_messages: messages + other_messages,
81
+ chats: chats, users: users, state: state
71
82
  )
72
83
  end
73
84
 
85
+ # other_updates: Vector<Update>. Mine the message-bearing updates
86
+ # (updateNewMessage / updateNewChannelMessage — each is the update ctor,
87
+ # then an inner Message, then pts/pts_count); advance the rest via schema.
88
+ def parse_other_updates(data, offset)
89
+ offset += 4 # vector constructor
90
+ count = data[offset, 4].unpack1('L<')
91
+ offset += 4
92
+
93
+ messages = []
94
+ count.times do
95
+ ctor = data[offset, 4].unpack1('L<')
96
+ if [UPDATE_NEW_MESSAGE, UPDATE_NEW_CHANNEL_MESSAGE].include?(ctor)
97
+ inner_off = offset + 4
98
+ msg = extract_message(data, inner_off, data[inner_off, 4].unpack1('L<'))
99
+ messages << msg if msg
100
+ end
101
+ offset = schema.skip(data, offset)
102
+ end
103
+ [messages, offset]
104
+ end
105
+
74
106
  def parse_messages_vector(data, offset)
75
107
  offset += 4 # vector constructor
76
108
  count = data[offset, 4].unpack1('L<')
@@ -97,20 +129,30 @@ module MTProto
97
129
  id = data[offset, 4].unpack1('l<')
98
130
  offset += 4
99
131
 
100
- offset = schema.skip(data, offset) if flags.anybits?(1 << 8) # from_id (Peer)
132
+ from_type = from_id = nil
133
+ from_type, from_id, offset = parse_peer(data, offset) if flags.anybits?(1 << 8) # from_id (Peer)
101
134
  offset += 4 if flags.anybits?(1 << 29) # from_boosts_applied
102
135
  peer_type, peer_id, offset = parse_peer(data, offset)
103
136
  offset = schema.skip(data, offset) if flags.anybits?(1 << 28) # saved_peer_id
104
137
  offset = schema.skip(data, offset) if flags.anybits?(1 << 2) # fwd_from
105
138
  offset += 8 if flags.anybits?(1 << 11) # via_bot_id
106
139
  offset += 8 if flags2.anybits?(1 << 0) # via_business_bot_id
107
- offset = schema.skip(data, offset) if flags.anybits?(1 << 3) # reply_to
140
+
141
+ is_reply = false
142
+ reply_to_msg_id = nil
143
+ if flags.anybits?(1 << 3) # reply_to
144
+ reply_to_msg_id, is_reply = parse_reply_to(data, offset)
145
+ offset = schema.skip(data, offset)
146
+ end
108
147
 
109
148
  date = data[offset, 4].unpack1('L<')
110
149
  offset += 4
111
- message_text, = read_tl_string(data, offset)
150
+ message_text, offset = read_tl_string(data, offset)
151
+ media = flags.anybits?(1 << 9) ? parse_media(data, offset) : nil
112
152
 
113
- { id: id, date: date, message: message_text, peer_type: peer_type, peer_id: peer_id }
153
+ { id: id, date: date, message: message_text, peer_type: peer_type, peer_id: peer_id,
154
+ from_type: from_type, from_id: from_id, out: flags.anybits?(1 << 1),
155
+ media: media, is_reply: is_reply, reply_to_msg_id: reply_to_msg_id }
114
156
  end
115
157
 
116
158
  def parse_peer(data, offset)
@@ -129,6 +171,43 @@ module MTProto
129
171
  end
130
172
  end
131
173
 
174
+ # messageReplyHeader: ctor, flags, reply_to_msg_id:flags.4?int (first payload
175
+ # field). A plain message in a forum topic carries this header (forum_topic
176
+ # flags.3) but is a reply only when it targets a message in the topic
177
+ # (reply_to_top_id flags.1). => [reply_to_msg_id, is_reply]
178
+ def parse_reply_to(data, offset)
179
+ return [nil, false] unless data[offset, 4].unpack1('L<') == MESSAGE_REPLY_HEADER
180
+
181
+ io = offset + 4
182
+ flags = data[io, 4].unpack1('L<')
183
+ io += 4
184
+ forum_topic = flags.anybits?(1 << 3)
185
+ msg_id = flags.anybits?(1 << 4) ? data[io, 4].unpack1('l<') : nil
186
+ [msg_id, forum_topic ? flags.anybits?(1 << 1) : true]
187
+ rescue StandardError
188
+ [nil, false]
189
+ end
190
+
191
+ # Classify message media into :photo / :video / :audio / :document, or nil.
192
+ def parse_media(data, offset)
193
+ case data[offset, 4].unpack1('L<')
194
+ when MEDIA_PHOTO
195
+ :photo
196
+ when MEDIA_DOCUMENT
197
+ document_kind(data[offset + 4, 4].unpack1('L<'))
198
+ end
199
+ rescue StandardError
200
+ nil
201
+ end
202
+
203
+ # messageMediaDocument flags: video flags.6 / round flags.7 / voice flags.8.
204
+ def document_kind(flags)
205
+ return :video if flags.anybits?(1 << 6) || flags.anybits?(1 << 7)
206
+ return :audio if flags.anybits?(1 << 8)
207
+
208
+ :document
209
+ end
210
+
132
211
  def read_tl_string(data, offset)
133
212
  first_byte = data.getbyte(offset)
134
213
  if first_byte == 254
@@ -0,0 +1,65 @@
1
+ # frozen_string_literal: true
2
+
3
+ module MTProto
4
+ module TL
5
+ class UploadProfilePhoto
6
+ include Binary
7
+
8
+ CONSTRUCTOR = 0x0388a3b5
9
+ INPUT_USER = 0xf21158c6
10
+ INPUT_USER_SELF = 0xf7c1b13f
11
+ INPUT_FILE = 0xf52ff27f
12
+
13
+ # bot: { id:, access_hash: } or { type: :self }; nil targets the calling bot
14
+ # file: { id:, parts:, name:, md5_checksum: }
15
+ def initialize(file:, bot: nil)
16
+ @bot = bot
17
+ @file = file
18
+ end
19
+
20
+ def serialize
21
+ flags = (1 << 0) # file
22
+ flags |= (1 << 5) if @bot
23
+
24
+ result = u32_b(CONSTRUCTOR)
25
+ result += u32_b(flags)
26
+ result += serialize_input_user(@bot) if @bot
27
+ result += serialize_input_file(@file)
28
+ result
29
+ end
30
+
31
+ private
32
+
33
+ def serialize_input_user(user)
34
+ if user[:type] == :self
35
+ u32_b(INPUT_USER_SELF)
36
+ else
37
+ u32_b(INPUT_USER) + u64_b(user[:id]) + u64_b(user[:access_hash] || 0)
38
+ end
39
+ end
40
+
41
+ def serialize_input_file(file)
42
+ u32_b(INPUT_FILE) +
43
+ u64_b(file[:id]) +
44
+ u32_b(file[:parts]) +
45
+ serialize_tl_string(file[:name].to_s) +
46
+ serialize_tl_string(file[:md5_checksum].to_s)
47
+ end
48
+
49
+ def serialize_tl_string(str)
50
+ bytes = str.encode('UTF-8').bytes
51
+ length = bytes.length
52
+ if length <= 253
53
+ [length] + bytes + padding(length + 1)
54
+ else
55
+ [254] + u32_b(length)[0, 3] + bytes + padding(length + 4)
56
+ end
57
+ end
58
+
59
+ def padding(current_length)
60
+ pad_length = (4 - (current_length % 4)) % 4
61
+ [0] * pad_length
62
+ end
63
+ end
64
+ end
65
+ end
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module MTProto
4
- VERSION = '0.0.14'
4
+ VERSION = '0.0.15'
5
5
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: mtproto
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.0.14
4
+ version: 0.0.15
5
5
  platform: ruby
6
6
  authors:
7
7
  - Artem Levenkov
@@ -108,12 +108,17 @@ files:
108
108
  - lib/mtproto/tl/objects/check_password.rb
109
109
  - lib/mtproto/tl/objects/client_dh_inner_data.rb
110
110
  - lib/mtproto/tl/objects/contacts.rb
111
+ - lib/mtproto/tl/objects/create_bot.rb
111
112
  - lib/mtproto/tl/objects/delete_messages.rb
112
113
  - lib/mtproto/tl/objects/dh_gen_response.rb
113
114
  - lib/mtproto/tl/objects/dialogs.rb
115
+ - lib/mtproto/tl/objects/edit_access_settings.rb
114
116
  - lib/mtproto/tl/objects/edit_message.rb
117
+ - lib/mtproto/tl/objects/export_bot_token.rb
115
118
  - lib/mtproto/tl/objects/export_login_token.rb
119
+ - lib/mtproto/tl/objects/exported_bot_token.rb
116
120
  - lib/mtproto/tl/objects/forward_messages.rb
121
+ - lib/mtproto/tl/objects/get_access_settings.rb
117
122
  - lib/mtproto/tl/objects/get_channel_difference.rb
118
123
  - lib/mtproto/tl/objects/get_config.rb
119
124
  - lib/mtproto/tl/objects/get_contacts.rb
@@ -132,21 +137,26 @@ files:
132
137
  - lib/mtproto/tl/objects/import_bot_authorization.rb
133
138
  - lib/mtproto/tl/objects/import_login_token.rb
134
139
  - lib/mtproto/tl/objects/init_connection.rb
140
+ - lib/mtproto/tl/objects/input_keyboard_button_request_peer.rb
135
141
  - lib/mtproto/tl/objects/invite_to_channel.rb
136
142
  - lib/mtproto/tl/objects/invoke_with_layer.rb
137
143
  - lib/mtproto/tl/objects/login_token.rb
138
144
  - lib/mtproto/tl/objects/message.rb
139
145
  - lib/mtproto/tl/objects/messages.rb
146
+ - lib/mtproto/tl/objects/messages_get_messages.rb
140
147
  - lib/mtproto/tl/objects/msg_container.rb
141
148
  - lib/mtproto/tl/objects/new_session_created.rb
142
149
  - lib/mtproto/tl/objects/pq_inner_data.rb
143
150
  - lib/mtproto/tl/objects/raw_response.rb
151
+ - lib/mtproto/tl/objects/reply_keyboard_markup.rb
144
152
  - lib/mtproto/tl/objects/req_dh_params.rb
145
153
  - lib/mtproto/tl/objects/req_pq_multi.rb
154
+ - lib/mtproto/tl/objects/request_peer_type_create_bot.rb
146
155
  - lib/mtproto/tl/objects/res_pq.rb
147
156
  - lib/mtproto/tl/objects/resolve_username.rb
148
157
  - lib/mtproto/tl/objects/rpc_error.rb
149
158
  - lib/mtproto/tl/objects/save_file_part.rb
159
+ - lib/mtproto/tl/objects/send_bot_requested_peer.rb
150
160
  - lib/mtproto/tl/objects/send_code.rb
151
161
  - lib/mtproto/tl/objects/send_media.rb
152
162
  - lib/mtproto/tl/objects/send_message.rb
@@ -154,6 +164,8 @@ files:
154
164
  - lib/mtproto/tl/objects/sent_code.rb
155
165
  - lib/mtproto/tl/objects/server_dh_inner_data.rb
156
166
  - lib/mtproto/tl/objects/server_dh_params.rb
167
+ - lib/mtproto/tl/objects/set_bot_commands.rb
168
+ - lib/mtproto/tl/objects/set_bot_info.rb
157
169
  - lib/mtproto/tl/objects/set_client_dh_params.rb
158
170
  - lib/mtproto/tl/objects/sign_in.rb
159
171
  - lib/mtproto/tl/objects/update.rb
@@ -163,6 +175,7 @@ files:
163
175
  - lib/mtproto/tl/objects/update_username.rb
164
176
  - lib/mtproto/tl/objects/updates_difference.rb
165
177
  - lib/mtproto/tl/objects/updates_state.rb
178
+ - lib/mtproto/tl/objects/upload_profile_photo.rb
166
179
  - lib/mtproto/tl/objects/users.rb
167
180
  - lib/mtproto/tl/schema.rb
168
181
  - lib/mtproto/transport/abridged_packet_codec.rb