mtproto 0.0.15 → 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.
- checksums.yaml +4 -4
- data/data/tl-schema.json +47158 -42684
- data/lib/mtproto/auth_key_generator.rb +2 -2
- data/lib/mtproto/client/api/export_authorization.rb +17 -0
- data/lib/mtproto/client/api/import_authorization.rb +23 -0
- data/lib/mtproto/client/api.rb +2 -0
- data/lib/mtproto/client.rb +1 -1
- data/lib/mtproto/file_downloader.rb +122 -0
- data/lib/mtproto/tl/constructor_names.rb +314 -109
- data/lib/mtproto/tl/constructors.rb +16 -8
- data/lib/mtproto/tl/objects/bot_command_scope.rb +81 -0
- data/lib/mtproto/tl/objects/channels_join_channel.rb +1 -1
- data/lib/mtproto/tl/objects/channels_update_username.rb +42 -0
- data/lib/mtproto/tl/objects/contacts.rb +1 -1
- data/lib/mtproto/tl/objects/dialogs.rb +15 -10
- data/lib/mtproto/tl/objects/export_authorization.rb +17 -0
- data/lib/mtproto/tl/objects/exported_authorization.rb +32 -0
- data/lib/mtproto/tl/objects/forward_messages.rb +5 -2
- data/lib/mtproto/tl/objects/get_bot_callback_answer.rb +67 -0
- data/lib/mtproto/tl/objects/get_bot_commands.rb +46 -0
- data/lib/mtproto/tl/objects/get_file.rb +10 -2
- data/lib/mtproto/tl/objects/import_authorization.rb +38 -0
- data/lib/mtproto/tl/objects/keyboard_button_callback.rb +50 -0
- data/lib/mtproto/tl/objects/message.rb +97 -21
- data/lib/mtproto/tl/objects/messages.rb +7 -74
- data/lib/mtproto/tl/objects/reply_inline_markup.rb +30 -0
- data/lib/mtproto/tl/objects/reset_bot_commands.rb +45 -0
- data/lib/mtproto/tl/objects/send_media.rb +76 -4
- data/lib/mtproto/tl/objects/send_message.rb +4 -2
- data/lib/mtproto/tl/objects/send_message_action.rb +48 -0
- data/lib/mtproto/tl/objects/set_bot_callback_answer.rb +54 -0
- data/lib/mtproto/tl/objects/set_bot_commands.rb +6 -3
- data/lib/mtproto/tl/objects/set_bot_guest_chat_result.rb +84 -0
- data/lib/mtproto/tl/objects/set_typing.rb +49 -0
- data/lib/mtproto/tl/objects/update_status.rb +24 -0
- data/lib/mtproto/tl/objects/updates.rb +117 -0
- data/lib/mtproto/tl/objects/updates_difference.rb +16 -119
- data/lib/mtproto/tl/reader.rb +188 -0
- data/lib/mtproto/transport/abridged_packet_codec.rb +5 -1
- data/lib/mtproto/unencrypted_message.rb +39 -0
- data/lib/mtproto/version.rb +1 -1
- data/lib/mtproto.rb +4 -1
- data/scripts/gen_constructor_names.rb +72 -0
- data/scripts/tl_to_json.rb +72 -0
- data/scripts/verify_ids.rb +33 -0
- metadata +25 -2
- data/lib/mtproto/message/message.rb +0 -85
|
@@ -1,37 +1,113 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
+
require_relative '../reader'
|
|
4
|
+
|
|
3
5
|
module MTProto
|
|
4
6
|
module TL
|
|
7
|
+
# The high-level chat-message TL structure `message#7600b9d3` (text/media/
|
|
8
|
+
# fwd_from/reply/sender/...). Owns the field walk for that one constructor; the
|
|
9
|
+
# low-level TL reads it needs (peer/string/bytes/media/document, plus the
|
|
10
|
+
# shared schema) live in Reader and are reused here.
|
|
11
|
+
#
|
|
12
|
+
# Reading is decoupled from alignment: `.deserialize` only READS the fields we
|
|
13
|
+
# surface and returns a Message (or nil for a non-`message` constructor — e.g.
|
|
14
|
+
# messageService/messageEmpty). Advancing past the whole object is the caller's
|
|
15
|
+
# job via `Schema#skip`, so any unmodelled tail never drops the message. This is
|
|
16
|
+
# the single parser for all three inbound paths (live `Updates` push,
|
|
17
|
+
# `updates.getDifference`, and `messages.getHistory`).
|
|
5
18
|
class Message
|
|
6
|
-
|
|
19
|
+
MESSAGE = Constructors::MESSAGE
|
|
20
|
+
MESSAGE_FWD_HEADER = 0x4e4df4bb
|
|
7
21
|
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
22
|
+
FIELDS = %i[id date message peer_type peer_id from_type from_id out media
|
|
23
|
+
document photo is_reply reply_to_msg_id fwd_from_type fwd_from_id].freeze
|
|
24
|
+
|
|
25
|
+
attr_reader(*FIELDS)
|
|
26
|
+
|
|
27
|
+
# `text` is the message body; a get_history-friendly alias for `message`.
|
|
28
|
+
alias text message
|
|
13
29
|
|
|
14
|
-
def
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
body_length = [@body.bytesize].pack('L<')
|
|
30
|
+
def initialize(attrs)
|
|
31
|
+
FIELDS.each { |f| instance_variable_set("@#{f}", attrs[f]) }
|
|
32
|
+
end
|
|
18
33
|
|
|
19
|
-
|
|
34
|
+
# The legacy hash shape the Updates / UpdatesDifference consumers expect.
|
|
35
|
+
def to_h
|
|
36
|
+
FIELDS.each_with_object({}) { |f, h| h[f] = public_send(f) }
|
|
20
37
|
end
|
|
21
38
|
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
39
|
+
class << self
|
|
40
|
+
def deserialize(data, offset, constructor)
|
|
41
|
+
return unless constructor == MESSAGE
|
|
42
|
+
|
|
43
|
+
offset += 4 # constructor
|
|
44
|
+
flags = data[offset, 4].unpack1('L<')
|
|
45
|
+
offset += 4
|
|
46
|
+
flags2 = data[offset, 4].unpack1('L<')
|
|
47
|
+
offset += 4
|
|
48
|
+
id = data[offset, 4].unpack1('l<')
|
|
49
|
+
offset += 4
|
|
50
|
+
|
|
51
|
+
from_type = from_id = nil
|
|
52
|
+
from_type, from_id, offset = x.parse_peer(data, offset) if flags.anybits?(1 << 8) # from_id (Peer)
|
|
53
|
+
offset += 4 if flags.anybits?(1 << 29) # from_boosts_applied
|
|
54
|
+
_, offset = x.read_tl_string(data, offset) if flags2.anybits?(1 << 12) # from_rank (layer 227)
|
|
55
|
+
peer_type, peer_id, offset = x.parse_peer(data, offset)
|
|
56
|
+
offset = x.schema.skip(data, offset) if flags.anybits?(1 << 28) # saved_peer_id
|
|
57
|
+
fwd_from_type = fwd_from_id = nil
|
|
58
|
+
if flags.anybits?(1 << 2) # fwd_from
|
|
59
|
+
fwd_from_type, fwd_from_id = parse_fwd_origin(data, offset)
|
|
60
|
+
offset = x.schema.skip(data, offset)
|
|
61
|
+
end
|
|
62
|
+
offset += 8 if flags.anybits?(1 << 11) # via_bot_id
|
|
63
|
+
offset += 8 if flags2.anybits?(1 << 0) # via_business_bot_id
|
|
64
|
+
offset = x.schema.skip(data, offset) if flags2.anybits?(1 << 19) # guestchat_via_from (layer 227, Peer)
|
|
65
|
+
|
|
66
|
+
is_reply = false
|
|
67
|
+
reply_to_msg_id = nil
|
|
68
|
+
if flags.anybits?(1 << 3) # reply_to
|
|
69
|
+
reply_to_msg_id, is_reply = x.parse_reply_to(data, offset)
|
|
70
|
+
offset = x.schema.skip(data, offset)
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
date = data[offset, 4].unpack1('L<')
|
|
74
|
+
offset += 4
|
|
75
|
+
message_text, offset = x.read_tl_string(data, offset)
|
|
76
|
+
has_media = flags.anybits?(1 << 9)
|
|
77
|
+
media = has_media ? x.parse_media(data, offset) : nil
|
|
78
|
+
document = has_media ? x.parse_document(data, offset) : nil
|
|
79
|
+
photo = has_media ? x.parse_photo(data, offset) : nil
|
|
80
|
+
|
|
81
|
+
new(id: id, date: date, message: message_text, peer_type: peer_type, peer_id: peer_id,
|
|
82
|
+
from_type: from_type, from_id: from_id, out: flags.anybits?(1 << 1),
|
|
83
|
+
media: media, document: document, photo: photo, is_reply: is_reply,
|
|
84
|
+
reply_to_msg_id: reply_to_msg_id, fwd_from_type: fwd_from_type, fwd_from_id: fwd_from_id)
|
|
27
85
|
end
|
|
28
86
|
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
87
|
+
private
|
|
88
|
+
|
|
89
|
+
# The low-level TL primitives, shared from Reader.
|
|
90
|
+
def x
|
|
91
|
+
Reader
|
|
92
|
+
end
|
|
33
93
|
|
|
34
|
-
|
|
94
|
+
# messageFwdHeader#4e4df4bb flags:# imported:flags.7?true from_id:flags.0?Peer
|
|
95
|
+
# ... — from_id is the forward origin (a peerChannel for a forwarded channel
|
|
96
|
+
# post). imported (flags.7) is a flag-only bool, so from_id sits right after
|
|
97
|
+
# the flags. => [type, id] or [nil, nil].
|
|
98
|
+
def parse_fwd_origin(data, offset)
|
|
99
|
+
return [nil, nil] unless data[offset, 4].unpack1('L<') == MESSAGE_FWD_HEADER
|
|
100
|
+
|
|
101
|
+
io = offset + 4
|
|
102
|
+
flags = data[io, 4].unpack1('L<')
|
|
103
|
+
io += 4
|
|
104
|
+
return [nil, nil] unless flags.anybits?(1 << 0) # from_id present
|
|
105
|
+
|
|
106
|
+
type, id, = x.parse_peer(data, io)
|
|
107
|
+
[type, id]
|
|
108
|
+
rescue StandardError
|
|
109
|
+
[nil, nil]
|
|
110
|
+
end
|
|
35
111
|
end
|
|
36
112
|
end
|
|
37
113
|
end
|
|
@@ -1,20 +1,17 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
require_relative '../schema'
|
|
4
|
+
require_relative 'message'
|
|
4
5
|
|
|
5
6
|
module MTProto
|
|
6
7
|
module TL
|
|
8
|
+
# A messages.messages / messagesSlice / channelMessages container (e.g. the
|
|
9
|
+
# messages.getHistory reply). Its messages are TL::Message instances.
|
|
7
10
|
class Messages
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
MESSAGES_MESSAGES = 0x8c718e87
|
|
11
|
-
MESSAGES_SLICE = 0x762b263d
|
|
11
|
+
MESSAGES_MESSAGES = 0x1d73e7ea
|
|
12
|
+
MESSAGES_SLICE = 0x5f206716
|
|
12
13
|
MESSAGES_CHANNEL = 0xc776ba4e
|
|
13
14
|
|
|
14
|
-
MESSAGE = Constructors::MESSAGE
|
|
15
|
-
MESSAGE_SERVICE = 0x7a800e0a
|
|
16
|
-
MESSAGE_EMPTY = 0x90a6ca84
|
|
17
|
-
|
|
18
15
|
VALID_CONSTRUCTORS = [MESSAGES_MESSAGES, MESSAGES_SLICE, MESSAGES_CHANNEL].freeze
|
|
19
16
|
|
|
20
17
|
attr_reader :messages, :count
|
|
@@ -86,76 +83,12 @@ module MTProto
|
|
|
86
83
|
messages = []
|
|
87
84
|
count.times do
|
|
88
85
|
constructor = data[offset, 4].unpack1('L<')
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
msg, = parse_message(data, offset)
|
|
92
|
-
messages << msg if msg
|
|
93
|
-
end
|
|
94
|
-
|
|
86
|
+
msg = Message.deserialize(data, offset, constructor)
|
|
87
|
+
messages << msg if msg
|
|
95
88
|
offset = schema.skip(data, offset)
|
|
96
89
|
end
|
|
97
90
|
[messages, offset]
|
|
98
91
|
end
|
|
99
|
-
|
|
100
|
-
# message#9815cec8 flags:# flags2:# id:int
|
|
101
|
-
# from_id:flags.8?Peer ... date:int message:string ...
|
|
102
|
-
def parse_message(data, offset)
|
|
103
|
-
offset += 4 # constructor
|
|
104
|
-
flags = data[offset, 4].unpack1('L<')
|
|
105
|
-
offset += 4
|
|
106
|
-
flags2 = data[offset, 4].unpack1('L<')
|
|
107
|
-
offset += 4
|
|
108
|
-
id = data[offset, 4].unpack1('l<')
|
|
109
|
-
offset += 4
|
|
110
|
-
|
|
111
|
-
from_id = nil
|
|
112
|
-
_, from_id, offset = parse_peer(data, offset) if flags.anybits?(1 << 8)
|
|
113
|
-
offset += 4 if flags.anybits?(1 << 29) # from_boosts_applied
|
|
114
|
-
_, _, offset = parse_peer(data, offset) # peer_id (skip)
|
|
115
|
-
_, _, offset = parse_peer(data, offset) if flags.anybits?(1 << 28) # saved_peer_id
|
|
116
|
-
offset = schema.skip(data, offset) if flags.anybits?(1 << 2) # fwd_from
|
|
117
|
-
offset += 8 if flags.anybits?(1 << 11) # via_bot_id
|
|
118
|
-
offset += 8 if flags2.anybits?(1 << 0) # via_business_bot_id
|
|
119
|
-
offset = schema.skip(data, offset) if flags.anybits?(1 << 3) # reply_to
|
|
120
|
-
|
|
121
|
-
date = data[offset, 4].unpack1('L<')
|
|
122
|
-
offset += 4
|
|
123
|
-
|
|
124
|
-
text, offset = read_tl_string(data, offset)
|
|
125
|
-
|
|
126
|
-
msg = Message.new(id: id, from_id: from_id, date: date, text: text)
|
|
127
|
-
[msg, offset]
|
|
128
|
-
end
|
|
129
|
-
|
|
130
|
-
def parse_peer(data, offset)
|
|
131
|
-
constructor = data[offset, 4].unpack1('L<')
|
|
132
|
-
offset += 4
|
|
133
|
-
case constructor
|
|
134
|
-
when Constructors::PEER_USER
|
|
135
|
-
[:user, data[offset, 8].unpack1('Q<'), offset + 8]
|
|
136
|
-
when Constructors::PEER_CHAT
|
|
137
|
-
[:chat, data[offset, 8].unpack1('Q<'), offset + 8]
|
|
138
|
-
when Constructors::PEER_CHANNEL
|
|
139
|
-
[:channel, data[offset, 8].unpack1('Q<'), offset + 8]
|
|
140
|
-
else
|
|
141
|
-
raise "Unknown peer constructor: 0x#{constructor.to_s(16)}"
|
|
142
|
-
end
|
|
143
|
-
end
|
|
144
|
-
|
|
145
|
-
def read_tl_string(data, offset)
|
|
146
|
-
first_byte = data.getbyte(offset)
|
|
147
|
-
if first_byte == 254
|
|
148
|
-
len = data.getbyte(offset + 1) |
|
|
149
|
-
(data.getbyte(offset + 2) << 8) |
|
|
150
|
-
(data.getbyte(offset + 3) << 16)
|
|
151
|
-
total = 4 + len
|
|
152
|
-
else
|
|
153
|
-
len = first_byte
|
|
154
|
-
total = 1 + len
|
|
155
|
-
end
|
|
156
|
-
padding = (4 - (total % 4)) % 4
|
|
157
|
-
[data[offset + (total - len), len].force_encoding('UTF-8'), offset + total + padding]
|
|
158
|
-
end
|
|
159
92
|
end
|
|
160
93
|
end
|
|
161
94
|
end
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module MTProto
|
|
4
|
+
module TL
|
|
5
|
+
class ReplyInlineMarkup
|
|
6
|
+
include Binary
|
|
7
|
+
|
|
8
|
+
CONSTRUCTOR = 0x48a30254
|
|
9
|
+
KEYBOARD_BUTTON_ROW = 0x77608b83
|
|
10
|
+
|
|
11
|
+
# rows: array of rows; each row is an array of button objects (each responds to #serialize).
|
|
12
|
+
def initialize(rows:)
|
|
13
|
+
@rows = rows
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
def serialize
|
|
17
|
+
result = u32_b(CONSTRUCTOR)
|
|
18
|
+
result += u32_b(Constructors::VECTOR)
|
|
19
|
+
result += u32_b(@rows.size)
|
|
20
|
+
@rows.each do |row|
|
|
21
|
+
result += u32_b(KEYBOARD_BUTTON_ROW)
|
|
22
|
+
result += u32_b(Constructors::VECTOR)
|
|
23
|
+
result += u32_b(row.size)
|
|
24
|
+
row.each { |button| result += button.serialize }
|
|
25
|
+
end
|
|
26
|
+
result
|
|
27
|
+
end
|
|
28
|
+
end
|
|
29
|
+
end
|
|
30
|
+
end
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative 'bot_command_scope'
|
|
4
|
+
|
|
5
|
+
module MTProto
|
|
6
|
+
module TL
|
|
7
|
+
# bots.resetBotCommands — clear the bot's commands for a given scope/language.
|
|
8
|
+
# Returns Bool.
|
|
9
|
+
class ResetBotCommands
|
|
10
|
+
include Binary
|
|
11
|
+
|
|
12
|
+
CONSTRUCTOR = 0x3d8de0f9
|
|
13
|
+
|
|
14
|
+
# scope: BotCommandScope hash (see BotCommandScope); defaults to default scope.
|
|
15
|
+
def initialize(scope: { type: :default }, lang_code: '')
|
|
16
|
+
@scope = scope
|
|
17
|
+
@lang_code = lang_code
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
def serialize
|
|
21
|
+
result = u32_b(CONSTRUCTOR)
|
|
22
|
+
result += BotCommandScope.serialize(@scope)
|
|
23
|
+
result += serialize_tl_string(@lang_code)
|
|
24
|
+
result
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
private
|
|
28
|
+
|
|
29
|
+
def serialize_tl_string(str)
|
|
30
|
+
bytes = str.to_s.encode('UTF-8').bytes
|
|
31
|
+
length = bytes.length
|
|
32
|
+
if length <= 253
|
|
33
|
+
[length] + bytes + padding(length + 1)
|
|
34
|
+
else
|
|
35
|
+
[254] + u32_b(length)[0, 3] + bytes + padding(length + 4)
|
|
36
|
+
end
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
def padding(current_length)
|
|
40
|
+
pad_length = (4 - (current_length % 4)) % 4
|
|
41
|
+
[0] * pad_length
|
|
42
|
+
end
|
|
43
|
+
end
|
|
44
|
+
end
|
|
45
|
+
end
|
|
@@ -7,19 +7,25 @@ module MTProto
|
|
|
7
7
|
class SendMedia
|
|
8
8
|
include Binary
|
|
9
9
|
|
|
10
|
-
INPUT_REPLY_TO_MESSAGE =
|
|
10
|
+
INPUT_REPLY_TO_MESSAGE = 0x3bd4b7c2
|
|
11
|
+
INPUT_MEDIA_PHOTO = 0xe3af4434
|
|
12
|
+
INPUT_MEDIA_DOCUMENT = 0xa8763ab5
|
|
13
|
+
INPUT_PHOTO = 0x3bb3b94a
|
|
14
|
+
INPUT_DOCUMENT = 0x1abfb575
|
|
11
15
|
|
|
12
|
-
def initialize(peer:, media:, message: '', random_id: nil, reply_to: nil)
|
|
16
|
+
def initialize(peer:, media:, message: '', random_id: nil, reply_to: nil, silent: false)
|
|
13
17
|
@peer = peer
|
|
14
18
|
@media = media
|
|
15
19
|
@message = message
|
|
16
20
|
@random_id = random_id || SecureRandom.random_number(2**63)
|
|
17
21
|
@reply_to = reply_to
|
|
22
|
+
@silent = silent
|
|
18
23
|
end
|
|
19
24
|
|
|
20
25
|
def serialize
|
|
21
26
|
flags = 0
|
|
22
27
|
flags |= (1 << 0) if @reply_to
|
|
28
|
+
flags |= (1 << 5) if @silent
|
|
23
29
|
|
|
24
30
|
result = u32_b(Constructors::MESSAGES_SEND_MEDIA)
|
|
25
31
|
result += u32_b(flags)
|
|
@@ -56,11 +62,27 @@ module MTProto
|
|
|
56
62
|
case @media[:type]
|
|
57
63
|
when :uploaded_photo
|
|
58
64
|
serialize_input_media_uploaded_photo(@media[:file])
|
|
65
|
+
when :uploaded_document
|
|
66
|
+
serialize_input_media_uploaded_document(@media)
|
|
67
|
+
when :photo
|
|
68
|
+
serialize_existing_media(INPUT_MEDIA_PHOTO, INPUT_PHOTO, @media)
|
|
69
|
+
when :document
|
|
70
|
+
serialize_existing_media(INPUT_MEDIA_DOCUMENT, INPUT_DOCUMENT, @media)
|
|
59
71
|
else
|
|
60
72
|
raise "Unknown media type: #{@media[:type]}"
|
|
61
73
|
end
|
|
62
74
|
end
|
|
63
75
|
|
|
76
|
+
# Re-send media that already lives on Telegram, by reference — inputMediaPhoto/
|
|
77
|
+
# inputMediaDocument (flags=0) wrapping inputPhoto/inputDocument(id, access_hash,
|
|
78
|
+
# file_reference). Used to re-post a guest-mention's photo/video, where the bot
|
|
79
|
+
# cannot forward from a chat it is not a member of.
|
|
80
|
+
def serialize_existing_media(media_ctor, input_ctor, media)
|
|
81
|
+
u32_b(media_ctor) + u32_b(0) +
|
|
82
|
+
u32_b(input_ctor) + u64_b(media[:id]) + u64_b(media[:access_hash]) +
|
|
83
|
+
serialize_bytes(media[:file_reference])
|
|
84
|
+
end
|
|
85
|
+
|
|
64
86
|
def serialize_input_media_uploaded_photo(file)
|
|
65
87
|
result = u32_b(Constructors::INPUT_MEDIA_UPLOADED_PHOTO)
|
|
66
88
|
result += u32_b(0) # flags
|
|
@@ -68,6 +90,48 @@ module MTProto
|
|
|
68
90
|
result
|
|
69
91
|
end
|
|
70
92
|
|
|
93
|
+
# inputMediaUploadedDocument with flags=0 (no thumb/stickers/ttl/video
|
|
94
|
+
# fields): file, mime_type, then the document-attributes vector. A generic
|
|
95
|
+
# file carries documentAttributeFilename; audio adds documentAttributeAudio.
|
|
96
|
+
def serialize_input_media_uploaded_document(media)
|
|
97
|
+
u32_b(Constructors::INPUT_MEDIA_UPLOADED_DOCUMENT) +
|
|
98
|
+
u32_b(0) +
|
|
99
|
+
serialize_input_file(media[:file]) +
|
|
100
|
+
serialize_tl_string(media[:mime_type].to_s) +
|
|
101
|
+
serialize_document_attributes(media[:attributes] || [])
|
|
102
|
+
end
|
|
103
|
+
|
|
104
|
+
def serialize_document_attributes(attributes)
|
|
105
|
+
result = u32_b(Constructors::VECTOR) + u32_b(attributes.length)
|
|
106
|
+
attributes.each { |attribute| result += serialize_document_attribute(attribute) }
|
|
107
|
+
result
|
|
108
|
+
end
|
|
109
|
+
|
|
110
|
+
def serialize_document_attribute(attribute)
|
|
111
|
+
case attribute[:type]
|
|
112
|
+
when :filename
|
|
113
|
+
u32_b(Constructors::DOCUMENT_ATTRIBUTE_FILENAME) +
|
|
114
|
+
serialize_tl_string(attribute[:file_name].to_s)
|
|
115
|
+
when :audio
|
|
116
|
+
serialize_document_attribute_audio(attribute)
|
|
117
|
+
else
|
|
118
|
+
raise "Unknown document attribute: #{attribute[:type]}"
|
|
119
|
+
end
|
|
120
|
+
end
|
|
121
|
+
|
|
122
|
+
# documentAttributeAudio: voice=flags.10, title=flags.0, performer=flags.1;
|
|
123
|
+
# title is written before performer on the wire.
|
|
124
|
+
def serialize_document_attribute_audio(attribute)
|
|
125
|
+
flags = 0
|
|
126
|
+
flags |= (1 << 0) if attribute[:title]
|
|
127
|
+
flags |= (1 << 1) if attribute[:performer]
|
|
128
|
+
flags |= (1 << 10) if attribute[:voice]
|
|
129
|
+
result = u32_b(Constructors::DOCUMENT_ATTRIBUTE_AUDIO) + u32_b(flags) + u32_b(attribute[:duration].to_i)
|
|
130
|
+
result += serialize_tl_string(attribute[:title]) if attribute[:title]
|
|
131
|
+
result += serialize_tl_string(attribute[:performer]) if attribute[:performer]
|
|
132
|
+
result
|
|
133
|
+
end
|
|
134
|
+
|
|
71
135
|
def serialize_input_file(file)
|
|
72
136
|
u32_b(Constructors::INPUT_FILE) +
|
|
73
137
|
u64_b(file[:id]) +
|
|
@@ -77,9 +141,17 @@ module MTProto
|
|
|
77
141
|
end
|
|
78
142
|
|
|
79
143
|
def serialize_tl_string(str)
|
|
80
|
-
|
|
81
|
-
|
|
144
|
+
serialize_byte_array(str.encode('UTF-8').bytes)
|
|
145
|
+
end
|
|
82
146
|
|
|
147
|
+
# TL bytes for a raw binary string (e.g. a file_reference) — NOT re-encoded as
|
|
148
|
+
# UTF-8, which would corrupt arbitrary bytes.
|
|
149
|
+
def serialize_bytes(raw)
|
|
150
|
+
serialize_byte_array(raw.to_s.b.bytes)
|
|
151
|
+
end
|
|
152
|
+
|
|
153
|
+
def serialize_byte_array(bytes)
|
|
154
|
+
length = bytes.length
|
|
83
155
|
if length <= 253
|
|
84
156
|
[length] + bytes + padding(length + 1)
|
|
85
157
|
else
|
|
@@ -7,20 +7,22 @@ module MTProto
|
|
|
7
7
|
class SendMessage
|
|
8
8
|
include Binary
|
|
9
9
|
|
|
10
|
-
INPUT_REPLY_TO_MESSAGE =
|
|
10
|
+
INPUT_REPLY_TO_MESSAGE = 0x3bd4b7c2
|
|
11
11
|
|
|
12
|
-
def initialize(peer:, message:, random_id: nil, reply_to: nil, reply_markup: nil)
|
|
12
|
+
def initialize(peer:, message:, random_id: nil, reply_to: nil, reply_markup: nil, silent: false)
|
|
13
13
|
@peer = peer
|
|
14
14
|
@message = message
|
|
15
15
|
@random_id = random_id || SecureRandom.random_number(2**63)
|
|
16
16
|
@reply_to = reply_to
|
|
17
17
|
@reply_markup = reply_markup
|
|
18
|
+
@silent = silent
|
|
18
19
|
end
|
|
19
20
|
|
|
20
21
|
def serialize
|
|
21
22
|
flags = 0
|
|
22
23
|
flags |= (1 << 0) if @reply_to
|
|
23
24
|
flags |= (1 << 2) if @reply_markup
|
|
25
|
+
flags |= (1 << 5) if @silent
|
|
24
26
|
|
|
25
27
|
result = u32_b(Constructors::MESSAGES_SEND_MESSAGE)
|
|
26
28
|
result += u32_b(flags)
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module MTProto
|
|
4
|
+
module TL
|
|
5
|
+
# A SendMessageAction — the payload of messages.setTyping. Every variant is
|
|
6
|
+
# just its own constructor id; the upload/import variants additionally carry
|
|
7
|
+
# a progress:int (0-100). One class covers them all.
|
|
8
|
+
class SendMessageAction
|
|
9
|
+
include Binary
|
|
10
|
+
|
|
11
|
+
CONSTRUCTORS = {
|
|
12
|
+
typing: 0x16bf744e,
|
|
13
|
+
cancel: 0xfd5ec8f5,
|
|
14
|
+
record_audio: 0xd52f73f7,
|
|
15
|
+
upload_audio: 0xf351d7ab,
|
|
16
|
+
record_video: 0xa187d66f,
|
|
17
|
+
upload_video: 0xe9763aec,
|
|
18
|
+
record_round: 0x88f27fbc,
|
|
19
|
+
upload_round: 0x243e1c66,
|
|
20
|
+
upload_photo: 0xd1d34a26,
|
|
21
|
+
upload_document: 0xaa0cd9e4,
|
|
22
|
+
geo_location: 0x176f8ba1,
|
|
23
|
+
choose_contact: 0x628cbc6f,
|
|
24
|
+
choose_sticker: 0xb05ac6b1,
|
|
25
|
+
game_play: 0xdd6a8f48,
|
|
26
|
+
history_import: 0xdbda9246
|
|
27
|
+
}.freeze
|
|
28
|
+
|
|
29
|
+
WITH_PROGRESS = %i[upload_audio upload_video upload_round upload_photo upload_document history_import].freeze
|
|
30
|
+
|
|
31
|
+
def self.types
|
|
32
|
+
CONSTRUCTORS.keys
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
def initialize(type, progress: 0)
|
|
36
|
+
@type = type
|
|
37
|
+
@progress = progress
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
def serialize
|
|
41
|
+
id = CONSTRUCTORS.fetch(@type) { raise ArgumentError, "unknown SendMessageAction: #{@type}" }
|
|
42
|
+
result = u32_b(id)
|
|
43
|
+
result += u32_b(@progress) if WITH_PROGRESS.include?(@type)
|
|
44
|
+
result
|
|
45
|
+
end
|
|
46
|
+
end
|
|
47
|
+
end
|
|
48
|
+
end
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module MTProto
|
|
4
|
+
module TL
|
|
5
|
+
# messages.setBotCallbackAnswer — the bot's reaction to a press. With no
|
|
6
|
+
# message it just dismisses the button's spinner; with a message it shows a
|
|
7
|
+
# toast (alert=false) or a modal alert (alert=true).
|
|
8
|
+
class SetBotCallbackAnswer
|
|
9
|
+
include Binary
|
|
10
|
+
|
|
11
|
+
CONSTRUCTOR = 0xd58f130a
|
|
12
|
+
|
|
13
|
+
def initialize(query_id:, message: nil, alert: false, url: nil, cache_time: 0)
|
|
14
|
+
@query_id = query_id
|
|
15
|
+
@message = message
|
|
16
|
+
@alert = alert
|
|
17
|
+
@url = url
|
|
18
|
+
@cache_time = cache_time
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
def serialize
|
|
22
|
+
flags = 0
|
|
23
|
+
flags |= (1 << 0) if @message
|
|
24
|
+
flags |= (1 << 1) if @alert
|
|
25
|
+
flags |= (1 << 2) if @url
|
|
26
|
+
|
|
27
|
+
result = u32_b(CONSTRUCTOR)
|
|
28
|
+
result += u32_b(flags)
|
|
29
|
+
result += u64_b(@query_id)
|
|
30
|
+
result += serialize_tl_string(@message) if @message
|
|
31
|
+
result += serialize_tl_string(@url) if @url
|
|
32
|
+
result += u32_b(@cache_time)
|
|
33
|
+
result
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
private
|
|
37
|
+
|
|
38
|
+
def serialize_tl_string(str)
|
|
39
|
+
bytes = str.to_s.encode('UTF-8').bytes
|
|
40
|
+
length = bytes.length
|
|
41
|
+
if length <= 253
|
|
42
|
+
[length] + bytes + padding(length + 1)
|
|
43
|
+
else
|
|
44
|
+
[254] + u32_b(length)[0, 3] + bytes + padding(length + 4)
|
|
45
|
+
end
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
def padding(current_length)
|
|
49
|
+
pad_length = (4 - (current_length % 4)) % 4
|
|
50
|
+
[0] * pad_length
|
|
51
|
+
end
|
|
52
|
+
end
|
|
53
|
+
end
|
|
54
|
+
end
|
|
@@ -1,23 +1,26 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
+
require_relative 'bot_command_scope'
|
|
4
|
+
|
|
3
5
|
module MTProto
|
|
4
6
|
module TL
|
|
5
7
|
class SetBotCommands
|
|
6
8
|
include Binary
|
|
7
9
|
|
|
8
10
|
CONSTRUCTOR = 0x0517165a
|
|
9
|
-
BOT_COMMAND_SCOPE_DEFAULT = 0x2f6cb2ab
|
|
10
11
|
BOT_COMMAND = 0xc27ac8c7
|
|
11
12
|
|
|
12
13
|
# commands: array of { command:, description: }
|
|
13
|
-
|
|
14
|
+
# scope: BotCommandScope hash (see BotCommandScope); defaults to default scope.
|
|
15
|
+
def initialize(commands:, scope: { type: :default }, lang_code: '')
|
|
14
16
|
@commands = commands
|
|
17
|
+
@scope = scope
|
|
15
18
|
@lang_code = lang_code
|
|
16
19
|
end
|
|
17
20
|
|
|
18
21
|
def serialize
|
|
19
22
|
result = u32_b(CONSTRUCTOR)
|
|
20
|
-
result +=
|
|
23
|
+
result += BotCommandScope.serialize(@scope)
|
|
21
24
|
result += serialize_tl_string(@lang_code)
|
|
22
25
|
result += u32_b(Constructors::VECTOR)
|
|
23
26
|
result += u32_b(@commands.size)
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'securerandom'
|
|
4
|
+
|
|
5
|
+
module MTProto
|
|
6
|
+
module TL
|
|
7
|
+
# messages.setBotGuestChatResult#b8f106e3 query_id:long result:InputBotInlineResult
|
|
8
|
+
# = InputBotInlineMessageID
|
|
9
|
+
# How a guest-mode bot answers an updateBotGuestChatQuery: the reply text is wrapped
|
|
10
|
+
# in inputBotInlineResult(type "article") -> inputBotInlineMessageText, and the server
|
|
11
|
+
# posts that message into the guest chat. Text-only for now (no media / buttons).
|
|
12
|
+
class SetBotGuestChatResult
|
|
13
|
+
include Binary
|
|
14
|
+
|
|
15
|
+
CONSTRUCTOR = 0xb8f106e3
|
|
16
|
+
INPUT_BOT_INLINE_RESULT = 0x88bf9319 # inputBotInlineResult
|
|
17
|
+
INPUT_BOT_INLINE_MESSAGE_TEXT = 0x3dcd7a87 # inputBotInlineMessageText
|
|
18
|
+
|
|
19
|
+
def initialize(query_id:, message:, result_id: nil, result_type: 'article', result_title: nil)
|
|
20
|
+
@query_id = query_id
|
|
21
|
+
@message = message
|
|
22
|
+
@result_id = result_id || SecureRandom.hex(8)
|
|
23
|
+
@result_type = result_type
|
|
24
|
+
# The "article" result type requires a non-empty title (the server rejects an
|
|
25
|
+
# empty one with ARTICLE_TITLE_EMPTY); the actual reply shown is send_message.
|
|
26
|
+
@result_title = result_title || message_title
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
def serialize
|
|
30
|
+
result = u32_b(CONSTRUCTOR)
|
|
31
|
+
result += u64_b(@query_id)
|
|
32
|
+
result += serialize_result
|
|
33
|
+
result
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
private
|
|
37
|
+
|
|
38
|
+
# inputBotInlineResult#88bf9319 flags:# id:string type:string
|
|
39
|
+
# title:flags.1?string description:flags.2? ... send_message:InputBotInlineMessage
|
|
40
|
+
# flags = 1<<1: id/type + the required title, then send_message.
|
|
41
|
+
def serialize_result
|
|
42
|
+
result = u32_b(INPUT_BOT_INLINE_RESULT)
|
|
43
|
+
result += u32_b(1 << 1) # title present
|
|
44
|
+
result += serialize_tl_string(@result_id)
|
|
45
|
+
result += serialize_tl_string(@result_type)
|
|
46
|
+
result += serialize_tl_string(@result_title)
|
|
47
|
+
result += serialize_send_message
|
|
48
|
+
result
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
def message_title
|
|
52
|
+
title = @message.to_s.strip
|
|
53
|
+
title = 'Ответ' if title.empty?
|
|
54
|
+
title[0, 64]
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
# inputBotInlineMessageText#3dcd7a87 flags:# no_webpage:flags.0?true
|
|
58
|
+
# invert_media:flags.3?true message:string entities:flags.1? reply_markup:flags.2?
|
|
59
|
+
# flags = 0: a plain text message.
|
|
60
|
+
def serialize_send_message
|
|
61
|
+
result = u32_b(INPUT_BOT_INLINE_MESSAGE_TEXT)
|
|
62
|
+
result += u32_b(0)
|
|
63
|
+
result += serialize_tl_string(@message)
|
|
64
|
+
result
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
def serialize_tl_string(str)
|
|
68
|
+
bytes = str.encode('UTF-8').bytes
|
|
69
|
+
length = bytes.length
|
|
70
|
+
|
|
71
|
+
if length <= 253
|
|
72
|
+
[length] + bytes + padding(length + 1)
|
|
73
|
+
else
|
|
74
|
+
[254] + u32_b(length)[0, 3] + bytes + padding(length + 4)
|
|
75
|
+
end
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
def padding(current_length)
|
|
79
|
+
pad_length = (4 - (current_length % 4)) % 4
|
|
80
|
+
[0] * pad_length
|
|
81
|
+
end
|
|
82
|
+
end
|
|
83
|
+
end
|
|
84
|
+
end
|