mtproto 0.0.13 → 0.0.15

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.
Files changed (93) hide show
  1. checksums.yaml +4 -4
  2. data/FUTURE.md +9 -0
  3. data/docs/test_architecture_level1.md +237 -0
  4. data/lib/mtproto/auth_key_generator.rb +5 -5
  5. data/lib/mtproto/client/api/get_contacts.rb +14 -0
  6. data/lib/mtproto/client/api/send_message.rb +15 -0
  7. data/lib/mtproto/client/api.rb +2 -0
  8. data/lib/mtproto/client/rpc/response.rb +1 -1
  9. data/lib/mtproto/client/rpc.rb +35 -2
  10. data/lib/mtproto/client.rb +81 -18
  11. data/lib/mtproto/tl/constructor_names.rb +10 -0
  12. data/lib/mtproto/tl/constructors.rb +26 -0
  13. data/lib/mtproto/tl/object.rb +1 -1
  14. data/lib/mtproto/tl/objects/account_password.rb +1 -1
  15. data/lib/mtproto/tl/objects/authorization.rb +1 -1
  16. data/lib/mtproto/tl/objects/channels_create_channel.rb +48 -0
  17. data/lib/mtproto/tl/objects/channels_delete_messages.rb +32 -0
  18. data/lib/mtproto/tl/objects/channels_delete_participant_history.rb +34 -0
  19. data/lib/mtproto/tl/objects/channels_edit_banned.rb +54 -0
  20. data/lib/mtproto/tl/objects/channels_get_messages.rb +33 -0
  21. data/lib/mtproto/tl/objects/channels_get_participant.rb +36 -0
  22. data/lib/mtproto/tl/objects/channels_join_channel.rb +22 -0
  23. data/lib/mtproto/tl/objects/channels_leave_channel.rb +22 -0
  24. data/lib/mtproto/tl/objects/channels_report_spam.rb +37 -0
  25. data/lib/mtproto/tl/objects/check_password.rb +1 -1
  26. data/lib/mtproto/tl/objects/client_dh_inner_data.rb +1 -1
  27. data/lib/mtproto/tl/objects/contacts.rb +156 -0
  28. data/lib/mtproto/tl/objects/create_bot.rb +54 -0
  29. data/lib/mtproto/tl/objects/delete_messages.rb +25 -0
  30. data/lib/mtproto/tl/objects/dh_gen_response.rb +1 -1
  31. data/lib/mtproto/tl/objects/dialogs.rb +18 -4
  32. data/lib/mtproto/tl/objects/edit_access_settings.rb +46 -0
  33. data/lib/mtproto/tl/objects/edit_message.rb +58 -0
  34. data/lib/mtproto/tl/objects/export_bot_token.rb +32 -0
  35. data/lib/mtproto/tl/objects/export_login_token.rb +1 -1
  36. data/lib/mtproto/tl/objects/exported_bot_token.rb +27 -0
  37. data/lib/mtproto/tl/objects/forward_messages.rb +55 -0
  38. data/lib/mtproto/tl/objects/get_access_settings.rb +28 -0
  39. data/lib/mtproto/tl/objects/get_channel_difference.rb +41 -0
  40. data/lib/mtproto/tl/objects/get_config.rb +1 -1
  41. data/lib/mtproto/tl/objects/get_contacts.rb +17 -0
  42. data/lib/mtproto/tl/objects/get_dialogs.rb +1 -1
  43. data/lib/mtproto/tl/objects/get_difference.rb +1 -1
  44. data/lib/mtproto/tl/objects/get_file.rb +54 -0
  45. data/lib/mtproto/tl/objects/get_full_channel.rb +29 -0
  46. data/lib/mtproto/tl/objects/get_full_user.rb +28 -0
  47. data/lib/mtproto/tl/objects/get_history.rb +1 -1
  48. data/lib/mtproto/tl/objects/get_messages_reactions.rb +39 -0
  49. data/lib/mtproto/tl/objects/get_password.rb +1 -1
  50. data/lib/mtproto/tl/objects/get_state.rb +1 -1
  51. data/lib/mtproto/tl/objects/get_users.rb +1 -1
  52. data/lib/mtproto/tl/objects/help_config.rb +1 -1
  53. data/lib/mtproto/tl/objects/import_bot_authorization.rb +44 -0
  54. data/lib/mtproto/tl/objects/import_login_token.rb +1 -1
  55. data/lib/mtproto/tl/objects/init_connection.rb +2 -2
  56. data/lib/mtproto/tl/objects/input_keyboard_button_request_peer.rb +54 -0
  57. data/lib/mtproto/tl/objects/invite_to_channel.rb +35 -0
  58. data/lib/mtproto/tl/objects/invoke_with_layer.rb +2 -2
  59. data/lib/mtproto/tl/objects/login_token.rb +2 -2
  60. data/lib/mtproto/tl/objects/messages.rb +1 -1
  61. data/lib/mtproto/tl/objects/messages_get_messages.rb +26 -0
  62. data/lib/mtproto/tl/objects/pq_inner_data.rb +1 -1
  63. data/lib/mtproto/tl/objects/raw_response.rb +29 -0
  64. data/lib/mtproto/tl/objects/reply_keyboard_markup.rb +35 -0
  65. data/lib/mtproto/tl/objects/req_dh_params.rb +1 -1
  66. data/lib/mtproto/tl/objects/req_pq_multi.rb +1 -1
  67. data/lib/mtproto/tl/objects/request_peer_type_create_bot.rb +47 -0
  68. data/lib/mtproto/tl/objects/res_pq.rb +1 -1
  69. data/lib/mtproto/tl/objects/resolve_username.rb +40 -0
  70. data/lib/mtproto/tl/objects/save_file_part.rb +40 -0
  71. data/lib/mtproto/tl/objects/send_bot_requested_peer.rb +51 -0
  72. data/lib/mtproto/tl/objects/send_code.rb +1 -1
  73. data/lib/mtproto/tl/objects/send_media.rb +96 -0
  74. data/lib/mtproto/tl/objects/send_message.rb +73 -0
  75. data/lib/mtproto/tl/objects/send_reaction.rb +68 -0
  76. data/lib/mtproto/tl/objects/sent_code.rb +1 -1
  77. data/lib/mtproto/tl/objects/server_dh_inner_data.rb +1 -1
  78. data/lib/mtproto/tl/objects/server_dh_params.rb +1 -1
  79. data/lib/mtproto/tl/objects/set_bot_commands.rb +50 -0
  80. data/lib/mtproto/tl/objects/set_bot_info.rb +64 -0
  81. data/lib/mtproto/tl/objects/set_client_dh_params.rb +1 -1
  82. data/lib/mtproto/tl/objects/sign_in.rb +1 -1
  83. data/lib/mtproto/tl/objects/update.rb +1 -1
  84. data/lib/mtproto/tl/objects/update_short.rb +2 -2
  85. data/lib/mtproto/tl/objects/update_short_message.rb +1 -1
  86. data/lib/mtproto/tl/objects/update_short_sent_message.rb +36 -0
  87. data/lib/mtproto/tl/objects/update_username.rb +39 -0
  88. data/lib/mtproto/tl/objects/updates_difference.rb +93 -14
  89. data/lib/mtproto/tl/objects/updates_state.rb +1 -1
  90. data/lib/mtproto/tl/objects/upload_profile_photo.rb +65 -0
  91. data/lib/mtproto/tl/objects/users.rb +1 -1
  92. data/lib/mtproto/version.rb +1 -1
  93. metadata +49 -1
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: ef5c11c5bf0b9bfe4f3b80e905270264b5658bc55a2932e92c945df8ba0dc723
4
- data.tar.gz: 58fec06d053d66b52c382049e7fb917dbf0c4af2395c7f81f5dcb66d70778228
3
+ metadata.gz: a52707d10eeb5e41cf6e1dcb01374d92092031ceba5f5f92dde31ce4965d174c
4
+ data.tar.gz: ddf47dbcf9612d686806ec83fd16470de46595300b76c733fc8af37be851f1ff
5
5
  SHA512:
