mtproto 0.0.15 → 0.0.17

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (47) hide show
  1. checksums.yaml +4 -4
  2. data/data/tl-schema.json +47158 -42684
  3. data/lib/mtproto/auth_key_generator.rb +2 -2
  4. data/lib/mtproto/client/api/export_authorization.rb +17 -0
  5. data/lib/mtproto/client/api/import_authorization.rb +23 -0
  6. data/lib/mtproto/client/api.rb +2 -0
  7. data/lib/mtproto/client.rb +1 -1
  8. data/lib/mtproto/file_downloader.rb +122 -0
  9. data/lib/mtproto/tl/constructor_names.rb +314 -109
  10. data/lib/mtproto/tl/constructors.rb +16 -8
  11. data/lib/mtproto/tl/objects/bot_command_scope.rb +81 -0
  12. data/lib/mtproto/tl/objects/channels_join_channel.rb +1 -1
  13. data/lib/mtproto/tl/objects/channels_update_username.rb +42 -0
  14. data/lib/mtproto/tl/objects/contacts.rb +1 -1
  15. data/lib/mtproto/tl/objects/dialogs.rb +15 -10
  16. data/lib/mtproto/tl/objects/export_authorization.rb +17 -0
  17. data/lib/mtproto/tl/objects/exported_authorization.rb +32 -0
  18. data/lib/mtproto/tl/objects/forward_messages.rb +5 -2
  19. data/lib/mtproto/tl/objects/get_bot_callback_answer.rb +67 -0
  20. data/lib/mtproto/tl/objects/get_bot_commands.rb +46 -0
  21. data/lib/mtproto/tl/objects/get_file.rb +10 -2
  22. data/lib/mtproto/tl/objects/import_authorization.rb +38 -0
  23. data/lib/mtproto/tl/objects/keyboard_button_callback.rb +50 -0
  24. data/lib/mtproto/tl/objects/message.rb +97 -21
  25. data/lib/mtproto/tl/objects/messages.rb +7 -74
  26. data/lib/mtproto/tl/objects/reply_inline_markup.rb +30 -0
  27. data/lib/mtproto/tl/objects/reset_bot_commands.rb +45 -0
  28. data/lib/mtproto/tl/objects/send_media.rb +76 -4
  29. data/lib/mtproto/tl/objects/send_message.rb +4 -2
  30. data/lib/mtproto/tl/objects/send_message_action.rb +48 -0
  31. data/lib/mtproto/tl/objects/set_bot_callback_answer.rb +54 -0
  32. data/lib/mtproto/tl/objects/set_bot_commands.rb +6 -3
  33. data/lib/mtproto/tl/objects/set_bot_guest_chat_result.rb +84 -0
  34. data/lib/mtproto/tl/objects/set_typing.rb +49 -0
  35. data/lib/mtproto/tl/objects/update_status.rb +24 -0
  36. data/lib/mtproto/tl/objects/updates.rb +117 -0
  37. data/lib/mtproto/tl/objects/updates_difference.rb +16 -119
  38. data/lib/mtproto/tl/reader.rb +188 -0
  39. data/lib/mtproto/transport/abridged_packet_codec.rb +5 -1
  40. data/lib/mtproto/unencrypted_message.rb +39 -0
  41. data/lib/mtproto/version.rb +1 -1
  42. data/lib/mtproto.rb +4 -1
  43. data/scripts/gen_constructor_names.rb +72 -0
  44. data/scripts/tl_to_json.rb +72 -0
  45. data/scripts/verify_ids.rb +33 -0
  46. metadata +25 -2
  47. data/lib/mtproto/message/message.rb +0 -85
