mtproto 0.0.13 → 0.0.14
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 +2 -2
- data/lib/mtproto/client.rb +28 -18
- 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/delete_messages.rb +25 -0
- data/lib/mtproto/tl/objects/dh_gen_response.rb +1 -1
- data/lib/mtproto/tl/objects/dialogs.rb +8 -4
- data/lib/mtproto/tl/objects/edit_message.rb +58 -0
- data/lib/mtproto/tl/objects/export_login_token.rb +1 -1
- data/lib/mtproto/tl/objects/forward_messages.rb +49 -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/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/pq_inner_data.rb +1 -1
- data/lib/mtproto/tl/objects/raw_response.rb +29 -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/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_code.rb +1 -1
- data/lib/mtproto/tl/objects/send_media.rb +85 -0
- data/lib/mtproto/tl/objects/send_message.rb +70 -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_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 +2 -2
- data/lib/mtproto/tl/objects/updates_state.rb +1 -1
- data/lib/mtproto/tl/objects/users.rb +1 -1
- data/lib/mtproto/version.rb +1 -1
- metadata +36 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: e4d643a3e49ecb3e3d2744952aa42edb40237c2307a0d7d316c1ed350443bab7
|
|
4
|
+
data.tar.gz: f3f21cb5f49140c69d1d8dd8d34b7b12f2cf0aaaae48efedc9bce6b954cdf1ae
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: ba8f0adc18f691fa85b9722dc994ac1784a30d9465f972f9992da6a95027722655467952c94b31877531c08fb5fa45effef9fc77558ff6b7e2a8cccc69d9310e
|
|
7
|
+
data.tar.gz: a95133f8932df90e61b39de4cd6342b6d5f5b790bd803eacb3cde964b6d2fbd1e6983fc6f2bfb0543c25568287225e094f10e95bdc52a68fbbc5ff9ebbdc8484
|
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
data/lib/mtproto/client.rb
CHANGED
|
@@ -92,24 +92,7 @@ module MTProto
|
|
|
92
92
|
|
|
93
93
|
begin
|
|
94
94
|
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
|
-
|
|
95
|
+
start_receiving!
|
|
113
96
|
yield self
|
|
114
97
|
ensure
|
|
115
98
|
disconnect!
|
|
@@ -119,6 +102,33 @@ module MTProto
|
|
|
119
102
|
end
|
|
120
103
|
end
|
|
121
104
|
|
|
105
|
+
# Start the receiver task without taking over the current Async reactor.
|
|
106
|
+
# Use when orchestrating multiple clients in a shared Async block — call
|
|
107
|
+
# from inside an Async do ... end. The caller is responsible for
|
|
108
|
+
# disconnect! at the end.
|
|
109
|
+
def start_receiving!
|
|
110
|
+
raise 'Auth key not set' unless auth_key?
|
|
111
|
+
raise 'Mainloop already running' if @running
|
|
112
|
+
|
|
113
|
+
@running = true
|
|
114
|
+
@receiver_task = Async do
|
|
115
|
+
@connection.receive do |packet, error|
|
|
116
|
+
if error
|
|
117
|
+
warn "[MTProto] Packet read error: #{error.message}"
|
|
118
|
+
next
|
|
119
|
+
end
|
|
120
|
+
|
|
121
|
+
decrypted = EncryptedMessage.decrypt(
|
|
122
|
+
auth_key: @auth_key,
|
|
123
|
+
encrypted_message_data: packet.data.pack('C*'),
|
|
124
|
+
sender: :server
|
|
125
|
+
)
|
|
126
|
+
|
|
127
|
+
process_message(decrypted[:body])
|
|
128
|
+
end
|
|
129
|
+
end
|
|
130
|
+
end
|
|
131
|
+
|
|
122
132
|
def disconnect!
|
|
123
133
|
@running = false
|
|
124
134
|
@receiver_task&.stop
|
|
@@ -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
|
@@ -16,7 +16,7 @@ module MTProto
|
|
|
16
16
|
!@sign_up_required && @user_id
|
|
17
17
|
end
|
|
18
18
|
|
|
19
|
-
def self.
|
|
19
|
+
def self.deserialize(data)
|
|
20
20
|
constructor = data[0, 4].unpack1('L<')
|
|
21
21
|
|
|
22
22
|
return new(sign_up_required: true) if constructor == Constructors::AUTH_AUTHORIZATION_SIGN_UP_REQUIRED
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module MTProto
|
|
4
|
+
module TL
|
|
5
|
+
# channels.createChannel — create a broadcast channel or, with the megagroup
|
|
6
|
+
# flag, a supergroup. Returns Updates carrying the new channel.
|
|
7
|
+
class ChannelsCreateChannel
|
|
8
|
+
include Binary
|
|
9
|
+
|
|
10
|
+
CONSTRUCTOR = 0x91006707
|
|
11
|
+
|
|
12
|
+
def initialize(title:, about: '', megagroup: true, broadcast: false)
|
|
13
|
+
@title = title
|
|
14
|
+
@about = about
|
|
15
|
+
@megagroup = megagroup
|
|
16
|
+
@broadcast = broadcast
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
def serialize
|
|
20
|
+
flags = 0
|
|
21
|
+
flags |= (1 << 0) if @broadcast
|
|
22
|
+
flags |= (1 << 1) if @megagroup
|
|
23
|
+
|
|
24
|
+
result = u32_b(CONSTRUCTOR)
|
|
25
|
+
result += u32_b(flags)
|
|
26
|
+
result += serialize_tl_string(@title)
|
|
27
|
+
result += serialize_tl_string(@about)
|
|
28
|
+
result
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
private
|
|
32
|
+
|
|
33
|
+
def serialize_tl_string(str)
|
|
34
|
+
bytes = str.to_s.b.bytes
|
|
35
|
+
length = bytes.length
|
|
36
|
+
if length <= 253
|
|
37
|
+
[length] + bytes + padding(length + 1)
|
|
38
|
+
else
|
|
39
|
+
[254] + u32_b(length)[0, 3] + bytes + padding(length + 4)
|
|
40
|
+
end
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
def padding(current_length)
|
|
44
|
+
[0] * ((4 - (current_length % 4)) % 4)
|
|
45
|
+
end
|
|
46
|
+
end
|
|
47
|
+
end
|
|
48
|
+
end
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module MTProto
|
|
4
|
+
module TL
|
|
5
|
+
class ChannelsDeleteMessages
|
|
6
|
+
include Binary
|
|
7
|
+
|
|
8
|
+
CONSTRUCTOR = 0x84c1fd4e
|
|
9
|
+
|
|
10
|
+
def initialize(channel:, ids:)
|
|
11
|
+
@channel = channel
|
|
12
|
+
@ids = ids
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
def serialize
|
|
16
|
+
result = u32_b(CONSTRUCTOR)
|
|
17
|
+
result += serialize_input_channel
|
|
18
|
+
result += u32_b(Constructors::VECTOR) + u32_b(@ids.length)
|
|
19
|
+
@ids.each { |id| result += u32_b(id) }
|
|
20
|
+
result
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
private
|
|
24
|
+
|
|
25
|
+
def serialize_input_channel
|
|
26
|
+
u32_b(Constructors::INPUT_CHANNEL) +
|
|
27
|
+
u64_b(@channel[:id]) +
|
|
28
|
+
u64_b(@channel[:access_hash])
|
|
29
|
+
end
|
|
30
|
+
end
|
|
31
|
+
end
|
|
32
|
+
end
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module MTProto
|
|
4
|
+
module TL
|
|
5
|
+
class ChannelsDeleteParticipantHistory
|
|
6
|
+
include Binary
|
|
7
|
+
|
|
8
|
+
CONSTRUCTOR = 0x367544db
|
|
9
|
+
|
|
10
|
+
def initialize(channel:, participant:)
|
|
11
|
+
@channel = channel
|
|
12
|
+
@participant = participant
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
def serialize
|
|
16
|
+
result = u32_b(CONSTRUCTOR)
|
|
17
|
+
result += u32_b(Constructors::INPUT_CHANNEL) + u64_b(@channel[:id]) + u64_b(@channel[:access_hash])
|
|
18
|
+
result += serialize_input_peer(@participant)
|
|
19
|
+
result
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
private
|
|
23
|
+
|
|
24
|
+
def serialize_input_peer(peer)
|
|
25
|
+
case peer[:type]
|
|
26
|
+
when :user
|
|
27
|
+
u32_b(Constructors::INPUT_PEER_USER) + u64_b(peer[:id]) + u64_b(peer[:access_hash] || 0)
|
|
28
|
+
else
|
|
29
|
+
raise "Unsupported participant peer type: #{peer[:type]}"
|
|
30
|
+
end
|
|
31
|
+
end
|
|
32
|
+
end
|
|
33
|
+
end
|
|
34
|
+
end
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module MTProto
|
|
4
|
+
module TL
|
|
5
|
+
class ChannelsEditBanned
|
|
6
|
+
include Binary
|
|
7
|
+
|
|
8
|
+
CONSTRUCTOR = 0x96e6cd81
|
|
9
|
+
CHAT_BANNED_RIGHTS = 0x9f120418
|
|
10
|
+
|
|
11
|
+
def initialize(channel:, participant:, view_messages: false, send_messages: false, until_date: 0)
|
|
12
|
+
@channel = channel
|
|
13
|
+
@participant = participant
|
|
14
|
+
@view_messages = view_messages
|
|
15
|
+
@send_messages = send_messages
|
|
16
|
+
@until_date = until_date
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
def serialize
|
|
20
|
+
result = u32_b(CONSTRUCTOR)
|
|
21
|
+
result += serialize_input_channel
|
|
22
|
+
result += serialize_input_peer(@participant)
|
|
23
|
+
result += serialize_banned_rights
|
|
24
|
+
result
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
private
|
|
28
|
+
|
|
29
|
+
def serialize_banned_rights
|
|
30
|
+
flags = 0
|
|
31
|
+
flags |= (1 << 0) if @view_messages
|
|
32
|
+
flags |= (1 << 1) if @send_messages
|
|
33
|
+
u32_b(CHAT_BANNED_RIGHTS) + u32_b(flags) + u32_b(@until_date)
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
def serialize_input_channel
|
|
37
|
+
u32_b(Constructors::INPUT_CHANNEL) +
|
|
38
|
+
u64_b(@channel[:id]) +
|
|
39
|
+
u64_b(@channel[:access_hash])
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
def serialize_input_peer(peer)
|
|
43
|
+
case peer[:type]
|
|
44
|
+
when :user
|
|
45
|
+
u32_b(Constructors::INPUT_PEER_USER) + u64_b(peer[:id]) + u64_b(peer[:access_hash] || 0)
|
|
46
|
+
when :channel
|
|
47
|
+
u32_b(Constructors::INPUT_PEER_CHANNEL) + u64_b(peer[:id]) + u64_b(peer[:access_hash] || 0)
|
|
48
|
+
else
|
|
49
|
+
raise "Unsupported participant peer type: #{peer[:type]}"
|
|
50
|
+
end
|
|
51
|
+
end
|
|
52
|
+
end
|
|
53
|
+
end
|
|
54
|
+
end
|