mtproto 0.0.32 → 0.0.34

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 (37) hide show
  1. checksums.yaml +4 -4
  2. data/docs/test_architecture_level1.md +23 -107
  3. data/lib/mtproto/auth_key_generator.rb +3 -1
  4. data/lib/mtproto/client/api/resolve_username.rb +18 -0
  5. data/lib/mtproto/client/api/send_message.rb +5 -2
  6. data/lib/mtproto/client/api/set_typing.rb +4 -4
  7. data/lib/mtproto/client/api/stream_draft.rb +13 -0
  8. data/lib/mtproto/client/api.rb +2 -0
  9. data/lib/mtproto/crypto/dh_key_exchange.rb +2 -1
  10. data/lib/mtproto/draft_stream.rb +52 -0
  11. data/lib/mtproto/tl/constructors.rb +1 -0
  12. data/lib/mtproto/tl/objects/authorization.rb +4 -3
  13. data/lib/mtproto/tl/objects/channel_difference.rb +13 -32
  14. data/lib/mtproto/tl/objects/contacts.rb +7 -89
  15. data/lib/mtproto/tl/objects/dialogs.rb +14 -352
  16. data/lib/mtproto/tl/objects/edit_message.rb +1 -1
  17. data/lib/mtproto/tl/objects/media_parts.rb +68 -0
  18. data/lib/mtproto/tl/objects/message.rb +14 -0
  19. data/lib/mtproto/tl/objects/message_service.rb +40 -0
  20. data/lib/mtproto/tl/objects/messages.rb +7 -32
  21. data/lib/mtproto/tl/objects/reply_keyboard_force_reply.rb +46 -0
  22. data/lib/mtproto/tl/objects/resolved_peer.rb +29 -0
  23. data/lib/mtproto/tl/objects/send_message.rb +1 -1
  24. data/lib/mtproto/tl/objects/send_message_text_draft_action.rb +52 -0
  25. data/lib/mtproto/tl/objects/send_multi_media.rb +52 -0
  26. data/lib/mtproto/tl/objects/update.rb +3 -35
  27. data/lib/mtproto/tl/objects/updates.rb +4 -6
  28. data/lib/mtproto/tl/objects/updates_difference.rb +9 -29
  29. data/lib/mtproto/tl/objects/upload_media.rb +26 -0
  30. data/lib/mtproto/tl/peers.rb +236 -0
  31. data/lib/mtproto/tl/reader.rb +29 -2
  32. data/lib/mtproto/updates/event_builder.rb +20 -32
  33. data/lib/mtproto/updates/parser.rb +9 -8
  34. data/lib/mtproto/updates/state_machine.rb +7 -7
  35. data/lib/mtproto/version.rb +1 -1
  36. data/lib/mtproto.rb +1 -0
  37. metadata +12 -1
