mtproto 0.0.14 → 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 (60) 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/rpc.rb +33 -0
  8. data/lib/mtproto/client.rb +54 -1
  9. data/lib/mtproto/file_downloader.rb +122 -0
  10. data/lib/mtproto/tl/constructor_names.rb +324 -109
  11. data/lib/mtproto/tl/constructors.rb +16 -8
  12. data/lib/mtproto/tl/objects/bot_command_scope.rb +81 -0
  13. data/lib/mtproto/tl/objects/channels_join_channel.rb +1 -1
  14. data/lib/mtproto/tl/objects/channels_update_username.rb +42 -0
  15. data/lib/mtproto/tl/objects/contacts.rb +1 -1
  16. data/lib/mtproto/tl/objects/create_bot.rb +54 -0
  17. data/lib/mtproto/tl/objects/dialogs.rb +25 -10
  18. data/lib/mtproto/tl/objects/edit_access_settings.rb +46 -0
  19. data/lib/mtproto/tl/objects/export_authorization.rb +17 -0
  20. data/lib/mtproto/tl/objects/export_bot_token.rb +32 -0
  21. data/lib/mtproto/tl/objects/exported_authorization.rb +32 -0
  22. data/lib/mtproto/tl/objects/exported_bot_token.rb +27 -0
  23. data/lib/mtproto/tl/objects/forward_messages.rb +12 -3
  24. data/lib/mtproto/tl/objects/get_access_settings.rb +28 -0
  25. data/lib/mtproto/tl/objects/get_bot_callback_answer.rb +67 -0
  26. data/lib/mtproto/tl/objects/get_bot_commands.rb +46 -0
  27. data/lib/mtproto/tl/objects/get_file.rb +10 -2
  28. data/lib/mtproto/tl/objects/import_authorization.rb +38 -0
  29. data/lib/mtproto/tl/objects/input_keyboard_button_request_peer.rb +54 -0
  30. data/lib/mtproto/tl/objects/keyboard_button_callback.rb +50 -0
  31. data/lib/mtproto/tl/objects/message.rb +97 -21
  32. data/lib/mtproto/tl/objects/messages.rb +7 -74
  33. data/lib/mtproto/tl/objects/messages_get_messages.rb +26 -0
  34. data/lib/mtproto/tl/objects/reply_inline_markup.rb +30 -0
  35. data/lib/mtproto/tl/objects/reply_keyboard_markup.rb +35 -0
  36. data/lib/mtproto/tl/objects/request_peer_type_create_bot.rb +47 -0
  37. data/lib/mtproto/tl/objects/reset_bot_commands.rb +45 -0
  38. data/lib/mtproto/tl/objects/send_bot_requested_peer.rb +51 -0
  39. data/lib/mtproto/tl/objects/send_media.rb +87 -4
  40. data/lib/mtproto/tl/objects/send_message.rb +7 -2
  41. data/lib/mtproto/tl/objects/send_message_action.rb +48 -0
  42. data/lib/mtproto/tl/objects/set_bot_callback_answer.rb +54 -0
  43. data/lib/mtproto/tl/objects/set_bot_commands.rb +53 -0
  44. data/lib/mtproto/tl/objects/set_bot_guest_chat_result.rb +84 -0
  45. data/lib/mtproto/tl/objects/set_bot_info.rb +64 -0
  46. data/lib/mtproto/tl/objects/set_typing.rb +49 -0
  47. data/lib/mtproto/tl/objects/update_status.rb +24 -0
  48. data/lib/mtproto/tl/objects/updates.rb +117 -0
  49. data/lib/mtproto/tl/objects/updates_difference.rb +45 -69
  50. data/lib/mtproto/tl/objects/upload_profile_photo.rb +65 -0
  51. data/lib/mtproto/tl/reader.rb +188 -0
  52. data/lib/mtproto/transport/abridged_packet_codec.rb +5 -1
  53. data/lib/mtproto/unencrypted_message.rb +39 -0
  54. data/lib/mtproto/version.rb +1 -1
  55. data/lib/mtproto.rb +4 -1
  56. data/scripts/gen_constructor_names.rb +72 -0
  57. data/scripts/tl_to_json.rb +72 -0
  58. data/scripts/verify_ids.rb +33 -0
  59. metadata +38 -2
  60. data/lib/mtproto/message/message.rb +0 -85
@@ -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,21 +1,28 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require_relative '../schema'
4
+ require_relative 'message'
5
+ require_relative 'dialogs'
6
+ require_relative 'updates'
4
7
 
5
8
  module MTProto
6
9
  module TL
7
10
  class UpdatesDifference
8
- attr_reader :type, :date, :seq, :new_messages, :pts, :state
11
+ attr_reader :type, :date, :seq, :new_messages, :pts, :state, :chats, :users, :guest_queries
9
12
 
10
13
  SCHEMA_PATH = File.expand_path('../../../../data/tl-schema.json', __dir__)
11
14
 
12
- def initialize(type:, date: nil, seq: nil, new_messages: [], pts: nil, state: nil)
15
+ def initialize(type:, date: nil, seq: nil, new_messages: [], pts: nil, state: nil, chats: [], users: [],
16
+ guest_queries: [])
13
17
  @type = type
14
18
  @date = date
15
19
  @seq = seq
16
20
  @new_messages = new_messages
17
21
  @pts = pts
18
22
  @state = state
23
+ @chats = chats
24
+ @users = users
25
+ @guest_queries = guest_queries
19
26
  end
20
27
 
21
28
  def self.schema
@@ -38,9 +45,8 @@ module MTProto
38
45
  end
39
46
  end
40
47
 
