mtproto 0.0.6 → 0.0.8

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 (69) hide show
  1. checksums.yaml +4 -4
  2. data/.env.example +4 -0
  3. data/lib/mtproto/async/middleware/base.rb +17 -0
  4. data/lib/mtproto/async/middleware/flood_wait.rb +42 -0
  5. data/lib/mtproto/async/request.rb +18 -0
  6. data/lib/mtproto/async/request_queue.rb +63 -0
  7. data/lib/mtproto/async_client.rb +201 -0
  8. data/lib/mtproto/auth_key_generator.rb +22 -12
  9. data/lib/mtproto/client/rpc.rb +165 -0
  10. data/lib/mtproto/client.rb +65 -176
  11. data/lib/mtproto/crypto/aes_ige.rb +1 -1
  12. data/lib/mtproto/crypto/factorization.rb +1 -1
  13. data/lib/mtproto/message_id.rb +13 -0
  14. data/lib/mtproto/rpc/get_config.rb +34 -0
  15. data/lib/mtproto/rpc/get_contacts.rb +29 -0
  16. data/lib/mtproto/rpc/get_updates_difference.rb +51 -0
  17. data/lib/mtproto/rpc/get_updates_state.rb +29 -0
  18. data/lib/mtproto/rpc/get_users.rb +29 -0
  19. data/lib/mtproto/rpc/ping.rb +33 -0
  20. data/lib/mtproto/rpc/send_code.rb +41 -0
  21. data/lib/mtproto/rpc/send_message.rb +47 -0
  22. data/lib/mtproto/rpc/sign_in.rb +48 -0
  23. data/lib/mtproto/type/auth_key/dh_gen_response.rb +37 -0
  24. data/lib/mtproto/type/auth_key/req_dh_params.rb +31 -0
  25. data/lib/mtproto/type/auth_key/req_pq_multi.rb +18 -0
  26. data/lib/mtproto/type/auth_key/res_pq.rb +62 -0
  27. data/lib/mtproto/type/auth_key/server_dh_params.rb +43 -0
  28. data/lib/mtproto/type/auth_key/set_client_dh_params.rb +25 -0
  29. data/lib/mtproto/{tl → type}/bad_msg_notification.rb +1 -1
  30. data/lib/mtproto/{tl → type}/client_dh_inner_data.rb +1 -1
  31. data/lib/mtproto/{tl → type}/code_settings.rb +1 -1
  32. data/lib/mtproto/{tl → type}/config.rb +1 -1
  33. data/lib/mtproto/{tl → type}/gzip_packed.rb +1 -1
  34. data/lib/mtproto/type/message.rb +38 -0
  35. data/lib/mtproto/{tl → type}/msg_container.rb +1 -1
  36. data/lib/mtproto/{tl → type}/new_session_created.rb +1 -1
  37. data/lib/mtproto/{tl/p_q_inner_data.rb → type/pq_inner_data.rb} +1 -1
  38. data/lib/mtproto/type/rpc/auth/authorization.rb +107 -0
  39. data/lib/mtproto/type/rpc/auth/send_code.rb +28 -0
  40. data/lib/mtproto/type/rpc/auth/sent_code.rb +36 -0
  41. data/lib/mtproto/type/rpc/auth/sign_in.rb +32 -0
  42. data/lib/mtproto/type/rpc/contacts/contacts.rb +155 -0
  43. data/lib/mtproto/type/rpc/contacts/get_contacts.rb +18 -0
  44. data/lib/mtproto/type/rpc/help/config.rb +35 -0
  45. data/lib/mtproto/type/rpc/help/get_config.rb +17 -0
  46. data/lib/mtproto/type/rpc/init_connection.rb +28 -0
  47. data/lib/mtproto/type/rpc/invoke_with_layer.rb +19 -0
  48. data/lib/mtproto/type/rpc/messages/send_message.rb +43 -0
  49. data/lib/mtproto/type/rpc/messages/updates.rb +87 -0
  50. data/lib/mtproto/type/rpc/ping.rb +18 -0
  51. data/lib/mtproto/type/rpc/pong.rb +46 -0
  52. data/lib/mtproto/type/rpc/updates/difference.rb +332 -0
  53. data/lib/mtproto/type/rpc/updates/get_difference.rb +42 -0
  54. data/lib/mtproto/type/rpc/updates/get_state.rb +17 -0
  55. data/lib/mtproto/type/rpc/updates/state.rb +59 -0
  56. data/lib/mtproto/type/rpc/users/get_users.rb +25 -0
  57. data/lib/mtproto/type/rpc/users/users.rb +99 -0
  58. data/lib/mtproto/{tl → type}/rpc_error.rb +1 -1
  59. data/lib/mtproto/{tl → type}/sent_code.rb +1 -1
  60. data/lib/mtproto/{tl → type}/serializer.rb +1 -1
  61. data/lib/mtproto/{tl → type}/server_dh_inner_data.rb +1 -1
  62. data/lib/mtproto/updates_poller.rb +111 -0
  63. data/lib/mtproto/version.rb +1 -1
  64. data/lib/mtproto.rb +28 -14
  65. metadata +86 -18
  66. data/ext/aes_ige/Makefile +0 -273
  67. data/ext/factorization/Makefile +0 -273
  68. data/lib/mtproto/connection.rb +0 -103
  69. data/lib/mtproto/tl/message.rb +0 -252
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: e7f507f0d6cf42ac479bf224933efca12e7519cd67d7e9d8796ceba14f517960
4
- data.tar.gz: 79c5137adfc60752fecb5367c6b90a08e7648f8fc523a95382fe7e755888c439
3
+ metadata.gz: 93bf74e47e4eb9cea99def85727f559cc3fcbe484411ba181b6ac596ed24fcbe
4
+ data.tar.gz: c0b1bc2e59a38eb2042d938692ba1959bbad1633a0594858ce9c1991444f4ef0
5
5
  SHA512:
6
- metadata.gz: 48de4193ba146630f831f690527cefcc0577c1bf56ab32452092e68dd75fa5bf86b294b8578e88e4daa26766c39ef78aa60c1a132e38975d6b01f0c21c508f29
7
- data.tar.gz: 46d2ad65c9c1f59ea5f75789fb27008620117ab63877dad711add23cb6d477ad9cea5e5dffb724cc4d1d128a67dd9b75f8db9eddaf8e30a16005c377cb1a7542
6
+ metadata.gz: 9a5cd8f10a36c676db3ba4e642f9f7d1a209600278b3d571c86dfcf78f489613603919a8c1f950b8e3b71e2a1a721a4b0fab721b2e318eb30ff7960edfe6b4df
7
+ data.tar.gz: e98baafabd0e49ea34d1cc7d92b6761a1b899fec53c19faccd96ada01989b8784ac6114e597f28a1f0a15f34ed95cb600948f89decdee66c0d84c6ec1e716194
data/.env.example CHANGED
@@ -3,3 +3,7 @@ TG_API_HASH=your_api_hash
3
3
  TG_TEST_DC='111.111.111.111:443'
4
4
  TG_TEST_DC_N='1'
5
5
  TG_TEST_DC_KEY='DC public key'
6
+
7
+ # For integration tests with authentication:
8
+ TG_AUTH='auth data 1'
9
+ TG_AUTH_2='auth data 2'
@@ -0,0 +1,17 @@
1
+ # frozen_string_literal: true
2
+
3
+ module MTProto
4
+ module Async
5
+ module Middleware
6
+ class Base
7
+ def initialize(app)
8
+ @app = app
9
+ end
10
+
11
+ def call(request, future, retry_fn, retries)
12
+ @app.call(request, future, retry_fn, retries)
13
+ end
14
+ end
15
+ end
16
+ end
17
+ end
@@ -0,0 +1,42 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'base'
4
+
5
+ module MTProto
6
+ module Async
7
+ module Middleware
8
+ class FloodWait < Base
9
+ def initialize(app, max_retries: 5)
10
+ super(app)
11
+ @max_retries = max_retries
12
+ end
13
+
14
+ def call(request, future, retry_fn, retries)
15
+ wrapped = @app.call(request, future, retry_fn, retries)
16
+
17
+ wrapped.rescue do |error|
18
+ if flood_wait_error?(error) && retries < @max_retries
19
+ delay = extract_delay(error.error_message)
20
+ sleep(delay)
21
+ retry_fn.call.value!
22
+ else
23
+ raise error
24
+ end
25
+ end
26
+ end
27
+
28
+ private
29
+
30
+ def flood_wait_error?(error)
31
+ error.is_a?(MTProto::RpcError) &&
32
+ error.error_code == 420 &&
33
+ error.error_message.start_with?('FLOOD_WAIT_')
34
+ end
35
+
36
+ def extract_delay(error_message)
37
+ error_message[11..-1].to_i
38
+ end
39
+ end
40
+ end
41
+ end
42
+ end
@@ -0,0 +1,18 @@
1
+ # frozen_string_literal: true
2
+
3
+ module MTProto
4
+ module Async
5
+ class Request
6
+ attr_reader :body, :metadata
7
+
8
+ def initialize(body:, metadata: {})
9
+ @body = body
10
+ @metadata = metadata
11
+ end
12
+
13
+ def key
14
+ metadata[:key] || :default
15
+ end
16
+ end
17
+ end
18
+ end
@@ -0,0 +1,63 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'concurrent-ruby'
4
+
5
+ module MTProto
6
+ module Async
7
+ class RequestQueue
8
+ def initialize
9
+ @queues = Concurrent::Hash.new
10
+ @threads = Concurrent::Hash.new
11
+ @running = Concurrent::AtomicBoolean.new(true)
12
+ end
13
+
14
+ def call(key:, &blk)
15
+ ensure_worker_running!(key)
16
+
17
+ future = Concurrent::Promises.resolvable_future
18
+ @queues[key] << [blk, future]
19
+ future
20
+ end
21
+
22
+ def shutdown
23
+ @running.make_false
24
+ @threads.each_value do |thread|
25
+ thread.join(2)
26
+ thread.kill if thread.alive?
27
+ end
28
+ @threads.clear
29
+ @queues.clear
30
+ end
31
+
32
+ private
33
+
34
+ def ensure_worker_running!(key)
35
+ return if @threads.key?(key) && @threads[key].alive?
36
+
37
+ @queues[key] ||= Queue.new
38
+ @threads[key] = Thread.new { worker_loop(key) }
39
+ end
40
+
41
+ def worker_loop(key)
42
+ queue = @queues[key]
43
+ loop do
44
+ break unless @running.true?
45
+
46
+ begin
47
+ blk, future = queue.pop(true)
48
+ result = blk.call
49
+ if result.is_a?(Concurrent::Promises::Future)
50
+ result.then { |v| future.fulfill(v) }.rescue { |e| future.reject(e) }
51
+ else
52
+ future.fulfill(result)
53
+ end
54
+ rescue ThreadError
55
+ sleep 0.01
56
+ rescue => e
57
+ future&.reject(e) if future
58
+ end
59
+ end
60
+ end
61
+ end
62
+ end
63
+ end
@@ -0,0 +1,201 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'concurrent'
4
+ require_relative 'async/request'
5
+ require_relative 'async/request_queue'
6
+ require_relative 'async/middleware/base'
7
+ require_relative 'async/middleware/flood_wait'
8
+
9
+ module MTProto
10
+ class AsyncClient
11
+ attr_reader :client
12
+
13
+ def initialize(client)
14
+ @client = client
15
+ @queue = Async::RequestQueue.new
16
+ @middleware_classes = []
17
+ @pending_requests = Concurrent::Hash.new
18
+ @timeout_threads = Concurrent::Hash.new
19
+ @receiver_thread = nil
20
+ @running = Concurrent::AtomicBoolean.new(false)
21
+
22
+ use(Async::Middleware::FloodWait)
23
+ end
24
+
25
+ def use(middleware_class, **options)
26
+ @middleware_classes << [middleware_class, options]
27
+ self
28
+ end
29
+
30
+ def call(body, metadata: {})
31
+ ensure_receiver_running!
32
+
33
+ request = Async::Request.new(body: body, metadata: metadata)
34
+
35
+ @queue.call(key: request.key) do
36
+ send_and_wrap = lambda do |retries = 0|
37
+ base_future = send_request(request.body)
38
+
39
+ retry_fn = lambda do
40
+ send_and_wrap.call(retries + 1)
41
+ end
42
+
43
+ build_middleware_stack.call(request, base_future, retry_fn, retries)
44
+ end
45
+
46
+ send_and_wrap.call(0)
47
+ end
48
+ end
49
+
50
+ def shutdown
51
+ @running.make_false
52
+
53
+ @client.disconnect! if @client.connected?
54
+
55
+ if @receiver_thread
56
+ @receiver_thread.join(2)
57
+ @receiver_thread.kill if @receiver_thread.alive?
58
+ @receiver_thread = nil
59
+ end
60
+
61
+ @timeout_threads.each_value(&:kill)
62
+ @timeout_threads.clear
63
+
64
+ @pending_requests.each_value do |future|
65
+ future.reject(MTProto::Transport::ConnectionError.new('Client shutting down')) unless future.resolved?
66
+ end
67
+ @pending_requests.clear
68
+
69
+ @queue.shutdown
70
+ end
71
+
72
+ private
73
+
74
+ def send_request(body)
75
+ msg_id = @client.rpc.call(body)
76
+ future = Concurrent::Promises.resolvable_future
77
+ @pending_requests[msg_id] = future
78
+
79
+ timeout_thread = Thread.new do
80
+ sleep @client.timeout
81
+ if @pending_requests.delete(msg_id)
82
+ future.reject(MTProto::Transport::ConnectionError.new('RPC timeout'))
83
+ end
84
+ @timeout_threads.delete(msg_id)
85
+ end
86
+ @timeout_threads[msg_id] = timeout_thread
87
+
88
+ future.on_fulfillment! do
89
+ timeout_thread.kill
90
+ @timeout_threads.delete(msg_id)
91
+ end
92
+ future.on_rejection! do
93
+ timeout_thread.kill
94
+ @timeout_threads.delete(msg_id)
95
+ end
96
+
97
+ future
98
+ end
99
+
100
+ def ensure_receiver_running!
101
+ return if @running.true?
102
+
103
+ @running.make_true
104
+ @receiver_thread = Thread.new { receiver_loop }
105
+ end
106
+
107
+ def receiver_loop
108
+ loop do
109
+ break unless @running.true?
110
+
111
+ begin
112
+ response_data = @client.connection.recv(timeout: 1)
113
+
114
+ decrypted = EncryptedMessage.decrypt(
115
+ auth_key: @client.auth_key,
116
+ encrypted_message_data: response_data,
117
+ sender: :server
118
+ )
119
+
120
+ process_message(decrypted[:body])
121
+ rescue MTProto::Transport::ConnectionError => e
122
+ next if e.message == 'Receive timeout' && @running.true?
123
+ break
124
+ rescue IOError => e
125
+ break
126
+ rescue => e
127
+ warn "AsyncClient receiver error: #{e.class}: #{e.message}"
128
+ warn e.backtrace.join("\n")
129
+ break
130
+ end
131
+ end
132
+ end
133
+
134
+ def process_message(response_body)
135
+ constructor = response_body[0, 4].unpack1('L<')
136
+
137
+ case constructor
138
+ when Client::RPC::CONSTRUCTOR_BAD_SERVER_SALT
139
+ handle_bad_server_salt(response_body)
140
+ when Type::NewSessionCreated::CONSTRUCTOR
141
+ handle_new_session(response_body)
142
+ when Type::MsgContainer::CONSTRUCTOR
143
+ handle_container(response_body)
144
+ when Client::RPC::CONSTRUCTOR_RPC_RESULT
145
+ handle_rpc_result(response_body)
146
+ end
147
+ end
148
+
149
+ def handle_bad_server_salt(response_body)
150
+ offset = 4
151
+ offset += 8
152
+ offset += 4
153
+ offset += 4
154
+ new_server_salt = response_body[offset, 8].unpack1('Q<')
155
+
156
+ @client.instance_variable_set(:@server_salt, new_server_salt)
157
+ end
158
+
159
+ def handle_new_session(response_body)
160
+ session_info = Type::NewSessionCreated.deserialize(response_body)
161
+ @client.instance_variable_set(:@server_salt, session_info.server_salt)
162
+ end
163
+
164
+ def handle_container(response_body)
165
+ container = Type::MsgContainer.deserialize(response_body)
166
+
167
+ container.messages.each do |msg|
168
+ process_message(msg[:body])
169
+ end
170
+ end
171
+
172
+ def handle_rpc_result(response_body)
173
+ offset = 4
174
+ req_msg_id = response_body[offset, 8].unpack1('Q<')
175
+ offset += 8
176
+ result = response_body[offset..]
177
+
178
+ future = @pending_requests.delete(req_msg_id)
179
+ return unless future # No pending request for this msg_id
180
+
181
+ # Check if result is an RPC error
182
+ result_constructor = result[0, 4].unpack1('L<')
183
+ if result_constructor == Type::RpcError::CONSTRUCTOR
184
+ error = Type::RpcError.deserialize(result)
185
+ future.reject(MTProto::RpcError.new(error.error_code, error.error_message))
186
+ else
187
+ future.fulfill(result)
188
+ end
189
+ end
190
+
191
+ def build_middleware_stack
192
+ # Middleware now wraps Futures and can trigger retries
193
+ # The base "app" just returns the Future as-is
194
+ app = ->(request, future, retry_fn, retries) { future }
195
+
196
+ @middleware_classes.reverse.reduce(app) do |stack, (middleware_class, options)|
197
+ middleware_class.new(stack, **options)
198
+ end
199
+ end
200
+ end
201
+ end
@@ -2,6 +2,13 @@
2
2
 