@@ -0,0 +1,46 @@
1
+ # frozen_string_literal: true
2
+
3
+ module MTProto
4
+ module TL
5
+ class ReplyKeyboardForceReply
6
+ include Binary
7
+
8
+ CONSTRUCTOR = 0x86b40b08
9
+
10
+ def initialize(single_use: false, selective: false, placeholder: nil)
11
+ @single_use = single_use
12
+ @selective = selective
13
+ @placeholder = placeholder
14
+ end
15
+
16
+ def serialize
17
+ flags = 0
18
+ flags |= (1 << 1) if @single_use
19
+ flags |= (1 << 2) if @selective
20
+ flags |= (1 << 3) if @placeholder
21
+
22
+ result = u32_b(CONSTRUCTOR)
23
+ result += u32_b(flags)
24
+ result += serialize_tl_string(@placeholder) if @placeholder
25
+ result
26
+ end
27
+
28
+ private
29
+
30
+ def serialize_tl_string(str)
31
+ bytes = str.encode('UTF-8').bytes
32
+ length = bytes.length
33
+ if length <= 253
34
+ [length] + bytes + padding(length + 1)
35
+ else
36
+ [254] + u32_b(length)[0, 3] + bytes + padding(length + 4)
37
+ end
38
+ end
39
+
40
+ def padding(current_length)
41
+ pad_length = (4 - (current_length % 4)) % 4
42
+ [0] * pad_length
43
+ end
44
+ end
45
+ end
46
+ end
@@ -0,0 +1,29 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative '../reader'
4
+ require_relative '../peers'
5
+
6
+ module MTProto
7
+ module TL
8
+ class ResolvedPeer
9
+ def self.deserialize(data)
10
+ constructor = data[0, 4].unpack1('L<')
11
+ raise UnexpectedConstructorError, constructor unless constructor == Constructors::CONTACTS_RESOLVED_PEER
12
+
13
+ type, id, offset = Reader.parse_peer(data, 4)
14
+ chats, offset = Peers.chats_at(data, offset)
15
+ users, = Peers.users_at(data, offset)
16
+
17
+ { type: type, id: id, access_hash: access_hash_for(type, id, chats, users) }
18
+ end
19
+
20
+ def self.access_hash_for(type, id, chats, users)
21
+ return chats.find { |chat| chat.id == id }&.access_hash unless type == :user
22
+
23
+ user = users.find { |candidate| candidate[:id] == id }
24
+ user && user[:access_hash]
25
+ end
26
+ private_class_method :access_hash_for
27
+ end
28
+ end
29
+ end
@@ -52,7 +52,7 @@ module MTProto
52
52
  private
53
53
 
54
54
  def entities?
55
- @entities && !@entities.empty? && !@rich_markdown
55
+ !@entities.nil? && !@rich_markdown
56
56
  end
57
57
 
58
58
  # inputRichMessageMarkdown#004b572c flags:# rtl:flags.0?true noautolink:flags.1?true
@@ -0,0 +1,52 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative '../message_entity'
4
+
5
+ module MTProto
6
+ module TL
7
+ # A SendMessageAction carrying a live draft: the partially generated answer the
8
+ # recipient's client shows at the bottom of the chat while it is still being
9
+ # written. Unlike the other actions it is not a bare constructor id, so it does
10
+ # not fit TL::SendMessageAction's id-plus-progress table.
11
+ #
12
+ # `random_id` identifies the draft: the same value updates the draft already on
13
+ # screen, a different one puts a second draft beside it.
14
+ class SendMessageTextDraftAction
15
+ include Binary
16
+
17
+ CONSTRUCTOR = 0x376d975c
18
+ TEXT_WITH_ENTITIES = 0x751f3146
19
+
20
+ def initialize(random_id:, text:, entities: [])
21
+ @random_id = random_id
22
+ @text = text
23
+ @entities = entities
24
+ end
25
+
26
+ # textWithEntities#751f3146 text:string entities:Vector<MessageEntity> — the
27
+ # entity vector is not optional, so an empty list still goes on the wire.
28
+ def serialize
29
+ u32_b(CONSTRUCTOR) + u64_b(@random_id) +
30
+ u32_b(TEXT_WITH_ENTITIES) + serialize_tl_string(@text) +
31
+ MessageEntities.serialize(@entities)
32
+ end
33
+
34
+ private
35
+
36
+ def serialize_tl_string(str)
37
+ bytes = str.encode('UTF-8').bytes
38
+ length = bytes.length
39
+ if length <= 253
40
+ [length] + bytes + padding(length + 1)
41
+ else
42
+ [254] + u32_b(length)[0, 3] + bytes + padding(length + 4)
43
+ end
44
+ end
45
+
46
+ def padding(current_length)
47
+ pad_length = (4 - (current_length % 4)) % 4
48
+ [0] * pad_length
49
+ end
50
+ end
51
+ end
52
+ end
@@ -0,0 +1,52 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'securerandom'
4
+
5
+ require_relative 'media_parts'
6
+
7
+ module MTProto
8
+ module TL
9
+ # messages.sendMultiMedia — several photos as ONE message, the way a person sends an
10
+ # album.
11
+ #
12
+ # The words ride on the first of them and nowhere else: an album carries a single
13
+ # caption, and repeating it under each picture is how it looks when somebody sends
14
+ # them one at a time.
15
+ class SendMultiMedia
16
+ include MediaParts
17
+
18
+ CONSTRUCTOR = 0x1bf89d74
19
+ INPUT_SINGLE_MEDIA = 0x1cc6e91f
20
+ INPUT_REPLY_TO_MESSAGE = 0x3bd4b7c2
21
+
22
+ def initialize(peer:, photos:, message: '', reply_to: nil, silent: false)
23
+ @peer = peer
24
+ @photos = photos
25
+ @message = message
26
+ @reply_to = reply_to
27
+ @silent = silent
28
+ end
29
+
30
+ def serialize
31
+ flags = 0
32
+ flags |= (1 << 0) if @reply_to
33
+ flags |= (1 << 5) if @silent
34
+
35
+ result = u32_b(CONSTRUCTOR) + u32_b(flags) + serialize_peer(@peer)
36
+ result += u32_b(INPUT_REPLY_TO_MESSAGE) + u32_b(0) + u32_b(@reply_to) if @reply_to
37
+ result += u32_b(Constructors::VECTOR) + u32_b(@photos.length)
38
+ @photos.each_with_index { |photo, nth| result += serialize_single(photo, nth) }
39
+ result
40
+ end
41
+
42
+ private
43
+
44
+ def serialize_single(photo, nth)
45
+ u32_b(INPUT_SINGLE_MEDIA) + u32_b(0) +
46
+ serialize_held_photo(photo) +
47
+ u64_b(SecureRandom.random_number(2**63)) +
48
+ serialize_text(nth.zero? ? @message.to_s : '')
49
+ end
50
+ end
51
+ end
52
+ end
@@ -1,5 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require_relative 'message'
4
+
3
5
  module MTProto
