telegram-mtproto-ruby 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,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