mtproto 0.0.14 → 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/rpc.rb +33 -0
- data/lib/mtproto/client.rb +54 -1
- data/lib/mtproto/file_downloader.rb +122 -0
- data/lib/mtproto/tl/constructor_names.rb +324 -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/create_bot.rb +54 -0
- data/lib/mtproto/tl/objects/dialogs.rb +25 -10
- data/lib/mtproto/tl/objects/edit_access_settings.rb +46 -0
- data/lib/mtproto/tl/objects/export_authorization.rb +17 -0
- data/lib/mtproto/tl/objects/export_bot_token.rb +32 -0
- data/lib/mtproto/tl/objects/exported_authorization.rb +32 -0
- data/lib/mtproto/tl/objects/exported_bot_token.rb +27 -0
- data/lib/mtproto/tl/objects/forward_messages.rb +12 -3
- data/lib/mtproto/tl/objects/get_access_settings.rb +28 -0
- 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/input_keyboard_button_request_peer.rb +54 -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/messages_get_messages.rb +26 -0
- data/lib/mtproto/tl/objects/reply_inline_markup.rb +30 -0
- data/lib/mtproto/tl/objects/reply_keyboard_markup.rb +35 -0
- data/lib/mtproto/tl/objects/request_peer_type_create_bot.rb +47 -0
- data/lib/mtproto/tl/objects/reset_bot_commands.rb +45 -0
- data/lib/mtproto/tl/objects/send_bot_requested_peer.rb +51 -0
- data/lib/mtproto/tl/objects/send_media.rb +87 -4
- data/lib/mtproto/tl/objects/send_message.rb +7 -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 +53 -0
- data/lib/mtproto/tl/objects/set_bot_guest_chat_result.rb +84 -0
- data/lib/mtproto/tl/objects/set_bot_info.rb +64 -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 +45 -69
- data/lib/mtproto/tl/objects/upload_profile_photo.rb +65 -0
- 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 +38 -2
- data/lib/mtproto/message/message.rb +0 -85
|
@@ -86,13 +86,13 @@ module MTProto
|
|
|
86
86
|
private
|
|
87
87
|
|
|
88
88
|
def send_and_receive(rpc, response_class)
|
|
89
|
-
message =
|
|
89
|
+
message = UnencryptedMessage.new(rpc.serialize)
|
|
90
90
|
@connection.send(Transport::Packet.new(message.bytes))
|
|
91
91
|
|
|
92
92
|
response_packet = @connection.receive
|
|
93
93
|
raise 'Receive timeout' if response_packet.nil?
|
|
94
94
|
|
|
95
|
-
result = response_class.deserialize(
|
|
95
|
+
result = response_class.deserialize(UnencryptedMessage.parse(response_packet))
|
|
96
96
|
|
|
97
97
|
raise 'Nonce mismatch!' if rpc.respond_to?(:nonce) && result.nonce != rpc.nonce
|
|
98
98
|
raise 'Server nonce mismatch!' if rpc.respond_to?(:server_nonce) && result.server_nonce != rpc.server_nonce
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative '../../tl/objects/export_authorization'
|
|
4
|
+
require_relative '../../tl/objects/exported_authorization'
|
|
5
|
+
|
|
6
|
+
module MTProto
|
|
7
|
+
class Client
|
|
8
|
+
class API
|
|
9
|
+
def export_authorization(dc_id:)
|
|
10
|
+
rpc_call(
|
|
11
|
+
TL::ExportAuthorization.new(dc_id: dc_id),
|
|
12
|
+
TL::ExportedAuthorization
|
|
13
|
+
).body
|
|
14
|
+
end
|
|
15
|
+
end
|
|
16
|
+
end
|
|
17
|
+
end
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative '../../tl/objects/import_authorization'
|
|
4
|
+
require_relative '../../tl/objects/authorization'
|
|
5
|
+
|
|
6
|
+
module MTProto
|
|
7
|
+
class Client
|
|
8
|
+
class API
|
|
9
|
+
def import_authorization(id:, bytes:)
|
|
10
|
+
result = rpc_call(
|
|
11
|
+
TL::ImportAuthorization.new(id: id, bytes: bytes),
|
|
12
|
+
TL::Authorization
|
|
13
|
+
).body
|
|
14
|
+
|
|
15
|
+
if result.authorization? && result.user_id
|
|
16
|
+
@client.update_user(user_id: result.user_id, access_hash: result.access_hash)
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
result
|
|
20
|
+
end
|
|
21
|
+
end
|
|
22
|
+
end
|
|
23
|
+
end
|
data/lib/mtproto/client/api.rb
CHANGED
|
@@ -2,6 +2,8 @@
|
|
|
2
2
|
|
|
3
3
|
require_relative 'api/send_code'
|
|
4
4
|
require_relative 'api/sign_in'
|
|
5
|
+
require_relative 'api/export_authorization'
|
|
6
|
+
require_relative 'api/import_authorization'
|
|
5
7
|
require_relative 'api/export_login_token'
|
|
6
8
|
require_relative 'api/import_login_token'
|
|
7
9
|
require_relative 'api/check_password'
|
data/lib/mtproto/client/rpc.rb
CHANGED
|
@@ -1,11 +1,16 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
+
require 'securerandom'
|
|
3
4
|
require_relative '../message_id'
|
|
4
5
|
require_relative 'rpc/response'
|
|
5
6
|
|
|
6
7
|
module MTProto
|
|
7
8
|
class Client
|
|
8
9
|
class RPC
|
|
10
|
+
MSGS_ACK = 0x62d6b459
|
|
11
|
+
VECTOR = 0x1cb5c415
|
|
12
|
+
PING_DELAY_DISCONNECT = 0xf3427b8c
|
|
13
|
+
|
|
9
14
|
attr_reader :pending_requests
|
|
10
15
|
|
|
11
16
|
def initialize(client)
|
|
@@ -66,6 +71,34 @@ module MTProto
|
|
|
66
71
|
@pending_requests.clear
|
|
67
72
|
end
|
|
68
73
|
|
|
74
|
+
# Acknowledge received server messages (msgs_ack). Without this the server
|
|
75
|
+
# keeps the unacknowledged messages, resends them, and drops the connection
|
|
76
|
+
# after a fixed window. Fire-and-forget — no rpc_result is expected.
|
|
77
|
+
def send_ack(msg_ids)
|
|
78
|
+
return if msg_ids.nil? || msg_ids.empty?
|
|
79
|
+
|
|
80
|
+
send_encrypted(self.class.ack_body(msg_ids), content_related: false)
|
|
81
|
+
end
|
|
82
|
+
|
|
83
|
+
# Keepalive: ping_delay_disconnect asks the server to disconnect us only if
|
|
84
|
+
# we stop pinging within disconnect_delay seconds. Fire-and-forget (the pong
|
|
85
|
+
# is ignored by process_message).
|
|
86
|
+
def send_ping(disconnect_delay = 75)
|
|
87
|
+
send_encrypted(self.class.ping_body(SecureRandom.random_number(2**63), disconnect_delay),
|
|
88
|
+
content_related: false)
|
|
89
|
+
end
|
|
90
|
+
|
|
91
|
+
# Serialized msgs_ack body (no envelope) — extracted for unit testing.
|
|
92
|
+
def self.ack_body(msg_ids)
|
|
93
|
+
[MSGS_ACK].pack('L<') + [VECTOR].pack('L<') + [msg_ids.length].pack('L<') +
|
|
94
|
+
msg_ids.map { |id| [id].pack('Q<') }.join
|
|
95
|
+
end
|
|
96
|
+
|
|
97
|
+
# Serialized ping_delay_disconnect body (no envelope) — extracted for testing.
|
|
98
|
+
def self.ping_body(ping_id, disconnect_delay)
|
|
99
|
+
[PING_DELAY_DISCONNECT].pack('L<') + [ping_id].pack('Q<') + [disconnect_delay].pack('l<')
|
|
100
|
+
end
|
|
101
|
+
|
|
69
102
|
private
|
|
70
103
|
|
|
71
104
|
def serialize_request(request)
|
data/lib/mtproto/client.rb
CHANGED
|
@@ -20,7 +20,7 @@ module MTProto
|
|
|
20
20
|
class Client
|
|
21
21
|
extend DelegateMethods
|
|
22
22
|
|
|
23
|
-
API_LAYER =
|
|
23
|
+
API_LAYER = 227
|
|
24
24
|
|
|
25
25
|
attr_reader :connection, :server_key, :auth_key, :server_salt, :time_offset, :session,
|
|
26
26
|
:timeout, :user_id, :access_hash, :dc_number
|
|
@@ -77,10 +77,17 @@ module MTProto
|
|
|
77
77
|
@lang_code = 'en'
|
|
78
78
|
|
|
79
79
|
@receiver_task = nil
|
|
80
|
+
@keepalive_task = nil
|
|
81
|
+
@ack_ids = []
|
|
80
82
|
@running = false
|
|
81
83
|
@on_update_callbacks = []
|
|
82
84
|
end
|
|
83
85
|
|
|
86
|
+
# Keepalive cadence: flush pending acks every ACK_INTERVAL seconds, send a
|
|
87
|
+
# ping every PING_INTERVAL seconds.
|
|
88
|
+
ACK_INTERVAL = 5
|
|
89
|
+
PING_INTERVAL = 30
|
|
90
|
+
|
|
84
91
|
def on_update(&block)
|
|
85
92
|
@on_update_callbacks << block
|
|
86
93
|
end
|
|
@@ -111,6 +118,7 @@ module MTProto
|
|
|
111
118
|
raise 'Mainloop already running' if @running
|
|
112
119
|
|
|
113
120
|
@running = true
|
|
121
|
+
@ack_ids = []
|
|
114
122
|
@receiver_task = Async do
|
|
115
123
|
@connection.receive do |packet, error|
|
|
116
124
|
if error
|
|
@@ -124,15 +132,20 @@ module MTProto
|
|
|
124
132
|
sender: :server
|
|
125
133
|
)
|
|
126
134
|
|
|
135
|
+
collect_ack(decrypted[:msg_id], decrypted[:seq_no], decrypted[:body])
|
|
127
136
|
process_message(decrypted[:body])
|
|
128
137
|
end
|
|
129
138
|
end
|
|
139
|
+
@keepalive_task = Async { keepalive_loop }
|
|
130
140
|
end
|
|
131
141
|
|
|
132
142
|
def disconnect!
|
|
133
143
|
@running = false
|
|
144
|
+
@keepalive_task&.stop
|
|
145
|
+
@keepalive_task = nil
|
|
134
146
|
@receiver_task&.stop
|
|
135
147
|
@receiver_task = nil
|
|
148
|
+
@ack_ids = []
|
|
136
149
|
|
|
137
150
|
rpc.signal_all_error(Transport::ConnectionError.new('Client shutting down'))
|
|
138
151
|
|
|
@@ -232,6 +245,46 @@ module MTProto
|
|
|
232
245
|
|
|
233
246
|
private
|
|
234
247
|
|
|
248
|
+
# Periodically acknowledge received messages and ping, so the server doesn't
|
|
249
|
+
# drop a long-lived connection. Ends when the connection dies (the wrapper
|
|
250
|
+
# reconnects) or when the task is stopped on disconnect!.
|
|
251
|
+
def keepalive_loop
|
|
252
|
+
elapsed = 0
|
|
253
|
+
loop do
|
|
254
|
+
sleep ACK_INTERVAL
|
|
255
|
+
flush_acks
|
|
256
|
+
elapsed += ACK_INTERVAL
|
|
257
|
+
next if elapsed < PING_INTERVAL
|
|
258
|
+
|
|
259
|
+
elapsed = 0
|
|
260
|
+
rpc.send_ping
|
|
261
|
+
end
|
|
262
|
+
rescue StandardError
|
|
263
|
+
nil
|
|
264
|
+
end
|
|
265
|
+
|
|
266
|
+
# Queue msg_ids of received content-related server messages for msgs_ack.
|
|
267
|
+
# A container carries its own inner msg_ids; content-related messages have an
|
|
268
|
+
# odd seq_no and must be acknowledged (acks/pongs are even and need none).
|
|
269
|
+
def collect_ack(msg_id, seq_no, body)
|
|
270
|
+
constructor = body[0, 4].unpack1('L<')
|
|
271
|
+
if constructor == TL::Constructors::MSG_CONTAINER
|
|
272
|
+
TL::MsgContainer.deserialize(body).messages.each do |msg|
|
|
273
|
+
collect_ack(msg[:msg_id], msg[:seqno], msg[:body])
|
|
274
|
+
end
|
|
275
|
+
elsif seq_no.odd?
|
|
276
|
+
@ack_ids << msg_id
|
|
277
|
+
end
|
|
278
|
+
end
|
|
279
|
+
|
|
280
|
+
def flush_acks
|
|
281
|
+
return if @ack_ids.empty?
|
|
282
|
+
|
|
283
|
+
ids = @ack_ids
|
|
284
|
+
@ack_ids = []
|
|
285
|
+
rpc.send_ack(ids)
|
|
286
|
+
end
|
|
287
|
+
|
|
235
288
|
def process_message(response_body)
|
|
236
289
|
constructor = response_body[0, 4].unpack1('L<')
|
|
237
290
|
|
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative 'tl/objects/get_file'
|
|
4
|
+
require_relative 'tl/objects/raw_response'
|
|
5
|
+
|
|
6
|
+
module MTProto
|
|
7
|
+
# Downloads a file's bytes via upload.getFile, handling the case where the file
|
|
8
|
+
# lives on a DC other than the home client's. For a foreign DC it opens one
|
|
9
|
+
# extra connection to that DC, transfers the home authorization there
|
|
10
|
+
# (auth.exportAuthorization on the home client + auth.importAuthorization on the
|
|
11
|
+
# new connection) and runs getFile on it. A FILE_MIGRATE_X reply is honoured the
|
|
12
|
+
# same way. DC addresses come from help.getConfig — never hardcoded, since test
|
|
13
|
+
# DC addresses and ports differ from prod and change over time.
|
|
14
|
+
class FileDownloader
|
|
15
|
+
CHUNK = 1024 * 1024
|
|
16
|
+
DC_OPTION_IPV6 = 1 << 0
|
|
17
|
+
DC_OPTION_CDN = 1 << 3
|
|
18
|
+
|
|
19
|
+
# home: a connected, authorized Client whose receiver loop is already running.
|
|
20
|
+
# dc_options: help.getConfig dc_options (e.g. Client#init_connection! result).
|
|
21
|
+
# public_key / test_mode: used to handshake fresh connections to other DCs.
|
|
22
|
+
def initialize(home, dc_options:, public_key:, test_mode: false)
|
|
23
|
+
@home = home
|
|
24
|
+
@dc_options = dc_options
|
|
25
|
+
@public_key = public_key
|
|
26
|
+
@test_mode = test_mode
|
|
27
|
+
@subs = {}
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
# location: { id:, access_hash:, file_reference:, thumb_size: }.
|
|
31
|
+
# dc_id: the DC that stores the file. type: :photo or :document.
|
|
32
|
+
def download(location, dc_id:, type: :document)
|
|
33
|
+
client = client_for(dc_id)
|
|
34
|
+
read_all(client, location, type)
|
|
35
|
+
rescue RpcError => e
|
|
36
|
+
migrate = e.error_message.to_s[/\AFILE_MIGRATE_(\d+)\z/, 1]
|
|
37
|
+
raise unless migrate
|
|
38
|
+
|
|
39
|
+
# client_for, not connection_to: a migrate back to the home DC must reuse
|
|
40
|
+
# @home — exporting authorization to your own DC answers DC_ID_INVALID.
|
|
41
|
+
read_all(client_for(migrate.to_i), location, type)
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
# Tear down the extra DC connections opened for downloads.
|
|
45
|
+
def close
|
|
46
|
+
@subs.each_value(&:disconnect!)
|
|
47
|
+
@subs.clear
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
private
|
|
51
|
+
|
|
52
|
+
def client_for(dc_id)
|
|
53
|
+
dc_id == @home.dc_number ? @home : connection_to(dc_id)
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
def read_all(client, location, type)
|
|
57
|
+
data = +''
|
|
58
|
+
offset = 0
|
|
59
|
+
loop do
|
|
60
|
+
req = TL::GetFile.new(location: location, type: type, offset: offset, limit: CHUNK)
|
|
61
|
+
resp = client.rpc.call(req, TL::RawResponse).wait!(client.timeout)
|
|
62
|
+
unless resp.constructor_name == 'upload.file'
|
|
63
|
+
raise UnexpectedConstructorError, resp.raw_bytes[0, 4].unpack1('L<')
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
chunk = upload_file_bytes(resp.raw_bytes)
|
|
67
|
+
data << chunk
|
|
68
|
+
break if chunk.bytesize < CHUNK
|
|
69
|
+
|
|
70
|
+
offset += chunk.bytesize
|
|
71
|
+
end
|
|
72
|
+
data
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
# One authorized connection per foreign DC, reused across calls.
|
|
76
|
+
def connection_to(dc_id)
|
|
77
|
+
@subs[dc_id] ||= begin
|
|
78
|
+
sub = Client.new(
|
|
79
|
+
api_id: @home.api_id, api_hash: @home.api_hash,
|
|
80
|
+
host: dc_host(dc_id), port: dc_port(dc_id),
|
|
81
|
+
public_key: @public_key, dc_number: dc_id, test_mode: @test_mode
|
|
82
|
+
)
|
|
83
|
+
sub.connect!
|
|
84
|
+
sub.exchange_keys!
|
|
85
|
+
sub.start_receiving!
|
|
86
|
+
sub.init_connection!
|
|
87
|
+
exported = @home.api.export_authorization(dc_id: dc_id)
|
|
88
|
+
sub.api.import_authorization(id: exported.id, bytes: exported.bytes)
|
|
89
|
+
sub
|
|
90
|
+
end
|
|
91
|
+
end
|
|
92
|
+
|
|
93
|
+
def dc_option(dc_id)
|
|
94
|
+
@dc_options.find { |o| o[:id] == dc_id && o[:flags].to_i.nobits?(DC_OPTION_IPV6 | DC_OPTION_CDN) } ||
|
|
95
|
+
@dc_options.find { |o| o[:id] == dc_id } ||
|
|
96
|
+
raise(ArgumentError, "no DC option for dc_id=#{dc_id}")
|
|
97
|
+
end
|
|
98
|
+
|
|
99
|
+
def dc_host(dc_id)
|
|
100
|
+
dc_option(dc_id)[:ip_address]
|
|
101
|
+
end
|
|
102
|
+
|
|
103
|
+
def dc_port(dc_id)
|
|
104
|
+
dc_option(dc_id)[:port]
|
|
105
|
+
end
|
|
106
|
+
|
|
107
|
+
# upload.file#096a18d5 type:storage.FileType mtime:int bytes:bytes — the type
|
|
108
|
+
# is a bare constructor, so the bytes string starts at a fixed offset.
|
|
109
|
+
def upload_file_bytes(raw)
|
|
110
|
+
offset = 12
|
|
111
|
+
first = raw.getbyte(offset)
|
|
112
|
+
if first == 254
|
|
113
|
+
len = raw.getbyte(offset + 1) | (raw.getbyte(offset + 2) << 8) | (raw.getbyte(offset + 3) << 16)
|
|
114
|
+
start = offset + 4
|
|
115
|
+
else
|
|
116
|
+
len = first
|
|
117
|
+
start = offset + 1
|
|
118
|
+
end
|
|
119
|
+
raw[start, len]
|
|
120
|
+
end
|
|
121
|
+
end
|
|
122
|
+
end
|