mtproto 0.0.5 → 0.0.6

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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: bd33fd4bcb039dfffa0a6d2c2193e05fdf306a24c8f34a09b197d8aa45dd9117
4
- data.tar.gz: 2061be3cf7378f0b86cff34eae3c34fa025bfdb252f61bc99307489437b9ab63
3
+ metadata.gz: e7f507f0d6cf42ac479bf224933efca12e7519cd67d7e9d8796ceba14f517960
4
+ data.tar.gz: 79c5137adfc60752fecb5367c6b90a08e7648f8fc523a95382fe7e755888c439
5
5
  SHA512:
6
- metadata.gz: cb9c3c9c4d59755b3e4693e475f51295476caaeb932239439ff8e0940484bb8fe0dd3420282fd6d2f807b8da6755737ece84d0bf464bd5554e6190dcdebc3845
7
- data.tar.gz: dad6e992b30ca7a7e9a20f9d027bd6436a6b90ab6721a45cb36b8ff3080d4e58633d1d50aa2a54c41568a048e93f7b2b9badc4fd7ad3fd361416a3b428da1b18
6
+ metadata.gz: 48de4193ba146630f831f690527cefcc0577c1bf56ab32452092e68dd75fa5bf86b294b8578e88e4daa26766c39ef78aa60c1a132e38975d6b01f0c21c508f29
7
+ data.tar.gz: 46d2ad65c9c1f59ea5f75789fb27008620117ab63877dad711add23cb6d477ad9cea5e5dffb724cc4d1d128a67dd9b75f8db9eddaf8e30a16005c377cb1a7542
data/.env.example ADDED
@@ -0,0 +1,5 @@
1
+ TG_API_ID=your_api_id
2
+ TG_API_HASH=your_api_hash
3
+ TG_TEST_DC='111.111.111.111:443'
4
+ TG_TEST_DC_N='1'
5
+ TG_TEST_DC_KEY='DC public key'
data/Rakefile CHANGED
@@ -14,7 +14,20 @@ end
14
14
 
15
15
  RSpec::Core::RakeTask.new(:spec) do |t|
16
16
  t.rspec_opts = '--require spec_helper'
17
+ t.pattern = 'spec/**/*_spec.rb'
18
+ end
19
+
20
+ RSpec::Core::RakeTask.new('spec:unit') do |t|
21
+ t.rspec_opts = '--require spec_helper'
22
+ t.pattern = 'spec/lib/**/*_spec.rb'
23
+ end
24
+
25
+ RSpec::Core::RakeTask.new('spec:integration') do |t|
26
+ t.rspec_opts = '--require spec_helper'
27
+ t.pattern = 'spec/integration/**/*_spec.rb'
17
28
  end
18
29
 
19
30
  task spec: :compile
31
+ task 'spec:unit': :compile
32
+ task 'spec:integration': :compile
20
33
  task default: :spec
@@ -5,10 +5,17 @@ require 'digest'
5
5
 
6
6
  module MTProto
7
7
  class AuthKeyGenerator
8
- attr_reader :connection, :auth_key, :server_salt, :time_offset
8
+ attr_reader :connection, :auth_key, :server_salt, :time_offset, :timeout
9
+
10
+ def initialize(connection, public_key, dc_number = nil, test_mode: false, timeout: 10)
11
+ raise ArgumentError, "public_key is required" if public_key.nil? || public_key.empty?
12
+ raise ArgumentError, "dc_number must be positive. Use test_mode: true for test DCs" if dc_number && dc_number < 0
9
13
 
10
- def initialize(connection)
11
14
  @connection = connection
15
+ @public_key = public_key
16
+ @dc_number = dc_number
17
+ @test_mode = test_mode
18
+ @timeout = timeout
12
19
  @auth_key = nil
13
20
  @server_salt = nil
14
21
  @time_offset = 0
@@ -76,7 +83,7 @@ module MTProto
76
83
  message = TL::Message.req_pq_multi(nonce)
77
84
  @connection.send(message.serialize)
78
85
 
79
- response_data = @connection.recv(timeout: 10)
86
+ response_data = @connection.recv(timeout: @timeout)
80
87
  response_message = TL::Message.deserialize(response_data)
81
88
  res_pq = response_message.parse_res_pq
82
89
 
@@ -86,13 +93,19 @@ module MTProto
86
93
  end
87
94
 
88
95
  def find_server_key(fingerprints)
89
- server_key = Crypto::RSAKey.find_by_fingerprint(fingerprints)
96
+ server_key = Crypto::RSAKey.find_by_fingerprint(fingerprints, @public_key)
90
97
  raise 'No matching RSA key found!' unless server_key
91
98
 
92
99
  server_key
93
100
  end
94
101
 
95
102
  def encrypt_pq_inner_data(res_pq, p, q, server_key, new_nonce)