3
3
  require 'securerandom'
4
4
  require 'digest'
5
+ require_relative 'message_id'
6
+ require_relative 'type/auth_key/req_pq_multi'
7
+ require_relative 'type/auth_key/res_pq'
8
+ require_relative 'type/auth_key/req_dh_params'
9
+ require_relative 'type/auth_key/server_dh_params'
10
+ require_relative 'type/auth_key/set_client_dh_params'
11
+ require_relative 'type/auth_key/dh_gen_response'
5
12
 
6
13
  module MTProto
7
14
  class AuthKeyGenerator
@@ -80,12 +87,13 @@ module MTProto
80
87
 
81
88
  def req_pq_multi
82
89
  nonce = SecureRandom.random_bytes(16)
83
- message = TL::Message.req_pq_multi(nonce)
90
+ body = Type::AuthKey::ReqPqMulti.build(nonce)
91
+ message = Type::Message.new(auth_key_id: 0, msg_id: MessageId.generate, body: body)
84
92
  @connection.send(message.serialize)
85
93
 
86
94
  response_data = @connection.recv(timeout: @timeout)
87
- response_message = TL::Message.deserialize(response_data)
88
- res_pq = response_message.parse_res_pq
95
+ response_message = Type::Message.deserialize(response_data)
96
+ res_pq = Type::AuthKey::ResPq.parse(response_message.body)
89
97
 
