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.
Files changed (60) hide show
  1. checksums.yaml +4 -4
  2. data/data/tl-schema.json +47158 -42684
  3. data/lib/mtproto/auth_key_generator.rb +2 -2
  4. data/lib/mtproto/client/api/export_authorization.rb +17 -0
  5. data/lib/mtproto/client/api/import_authorization.rb +23 -0
  6. data/lib/mtproto/client/api.rb +2 -0
  7. data/lib/mtproto/client/rpc.rb +33 -0
  8. data/lib/mtproto/client.rb +54 -1
  9. data/lib/mtproto/file_downloader.rb +122 -0
  10. data/lib/mtproto/tl/constructor_names.rb +324 -109
  11. data/lib/mtproto/tl/constructors.rb +16 -8
  12. data/lib/mtproto/tl/objects/bot_command_scope.rb +81 -0
  13. data/lib/mtproto/tl/objects/channels_join_channel.rb +1 -1
  14. data/lib/mtproto/tl/objects/channels_update_username.rb +42 -0
  15. data/lib/mtproto/tl/objects/contacts.rb +1 -1
  16. data/lib/mtproto/tl/objects/create_bot.rb +54 -0
  17. data/lib/mtproto/tl/objects/dialogs.rb +25 -10
  18. data/lib/mtproto/tl/objects/edit_access_settings.rb +46 -0
  19. data/lib/mtproto/tl/objects/export_authorization.rb +17 -0
  20. data/lib/mtproto/tl/objects/export_bot_token.rb +32 -0
  21. data/lib/mtproto/tl/objects/exported_authorization.rb +32 -0
  22. data/lib/mtproto/tl/objects/exported_bot_token.rb +27 -0
  23. data/lib/mtproto/tl/objects/forward_messages.rb +12 -3
  24. data/lib/mtproto/tl/objects/get_access_settings.rb +28 -0
  25. data/lib/mtproto/tl/objects/get_bot_callback_answer.rb +67 -0
  26. data/lib/mtproto/tl/objects/get_bot_commands.rb +46 -0
  27. data/lib/mtproto/tl/objects/get_file.rb +10 -2
  28. data/lib/mtproto/tl/objects/import_authorization.rb +38 -0
  29. data/lib/mtproto/tl/objects/input_keyboard_button_request_peer.rb +54 -0
  30. data/lib/mtproto/tl/objects/keyboard_button_callback.rb +50 -0
  31. data/lib/mtproto/tl/objects/message.rb +97 -21
  32. data/lib/mtproto/tl/objects/messages.rb +7 -74
  33. data/lib/mtproto/tl/objects/messages_get_messages.rb +26 -0
  34. data/lib/mtproto/tl/objects/reply_inline_markup.rb +30 -0
  35. data/lib/mtproto/tl/objects/reply_keyboard_markup.rb +35 -0
  36. data/lib/mtproto/tl/objects/request_peer_type_create_bot.rb +47 -0
  37. data/lib/mtproto/tl/objects/reset_bot_commands.rb +45 -0
  38. data/lib/mtproto/tl/objects/send_bot_requested_peer.rb +51 -0
  39. data/lib/mtproto/tl/objects/send_media.rb +87 -4
  40. data/lib/mtproto/tl/objects/send_message.rb +7 -2
  41. data/lib/mtproto/tl/objects/send_message_action.rb +48 -0
  42. data/lib/mtproto/tl/objects/set_bot_callback_answer.rb +54 -0
  43. data/lib/mtproto/tl/objects/set_bot_commands.rb +53 -0
  44. data/lib/mtproto/tl/objects/set_bot_guest_chat_result.rb +84 -0
  45. data/lib/mtproto/tl/objects/set_bot_info.rb +64 -0
  46. data/lib/mtproto/tl/objects/set_typing.rb +49 -0
  47. data/lib/mtproto/tl/objects/update_status.rb +24 -0
  48. data/lib/mtproto/tl/objects/updates.rb +117 -0
  49. data/lib/mtproto/tl/objects/updates_difference.rb +45 -69
  50. data/lib/mtproto/tl/objects/upload_profile_photo.rb +65 -0
  51. data/lib/mtproto/tl/reader.rb +188 -0
  52. data/lib/mtproto/transport/abridged_packet_codec.rb +5 -1
  53. data/lib/mtproto/unencrypted_message.rb +39 -0
  54. data/lib/mtproto/version.rb +1 -1
  55. data/lib/mtproto.rb +4 -1
  56. data/scripts/gen_constructor_names.rb +72 -0
  57. data/scripts/tl_to_json.rb +72 -0
  58. data/scripts/verify_ids.rb +33 -0
  59. metadata +38 -2
  60. 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 = Message.new(rpc.serialize)
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(Message.parse(response_packet))
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
@@ -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'
@@ -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)
@@ -20,7 +20,7 @@ module MTProto
20
20
  class Client
21
21
  extend DelegateMethods
22
22
 
23
- API_LAYER = 214
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