@@ -0,0 +1,49 @@
1
+ # frozen_string_literal: true
2
+
3
+ module MTProto
4
+ module TL
5
+ # messages.setTyping — show a chat action ("typing…", "sending photo…", …)
6
+ # in the target peer. action is a SendMessageAction; top_msg_id targets a
7
+ # forum topic / thread. Returns Bool.
8
+ class SetTyping
9
+ include Binary
10
+
11
+ CONSTRUCTOR = 0x58943ee2
12
+
13
+ def initialize(peer:, action:, top_msg_id: nil)
14
+ @peer = peer
15
+ @action = action
16
+ @top_msg_id = top_msg_id
17
+ end
18
+
19
+ def serialize
20
+ flags = 0
21
+ flags |= (1 << 0) if @top_msg_id
22
+
23
+ result = u32_b(CONSTRUCTOR)
24
+ result += u32_b(flags)
25
+ result += serialize_input_peer
26
+ result += u32_b(@top_msg_id) if @top_msg_id
27
+ result += @action.serialize
28
+ result
29
+ end
30
+
31
+ private
32
+
33
+ def serialize_input_peer
34
+ case @peer[:type]
35
+ when :self
36
+ u32_b(Constructors::INPUT_PEER_SELF)
37
+ when :user
38
+ u32_b(Constructors::INPUT_PEER_USER) + u64_b(@peer[:id]) + u64_b(@peer[:access_hash] || 0)
39
+ when :chat
40
+ u32_b(Constructors::INPUT_PEER_CHAT) + u64_b(@peer[:id])
41
+ when :channel
42
+ u32_b(Constructors::INPUT_PEER_CHANNEL) + u64_b(@peer[:id]) + u64_b(@peer[:access_hash] || 0)
43
+ else
44
+ raise "Unknown peer type: #{@peer[:type]}"
45
+ end
46
+ end
47
+ end
48
+ end
49
+ end
@@ -0,0 +1,24 @@
1
+ # frozen_string_literal: true
2
+
3
+ module MTProto
4
+ module TL
5
+ # account.updateStatus — mark the session online or offline. offline:false
6
+ # is what a foreground client sends to go online (and to start receiving
7
+ # transient updates like updateUserTyping). Returns Bool.
8
+ class UpdateStatus
9
+ include Binary
10
+
11
+ CONSTRUCTOR = 0x6628562c
12
+ BOOL_TRUE = 0x997275b5
13
+ BOOL_FALSE = 0xbc799737
14
+
15
+ def initialize(offline:)
16
+ @offline = offline
17
+ end
18
+
19
+ def serialize
20
+ u32_b(CONSTRUCTOR) + u32_b(@offline ? BOOL_TRUE : BOOL_FALSE)
21
+ end
22
+ end
23
+ end
24
+ end
@@ -0,0 +1,117 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative '../reader'
4
+ require_relative 'message'
5
+ require_relative 'dialogs'
6
+
7
+ module MTProto
8
+ module TL
9
+ # The live `updates`#74ae4240 / `updatesCombined`#725b04c3 push container a bot
10
+ # receives in real time (as opposed to the `updates.getDifference` reply
11
+ # UpdatesDifference handles). Mines the message-bearing updates into structured
12
+ # messages — including any media — and returns the senders in users[] for the
13
+ # peer/access_hash cache. Each update is advanced via Schema#skip, so an update
14
+ # type we don't model never drops the ones we do.
15
+ class Updates
16
+ attr_reader :messages, :users, :chats, :callback_queries, :guest_queries
17
+
18
+ UPDATE_NEW_MESSAGE = 0x1f2b0afd
19
+ UPDATE_NEW_CHANNEL_MESSAGE = 0x62ba04d9
20
+ UPDATE_BOT_CALLBACK_QUERY = 0xb9cfc48d
21
+ UPDATE_BOT_GUEST_CHAT_QUERY = 0xcdd4093d
22
+
23
+ def initialize(messages:, users:, chats:, callback_queries: [], guest_queries: [])
24
+ @messages = messages
25
+ @users = users
26
+ @chats = chats
27
+ @callback_queries = callback_queries
28
+ @guest_queries = guest_queries
29
+ end
30
+
31
+ # updates: updates:Vector<Update> users:Vector<User> chats:Vector<Chat> ...
32
+ # (updatesCombined adds a seq_start before seq; both share this prefix).
33
+ def self.deserialize(data)
34
+ constructor = data[0, 4].unpack1('L<')
35
+ unless [Constructors::UPDATES, Constructors::UPDATES_COMBINED].include?(constructor)
36
+ return new(messages: [], users: [], chats: [], callback_queries: [], guest_queries: [])
37
+ end
38
+
39
+ messages, callback_queries, guest_queries, offset = parse_updates_vector(data, 4)
40
+ users, offset = Dialogs.users_at(data, offset)
41
+ chats, = Dialogs.chats_at(data, offset)
42
+ new(messages: messages, users: users, chats: chats,
43
+ callback_queries: callback_queries, guest_queries: guest_queries)
44
+ end
45
+
46
+ def self.parse_updates_vector(data, offset)
47
+ offset += 4 # vector constructor
48
+ count = data[offset, 4].unpack1('L<')
49
+ offset += 4
50
+
51
+ messages = []
52
+ callback_queries = []
53
+ guest_queries = []
54
+ count.times do
55
+ ctor = data[offset, 4].unpack1('L<')
56
+ if [UPDATE_NEW_MESSAGE, UPDATE_NEW_CHANNEL_MESSAGE].include?(ctor)
57
+ inner = offset + 4
58
+ msg = Message.deserialize(data, inner, data[inner, 4].unpack1('L<'))
59
+ messages << msg.to_h if msg
60
+ elsif ctor == UPDATE_BOT_CALLBACK_QUERY
61
+ callback_queries << extract_callback_query(data, offset)
62
+ elsif ctor == UPDATE_BOT_GUEST_CHAT_QUERY
63
+ guest_queries << extract_guest_query(data, offset)
64
+ end
65
+ offset = Reader.schema.skip(data, offset)
66
+ end
67
+ [messages, callback_queries, guest_queries, offset]
68
+ end
69
+
70
+ # updateBotCallbackQuery#b9cfc48d flags:# query_id:long user_id:long peer:Peer
71
+ # msg_id:int chat_instance:long data:flags.0?bytes game_short_name:flags.1?string
72
+ # A bot receives this when a user taps an inline callback button; the data is
73
+ # the button's callback payload. The outer loop advances past it via Schema#skip.
74
+ def self.extract_callback_query(data, offset)
75
+ offset += 4 # constructor
76
+ flags = data[offset, 4].unpack1('L<')
77
+ offset += 4
78
+ query_id = data[offset, 8].unpack1('Q<')
79
+ offset += 8
80
+ user_id = data[offset, 8].unpack1('Q<')
81
+ offset += 8
82
+ peer_type, peer_id, offset = Reader.parse_peer(data, offset)
83
+ msg_id = data[offset, 4].unpack1('l<')
84
+ offset += 4
85
+ offset += 8 # chat_instance (unused)
86
+ payload = nil
87
+ payload, = Reader.read_tl_bytes(data, offset) if flags.anybits?(1 << 0)
88
+
89
+ { query_id: query_id, user_id: user_id, peer_type: peer_type, peer_id: peer_id,
90
+ msg_id: msg_id, data: payload }
91
+ end
92
+
93
+ # updateBotGuestChatQuery#cdd4093d flags:# query_id:long message:Message
94
+ # reference_messages:flags.0?Vector<Message> qts:int
95
+ # A guest-mode bot receives this when a user @-mentions it in a chat it is NOT a
96
+ # member of; the inner message is the mentioning message (sender in from_id). The
97
+ # bot answers once with messages.setBotGuestChatResult(query_id, result). The outer
98
+ # loop advances past the whole update via Schema#skip, so this only needs to read
99
+ # the fields we surface (query_id + the parsed message + qts).
100
+ def self.extract_guest_query(data, offset)
101
+ offset += 4 # constructor
102
+ flags = data[offset, 4].unpack1('L<')
103
+ offset += 4
104
+ query_id = data[offset, 8].unpack1('Q<')
105
+ offset += 8
106
+ msg = Message.deserialize(data, offset, data[offset, 4].unpack1('L<'))
107
+ offset = Reader.schema.skip(data, offset) # message:Message
108
+ if flags.anybits?(1 << 0) # reference_messages:Vector<Message>
109
+ offset = Reader.schema.skip_vector(data, offset) { |d, o| Reader.schema.skip(d, o) }
110
+ end
111
+ qts = data[offset, 4].unpack1('l<')
112
+
113
+ { query_id: query_id, message: msg&.to_h, qts: qts }
114
+ end
115
+ end
116
+ end
117
+ end
@@ -1,16 +1,19 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require_relative '../schema'
4
+ require_relative 'message'
4
5
  require_relative 'dialogs'
