telegram-mtproto-ruby 0.1.0

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.
@@ -0,0 +1,1456 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'socket'
4
+ require 'zlib'
5
+ require 'securerandom'
6
+ require_relative 'telegram_plain_tcp'
7
+ require 'digest'
8
+ require_relative 'telegram/auth'
9
+ require_relative 'telegram/crypto'
10
+ require_relative 'telegram/serialization'
11
+ require_relative 'telegram/connection/tcp_full_connection'
12
+ require_relative 'telegram/senders/mtproto_plain_sender'
13
+ require_relative 'telegram/senders/mtproto_encrypted_sender'
14
+ require_relative 'telegram/tl_object'
15
+
16
+ # Clean MTProto implementation using modular architecture
17
+ class TelegramMTProtoClean
18
+ class ConnectionError < StandardError; end
19
+ class AuthenticationError < StandardError; end
20
+
21
+ def initialize(api_id, api_hash, phone_number)
22
+ @api_id = api_id
23
+ @api_hash = api_hash
24
+ @phone_number = phone_number
25
+ @auth_key = nil
26
+ @connection = nil
27
+ @dc_info = nil
28
+ end
29
+
30
+ # Main entry point for authentication
31
+ def authenticate
32
+ Rails.logger.info "🚀 Starting CLEAN MTProto authentication for #{@phone_number}"
33
+
34
+ connect_to_dc
35
+ perform_dh_handshake
36
+
37
+ Rails.logger.info "✅ Authentication completed successfully"
38
+ @auth_key
39
+ end
40
+
41
+ # Send auth.sendCode request with retry logic like Telethon
42
+ def send_code(retry_count = 0)
43
+ Rails.logger.info "🔍 AuthenticationService: calling mtproto.send_code... (attempt #{retry_count + 1})"
44
+
45
+ unless @auth_key
46
+ Rails.logger.info "📡 No auth_key found, starting DH handshake..."
47
+ authenticate
48
+ end
49
+
50
+ if @auth_key
51
+ Rails.logger.info "✅ Auth key ready, sending auth.sendCode"
52
+
53
+ # REUSE existing TCP connection from DH handshake (like Telethon!)
54
+ existing_connection = @plain_sender.instance_variable_get(:@connection)
55
+
56
+ # Create or reuse encrypted sender with SAME connection like Telethon
57
+ unless @encrypted_sender
58
+ @encrypted_sender = Telegram::Senders::MTProtoEncryptedSender.new(@dc_info[:ip], @dc_info[:port], @auth_key, @time_offset)
59
+ @encrypted_sender.instance_variable_set(:@connection, existing_connection)
60
+ Rails.logger.info "🔄 Created encrypted sender with TCP connection from DH handshake"
61
+ else
62
+ Rails.logger.info "🔄 Reusing existing encrypted sender"
63
+ end
64
+
65
+ # EXACTLY like Telethon: Send auth.sendCode FIRST (no help.GetConfig before)
66
+ Rails.logger.info "🚀 Sending auth.sendCode FIRST like Telethon"
67
+ result = send_auth_send_code(@encrypted_sender)
68
+
69
+ if result[:success]
70
+ Rails.logger.info "✅ auth.sendCode successful: #{result[:phone_code_hash]}"
71
+ { success: true, phone_code_hash: result[:phone_code_hash] }
72
+ else
73
+ Rails.logger.error "❌ auth.sendCode failed: #{result[:error]}"
74
+ Rails.logger.debug "🔍 Debug retry conditions: error='#{result[:error]}', retry_count=#{retry_count}"
75
+
76
+ # Retry logic like Telethon for AUTH_RESTART and bad_server_salt errors
77
+ if (result[:error].include?('AUTH_RESTART') || result[:error].include?('-404')) && retry_count < 2
78
+ Rails.logger.warn "🔄 Got AUTH_RESTART error, retrying with CLEAN state like Telethon (attempt #{retry_count + 1}/3)"
79
+
80
+ # Clean disconnect like Telethon
81
+ cleanup_connection_state
82
+ sleep(3) # Wait before retry
83
+ return send_code(retry_count + 1)
84
+ elsif result[:error] == 'bad_server_salt' && retry_count < 10
85
+ Rails.logger.warn "🧂 Got bad_server_salt, retrying with new salt (attempt #{retry_count + 1}/11)"
86
+
87
+ # For bad_server_salt, just retry with updated salt - DON'T cleanup connection
88
+ Rails.logger.info "🧂 Retrying with updated salt WITHOUT connection cleanup"
89
+ sleep(2) # Brief pause
90
+ return send_code(retry_count + 1)
91
+ elsif result[:error] == 'container_processed' && retry_count < 5
92
+ Rails.logger.info "📦 Container processed, waiting for auth.sendCode response..."
93
+
94
+ # Wait a bit and try to receive next message
95
+ sleep(1)
96
+ return send_code(retry_count + 1)
97
+
98
+ else
99
+ Rails.logger.debug "🔍 Not retrying: error='#{result[:error]}', retry_count=#{retry_count}"
100
+ end
101
+
102
+ { success: false, error: result[:error] }
103
+ end
104
+ else
105
+ Rails.logger.error "❌ Failed to establish auth_key"
106
+
107
+ # Retry DH handshake if it failed and we haven't exceeded retry limit
108
+ if retry_count < 2
109
+ Rails.logger.warn "🔄 DH handshake failed, retrying with CLEAN state (attempt #{retry_count + 1}/3)"
110
+ cleanup_connection_state # Complete cleanup like Telethon
111
+ sleep(5) # Longer wait for DH retry
112
+ return send_code(retry_count + 1)
113
+ end
114
+
115
+ { success: false, error: "Authentication failed after #{retry_count + 1} attempts" }
116
+ end
117
+ rescue => e
118
+ Rails.logger.error "❌ MTProto send_code exception: #{e.message}"
119
+
120
+ # Retry on any exception as well
121
+ if retry_count < 2
122
+ Rails.logger.warn "🔄 Exception occurred, retrying with CLEAN state (attempt #{retry_count + 1}/3)"
123
+ cleanup_connection_state # Complete cleanup like Telethon
124
+ sleep(5)
125
+ return send_code(retry_count + 1)
126
+ end
127
+
128
+ { success: false, error: e.message }
129
+ end
130
+
131
+ # Clean connection state like Telethon._state.reset() + disconnect
132
+ def cleanup_connection_state
133
+ Rails.logger.info "🧹 Cleaning connection state like Telethon (reset auth_key, close connections, reset counters)"
134
+
135
+ # Reset auth key
136
+ @auth_key = nil
137
+
138
+ # Close all existing connections
139
+ if @plain_sender
140
+ begin
141
+ connection = @plain_sender.instance_variable_get(:@connection)
142
+ if connection
143
+ Rails.logger.debug "🔌 Closing plain sender connection"
144
+ connection.close rescue nil
145
+ end
146
+ rescue => e
147
+ Rails.logger.debug "❌ Error closing plain sender connection: #{e.message}"
148
+ end
149
+ @plain_sender = nil
150
+ end
151
+
152
+ # Reset encrypted sender connections as well
153
+ if instance_variable_defined?(:@encrypted_sender) && @encrypted_sender
154
+ begin
155
+ connection = @encrypted_sender.instance_variable_get(:@connection)
156
+ if connection
157
+ Rails.logger.debug "🔌 Closing encrypted sender connection"
158
+ connection.close rescue nil
159
+ end
160
+ rescue => e
161
+ Rails.logger.debug "❌ Error closing encrypted sender connection: #{e.message}"
162
+ end
163
+ @encrypted_sender = nil
164
+ end
165
+
166
+ Rails.logger.info "✅ Connection state cleaned - ready for fresh start"
167
+ end
168
+
169
+ # Placeholder methods for compatibility with old service
170
+ def authenticate_with_code(code)
171
+ Rails.logger.info "🔍 authenticate_with_code called with: #{code}"
172
+ { success: false, error: "Not implemented yet" }
173
+ end
174
+
175
+ def authenticate_with_password(password)
176
+ Rails.logger.info "🔍 authenticate_with_password called"
177
+ { success: false, error: "Not implemented yet" }
178
+ end
179
+
180
+ def get_me
181
+ Rails.logger.info "🔍 get_me called"
182
+ { success: false, error: "Not implemented yet" }
183
+ end
184
+
185
+ private
186
+
187
+ def connect_to_dc
188
+ # Telegram DC list - try different DCs if one fails
189
+ dcs = [
190
+ { id: 2, ip: '149.154.167.51', port: 443 }, # DC2 - Europe
191
+ { id: 1, ip: '149.154.175.53', port: 443 }, # DC1 - Miami
192
+ { id: 4, ip: '149.154.167.91', port: 443 }, # DC4 - Europe
193
+ { id: 5, ip: '149.154.171.5', port: 443 } # DC5 - Singapore
194
+ ]
195
+
196
+ # Try DC2 (default like Telethon) to match exact behavior
197
+ @dc_info = dcs[0] # DC2 - Amsterdam (Telethon default) - CORRECTED INDEX!
198
+ Rails.logger.info "🌐 Using DC#{@dc_info[:id]}: #{@dc_info[:ip]}:#{@dc_info[:port]} (using Telethon default DC)"
199
+ end
200
+
201
+ def perform_dh_handshake
202
+ @plain_sender = Telegram::Senders::MTProtoPlainSender.new(@dc_info[:ip], @dc_info[:port])
203
+
204
+ auth_result = Telegram::Auth.perform_dh_handshake(@plain_sender)
205
+
206
+ if auth_result
207
+ # Extract auth_key and time_offset like Telethon
208
+ @auth_key = auth_result[:auth_key]
209
+ @time_offset = auth_result[:time_offset] || 0
210
+
211
+ Rails.logger.info "🔑 DH handshake successful, auth_key: #{@auth_key.length} bytes"
212
+ Rails.logger.info "⏰ Time offset from server: #{@time_offset} seconds"
213
+ else
214
+ raise AuthenticationError.new("DH handshake failed")
215
+ end
216
+ end
217
+
218
+ def send_help_get_config(encrypted_sender)
219
+ Rails.logger.info "🚀 Building EXACT Telethon InitConnection + help.GetConfig"
220
+
221
+ # EXACT values from Telethon
222
+ layer = 201 # from telethon.tl.alltlobjects import LAYER
223
+
224
+ # help.GetConfigRequest constructor_id = 0xc4f9186b (verified)
225
+ get_config_request = [0xc4f9186b].pack('L<')
226
+
227
+ # InitConnectionRequest constructor_id = 0xc1cd5ea9 (CORRECTED!)
228
+ # Parameters EXACTLY like Telethon:
229
+ # api_id, device_model, system_version, app_version, system_lang_code, lang_pack, lang_code, query, proxy, params
230
+ api_id = @api_id
231
+ device_model = "Desktop"
232
+ system_version = "macOS 14.0"
233
+ app_version = "1.0"
234
+ system_lang_code = "en"
235
+ lang_pack = ""
236
+ lang_code = "en"
237
+ # proxy = None (optional)
238
+ # params = None (optional)
239
+
240
+ # Build InitConnectionRequest TL structure WITH CORRECT CONSTRUCTOR
241
+ init_connection_request = [0xc1cd5ea9].pack('L<') + # CORRECTED constructor_id
242
+ [api_id].pack('L<') + # api_id:int
243
+ build_tl_string(device_model) + # device_model:string
244
+ build_tl_string(system_version) + # system_version:string
245
+ build_tl_string(app_version) + # app_version:string
246
+ build_tl_string(system_lang_code) + # system_lang_code:string
247
+ build_tl_string(lang_pack) + # lang_pack:string
248
+ build_tl_string(lang_code) + # lang_code:string
249
+ get_config_request # query:X (help.GetConfigRequest)
250
+ # proxy and params are optional - omitting
251
+
252
+ # InvokeWithLayerRequest constructor_id = 0xda9b0d0d (verified correct)
253
+ invoke_with_layer_request = [0xda9b0d0d].pack('L<') + # constructor_id
254
+ [layer].pack('L<') + # layer:int
255
+ init_connection_request # query:X
256
+
257
+ Rails.logger.info "📤 Sending EXACT Telethon: InvokeWithLayerRequest(#{layer}, InitConnectionRequest(help.GetConfigRequest))"
258
+ Rails.logger.info "🔧 Using CORRECTED constructor IDs: InvokeWithLayer=0xda9b0d0d, InitConnection=0xc1cd5ea9"
259
+
260
+ begin
261
+ response = encrypted_sender.send(invoke_with_layer_request)
262
+ Rails.logger.info "✅ help.GetConfig response received: #{response.length} bytes"
263
+ { success: true, response: response }
264
+ rescue => e
265
+ Rails.logger.error "❌ help.GetConfig error: #{e.message}"
266
+ { success: false, error: e.message }
267
+ end
268
+ end
269
+
270
+ # Send auth.sendCode using encrypted MTProto
271
+ def send_auth_send_code(encrypted_sender)
272
+ Rails.logger.info "📱 Sending auth.sendCode for #{@phone_number}..."
273
+
274
+ # First build the inner auth.sendCode request
275
+ auth_send_code_data = build_auth_send_code_request()
276
+
277
+ # Wrap in InitConnection + InvokeWithLayer like Telethon
278
+ request_data = build_init_connection_wrapper(auth_send_code_data)
279
+
280
+ Rails.logger.info "📱 Built wrapped auth.sendCode request: #{request_data.length} bytes"
281
+
282
+ begin
283
+ response = encrypted_sender.send(request_data)
284
+ parse_auth_send_code_response(response)
285
+ rescue => e
286
+ Rails.logger.error "❌ auth.sendCode error: #{e.class}: #{e.message}"
287
+ Rails.logger.error "❌ auth.sendCode backtrace: #{e.backtrace.first(3).join("\n")}"
288
+
289
+ # Check if this is a -404 error (like Telethon's AuthRestartError)
290
+ if e.message.include?('AUTH_KEY_UNREGISTERED') || e.message.include?('-404')
291
+ Rails.logger.warn "🔄 Got -404 (AUTH_RESTART) - this might be normal for first auth.sendCode"
292
+ # This is similar to Telethon's AuthRestartError handling
293
+ { success: false, error: "AUTH_RESTART: Connection needs to be restarted (-404)" }
294
+ elsif e.message.include?('FLOOD_WAIT') || e.message.include?('-420')
295
+ { success: false, error: "FLOOD_WAIT: Too many requests, please wait" }
296
+ elsif e.message.include?('Telegram server error')
297
+ error_code = e.message.match(/error: (-?\d+)/)&.[](1)
298
+ { success: false, error: "Telegram server error: #{error_code}" }
299
+ else
300
+ { success: false, error: e.message }
301
+ end
302
+ end
303
+ end
304
+
305
+ # Parse auth.sendCode response
306
+ def parse_auth_send_code_response(response)
307
+ Rails.logger.info "📥 Parsing auth.sendCode response: #{response.length} bytes"
308
+ Rails.logger.info "🔍 Response HEX: #{response.unpack('H*')[0]}"
309
+
310
+ return { success: false, error: "Empty response" } if response.length < 4
311
+
312
+ # Read constructor
313
+ constructor = response[0, 4].unpack('L<')[0]
314
+ Rails.logger.info "🔍 Response constructor: 0x#{constructor.to_s(16)}"
315
+
316
+ case constructor
317
+ when 0x5e002502 # auth.sentCode
318
+ Rails.logger.info "✅ Got auth.sentCode"
319
+
320
+ # Simple parsing - extract phone_code_hash
321
+ # auth.sentCode#5e002502 type:auth.SentCodeType phone_code_hash:string next_type:auth.CodeType timeout:int
322
+ offset = 4
323
+
324
+ # Skip type (varies)
325
+ type_constructor = response[offset, 4].unpack('L<')[0]
326
+ offset += 4
327
+
328
+ # Skip type-specific data (simplified)
329
+ case type_constructor
330
+ when 0x3dbb5986 # auth.sentCodeTypeSms
331
+ # No additional fields
332
+ when 0xc000bba2 # auth.sentCodeTypeCall
333
+ # length:int
334
+ offset += 4
335
+ when 0x5353e5a7 # auth.sentCodeTypeFlashCall
336
+ # pattern:string
337
+ pattern_len = response[offset, 1].unpack('C')[0]
338
+ offset += 1 + pattern_len + ((4 - ((1 + pattern_len) % 4)) % 4)
339
+ else
340
+ Rails.logger.warn "⚠️ Unknown sentCodeType: 0x#{type_constructor.to_s(16)}"
341
+ # Try to continue anyway
342
+ end
343
+
344
+ # phone_code_hash:string
345
+ hash_len = response[offset, 1].unpack('C')[0]
346
+ offset += 1
347
+ phone_code_hash = response[offset, hash_len].force_encoding('UTF-8')
348
+
349
+ Rails.logger.info "📱 Phone code hash: #{phone_code_hash}"
350
+
351
+ { success: true, phone_code_hash: phone_code_hash }
352
+
353
+ when 0x446d7816 # auth.sentCodeSuccess
354
+ Rails.logger.info "✅ Got auth.sentCodeSuccess (already authorized)"
355
+ { success: true, phone_code_hash: "already_authorized" }
356
+
357
+ when 0xedab447b # bad_server_salt
358
+ Rails.logger.info "🧂 Received bad_server_salt - extracting new salt"
359
+ # bad_server_salt#edab447b bad_msg_id:long bad_msg_seqno:int error_code:int new_server_salt:long
360
+ offset = 4 # Skip constructor
361
+ bad_msg_id = response[offset, 8].unpack('q<')[0]
362
+ offset += 8
363
+ bad_msg_seqno = response[offset, 4].unpack('l<')[0]
364
+ offset += 4
365
+ error_code = response[offset, 4].unpack('l<')[0]
366
+ offset += 4
367
+ new_server_salt = response[offset, 8].unpack('q<')[0]
368
+
369
+ Rails.logger.info "🧂 Bad salt details: msg_id=#{bad_msg_id}, seqno=#{bad_msg_seqno}, error=#{error_code}, new_salt=#{new_server_salt}"
370
+
371
+ # Update salt in encrypted sender if exists
372
+ if @encrypted_sender
373
+ Rails.logger.info "🧂 Updating salt from #{@encrypted_sender.instance_variable_get(:@salt)} to #{new_server_salt}"
374
+ @encrypted_sender.instance_variable_set(:@salt, new_server_salt)
375
+ end
376
+
377
+ { success: false, error: "bad_server_salt", retry: true, new_salt: new_server_salt }
378
+
379
+ when 0x73f1f8dc # msg_container
380
+ Rails.logger.info "📦 Received msg_container - parsing messages"
381
+ # msg_container#73f1f8dc messages:vector<message> = MessageContainer
382
+ offset = 4 # Skip constructor
383
+ msg_count = response[offset, 4].unpack('L<')[0]
384
+ offset += 4
385
+
386
+ Rails.logger.info "📦 Container has #{msg_count} messages"
387
+
388
+ # Parse each message in container
389
+ (0...msg_count).each do |i|
390
+ Rails.logger.info "📦 Parsing message #{i + 1}/#{msg_count}"
391
+
392
+ # message msg_id:long seqno:int bytes:int body:bytes = Message;
393
+ msg_id = response[offset, 8].unpack('q<')[0]
394
+ offset += 8
395
+ seqno = response[offset, 4].unpack('l<')[0]
396
+ offset += 4
397
+ msg_bytes = response[offset, 4].unpack('l<')[0]
398
+ offset += 4
399
+ msg_body = response[offset, msg_bytes]
400
+ offset += msg_bytes
401
+
402
+ Rails.logger.info "📦 Message #{i + 1}: msg_id=#{msg_id}, seqno=#{seqno}, bytes=#{msg_bytes}"
403
+
404
+ # Check if this is a system message or actual auth.sendCode
405
+ msg_constructor = msg_body[0, 4].unpack('L<')[0]
406
+ Rails.logger.info "📦 Message #{i + 1} constructor: 0x#{msg_constructor.to_s(16)}"
407
+
408
+ case msg_constructor
409
+ when 0x9ec20908 # new_session_created
410
+ Rails.logger.info "🆕 Received new_session_created - updating salt"
411
+ # new_session_created#9ec20908 first_msg_id:long unique_id:long server_salt:long
412
+ new_salt = msg_body[20, 8].unpack('q<')[0]
413
+ if @encrypted_sender
414
+ Rails.logger.info "🧂 Updating salt from new_session_created: #{new_salt}"
415
+ @encrypted_sender.instance_variable_set(:@salt, new_salt)
416
+ end
417
+ when 0x62d6b459 # msgs_ack
418
+ Rails.logger.info "✅ Received msgs_ack - acknowledging messages"
419
+ # Just acknowledge, no action needed
420
+ else
421
+ # Try to parse as auth.sendCode response
422
+ result = parse_auth_send_code_response(msg_body)
423
+ if result[:success]
424
+ Rails.logger.info "✅ Found successful auth.sendCode in message #{i + 1}"
425
+ return result
426
+ end
427
+ end
428
+ end
429
+
430
+ # If container only has system messages, treat as success and wait for next message
431
+ Rails.logger.info "📦 Container processed - waiting for auth.sendCode in next message"
432
+ { success: false, error: "container_processed", wait_for_next: true }
433
+
434
+ when 0xf35c6d01 # rpc_result
435
+ Rails.logger.info "🎯 Received rpc_result - extracting auth.sendCode result"
436
+ # rpc_result#f35c6d01 req_msg_id:long result:Object
437
+ offset = 4 # Skip constructor
438
+ req_msg_id = response[offset, 8].unpack('q<')[0]
439
+ offset += 8
440
+
441
+ Rails.logger.info "🎯 RPC result for msg_id: #{req_msg_id}"
442
+
443
+ # Extract the actual result (auth.sentCode or error)
444
+ result_data = response[offset..-1]
445
+ Rails.logger.info "🎯 RPC result data: #{result_data.length} bytes"
446
+
447
+ # Parse the inner result recursively
448
+ parse_auth_send_code_response(result_data)
449
+
450
+ when 0x2144ca19 # rpc_error
451
+ Rails.logger.info "❌ Received rpc_error"
452
+ # rpc_error#2144ca19 error_code:int error_message:string
453
+ offset = 4 # Skip constructor
454
+ error_code = response[offset, 4].unpack('l<')[0]
455
+ offset += 4
456
+
457
+ # Parse error message (TL string)
458
+ msg_len = response[offset, 1].unpack('C')[0]
459
+ offset += 1
460
+ error_message = response[offset, msg_len]
461
+
462
+ Rails.logger.error "❌ RPC Error: code=#{error_code}, message='#{error_message}'"
463
+ { success: false, error: "RPC_ERROR_#{error_code}: #{error_message}" }
464
+
465
+ when 0x78d4dec1 # updateShort
466
+ Rails.logger.info "📢 Received updateShort (session initialized!) - IGNORING and waiting for next message in container"
467
+ # updateShort#78d4dec1 update:Update date:int = Updates;
468
+ # This means InitConnection worked and session is active!
469
+ # This is system message, auth.sendCode result should be in msg_container
470
+ Rails.logger.info "✅ InitConnection successful! Session initialized, looking for auth.sendCode in container..."
471
+ { success: false, error: "updateShort_ignored", wait_for_container: true }
472
+
473
+ else
474
+ Rails.logger.error "❌ Unknown auth.sendCode response: 0x#{constructor.to_s(16)}"
475
+ { success: false, error: "Unknown response type" }
476
+ end
477
+ end
478
+
479
+ # Pack TL string helper
480
+ def pack_tl_string(str)
481
+ bytes = str.encode('UTF-8').force_encoding('ASCII-8BIT')
482
+
483
+ if bytes.length >= 254
484
+ result = "\xfe".dup.force_encoding('ASCII-8BIT') + [bytes.length].pack('L<')[0, 3] + bytes
485
+ else
486
+ result = [bytes.length].pack('C') + bytes
487
+ end
488
+
489
+ # Padding to 4-byte boundary
490
+ padding = (4 - (result.length % 4)) % 4
491
+ if padding > 0
492
+ result += "\x00".dup.force_encoding('ASCII-8BIT') * padding
493
+ end
494
+
495
+ result.force_encoding('ASCII-8BIT')
496
+ end
497
+
498
+ # Helper method for TL string encoding - available to main class
499
+ def build_tl_string(str)
500
+ bytes = str.encode('UTF-8').force_encoding('ASCII-8BIT')
501
+ length = bytes.length
502
+
503
+ if length < 254
504
+ # Short string: length (1 byte) + data + padding
505
+ padding = (4 - ((length + 1) % 4)) % 4
506
+ [length].pack('C') + bytes + ("\x00" * padding)
507
+ else
508
+ # Long string: 254 (1 byte) + length (3 bytes) + data + padding
509
+ padding = (4 - (length % 4)) % 4
510
+ "\xfe" + [length].pack('L<')[0,3] + bytes + ("\x00" * padding)
511
+ end
512
+ end
513
+
514
+ # Build the inner auth.sendCode request using PURE TL SCHEMAS
515
+
516
+
517
+ def build_auth_send_code_request
518
+ Rails.logger.info "🔧 Building auth.sendCode request using PURE TL SCHEMAS (НЕ ХАРДКОД!)"
519
+
520
+ # Clean phone number
521
+ clean_phone = @phone_number.gsub(/[^0-9]/, '')
522
+
523
+ # Build CodeSettings using TL schema - НЕ ХАРДКОД!
524
+ code_settings = Telegram::TLObject.serialize('codeSettings', flags: 0)
525
+
526
+ # Build auth.sendCode using TL schema with nested object - НЕ ХАРДКОД!
527
+ auth_send_code = Telegram::TLObject.serialize('auth.sendCode',
528
+ phone_number: clean_phone,
529
+ api_id: @api_id.to_i,
530
+ api_hash: @api_hash,
531
+ settings: code_settings # Передаём уже сериализованный объект
532
+ )
533
+
534
+ Rails.logger.info "🔧 Built auth.sendCode using TL schemas: #{auth_send_code.length} bytes"
535
+ auth_send_code
536
+ end
537
+
538
+ # Build InitConnection wrapper using PURE TL SCHEMAS - НЕ ХАРДКОД!
539
+ def build_init_connection_wrapper(inner_request)
540
+ Rails.logger.info "🔧 Building InitConnection wrapper using PURE TL SCHEMAS (НИКАКОГО ХАРДКОДА!)"
541
+
542
+ # Layer 182 - current Telegram Desktop layer
543
+ layer = 182
544
+
545
+ # Use PURE TL schema for InitConnection - НИ ОДНОГО ХАРДКОДА!
546
+ # UPDATED parameters to avoid APP_OUTDATED error
547
+ init_connection = Telegram::TLObject.serialize('initConnection',
548
+ flags: 0, # No proxy, no params
549
+ api_id: @api_id.to_i,
550
+ device_model: "Desktop", # Like Telethon
551
+ system_version: "macOS 14.7.1", # Updated version
552
+ app_version: "4.16.8", # Modern Telegram Desktop version
553
+ system_lang_code: "en",
554
+ lang_pack: "macos", # Telegram Desktop pack
555
+ lang_code: "en",
556
+ query: inner_request # Передаём уже сериализованный auth.sendCode
557
+ )
558
+
559
+ # Use PURE TL schema for InvokeWithLayer - НИ ОДНОГО ХАРДКОДА!
560
+ invoke_with_layer = Telegram::TLObject.serialize('invokeWithLayer',
561
+ layer: layer,
562
+ query: init_connection # Передаём уже сериализованный initConnection
563
+ )
564
+
565
+ Rails.logger.info "🔧 Built wrapper using PURE TL SCHEMAS: InvokeWithLayer(#{layer}) + InitConnection + auth.sendCode"
566
+ Rails.logger.info "🔧 Total request size: #{invoke_with_layer.length} bytes"
567
+ Rails.logger.info "🔧 100% TL schemas, 0% хардкода! Как и должно быть!"
568
+
569
+ invoke_with_layer
570
+ end
571
+
572
+ # Get contacts from Telegram
573
+ def get_contacts(retry_count = 0)
574
+ Rails.logger.info "📞 Starting contacts.getContacts"
575
+
576
+ unless @encrypted_sender
577
+ Rails.logger.error "❌ No encrypted sender available - call send_code first"
578
+ return { success: false, error: "No encrypted sender" }
579
+ end
580
+
581
+ begin
582
+ result = send_contacts_get_contacts
583
+
584
+ if result[:success]
585
+ Rails.logger.info "✅ Got contacts successfully!"
586
+ result
587
+ elsif result[:error].include?('bad_server_salt') && retry_count < 10
588
+ Rails.logger.info "🧂 Bad salt, retrying contacts.getContacts..."
589
+ sleep(2)
590
+ return get_contacts(retry_count + 1)
591
+ else
592
+ Rails.logger.debug "🔍 Not retrying contacts.getContacts: error='#{result[:error]}', retry_count=#{retry_count}"
593
+ { success: false, error: result[:error] }
594
+ end
595
+ rescue => e
596
+ Rails.logger.error "❌ MTProto get_contacts exception: #{e.message}"
597
+ { success: false, error: e.message }
598
+ end
599
+ end
600
+
601
+ # Get updates (incoming messages) from Telegram
602
+ def get_updates(pts = 0, date = 0, qts = 0, retry_count = 0)
603
+ Rails.logger.info "📥 Starting updates.getDifference"
604
+
605
+ unless @encrypted_sender
606
+ Rails.logger.error "❌ No encrypted sender available - call send_code first"
607
+ return { success: false, error: "No encrypted sender" }
608
+ end
609
+
610
+ begin
611
+ result = send_updates_get_difference(pts, date, qts)
612
+
613
+ if result[:success]
614
+ Rails.logger.info "✅ Got updates successfully!"
615
+ result
616
+ elsif result[:error].include?('bad_server_salt') && retry_count < 10
617
+ Rails.logger.info "🧂 Bad salt, retrying updates.getDifference..."
618
+ sleep(2)
619
+ return get_updates(pts, date, qts, retry_count + 1)
620
+ else
621
+ Rails.logger.debug "🔍 Not retrying updates.getDifference: error='#{result[:error]}', retry_count=#{retry_count}"
622
+ { success: false, error: result[:error] }
623
+ end
624
+ rescue => e
625
+ Rails.logger.error "❌ MTProto get_updates exception: #{e.message}"
626
+ { success: false, error: e.message }
627
+ end
628
+ end
629
+
630
+ # Start updates polling (for background processing)
631
+ def start_updates_polling(callback_proc = nil)
632
+ Rails.logger.info "🔄 Starting updates polling"
633
+
634
+ @polling_active = true
635
+ @updates_state = { pts: 0, date: 0, qts: 0 }
636
+
637
+ Thread.new do
638
+ while @polling_active
639
+ begin
640
+ result = get_updates(@updates_state[:pts], @updates_state[:date], @updates_state[:qts])
641
+
642
+ if result[:success]
643
+ # Update state
644
+ if result[:state]
645
+ @updates_state = result[:state]
646
+ Rails.logger.debug "📥 Updated state: pts=#{@updates_state[:pts]}, date=#{@updates_state[:date]}, qts=#{@updates_state[:qts]}"
647
+ end
648
+
649
+ # Process new messages
650
+ if result[:new_messages] && result[:new_messages].any?
651
+ Rails.logger.info "📥 Got #{result[:new_messages].length} new messages"
652
+
653
+ result[:new_messages].each do |message|
654
+ Rails.logger.info "📥 New message: #{message[:message]} from user_id=#{message[:from_id]}"
655
+
656
+ # Call callback if provided
657
+ callback_proc&.call(message)
658
+ end
659
+ end
660
+ else
661
+ Rails.logger.warn "⚠️ Updates polling error: #{result[:error]}"
662
+ end
663
+
664
+ # Poll every 2 seconds
665
+ sleep(2)
666
+ rescue => e
667
+ Rails.logger.error "❌ Updates polling exception: #{e.message}"
668
+ sleep(5) # Wait longer on error
669
+ end
670
+ end
671
+
672
+ Rails.logger.info "🛑 Updates polling stopped"
673
+ end
674
+ end
675
+
676
+ # Stop updates polling
677
+ def stop_updates_polling
678
+ Rails.logger.info "🛑 Stopping updates polling"
679
+ @polling_active = false
680
+ end
681
+
682
+ # Send message to Telegram contact
683
+ def send_message(user_id, message_text, retry_count = 0)
684
+ Rails.logger.info "📤 Starting messages.sendMessage to user_id=#{user_id}"
685
+
686
+ unless @encrypted_sender
687
+ Rails.logger.error "❌ No encrypted sender available - call send_code first"
688
+ return { success: false, error: "No encrypted sender" }
689
+ end
690
+
691
+ begin
692
+ result = send_messages_send_message(user_id, message_text)
693
+
694
+ if result[:success]
695
+ Rails.logger.info "✅ Message sent successfully!"
696
+ result
697
+ elsif result[:error].include?('bad_server_salt') && retry_count < 10
698
+ Rails.logger.info "🧂 Bad salt, retrying messages.sendMessage..."
699
+ sleep(2)
700
+ return send_message(user_id, message_text, retry_count + 1)
701
+ else
702
+ Rails.logger.debug "🔍 Not retrying messages.sendMessage: error='#{result[:error]}', retry_count=#{retry_count}"
703
+ { success: false, error: result[:error] }
704
+ end
705
+ rescue => e
706
+ Rails.logger.error "❌ MTProto send_message exception: #{e.message}"
707
+ { success: false, error: e.message }
708
+ end
709
+ end
710
+
711
+ public
712
+
713
+ # Sign in with PIN code received from auth.sendCode
714
+ def sign_in(phone_code_hash, code, retry_count = 0)
715
+ Rails.logger.info "🔐 Starting auth.signIn with code: #{code}"
716
+
717
+ unless @encrypted_sender
718
+ Rails.logger.error "❌ No encrypted sender available - call send_code first"
719
+ return { success: false, error: "No encrypted sender" }
720
+ end
721
+
722
+ begin
723
+ result = send_auth_sign_in(@phone_number, phone_code_hash, code)
724
+
725
+ if result[:success]
726
+ Rails.logger.info "✅ Sign in successful!"
727
+ result
728
+ elsif result[:error].include?('bad_server_salt') && retry_count < 10
729
+ Rails.logger.info "🧂 Bad salt, retrying auth.signIn..."
730
+ sleep(2)
731
+ return sign_in(phone_code_hash, code, retry_count + 1)
732
+ else
733
+ Rails.logger.debug "🔍 Not retrying auth.signIn: error='#{result[:error]}', retry_count=#{retry_count}"
734
+ { success: false, error: result[:error] }
735
+ end
736
+ rescue => e
737
+ Rails.logger.error "❌ MTProto sign_in exception: #{e.message}"
738
+ { success: false, error: e.message }
739
+ end
740
+ end
741
+
742
+ private
743
+
744
+ # Send auth.signIn request
745
+ def send_auth_sign_in(phone, phone_code_hash, code)
746
+ Rails.logger.info "📱 Building auth.signIn request for phone: #{phone}"
747
+
748
+ # Build auth.signIn using PURE TL schemas
749
+ auth_sign_in_data = build_auth_sign_in_request(phone, phone_code_hash, code)
750
+ Rails.logger.info "📱 Built auth.signIn request: #{auth_sign_in_data.length} bytes"
751
+
752
+ begin
753
+ response = @encrypted_sender.send(auth_sign_in_data)
754
+ parse_auth_sign_in_response(response)
755
+ rescue => e
756
+ Rails.logger.error "❌ auth.signIn error: #{e.class}: #{e.message}"
757
+ { success: false, error: e.message }
758
+ end
759
+ end
760
+
761
+ # Build auth.signIn request using PURE TL schemas
762
+ def build_auth_sign_in_request(phone, phone_code_hash, code)
763
+ Rails.logger.info "🔧 Building auth.signIn using PURE TL SCHEMAS"
764
+
765
+ # Use PURE TL schema for auth.signIn
766
+ auth_sign_in = Telegram::TLObject.serialize('auth.signIn',
767
+ phone_number: phone,
768
+ phone_code_hash: phone_code_hash,
769
+ phone_code: code
770
+ )
771
+
772
+ Rails.logger.info "✅ Built auth.signIn: #{auth_sign_in.length} bytes"
773
+ auth_sign_in
774
+ end
775
+
776
+ # Parse auth.signIn response
777
+ def parse_auth_sign_in_response(response)
778
+ Rails.logger.info "📥 Parsing auth.signIn response: #{response.length} bytes"
779
+ Rails.logger.info "🔍 Response HEX: #{response.unpack('H*')[0]}"
780
+
781
+ return { success: false, error: "Empty response" } if response.length < 4
782
+
783
+ # Read constructor
784
+ constructor = response[0, 4].unpack('L<')[0]
785
+ Rails.logger.info "🔍 Response constructor: 0x#{constructor.to_s(16)}"
786
+
787
+ case constructor
788
+ when 0x73f1f8dc # msg_container
789
+ parse_msg_container_auth_response(response)
790
+ when 0xf35c6d01 # rpc_result
791
+ parse_rpc_result_auth_response(response)
792
+ when 0x2144ca19 # rpc_error
793
+ parse_rpc_error_response(response)
794
+ when 0xedab447b # bad_server_salt
795
+ parse_bad_server_salt_response(response)
796
+ when 0x3dbb5986 # auth.authorization
797
+ Rails.logger.info "✅ Got auth.authorization - user authenticated!"
798
+ # TODO: Parse user details
799
+ { success: true, authorized: true }
800
+ when 0x44747e9a # auth.authorizationSignUpRequired
801
+ Rails.logger.info "📝 Sign up required"
802
+ # TODO: Handle sign up flow
803
+ { success: false, error: "SIGN_UP_REQUIRED" }
804
+ else
805
+ Rails.logger.error "❌ Unknown auth.signIn response: 0x#{constructor.to_s(16)}"
806
+ { success: false, error: "Unknown response type" }
807
+ end
808
+ end
809
+
810
+ # Parse msg_container for auth responses
811
+ def parse_msg_container_auth_response(response)
812
+ Rails.logger.info "📦 Parsing msg_container for auth response"
813
+
814
+ offset = 4 # Skip constructor
815
+ count = response[offset, 4].unpack('i<')[0]
816
+ offset += 4
817
+
818
+ Rails.logger.info "📦 Container has #{count} messages"
819
+
820
+ count.times do |i|
821
+ Rails.logger.info "📦 Parsing message #{i + 1}/#{count}"
822
+
823
+ msg_id = response[offset, 8].unpack('q<')[0]
824
+ offset += 8
825
+
826
+ seqno = response[offset, 4].unpack('i<')[0]
827
+ offset += 4
828
+
829
+ bytes = response[offset, 4].unpack('i<')[0]
830
+ offset += 4
831
+
832
+ msg_body = response[offset, bytes]
833
+ offset += bytes
834
+
835
+ Rails.logger.info "📦 Message #{i + 1}: msg_id=#{msg_id}, seqno=#{seqno}, bytes=#{bytes}"
836
+
837
+ # Recursively parse message body
838
+ result = parse_auth_sign_in_response(msg_body)
839
+ return result if result[:success] || !result[:error].include?('ignored')
840
+ end
841
+
842
+ { success: false, error: "container_processed" }
843
+ end
844
+
845
+ # Parse rpc_result for auth responses
846
+ def parse_rpc_result_auth_response(response)
847
+ Rails.logger.info "📦 Parsing rpc_result for auth response"
848
+
849
+ offset = 4 # Skip constructor
850
+ req_msg_id = response[offset, 8].unpack('q<')[0]
851
+ offset += 8
852
+
853
+ result_data = response[offset..-1]
854
+ Rails.logger.info "📦 RPC result for msg_id=#{req_msg_id}, result=#{result_data.length} bytes"
855
+
856
+ # Recursively parse result data
857
+ parse_auth_sign_in_response(result_data)
858
+ end
859
+
860
+ # Parse rpc_error response
861
+ def parse_rpc_error_response(response)
862
+ Rails.logger.info "❌ Parsing rpc_error"
863
+
864
+ offset = 4 # Skip constructor
865
+ error_code = response[offset, 4].unpack('i<')[0]
866
+ offset += 4
867
+
868
+ # Error message is a TL string
869
+ msg_len_byte = response[offset, 1].unpack('C')[0]
870
+ offset += 1
871
+
872
+ if msg_len_byte == 254
873
+ # Long string: 3 more bytes for length
874
+ msg_len = response[offset, 3].unpack('L<')[0] & 0xFFFFFF
875
+ offset += 3
876
+ else
877
+ msg_len = msg_len_byte
878
+ end
879
+
880
+ error_message = response[offset, msg_len].force_encoding('UTF-8')
881
+
882
+ Rails.logger.error "❌ RPC Error: code=#{error_code}, message='#{error_message}'"
883
+ { success: false, error: "RPC_ERROR_#{error_code}: #{error_message}" }
884
+ end
885
+
886
+ # Parse bad_server_salt response
887
+ def parse_bad_server_salt_response(response)
888
+ Rails.logger.info "🧂 Parsing bad_server_salt"
889
+
890
+ offset = 4 # Skip constructor
891
+ bad_msg_id = response[offset, 8].unpack('q<')[0]
892
+ offset += 8
893
+
894
+ bad_msg_seqno = response[offset, 4].unpack('i<')[0]
895
+ offset += 4
896
+
897
+ error_code = response[offset, 4].unpack('i<')[0]
898
+ offset += 4
899
+
900
+ new_server_salt = response[offset, 8].unpack('q<')[0]
901
+
902
+ Rails.logger.info "🧂 Bad salt details: msg_id=#{bad_msg_id}, seqno=#{bad_msg_seqno}, error=#{error_code}, new_salt=#{new_server_salt}"
903
+ Rails.logger.info "🧂 Updating salt from #{@encrypted_sender.instance_variable_get(:@salt)} to #{new_server_salt}"
904
+
905
+ # Update salt in encrypted sender
906
+ @encrypted_sender.instance_variable_set(:@salt, new_server_salt)
907
+
908
+ { success: false, error: "bad_server_salt" }
909
+ end
910
+
911
+ # Send contacts.getContacts request
912
+ def send_contacts_get_contacts
913
+ Rails.logger.info "📞 Building contacts.getContacts request"
914
+
915
+ # Build contacts.getContacts using PURE TL schemas
916
+ contacts_data = build_contacts_get_contacts_request
917
+ Rails.logger.info "📞 Built contacts.getContacts request: #{contacts_data.length} bytes"
918
+
919
+ begin
920
+ response = @encrypted_sender.send(contacts_data)
921
+ parse_contacts_response(response)
922
+ rescue => e
923
+ Rails.logger.error "❌ contacts.getContacts error: #{e.class}: #{e.message}"
924
+ { success: false, error: e.message }
925
+ end
926
+ end
927
+
928
+ # Build contacts.getContacts request using PURE TL schemas
929
+ def build_contacts_get_contacts_request
930
+ Rails.logger.info "🔧 Building contacts.getContacts using PURE TL SCHEMAS"
931
+
932
+ # Use PURE TL schema for contacts.getContacts
933
+ # contacts.getContacts#c023849f hash:int = contacts.Contacts;
934
+ contacts_request = Telegram::TLObject.serialize('contacts.getContacts',
935
+ hash: 0 # 0 = get all contacts
936
+ )
937
+
938
+ Rails.logger.info "✅ Built contacts.getContacts: #{contacts_request.length} bytes"
939
+ contacts_request
940
+ end
941
+
942
+ # Parse contacts.getContacts response
943
+ def parse_contacts_response(response)
944
+ Rails.logger.info "📥 Parsing contacts.getContacts response: #{response.length} bytes"
945
+ Rails.logger.info "🔍 Response HEX: #{response.unpack('H*')[0]}"
946
+
947
+ return { success: false, error: "Empty response" } if response.length < 4
948
+
949
+ # Read constructor
950
+ constructor = response[0, 4].unpack('L<')[0]
951
+ Rails.logger.info "🔍 Response constructor: 0x#{constructor.to_s(16)}"
952
+
953
+ case constructor
954
+ when 0x73f1f8dc # msg_container
955
+ parse_msg_container_contacts_response(response)
956
+ when 0xf35c6d01 # rpc_result
957
+ parse_rpc_result_contacts_response(response)
958
+ when 0x2144ca19 # rpc_error
959
+ parse_rpc_error_response(response)
960
+ when 0xedab447b # bad_server_salt
961
+ parse_bad_server_salt_response(response)
962
+ when 0xeae87e42 # contacts.contacts
963
+ Rails.logger.info "✅ Got contacts.contacts"
964
+ parse_contacts_contacts(response)
965
+ when 0xb74ba9d2 # contacts.contactsNotModified
966
+ Rails.logger.info "✅ Got contacts.contactsNotModified"
967
+ { success: true, contacts: [], not_modified: true }
968
+ else
969
+ Rails.logger.error "❌ Unknown contacts.getContacts response: 0x#{constructor.to_s(16)}"
970
+ { success: false, error: "Unknown response type" }
971
+ end
972
+ end
973
+
974
+ # Parse contacts.contacts response
975
+ def parse_contacts_contacts(response)
976
+ Rails.logger.info "📞 Parsing contacts.contacts"
977
+
978
+ # contacts.contacts#eae87e42 contacts:Vector<Contact> saved_count:int users:Vector<User> = contacts.Contacts;
979
+ offset = 4 # Skip constructor
980
+
981
+ # Parse contacts vector
982
+ contacts_count = response[offset, 4].unpack('L<')[0]
983
+ offset += 4
984
+
985
+ Rails.logger.info "📞 Found #{contacts_count} contacts"
986
+
987
+ contacts = []
988
+ contacts_count.times do
989
+ # Contact structure: contact#f911c994 user_id:int mutual:Bool = Contact;
990
+ contact_constructor = response[offset, 4].unpack('L<')[0]
991
+ offset += 4
992
+
993
+ if contact_constructor == 0x94c911f9 # contact
994
+ user_id = response[offset, 4].unpack('i<')[0]
995
+ offset += 4
996
+
997
+ mutual = response[offset, 4].unpack('L<')[0] == 0x997275b5 # boolTrue
998
+ offset += 4
999
+
1000
+ contacts << { user_id: user_id, mutual: mutual }
1001
+ Rails.logger.debug "📞 Contact: user_id=#{user_id}, mutual=#{mutual}"
1002
+ else
1003
+ Rails.logger.warn "⚠️ Unknown contact constructor: 0x#{contact_constructor.to_s(16)}"
1004
+ offset += 8 # Skip unknown contact
1005
+ end
1006
+ end
1007
+
1008
+ # Skip saved_count
1009
+ saved_count = response[offset, 4].unpack('i<')[0]
1010
+ offset += 4
1011
+
1012
+ # Parse users vector
1013
+ users_count = response[offset, 4].unpack('L<')[0]
1014
+ offset += 4
1015
+
1016
+ Rails.logger.info "📞 Found #{users_count} users"
1017
+
1018
+ users = []
1019
+ users_count.times do |i|
1020
+ # Parse User object (simplified)
1021
+ user_constructor = response[offset, 4].unpack('L<')[0]
1022
+ offset += 4
1023
+
1024
+ if user_constructor == 0x938458c1 # user
1025
+ # Simplified user parsing - just extract id and first_name
1026
+ user_id = response[offset, 8].unpack('q<')[0]
1027
+ offset += 8
1028
+
1029
+ # Skip flags and access_hash for now
1030
+ offset += 12
1031
+
1032
+ # Try to get first_name (basic parsing)
1033
+ first_name_len = response[offset, 1].unpack('C')[0]
1034
+ offset += 1
1035
+
1036
+ if first_name_len < 254
1037
+ first_name = response[offset, first_name_len].force_encoding('UTF-8')
1038
+ offset += first_name_len
1039
+ # Align to 4-byte boundary
1040
+ offset += (4 - ((first_name_len + 1) % 4)) % 4
1041
+ else
1042
+ first_name = "User"
1043
+ offset += 100 # Skip to avoid parsing complexity
1044
+ end
1045
+
1046
+ users << { id: user_id, first_name: first_name }
1047
+ Rails.logger.debug "📞 User: id=#{user_id}, name=#{first_name}"
1048
+ else
1049
+ Rails.logger.debug "📞 Skipping user #{i + 1} with constructor 0x#{user_constructor.to_s(16)}"
1050
+ offset += 50 # Skip user data
1051
+ end
1052
+ end
1053
+
1054
+ Rails.logger.info "✅ Parsed #{contacts.length} contacts and #{users.length} users"
1055
+ { success: true, contacts: contacts, users: users, saved_count: saved_count }
1056
+ end
1057
+
1058
+ # Parse msg_container for contacts responses
1059
+ def parse_msg_container_contacts_response(response)
1060
+ Rails.logger.info "📦 Parsing msg_container for contacts response"
1061
+
1062
+ offset = 4 # Skip constructor
1063
+ count = response[offset, 4].unpack('i<')[0]
1064
+ offset += 4
1065
+
1066
+ Rails.logger.info "📦 Container has #{count} messages"
1067
+
1068
+ count.times do |i|
1069
+ Rails.logger.info "📦 Parsing message #{i + 1}/#{count}"
1070
+
1071
+ msg_id = response[offset, 8].unpack('q<')[0]
1072
+ offset += 8
1073
+
1074
+ seqno = response[offset, 4].unpack('i<')[0]
1075
+ offset += 4
1076
+
1077
+ bytes = response[offset, 4].unpack('i<')[0]
1078
+ offset += 4
1079
+
1080
+ msg_body = response[offset, bytes]
1081
+ offset += bytes
1082
+
1083
+ Rails.logger.info "📦 Message #{i + 1}: msg_id=#{msg_id}, seqno=#{seqno}, bytes=#{bytes}"
1084
+
1085
+ # Recursively parse message body
1086
+ result = parse_contacts_response(msg_body)
1087
+ return result if result[:success] || !result[:error].include?('ignored')
1088
+ end
1089
+
1090
+ { success: false, error: "container_processed" }
1091
+ end
1092
+
1093
+ # Parse rpc_result for contacts responses
1094
+ def parse_rpc_result_contacts_response(response)
1095
+ Rails.logger.info "📦 Parsing rpc_result for contacts response"
1096
+
1097
+ offset = 4 # Skip constructor
1098
+ req_msg_id = response[offset, 8].unpack('q<')[0]
1099
+ offset += 8
1100
+
1101
+ result_data = response[offset..-1]
1102
+ Rails.logger.info "📦 RPC result for msg_id=#{req_msg_id}, result=#{result_data.length} bytes"
1103
+
1104
+ # Recursively parse result data
1105
+ parse_contacts_response(result_data)
1106
+ end
1107
+
1108
+ # Send messages.sendMessage request
1109
+ def send_messages_send_message(user_id, message_text)
1110
+ Rails.logger.info "📤 Building messages.sendMessage request for user_id=#{user_id}"
1111
+
1112
+ # Build messages.sendMessage using PURE TL schemas
1113
+ message_data = build_messages_send_message_request(user_id, message_text)
1114
+ Rails.logger.info "📤 Built messages.sendMessage request: #{message_data.length} bytes"
1115
+
1116
+ begin
1117
+ response = @encrypted_sender.send(message_data)
1118
+ parse_message_response(response)
1119
+ rescue => e
1120
+ Rails.logger.error "❌ messages.sendMessage error: #{e.class}: #{e.message}"
1121
+ { success: false, error: e.message }
1122
+ end
1123
+ end
1124
+
1125
+ # Build messages.sendMessage request using PURE TL schemas
1126
+ def build_messages_send_message_request(user_id, message_text)
1127
+ Rails.logger.info "🔧 Building messages.sendMessage using PURE TL SCHEMAS"
1128
+
1129
+ # First build inputPeerUser
1130
+ input_peer = Telegram::TLObject.serialize('inputPeerUser',
1131
+ user_id: user_id,
1132
+ access_hash: 0 # Simplified - in real app should store access_hash
1133
+ )
1134
+
1135
+ # Generate random_id
1136
+ random_id = SecureRandom.random_number(2**63)
1137
+
1138
+ # Build messages.sendMessage
1139
+ # messages.sendMessage#280d096f flags:# no_webpage:flags.1?true silent:flags.5?true background:flags.6?true clear_draft:flags.7?true peer:InputPeer random_id:long message:string reply_markup:flags.2?ReplyMarkup entities:flags.3?Vector<MessageEntity> schedule_date:flags.10?int = Updates;
1140
+ message_request = Telegram::TLObject.serialize('messages.sendMessage',
1141
+ flags: 0, # No special flags
1142
+ peer: input_peer,
1143
+ random_id: random_id,
1144
+ message: message_text
1145
+ )
1146
+
1147
+ Rails.logger.info "✅ Built messages.sendMessage: #{message_request.length} bytes"
1148
+ message_request
1149
+ end
1150
+
1151
+ # Parse messages.sendMessage response
1152
+ def parse_message_response(response)
1153
+ Rails.logger.info "📥 Parsing messages.sendMessage response: #{response.length} bytes"
1154
+ Rails.logger.info "🔍 Response HEX: #{response.unpack('H*')[0]}"
1155
+
1156
+ return { success: false, error: "Empty response" } if response.length < 4
1157
+
1158
+ # Read constructor
1159
+ constructor = response[0, 4].unpack('L<')[0]
1160
+ Rails.logger.info "🔍 Response constructor: 0x#{constructor.to_s(16)}"
1161
+
1162
+ case constructor
1163
+ when 0x73f1f8dc # msg_container
1164
+ parse_msg_container_message_response(response)
1165
+ when 0xf35c6d01 # rpc_result
1166
+ parse_rpc_result_message_response(response)
1167
+ when 0x2144ca19 # rpc_error
1168
+ parse_rpc_error_response(response)
1169
+ when 0xedab447b # bad_server_salt
1170
+ parse_bad_server_salt_response(response)
1171
+ when 0x725b04c3, 0x56826fa8, 0x629d0e7f # Various Updates types
1172
+ Rails.logger.info "✅ Got message Updates response"
1173
+ { success: true, message_sent: true, constructor: "0x#{constructor.to_s(16)}" }
1174
+ else
1175
+ Rails.logger.info "✅ Message sent - unknown response type: 0x#{constructor.to_s(16)}"
1176
+ { success: true, message_sent: true, constructor: "0x#{constructor.to_s(16)}" }
1177
+ end
1178
+ end
1179
+
1180
+ # Parse msg_container for message responses
1181
+ def parse_msg_container_message_response(response)
1182
+ Rails.logger.info "📦 Parsing msg_container for message response"
1183
+
1184
+ offset = 4 # Skip constructor
1185
+ count = response[offset, 4].unpack('i<')[0]
1186
+ offset += 4
1187
+
1188
+ Rails.logger.info "📦 Container has #{count} messages"
1189
+
1190
+ count.times do |i|
1191
+ Rails.logger.info "📦 Parsing message #{i + 1}/#{count}"
1192
+
1193
+ msg_id = response[offset, 8].unpack('q<')[0]
1194
+ offset += 8
1195
+
1196
+ seqno = response[offset, 4].unpack('i<')[0]
1197
+ offset += 4
1198
+
1199
+ bytes = response[offset, 4].unpack('i<')[0]
1200
+ offset += 4
1201
+
1202
+ msg_body = response[offset, bytes]
1203
+ offset += bytes
1204
+
1205
+ Rails.logger.info "📦 Message #{i + 1}: msg_id=#{msg_id}, seqno=#{seqno}, bytes=#{bytes}"
1206
+
1207
+ # Recursively parse message body
1208
+ result = parse_message_response(msg_body)
1209
+ return result if result[:success] || !result[:error].include?('ignored')
1210
+ end
1211
+
1212
+ { success: false, error: "container_processed" }
1213
+ end
1214
+
1215
+ # Parse rpc_result for message responses
1216
+ def parse_rpc_result_message_response(response)
1217
+ Rails.logger.info "📦 Parsing rpc_result for message response"
1218
+
1219
+ offset = 4 # Skip constructor
1220
+ req_msg_id = response[offset, 8].unpack('q<')[0]
1221
+ offset += 8
1222
+
1223
+ result_data = response[offset..-1]
1224
+ Rails.logger.info "📦 RPC result for msg_id=#{req_msg_id}, result=#{result_data.length} bytes"
1225
+
1226
+ # Recursively parse result data
1227
+ parse_message_response(result_data)
1228
+ end
1229
+
1230
+ # Send updates.getDifference request
1231
+ def send_updates_get_difference(pts, date, qts)
1232
+ Rails.logger.info "📥 Building updates.getDifference request"
1233
+
1234
+ # Build updates.getDifference using PURE TL schemas
1235
+ updates_data = build_updates_get_difference_request(pts, date, qts)
1236
+ Rails.logger.info "📥 Built updates.getDifference request: #{updates_data.length} bytes"
1237
+
1238
+ begin
1239
+ response = @encrypted_sender.send(updates_data)
1240
+ parse_updates_response(response)
1241
+ rescue => e
1242
+ Rails.logger.error "❌ updates.getDifference error: #{e.class}: #{e.message}"
1243
+ { success: false, error: e.message }
1244
+ end
1245
+ end
1246
+
1247
+ # Build updates.getDifference request using PURE TL schemas
1248
+ def build_updates_get_difference_request(pts, date, qts)
1249
+ Rails.logger.info "🔧 Building updates.getDifference using PURE TL SCHEMAS"
1250
+
1251
+ # Use PURE TL schema for updates.getDifference
1252
+ # updates.getDifference#25939651 flags:# pts:int pts_total_limit:flags.0?int date:int qts:int = updates.Difference;
1253
+ updates_request = Telegram::TLObject.serialize('updates.getDifference',
1254
+ flags: 0, # No pts_total_limit
1255
+ pts: pts,
1256
+ date: date,
1257
+ qts: qts
1258
+ )
1259
+
1260
+ Rails.logger.info "✅ Built updates.getDifference: #{updates_request.length} bytes"
1261
+ updates_request
1262
+ end
1263
+
1264
+ # Parse updates.getDifference response
1265
+ def parse_updates_response(response)
1266
+ Rails.logger.info "📥 Parsing updates.getDifference response: #{response.length} bytes"
1267
+ Rails.logger.info "🔍 Response HEX: #{response.unpack('H*')[0]}"
1268
+
1269
+ return { success: false, error: "Empty response" } if response.length < 4
1270
+
1271
+ # Read constructor
1272
+ constructor = response[0, 4].unpack('L<')[0]
1273
+ Rails.logger.info "🔍 Response constructor: 0x#{constructor.to_s(16)}"
1274
+
1275
+ case constructor
1276
+ when 0x73f1f8dc # msg_container
1277
+ parse_msg_container_updates_response(response)
1278
+ when 0xf35c6d01 # rpc_result
1279
+ parse_rpc_result_updates_response(response)
1280
+ when 0x2144ca19 # rpc_error
1281
+ parse_rpc_error_response(response)
1282
+ when 0xedab447b # bad_server_salt
1283
+ parse_bad_server_salt_response(response)
1284
+ when 0x00f49ca0 # updates.differenceEmpty
1285
+ Rails.logger.info "✅ Got updates.differenceEmpty - no new updates"
1286
+ { success: true, new_messages: [], updates: [] }
1287
+ when 0x5d75a138 # updates.difference
1288
+ Rails.logger.info "✅ Got updates.difference"
1289
+ parse_updates_difference(response)
1290
+ when 0xa8fb1981 # updates.differenceSlice
1291
+ Rails.logger.info "✅ Got updates.differenceSlice"
1292
+ parse_updates_difference_slice(response)
1293
+ when 0x4afe8f6d # updates.differenceTooLong
1294
+ Rails.logger.info "✅ Got updates.differenceTooLong"
1295
+ parse_updates_difference_too_long(response)
1296
+ else
1297
+ Rails.logger.error "❌ Unknown updates.getDifference response: 0x#{constructor.to_s(16)}"
1298
+ { success: false, error: "Unknown response type" }
1299
+ end
1300
+ end
1301
+
1302
+ # Parse updates.difference response
1303
+ def parse_updates_difference(response)
1304
+ Rails.logger.info "📥 Parsing updates.difference"
1305
+
1306
+ # updates.difference#5d75a138 new_messages:Vector<Message> new_encrypted_messages:Vector<EncryptedMessage> other_updates:Vector<Update> chats:Vector<Chat> users:Vector<User> state:updates.State = updates.Difference;
1307
+ offset = 4 # Skip constructor
1308
+
1309
+ # Parse new_messages vector
1310
+ new_messages_count = response[offset, 4].unpack('L<')[0]
1311
+ offset += 4
1312
+
1313
+ Rails.logger.info "📥 Found #{new_messages_count} new messages"
1314
+
1315
+ new_messages = []
1316
+ new_messages_count.times do |i|
1317
+ # Parse Message object (simplified)
1318
+ message_constructor = response[offset, 4].unpack('L<')[0]
1319
+ offset += 4
1320
+
1321
+ if message_constructor == 0x38116ee0 # message
1322
+ # Simplified message parsing
1323
+ message_id = response[offset, 4].unpack('i<')[0]
1324
+ offset += 4
1325
+
1326
+ from_id = response[offset, 8].unpack('q<')[0]
1327
+ offset += 8
1328
+
1329
+ to_id = response[offset, 8].unpack('q<')[0]
1330
+ offset += 8
1331
+
1332
+ date = response[offset, 4].unpack('i<')[0]
1333
+ offset += 4
1334
+
1335
+ # Get message text (basic parsing)
1336
+ message_len = response[offset, 1].unpack('C')[0]
1337
+ offset += 1
1338
+
1339
+ if message_len < 254
1340
+ message_text = response[offset, message_len].force_encoding('UTF-8')
1341
+ offset += message_len
1342
+ # Align to 4-byte boundary
1343
+ offset += (4 - ((message_len + 1) % 4)) % 4
1344
+ else
1345
+ message_text = "Message"
1346
+ offset += 100 # Skip to avoid parsing complexity
1347
+ end
1348
+
1349
+ new_messages << {
1350
+ id: message_id,
1351
+ from_id: from_id,
1352
+ to_id: to_id,
1353
+ date: date,
1354
+ message: message_text
1355
+ }
1356
+
1357
+ Rails.logger.info "📥 New message #{i + 1}: '#{message_text}' from #{from_id}"
1358
+ else
1359
+ Rails.logger.debug "📥 Skipping message #{i + 1} with constructor 0x#{message_constructor.to_s(16)}"
1360
+ offset += 100 # Skip message data
1361
+ end
1362
+ end
1363
+
1364
+ # Skip encrypted messages, other updates, chats for now
1365
+ # Just extract state at the end
1366
+
1367
+ # For simplicity, assume state is at a known offset or skip complex parsing
1368
+ # In real implementation, would need to parse all vectors properly
1369
+
1370
+ Rails.logger.info "✅ Parsed #{new_messages.length} new messages"
1371
+ {
1372
+ success: true,
1373
+ new_messages: new_messages,
1374
+ state: { pts: 1, date: Time.now.to_i, qts: 1 } # Simplified state
1375
+ }
1376
+ end
1377
+
1378
+ # Parse updates.differenceSlice response
1379
+ def parse_updates_difference_slice(response)
1380
+ Rails.logger.info "📥 Parsing updates.differenceSlice (simplified as difference)"
1381
+ # For now, parse same as regular difference
1382
+ parse_updates_difference(response)
1383
+ end
1384
+
1385
+ # Parse updates.differenceTooLong response
1386
+ def parse_updates_difference_too_long(response)
1387
+ Rails.logger.info "📥 Parsing updates.differenceTooLong"
1388
+
1389
+ # updates.differenceTooLong#4afe8f6d pts:int = updates.Difference;
1390
+ offset = 4 # Skip constructor
1391
+ pts = response[offset, 4].unpack('i<')[0]
1392
+
1393
+ Rails.logger.info "📥 Too many updates, pts=#{pts}"
1394
+ {
1395
+ success: true,
1396
+ new_messages: [],
1397
+ too_long: true,
1398
+ state: { pts: pts, date: Time.now.to_i, qts: 1 }
1399
+ }
1400
+ end
1401
+
1402
+ # Parse msg_container for updates responses
1403
+ def parse_msg_container_updates_response(response)
1404
+ Rails.logger.info "📦 Parsing msg_container for updates response"
1405
+
1406
+ offset = 4 # Skip constructor
1407
+ count = response[offset, 4].unpack('i<')[0]
1408
+ offset += 4
1409
+
1410
+ Rails.logger.info "📦 Container has #{count} messages"
1411
+
1412
+ count.times do |i|
1413
+ Rails.logger.info "📦 Parsing message #{i + 1}/#{count}"
1414
+
1415
+ msg_id = response[offset, 8].unpack('q<')[0]
1416
+ offset += 8
1417
+
1418
+ seqno = response[offset, 4].unpack('i<')[0]
1419
+ offset += 4
1420
+
1421
+ bytes = response[offset, 4].unpack('i<')[0]
1422
+ offset += 4
1423
+
1424
+ msg_body = response[offset, bytes]
1425
+ offset += bytes
1426
+
1427
+ Rails.logger.info "📦 Message #{i + 1}: msg_id=#{msg_id}, seqno=#{seqno}, bytes=#{bytes}"
1428
+
1429
+ # Recursively parse message body
1430
+ result = parse_updates_response(msg_body)
1431
+ return result if result[:success] || !result[:error].include?('ignored')
1432
+ end
1433
+
1434
+ { success: false, error: "container_processed" }
1435
+ end
1436
+
1437
+ # Parse rpc_result for updates responses
1438
+ def parse_rpc_result_updates_response(response)
1439
+ Rails.logger.info "📦 Parsing rpc_result for updates response"
1440
+
1441
+ offset = 4 # Skip constructor
1442
+ req_msg_id = response[offset, 8].unpack('q<')[0]
1443
+ offset += 8
1444
+
1445
+ result_data = response[offset..-1]
1446
+ Rails.logger.info "📦 RPC result for msg_id=#{req_msg_id}, result=#{result_data.length} bytes"
1447
+
1448
+ # Recursively parse result data
1449
+ parse_updates_response(result_data)
1450
+ end
1451
+
1452
+ # Re-export classes for backward compatibility
1453
+ TcpFullConnection = Telegram::Connection::TcpFullConnection
1454
+ MTProtoPlainSender = Telegram::Senders::MTProtoPlainSender
1455
+ MTProtoEncryptedSender = Telegram::Senders::MTProtoEncryptedSender
1456
+ end