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.



74
75
76
77
78
79
# File 'lib/mtproto/tl/reader.rb', line 74

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.



85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
# File 'lib/mtproto/tl/reader.rb', line 85

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<DocumentAttribute>: 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.



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

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.



62
63
64
65
66
67
68
69
70
71
# File 'lib/mtproto/tl/reader.rb', line 62

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.



121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
# File 'lib/mtproto/tl/reader.rb', line 121

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: ctor, flags, reply_to_msg_id:flags.4?int (first payload field). 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). => [reply_to_msg_id, is_reply]



48
49
50
51
52
53
54
55
56
57
58
59
# File 'lib/mtproto/tl/reader.rb', line 48

def parse_reply_to(data, offset)
  return [nil, false] 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 = flags.anybits?(1 << 4) ? data[io, 4].unpack1('l<') : nil
  [msg_id, forum_topic ? flags.anybits?(1 << 1) : true]
rescue StandardError
  [nil, false]
end

.read_tl_bytes(data, offset) ⇒ Object



172
173
174
175
176
177
178
179
180
181
182
183
184
185
# File 'lib/mtproto/tl/reader.rb', line 172

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



167
168
169
170
# File 'lib/mtproto/tl/reader.rb', line 167

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