4
6
  module TL
5
7
  class Update
@@ -50,41 +52,7 @@ module MTProto
50
52
  end
51
53
 
52
54
  def extract_new_message_text(bytes)
53
- offset = 4 # skip updateNewMessage constructor
54
-
55
- msg_constructor = bytes[offset, 4].unpack1('L<')
56
- return nil unless msg_constructor == Constructors::MESSAGE
57
-
58
- offset += 4
59
- flags = bytes[offset, 4].unpack1('L<')
60
- offset += 4
61
- flags2 = bytes[offset, 4].unpack1('L<')
62
- offset += 4
63
- offset += 4 # id
64
-
65
- offset += 12 if flags & (1 << 8) != 0 # from_id: Peer
66
- offset += 12 # peer_id: Peer
67
- offset += 12 if flags & (1 << 28) != 0 # saved_peer_id: Peer
68
- return nil if flags & (1 << 2) != 0 # fwd_from: variable length
69
-
70
- offset += 8 if flags & (1 << 11) != 0 # via_bot_id: long
71
- offset += 8 if flags2 & 1 != 0 # via_business_bot_id: long
72
- return nil if flags & (1 << 3) != 0 # reply_to: variable length
73
-
74
- offset += 4 # date
75
- read_tl_string(bytes, offset)
76
- end
77
-
78
- def read_tl_string(bytes, offset)
79
- first_byte = bytes.getbyte(offset)
80
- if first_byte == 254
81
- len = bytes.getbyte(offset + 1) |
82
- (bytes.getbyte(offset + 2) << 8) |
83
- (bytes.getbyte(offset + 3) << 16)
84
- bytes[offset + 4, len]
85
- else
86
- bytes[offset + 1, first_byte]
87
- end
55
+ Message.deserialize(bytes, 4, bytes[4, 4].unpack1('L<'))&.text
88
56
  end
89
57
  end
90
58
  end
@@ -1,8 +1,8 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require_relative '../reader'
4
+ require_relative '../peers'
4
5
  require_relative 'message'
5
- require_relative 'dialogs'
6
6
 
7
7
  module MTProto
8
8
  module TL
@@ -37,8 +37,8 @@ module MTProto
37
37
  end
38
38
 
39
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)
40
+ users, offset = Peers.users_at(data, offset)
41
+ chats, = Peers.chats_at(data, offset)
42
42
  new(messages: messages, users: users, chats: chats,
43
43
  callback_queries: callback_queries, guest_queries: guest_queries)
