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.
- checksums.yaml +7 -0
- data/README.md +188 -0
- data/Rakefile +8 -0
- data/examples/complete_demo.rb +211 -0
- data/lib/telegram/auth.rb +438 -0
- data/lib/telegram/binary_reader.rb +156 -0
- data/lib/telegram/connection/tcp_full_connection.rb +248 -0
- data/lib/telegram/crypto.rb +323 -0
- data/lib/telegram/crypto_rsa_keys.rb +86 -0
- data/lib/telegram/senders/mtproto_encrypted_sender.rb +234 -0
- data/lib/telegram/senders/mtproto_plain_sender.rb +116 -0
- data/lib/telegram/serialization.rb +106 -0
- data/lib/telegram/tl/api.tl +2750 -0
- data/lib/telegram/tl/mtproto.tl +116 -0
- data/lib/telegram/tl_object.rb +132 -0
- data/lib/telegram/tl_reader.rb +120 -0
- data/lib/telegram/tl_schema.rb +113 -0
- data/lib/telegram/tl_writer.rb +103 -0
- data/lib/telegram_m_t_proto_clean.rb +1456 -0
- data/lib/telegram_mtproto/ruby/version.rb +9 -0
- data/lib/telegram_mtproto/ruby.rb +12 -0
- data/lib/telegram_mtproto/version.rb +5 -0
- data/lib/telegram_mtproto.rb +20 -0
- data/lib/telegram_plain_tcp.rb +92 -0
- data/sig/telegram/mtproto/ruby.rbs +8 -0
- metadata +69 -0
@@ -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
|