Module: MTProto::TL::Reader

Defined in:
lib/mtproto/tl/reader.rb

Overview

Shared read-only TL primitives — peer/string/bytes/media/document reads and the schema handle — reused by the message parser (TL::Message) and the other update parsers. Each method only READS; advancing past a whole object is the caller's job via Schema#skip, so any unmodelled tail is sized by the schema.

Constant Summary collapse

SCHEMA_PATH =
File.expand_path('../../../data/tl-schema.json', __dir__)
MESSAGE_REPLY_HEADER =
0x1b97dd66
MEDIA_PHOTO =
0xe216eb63
MEDIA_DOCUMENT =
0x52d8ccd9
DOCUMENT =
0x8fd4c4d8
PHOTO =
0xfb197a65
DOCUMENT_ATTRIBUTE_AUDIO =
0x9852f9c6
DOCUMENT_ATTRIBUTE_FILENAME =
0x15590068

Class Method Summary collapse

Class Method Details

.document_kind(flags) ⇒ Object

messageMediaDocument flags: video flags.6 / round flags.7 / voice flags.8.



101
102
103
104
105
106
# File 'lib/mtproto/tl/reader.rb', line 101

def document_kind(flags)
  return :video if flags.anybits?(1 << 6) || flags.anybits?(1 << 7)
  return :audio if flags.anybits?(1 << 8)

  :document
end

.parse_document(data, offset) ⇒ Object

When the media is messageMediaDocument, pull the document's downloadable fields (file location + audio metadata); nil for any other media. The thumb vectors before dc_id are sized by the schema, so this reaches dc_id and the attribute list correctly regardless of thumbnails.



112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
# File 'lib/mtproto/tl/reader.rb', line 112

def parse_document(data, offset)
  return unless data[offset, 4].unpack1('L<') == MEDIA_DOCUMENT

  media_flags = data[offset + 4, 4].unpack1('L<')
  return unless media_flags.anybits?(1 << 0) # document present

  offset += 8
  return unless data[offset, 4].unpack1('L<') == DOCUMENT

  offset += 4
  flags = data[offset, 4].unpack1('L<')
  offset += 4
  id = data[offset, 8].unpack1('q<')
  access_hash = data[offset + 8, 8].unpack1('q<')
  offset += 16
  file_reference, offset = read_tl_bytes(data, offset)
  offset += 4 # date
  mime_type, offset = read_tl_string(data, offset)
  size = data[offset, 8].unpack1('q<')
  offset += 8
  offset = schema.skip_vector(data, offset) if flags.anybits?(1 << 0) # thumbs
  offset = schema.skip_vector(data, offset) if flags.anybits?(1 << 1) # video_thumbs
  dc_id = data[offset, 4].unpack1('l<')
  offset += 4
  voice, duration, file_name = parse_document_attributes(data, offset)

  { id: id, access_hash: access_hash, file_reference: file_reference, dc_id: dc_id,
    mime_type: mime_type, size: size, voice: voice, duration: duration, file_name: file_name }
rescue StandardError
  nil
end

.parse_document_attributes(data, offset) ⇒ Object

Vector: pull voice/duration from documentAttributeAudio and the name from documentAttributeFilename; advance over the rest via schema so an unknown attribute never throws off the read.



171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
# File 'lib/mtproto/tl/reader.rb', line 171

def parse_document_attributes(data, offset)
  offset += 4 # vector constructor
  count = data[offset, 4].unpack1('L<')
  offset += 4

  voice = false
  duration = nil
  file_name = nil
  count.times do
    ctor = data[offset, 4].unpack1('L<')
    case ctor
    when DOCUMENT_ATTRIBUTE_AUDIO
      aflags = data[offset + 4, 4].unpack1('L<')
      voice ||= aflags.anybits?(1 << 10)
      duration = data[offset + 8, 4].unpack1('l<')
    when DOCUMENT_ATTRIBUTE_FILENAME
      file_name, = read_tl_string(data, offset + 4)
    end
    offset = schema.skip(data, offset)
  end
  [voice, duration, file_name]
end

.parse_media(data, offset) ⇒ Object

Classify message media into :photo / :video / :audio / :document, or nil.



89
90
91
92
93
94
95
96
97
98
# File 'lib/mtproto/tl/reader.rb', line 89

def parse_media(data, offset)
  case data[offset, 4].unpack1('L<')
  when MEDIA_PHOTO
    :photo
  when MEDIA_DOCUMENT
    document_kind(data[offset + 4, 4].unpack1('L<'))
  end
rescue StandardError
  nil
end

.parse_peer(data, offset) ⇒ Object



28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
# File 'lib/mtproto/tl/reader.rb', line 28