103
+ raise ArgumentError, "dc_number is required for auth key generation" if @dc_number.nil?
104
+
105
+ # For test DCs, add 10000 to the DC number per MTProto spec
106
+ # For production DCs, use the DC number as-is
107
+ dc_value = @test_mode ? (@dc_number + 10000) : @dc_number
108
+
96
109
  inner_data = TL::PQInnerData.new(
97
110
  pq: Crypto::Factorization.bytes_to_integer(res_pq[:pq]),
98
111
  p: p,
@@ -100,7 +113,7 @@ module MTProto
100
113
  nonce: res_pq[:nonce],
101
114
  server_nonce: res_pq[:server_nonce],
102
115
  new_nonce: new_nonce,
103
- dc: @connection.port == 443 ? 2 : 1
116
+ dc: dc_value
104
117
  )
105
118
 
106
119
  Crypto::RSA_PAD.encrypt(inner_data.serialize, server_key)
@@ -118,7 +131,7 @@ module MTProto
118
131
 
119
132
  @connection.send(message.serialize)
120
133
 
121
- response_data = @connection.recv(timeout: 10)
134
+ response_data = @connection.recv(timeout: @timeout)
122
135
  response_message = TL::Message.deserialize(response_data)
123
136
  server_dh_params = response_message.parse_server_DH_params_ok
124
137
 
@@ -186,7 +199,7 @@ module MTProto
186
199
 
187
200
  @connection.send(message.serialize)
188
201
 
189
- response_data = @connection.recv(timeout: 10)
202
+ response_data = @connection.recv(timeout: @timeout)
190
203
  response_message = TL::Message.deserialize(response_data)
191
204
  response_message.parse_dh_gen_response
192
205
  end
@@ -8,36 +8,45 @@ require_relative 'tl/message'
8
8
 
9
9
  module MTProto
10
10
  class Client
11
- DC_ADDRESSES = {
12
- 1 => ['149.154.175.50', 443],
13
- 2 => ['149.154.167.51', 443],
14
- 3 => ['149.154.175.100', 443],
15
- 4 => ['149.154.167.91', 443],
16
- 5 => ['91.108.56.130', 443]
17
- }.freeze
11
+ attr_reader :connection, :server_key, :auth_key, :server_salt, :time_offset, :session, :timeout
12
+ attr_accessor :api_id, :api_hash, :device_model, :system_version, :app_version, :system_lang_code, :lang_pack, :lang_code
18
13
 
19
- attr_reader :connection, :server_key, :auth_key, :server_salt, :time_offset, :session
14
+ CONSTRUCTOR_MSGS_ACK = 0x62d6b459
20
15
 
21
- def initialize(dc_id: 2)
22
- host, port = DC_ADDRESSES[dc_id]
23
- raise ArgumentError, "Unknown DC ID: #{dc_id}" unless host
16
+ def initialize(api_id: nil, api_hash: nil, host:, port: 443, public_key: nil, dc_number: nil, test_mode: false, timeout: 10)
17
+ raise ArgumentError, "host is required" if host.nil? || host.empty?
18
+ raise ArgumentError, "dc_number must be positive. Use test_mode: true for test DCs" if dc_number && dc_number < 0
24
19
 
25
20
  codec = Transport::AbridgedPacketCodec.new
26
21
  @connection = Transport::TCPConnection.new(host, port, codec)
22
+ @public_key = public_key
23
+ @dc_number = dc_number
24
+ @test_mode = test_mode
25
+ @timeout = timeout
27
26
  @server_key = nil
28
27
  @auth_key = nil
29
28
  @server_salt = nil
30
29
  @time_offset = 0
31
30
  @session = nil
32
31
  @connection_initialized = false
32
+
33
+ # Client configuration defaults
34
+ @api_id = api_id || 0
35
+ @api_hash = api_hash || ''
36
+ @device_model = 'Ruby MTProto'
37
+ @system_version = RUBY_DESCRIPTION
38
+ @app_version = '0.1.0'
39
+ @system_lang_code = 'en'
40
+ @lang_pack = ''
41
+ @lang_code = 'en'
33
42
  end
34
43
 
35
- def connect
36
- @connection.connect
44
+ def connect!
45
+ @connection.connect!
37
46
  end
38
47
 
39
- def disconnect
40
- @connection.close
48
+ def disconnect!
49
+ @connection.close if @connection
41
50
  end
42
51
 
43
52
  def req_pq_multi
@@ -48,7 +57,7 @@ module MTProto
48
57
 
49
58
  @connection.send(payload)
50
59
 
51
- response_data = @connection.recv(timeout: 10)
60
+ response_data = @connection.recv(timeout: @timeout)
52
61
  response_message = TL::Message.deserialize(response_data)
53
62
 
54
63
  res_pq = response_message.parse_res_pq
