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.
- checksums.yaml +4 -4
- data/FUTURE.md +9 -0
- data/docs/test_architecture_level1.md +237 -0
- data/lib/mtproto/auth_key_generator.rb +5 -5
- data/lib/mtproto/client/api/get_contacts.rb +14 -0
- data/lib/mtproto/client/api/send_message.rb +15 -0
- data/lib/mtproto/client/api.rb +2 -0
- data/lib/mtproto/client/rpc/response.rb +1 -1
- data/lib/mtproto/client/rpc.rb +35 -2
- data/lib/mtproto/client.rb +81 -18
- data/lib/mtproto/tl/constructor_names.rb +10 -0
- data/lib/mtproto/tl/constructors.rb +26 -0
- data/lib/mtproto/tl/object.rb +1 -1
- data/lib/mtproto/tl/objects/account_password.rb +1 -1
- data/lib/mtproto/tl/objects/authorization.rb +1 -1
- data/lib/mtproto/tl/objects/channels_create_channel.rb +48 -0
- data/lib/mtproto/tl/objects/channels_delete_messages.rb +32 -0
- data/lib/mtproto/tl/objects/channels_delete_participant_history.rb +34 -0
- data/lib/mtproto/tl/objects/channels_edit_banned.rb +54 -0
- data/lib/mtproto/tl/objects/channels_get_messages.rb +33 -0
- data/lib/mtproto/tl/objects/channels_get_participant.rb +36 -0
- data/lib/mtproto/tl/objects/channels_join_channel.rb +22 -0
- data/lib/mtproto/tl/objects/channels_leave_channel.rb +22 -0
- data/lib/mtproto/tl/objects/channels_report_spam.rb +37 -0
- data/lib/mtproto/tl/objects/check_password.rb +1 -1
- data/lib/mtproto/tl/objects/client_dh_inner_data.rb +1 -1
- data/lib/mtproto/tl/objects/contacts.rb +156 -0
- data/lib/mtproto/tl/objects/create_bot.rb +54 -0
- data/lib/mtproto/tl/objects/delete_messages.rb +25 -0
- data/lib/mtproto/tl/objects/dh_gen_response.rb +1 -1
- data/lib/mtproto/tl/objects/dialogs.rb +18 -4
- data/lib/mtproto/tl/objects/edit_access_settings.rb +46 -0
- data/lib/mtproto/tl/objects/edit_message.rb +58 -0
- data/lib/mtproto/tl/objects/export_bot_token.rb +32 -0
- data/lib/mtproto/tl/objects/export_login_token.rb +1 -1
- data/lib/mtproto/tl/objects/exported_bot_token.rb +27 -0
- data/lib/mtproto/tl/objects/forward_messages.rb +55 -0
- data/lib/mtproto/tl/objects/get_access_settings.rb +28 -0
- data/lib/mtproto/tl/objects/get_channel_difference.rb +41 -0
- data/lib/mtproto/tl/objects/get_config.rb +1 -1
- data/lib/mtproto/tl/objects/get_contacts.rb +17 -0
- data/lib/mtproto/tl/objects/get_dialogs.rb +1 -1
- data/lib/mtproto/tl/objects/get_difference.rb +1 -1
- data/lib/mtproto/tl/objects/get_file.rb +54 -0
- data/lib/mtproto/tl/objects/get_full_channel.rb +29 -0
- data/lib/mtproto/tl/objects/get_full_user.rb +28 -0
- data/lib/mtproto/tl/objects/get_history.rb +1 -1
- data/lib/mtproto/tl/objects/get_messages_reactions.rb +39 -0
- data/lib/mtproto/tl/objects/get_password.rb +1 -1
- data/lib/mtproto/tl/objects/get_state.rb +1 -1
- data/lib/mtproto/tl/objects/get_users.rb +1 -1
- data/lib/mtproto/tl/objects/help_config.rb +1 -1
- data/lib/mtproto/tl/objects/import_bot_authorization.rb +44 -0
- data/lib/mtproto/tl/objects/import_login_token.rb +1 -1
- data/lib/mtproto/tl/objects/init_connection.rb +2 -2
- data/lib/mtproto/tl/objects/input_keyboard_button_request_peer.rb +54 -0
- data/lib/mtproto/tl/objects/invite_to_channel.rb +35 -0
- data/lib/mtproto/tl/objects/invoke_with_layer.rb +2 -2
- data/lib/mtproto/tl/objects/login_token.rb +2 -2
- data/lib/mtproto/tl/objects/messages.rb +1 -1
- data/lib/mtproto/tl/objects/messages_get_messages.rb +26 -0
- data/lib/mtproto/tl/objects/pq_inner_data.rb +1 -1
- data/lib/mtproto/tl/objects/raw_response.rb +29 -0
- data/lib/mtproto/tl/objects/reply_keyboard_markup.rb +35 -0
- data/lib/mtproto/tl/objects/req_dh_params.rb +1 -1
- data/lib/mtproto/tl/objects/req_pq_multi.rb +1 -1
- data/lib/mtproto/tl/objects/request_peer_type_create_bot.rb +47 -0
- data/lib/mtproto/tl/objects/res_pq.rb +1 -1
- data/lib/mtproto/tl/objects/resolve_username.rb +40 -0
- data/lib/mtproto/tl/objects/save_file_part.rb +40 -0
- data/lib/mtproto/tl/objects/send_bot_requested_peer.rb +51 -0
- data/lib/mtproto/tl/objects/send_code.rb +1 -1
- data/lib/mtproto/tl/objects/send_media.rb +96 -0
- data/lib/mtproto/tl/objects/send_message.rb +73 -0
- data/lib/mtproto/tl/objects/send_reaction.rb +68 -0
- data/lib/mtproto/tl/objects/sent_code.rb +1 -1
- data/lib/mtproto/tl/objects/server_dh_inner_data.rb +1 -1
- data/lib/mtproto/tl/objects/server_dh_params.rb +1 -1
- data/lib/mtproto/tl/objects/set_bot_commands.rb +50 -0
- data/lib/mtproto/tl/objects/set_bot_info.rb +64 -0
- data/lib/mtproto/tl/objects/set_client_dh_params.rb +1 -1
- data/lib/mtproto/tl/objects/sign_in.rb +1 -1
- data/lib/mtproto/tl/objects/update.rb +1 -1
- data/lib/mtproto/tl/objects/update_short.rb +2 -2
- data/lib/mtproto/tl/objects/update_short_message.rb +1 -1
- data/lib/mtproto/tl/objects/update_short_sent_message.rb +36 -0
- data/lib/mtproto/tl/objects/update_username.rb +39 -0
- data/lib/mtproto/tl/objects/updates_difference.rb +93 -14
- data/lib/mtproto/tl/objects/updates_state.rb +1 -1
- data/lib/mtproto/tl/objects/upload_profile_photo.rb +65 -0
- data/lib/mtproto/tl/objects/users.rb +1 -1
- data/lib/mtproto/version.rb +1 -1
- metadata +49 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: a52707d10eeb5e41cf6e1dcb01374d92092031ceba5f5f92dde31ce4965d174c
|
|
4
|
+
data.tar.gz: ddf47dbcf9612d686806ec83fd16470de46595300b76c733fc8af37be851f1ff
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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
|
data/lib/mtproto/client/api.rb
CHANGED
|
@@ -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
|
data/lib/mtproto/client/rpc.rb
CHANGED
|
@@ -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?(:
|
|
73
|
-
request.
|
|
105
|
+
if request.respond_to?(:serialize)
|
|
106
|
+
request.serialize.pack('C*')
|
|
74
107
|
else
|
|
75
108
|
request
|
|
76
109
|
end
|
data/lib/mtproto/client.rb
CHANGED
|
@@ -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
|
-
|
|
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
|
data/lib/mtproto/tl/object.rb
CHANGED