def parse_peer(data, offset)
  constructor = data[offset, 4].unpack1('L<')
  offset += 4

  case constructor
  when Constructors::PEER_USER
    [:user, data[offset, 8].unpack1('Q<'), offset + 8]
  when Constructors::PEER_CHAT
    [:chat, data[offset, 8].unpack1('Q<'), offset + 8]
  when Constructors::PEER_CHANNEL
    [:channel, data[offset, 8].unpack1('Q<'), offset + 8]
  else
    raise "Unknown peer constructor: 0x#{constructor.to_s(16)}"
  end
end

.parse_photo(data, offset) ⇒ Object

When the media is messageMediaPhoto, pull the photo's input reference (id/access_hash/file_reference) so it can be re-sent via inputMediaPhoto — e.g. re-posting a guest-mention's photo, where forwarding is not allowed. has_stickers is a true-flag (no body), so id starts right after photo's flags.



148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
# File 'lib/mtproto/tl/reader.rb', line 148

def parse_photo(data, offset)
  return unless data[offset, 4].unpack1('L<') == MEDIA_PHOTO

  media_flags = data[offset + 4, 4].unpack1('L<')
  return unless media_flags.anybits?(1 << 0) # photo present

  offset += 8 # messageMediaPhoto ctor + flags
  return unless data[offset, 4].unpack1('L<') == PHOTO

  offset += 8 # photo ctor + flags
  id = data[offset, 8].unpack1('q<')
  access_hash = data[offset + 8, 8].unpack1('q<')
  offset += 16
  file_reference, = read_tl_bytes(data, offset)

  { id: id, access_hash: access_hash, file_reference: file_reference }
rescue StandardError
  nil
end

.parse_reply_to(data, offset) ⇒ Object

messageReplyHeader#1b97dd66: flags, then (in wire order) reply_to_msg_id flags.4?int, reply_to_peer_id flags.0?Peer, reply_from flags.5?MessageFwdHeader, reply_media flags.8?MessageMedia, reply_to_top_id flags.1?int, quote_text flags.6?string, quote_entities flags.7?Vector, quote_offset flags.10?int. A manual quote sets flags.9 and carries quote_text/offset — the fragment the user selected out of the replied-to message. The quote fields sit past the variable-length reply_from/reply_media, so we walk the whole header to reach them (reply_from/reply_media are sized via the schema). A plain message in a forum topic carries this header (forum_topic flags.3) but is a reply only when it targets a message in the topic (reply_to_top_id flags.1). Returns a descriptor hash, or nil for a non-messageReplyHeader (e.g. a story reply).



55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
# File 'lib/mtproto/tl/reader.rb', line 55

def parse_reply_to(data, offset)
  return unless data[offset, 4].unpack1('L<') == MESSAGE_REPLY_HEADER

  io = offset + 4
  flags = data[io, 4].unpack1('L<')
  io += 4
  forum_topic = flags.anybits?(1 << 3)

  msg_id = nil
  if flags.anybits?(1 << 4) # reply_to_msg_id
    msg_id = data[io, 4].unpack1('l<')
    io += 4
  end

  peer_type = peer_id = nil
  peer_type, peer_id, io = parse_peer(data, io) if flags.anybits?(1 << 0) # reply_to_peer_id

  io = schema.skip(data, io) if flags.anybits?(1 << 5) # reply_from
  io = schema.skip(data, io) if flags.anybits?(1 << 8) # reply_media
  io += 4 if flags.anybits?(1 << 1) # reply_to_top_id

  quote_text = nil
  quote_text, io = read_tl_string(data, io) if flags.anybits?(1 << 6)
  io = schema.skip_vector(data, io) if flags.anybits?(1 << 7) # quote_entities
  quote_offset = flags.anybits?(1 << 10) ? data[io, 4].unpack1('l<') : nil

  { msg_id: msg_id, is_reply: forum_topic ? flags.anybits?(1 << 1) : true,
    is_quote: flags.anybits?(1 << 9), quote_text: quote_text, quote_offset: quote_offset,
    peer_type: peer_type, peer_id: peer_id }
rescue StandardError
  nil
end

.read_tl_bytes(data, offset) ⇒ Object



199
200
201
202
203
204
205
206
207
208
209
210
211
212
# File 'lib/mtproto/tl/reader.rb', line 199

def read_tl_bytes(data, offset)
  first_byte = data.getbyte(offset)
  if first_byte == 254
    len = data.getbyte(offset + 1) |
          (data.getbyte(offset + 2) << 8) |
          (data.getbyte(offset + 3) << 16)
    total = 4 + len
  else
    len = first_byte
    total = 1 + len
  end
  padding = (4 - (total % 4)) % 4
  [data[offset + (total - len), len], offset + total + padding]
end

.read_tl_string(data, offset) ⇒ Object



194
195
196
197
# File 'lib/mtproto/tl/reader.rb', line 194

def read_tl_string(data, offset)
  bytes, offset = read_tl_bytes(data, offset)
  [bytes.force_encoding('UTF-8'), offset]
end

.schemaObject



24
25
26
# File 'lib/mtproto/tl/reader.rb', line 24

def schema
  @schema ||= Schema.new(SCHEMA_PATH)
end