44
44
  end
@@ -105,9 +105,7 @@ module MTProto
105
105
  offset += 8
106
106
  msg = Message.deserialize(data, offset, data[offset, 4].unpack1('L<'))
107
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
108
+ offset = Reader.schema.skip_vector(data, offset) if flags.anybits?(1 << 0) # reference_messages
111
109
  qts = data[offset, 4].unpack1('l<')
112
110
 
113
111
  { query_id: query_id, message: msg&.to_h, qts: qts }
@@ -1,8 +1,8 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require_relative '../schema'
3
+ require_relative '../reader'
4
+ require_relative '../peers'
4
5
  require_relative 'message'
5
- require_relative 'dialogs'
6
6
  require_relative 'updates'
7
7
 
8
8
  module MTProto
@@ -10,8 +10,6 @@ module MTProto
10
10
  class UpdatesDifference
11
11
  attr_reader :type, :date, :seq, :new_messages, :pts, :state, :chats, :users, :guest_queries, :managed_bots
12
12
 
13
- SCHEMA_PATH = File.expand_path('../../../../data/tl-schema.json', __dir__)
14
-
15
13
  def initialize(type:, date: nil, seq: nil, new_messages: [], pts: nil, state: nil, chats: [], users: [],
16
14
  guest_queries: [], managed_bots: [])
17
15
  @type = type
@@ -26,10 +24,6 @@ module MTProto
26
24
  @managed_bots = managed_bots
27
25
  end
28
26
 
29
- def self.schema
30
- @schema ||= Schema.new(SCHEMA_PATH)
31
- end
32
-
33
27
  def self.deserialize(data)
34
28
  constructor = data[0, 4].unpack1('L<')
35
29
 
@@ -68,11 +62,12 @@ module MTProto
68
62
  def parse_difference(data, constructor)
69
63
  offset = 4
70
64
 
71
- messages, offset = parse_messages_vector(data, offset)
72
- offset = schema.skip_vector(data, offset) # new_encrypted_messages
65
+ raw_messages, offset = Message.vector_at(data, offset)
66
+ messages = raw_messages.map(&:to_h)
67
+ offset = Reader.schema.skip_vector(data, offset) # new_encrypted_messages
73
68
  other_messages, guest_queries, managed_bots, offset = parse_other_updates(data, offset)
74
- chats, offset = Dialogs.chats_at(data, offset)
75
- users, offset = Dialogs.users_at(data, offset)
69
+ chats, offset = Peers.chats_at(data, offset)
70
+ users, offset = Peers.users_at(data, offset)
76
71
  state = UpdatesState.deserialize(data[offset..])
77
72
 