41
- MESSAGE_CONSTRUCTOR = Constructors::MESSAGE
42
- MESSAGE_SERVICE = 0x7a800e0a
43
- MESSAGE_EMPTY = 0x90a6ca84
48
+ UPDATE_NEW_MESSAGE = 0x1f2b0afd
49
+ UPDATE_NEW_CHANNEL_MESSAGE = 0x62ba04d9
44
50
 
45
51
  class << self
46
52
  private
@@ -53,95 +59,65 @@ module MTProto
53
59
  )
54
60
  end
55
61
 
56
- # updates.difference: new_messages new_encrypted_messages other_updates chats users state
62
+ # updates.difference: new_messages new_encrypted_messages other_updates chats users state.
63
+ # Channel/supergroup messages arrive in other_updates as
64
+ # updateNewChannelMessage (not in new_messages), so we mine those too; and
65
+ # chats/users carry the peers' access_hashes for the bot's peer cache.
57
66
  def parse_difference(data, constructor)
58
67
  offset = 4
59
68
 
60
69
  messages, offset = parse_messages_vector(data, offset)
61
70
  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
71
+ other_messages, guest_queries, offset = parse_other_updates(data, offset)
72
+ chats, offset = Dialogs.chats_at(data, offset)
73
+ users, offset = Dialogs.users_at(data, offset)
65
74
  state = UpdatesState.deserialize(data[offset..])
66
75
 
67
76
  new(
68
77
  type: constructor == Constructors::UPDATES_DIFFERENCE ? :difference : :slice,
69
- new_messages: messages,
70
- state: state
78
+ new_messages: messages + other_messages,
79
+ guest_queries: guest_queries,
80
+ chats: chats, users: users, state: state
71
81
  )
72
82
  end
73
83
 
74
- def parse_messages_vector(data, offset)
84
+ # other_updates: Vector<Update>. Mine the message-bearing updates
85
+ # (updateNewMessage / updateNewChannelMessage — each is the update ctor,
86
+ # then an inner Message, then pts/pts_count); advance the rest via schema.
87
+ def parse_other_updates(data, offset)
75
88
  offset += 4 # vector constructor
76
89
  count = data[offset, 4].unpack1('L<')
77
90
  offset += 4
78
91
 
79
92
  messages = []
93
+ guest_queries = []
80
94
  count.times do
81
- constructor = data[offset, 4].unpack1('L<')
82
- msg = extract_message(data, offset, constructor)
83
- messages << msg if msg
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 = 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)
102
+ end
84
103
  offset = schema.skip(data, offset)
85
104
  end
86
- [messages, offset]
87
- end
88
-
89
- def extract_message(data, offset, constructor)
90
- return unless constructor == MESSAGE_CONSTRUCTOR
91
-
92
- offset += 4 # constructor
93
- flags = data[offset, 4].unpack1('L<')
94
- offset += 4
95
- flags2 = data[offset, 4].unpack1('L<')
96
- offset += 4
97
- id = data[offset, 4].unpack1('l<')
98
- offset += 4
99
-
100
- offset = schema.skip(data, offset) if flags.anybits?(1 << 8) # from_id (Peer)
101
- offset += 4 if flags.anybits?(1 << 29) # from_boosts_applied
102
- peer_type, peer_id, offset = parse_peer(data, offset)
103
- offset = schema.skip(data, offset) if flags.anybits?(1 << 28) # saved_peer_id
104
- offset = schema.skip(data, offset) if flags.anybits?(1 << 2) # fwd_from
105
- offset += 8 if flags.anybits?(1 << 11) # via_bot_id
106
- offset += 8 if flags2.anybits?(1 << 0) # via_business_bot_id
107
- offset = schema.skip(data, offset) if flags.anybits?(1 << 3) # reply_to
108
-
109
- date = data[offset, 4].unpack1('L<')
110
- offset += 4
111
- message_text, = read_tl_string(data, offset)
112
-
113
- { id: id, date: date, message: message_text, peer_type: peer_type, peer_id: peer_id }
105
+ [messages, guest_queries, offset]
114
106
  end
115
107
 
116
- def parse_peer(data, offset)
117
- constructor = data[offset, 4].unpack1('L<')
108
+ def parse_messages_vector(data, offset)
109
+ offset += 4 # vector constructor
110
+ count = data[offset, 4].unpack1('L<')
118
111
  offset += 4
119
112
 
120
- case constructor
121
- when Constructors::PEER_USER
122
- [:user, data[offset, 8].unpack1('Q<'), offset + 8]
123
- when Constructors::PEER_CHAT
124
- [:chat, data[offset, 8].unpack1('Q<'), offset + 8]
125
- when Constructors::PEER_CHANNEL
126
- [:channel, data[offset, 8].unpack1('Q<'), offset + 8]
127
- else
128
- raise "Unknown peer constructor: 0x#{constructor.to_s(16)}"
129
- end
130
- end
131
-
132
- def read_tl_string(data, offset)
133
- first_byte = data.getbyte(offset)
134
- if first_byte == 254
135
- len = data.getbyte(offset + 1) |
136
- (data.getbyte(offset + 2) << 8) |
137
- (data.getbyte(offset + 3) << 16)
138
- total = 4 + len
139
- else
140
- len = first_byte
141
- total = 1 + len
113
+ messages = []
114
+ count.times do
115
+ constructor = data[offset, 4].unpack1('L<')
116
+ msg = Message.deserialize(data, offset, constructor)
117
+ messages << msg.to_h if msg
118
+ offset = schema.skip(data, offset)
142
119
  end
143
- padding = (4 - (total % 4)) % 4
144
- [data[offset + (total - len), len].force_encoding('UTF-8'), offset + total + padding]
120
+ [messages, offset]
145
121
  end
146
122
  end
147
123
  end
@@ -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
@@ -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.14'
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