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,116 @@
|
|
1
|
+
// Core types (no need to gen)
|
2
|
+
|
3
|
+
//vector#1cb5c415 {t:Type} # [ t ] = Vector t;
|
4
|
+
|
5
|
+
///////////////////////////////
|
6
|
+
/// Authorization key creation
|
7
|
+
///////////////////////////////
|
8
|
+
|
9
|
+
resPQ#05162463 nonce:int128 server_nonce:int128 pq:string server_public_key_fingerprints:Vector<long> = ResPQ;
|
10
|
+
|
11
|
+
p_q_inner_data#83c95aec pq:string p:string q:string nonce:int128 server_nonce:int128 new_nonce:int256 = P_Q_inner_data;
|
12
|
+
p_q_inner_data_dc#a9f55f95 pq:string p:string q:string nonce:int128 server_nonce:int128 new_nonce:int256 dc:int = P_Q_inner_data;
|
13
|
+
p_q_inner_data_temp#3c6a84d4 pq:string p:string q:string nonce:int128 server_nonce:int128 new_nonce:int256 expires_in:int = P_Q_inner_data;
|
14
|
+
p_q_inner_data_temp_dc#56fddf88 pq:string p:string q:string nonce:int128 server_nonce:int128 new_nonce:int256 dc:int expires_in:int = P_Q_inner_data;
|
15
|
+
|
16
|
+
bind_auth_key_inner#75a3f765 nonce:long temp_auth_key_id:long perm_auth_key_id:long temp_session_id:long expires_at:int = BindAuthKeyInner;
|
17
|
+
|
18
|
+
server_DH_params_fail#79cb045d nonce:int128 server_nonce:int128 new_nonce_hash:int128 = Server_DH_Params;
|
19
|
+
server_DH_params_ok#d0e8075c nonce:int128 server_nonce:int128 encrypted_answer:string = Server_DH_Params;
|
20
|
+
|
21
|
+
server_DH_inner_data#b5890dba nonce:int128 server_nonce:int128 g:int dh_prime:string g_a:string server_time:int = Server_DH_inner_data;
|
22
|
+
|
23
|
+
client_DH_inner_data#6643b654 nonce:int128 server_nonce:int128 retry_id:long g_b:string = Client_DH_Inner_Data;
|
24
|
+
|
25
|
+
dh_gen_ok#3bcbf734 nonce:int128 server_nonce:int128 new_nonce_hash1:int128 = Set_client_DH_params_answer;
|
26
|
+
dh_gen_retry#46dc1fb9 nonce:int128 server_nonce:int128 new_nonce_hash2:int128 = Set_client_DH_params_answer;
|
27
|
+
dh_gen_fail#a69dae02 nonce:int128 server_nonce:int128 new_nonce_hash3:int128 = Set_client_DH_params_answer;
|
28
|
+
|
29
|
+
destroy_auth_key_ok#f660e1d4 = DestroyAuthKeyRes;
|
30
|
+
destroy_auth_key_none#0a9f2259 = DestroyAuthKeyRes;
|
31
|
+
destroy_auth_key_fail#ea109b13 = DestroyAuthKeyRes;
|
32
|
+
|
33
|
+
---functions---
|
34
|
+
|
35
|
+
req_pq#60469778 nonce:int128 = ResPQ;
|
36
|
+
req_pq_multi#be7e8ef1 nonce:int128 = ResPQ;
|
37
|
+
|
38
|
+
req_DH_params#d712e4be nonce:int128 server_nonce:int128 p:string q:string public_key_fingerprint:long encrypted_data:string = Server_DH_Params;
|
39
|
+
|
40
|
+
set_client_DH_params#f5045f1f nonce:int128 server_nonce:int128 encrypted_data:string = Set_client_DH_params_answer;
|
41
|
+
|
42
|
+
destroy_auth_key#d1435160 = DestroyAuthKeyRes;
|
43
|
+
|
44
|
+
///////////////////////////////
|
45
|
+
////////////// System messages
|
46
|
+
///////////////////////////////
|
47
|
+
|
48
|
+
---types---
|
49
|
+
|
50
|
+
msgs_ack#62d6b459 msg_ids:Vector<long> = MsgsAck;
|
51
|
+
|
52
|
+
bad_msg_notification#a7eff811 bad_msg_id:long bad_msg_seqno:int error_code:int = BadMsgNotification;
|
53
|
+
bad_server_salt#edab447b bad_msg_id:long bad_msg_seqno:int error_code:int new_server_salt:long = BadMsgNotification;
|
54
|
+
|
55
|
+
msgs_state_req#da69fb52 msg_ids:Vector<long> = MsgsStateReq;
|
56
|
+
msgs_state_info#04deb57d req_msg_id:long info:string = MsgsStateInfo;
|
57
|
+
msgs_all_info#8cc0d131 msg_ids:Vector<long> info:string = MsgsAllInfo;
|
58
|
+
|
59
|
+
msg_detailed_info#276d3ec6 msg_id:long answer_msg_id:long bytes:int status:int = MsgDetailedInfo;
|
60
|
+
msg_new_detailed_info#809db6df answer_msg_id:long bytes:int status:int = MsgDetailedInfo;
|
61
|
+
|
62
|
+
msg_resend_req#7d861a08 msg_ids:Vector<long> = MsgResendReq;
|
63
|
+
|
64
|
+
//rpc_result#f35c6d01 req_msg_id:long result:Object = RpcResult; // parsed manually
|
65
|
+
|
66
|
+
rpc_error#2144ca19 error_code:int error_message:string = RpcError;
|
67
|
+
|
68
|
+
rpc_answer_unknown#5e2ad36e = RpcDropAnswer;
|
69
|
+
rpc_answer_dropped_running#cd78e586 = RpcDropAnswer;
|
70
|
+
rpc_answer_dropped#a43ad8b7 msg_id:long seq_no:int bytes:int = RpcDropAnswer;
|
71
|
+
|
72
|
+
future_salt#0949d9dc valid_since:int valid_until:int salt:long = FutureSalt;
|
73
|
+
future_salts#ae500895 req_msg_id:long now:int salts:vector<future_salt> = FutureSalts;
|
74
|
+
|
75
|
+
pong#347773c5 msg_id:long ping_id:long = Pong;
|
76
|
+
|
77
|
+
destroy_session_ok#e22045fc session_id:long = DestroySessionRes;
|
78
|
+
destroy_session_none#62d350c9 session_id:long = DestroySessionRes;
|
79
|
+
|
80
|
+
new_session_created#9ec20908 first_msg_id:long unique_id:long server_salt:long = NewSession;
|
81
|
+
|
82
|
+
//message msg_id:long seqno:int bytes:int body:Object = Message; // parsed manually
|
83
|
+
//msg_container#73f1f8dc messages:vector<message> = MessageContainer; // parsed manually
|
84
|
+
//msg_copy#e06046b2 orig_message:Message = MessageCopy; // parsed manually, not used - use msg_container
|
85
|
+
//gzip_packed#3072cfa1 packed_data:string = Object; // parsed manually
|
86
|
+
|
87
|
+
http_wait#9299359f max_delay:int wait_after:int max_wait:int = HttpWait;
|
88
|
+
|
89
|
+
//ipPort ipv4:int port:int = IpPort;
|
90
|
+
//help.configSimple#d997c3c5 date:int expires:int dc_id:int ip_port_list:Vector<ipPort> = help.ConfigSimple;
|
91
|
+
|
92
|
+
ipPort#d433ad73 ipv4:int port:int = IpPort;
|
93
|
+
ipPortSecret#37982646 ipv4:int port:int secret:bytes = IpPort;
|
94
|
+
accessPointRule#4679b65f phone_prefix_rules:string dc_id:int ips:vector<IpPort> = AccessPointRule;
|
95
|
+
help.configSimple#5a592a6c date:int expires:int rules:vector<AccessPointRule> = help.ConfigSimple;
|
96
|
+
|
97
|
+
tlsClientHello blocks:vector<TlsBlock> = TlsClientHello;
|
98
|
+
|
99
|
+
tlsBlockString data:string = TlsBlock;
|
100
|
+
tlsBlockRandom length:int = TlsBlock;
|
101
|
+
tlsBlockZero length:int = TlsBlock;
|
102
|
+
tlsBlockDomain = TlsBlock;
|
103
|
+
tlsBlockGrease seed:int = TlsBlock;
|
104
|
+
tlsBlockPublicKey = TlsBlock;
|
105
|
+
tlsBlockScope entries:Vector<TlsBlock> = TlsBlock;
|
106
|
+
|
107
|
+
---functions---
|
108
|
+
|
109
|
+
rpc_drop_answer#58e4a740 req_msg_id:long = RpcDropAnswer;
|
110
|
+
|
111
|
+
get_future_salts#b921bd04 num:int = FutureSalts;
|
112
|
+
|
113
|
+
ping#7abe77ec ping_id:long = Pong;
|
114
|
+
ping_delay_disconnect#f3427b8c ping_id:long disconnect_delay:int = Pong;
|
115
|
+
|
116
|
+
destroy_session#e7512126 session_id:long = DestroySessionRes;
|
@@ -0,0 +1,132 @@
|
|
1
|
+
require_relative 'tl_schema'
|
2
|
+
require_relative 'serialization'
|
3
|
+
|
4
|
+
module Telegram
|
5
|
+
class TLObject
|
6
|
+
include Serialization
|
7
|
+
|
8
|
+
@@schema = nil
|
9
|
+
|
10
|
+
def self.schema
|
11
|
+
@@schema ||= TLSchema.new
|
12
|
+
end
|
13
|
+
|
14
|
+
def self.serialize(constructor_name, **params)
|
15
|
+
constructor = schema.get_constructor(constructor_name)
|
16
|
+
raise "Unknown TL constructor: #{constructor_name}" unless constructor
|
17
|
+
|
18
|
+
data = ''.b
|
19
|
+
|
20
|
+
# Write constructor ID
|
21
|
+
data += [constructor[:id]].pack('L<')
|
22
|
+
|
23
|
+
# Calculate flags value first
|
24
|
+
flags_value = 0
|
25
|
+
constructor[:params].each do |param_def|
|
26
|
+
if param_def[:optional] && param_def[:flag_bit]
|
27
|
+
param_name = param_def[:name].to_sym
|
28
|
+
param_value = params[param_name]
|
29
|
+
if param_value && param_value != false
|
30
|
+
flags_value |= (1 << param_def[:flag_bit])
|
31
|
+
end
|
32
|
+
end
|
33
|
+
end
|
34
|
+
|
35
|
+
# Write parameters in order defined in schema
|
36
|
+
constructor[:params].each do |param_def|
|
37
|
+
param_name = param_def[:name].to_sym
|
38
|
+
param_type = param_def[:type]
|
39
|
+
param_value = params[param_name]
|
40
|
+
|
41
|
+
# Handle flags parameter specially
|
42
|
+
if param_name == :flags
|
43
|
+
data += [flags_value].pack('L<')
|
44
|
+
next
|
45
|
+
end
|
46
|
+
|
47
|
+
# Skip optional parameters if not provided or flag bit not set
|
48
|
+
if param_def[:optional]
|
49
|
+
if param_def[:flag_bit]
|
50
|
+
# Check if flag bit is set
|
51
|
+
flag_set = (flags_value & (1 << param_def[:flag_bit])) != 0
|
52
|
+
next unless flag_set && param_value
|
53
|
+
else
|
54
|
+
next unless param_value
|
55
|
+
end
|
56
|
+
else
|
57
|
+
# Required parameter
|
58
|
+
raise "Missing required parameter: #{param_name}" if param_value.nil?
|
59
|
+
end
|
60
|
+
|
61
|
+
data += serialize_param(param_value, param_type)
|
62
|
+
end
|
63
|
+
|
64
|
+
data
|
65
|
+
end
|
66
|
+
|
67
|
+
def self.serialize_param(value, type)
|
68
|
+
case type
|
69
|
+
when 'int'
|
70
|
+
[value].pack('l<')
|
71
|
+
when 'long'
|
72
|
+
[value].pack('q<')
|
73
|
+
when 'int128'
|
74
|
+
Serialization.serialize_int128(value)
|
75
|
+
when 'int256'
|
76
|
+
Serialization.serialize_int256(value)
|
77
|
+
when 'string'
|
78
|
+
Serialization.pack_tl_string(value)
|
79
|
+
when 'bytes'
|
80
|
+
Serialization.pack_tl_string(value)
|
81
|
+
else
|
82
|
+
if type.start_with?('Vector<')
|
83
|
+
# Handle vectors
|
84
|
+
Serialization.pack_tl_string(value) # Simplified for now
|
85
|
+
elsif value.is_a?(String) && value.encoding == Encoding::ASCII_8BIT
|
86
|
+
# Already serialized TL object - use as-is
|
87
|
+
value
|
88
|
+
else
|
89
|
+
# Try to serialize as nested TL object
|
90
|
+
if schema.get_constructor(type)
|
91
|
+
# It's a known TL type, serialize it
|
92
|
+
serialize(type, **value)
|
93
|
+
else
|
94
|
+
# Unknown type, try as bytes
|
95
|
+
Serialization.pack_tl_string(value.to_s)
|
96
|
+
end
|
97
|
+
end
|
98
|
+
end
|
99
|
+
end
|
100
|
+
|
101
|
+
# Convenience methods for common MTProto objects
|
102
|
+
def self.req_pq_multi(nonce:)
|
103
|
+
serialize('req_pq_multi', nonce: nonce)
|
104
|
+
end
|
105
|
+
|
106
|
+
def self.p_q_inner_data(pq:, p:, q:, nonce:, server_nonce:, new_nonce:)
|
107
|
+
serialize('p_q_inner_data',
|
108
|
+
pq: pq, p: p, q: q,
|
109
|
+
nonce: nonce, server_nonce: server_nonce, new_nonce: new_nonce)
|
110
|
+
end
|
111
|
+
|
112
|
+
def self.req_dh_params(nonce:, server_nonce:, p:, q:, public_key_fingerprint:, encrypted_data:)
|
113
|
+
serialize('req_DH_params',
|
114
|
+
nonce: nonce, server_nonce: server_nonce,
|
115
|
+
p: p, q: q,
|
116
|
+
public_key_fingerprint: public_key_fingerprint,
|
117
|
+
encrypted_data: encrypted_data)
|
118
|
+
end
|
119
|
+
|
120
|
+
def self.client_dh_inner_data(nonce:, server_nonce:, retry_id:, g_b:)
|
121
|
+
serialize('client_DH_inner_data',
|
122
|
+
nonce: nonce, server_nonce: server_nonce,
|
123
|
+
retry_id: retry_id, g_b: g_b)
|
124
|
+
end
|
125
|
+
|
126
|
+
def self.set_client_dh_params(nonce:, server_nonce:, encrypted_data:)
|
127
|
+
serialize('set_client_DH_params',
|
128
|
+
nonce: nonce, server_nonce: server_nonce,
|
129
|
+
encrypted_data: encrypted_data)
|
130
|
+
end
|
131
|
+
end
|
132
|
+
end
|
@@ -0,0 +1,120 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Telegram
|
4
|
+
# Simple TL (Type Language) binary reader for Telegram MTProto
|
5
|
+
# Based on Telethon's BinaryReader
|
6
|
+
class TLReader
|
7
|
+
def initialize(data)
|
8
|
+
@data = data || ''
|
9
|
+
@position = 0
|
10
|
+
end
|
11
|
+
|
12
|
+
def read_byte
|
13
|
+
if @position >= @data.length
|
14
|
+
raise "Not enough data for byte at position #{@position} (total: #{@data.length})"
|
15
|
+
end
|
16
|
+
value = @data[@position].unpack1('C')
|
17
|
+
@position += 1
|
18
|
+
value
|
19
|
+
end
|
20
|
+
|
21
|
+
def read_int(signed: true)
|
22
|
+
if @position + 4 > @data.length
|
23
|
+
raise "Not enough data for int at position #{@position} (need 4, have #{@data.length - @position})"
|
24
|
+
end
|
25
|
+
format = signed ? 'l<' : 'L<'
|
26
|
+
value = @data[@position, 4].unpack1(format)
|
27
|
+
@position += 4
|
28
|
+
value
|
29
|
+
end
|
30
|
+
|
31
|
+
def read_long(signed: true)
|
32
|
+
if @position + 8 > @data.length
|
33
|
+
raise "Not enough data for long at position #{@position} (need 8, have #{@data.length - @position})"
|
34
|
+
end
|
35
|
+
format = signed ? 'q<' : 'Q<'
|
36
|
+
value = @data[@position, 8].unpack1(format)
|
37
|
+
@position += 8
|
38
|
+
value
|
39
|
+
end
|
40
|
+
|
41
|
+
def read_int128
|
42
|
+
if @position + 16 > @data.length
|
43
|
+
raise "Not enough data for int128 at position #{@position} (need 16, have #{@data.length - @position})"
|
44
|
+
end
|
45
|
+
@data[@position, 16].tap { @position += 16 }
|
46
|
+
end
|
47
|
+
|
48
|
+
def read_string
|
49
|
+
# TL string format: length + data + padding
|
50
|
+
if @position >= @data.length
|
51
|
+
raise "Not enough data for string length"
|
52
|
+
end
|
53
|
+
|
54
|
+
length_byte = read_byte
|
55
|
+
puts "TL_DEBUG: read length_byte: #{length_byte} at position #{@position - 1}"
|
56
|
+
|
57
|
+
if length_byte == 254
|
58
|
+
# Long string format: 254 + 3 bytes length + data + padding
|
59
|
+
if @position + 3 > @data.length
|
60
|
+
raise "Not enough data for long string length"
|
61
|
+
end
|
62
|
+
|
63
|
+
length_bytes = @data[@position, 3]
|
64
|
+
puts "TL_DEBUG: long string length_bytes: #{length_bytes.unpack('H*')[0]}"
|
65
|
+
length = length_bytes.unpack1('V') & 0xffffff
|
66
|
+
@position += 3
|
67
|
+
puts "TL_DEBUG: long string length: #{length}"
|
68
|
+
padding = (4 - (length % 4)) % 4
|
69
|
+
puts "TL_DEBUG: long string padding: #{padding}"
|
70
|
+
else
|
71
|
+
# Short string format: 1 byte length + data + padding
|
72
|
+
length = length_byte
|
73
|
+
puts "TL_DEBUG: short string length: #{length}"
|
74
|
+
padding = (4 - ((length + 1) % 4)) % 4
|
75
|
+
puts "TL_DEBUG: short string padding: #{padding}"
|
76
|
+
end
|
77
|
+
|
78
|
+
if @position + length > @data.length
|
79
|
+
raise "Not enough data for string content: need #{length}, have #{@data.length - @position}"
|
80
|
+
end
|
81
|
+
|
82
|
+
data = @data[@position, length]
|
83
|
+
@position += length + padding
|
84
|
+
data
|
85
|
+
end
|
86
|
+
|
87
|
+
def read_tl_object
|
88
|
+
constructor_id = read_int(signed: false)
|
89
|
+
|
90
|
+
case constructor_id
|
91
|
+
when 0xb5890dba # server_DH_inner_data
|
92
|
+
read_server_dh_inner_data
|
93
|
+
else
|
94
|
+
raise "Unknown TL constructor: 0x#{constructor_id.to_s(16)}"
|
95
|
+
end
|
96
|
+
end
|
97
|
+
|
98
|
+
private
|
99
|
+
|
100
|
+
def read_server_dh_inner_data
|
101
|
+
# server_DH_inner_data#b5890dba nonce:int128 server_nonce:int128 g:int dh_prime:string g_a:string server_time:int
|
102
|
+
nonce = read_int128
|
103
|
+
server_nonce = read_int128
|
104
|
+
g = read_int(signed: false)
|
105
|
+
dh_prime = read_string
|
106
|
+
g_a = read_string
|
107
|
+
server_time = read_int(signed: true)
|
108
|
+
|
109
|
+
{
|
110
|
+
constructor_id: 0xb5890dba,
|
111
|
+
nonce: nonce,
|
112
|
+
server_nonce: server_nonce,
|
113
|
+
g: g,
|
114
|
+
dh_prime: dh_prime,
|
115
|
+
g_a: g_a,
|
116
|
+
server_time: server_time
|
117
|
+
}
|
118
|
+
end
|
119
|
+
end
|
120
|
+
end
|
@@ -0,0 +1,113 @@
|
|
1
|
+
module Telegram
|
2
|
+
class TLSchema
|
3
|
+
attr_reader :constructors, :functions
|
4
|
+
|
5
|
+
def initialize
|
6
|
+
@constructors = {}
|
7
|
+
@functions = {}
|
8
|
+
load_schemas
|
9
|
+
end
|
10
|
+
|
11
|
+
def load_schemas
|
12
|
+
# Load mtproto.tl
|
13
|
+
mtproto_path = File.join(File.dirname(__FILE__), 'tl/mtproto.tl')
|
14
|
+
api_path = File.join(File.dirname(__FILE__), 'tl/api.tl')
|
15
|
+
|
16
|
+
parse_tl_file(mtproto_path) if File.exist?(mtproto_path)
|
17
|
+
parse_tl_file(api_path) if File.exist?(api_path)
|
18
|
+
end
|
19
|
+
|
20
|
+
def parse_tl_file(path)
|
21
|
+
File.readlines(path).each do |line|
|
22
|
+
line = line.strip
|
23
|
+
next if line.empty? || line.start_with?('//') || line.start_with?('---')
|
24
|
+
|
25
|
+
parse_tl_line(line)
|
26
|
+
end
|
27
|
+
end
|
28
|
+
|
29
|
+
def parse_tl_line(line)
|
30
|
+
# Parse TL definitions like:
|
31
|
+
# p_q_inner_data#83c95aec pq:string p:string q:string nonce:int128 server_nonce:int128 new_nonce:int256 = P_Q_inner_data;
|
32
|
+
# req_DH_params#d712e4be nonce:int128 server_nonce:int128 p:string q:string public_key_fingerprint:long encrypted_data:string = Server_DH_Params;
|
33
|
+
|
34
|
+
return unless line.include?('#') && line.include?('=')
|
35
|
+
|
36
|
+
parts = line.split('=')
|
37
|
+
return if parts.length != 2
|
38
|
+
|
39
|
+
left = parts[0].strip
|
40
|
+
result_type = parts[1].strip.gsub(';', '')
|
41
|
+
|
42
|
+
# Extract constructor name and ID
|
43
|
+
if left =~ /^([^#]+)#([0-9a-fA-F]+)\s+(.*)$/
|
44
|
+
constructor_name = $1.strip
|
45
|
+
constructor_id = $2.hex
|
46
|
+
params_str = $3.strip
|
47
|
+
|
48
|
+
# Remove generic type parameters {X:Type} from params
|
49
|
+
params_str = params_str.gsub(/\{[^}]+\}\s*/, '')
|
50
|
+
|
51
|
+
# Parse parameters
|
52
|
+
params = parse_parameters(params_str)
|
53
|
+
|
54
|
+
constructor_info = {
|
55
|
+
name: constructor_name,
|
56
|
+
id: constructor_id,
|
57
|
+
params: params,
|
58
|
+
result_type: result_type
|
59
|
+
}
|
60
|
+
|
61
|
+
@constructors[constructor_id] = constructor_info
|
62
|
+
@constructors[constructor_name] = constructor_info
|
63
|
+
|
64
|
+
Rails.logger.debug "📋 Loaded TL constructor: #{constructor_name}##{constructor_id.to_s(16)} -> #{result_type}"
|
65
|
+
end
|
66
|
+
end
|
67
|
+
|
68
|
+
def parse_parameters(params_str)
|
69
|
+
return [] if params_str.empty?
|
70
|
+
|
71
|
+
params = []
|
72
|
+
# Split carefully to handle flags.X?type syntax
|
73
|
+
parts = params_str.split(/\s+/)
|
74
|
+
|
75
|
+
parts.each do |part|
|
76
|
+
next if part.empty?
|
77
|
+
|
78
|
+
if part.include?(':')
|
79
|
+
name, type = part.split(':', 2)
|
80
|
+
|
81
|
+
# Check if this is a flags.X?type optional parameter
|
82
|
+
if type.match(/^flags\.(\d+)\?(.+)$/)
|
83
|
+
flag_bit = $1.to_i
|
84
|
+
actual_type = $2
|
85
|
+
params << {
|
86
|
+
name: name,
|
87
|
+
type: actual_type,
|
88
|
+
optional: true,
|
89
|
+
flag_bit: flag_bit
|
90
|
+
}
|
91
|
+
else
|
92
|
+
params << {
|
93
|
+
name: name,
|
94
|
+
type: type,
|
95
|
+
optional: false
|
96
|
+
}
|
97
|
+
end
|
98
|
+
end
|
99
|
+
end
|
100
|
+
|
101
|
+
params
|
102
|
+
end
|
103
|
+
|
104
|
+
def get_constructor(name_or_id)
|
105
|
+
@constructors[name_or_id]
|
106
|
+
end
|
107
|
+
|
108
|
+
def get_constructor_id(name)
|
109
|
+
constructor = @constructors[name]
|
110
|
+
constructor ? constructor[:id] : nil
|
111
|
+
end
|
112
|
+
end
|
113
|
+
end
|
@@ -0,0 +1,103 @@
|
|
1
|
+
module Telegram
|
2
|
+
class TLWriter
|
3
|
+
def initialize
|
4
|
+
@data = ''.b
|
5
|
+
end
|
6
|
+
|
7
|
+
def to_s
|
8
|
+
@data
|
9
|
+
end
|
10
|
+
|
11
|
+
def length
|
12
|
+
@data.length
|
13
|
+
end
|
14
|
+
|
15
|
+
# Basic TL data types
|
16
|
+
def write_int(value, signed: true)
|
17
|
+
format = signed ? 'l<' : 'L<'
|
18
|
+
@data += [value].pack(format)
|
19
|
+
end
|
20
|
+
|
21
|
+
def write_long(value, signed: true)
|
22
|
+
format = signed ? 'q<' : 'Q<'
|
23
|
+
@data += [value].pack(format)
|
24
|
+
end
|
25
|
+
|
26
|
+
def write_int128(value)
|
27
|
+
# Convert to bytes in little endian using our existing serialization
|
28
|
+
if value.is_a?(Integer)
|
29
|
+
# Use the same method as our working serialization
|
30
|
+
@data += Telegram::Serialization.serialize_int128(value)
|
31
|
+
else
|
32
|
+
# Assume it's already bytes
|
33
|
+
@data += value
|
34
|
+
end
|
35
|
+
end
|
36
|
+
|
37
|
+
def write_int256(value)
|
38
|
+
# Convert to bytes in little endian using our existing serialization
|
39
|
+
if value.is_a?(Integer)
|
40
|
+
# Use the same method as our working serialization
|
41
|
+
@data += Telegram::Serialization.serialize_int256(value)
|
42
|
+
else
|
43
|
+
# Assume it's already bytes
|
44
|
+
@data += value
|
45
|
+
end
|
46
|
+
end
|
47
|
+
|
48
|
+
def write_bytes(data)
|
49
|
+
@data += data
|
50
|
+
end
|
51
|
+
|
52
|
+
по # TL string serialization using our existing method
|
53
|
+
def write_string(data)
|
54
|
+
@data += Telegram::Serialization.pack_tl_string(data)
|
55
|
+
end
|
56
|
+
|
57
|
+
# TL object serialization
|
58
|
+
def write_constructor(constructor_id)
|
59
|
+
write_int(constructor_id, signed: false)
|
60
|
+
end
|
61
|
+
|
62
|
+
# High-level TL objects
|
63
|
+
|
64
|
+
# p_q_inner_data#83c95aec pq:string p:string q:string nonce:int128 server_nonce:int128 new_nonce:int256
|
65
|
+
def write_pq_inner_data(pq_bytes, p_bytes, q_bytes, nonce, server_nonce, new_nonce)
|
66
|
+
write_constructor(0x83c95aec)
|
67
|
+
write_string(pq_bytes)
|
68
|
+
write_string(p_bytes)
|
69
|
+
write_string(q_bytes)
|
70
|
+
write_int128(nonce)
|
71
|
+
write_int128(server_nonce)
|
72
|
+
write_int256(new_nonce)
|
73
|
+
end
|
74
|
+
|
75
|
+
# req_DH_params#d712e4be nonce:int128 server_nonce:int128 p:string q:string public_key_fingerprint:long encrypted_data:string
|
76
|
+
def write_req_dh_params(nonce, server_nonce, p_bytes, q_bytes, fingerprint, encrypted_data)
|
77
|
+
write_constructor(0xd712e4be)
|
78
|
+
write_int128(nonce)
|
79
|
+
write_int128(server_nonce)
|
80
|
+
write_string(p_bytes)
|
81
|
+
write_string(q_bytes)
|
82
|
+
write_long(fingerprint, signed: true)
|
83
|
+
write_string(encrypted_data)
|
84
|
+
end
|
85
|
+
|
86
|
+
# client_DH_inner_data#6643b654 nonce:int128 server_nonce:int128 retry_id:long g_b:string
|
87
|
+
def write_client_dh_inner_data(nonce, server_nonce, retry_id, g_b_bytes)
|
88
|
+
write_constructor(0x6643b654)
|
89
|
+
write_int128(nonce)
|
90
|
+
write_int128(server_nonce)
|
91
|
+
write_long(retry_id, signed: true)
|
92
|
+
write_string(g_b_bytes)
|
93
|
+
end
|
94
|
+
|
95
|
+
# set_client_DH_params#f5045f1f nonce:int128 server_nonce:int128 encrypted_data:string
|
96
|
+
def write_set_client_dh_params(nonce, server_nonce, encrypted_data)
|
97
|
+
write_constructor(0xf5045f1f)
|
98
|
+
write_int128(nonce)
|
99
|
+
write_int128(server_nonce)
|
100
|
+
write_string(encrypted_data)
|
101
|
+
end
|
102
|
+
end
|
103
|
+
end
|