78
73
  new(
@@ -86,7 +81,7 @@ module MTProto
86
81
 
87
82
  # other_updates: Vector<Update>. Mine the message-bearing updates
88
83
  # (updateNewMessage / updateNewChannelMessage — each is the update ctor,
89
- # then an inner Message, then pts/pts_count); advance the rest via schema.
84
+ # then an inner Message, then pts/pts_count); advance the rest via Reader.schema.
90
85
  def parse_other_updates(data, offset)
91
86
  offset += 4 # vector constructor
92
87
  count = data[offset, 4].unpack1('L<')
@@ -106,25 +101,10 @@ module MTProto
106
101
  elsif ctor == UPDATE_MANAGED_BOT
107
102
  managed_bots << { user_id: data[offset + 4, 8].unpack1('Q<'), bot_id: data[offset + 12, 8].unpack1('Q<') }
108
103
  end
109
- offset = schema.skip(data, offset)
104
+ offset = Reader.schema.skip(data, offset)
110
105
  end
111
106
  [messages, guest_queries, managed_bots, offset]
112
107
  end
113
-
114
- def parse_messages_vector(data, offset)
115
- offset += 4 # vector constructor
116
- count = data[offset, 4].unpack1('L<')
117
- offset += 4
118
-
119
- messages = []
120
- count.times do
121
- constructor = data[offset, 4].unpack1('L<')
122
- msg = Message.deserialize(data, offset, constructor)
123
- messages << msg.to_h if msg
124
- offset = schema.skip(data, offset)
125
- end
126
- [messages, offset]
127
- end
128
108
  end
129
109
  end
130
110
  end
@@ -0,0 +1,26 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'media_parts'
4
+
5
+ module MTProto
6
+ module TL
7
+ # messages.uploadMedia — hand Telegram an upload and get back the photo it now holds.
8
+ #
9
+ # The step before an album: a group is sent by naming photos Telegram already has, so
10
+ # each file is offered once on its own before the group is sent at all.
11
+ class UploadMedia
12
+ include MediaParts
13
+
14
+ CONSTRUCTOR = 0x14967978
15
+
16
+ def initialize(peer:, file:)
17
+ @peer = peer
18
+ @file = file
19
+ end
20
+
21
+ def serialize
22
+ u32_b(CONSTRUCTOR) + u32_b(0) + serialize_peer(@peer) + serialize_uploaded_photo(@file)
23
+ end
24
+ end
25
+ end
26
+ end
@@ -0,0 +1,236 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'reader'
4
+
5
+ module MTProto
6
+ module TL
7
+ module Peers
8
+ module_function
9
+
10
+ Chat = Struct.new(:id, :title, :type, :access_hash, :username, :min, keyword_init: true)
11
+
12
+ def chats_at(data, offset)
13
+ offset += 4 # vector constructor
14
+ count = data[offset, 4].unpack1('L<')
15
+ offset += 4
16
+
17
+ chats = []
18
+ count.times do
19
+ chat, offset = parse_chat(data, offset)
20
+ chats << chat if chat
21
+ end
22
+ [chats, offset]
23
+ end
24
+
25
+ def parse_chat(data, offset)
26
+ constructor = data[offset, 4].unpack1('L<')
27
+ offset += 4
28
+
29
+ case constructor
30
+ when Constructors::CHAT
31
+ parse_basic_chat(data, offset)
32
+ when Constructors::CHANNEL
33
+ parse_channel(data, offset)
34
+ when Constructors::CHAT_FORBIDDEN
35
+ parse_chat_forbidden(data, offset)
36
+ when Constructors::CHANNEL_FORBIDDEN
37
+ parse_channel_forbidden(data, offset)
38
+ when CHAT_EMPTY
39
+ id = data[offset, 8].unpack1('Q<')
40
+ offset += 8
41
+ [Chat.new(id: id, title: '(empty)', type: :chat_empty), offset]
42
+ else
43
+ raise "Unknown Chat constructor: 0x#{constructor.to_s(16).rjust(8, '0')}"
44
+ end
45
+ end
46
+
47
+ CHAT_EMPTY = 0x29562865
48
+
49
+ # chat#41cbf256 flags:#
50
+ # id:long title:string photo:ChatPhoto
51
+ # participants_count:int date:int version:int
52
+ # migrated_to:flags.6?InputChannel
53
+ # admin_rights:flags.14?ChatAdminRights
54
+ # default_banned_rights:flags.18?ChatBannedRights
55
+ def parse_basic_chat(data, offset)
56
+ flags = data[offset, 4].unpack1('L<')
57
+ offset += 4
58
+ id = data[offset, 8].unpack1('Q<')
59
+ offset += 8
60
+ title, offset = Reader.read_tl_string(data, offset)
61
+ offset = Reader.schema.skip(data, offset) # photo (ChatPhoto)
62
+ offset += 4 # participants_count
63
+ offset += 4 # date
64
+ offset += 4 # version
65
+ offset = Reader.schema.skip(data, offset) if flags.anybits?(1 << 6) # migrated_to (InputChannel)
66
+ offset = Reader.schema.skip(data, offset) if flags.anybits?(1 << 14) # admin_rights
67
+ offset = Reader.schema.skip(data, offset) if flags.anybits?(1 << 18) # default_banned_rights
68
+
69
+ [Chat.new(id: id, title: title, type: :chat), offset]
70
+ end
71
+
72
+ # channel#1c32b11c flags:# flags2:#
73
+ # id:long access_hash:flags.13?long title:string
74
+ # username:flags.6?string photo:ChatPhoto date:int
75
+ # restriction_reason:flags.9?Vector<RestrictionReason>
76
+ # admin_rights:flags.14?ChatAdminRights
77
+ # banned_rights:flags.15?ChatBannedRights
78
+ # default_banned_rights:flags.18?ChatBannedRights
79
+ # participants_count:flags.17?int
80
+ # usernames:flags2.0?Vector<Username>
81
+ # stories_max_id:flags2.4?int
82
+ # color:flags2.7?PeerColor profile_color:flags2.8?PeerColor
83
+ # emoji_status:flags2.9?EmojiStatus
84
+ # level:flags2.10?int subscription_until_date:flags2.11?int
85
+ # bot_verification_icon:flags2.13?long
86
+ # send_paid_messages_stars:flags2.14?long
87
+ # linked_monoforum_id:flags2.18?long
88
+ def parse_channel(data, offset)
89
+ flags = data[offset, 4].unpack1('L<')
90
+ offset += 4
91
+ flags2 = data[offset, 4].unpack1('L<')
92
+ offset += 4
93
+ id = data[offset, 8].unpack1('Q<')
94
+ offset += 8
95
+ access_hash = nil
96
+ if flags.anybits?(1 << 13)
97
+ access_hash = data[offset, 8].unpack1('Q<')
98
+ offset += 8
99
+ end
100
+ title, offset = Reader.read_tl_string(data, offset)
101
+ username = nil
102
+ username, offset = Reader.read_tl_string(data, offset) if flags.anybits?(1 << 6) # username
103
+ offset = Reader.schema.skip(data, offset) # photo (ChatPhoto)
104
+ offset += 4 # date
105
+ offset = Reader.schema.skip_vector(data, offset) if flags.anybits?(1 << 9) # restriction_reason
106
+ offset = Reader.schema.skip(data, offset) if flags.anybits?(1 << 14) # admin_rights
107
+ offset = Reader.schema.skip(data, offset) if flags.anybits?(1 << 15) # banned_rights
108
+ offset = Reader.schema.skip(data, offset) if flags.anybits?(1 << 18) # default_banned_rights
109
+ offset += 4 if flags.anybits?(1 << 17) # participants_count
110
+ offset = Reader.schema.skip_vector(data, offset) if flags2.anybits?(1 << 0) # usernames Vector<Username>
111
+ offset = Reader.schema.skip(data, offset) if flags2.anybits?(1 << 4) # stories_max_id (layer 227: RecentStory)
112
+ offset = Reader.schema.skip(data, offset) if flags2.anybits?(1 << 7) # color (PeerColor)
113
+ offset = Reader.schema.skip(data, offset) if flags2.anybits?(1 << 8) # profile_color (PeerColor)
114
+ offset = Reader.schema.skip(data, offset) if flags2.anybits?(1 << 9) # emoji_status
115
+ offset += 4 if flags2.anybits?(1 << 10) # level
116
+ offset += 4 if flags2.anybits?(1 << 11) # subscription_until_date
117
+ offset += 8 if flags2.anybits?(1 << 13) # bot_verification_icon (long)
118
+ offset += 8 if flags2.anybits?(1 << 14) # send_paid_messages_stars (long)
119
+ offset += 8 if flags2.anybits?(1 << 18) # linked_monoforum_id (long)
120
+
121
+ type = flags.anybits?(1 << 8) ? :supergroup : :channel
122
+ [Chat.new(id: id, title: title, type: type, access_hash: access_hash, username: username,
123
+ min: flags.anybits?(1 << 12)), offset]
124
+ end
125
+
126
+ # chatForbidden#6592a1a7 id:long title:string
127
+ def parse_chat_forbidden(data, offset)
128
+ id = data[offset, 8].unpack1('Q<')
129
+ offset += 8
130
+ title, offset = Reader.read_tl_string(data, offset)
131
+ [Chat.new(id: id, title: title, type: :chat_forbidden), offset]
132
+ end
133
+
134
+ # channelForbidden#17d493d5 flags:#
135
+ # id:long access_hash:long title:string until_date:flags.16?int
136
+ def parse_channel_forbidden(data, offset)
137
+ flags = data[offset, 4].unpack1('L<')
138
+ offset += 4
139
+ id = data[offset, 8].unpack1('Q<')
140
+ offset += 8
141
+ access_hash = data[offset, 8].unpack1('Q<')
142
+ offset += 8
143
+ title, offset = Reader.read_tl_string(data, offset)
144
+ offset += 4 if flags.anybits?(1 << 16) # until_date
145
+ [Chat.new(id: id, title: title, type: :channel_forbidden, access_hash: access_hash), offset]
146
+ end
147
+
148
+ def users_at(data, offset)
149
+ offset += 4 # vector constructor
150
+ count = data[offset, 4].unpack1('L<')
151
+ offset += 4
152
+
153
+ users = []
154
+ count.times do
155
+ user, offset = parse_user(data, offset)
156
+ users << user if user
157
+ end
158
+ [users, offset]
159
+ end
160
+
161
+ USER_EMPTY = 0xd3bc4b7a
162
+
163
+ # user#31774388 flags:# flags2:#
164
+ # id:long access_hash:flags.0?long
165
+ # first_name:flags.1?string last_name:flags.2?string
166
+ # username:flags.3?string phone:flags.4?string
167
+ # photo:flags.5?UserProfilePhoto status:flags.6?UserStatus
168
+ # bot_info_version:flags.14?int
169
+ # restriction_reason:flags.18?Vector<RestrictionReason>
170
+ # bot_inline_placeholder:flags.19?string
171
+ # lang_code:flags.22?string
172
+ # emoji_status:flags.30?EmojiStatus
173
+ # usernames:flags2.0?Vector<Username>
174
+ # stories_max_id:flags2.5?int
175
+ # color:flags2.8?PeerColor profile_color:flags2.9?PeerColor
176
+ # bot_active_users:flags2.12?int
177
+ # bot_verification_icon:flags2.14?long
178
+ # send_paid_messages_stars:flags2.15?long
179
+ def parse_user(data, offset)
180
+ constructor = data[offset, 4].unpack1('L<')
181
+ offset += 4
182
+
183
+ if constructor == USER_EMPTY
184
+ offset += 8 # id (long)
185
+ return [nil, offset]
186
+ end
187
+
188
+ raise "Unknown User constructor: 0x#{constructor.to_s(16)}" unless constructor == Constructors::USER
189
+
190
+ flags = data[offset, 4].unpack1('L<')
191
+ offset += 4
192
+ flags2 = data[offset, 4].unpack1('L<')
193
+ offset += 4
194
+ id = data[offset, 8].unpack1('Q<')
195
+ offset += 8
196
+ carries_hash = flags.anybits?(1 << 0)
197
+ if carries_hash
198
+ access_hash = data[offset, 8].unpack1('Q<')
199
+ offset += 8
200
+ end
201
+
202
+ first_name = nil
203
+ first_name, offset = Reader.read_tl_string(data, offset) if flags.anybits?(1 << 1)
204
+
205
+ last_name = nil
206
+ last_name, offset = Reader.read_tl_string(data, offset) if flags.anybits?(1 << 2)
207
+
208
+ username = nil
209
+ username, offset = Reader.read_tl_string(data, offset) if flags.anybits?(1 << 3) # username
210
+
211
+ _, offset = Reader.read_tl_string(data, offset) if flags.anybits?(1 << 4) # phone
212
+ offset = Reader.schema.skip(data, offset) if flags.anybits?(1 << 5) # photo
213
+ offset = Reader.schema.skip(data, offset) if flags.anybits?(1 << 6) # status
214
+ offset += 4 if flags.anybits?(1 << 14) # bot_info_version
215
+ offset = Reader.schema.skip_vector(data, offset) if flags.anybits?(1 << 18) # restriction_reason
216
+ _, offset = Reader.read_tl_string(data, offset) if flags.anybits?(1 << 19) # bot_inline_placeholder
217
+ _, offset = Reader.read_tl_string(data, offset) if flags.anybits?(1 << 22) # lang_code
218
+ offset = Reader.schema.skip(data, offset) if flags.anybits?(1 << 30) # emoji_status
219
+ offset = Reader.schema.skip_vector(data, offset) if flags2.anybits?(1 << 0) # usernames
220
+ offset = Reader.schema.skip(data, offset) if flags2.anybits?(1 << 5) # stories_max_id (layer 227: RecentStory)
221
+ offset = Reader.schema.skip(data, offset) if flags2.anybits?(1 << 8) # color
222
+ offset = Reader.schema.skip(data, offset) if flags2.anybits?(1 << 9) # profile_color
223
+ offset += 4 if flags2.anybits?(1 << 12) # bot_active_users
224
+ offset += 8 if flags2.anybits?(1 << 14) # bot_verification_icon
225
+ offset += 8 if flags2.anybits?(1 << 15) # send_paid_messages_stars
226
+
227
+ user = { id: id, first_name: first_name, last_name: last_name, username: username,
228
+ is_bot: flags.anybits?(1 << 14), min: flags.anybits?(1 << 20) }
229
+ user[:access_hash] = access_hash if carries_hash
230
+ [user, offset]
231
+ end
232
+ private_class_method :parse_chat, :parse_basic_chat, :parse_channel,
233
+ :parse_chat_forbidden, :parse_channel_forbidden, :parse_user
234
+ end
235
+ end
236
+ end
@@ -18,6 +18,9 @@ module MTProto
18
18
  MEDIA_DOCUMENT = 0x52d8ccd9
19
19
  DOCUMENT = 0x8fd4c4d8
20
20
  PHOTO = 0xfb197a65
21
+ PHOTO_SIZE = 0x75c78e60
22
+ PHOTO_CACHED_SIZE = 0x021e1ad6
23
+ PHOTO_SIZE_PROGRESSIVE = 0xfa3efb95
21
24
  DOCUMENT_ATTRIBUTE_AUDIO = 0x9852f9c6
22
25
  DOCUMENT_ATTRIBUTE_FILENAME = 0x15590068
23
26
 
@@ -158,13 +161,37 @@ module MTProto
158
161
  id = data[offset, 8].unpack1('q<')
159
162
  access_hash = data[offset + 8, 8].unpack1('q<')
160
163
  offset += 16
161
- file_reference, = read_tl_bytes(data, offset)
164
+ file_reference, offset = read_tl_bytes(data, offset)
162
165
 
163
- { id: id, access_hash: access_hash, file_reference: file_reference }
166
+ { id: id, access_hash: access_hash, file_reference: file_reference,
167
+ sizes: photo_sizes(data, offset + 4) } # +4: date, which precedes the sizes
164
168
  rescue StandardError
165
169
  nil
166
170
  end
167
171
 
172
+ # Every size Telegram holds this photo in, by the name upload.getFile asks for it.
173
+ #
174
+ # A photo, unlike a document, is not fetched whole: the location names which size is
175
+ # wanted, and naming none is answered as if the reference had gone stale — which is
176
+ # what makes a missing size look like an expired one.
177
+ #
178
+ # Only the sizes with a width can be fetched: the other kinds carry their own bytes
179
+ # inline (a blur, an outline) and have nothing to ask for.
180
+ SIZES_WITH_WIDTH = [PHOTO_SIZE, PHOTO_CACHED_SIZE, PHOTO_SIZE_PROGRESSIVE].freeze
181
+
182
+ def photo_sizes(data, offset)
183
+ found = []
184
+ schema.skip_vector(data, offset) do |bytes, at|
185
+ constructor = bytes[at, 4].unpack1('L<')
186
+ name, after = read_tl_string(bytes, at + 4)
187
+ found << { type: name, width: bytes[after, 4].unpack1('l<') } if SIZES_WITH_WIDTH.include?(constructor)
188
+ schema.skip(bytes, at)
189
+ end
190
+ found
191
+ rescue StandardError
192
+ []
193
+ end
194
+
168
195
  # Vector<DocumentAttribute>: pull voice/duration from documentAttributeAudio
169
196
  # and the name from documentAttributeFilename; advance over the rest via
170
197
  # schema so an unknown attribute never throws off the read.