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.
- checksums.yaml +4 -4
- data/docs/test_architecture_level1.md +23 -107
- data/lib/mtproto/auth_key_generator.rb +3 -1
- data/lib/mtproto/client/api/resolve_username.rb +18 -0
- data/lib/mtproto/client/api/send_message.rb +5 -2
- data/lib/mtproto/client/api/set_typing.rb +4 -4
- data/lib/mtproto/client/api/stream_draft.rb +13 -0
- data/lib/mtproto/client/api.rb +2 -0
- data/lib/mtproto/crypto/dh_key_exchange.rb +2 -1
- data/lib/mtproto/draft_stream.rb +52 -0
- data/lib/mtproto/tl/constructors.rb +1 -0
- data/lib/mtproto/tl/objects/authorization.rb +4 -3
- data/lib/mtproto/tl/objects/channel_difference.rb +13 -32
- data/lib/mtproto/tl/objects/contacts.rb +7 -89
- data/lib/mtproto/tl/objects/dialogs.rb +14 -352
- data/lib/mtproto/tl/objects/edit_message.rb +1 -1
- data/lib/mtproto/tl/objects/media_parts.rb +68 -0
- data/lib/mtproto/tl/objects/message.rb +14 -0
- data/lib/mtproto/tl/objects/message_service.rb +40 -0
- data/lib/mtproto/tl/objects/messages.rb +7 -32
- data/lib/mtproto/tl/objects/reply_keyboard_force_reply.rb +46 -0
- data/lib/mtproto/tl/objects/resolved_peer.rb +29 -0
- data/lib/mtproto/tl/objects/send_message.rb +1 -1
- data/lib/mtproto/tl/objects/send_message_text_draft_action.rb +52 -0
- data/lib/mtproto/tl/objects/send_multi_media.rb +52 -0
- data/lib/mtproto/tl/objects/update.rb +3 -35
- data/lib/mtproto/tl/objects/updates.rb +4 -6
- data/lib/mtproto/tl/objects/updates_difference.rb +9 -29
- data/lib/mtproto/tl/objects/upload_media.rb +26 -0
- data/lib/mtproto/tl/peers.rb +236 -0
- data/lib/mtproto/tl/reader.rb +29 -2
- data/lib/mtproto/updates/event_builder.rb +20 -32
- data/lib/mtproto/updates/parser.rb +9 -8
- data/lib/mtproto/updates/state_machine.rb +7 -7
- data/lib/mtproto/version.rb +1 -1
- data/lib/mtproto.rb +1 -0
- 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
|
|
@@ -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
|
-
|
|
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 =
|
|
41
|
-
chats, =
|
|
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
|
|
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 '../
|
|
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
|
-
|
|
72
|
-
|
|
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 =
|
|
75
|
-
users, 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
|
data/lib/mtproto/tl/reader.rb
CHANGED
|
@@ -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.
|