6
+ require_relative 'updates'
5
7
 
6
8
  module MTProto
7
9
  module TL
8
10
  class UpdatesDifference
9
- attr_reader :type, :date, :seq, :new_messages, :pts, :state, :chats, :users
11
+ attr_reader :type, :date, :seq, :new_messages, :pts, :state, :chats, :users, :guest_queries
10
12
 
11
13
  SCHEMA_PATH = File.expand_path('../../../../data/tl-schema.json', __dir__)
12
14
 
13
- def initialize(type:, date: nil, seq: nil, new_messages: [], pts: nil, state: nil, chats: [], users: [])
15
+ def initialize(type:, date: nil, seq: nil, new_messages: [], pts: nil, state: nil, chats: [], users: [],
16
+ guest_queries: [])
14
17
  @type = type
15
18
  @date = date
16
19
  @seq = seq
@@ -19,6 +22,7 @@ module MTProto
19
22
  @state = state
20
23
  @chats = chats
21
24
  @users = users
25
+ @guest_queries = guest_queries
22
26
  end
23
27
 
24
28
  def self.schema
@@ -41,12 +45,6 @@ module MTProto
41
45
  end
42
46
  end
43
47
 
44
- MESSAGE_CONSTRUCTOR = Constructors::MESSAGE
45
- MESSAGE_SERVICE = 0x7a800e0a
46
- MESSAGE_EMPTY = 0x90a6ca84
47
- MESSAGE_REPLY_HEADER = 0x6917560b
48
- MEDIA_PHOTO = 0x695150d7
49
- MEDIA_DOCUMENT = 0x52d8ccd9
50
48
  UPDATE_NEW_MESSAGE = 0x1f2b0afd