6
- metadata.gz: b108736ec52172e501b8609b6c8b5596052b7fa80d72bfaa6cadb393baddbe89ec009148da487f3b8c16b9a6000e5bdea4aa5b15c77fd9ddd19ce43ae8db6763
7
- data.tar.gz: 3268f1b6943ea7c470ee274a56cbc3e4d3c5a2aebff7e8b5b4522805e8a922756932cb396909650f520cb3153d75d8e34a61e5952916602421ed01df2b9e5c7e
6
+ metadata.gz: c97f61c1f21c9b2221b90333dd12156b77617e7debd04424a0a4996dd7c50c7b570ba0777d043b966032ad2827db4d70498cc362cd9966c1342bd2b4c711d91c
7
+ data.tar.gz: 5f235da4dac488f6c9853affca03de93bfecf2b35eacaedf13e933a4400a099e55d1831e2002c63a0687fc326e74c38e86c1e82da9bd9c95c5f07bd41ee846f7
data/FUTURE.md ADDED
@@ -0,0 +1,9 @@
1
+ # Future Tasks
2
+
3
+ ## Implement msgs_ack
4
+
5
+ Client should acknowledge received messages by sending `msgs_ack` with a list of received
6
+ msg_ids. Without this, the server may redeliver messages it considers unacknowledged.
7
+
8
+ Also add handling for `bad_msg_notification` responses from the server (constructor exists
9
+ in constructors.rb but no handler in Client#process_message).
@@ -0,0 +1,237 @@
1
+ # Level 1 Test Architecture: Constructor Capture and Test Cases
2
+
3
+ ## Purpose
4
+
5
+ Collect real data from Telegram in specific user-facing scenarios and use it as the
6
+ source for two things:
7
+
8
+ 1. **A library of TL constructor variants**: every concrete form of every constructor
9
+ we ever observe (across all the optional fields and flag-driven shapes) — stored
10
+ under `spec/fixtures/level1/constructors/`. This is the format catalog Level 2
11
+ (deserialization) and Level 3 (API logic) tests work against.
12
+ 2. **Test cases**: one user-facing scenario per executable script under `spec/level1/`,
13
+ producing a folder of fixtures under `spec/fixtures/level1/test_cases/<scenario>/`.
14
+ A test case captures whatever Telegram actually sends end-to-end during that
15
+ scenario.
16
+
17
+ The set of test cases is chosen so that, taken together, they cover every variant
18
+ of every constructor we need to characterize — not so that they reproduce every
19
+ possible user-facing scenario. The same constructor can show up in many test cases
20
+ and inside different parent constructors; we only need to see each variant at least
21
+ once.
22
+
23
+ ## How It Works
24
+
25
+ These are semi-manual tests. Each test case:
26
+
27
+ 1. Connects to Telegram with a pre-authenticated session
28
+ 2. Tells the user what to do (e.g., "Send a text message to this account")
29
+ 3. Waits until the corresponding data arrives from Telegram
30
+ 4. Saves the raw decrypted message bodies as `.bin` files and a `captured.json`
31
+ listing the TL constructors received
32
+
33
+ On subsequent runs, the freshly captured constructor list is compared to the
34
+ previously stored `captured.json` for the same test case. If they differ, the
35
+ script prints a `CHANGED` warning — usually a signal that Telegram's wire-level
36
+ behavior shifted and the fixtures need to be re-examined.
37
+
38
+ ## Example Scenarios
39
+
40
+ The list below illustrates the kinds of situations these tests cover.
41
+ It is not exhaustive — new scenarios are added as needed.
42
+
43
+ - Text message in private chat
44
+ - Text message in a group
45
+ - Text message in a channel
46
+ - Reply to a message
47
+ - Forwarded message
48
+ - Message with photo
49
+ - Message with document
50
+ - Message in a forum topic
51
+ - User joined a group
52
+ - User left a group
53
+ - Message edited
54
+ - Message deleted
55
+
56
+ ## Fixture Layout
57
+
58
+ ```
59
+ spec/fixtures/level1/
60
+ test_cases/
61
+ receive_private_text_message/
62
+ 001.bin
63
+ 002.bin
64
+ 003.bin
65
+ 004.bin
66
+ captured.json
67
+ receive_group_text_message/
68
+ 001.bin
69
+ 002.bin
70
+ captured.json
71
+ ...
72
+ constructors/
73
+ updateShortMessage/
74
+ plain_in.bin
75
+ with_fwd_from.bin
76
+ with_reply_to.bin
77
+ with_entities_bold.bin
78
+ ...
79
+ messageFwdHeader/
80
+ from_user.bin
81
+ from_channel.bin
82
+ ...
83
+ messageEntity/
84
+ bold.bin
85
+ italic.bin
86
+ url.bin
87
+ ...
88
+ ...
89
+ ```
90
+
91
+ Each test case produces:
92
+
93
+ - One or more `.bin` files with raw decrypted message bodies, numbered sequentially
94
+ in arrival order (one user action may trigger multiple messages from Telegram).
95
+ - A `captured.json` listing each captured message as an object with two fields —
96
+ the TL `constructor` name and the `file` containing its raw body bytes.
97
+
98
+ Shape of `captured.json`:
99
+
100
+ ```json
101
+ [
102
+ { "constructor": "msgs_ack", "file": "001.bin" },
103
+ { "constructor": "updateShort", "file": "002.bin" },
104
+ { "constructor": "updateShortMessage", "file": "003.bin" }
105
+ ]
106
+ ```
107
+
108
+ The `constructors/` tree is a separate, deduplicated library populated from
109
+ test-case captures: one `.bin` per distinct variant of each constructor, with
110
+ the filename describing the variant (e.g. `plain_in.bin`, `with_fwd_from.bin`).
111
+ No metadata file — the folder + filename are the catalogue.
112
+
113
+ ## File Structure
114
+
115
+ Each scenario is an executable script (not an RSpec spec). The flow is
116
+ interactive — the script prints instructions, waits on STDIN, then captures
117
+ the resulting Telegram traffic. RSpec's declarative `describe`/`it` shape
118
+ doesn't fit that, so these scripts live under `spec/level1/` alongside the
119
+ rspec tree but are invoked directly, not through `rake spec`.
120
+
121
+ ```
122
+ spec/
123
+ level1/
124
+ support/ # Shared test infrastructure
125
+ receive_private_text_message # Executable script (one per test case)
126
+ receive_group_text_message
127
+ ...
128
+ fixtures/
129
+ level1/
130
+ test_cases/ # Per-test-case captures (folder per scenario)
131
+ constructors/ # Deduplicated constructor variant library
132
+ ```
133
+
134
+ ## Running with xp
135
+
136
+ These scripts are interactive — they print a prompt, block on STDIN, run
137
+ until the captured traffic arrives, then exit. When the operator is a
138
+ Claude Code bot driving the repo from short-lived Bash invocations, a
139
+ plain shell can't keep the script alive across calls: each tool call is
140
+ a fresh process, so the script would exit (or be killed) the moment the
141
+ shell returns.
142
+
143
+ The way around it is [xp](https://github.com/alev-pro/xp) — a small
144
+ process stdin/stdout multiplexer. A long-lived xp daemon owns the
145
+ script under a named topic; separate client invocations send to its
146
+ stdin and read from its stdout. The script keeps running between
147
+ calls.
148
+
149
+ The recipe below assumes xp is built at
150
+ `~/devel/alev-pro/xp/target/release/xp`. Adjust the path if it lives
151
+ elsewhere.
152
+
153
+ ### 1. Start the xp daemon
154
+
155
+ ```sh
156
+ nohup ~/devel/alev-pro/xp/target/release/xp --run-daemon \
157
+ >/tmp/xp-daemon.log 2>&1 &
158
+ echo $! > /tmp/xp-daemon.pid
159
+ ```
160
+
161
+ The daemon binds a Unix socket at `/tmp/xp-<uid>.sock`. The PID file
162
+ is just a convention so teardown can `kill` by PID — `pkill -f` on
163
+ the daemon command line is dangerous because it can also match the
164
+ caller's own wrapper shell.
165
+
166
+ ### 2. Spawn the Level 1 script under a topic
167
+
168
+ ```sh
169
+ ~/devel/alev-pro/xp/target/release/xp -t l1 -- \
170
+ bash -lc 'cd ~/devel/alev-pro/mtproto-ruby \
171
+ && bundle exec spec/level1/receive_private_text_message'
172
+ ```
173
+
174
+ `l1` is just a chosen topic name. Any string works.
175
+
176
+ ### 3. Read the script's stdout
177
+
178
+ ```sh
179
+ ~/devel/alev-pro/xp/target/release/xp -t l1 --read-stdout --cursor c1
180
+ ```
181
+
182
+ `--cursor c1` keeps a stable read position across separate xp client
183
+ invocations. Without `--cursor`, xp keys the cursor on the caller's
184
+ posix session id (`getsid(0)`), and every fresh Bash tool call has a
185
+ new sid — meaning every read replays the buffer from byte 0.
186
+
187
+ Expected initial output:
188
+
189
+ ```
190
+ Connecting...
191
+ Connected as: <first> <last>
192
+ Send a text message to this account in a private chat.
193
+ Press Enter when done.
194
+ ```
195
+
196
+ ### 4. Trigger the scenario
197
+
198
+ Perform the user-facing action the script asks for (in this case,
199
+ send a private text message to the test-DC account from another
200
+ Telegram client).
201
+
202
+ ### 5. Unblock the script
203
+
204
+ ```sh
205
+ echo "" | ~/devel/alev-pro/xp/target/release/xp -t l1 --put-to-stdin
206
+ ```
207
+
208
+ The script then prints the captured constructor list and writes the
209
+ fixtures + `captured.json` to `spec/fixtures/level1/test_cases/<scenario>/`.
210
+
211
+ ### 6. Tear down
212
+
213
+ ```sh
214
+ kill "$(cat /tmp/xp-daemon.pid)"
215
+ rm -f /tmp/xp-<uid>.sock /tmp/xp-daemon.pid
216
+ ```
217
+
218
+ ### STDOUT buffering caveat
219
+
220
+ Ruby's `STDOUT` defaults to full buffering when stdout is not a tty,
221
+ and xp pipes the child's stdout through a plain pipe rather than a
222
+ pty. Without an explicit flush the script's `puts` calls would stay
223
+ in-process until exit, and step 3 would read nothing. Each Level 1
224
+ script must therefore set `STDOUT.sync = true` near the top, before
225
+ any output.
226
+
227
+ ### `bash -lc` noise
228
+
229
+ When `bash -i` runs under xp without a pty, it complains:
230
+
231
+ ```
232
+ bash: cannot set terminal process group (...): Inappropriate ioctl for device
233
+ bash: no job control in this shell
234
+ ```
235
+
236
+ That noise lands on stderr, which the daemon inherits and writes into
237
+ `/tmp/xp-daemon.log`. The script's own stdout stays clean.
@@ -86,13 +86,13 @@ module MTProto
86
86
  private
87
87
 
88
88
  def send_and_receive(rpc, response_class)
89
- message = Message.new(rpc.body)
89
+ message = Message.new(rpc.serialize)
90
90
  @connection.send(Transport::Packet.new(message.bytes))
91
91
 
92
92
  response_packet = @connection.receive
93
93
  raise 'Receive timeout' if response_packet.nil?
94
94
 
95
- result = response_class.parse(Message.parse(response_packet))
95
+ result = response_class.deserialize(Message.parse(response_packet))
96
96
 
97
97
  raise 'Nonce mismatch!' if rpc.respond_to?(:nonce) && result.nonce != rpc.nonce
98
98
  raise 'Server nonce mismatch!' if rpc.respond_to?(:server_nonce) && result.server_nonce != rpc.server_nonce
@@ -114,7 +114,7 @@ module MTProto
114
114
  new_nonce: new_nonce,
115
115
  dc: dc_value
116
116
  )
117
- encrypted_data = Crypto::RSA_PAD.encrypt(inner_data.body.pack('C*'), server_key)
117
+ encrypted_data = Crypto::RSA_PAD.encrypt(inner_data.serialize.pack('C*'), server_key)
118
118
 
119
119
  rpc = TL::ReqDHParams.new(
120
120
  nonce: res_pq.nonce,
@@ -149,7 +149,7 @@ module MTProto
149
149
  answer = answer[0..-2]
150
150
  end
151
151
 
152
- TL::ServerDHInnerData.parse(answer)
152
+ TL::ServerDHInnerData.deserialize(answer)
153
153
  end
154
154
 
155
155
  def send_client_dh_params(res_pq, _new_nonce, client_dh_params, tmp_aes_key, tmp_aes_iv)
@@ -160,7 +160,7 @@ module MTProto
160
160
  g_b: client_dh_params[:g_b_bytes]
161
161
  )
162
162
 
163
- client_dh_data = client_dh_inner_data.body.pack('C*')
163
+ client_dh_data = client_dh_inner_data.serialize.pack('C*')
164
164
  client_dh_data_with_hash = Digest::SHA1.digest(client_dh_data) + client_dh_data
165
165
 
166
166
  padding_length = (16 - (client_dh_data_with_hash.bytesize % 16)) % 16
@@ -0,0 +1,14 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative '../../tl/objects/get_contacts'
4
+ require_relative '../../tl/objects/contacts'
5
+
6
+ module MTProto
7
+ class Client
8
+ class API
9
+ def get_contacts(hash: 0)
10
+ rpc_call(TL::GetContacts.new(hash: hash), TL::Contacts).body
11
+ end
12
+ end
13
+ end
14
+ end
@@ -0,0 +1,15 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative '../../tl/objects/send_message'
4
+ require_relative '../../tl/objects/update_short_sent_message'
5
+
6
+ module MTProto
7
+ class Client
8
+ class API
9
+ def send_message(peer:, message:, random_id: nil)
10
+ rpc_call(TL::SendMessage.new(peer: peer, message: message, random_id: random_id),
11
+ TL::UpdateShortSentMessage).body
12
+ end
13
+ end
14
+ end
15
+ end
@@ -10,6 +10,8 @@ require_relative 'api/get_dialogs'
10
10
  require_relative 'api/get_history'
11
11
  require_relative 'api/get_updates_state'
12
12
  require_relative 'api/get_updates_difference'
13
+ require_relative 'api/send_message'
14
+ require_relative 'api/get_contacts'
13
15
 
14
16
  module MTProto
15
17
  class Client
@@ -43,7 +43,7 @@ module MTProto
43
43
  end
44
44
 
45
45
  def signal(raw_body)
46
- @result = @response_class.parse(raw_body)
46
+ @result = @response_class.deserialize(raw_body)
47
47
  @condition.signal
48
48
  end
49
49
 
@@ -1,11 +1,16 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require 'securerandom'
3
4
  require_relative '../message_id'
4
5
  require_relative 'rpc/response'
5
6
 
6
7
  module MTProto
7
8
  class Client
8
9
  class RPC
10
+ MSGS_ACK = 0x62d6b459
11
+ VECTOR = 0x1cb5c415
12
+ PING_DELAY_DISCONNECT = 0xf3427b8c
13
+
9
14
  attr_reader :pending_requests
10
15
 
11
16
  def initialize(client)
@@ -66,11 +71,39 @@ module MTProto
66
71
  @pending_requests.clear
67
72
  end
68
73
 
74
+ # Acknowledge received server messages (msgs_ack). Without this the server
75
+ # keeps the unacknowledged messages, resends them, and drops the connection
76
+ # after a fixed window. Fire-and-forget — no rpc_result is expected.
77
+ def send_ack(msg_ids)
78
+ return if msg_ids.nil? || msg_ids.empty?
79
+
80
+ send_encrypted(self.class.ack_body(msg_ids), content_related: false)
81
+ end
82
+
83
+ # Keepalive: ping_delay_disconnect asks the server to disconnect us only if
84
+ # we stop pinging within disconnect_delay seconds. Fire-and-forget (the pong
85
+ # is ignored by process_message).
86
+ def send_ping(disconnect_delay = 75)
87
+ send_encrypted(self.class.ping_body(SecureRandom.random_number(2**63), disconnect_delay),
88
+ content_related: false)
89
+ end
90
+
91
+ # Serialized msgs_ack body (no envelope) — extracted for unit testing.
92
+ def self.ack_body(msg_ids)
93
+ [MSGS_ACK].pack('L<') + [VECTOR].pack('L<') + [msg_ids.length].pack('L<') +
94
+ msg_ids.map { |id| [id].pack('Q<') }.join
95
+ end
96
+
97
+ # Serialized ping_delay_disconnect body (no envelope) — extracted for testing.
98
+ def self.ping_body(ping_id, disconnect_delay)
99
+ [PING_DELAY_DISCONNECT].pack('L<') + [ping_id].pack('Q<') + [disconnect_delay].pack('l<')
100
+ end
101
+
69
102
  private
70
103
 
71
104
  def serialize_request(request)
72
- if request.respond_to?(:body)
73
- request.body.pack('C*')
105
+ if request.respond_to?(:serialize)
106
+ request.serialize.pack('C*')
74
107
  else
75
108
  request
76
109
  end
@@ -77,10 +77,17 @@ module MTProto
77
77
  @lang_code = 'en'
78
78
 
79
79
  @receiver_task = nil
80
+ @keepalive_task = nil
81
+ @ack_ids = []
80
82
  @running = false
81
83
  @on_update_callbacks = []
82
84
  end
83
85
 
86
+ # Keepalive cadence: flush pending acks every ACK_INTERVAL seconds, send a
87
+ # ping every PING_INTERVAL seconds.
88
+ ACK_INTERVAL = 5
89
+ PING_INTERVAL = 30
90
+
84
91
  def on_update(&block)
85
92
  @on_update_callbacks << block
86
93
  end
@@ -92,24 +99,7 @@ module MTProto
92
99
 
93
100
  begin
94
101
  Async do
95
- @running = true
96
- @receiver_task = Async do
97
- @connection.receive do |packet, error|
98
- if error
99
- warn "[MTProto] Packet read error: #{error.message}"
100
- next
101
- end
102
-
103
- decrypted = EncryptedMessage.decrypt(
104
- auth_key: @auth_key,
105
- encrypted_message_data: packet.data.pack('C*'),
106
- sender: :server
107
- )
108
-
109
- process_message(decrypted[:body])
110
- end
111
- end
112
-
102
+ start_receiving!
113
103
  yield self
114
104
  ensure
115
105
  disconnect!
@@ -119,10 +109,43 @@ module MTProto
119
109
  end
120
110
  end
121
111
 
112
+ # Start the receiver task without taking over the current Async reactor.
113
+ # Use when orchestrating multiple clients in a shared Async block — call
114
+ # from inside an Async do ... end. The caller is responsible for
115
+ # disconnect! at the end.
116
+ def start_receiving!
117
+ raise 'Auth key not set' unless auth_key?
118
+ raise 'Mainloop already running' if @running
119
+
120
+ @running = true
121
+ @ack_ids = []
122
+ @receiver_task = Async do
123
+ @connection.receive do |packet, error|
124
+ if error
125
+ warn "[MTProto] Packet read error: #{error.message}"
126
+ next
127
+ end
128
+
129
+ decrypted = EncryptedMessage.decrypt(
130
+ auth_key: @auth_key,
131
+ encrypted_message_data: packet.data.pack('C*'),
132
+ sender: :server
133
+ )
134
+
135
+ collect_ack(decrypted[:msg_id], decrypted[:seq_no], decrypted[:body])
136
+ process_message(decrypted[:body])
137
+ end
138
+ end
139
+ @keepalive_task = Async { keepalive_loop }
140
+ end
141
+
122
142
  def disconnect!
123
143
  @running = false
144
+ @keepalive_task&.stop
145
+ @keepalive_task = nil
124
146
  @receiver_task&.stop
125
147
  @receiver_task = nil
148
+ @ack_ids = []
126
149
 
127
150
  rpc.signal_all_error(Transport::ConnectionError.new('Client shutting down'))
128
151
 
@@ -222,6 +245,46 @@ module MTProto
222
245
 
223
246
  private
224
247
 
248
+ # Periodically acknowledge received messages and ping, so the server doesn't
249
+ # drop a long-lived connection. Ends when the connection dies (the wrapper
250
+ # reconnects) or when the task is stopped on disconnect!.
251
+ def keepalive_loop
252
+ elapsed = 0
253
+ loop do
254
+ sleep ACK_INTERVAL
255
+ flush_acks
256
+ elapsed += ACK_INTERVAL
257
+ next if elapsed < PING_INTERVAL
258
+
259
+ elapsed = 0
260
+ rpc.send_ping
261
+ end
262
+ rescue StandardError
263
+ nil
264
+ end
265
+
266
+ # Queue msg_ids of received content-related server messages for msgs_ack.
267
+ # A container carries its own inner msg_ids; content-related messages have an
268
+ # odd seq_no and must be acknowledged (acks/pongs are even and need none).
269
+ def collect_ack(msg_id, seq_no, body)
270
+ constructor = body[0, 4].unpack1('L<')
271
+ if constructor == TL::Constructors::MSG_CONTAINER
272
+ TL::MsgContainer.deserialize(body).messages.each do |msg|
273
+ collect_ack(msg[:msg_id], msg[:seqno], msg[:body])
274
+ end
275
+ elsif seq_no.odd?
276
+ @ack_ids << msg_id
277
+ end
278
+ end
279
+
280
+ def flush_acks
281
+ return if @ack_ids.empty?
282
+
283
+ ids = @ack_ids
284
+ @ack_ids = []
285
+ rpc.send_ack(ids)
286
+ end
287
+
225
288
  def process_message(response_body)
226
289
  constructor = response_body[0, 4].unpack1('L<')
227
290
 
@@ -192,6 +192,7 @@ module MTProto
192
192
  0x16115a96 => 'pageBlockRelatedArticles',
193
193
  0x161d9628 => 'topPeerCategoryChannels',
194
194
  0x16484857 => 'account.chatThemes',
195
+ 0x16605e3e => 'messageActionManagedBotCreated',
195
196
  0x1662af0b => 'messages.historyImport',
196
197
  0x167bd90b => 'payments.starGiftUpgradePreview',
197
198
  0x167fc0a1 => 'channels.toggleAutotranslation',
@@ -304,6 +305,7 @@ module MTProto
304
305
  0x21108ff7 => 'businessRecipients',
305
306
  0x211a1788 => 'webPageEmpty',
306
307
  0x21202222 => 'messages.getDialogUnreadMarks',
308
+ 0x213853a3 => 'bots.getAccessSettings',
307
309
  0x2144ca19 => 'rpc_error',
308
310
  0x21461b5d => 'privacyValueAllowBots',
309
311
  0x219c34e6 => 'phone.toggleGroupCallStartSubscription',
@@ -428,6 +430,8 @@ module MTProto
428
430
  0x31518e9b => 'messageActionRequestedPeer',
429
431
  0x315a4974 => 'users.usersSlice',
430
432
  0x316ce548 => 'account.setReactionsNotifySettings',
433
+ 0x31774388 => 'user',
434
+ 0x31813cd8 => 'bots.editAccessSettings',
431
435
  0x31bb5d52 => 'channelAdminLogEventActionChangeWallpaper',
432
436
  0x31bd492d => 'messages.messageReactionsList',
433
437
  0x31c1c44f => 'messages.getMessageReadParticipants',
@@ -525,6 +529,7 @@ module MTProto
525
529
  0x3c479971 => 'phone.declineConferenceCallInvite',
526
530
  0x3c4f04d8 => 'botCommandScopeUsers',
527
531
  0x3c5693e9 => 'inputTheme',
532
+ 0x3c60b621 => 'bots.exportedBotToken',
528
533
  0x3cbc93f8 => 'chatParticipants',
529
534
  0x3cc04740 => 'messages.deleteQuickReplyShortcut',
530
535
  0x3cd930b7 => 'channels.setEmojiStickers',
@@ -616,6 +621,7 @@ module MTProto
616
621
  0x47dd8079 => 'messageActionWebViewDataSentMe',
617
622
  0x481eadfa => 'emojiListNotModified',
618
623
  0x48222faf => 'inputGeoPoint',
624
+ 0x4880ed9a => 'updateManagedBot',
619
625
  0x48870999 => 'pageBlockFooter',
620
626
  0x4899484e => 'messages.votesList',
621
627
  0x48a30254 => 'replyInlineMarkup',
@@ -918,6 +924,7 @@ module MTProto
918
924
  0x6c47ac9f => 'langPackStringPluralized',
919
925
  0x6c50051c => 'messages.importChatInvite',
920
926
  0x6c5a5b37 => 'account.saveWallPaper',
927
+ 0x6c5cf2a7 => 'messages.sendBotRequestedPeer',
921
928
  0x6c6274fa => 'messageActionGiftPremium',
922
929
  0x6c750de1 => 'messages.sendQuickReplyMessages',
923
930
  0x6c8e1e06 => 'birthday',
@@ -1651,6 +1658,7 @@ module MTProto
1651
1658
  0xbc799737 => 'boolFalse',
1652
1659
  0xbcf22685 => 'phone.inviteConferenceCallParticipant',
1653
1660
  0xbd0415c4 => 'stories.togglePeerStoriesHidden',
1661
+ 0xbd0d99eb => 'bots.exportBotToken',
1654
1662
  0xbd17a14a => 'topPeerCategoryGroups',
1655
1663
  0xbd1efd3e => 'payments.getStarsGiveawayOptions',
1656
1664
  0xbd2a0840 => 'inputPeerChannelFromMessage',
@@ -1927,6 +1935,7 @@ module MTProto
1927
1935
  0xdcdf8607 => 'stats.getMegagroupStats',
1928
1936
  0xdd0c66f2 => 'starRefProgram',
1929
1937
  0xdd18782e => 'help.appConfig',
1938
+ 0xdd1fbf93 => 'bots.accessSettings',
1930
1939
  0xdd289f8e => 'invokeWithBusinessConnection',
1931
1940
  0xdd6a8f48 => 'sendMessageGamePlayAction',
1932
1941
  0xdde8a54c => 'inputPeerUser',
@@ -1999,6 +2008,7 @@ module MTProto
1999
2008
  0xe56dbf05 => 'dialogPeer',
2000
2009
  0xe581e4e9 => 'requirementToContactPremium',
2001
2010
  0xe58e95d2 => 'messages.deleteMessages',
2011
+ 0xe5b17f2b => 'bots.createBot',
2002
2012
  0xe5bbfe1a => 'inputMediaPhotoExternal',
2003
2013
  0xe5bdf8de => 'updateUserStatus',
2004
2014
  0xe5bfffcd => 'auth.exportAuthorization',
@@ -19,6 +19,7 @@ module MTProto
19
19
  PEER_CHAT = 0x36c6019a
20
20
  PEER_CHANNEL = 0xa2a5371e
21
21
  INPUT_PEER_EMPTY = 0x7f3b18ea
22
+ INPUT_PEER_SELF = 0x7da07ec9
22
23
  INPUT_PEER_USER = 0xdde8a54c
23
24
  INPUT_PEER_CHAT = 0x35a95cb9
24
25
  INPUT_PEER_CHANNEL = 0x27bcbbfc
@@ -71,9 +72,24 @@ module MTProto
71
72
  USERS_GET_USERS = 0x0d91a548
72
73
  USER = 0x020b1422
73
74
 
75
+ # Contacts
76
+ CONTACTS_GET_CONTACTS = 0x5dd69e12
77
+ CONTACTS_CONTACTS = 0xeae87e42
78
+ CONTACTS_CONTACTS_NOT_MODIFIED = 0xb74ba9d2
79
+ CONTACT = 0x145ade0b
80
+
74
81
  # Messages
75
82
  MESSAGES_GET_DIALOGS = 0xa0f4cb4f
76
83
  MESSAGES_GET_HISTORY = 0x4423e6c5
84
+ MESSAGES_SEND_MESSAGE = 0xfe05dc9a
85
+ MESSAGES_SEND_MEDIA = 0xac55d9c1
86
+ MESSAGES_EDIT_MESSAGE = 0xdfd14005
87
+ MESSAGES_DELETE_MESSAGES = 0xe58e95d2
88
+
89
+ # Upload + media
90
+ UPLOAD_SAVE_FILE_PART = 0xb304a621
91
+ INPUT_FILE = 0xf52ff27f
92
+ INPUT_MEDIA_UPLOADED_PHOTO = 0x1e287d04
77
93
  MESSAGES_DIALOGS = 0x15ba6c40
78
94
  MESSAGES_DIALOGS_SLICE = 0x71e094f3
79
95
  MESSAGE = 0x9815cec8
@@ -87,6 +103,16 @@ module MTProto
87
103
  UPDATE_NEW_MESSAGE = 0x1f2b0afd
88
104
  UPDATE_SHORT = 0x78d4dec1
89
105
  UPDATE_SHORT_MESSAGE = 0x313bc7f8
106
+ UPDATE_SHORT_SENT_MESSAGE = 0x9015e101
107
+
108
+ # Channel updates
109
+ INPUT_CHANNEL = 0xf35aec28
110
+ INPUT_CHANNEL_EMPTY = 0xee8c1e86
111
+ CHANNEL_MESSAGES_FILTER_EMPTY = 0x94d42ee7
112
+ UPDATES_GET_CHANNEL_DIFFERENCE = 0x03173d78
113
+ UPDATES_CHANNEL_DIFFERENCE = 0x2064674e
114
+ UPDATES_CHANNEL_DIFFERENCE_EMPTY = 0x3e11affb
115
+ UPDATES_CHANNEL_DIFFERENCE_TOO_LONG = 0xa4bcc6fe
90
116
  UPDATES_GET_STATE = 0xedd4882a
91
117
  UPDATES_STATE = 0xa56c2a3e
92
118
  UPDATES_GET_DIFFERENCE = 0x19c2f763
@@ -12,7 +12,7 @@ module MTProto
12
12
  @payload = payload
13
13
  end
14
14
 
15
- def self.parse(message)
15
+ def self.deserialize(message)
16
16
  body = message.body
17
17
  constructor_id = b_u32(body[0, 4])
18
18
  constructor = ConstructorNames::NAMES.fetch(constructor_id)
@@ -12,7 +12,7 @@ module MTProto
12
12
  @srp_id = srp_id
13
13
  end
14
14
 
15
- def self.parse(data)
15
+ def self.deserialize(data)
16
16
  constructor = data[0, 4].unpack1('L<')
17
17
  raise UnexpectedConstructorError, constructor unless constructor == Constructors::ACCOUNT_PASSWORD
18
18