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,234 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'securerandom'
|
4
|
+
require 'digest'
|
5
|
+
require_relative '../connection/tcp_full_connection'
|
6
|
+
require_relative '../crypto'
|
7
|
+
|
8
|
+
module Telegram
|
9
|
+
module Senders
|
10
|
+
# Encrypted MTProto sender EXACTLY like Telethon MTProtoSender
|
11
|
+
class MTProtoEncryptedSender
|
12
|
+
def initialize(ip, port, auth_key, time_offset = 0)
|
13
|
+
@ip = ip
|
14
|
+
@port = port
|
15
|
+
@auth_key = auth_key
|
16
|
+
@time_offset = time_offset # Time offset from server like Telethon
|
17
|
+
@connection = nil
|
18
|
+
@last_msg_id = 0
|
19
|
+
session_bytes = SecureRandom.random_bytes(8)
|
20
|
+
@session_id = session_bytes.unpack('q<')[0]
|
21
|
+
Rails.logger.debug "🔐 Generated session_id: #{@session_id} from bytes: #{session_bytes.unpack('H*')[0]}"
|
22
|
+
Rails.logger.debug "⏰ Using time_offset: #{@time_offset} seconds"
|
23
|
+
@sequence = 0
|
24
|
+
@salt = 0 # Initialize salt to 0 like Telethon
|
25
|
+
Rails.logger.debug "🧂 Initialized salt to 0"
|
26
|
+
end
|
27
|
+
|
28
|
+
def send(request_body)
|
29
|
+
Rails.logger.info "🔐 MTProtoEncryptedSender.send() starting, request_body=#{request_body.length} bytes (#{request_body.class})"
|
30
|
+
|
31
|
+
# Ensure request_body is a binary string
|
32
|
+
request_body = request_body.force_encoding('ASCII-8BIT') if request_body.is_a?(String)
|
33
|
+
|
34
|
+
@connection ||= Telegram::Connection::TcpFullConnection.new(@ip, @port)
|
35
|
+
|
36
|
+
Rails.logger.info "🔐 Current TCP connection counter: #{@connection.instance_variable_get(:@send_counter) || 'unknown'}"
|
37
|
+
|
38
|
+
# Create encrypted message EXACTLY like Telethon
|
39
|
+
msg_id = get_new_msg_id
|
40
|
+
seq_no = get_sequence_number(content_related: true)
|
41
|
+
|
42
|
+
Rails.logger.info "🔐 Encrypted message params: session_id=#{@session_id}, msg_id=#{msg_id}, seq_no=#{seq_no}, body_len=#{request_body.length}"
|
43
|
+
Rails.logger.info "🔐 Auth key length: #{@auth_key.length} bytes"
|
44
|
+
Rails.logger.info "🔐 Auth key first 32 bytes: #{@auth_key[0, 32].unpack('H*')[0]}"
|
45
|
+
|
46
|
+
# Plain message EXACTLY like Telethon MTProtoState.encrypt_message_data (line 135)
|
47
|
+
begin
|
48
|
+
# In Telethon: data = struct.pack('<qq', self.salt, self.id) + data
|
49
|
+
# where 'data' already contains: msg_id + seq_no + length + body
|
50
|
+
|
51
|
+
# First, build the inner data (msg_id + seq_no + length + body)
|
52
|
+
inner_data = [msg_id].pack('q<') +
|
53
|
+
[seq_no].pack('i<') +
|
54
|
+
[request_body.length].pack('i<') +
|
55
|
+
request_body
|
56
|
+
|
57
|
+
# Then add salt + session_id before it (like Telethon line 135)
|
58
|
+
plain_message = [@salt].pack('q<') + [@session_id].pack('q<') + inner_data
|
59
|
+
|
60
|
+
Rails.logger.info "🔐 MTProto 2.0 encrypted message structure:"
|
61
|
+
Rails.logger.info " salt: #{@salt} (8 bytes)"
|
62
|
+
Rails.logger.info " session_id: #{@session_id} (8 bytes)"
|
63
|
+
Rails.logger.info " msg_id: #{msg_id} (8 bytes)"
|
64
|
+
Rails.logger.info " seq_no: #{seq_no} (4 bytes)"
|
65
|
+
Rails.logger.info " length: #{request_body.length} (4 bytes)"
|
66
|
+
Rails.logger.info " body: #{request_body.length} bytes"
|
67
|
+
Rails.logger.info " total plain_message: #{plain_message.length} bytes"
|
68
|
+
Rails.logger.debug "🔐 Plain message HEX: #{plain_message.unpack('H*')[0]}"
|
69
|
+
rescue => e
|
70
|
+
Rails.logger.error "❌ Failed to pack encrypted message: #{e.message}"
|
71
|
+
Rails.logger.error " session_id: #{@session_id} (#{@session_id.class})"
|
72
|
+
Rails.logger.error " msg_id: #{msg_id} (#{msg_id.class})"
|
73
|
+
Rails.logger.error " seq_no: #{seq_no} (#{seq_no.class})"
|
74
|
+
Rails.logger.error " body_len: #{request_body.length} (#{request_body.length.class})"
|
75
|
+
raise
|
76
|
+
end
|
77
|
+
|
78
|
+
# Add padding like Telethon (line 136): padding = os.urandom(-(len(data) + 12) % 16 + 12)
|
79
|
+
# This ensures minimum 12 bytes padding, aligned to 16-byte boundary
|
80
|
+
padding_length = (-(plain_message.length + 12) % 16) + 12
|
81
|
+
padding = SecureRandom.random_bytes(padding_length)
|
82
|
+
Rails.logger.debug "🔐 Telethon-style padding: message=#{plain_message.length}, padding=#{padding_length}, total=#{plain_message.length + padding_length}"
|
83
|
+
|
84
|
+
# Calculate msg_key from auth_key and message+padding (like Telethon line 140-141)
|
85
|
+
message_with_padding = plain_message + padding
|
86
|
+
msg_key = calculate_msg_key(message_with_padding, true) # true = from client
|
87
|
+
|
88
|
+
# Derive AES key and IV from auth_key and msg_key
|
89
|
+
aes_key, aes_iv = derive_aes_key_iv(msg_key, true)
|
90
|
+
|
91
|
+
# Encrypt with AES-IGE (message + padding)
|
92
|
+
encrypted_data = Telegram::Crypto.encrypt_ige(message_with_padding, aes_key, aes_iv)
|
93
|
+
|
94
|
+
# Final message: auth_key_id + msg_key + encrypted_data
|
95
|
+
auth_key_id = calculate_auth_key_id
|
96
|
+
final_message = [auth_key_id].pack('q<') + msg_key + encrypted_data
|
97
|
+
|
98
|
+
Rails.logger.info "🔐 Sending encrypted MTProto: auth_key_id=#{auth_key_id}, msg_id=#{msg_id}, seq_no=#{seq_no}"
|
99
|
+
Rails.logger.info "🔐 Final message structure:"
|
100
|
+
Rails.logger.info " auth_key_id: #{auth_key_id} (8 bytes)"
|
101
|
+
Rails.logger.info " msg_key: #{msg_key.unpack('H*')[0]} (16 bytes)"
|
102
|
+
Rails.logger.info " encrypted_data: #{encrypted_data.length} bytes"
|
103
|
+
Rails.logger.info " total: #{final_message.length} bytes"
|
104
|
+
Rails.logger.info "🔐 Final message HEX: #{final_message.unpack('H*')[0]}"
|
105
|
+
|
106
|
+
@connection.send(final_message)
|
107
|
+
|
108
|
+
# Read and decrypt response
|
109
|
+
response_body = @connection.recv(timeout: 30)
|
110
|
+
decrypt_response(response_body)
|
111
|
+
end
|
112
|
+
|
113
|
+
private
|
114
|
+
|
115
|
+
def get_new_msg_id
|
116
|
+
# Generate msg_id EXACTLY like Telethon (lines 244-252)
|
117
|
+
now = Time.now.to_f # time.time() equivalent
|
118
|
+
now_adjusted = now + @time_offset # Use server time_offset like Telethon!
|
119
|
+
|
120
|
+
|
121
|
+
seconds = now_adjusted.to_i
|
122
|
+
nanoseconds = ((now_adjusted - seconds) * 1e9).to_i
|
123
|
+
new_msg_id = (seconds << 32) | (nanoseconds << 2)
|
124
|
+
|
125
|
+
if @last_msg_id >= new_msg_id
|
126
|
+
new_msg_id = @last_msg_id + 4
|
127
|
+
end
|
128
|
+
|
129
|
+
@last_msg_id = new_msg_id
|
130
|
+
Rails.logger.debug "🕒 Generated msg_id: #{new_msg_id} (seconds=#{seconds}, nanoseconds=#{nanoseconds})"
|
131
|
+
new_msg_id
|
132
|
+
end
|
133
|
+
|
134
|
+
def get_sequence_number(content_related: false)
|
135
|
+
result = @sequence * 2
|
136
|
+
result += 1 if content_related
|
137
|
+
@sequence += 1 if content_related
|
138
|
+
result
|
139
|
+
end
|
140
|
+
|
141
|
+
def calculate_auth_key_id
|
142
|
+
# auth_key_id = bytes 12-20 of SHA1(auth_key) EXACTLY like Telethon AuthKey (lines 39-42)
|
143
|
+
hash = Digest::SHA1.digest(@auth_key)
|
144
|
+
# Telethon: aux_hash (8 bytes) + skip(4 bytes) + key_id (8 bytes) = bytes 12-20
|
145
|
+
auth_key_id = hash[12, 8].unpack('q<')[0]
|
146
|
+
Rails.logger.debug "🔐 Auth key ID calculation (Telethon format):"
|
147
|
+
Rails.logger.debug " auth_key: #{@auth_key.length} bytes"
|
148
|
+
Rails.logger.debug " sha1: #{hash.unpack('H*')[0]}"
|
149
|
+
Rails.logger.debug " aux_hash: #{hash[0, 8].unpack('Q<')[0]} (bytes 0-8)"
|
150
|
+
Rails.logger.debug " skipped: #{hash[8, 4].unpack('H*')[0]} (bytes 8-12)"
|
151
|
+
Rails.logger.debug " auth_key_id: #{auth_key_id} (bytes 12-20)"
|
152
|
+
auth_key_id
|
153
|
+
end
|
154
|
+
|
155
|
+
def calculate_msg_key(message, from_client)
|
156
|
+
# MTProto 2.0 msg_key calculation like Telethon (line 140-144)
|
157
|
+
# msg_key_large = SHA256(substr(auth_key, 88+x, 32) + message)
|
158
|
+
x = from_client ? 0 : 8
|
159
|
+
auth_key_part = @auth_key[88 + x, 32]
|
160
|
+
msg_key_large = Digest::SHA256.digest(auth_key_part + message)
|
161
|
+
msg_key = msg_key_large[8, 16] # bytes 8-24 of SHA256
|
162
|
+
|
163
|
+
Rails.logger.debug "🔐 Msg key calculation (MTProto 2.0):"
|
164
|
+
Rails.logger.debug " from_client: #{from_client}, x: #{x}"
|
165
|
+
Rails.logger.debug " auth_key_part: #{auth_key_part.unpack('H*')[0][0..20]}..."
|
166
|
+
Rails.logger.debug " message: #{message.length} bytes"
|
167
|
+
Rails.logger.debug " msg_key_large: #{msg_key_large.unpack('H*')[0]}"
|
168
|
+
Rails.logger.debug " msg_key: #{msg_key.unpack('H*')[0]}"
|
169
|
+
|
170
|
+
msg_key
|
171
|
+
end
|
172
|
+
|
173
|
+
def derive_aes_key_iv(msg_key, from_client)
|
174
|
+
# MTProto 2.0 AES key/IV derivation like Telethon _calc_key (lines 100-107)
|
175
|
+
x = from_client ? 0 : 8
|
176
|
+
sha256a = Digest::SHA256.digest(msg_key + @auth_key[x, 36])
|
177
|
+
sha256b = Digest::SHA256.digest(@auth_key[x + 40, 36] + msg_key)
|
178
|
+
|
179
|
+
aes_key = sha256a[0, 8] + sha256b[8, 16] + sha256a[24, 8]
|
180
|
+
aes_iv = sha256b[0, 8] + sha256a[8, 16] + sha256b[24, 8]
|
181
|
+
|
182
|
+
Rails.logger.debug "🔐 AES key/IV derivation (MTProto 2.0):"
|
183
|
+
Rails.logger.debug " from_client: #{from_client}, x: #{x}"
|
184
|
+
Rails.logger.debug " aes_key: #{aes_key.unpack('H*')[0]}"
|
185
|
+
Rails.logger.debug " aes_iv: #{aes_iv.unpack('H*')[0]}"
|
186
|
+
|
187
|
+
[aes_key, aes_iv]
|
188
|
+
end
|
189
|
+
|
190
|
+
def decrypt_response(response_body)
|
191
|
+
return nil if response_body.length < 24 # auth_key_id(8) + msg_key(16)
|
192
|
+
|
193
|
+
# Parse response
|
194
|
+
offset = 0
|
195
|
+
auth_key_id = response_body[offset, 8].unpack('q<')[0]
|
196
|
+
offset += 8
|
197
|
+
|
198
|
+
msg_key = response_body[offset, 16]
|
199
|
+
offset += 16
|
200
|
+
|
201
|
+
encrypted_data = response_body[offset..-1]
|
202
|
+
|
203
|
+
# Derive decryption key/IV
|
204
|
+
aes_key, aes_iv = derive_aes_key_iv(msg_key, false) # false = from server
|
205
|
+
|
206
|
+
# Decrypt
|
207
|
+
plain_data = Telegram::Crypto.decrypt_ige(encrypted_data, aes_key, aes_iv)
|
208
|
+
|
209
|
+
# Parse decrypted message - MTProto 2.0 structure: salt + session_id + msg_id + seq_no + length + message_data
|
210
|
+
offset = 0
|
211
|
+
salt = plain_data[offset, 8].unpack('q<')[0]
|
212
|
+
offset += 8
|
213
|
+
|
214
|
+
session_id = plain_data[offset, 8].unpack('q<')[0]
|
215
|
+
offset += 8
|
216
|
+
|
217
|
+
msg_id = plain_data[offset, 8].unpack('q<')[0]
|
218
|
+
offset += 8
|
219
|
+
|
220
|
+
seq_no = plain_data[offset, 4].unpack('i<')[0]
|
221
|
+
offset += 4
|
222
|
+
|
223
|
+
length = plain_data[offset, 4].unpack('i<')[0]
|
224
|
+
offset += 4
|
225
|
+
|
226
|
+
message_data = plain_data[offset, length]
|
227
|
+
|
228
|
+
Rails.logger.debug "🔓 Decrypted MTProto response: salt=#{salt}, session_id=#{session_id}, msg_id=#{msg_id}, seq_no=#{seq_no}, length=#{length}"
|
229
|
+
|
230
|
+
message_data
|
231
|
+
end
|
232
|
+
end
|
233
|
+
end
|
234
|
+
end
|
@@ -0,0 +1,116 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require_relative '../connection/tcp_full_connection'
|
4
|
+
|
5
|
+
module Telegram
|
6
|
+
module Senders
|
7
|
+
# Plain sender EXACTLY like Telethon MTProtoPlainSender
|
8
|
+
class MTProtoPlainSender
|
9
|
+
class ConnectionError < StandardError; end
|
10
|
+
class SecurityError < StandardError; end
|
11
|
+
|
12
|
+
def initialize(ip, port)
|
13
|
+
@ip = ip
|
14
|
+
@port = port
|
15
|
+
@connection = nil
|
16
|
+
@last_msg_id = 0
|
17
|
+
end
|
18
|
+
|
19
|
+
def send(request_body)
|
20
|
+
Rails.logger.debug "📡 MTProtoPlainSender.send() starting, request_body=#{request_body.length} bytes"
|
21
|
+
|
22
|
+
@connection ||= Telegram::Connection::TcpFullConnection.new(@ip, @port)
|
23
|
+
|
24
|
+
# EXACTLY like Telethon: struct.pack('<qqi', 0, msg_id, len(body)) + body
|
25
|
+
msg_id = get_new_msg_id
|
26
|
+
unencrypted_message = [0].pack('q<') + [msg_id].pack('q<') + [request_body.length].pack('i<') + request_body
|
27
|
+
|
28
|
+
Rails.logger.info "📤 Sending unencrypted MTProto: auth_key_id=0, msg_id=#{msg_id}, len=#{request_body.length}"
|
29
|
+
Rails.logger.info "🔍 MTProto message structure:"
|
30
|
+
Rails.logger.info " auth_key_id: 0 (8 bytes)"
|
31
|
+
Rails.logger.info " msg_id: #{msg_id} (8 bytes)"
|
32
|
+
Rails.logger.info " length: #{request_body.length} (4 bytes)"
|
33
|
+
Rails.logger.info " body: #{request_body.length} bytes"
|
34
|
+
Rails.logger.info " total: #{unencrypted_message.length} bytes"
|
35
|
+
Rails.logger.debug "🔍 MTProto message HEX: #{unencrypted_message.unpack('H*')[0]}"
|
36
|
+
Rails.logger.debug "🔍 Request body HEX: #{request_body.unpack('H*')[0]}"
|
37
|
+
|
38
|
+
@connection.send(unencrypted_message)
|
39
|
+
|
40
|
+
# Read response
|
41
|
+
Rails.logger.info "📥 Waiting for MTProto response..."
|
42
|
+
|
43
|
+
begin
|
44
|
+
body = @connection.recv(timeout: 30)
|
45
|
+
Rails.logger.info "📥 Got MTProto response: #{body.length} bytes"
|
46
|
+
rescue => e
|
47
|
+
Rails.logger.error "❌ MTProto response failed: #{e.class}: #{e.message}"
|
48
|
+
raise
|
49
|
+
end
|
50
|
+
|
51
|
+
if body.length < 20 # auth_key_id(8) + msg_id(8) + length(4) = 20 minimum
|
52
|
+
raise ConnectionError.new("MTProto response too short: #{body.length}")
|
53
|
+
end
|
54
|
+
|
55
|
+
# Parse response EXACTLY like Telethon BinaryReader
|
56
|
+
offset = 0
|
57
|
+
auth_key_id = body[offset, 8].unpack('q<')[0]
|
58
|
+
offset += 8
|
59
|
+
|
60
|
+
unless auth_key_id == 0
|
61
|
+
raise SecurityError.new("Bad auth_key_id: #{auth_key_id}")
|
62
|
+
end
|
63
|
+
|
64
|
+
remote_msg_id = body[offset, 8].unpack('q<')[0]
|
65
|
+
offset += 8
|
66
|
+
|
67
|
+
unless remote_msg_id != 0
|
68
|
+
raise SecurityError.new("Bad msg_id: #{remote_msg_id}")
|
69
|
+
end
|
70
|
+
|
71
|
+
# Update last_msg_id like Telethon to ensure monotonicity
|
72
|
+
if remote_msg_id > @last_msg_id
|
73
|
+
@last_msg_id = remote_msg_id
|
74
|
+
Rails.logger.debug "🕒 Updated last_msg_id from server: #{@last_msg_id}"
|
75
|
+
end
|
76
|
+
|
77
|
+
length = body[offset, 4].unpack('i<')[0]
|
78
|
+
offset += 4
|
79
|
+
|
80
|
+
unless length > 0
|
81
|
+
raise SecurityError.new("Bad length: #{length}")
|
82
|
+
end
|
83
|
+
|
84
|
+
# Extract TL object data
|
85
|
+
if offset + length > body.length
|
86
|
+
raise ConnectionError.new("Response body truncated: need #{length}, have #{body.length - offset}")
|
87
|
+
end
|
88
|
+
|
89
|
+
response_body = body[offset, length]
|
90
|
+
Rails.logger.info "✅ MTProto response parsed: #{response_body.length} bytes TL object"
|
91
|
+
response_body
|
92
|
+
end
|
93
|
+
|
94
|
+
private
|
95
|
+
|
96
|
+
def get_new_msg_id
|
97
|
+
# Generate msg_id EXACTLY like Telethon (lines 244-252)
|
98
|
+
now = Time.now.to_f # time.time() equivalent
|
99
|
+
time_offset = 0 # time_offset for plain sender is 0
|
100
|
+
now_adjusted = now + time_offset
|
101
|
+
|
102
|
+
seconds = now_adjusted.to_i
|
103
|
+
nanoseconds = ((now_adjusted - seconds) * 1e9).to_i
|
104
|
+
new_msg_id = (seconds << 32) | (nanoseconds << 2)
|
105
|
+
|
106
|
+
if @last_msg_id >= new_msg_id
|
107
|
+
new_msg_id = @last_msg_id + 4
|
108
|
+
end
|
109
|
+
|
110
|
+
@last_msg_id = new_msg_id
|
111
|
+
Rails.logger.debug "🕒 Generated msg_id: #{new_msg_id} (seconds=#{seconds}, nanoseconds=#{nanoseconds})"
|
112
|
+
new_msg_id
|
113
|
+
end
|
114
|
+
end
|
115
|
+
end
|
116
|
+
end
|
@@ -0,0 +1,106 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Telegram
|
4
|
+
module Serialization
|
5
|
+
# Convert integer to big-endian byte array EXACTLY like Telethon
|
6
|
+
def self.integer_to_byte_array(value, signed: true)
|
7
|
+
return "\x00".force_encoding('ASCII-8BIT') if value == 0
|
8
|
+
|
9
|
+
# Handle negative numbers with two's complement
|
10
|
+
if signed && value < 0
|
11
|
+
# Calculate the minimum number of bits needed
|
12
|
+
bits_needed = (value.abs - 1).bit_length + 1
|
13
|
+
# Round up to next byte boundary
|
14
|
+
byte_length = (bits_needed + 7) / 8
|
15
|
+
# Convert to unsigned using two's complement
|
16
|
+
value = (1 << (byte_length * 8)) + value
|
17
|
+
end
|
18
|
+
|
19
|
+
# Convert to hex and ensure even length
|
20
|
+
hex = value.to_s(16)
|
21
|
+
hex = '0' + hex if hex.length.odd?
|
22
|
+
|
23
|
+
# Convert hex to bytes (big-endian)
|
24
|
+
bytes = [hex].pack('H*')
|
25
|
+
bytes.force_encoding('ASCII-8BIT')
|
26
|
+
end
|
27
|
+
|
28
|
+
# Convert bytes to integer EXACTLY like Python's int.from_bytes
|
29
|
+
def self.bytes_to_int(bytes, byteorder, signed: false)
|
30
|
+
bytes = bytes.force_encoding('ASCII-8BIT')
|
31
|
+
|
32
|
+
if byteorder == 'little'
|
33
|
+
bytes = bytes.reverse
|
34
|
+
elsif byteorder != 'big'
|
35
|
+
raise ArgumentError, "byteorder must be 'big' or 'little'"
|
36
|
+
end
|
37
|
+
|
38
|
+
# Convert to integer (always unsigned first)
|
39
|
+
value = bytes.unpack1('H*').to_i(16)
|
40
|
+
|
41
|
+
# Handle signed conversion
|
42
|
+
if signed && bytes.length > 0
|
43
|
+
sign_bit = 1 << ((bytes.length * 8) - 1)
|
44
|
+
value -= (1 << (bytes.length * 8)) if value >= sign_bit
|
45
|
+
end
|
46
|
+
|
47
|
+
value
|
48
|
+
end
|
49
|
+
|
50
|
+
# Serialize int128 to little endian bytes EXACTLY like Telethon
|
51
|
+
def self.serialize_int128(value)
|
52
|
+
# EXACTLY copy Python's int.to_bytes(16, 'little', signed=True) logic
|
53
|
+
if value < 0
|
54
|
+
# Convert negative to unsigned (two's complement)
|
55
|
+
value = (1 << 128) + value
|
56
|
+
end
|
57
|
+
|
58
|
+
# Split into two 64-bit parts for little endian
|
59
|
+
low = value & 0xFFFFFFFFFFFFFFFF
|
60
|
+
high = (value >> 64) & 0xFFFFFFFFFFFFFFFF
|
61
|
+
|
62
|
+
# Pack as two little-endian 64-bit integers
|
63
|
+
[low, high].pack('Q<Q<')
|
64
|
+
end
|
65
|
+
|
66
|
+
# Serialize int256 to little endian bytes EXACTLY like Telethon
|
67
|
+
def self.serialize_int256(value)
|
68
|
+
# int256 = 32 bytes little-endian signed integer
|
69
|
+
# EXACTLY like Python's int.to_bytes(32, 'little', signed=True)
|
70
|
+
if value < 0
|
71
|
+
# Handle negative numbers (two's complement for 256-bit)
|
72
|
+
value = (1 << 256) + value
|
73
|
+
end
|
74
|
+
|
75
|
+
# Split into four 64-bit parts for little endian
|
76
|
+
parts = []
|
77
|
+
4.times do
|
78
|
+
parts << (value & 0xFFFFFFFFFFFFFFFF)
|
79
|
+
value >>= 64
|
80
|
+
end
|
81
|
+
|
82
|
+
# Pack as four little-endian 64-bit integers
|
83
|
+
parts.pack('Q<Q<Q<Q<')
|
84
|
+
end
|
85
|
+
|
86
|
+
# Pack TL string EXACTLY like Telethon
|
87
|
+
def self.pack_tl_string(data)
|
88
|
+
# Convert to byte string if needed
|
89
|
+
byte_data = data.dup.force_encoding('ASCII-8BIT')
|
90
|
+
|
91
|
+
result = if byte_data.length >= 254
|
92
|
+
# Long form: 254 + 3-byte length + data + padding
|
93
|
+
length_bytes = [byte_data.length].pack('L<')[0, 3] # First 3 bytes of little-endian uint32
|
94
|
+
[254].pack('C') + length_bytes + byte_data
|
95
|
+
else
|
96
|
+
# Short form: length + data + padding
|
97
|
+
[byte_data.length].pack('C') + byte_data
|
98
|
+
end
|
99
|
+
|
100
|
+
# Padding to 4-byte boundary (EXACTLY like Telethon)
|
101
|
+
padding = (4 - (result.length % 4)) % 4
|
102
|
+
result += "\x00" * padding
|
103
|
+
result
|
104
|
+
end
|
105
|
+
end
|
106
|
+
end
|