51
49
  UPDATE_NEW_CHANNEL_MESSAGE = 0x62ba04d9
52
50
 
@@ -70,7 +68,7 @@ module MTProto
70
68
 
71
69
  messages, offset = parse_messages_vector(data, offset)
72
70
  offset = schema.skip_vector(data, offset) # new_encrypted_messages
73
- other_messages, offset = parse_other_updates(data, offset)
71
+ other_messages, guest_queries, offset = parse_other_updates(data, offset)
74
72
  chats, offset = Dialogs.chats_at(data, offset)
75
73
  users, offset = Dialogs.users_at(data, offset)
76
74
  state = UpdatesState.deserialize(data[offset..])
@@ -78,6 +76,7 @@ module MTProto
78
76
  new(
79
77
  type: constructor == Constructors::UPDATES_DIFFERENCE ? :difference : :slice,
80
78
  new_messages: messages + other_messages,
79
+ guest_queries: guest_queries,
81
80
  chats: chats, users: users, state: state
82
81
  )
83
82
  end
@@ -91,16 +90,19 @@ module MTProto
91
90
  offset += 4
92
91
 
93
92
  messages = []
93
+ guest_queries = []
94
94
  count.times do
95
95
  ctor = data[offset, 4].unpack1('L<')
96
96
  if [UPDATE_NEW_MESSAGE, UPDATE_NEW_CHANNEL_MESSAGE].include?(ctor)
97
97
  inner_off = offset + 4
98
- msg = extract_message(data, inner_off, data[inner_off, 4].unpack1('L<'))
99
- messages << msg if msg
98
+ msg = Message.deserialize(data, inner_off, data[inner_off, 4].unpack1('L<'))
99
+ messages << msg.to_h if msg
100
+ elsif ctor == Updates::UPDATE_BOT_GUEST_CHAT_QUERY
101
+ guest_queries << Updates.extract_guest_query(data, offset)
100
102
  end