@@ -59,7 +68,9 @@ module MTProto
59
68
  end
60
69
 
61
70
  def make_auth_key
62
- generator = AuthKeyGenerator.new(@connection)
71
+ raise ArgumentError, "public_key is required for auth key generation" if @public_key.nil? || @public_key.empty?
72
+
73
+ generator = AuthKeyGenerator.new(@connection, @public_key, @dc_number, test_mode: @test_mode, timeout: @timeout)
63
74
  result = generator.generate
64
75
 
65
76
  @auth_key = generator.auth_key
@@ -94,7 +105,7 @@ module MTProto
94
105
 
95
106
  @connection.send(encrypted_msg.serialize)
96
107
 
97
- response_data = @connection.recv(timeout: 10)
108
+ response_data = @connection.recv(timeout: @timeout)
98
109
 
99
110
  decrypted = EncryptedMessage.decrypt(
100
111
  auth_key: @auth_key,
@@ -105,16 +116,12 @@ module MTProto
105
116
  response_body = decrypted[:body]
106
117
 
107
118
  constructor = response_body[0, 4].unpack1('L<')
108
- puts " [RPC] First response constructor: 0x#{constructor.to_s(16)} (#{response_body.bytesize} bytes)"
109
119
 
110
120
  if constructor == TL::NewSessionCreated::CONSTRUCTOR
111
- puts " [RPC] Got new_session_created, waiting for actual response..."
112
121
  session_info = TL::NewSessionCreated.deserialize(response_body)
113
122
  @server_salt = session_info.server_salt
114
- puts " [RPC] Updated server_salt to: 0x#{@server_salt.to_s(16)}"
115
123
 
116
- response_data = @connection.recv(timeout: 10)
117
- puts " [RPC] Second response received (#{response_data.bytesize} bytes encrypted)"
124
+ response_data = @connection.recv(timeout: @timeout)
118
125
  decrypted = EncryptedMessage.decrypt(
119
126
  auth_key: @auth_key,
120
127
  encrypted_message_data: response_data,
@@ -122,37 +129,32 @@ module MTProto
122
129
  )
123
130
  response_body = decrypted[:body]
124
131
  constructor = response_body[0, 4].unpack1('L<')
125
- puts " [RPC] Second response constructor: 0x#{constructor.to_s(16)} (#{response_body.bytesize} bytes)"
126
132
  end
127
133
 
128
134
  if constructor == TL::Message::CONSTRUCTOR_MSG_CONTAINER
129
- puts " [RPC] Response is a container, unpacking..."
130
135
  container = TL::MsgContainer.deserialize(response_body)
131
- puts " [RPC] Container has #{container.messages.size} messages"
132
-
133
- container.messages.each_with_index do |msg, i|
134
- msg_constructor = msg[:body][0, 4].unpack1('L<')
135
- puts " [RPC] Message #{i}: constructor=0x#{msg_constructor.to_s(16)}, size=#{msg[:body].bytesize}"
136
- end
137
136
 
138
137
  rpc_result = container.messages.find do |msg|
139
138
  msg[:body][0, 4].unpack1('L<') == 0xf35c6d01
140
139
  end
141
140
 
142
- if rpc_result
143
- puts " [RPC] Found rpc_result in container, extracting..."
144
- return rpc_result[:body][12..]
145
- end
141
+ return rpc_result[:body][12..] if rpc_result
146
142
 
147
143
  new_session = container.messages.find { |msg| msg[:body][0, 4].unpack1('L<') == TL::NewSessionCreated::CONSTRUCTOR }
148
144
  if new_session
149
- puts " [RPC] Container has new_session_created, updating salt and waiting for RPC result..."
150
145
  session_info = TL::NewSessionCreated.deserialize(new_session[:body])
151
146
  @server_salt = session_info.server_salt
152
- puts " [RPC] Updated server_salt to: 0x#{@server_salt.to_s(16)}"
153
147
 
154
- response_data = @connection.recv(timeout: 10)
155
- puts " [RPC] Next response received (#{response_data.bytesize} bytes encrypted)"
148
+ other_messages = container.messages.reject do |msg|
149
+ constructor = msg[:body][0, 4].unpack1('L<')
150
+ constructor == TL::NewSessionCreated::CONSTRUCTOR || constructor == CONSTRUCTOR_MSGS_ACK
151
+ end
152
+
153
+ if !other_messages.empty?
154
+ return other_messages.first[:body]
155
+ end
156
+
157
+ response_data = @connection.recv(timeout: @timeout)
156
158
  decrypted = EncryptedMessage.decrypt(
157
159
  auth_key: @auth_key,
158
160
  encrypted_message_data: response_data,
@@ -160,15 +162,10 @@ module MTProto
160
162
  )
161
163
  response_body = decrypted[:body]
162
164
  constructor = response_body[0, 4].unpack1('L<')
163
- puts " [RPC] Next response constructor: 0x#{constructor.to_s(16)} (#{response_body.bytesize} bytes)"
164
165
 
165
- if constructor == 0xf35c6d01
166
- puts " [RPC] Next response is rpc_result, extracting payload..."
167
- return response_body[12..]
168
- end
166
+ return response_body[12..] if constructor == 0xf35c6d01
169
167
 
170
168
  if constructor == TL::Message::CONSTRUCTOR_MSG_CONTAINER
171
- puts " [RPC] Next response is also a container, looking for rpc_result..."
172
169
  container = TL::MsgContainer.deserialize(response_body)
173
170
  rpc_result = container.messages.find { |msg| msg[:body][0, 4].unpack1('L<') == 0xf35c6d01 }
174
171
  return rpc_result[:body][12..] if rpc_result
@@ -177,16 +174,11 @@ module MTProto
177
174
  return response_body
178
175
  end
179
176
 
180
- puts " [RPC] No rpc_result found, returning first message body"
181
177
  return container.messages.first[:body]
182
178
  end
183
179
 
184
- if constructor == 0xf35c6d01
185
- puts " [RPC] Response is rpc_result, extracting payload..."
186
- return response_body[12..]
187
- end
180
+ return response_body[12..] if constructor == 0xf35c6d01
188
181
 
189
- puts " [RPC] Returning raw response body (constructor: 0x#{constructor.to_s(16)})"
190
182
  response_body
191
183
  end
192
184
 
@@ -197,7 +189,7 @@ module MTProto
197
189
  body
198
190
  end
199
191
 
200
- def init_connection(api_id, device_model, system_version, app_version, system_lang_code, lang_code, query)
192
+ def init_connection(api_id:, device_model:, system_version:, app_version:, system_lang_code:, lang_pack:, lang_code:, query:)
201
193
  body = TL::Serializer.serialize_int(0xc1cd5ea9)
202
194
  flags = 0
203
195
  body += TL::Serializer.serialize_int(flags)
@@ -206,7 +198,7 @@ module MTProto
206
198
  body += TL::Serializer.serialize_string(system_version)
207
199
  body += TL::Serializer.serialize_string(app_version)
208
200
  body += TL::Serializer.serialize_string(system_lang_code)
209
- body += TL::Serializer.serialize_string('')
201
+ body += TL::Serializer.serialize_string(lang_pack)
210
202
  body += TL::Serializer.serialize_string(lang_code)
211
203
  body += query
212
204
  body
@@ -217,13 +209,41 @@ module MTProto
217
209
 
218
210
  unless @connection_initialized
219
211
  query = init_connection(
220
- 123456,
221
- 'Ruby MTProto',
222
- 'Ruby 3.x',
223
- '0.1.0',
224
- 'en',
225
- 'en',
226
- query
212
+ api_id: @api_id,
213
+ device_model: @device_model,
214
+ system_version: @system_version,
215
+ app_version: @app_version,
216
+ system_lang_code: @system_lang_code,
217
+ lang_pack: @lang_pack,
218
+ lang_code: @lang_code,
219
+ query: query
220
+ )
221
+ query = invoke_with_layer(214, query)
222
+ @connection_initialized = true
223
+ end
224
+
225
+ rpc_call(query)
226
+ end
227
+
228
+ def auth_send_code(phone_number, code_settings: {})
229
+ raise ArgumentError, 'phone_number is required' if phone_number.nil? || phone_number.empty?
230
+
231
+ query = [0xa677244f].pack('L<')
232
+ query += TL::Serializer.serialize_string(phone_number)
233
+ query += TL::Serializer.serialize_int(@api_id)
234
+ query += TL::Serializer.serialize_string(@api_hash)
235
+ query += TL::CodeSettings.serialize(code_settings)
236
+
237
+ unless @connection_initialized
238
+ query = init_connection(
239
+ api_id: @api_id,
240
+ device_model: @device_model,
241
+ system_version: @system_version,
242
+ app_version: @app_version,
243
+ system_lang_code: @system_lang_code,
244
+ lang_pack: @lang_pack,
245
+ lang_code: @lang_code,
246
+ query: query
227
247
  )
228
248
  query = invoke_with_layer(214, query)
229
249
  @connection_initialized = true
@@ -0,0 +1,103 @@
1
+ # frozen_string_literal: true
2
+
3
+ module MTProto
4
+ class Connection
5
+ attr_reader :client
6
+
7
+ def initialize(api_id: nil, api_hash: nil, host:, port: 443, public_key:, dc_number:, test_mode: false, timeout: 10)
8
+ @client = Client.new(
9
+ api_id: api_id,
10
+ api_hash: api_hash,
11
+ host: host,
12
+ port: port,
13
+ public_key: public_key,
14
+ dc_number: dc_number,
15
+ test_mode: test_mode,
16
+ timeout: timeout
17
+ )
18
+ @connected = false
19
+ end
20
+
21
+ def connect!
22
+ return if @connected
23
+
24
+ @client.connect!
25
+ @client.make_auth_key
26
+ @connected = true
27
+ end
28
+
29
+ def disconnect!
30
+ @client.disconnect! if @connected
31
+ @connected = false
32
+ end
33
+
34
+ def ping(ping_id = nil)
35
+ raise NotConnectedError unless @connected
36
+
37
+ ping_id ||= rand(2**63)
38
+ body = TL::Message.ping(ping_id)
39
+
40
+ response_body = @client.rpc_call(body)
41
+ message = TL::Message.new(body: response_body)
42
+ pong = message.parse_pong
43
+
44
+ raise PingMismatchError unless pong[:ping_id] == ping_id
45
+
46
+ true
47
+ end
48
+
49
+ def get_config
50
+ raise NotConnectedError unless @connected
51
+
52
+ response = @client.help_get_config
53
+
54
+ # Handle gzip compression
55
+ constructor = response[0, 4].unpack1('L<')
56
+ if constructor == TL::GzipPacked::CONSTRUCTOR
57
+ response = TL::GzipPacked.unpack(response)
58
+ constructor = response[0, 4].unpack1('L<')
59
+ end
60
+
61
+ # Handle RPC errors
62
+ if constructor == TL::RpcError::CONSTRUCTOR
63
+ error = TL::RpcError.deserialize(response)
64
+ raise MTProto::RpcError.new(error.error_code, error.error_message)
65
+ end
66
+
67
+ # Parse and return config
68
+ if constructor == TL::Config::CONSTRUCTOR || constructor == TL::Config::CONSTRUCTOR_ALT
69
+ TL::Config.deserialize(response)
70
+ else
71
+ raise UnexpectedConstructorError.new(constructor)
72
+ end
73
+ end
74
+
75
+ def send_code(phone_number, code_settings: {})
76
+ raise NotConnectedError unless @connected
77
+ raise ArgumentError, 'phone_number is required' if phone_number.nil? || phone_number.empty?
78
+
79
+ response = @client.auth_send_code(phone_number, code_settings: code_settings)
80
+
81
+ # Handle gzip compression
82
+ constructor = response[0, 4].unpack1('L<')
83
+ if constructor == TL::GzipPacked::CONSTRUCTOR
84
+ response = TL::GzipPacked.unpack(response)
85
+ constructor = response[0, 4].unpack1('L<')
86
+ end
87
+
88
+ # Handle RPC errors
89
+ if constructor == TL::RpcError::CONSTRUCTOR
90
+ error = TL::RpcError.deserialize(response)
91
+ raise MTProto::RpcError.new(error.error_code, error.error_message)
92
+ end
93
+
94
+ # Parse and return sent code
95
+ if constructor == TL::SentCode::CONSTRUCTOR
96
+ sent_code = TL::SentCode.deserialize(response)
97
+ sent_code.to_h
98
+ else
99
+ raise UnexpectedConstructorError.new(constructor)
100
+ end
101
+ end
102
+ end
103
+ end
@@ -6,30 +6,24 @@ require 'digest'
6
6
  module MTProto
7
7
  module Crypto
8
8
  class RSAKey
9
- TELEGRAM_KEY = <<~PEM
10
- -----BEGIN RSA PUBLIC KEY-----
11
- MIIBCgKCAQEA6LszBcC1LGzyr992NzE0ieY+BSaOW622Aa9Bd4ZHLl+TuFQ4lo4g
12
- 5nKaMBwK/BIb9xUfg0Q29/2mgIR6Zr9krM7HjuIcCzFvDtr+L0GQjae9H0pRB2OO
13
- 62cECs5HKhT5DZ98K33vmWiLowc621dQuwKWSQKjWf50XYFw42h21P2KXUGyp2y/
14
- +aEyZ+uVgLLQbRA1dEjSDZ2iGRy12Mk5gpYc397aYp438fsJoHIgJ2lgMv5h7WY9
15
- t6N/byY9Nw9p21Og3AoXSL2q/2IJ1WRUhebgAdGVMlV1fkuOQoEzR7EdpqtQD9Cs
16
- 5+bfo3Nhmcyvk5ftB0WkJ9z6bNZ7yxrP8wIDAQAB
17
- -----END RSA PUBLIC KEY-----
18
- PEM
19
-
20
9
  attr_reader :key, :fingerprint
21
10
 
22
11
  def initialize(pem_string)
12
+ raise ArgumentError, "pem_string is required" if pem_string.nil? || pem_string.empty?
13
+
23
14
  @key = OpenSSL::PKey::RSA.new(pem_string)
24
15
  @fingerprint = calculate_fingerprint
25
16
  end
26
17
 
27
- def self.telegram_key
28
- @telegram_key ||= new(TELEGRAM_KEY)
18
+ def self.from_pem(pem_string)
19
+ new(pem_string)
29
20
  end
30
21
 
31
- def self.find_by_fingerprint(fingerprints)
32
- telegram_key if fingerprints.include?(telegram_key.fingerprint)
22
+ def self.find_by_fingerprint(fingerprints, public_key)
23
+ raise ArgumentError, "public_key is required" if public_key.nil? || public_key.empty?
24
+
25
+ key = new(public_key)
26
+ key if fingerprints.include?(key.fingerprint)
33
27
  end
34
28
 
35
29
  private
@@ -0,0 +1,33 @@
1
+ # frozen_string_literal: true
2
+
3
+ module MTProto
4
+ class Error < StandardError; end
5
+
6
+ class NotConnectedError < Error
7
+ def initialize(msg = 'Not connected. Call connect first.')
8
+ super
9
+ end
10
+ end
11
+
12
+ class PingMismatchError < Error
13
+ def initialize(msg = 'Ping ID mismatch')
14
+ super
15
+ end
16
+ end
17
+
18
+ class RpcError < Error
19
+ attr_reader :error_code, :error_message
20
+
21
+ def initialize(error_code, error_message)
22
+ @error_code = error_code
23
+ @error_message = error_message
24
+ super("RPC Error #{error_code}: #{error_message}")
25
+ end
26
+ end
27
+
28
+ class UnexpectedConstructorError < Error
29
+ def initialize(constructor)
30
+ super("Unexpected constructor: 0x#{constructor.to_s(16)}")
31
+ end
32
+ end
33
+ end
@@ -0,0 +1,25 @@
1
+ # frozen_string_literal: true
2
+
3
+ module MTProto
4
+ module TL
5
+ class CodeSettings
6
+ CONSTRUCTOR = 0xad253d78
7
+
8
+ def self.serialize(settings = {})
9
+ body = Serializer.serialize_int(CONSTRUCTOR)
10
+
11
+ flags = 0
12
+ flags |= (1 << 0) if settings[:allow_flashcall]
13
+ flags |= (1 << 1) if settings[:current_number]
14
+ flags |= (1 << 4) if settings[:allow_app_hash]
15
+ flags |= (1 << 5) if settings[:allow_missed_call]
16
+ flags |= (1 << 7) if settings[:allow_firebase]
17
+ flags |= (1 << 9) if settings[:unknown_number]
18
+
19
+ body += Serializer.serialize_int(flags)
20
+
21
+ body
22
+ end
23
+ end
24
+ end
25
+ end
@@ -10,6 +10,7 @@ module MTProto
10
10
 
11
11
  def self.deserialize(data)
12
12
  offset = 4
13
+
13
14
  flags = data[offset, 4].unpack1('L<')
14
15
  offset += 4
15
16
 
@@ -28,7 +29,7 @@ module MTProto
28
29
  dc_options_constructor = data[offset, 4].unpack1('L<')
29
30
  offset += 4
30
31
 
31
- raise "Expected vector constructor" unless dc_options_constructor == 0x1cb5c415
32
+ raise "Expected vector constructor 0x1cb5c415, got 0x#{dc_options_constructor.to_s(16)}" unless dc_options_constructor == 0x1cb5c415
32
33
 
33
34
  dc_options_count = data[offset, 4].unpack1('L<')
34
35
  offset += 4
@@ -65,10 +66,11 @@ module MTProto
65
66
 
66
67
  def self.deserialize_from(data)
67
68
  offset = 0
69
+
68
70
  constructor = data[offset, 4].unpack1('L<')
69
71
  offset += 4
70
72
 
71
- raise "Expected dcOption constructor" unless constructor == 0x18b7a10d
73
+ raise "Expected dcOption constructor 0x18b7a10d, got 0x#{constructor.to_s(16)}" unless constructor == 0x18b7a10d
72
74
 
73
75
  flags = data[offset, 4].unpack1('L<')
74
76
  offset += 4
@@ -34,7 +34,7 @@ module MTProto
34
34
  compressed_data = data[offset, length]
35
35
  raise "Not enough data: expected #{length}, got #{compressed_data&.bytesize}" if compressed_data.nil? || compressed_data.bytesize < length
36
36
 
37
- Zlib::GzipReader.new(StringIO.new(compressed_data)).read
37
+ Zlib::GzipReader.new(StringIO.new(compressed_data)).read.force_encoding(Encoding::BINARY)
38
38
  end
39
39
  end
40
40
  end
@@ -74,6 +74,12 @@ module MTProto
74
74
  end
75
75
 
76
76
  def self.deserialize(data)
77
+ if data.bytesize < 20
78
+ raise(ArgumentError,
79
+ "Invalid MTProto message: expected at least 20 bytes, got #{data.bytesize} bytes (hex: #{data.unpack1('H*')})",
80
+ )
81
+ end
82
+
77
83
  auth_key_id = data[0, 8].unpack1('Q<')
78
84
  msg_id = data[8, 8].unpack1('Q<')
79
85
  body_length = data[16, 4].unpack1('L<')
@@ -0,0 +1,128 @@
1
+ # frozen_string_literal: true
2
+
3
+ module MTProto
4
+ module TL
5
+ class SentCode
6
+ CONSTRUCTOR = 0x5e002502
7
+
8
+ attr_reader :flags, :type, :phone_code_hash, :next_type, :timeout
9
+
10
+ def self.deserialize(data)
11
+ offset = 4
12
+
13
+ flags = data[offset, 4].unpack1('L<')
14
+ offset += 4
15
+
16
+ type, type_bytes_read = SentCodeType.deserialize_from(data[offset..])
17
+ offset += type_bytes_read
18
+
19
+ phone_code_hash_length = data[offset].ord
20
+ offset += 1
21
+
22
+ phone_code_hash = data[offset, phone_code_hash_length]
23
+ offset += phone_code_hash_length
24
+
25
+ padding = (4 - ((phone_code_hash_length + 1) % 4)) % 4
26
+ offset += padding
27
+
28
+ next_type = nil
29
+ if (flags & (1 << 1)) != 0
30
+ next_type, next_type_bytes_read = CodeType.deserialize_from(data[offset..])
31
+ offset += next_type_bytes_read
32
+ end
33
+
34
+ timeout = nil
35
+ if (flags & (1 << 2)) != 0
36
+ timeout = data[offset, 4].unpack1('L<')
37
+ end
38
+
39
+ new(
40
+ flags: flags,
41
+ type: type,
42
+ phone_code_hash: phone_code_hash,
43
+ next_type: next_type,
44
+ timeout: timeout
45
+ )
46
+ end
47
+
48
+ def initialize(flags:, type:, phone_code_hash:, next_type: nil, timeout: nil)
49
+ @flags = flags
50
+ @type = type
51
+ @phone_code_hash = phone_code_hash
52
+ @next_type = next_type
53
+ @timeout = timeout
54
+ end
55
+
56
+ def to_h
57
+ {
58
+ phone_code_hash: @phone_code_hash,
59
+ type: @type,
60
+ next_type: @next_type,
61
+ timeout: @timeout
62
+ }.compact
63
+ end
64
+ end
65
+
66
+ module SentCodeType
67
+ def self.deserialize_from(data)
68
+ constructor = data[0, 4].unpack1('L<')
69
+ offset = 4
70
+
71
+ case constructor
72
+ when 0x3dbb5986 # auth.sentCodeTypeApp
73
+ length = data[offset, 4].unpack1('L<')
74
+ offset += 4
75
+ [{ _: :sent_code_type_app, length: length }, offset]
76
+ when 0xc000bba2 # auth.sentCodeTypeSms
77
+ length = data[offset, 4].unpack1('L<')
78
+ offset += 4
79
+ [{ _: :sent_code_type_sms, length: length }, offset]
80
+ when 0x5353e5a7 # auth.sentCodeTypeCall
81
+ length = data[offset, 4].unpack1('L<')
82
+ offset += 4
83
+ [{ _: :sent_code_type_call, length: length }, offset]
84
+ when 0xab03c6d9 # auth.sentCodeTypeFlashCall
85
+ pattern_length = data[offset].ord
86
+ offset += 1
87
+ pattern = data[offset, pattern_length]
88
+ offset += pattern_length
89
+ padding = (4 - ((pattern_length + 1) % 4)) % 4
90
+ offset += padding
91
+ [{ _: :sent_code_type_flash_call, pattern: pattern }, offset]
92
+ when 0x82006484 # auth.sentCodeTypeMissedCall
93
+ prefix_length = data[offset].ord
94
+ offset += 1
95
+ prefix = data[offset, prefix_length]
96
+ offset += prefix_length
97
+ padding = (4 - ((prefix_length + 1) % 4)) % 4
98
+ offset += padding
99
+ length = data[offset, 4].unpack1('L<')
100
+ offset += 4
101
+ [{ _: :sent_code_type_missed_call, prefix: prefix, length: length }, offset]
102
+ else
103
+ raise "Unknown SentCodeType constructor: 0x#{constructor.to_s(16)}"
104
+ end
105
+ end
106
+ end
107
+
108
+ module CodeType
109
+ def self.deserialize_from(data)
110
+ constructor = data[0, 4].unpack1('L<')
111
+ offset = 4
112
+
113
+ case constructor
114
+ when 0x72a3158c # codeTypeSms
115
+ [{ _: :code_type_sms }, offset]
116
+ when 0x741cd3e3 # codeTypeCall
117
+ [{ _: :code_type_call }, offset]
118
+ when 0x226ccefb # codeTypeFlashCall
119
+ [{ _: :code_type_flash_call }, offset]
120
+ when 0xd61ad6ee # codeTypeMissedCall
121
+ [{ _: :code_type_missed_call }, offset]
122
+ else
123
+ raise "Unknown CodeType constructor: 0x#{constructor.to_s(16)}"
124
+ end
125
+ end
126
+ end
127
+ end
128
+ end
@@ -17,7 +17,7 @@ module MTProto
17
17
  @socket = nil
18
18
  end
19
19
 
20
- def connect
20
+ def connect!
21
21
  return if connected?
22
22
 
23
23
  @socket = TCPSocket.new(@host, @port)
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module MTProto
4
- VERSION = '0.0.5'
4
+ VERSION = '0.0.6'
5
5
  end
data/lib/mtproto.rb CHANGED
@@ -1,6 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require_relative 'mtproto/version'
4
+ require_relative 'mtproto/errors'
4
5
  require_relative 'mtproto/transport/abridged_packet_codec'
5
6
  require_relative 'mtproto/transport/tcp_connection'
6
7
  require_relative 'mtproto/tl/message'
@@ -14,6 +15,8 @@ require_relative 'mtproto/tl/new_session_created'
14
15
  require_relative 'mtproto/tl/rpc_error'
15
16
  require_relative 'mtproto/tl/gzip_packed'
16
17
  require_relative 'mtproto/tl/config'
18
+ require_relative 'mtproto/tl/code_settings'
19
+ require_relative 'mtproto/tl/sent_code'
17
20
  require_relative 'mtproto/crypto/rsa_key'
18
21
  require_relative 'mtproto/crypto/factorization'
19
22
  require_relative 'mtproto/crypto/aes_ige'
@@ -26,6 +29,7 @@ require_relative 'mtproto/auth_key_generator'
26
29
  require_relative 'mtproto/session'
27
30
  require_relative 'mtproto/encrypted_message'
28
31
  require_relative 'mtproto/client'
32
+ require_relative 'mtproto/connection'
29
33
 
30
34
  module MTProto
31
35
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: mtproto
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.0.5
4
+ version: 0.0.6
5
5
  platform: ruby
6
6
  authors:
7
7
  - Artem Levenkov
@@ -17,19 +17,19 @@ executables: []
17
17
  extensions: []
18
18
  extra_rdoc_files: []
19
19
  files:
20
+ - ".env.example"
20
21
  - ".ruby-version"
21
22
  - Rakefile
22
23
  - ext/aes_ige/Makefile
23
- - ext/aes_ige/aes_ige.bundle
24
24
  - ext/aes_ige/aes_ige.c
25
25
  - ext/aes_ige/extconf.rb
26
26
  - ext/factorization/Makefile
27
27
  - ext/factorization/extconf.rb
28
- - ext/factorization/factorization.bundle
29
28
  - ext/factorization/factorization.c
30
29
  - lib/mtproto.rb
31
30
  - lib/mtproto/auth_key_generator.rb
32
31
  - lib/mtproto/client.rb
32
+ - lib/mtproto/connection.rb
33
33
  - lib/mtproto/crypto/aes_ige.rb
34
34
  - lib/mtproto/crypto/auth_key_helper.rb
35
35
  - lib/mtproto/crypto/dh_key_exchange.rb
@@ -39,9 +39,11 @@ files:
39
39
  - lib/mtproto/crypto/rsa_key.rb
40
40
  - lib/mtproto/crypto/rsa_pad.rb
41
41
  - lib/mtproto/encrypted_message.rb
42
+ - lib/mtproto/errors.rb
42
43
  - lib/mtproto/session.rb
43
44
  - lib/mtproto/tl/bad_msg_notification.rb
44
45
  - lib/mtproto/tl/client_dh_inner_data.rb
46
+ - lib/mtproto/tl/code_settings.rb
45
47
  - lib/mtproto/tl/config.rb
46
48
  - lib/mtproto/tl/gzip_packed.rb
47
49
  - lib/mtproto/tl/message.rb
@@ -49,6 +51,7 @@ files:
49
51
  - lib/mtproto/tl/new_session_created.rb
50
52
  - lib/mtproto/tl/p_q_inner_data.rb
51
53
  - lib/mtproto/tl/rpc_error.rb
54
+ - lib/mtproto/tl/sent_code.rb
52
55
  - lib/mtproto/tl/serializer.rb
53
56
  - lib/mtproto/tl/server_dh_inner_data.rb
54
57
  - lib/mtproto/transport/abridged_packet_codec.rb
Binary file
Binary file