90
98
  raise 'Nonce mismatch!' unless res_pq[:nonce] == nonce
91
99
 
@@ -106,7 +114,7 @@ module MTProto
106
114
  # For production DCs, use the DC number as-is
107
115
  dc_value = @test_mode ? (@dc_number + 10000) : @dc_number
108
116
 
109
- inner_data = TL::PQInnerData.new(
117
+ inner_data = Type::PQInnerData.new(
110
118
  pq: Crypto::Factorization.bytes_to_integer(res_pq[:pq]),
111
119
  p: p,
112
120
  q: q,
@@ -120,7 +128,7 @@ module MTProto
120
128
  end
121
129
 
122
130
  def send_req_dh_params(res_pq, p, q, server_key, encrypted_data)
123
- message = TL::Message.req_DH_params(
131
+ body = Type::AuthKey::ReqDHParams.build(
124
132
  nonce: res_pq[:nonce],
125
133
  server_nonce: res_pq[:server_nonce],
126
134
  p: p,
@@ -128,12 +136,13 @@ module MTProto
128
136
  public_key_fingerprint: server_key.fingerprint,
129
137
  encrypted_data: encrypted_data
130
138
  )
139
+ message = Type::Message.new(auth_key_id: 0, msg_id: MessageId.generate, body: body)
131
140
 
132
141
  @connection.send(message.serialize)
133
142
 
134
143
  response_data = @connection.recv(timeout: @timeout)
135
- response_message = TL::Message.deserialize(response_data)
136
- server_dh_params = response_message.parse_server_DH_params_ok
144
+ response_message = Type::Message.deserialize(response_data)
145
+ server_dh_params = Type::AuthKey::ServerDHParams.parse(response_message.body)
137
146
 
138
147
  raise 'Nonce mismatch!' unless server_dh_params[:nonce] == res_pq[:nonce]
139
148
  raise 'Server nonce mismatch!' unless server_dh_params[:server_nonce] == res_pq[:server_nonce]
@@ -163,7 +172,7 @@ module MTProto
163
172
  answer = answer[0..-2]
164
173
  end
165
174
 
166
- server_dh_inner_data = TL::ServerDHInnerData.deserialize(answer)
175
+ server_dh_inner_data = Type::ServerDHInnerData.deserialize(answer)
167
176
 
168
177
  raise 'Nonce mismatch in DH inner data!' unless server_dh_inner_data.nonce == res_pq[:nonce]
169
178
  raise 'Server nonce mismatch in DH inner data!' unless server_dh_inner_data.server_nonce == res_pq[:server_nonce]
@@ -172,7 +181,7 @@ module MTProto
172
181
  end
173
182
 
174
183
  def send_client_dh_params(res_pq, new_nonce, client_dh_params, tmp_aes_key, tmp_aes_iv)
175
- client_dh_inner_data = TL::ClientDHInnerData.new(
184
+ client_dh_inner_data = Type::ClientDHInnerData.new(
176
185
  nonce: res_pq[:nonce],
177
186
  server_nonce: res_pq[:server_nonce],
178
187
  retry_id: 0,
@@ -191,17 +200,18 @@ module MTProto
191
200
  tmp_aes_iv
192
201
  )
193
202
 
194
- message = TL::Message.set_client_DH_params(
203
+ body = Type::AuthKey::SetClientDHParams.build(
195
204
  nonce: res_pq[:nonce],
196
205
  server_nonce: res_pq[:server_nonce],
197
206
  encrypted_data: client_dh_encrypted
198
207
  )
208
+ message = Type::Message.new(auth_key_id: 0, msg_id: MessageId.generate, body: body)
199
209
 
200
210
  @connection.send(message.serialize)
201
211
 
202
212
  response_data = @connection.recv(timeout: @timeout)
203
- response_message = TL::Message.deserialize(response_data)
204
- response_message.parse_dh_gen_response
213
+ response_message = Type::Message.deserialize(response_data)
214
+ Type::AuthKey::DHGenResponse.parse(response_message.body)
205
215
  end
206
216
 
207
217
  def verify_dh_gen_response(dh_gen_response, res_pq, new_nonce, auth_key)
@@ -0,0 +1,165 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative '../message_id'
4
+
5
+ module MTProto
6
+ class Client
7
+ class RPC
8
+ CONSTRUCTOR_RPC_RESULT = 0xf35c6d01
9
+ CONSTRUCTOR_MSGS_ACK = 0x62d6b459
10
+ CONSTRUCTOR_BAD_SERVER_SALT = 0xedab447b
11
+
12
+ def initialize(client)
13
+ @client = client
14
+ @connection_initialized = false
15
+ end
16
+
17
+ def call(body, content_related: true)
18
+ raise 'Auth key not generated' unless @client.auth_key
19
+ raise 'Session not initialized' unless @client.session
20
+
21
+ msg_id = MessageId.generate(time_offset: @client.time_offset)
22
+ seq_no = @client.session.next_seq_no(content_related: content_related)
23
+
24
+ encrypted_msg = EncryptedMessage.encrypt(
25
+ auth_key: @client.auth_key,
26
+ server_salt: @client.server_salt,
27
+ session_id: @client.session.session_id,
28
+ msg_id: msg_id,
29
+ seq_no: seq_no,
30
+ body: body
31
+ )
32
+
33
+ @client.connection.send(encrypted_msg.serialize)
34
+
35
+ msg_id
36
+ end
37
+
38
+ def call_sync(body, content_related: true)
39
+ raise 'Auth key not generated' unless @client.auth_key
40
+ raise 'Session not initialized' unless @client.session
41
+
42
+ msg_id = MessageId.generate(time_offset: @client.time_offset)
43
+ seq_no = @client.session.next_seq_no(content_related: content_related)
44
+
45
+ encrypted_msg = EncryptedMessage.encrypt(
46
+ auth_key: @client.auth_key,
47
+ server_salt: @client.server_salt,
48
+ session_id: @client.session.session_id,
49
+ msg_id: msg_id,
50
+ seq_no: seq_no,
51
+ body: body
52
+ )
53
+
54
+ @client.connection.send(encrypted_msg.serialize)
55
+
56
+ response_data = @client.connection.recv(timeout: @client.timeout)
57
+
58
+ decrypted = EncryptedMessage.decrypt(
59
+ auth_key: @client.auth_key,
60
+ encrypted_message_data: response_data,
61
+ sender: :server
62
+ )
63
+
64
+ response_body = decrypted[:body]
65
+
66
+ constructor = response_body[0, 4].unpack1('L<')
67
+
68
+ if constructor == CONSTRUCTOR_BAD_SERVER_SALT
69
+ offset = 4
70
+ offset += 8
71
+ offset += 4
72
+ offset += 4
73
+ new_server_salt = response_body[offset, 8].unpack1('Q<')
74
+
75
+ update_server_salt(new_server_salt)
76
+ return call_sync(body, content_related: content_related)
77
+ end
78
+
79
+ if constructor == Type::NewSessionCreated::CONSTRUCTOR
80
+ session_info = Type::NewSessionCreated.deserialize(response_body)
81
+ update_server_salt(session_info.server_salt)
82
+
83
+ response_data = @client.connection.recv(timeout: @client.timeout)
84
+ decrypted = EncryptedMessage.decrypt(
85
+ auth_key: @client.auth_key,
86
+ encrypted_message_data: response_data,
87
+ sender: :server
88
+ )
89
+ response_body = decrypted[:body]
90
+ constructor = response_body[0, 4].unpack1('L<')
91
+ end
92
+
93
+ if constructor == Type::MsgContainer::CONSTRUCTOR
94
+ container = Type::MsgContainer.deserialize(response_body)
95
+
96
+ rpc_result = container.messages.find do |msg|
97
+ msg[:body][0, 4].unpack1('L<') == CONSTRUCTOR_RPC_RESULT
98
+ end
99
+
100
+ return extract_rpc_result(rpc_result[:body][12..]) if rpc_result
101
+
102
+ new_session = container.messages.find do |msg|
103
+ msg[:body][0, 4].unpack1('L<') == Type::NewSessionCreated::CONSTRUCTOR
104
+ end
105
+ if new_session
106
+ session_info = Type::NewSessionCreated.deserialize(new_session[:body])
107
+ update_server_salt(session_info.server_salt)
108
+
109
+ other_messages = container.messages.reject do |msg|
110
+ constructor = msg[:body][0, 4].unpack1('L<')
111
+ (
112
+ constructor == Type::NewSessionCreated::CONSTRUCTOR ||
113
+ constructor == CONSTRUCTOR_MSGS_ACK
114
+ )
115
+ end
116
+
117
+ return other_messages.first[:body] unless other_messages.empty?
118
+
119
+ response_data = @client.connection.recv(timeout: @client.timeout)
120
+ decrypted = EncryptedMessage.decrypt(
121
+ auth_key: @client.auth_key,
122
+ encrypted_message_data: response_data,
123
+ sender: :server
124
+ )
125
+ response_body = decrypted[:body]
126
+ constructor = response_body[0, 4].unpack1('L<')
127
+
128
+ return extract_rpc_result(response_body[12..]) if constructor == CONSTRUCTOR_RPC_RESULT
129
+
130
+ if constructor == Type::MsgContainer::CONSTRUCTOR
131
+ container = Type::MsgContainer.deserialize(response_body)
132
+ rpc_result = container.messages.find do |msg|
133
+ msg[:body][0, 4].unpack1('L<') == CONSTRUCTOR_RPC_RESULT
134
+ end
135
+ return extract_rpc_result(rpc_result[:body][12..]) if rpc_result
136
+ end
137
+
138
+ return response_body
139
+ end
140
+
141
+ return container.messages.first[:body]
142
+ end
143
+
144
+ return extract_rpc_result(response_body[12..]) if constructor == CONSTRUCTOR_RPC_RESULT
145
+
146
+ response_body
147
+ end
148
+
149
+ private
150
+
151
+ def extract_rpc_result(result)
152
+ result_constructor = result[0, 4].unpack1('L<')
153
+ if result_constructor == Type::RpcError::CONSTRUCTOR
154
+ error = Type::RpcError.deserialize(result)
155
+ raise MTProto::RpcError.new(error.error_code, error.error_message)
156
+ end
157
+ result
158
+ end
159
+
160
+ def update_server_salt(server_salt)
161
+ @client.instance_variable_set(:@server_salt, server_salt)
162
+ end
163
+ end
164
+ end
165
+ end