101
103
  offset = schema.skip(data, offset)
102
104
  end
103
- [messages, offset]
105
+ [messages, guest_queries, offset]
104
106
  end
105
107
 
106
108
  def parse_messages_vector(data, offset)
@@ -111,117 +113,12 @@ module MTProto
111
113
  messages = []
112
114
  count.times do
113
115
  constructor = data[offset, 4].unpack1('L<')
114
- msg = extract_message(data, offset, constructor)
115
- messages << msg if msg
116
+ msg = Message.deserialize(data, offset, constructor)
117
+ messages << msg.to_h if msg
116
118
  offset = schema.skip(data, offset)
117
119
  end
118
120
  [messages, offset]
119
121
  end
120
-
121
- def extract_message(data, offset, constructor)
122
- return unless constructor == MESSAGE_CONSTRUCTOR
123
-
124
- offset += 4 # constructor
125
- flags = data[offset, 4].unpack1('L<')
126
- offset += 4
127
- flags2 = data[offset, 4].unpack1('L<')
128
- offset += 4
129
- id = data[offset, 4].unpack1('l<')
130
- offset += 4
131
-
132
- from_type = from_id = nil
133
- from_type, from_id, offset = parse_peer(data, offset) if flags.anybits?(1 << 8) # from_id (Peer)
134
- offset += 4 if flags.anybits?(1 << 29) # from_boosts_applied
135
- peer_type, peer_id, offset = parse_peer(data, offset)
136
- offset = schema.skip(data, offset) if flags.anybits?(1 << 28) # saved_peer_id
137
- offset = schema.skip(data, offset) if flags.anybits?(1 << 2) # fwd_from
138
- offset += 8 if flags.anybits?(1 << 11) # via_bot_id
139
- offset += 8 if flags2.anybits?(1 << 0) # via_business_bot_id
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
147
-
148
- date = data[offset, 4].unpack1('L<')
149
- offset += 4
150
- message_text, offset = read_tl_string(data, offset)
151
- media = flags.anybits?(1 << 9) ? parse_media(data, offset) : nil
152
-
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 }
156
- end
157
-
158
- def parse_peer(data, offset)
159
- constructor = data[offset, 4].unpack1('L<')
160
- offset += 4
161
-
162
- case constructor
163
- when Constructors::PEER_USER
164
- [:user, data[offset, 8].unpack1('Q<'), offset + 8]
165
- when Constructors::PEER_CHAT
166
- [:chat, data[offset, 8].unpack1('Q<'), offset + 8]
167
- when Constructors::PEER_CHANNEL
168
- [:channel, data[offset, 8].unpack1('Q<'), offset + 8]
169
- else
170
- raise "Unknown peer constructor: 0x#{constructor.to_s(16)}"
171
- end
172
- end
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
-
211
- def read_tl_string(data, offset)
212
- first_byte = data.getbyte(offset)
213
- if first_byte == 254
214
- len = data.getbyte(offset + 1) |
215
- (data.getbyte(offset + 2) << 8) |
216
- (data.getbyte(offset + 3) << 16)
217
- total = 4 + len
218
- else
219
- len = first_byte
220
- total = 1 + len
221
- end
222
- padding = (4 - (total % 4)) % 4
223
- [data[offset + (total - len), len].force_encoding('UTF-8'), offset + total + padding]
224
- end
225
122
  end
226
123
  end
227
124
  end
@@ -0,0 +1,188 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'schema'
4
+
5
+ module MTProto
6
+ module TL
7
+ # Shared read-only TL primitives — peer/string/bytes/media/document reads and
8
+ # the schema handle — reused by the message parser (TL::Message) and the other
9
+ # update parsers. Each method only READS; advancing past a whole object is the
10
+ # caller's job via `Schema#skip`, so any unmodelled tail is sized by the schema.
11
+ module Reader
12
+ module_function
13
+
14
+ SCHEMA_PATH = File.expand_path('../../../data/tl-schema.json', __dir__)
15
+
16
+ MESSAGE_REPLY_HEADER = 0x1b97dd66
17
+ MEDIA_PHOTO = 0xe216eb63
18
+ MEDIA_DOCUMENT = 0x52d8ccd9
19
+ DOCUMENT = 0x8fd4c4d8
20
+ PHOTO = 0xfb197a65
21
+ DOCUMENT_ATTRIBUTE_AUDIO = 0x9852f9c6
22
+ DOCUMENT_ATTRIBUTE_FILENAME = 0x15590068
23
+
24
+ def schema
25
+ @schema ||= Schema.new(SCHEMA_PATH)
26
+ end
27
+
28
+ def parse_peer(data, offset)
29
+ constructor = data[offset, 4].unpack1('L<')
30
+ offset += 4
31
+
32
+ case constructor
33
+ when Constructors::PEER_USER
34
+ [:user, data[offset, 8].unpack1('Q<'), offset + 8]
35
+ when Constructors::PEER_CHAT
36
+ [:chat, data[offset, 8].unpack1('Q<'), offset + 8]
37
+ when Constructors::PEER_CHANNEL
38
+ [:channel, data[offset, 8].unpack1('Q<'), offset + 8]
39
+ else
40
+ raise "Unknown peer constructor: 0x#{constructor.to_s(16)}"
41
+ end
42
+ end
43
+
44
+ # messageReplyHeader: ctor, flags, reply_to_msg_id:flags.4?int (first payload
45
+ # field). A plain message in a forum topic carries this header (forum_topic
46
+ # flags.3) but is a reply only when it targets a message in the topic
47
+ # (reply_to_top_id flags.1). => [reply_to_msg_id, is_reply]
48
+ def parse_reply_to(data, offset)
49
+ return [nil, false] unless data[offset, 4].unpack1('L<') == MESSAGE_REPLY_HEADER
50
+
51
+ io = offset + 4
52
+ flags = data[io, 4].unpack1('L<')
53
+ io += 4
54
+ forum_topic = flags.anybits?(1 << 3)
55
+ msg_id = flags.anybits?(1 << 4) ? data[io, 4].unpack1('l<') : nil
56
+ [msg_id, forum_topic ? flags.anybits?(1 << 1) : true]
57
+ rescue StandardError
58
+ [nil, false]
59
+ end
60
+
61
+ # Classify message media into :photo / :video / :audio / :document, or nil.
62
+ def parse_media(data, offset)
63
+ case data[offset, 4].unpack1('L<')
64
+ when MEDIA_PHOTO
65
+ :photo
66
+ when MEDIA_DOCUMENT
67
+ document_kind(data[offset + 4, 4].unpack1('L<'))
68
+ end
69
+ rescue StandardError
70
+ nil
71
+ end
72
+
73
+ # messageMediaDocument flags: video flags.6 / round flags.7 / voice flags.8.
74
+ def document_kind(flags)
75
+ return :video if flags.anybits?(1 << 6) || flags.anybits?(1 << 7)
76
+ return :audio if flags.anybits?(1 << 8)
77
+
78
+ :document
79
+ end
80
+
81
+ # When the media is messageMediaDocument, pull the document's downloadable
82
+ # fields (file location + audio metadata); nil for any other media. The
83
+ # thumb vectors before dc_id are sized by the schema, so this reaches dc_id
84
+ # and the attribute list correctly regardless of thumbnails.
85
+ def parse_document(data, offset)
86
+ return unless data[offset, 4].unpack1('L<') == MEDIA_DOCUMENT
87
+
88
+ media_flags = data[offset + 4, 4].unpack1('L<')
89
+ return unless media_flags.anybits?(1 << 0) # document present
90
+
91
+ offset += 8
92
+ return unless data[offset, 4].unpack1('L<') == DOCUMENT
93
+
94
+ offset += 4
95
+ flags = data[offset, 4].unpack1('L<')
96
+ offset += 4
97
+ id = data[offset, 8].unpack1('q<')
98
+ access_hash = data[offset + 8, 8].unpack1('q<')
99
+ offset += 16
100
+ file_reference, offset = read_tl_bytes(data, offset)
101
+ offset += 4 # date
102
+ mime_type, offset = read_tl_string(data, offset)
103
+ size = data[offset, 8].unpack1('q<')
104
+ offset += 8
105
+ offset = schema.skip_vector(data, offset) if flags.anybits?(1 << 0) # thumbs
106
+ offset = schema.skip_vector(data, offset) if flags.anybits?(1 << 1) # video_thumbs
107
+ dc_id = data[offset, 4].unpack1('l<')
108
+ offset += 4
109
+ voice, duration, file_name = parse_document_attributes(data, offset)
110
+
111
+ { id: id, access_hash: access_hash, file_reference: file_reference, dc_id: dc_id,
112
+ mime_type: mime_type, size: size, voice: voice, duration: duration, file_name: file_name }
113
+ rescue StandardError
114
+ nil
115
+ end
116
+
117
+ # When the media is messageMediaPhoto, pull the photo's input reference
118
+ # (id/access_hash/file_reference) so it can be re-sent via inputMediaPhoto —
119
+ # e.g. re-posting a guest-mention's photo, where forwarding is not allowed.
120
+ # has_stickers is a true-flag (no body), so id starts right after photo's flags.
121
+ def parse_photo(data, offset)
122
+ return unless data[offset, 4].unpack1('L<') == MEDIA_PHOTO
123
+
124
+ media_flags = data[offset + 4, 4].unpack1('L<')
125
+ return unless media_flags.anybits?(1 << 0) # photo present
126
+
127
+ offset += 8 # messageMediaPhoto ctor + flags
128
+ return unless data[offset, 4].unpack1('L<') == PHOTO
129
+
130
+ offset += 8 # photo ctor + flags
131
+ id = data[offset, 8].unpack1('q<')
132
+ access_hash = data[offset + 8, 8].unpack1('q<')
133
+ offset += 16
134
+ file_reference, = read_tl_bytes(data, offset)
135
+
136
+ { id: id, access_hash: access_hash, file_reference: file_reference }
137
+ rescue StandardError
138
+ nil
139
+ end
140
+
141
+ # Vector<DocumentAttribute>: pull voice/duration from documentAttributeAudio
142
+ # and the name from documentAttributeFilename; advance over the rest via
143
+ # schema so an unknown attribute never throws off the read.
144
+ def parse_document_attributes(data, offset)
145
+ offset += 4 # vector constructor
146
+ count = data[offset, 4].unpack1('L<')
147
+ offset += 4
148
+
149
+ voice = false
150
+ duration = nil
151
+ file_name = nil
152
+ count.times do
153
+ ctor = data[offset, 4].unpack1('L<')
154
+ case ctor
155
+ when DOCUMENT_ATTRIBUTE_AUDIO
156
+ aflags = data[offset + 4, 4].unpack1('L<')
157
+ voice ||= aflags.anybits?(1 << 10)
158
+ duration = data[offset + 8, 4].unpack1('l<')
159
+ when DOCUMENT_ATTRIBUTE_FILENAME
160
+ file_name, = read_tl_string(data, offset + 4)
161
+ end
162
+ offset = schema.skip(data, offset)
163
+ end
164
+ [voice, duration, file_name]
165
+ end
166
+
167
+ def read_tl_string(data, offset)
168
+ bytes, offset = read_tl_bytes(data, offset)
169
+ [bytes.force_encoding('UTF-8'), offset]
170
+ end
171
+
172
+ def read_tl_bytes(data, offset)
173
+ first_byte = data.getbyte(offset)
174
+ if first_byte == 254
175
+ len = data.getbyte(offset + 1) |
176
+ (data.getbyte(offset + 2) << 8) |
177
+ (data.getbyte(offset + 3) << 16)
178
+ total = 4 + len
179
+ else
180
+ len = first_byte
181
+ total = 1 + len
182
+ end
183
+ padding = (4 - (total % 4)) % 4
184
+ [data[offset + (total - len), len], offset + total + padding]
185
+ end
186
+ end
187
+ end
188
+ end
@@ -22,7 +22,11 @@ module MTProto
22
22
  @stream.write packet.data.pack('C*')
23
23
  end
24
24
 
25
- MAX_PACKET_SIZE = 1_048_576
25
+ # The largest packet the abridged length field can express: a 3-byte
26
+ # little-endian count of 4-byte words, i.e. ((1 << 24) - 1) words << 2.
27
+ # The old 1 MiB cap couldn't receive a full 1 MiB getFile reply (~1 MiB +
28
+ # envelope ≈ 1048680 B), so files larger than 1 MiB never downloaded.
29
+ MAX_PACKET_SIZE = ((1 << 24) - 1) << 2
26
30
 
27
31
  def scan
28
32
  first_byte = read_exactly(1)
@@ -0,0 +1,39 @@
1
+ # frozen_string_literal: true
2
+
3
+ module MTProto
4
+ # The unencrypted MTProto transport envelope (auth_key_id=0 | msg_id | len | body),
5
+ # used for the plaintext request/response of the DH key exchange. Its encrypted
6
+ # sibling is EncryptedMessage; the runtime never decrypts through this class.
7
+ class UnencryptedMessage
8
+ include Binary
9
+ extend Binary
10
+
11
+ attr_reader :auth_key_id, :msg_id, :body
12
+
13
+ def initialize(body, msg_id: self.class.generate_msg_id, auth_key_id: 0)
14
+ @auth_key_id = auth_key_id
15
+ @msg_id = msg_id
16
+ @body = body
17
+ end
18
+
19
+ def self.parse(packet)
20
+ data = packet.data
21
+ msg_id = b_u64(data[8, 8])
22
+ body_length = b_u32(data[16, 4])
23
+ body = data[20, body_length]
24
+
25
+ new(body, msg_id: msg_id, auth_key_id: 0)
26
+ end
27
+
28
+ def bytes
29
+ u64_b(@auth_key_id) +
30
+ u64_b(@msg_id) +
31
+ u32_b(@body.length) +
32
+ @body
33
+ end
34
+
35
+ def self.generate_msg_id
36
+ (Time.now.to_f * (2**32)).to_i & ~3
37
+ end
38
+ end
39
+ end
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module MTProto
4
- VERSION = '0.0.15'
4
+ VERSION = '0.0.17'
5
5
  end
data/lib/mtproto.rb CHANGED
@@ -9,13 +9,15 @@ require_relative 'mtproto/transport/packet'
9
9
  require_relative 'mtproto/transport/abridged_packet_codec'
10
10
  require_relative 'mtproto/transport/tcp_connection'
11
11
  require_relative 'mtproto/transport/connection'
12
- require_relative 'mtproto/message/message'
12
+ require_relative 'mtproto/unencrypted_message'
13
13
  require_relative 'mtproto/tl/constructors'
14
14
  require_relative 'mtproto/tl/constructor_names'
15
+ require_relative 'mtproto/tl/reader'
15
16
  require_relative 'mtproto/tl/object'
16
17
  require_relative 'mtproto/tl/objects/update'
17
18
  require_relative 'mtproto/tl/objects/update_short'
18
19
  require_relative 'mtproto/tl/objects/update_short_message'
20
+ require_relative 'mtproto/tl/objects/updates'
19
21
  require_relative 'mtproto/tl/objects/message'
20
22
  require_relative 'mtproto/tl/objects/msg_container'
21
23
  require_relative 'mtproto/tl/objects/new_session_created'
@@ -33,6 +35,7 @@ require_relative 'mtproto/auth_key_generator'
33
35
  require_relative 'mtproto/session'
34
36
  require_relative 'mtproto/encrypted_message'
35
37
  require_relative 'mtproto/client'
38
+ require_relative 'mtproto/file_downloader'
36
39
 
37
40
  module MTProto
38
41
  end